[BACK]Return to mklineparser_test.go CVS log [TXT][DIR] Up to [cvs.NetBSD.org] / pkgsrc / pkgtools / pkglint / files

File: [cvs.NetBSD.org] / pkgsrc / pkgtools / pkglint / files / mklineparser_test.go (download)

Revision 1.6, Sun Dec 8 00:06:38 2019 UTC (2 months, 2 weeks ago) by rillig
Branch: MAIN
Changes since 1.5: +2 -2 lines

pkgtools/pkglint: update to 19.3.14

Changes since 19.3.13:

When pkglint suggests to replace !empty(VARNAME:Mfixed) with ${VARNAME}
== fixed, the exact suggested expression is now part of the diagnostic.
The check and the autofix have been improved. They now apply only to the
last modifier in the whole chain, everything else was a bug in pkglint.

Pkglint now knows the scope of variables better than before. It knows
the difference between variables from <sys.mk> like MACHINE_ARCH, which
are always in scope, and those from mk/defaults/mk.conf, which only come
into scope later, after bsd.prefs.mk has been included. It warns when
variables are used too early, for example in .if conditions.

The pathnames in ALTERNATIVES files are now checked for absolute
pathnames. This mistake doesn't happen in practice, but the code for
converting the different path types internally made it necessary to add
these checks. At least this prevents typos.

The special check for obsolete licenses has been removed since their
license files have been removed and that is checked as well.

Variables named *_AWK may be appended to.

The variables _PKG_SILENT and _PKG_DEBUG are no longer deprecated, they
are obsolete now. They are not used in main pkgsrc and pkgsrc-wip
anymore.

When a package sets a default value for a user-settable variable (which
is something that should not happen anyway), it should .include
bsd.prefs.mk before, in order to not accidentally overwrite the
user-specified value.

Variable modifiers of the form :from=to are now parsed like in bmake.
They are greedy and eat up any following colons as well. This means that
${VAR:.c=.o:Q} replaces source.c with source.o:Q, instead of quoting it.
Pkglint now warns about such cases.

The handling of relative paths in diagnostics is now consistent. All
paths that are part of a diagnostic are relative to the line that issues
the diagnostic.

Fatal errors are no longer suppressed in --autofix mode.

Plus lots of refactoring, to prevent accidental mixing of incompatible
relative paths.

package pkglint

import "gopkg.in/check.v1"

// Exotic code examples from the pkgsrc infrastructure.
// Hopefully, pkgsrc packages don't need such complicated code.
// Still, pkglint needs to parse them correctly, or it would not
// be able to parse and check the infrastructure files as well.
//
// See Pkgsrc.loadUntypedVars.
func (s *Suite) Test_MkLineParser_Parse__infrastructure(c *check.C) {
	t := s.Init(c)

	mklines := t.NewMkLines("infra.mk",
		MkCvsID,
		"         USE_BUILTIN.${_pkg_:S/^-//}:=no",
		".error \"Something went wrong\"",
		".export WRKDIR",
		".export",
		".unexport-env WRKDIR",
		"",
		".ifmake target1",    // Luckily, this is not used in the wild.
		".elifnmake target2", // Neither is this.
		".endif")

	t.CheckEquals(mklines.mklines[1].Varcanon(), "USE_BUILTIN.*")
	t.CheckEquals(mklines.mklines[2].Directive(), "error")
	t.CheckEquals(mklines.mklines[3].Directive(), "export")

	t.CheckOutputLines(
		"WARN: infra.mk:2: Makefile lines should not start with space characters.",
		"ERROR: infra.mk:8: Unknown Makefile line format: \".ifmake target1\".",
		"ERROR: infra.mk:9: Unknown Makefile line format: \".elifnmake target2\".")

	mklines.Check()

	t.CheckOutputLines(
		"WARN: infra.mk:2: USE_BUILTIN.${_pkg_:S/^-//} is defined but not used.",
		"WARN: infra.mk:2: _pkg_ is used but not defined.",
		"ERROR: infra.mk:5: \".export\" requires arguments.",
		"NOTE: infra.mk:2: This variable value should be aligned to column 41.",
		"ERROR: infra.mk:10: Unmatched .endif.")
}

// In variable assignments, a plain '#' introduces a line comment, unless
// it is escaped by a backslash. In shell commands, on the other hand, it
// is interpreted literally.
func (s *Suite) Test_MkLineParser_Parse__comment_or_not(c *check.C) {
	t := s.Init(c)

	mklineVarassignEscaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,\\#,hash,g'")

	t.CheckEquals(mklineVarassignEscaped.Varname(), "SED_CMD")
	t.CheckEquals(mklineVarassignEscaped.Value(), "'s,#,hash,g'")

	mklineCommandEscaped := t.NewMkLine("filename.mk", 1, "\tsed -e 's,\\#,hash,g'")

	t.CheckEquals(mklineCommandEscaped.ShellCommand(), "sed -e 's,\\#,hash,g'")

	// From shells/zsh/Makefile.common, rev. 1.78
	mklineCommandUnescaped := t.NewMkLine("filename.mk", 1, "\t# $ sha1 patches/patch-ac")

	t.CheckEquals(mklineCommandUnescaped.IsComment(), true)
	t.CheckEquals(mklineCommandUnescaped.Comment(), " $ sha1 patches/patch-ac")
	t.CheckOutputEmpty() // No warning about parsing the lonely dollar sign.

	mklineVarassignUnescaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,#,hash,'")

	t.CheckEquals(mklineVarassignUnescaped.Value(), "'s,")
	t.CheckOutputLines(
		"WARN: filename.mk:1: The # character starts a Makefile comment.")
}

func (s *Suite) Test_MkLineParser_Parse__commented_lines(c *check.C) {
	t := s.Init(c)

	test := func(text string) {
		mkline := t.NewMkLines("filename.mk", text).mklines[0]
		t.CheckEquals(mkline.HasComment(), true)
		t.CheckEquals(mkline.Comment(), " the comment")
	}

	test("VAR=value # the comment")
	test("# the comment")
	test(".if 0 # the comment")
	test(".include \"other.mk\" # the comment")
	test(".include <other.mk> # the comment")
	test("target: source # the comment")
	test("\t\t# the comment")
}

func (s *Suite) Test_MkLineParser_parseVarassign(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		"VARNAME.param?=value # varassign comment")

	t.CheckEquals(mkline.IsVarassign(), true)
	t.CheckEquals(mkline.Varname(), "VARNAME.param")
	t.CheckEquals(mkline.Varcanon(), "VARNAME.*")
	t.CheckEquals(mkline.Varparam(), "param")
	t.CheckEquals(mkline.Op(), opAssignDefault)
	t.CheckEquals(mkline.Value(), "value")
	t.CheckEquals(mkline.Comment(), " varassign comment")
}

func (s *Suite) Test_MkLineParser_parseVarassign__empty_multiline(c *check.C) {
	t := s.Init(c)

	mklines := t.NewMkLines("test.mk",
		"VAR=\t\\",
		"\t\\",
		"\t\\",
		"\t# nothing",
		"",
		"VAR=\t1\\",
		"\t\\",
		"\t\\",
		"\t# a single letter")

	// Bmake and pkglint agree that the variable value is an empty string.
	// They don't agree on the exact whitespace in the line, though,
	// but this doesn't matter in practice. To see the difference, run:
	//  bmake -dA 2>&1 | grep 'ParseReadLine.*VAR'
	// See devel/bmake/files/parse.c:/non-comment, non-blank line/
	t.CheckEquals(mklines.mklines[0].Text, "VAR=   # nothing")
	t.CheckEquals(mklines.mklines[2].Text, "VAR=\t1   # a single letter")

	mkline := mklines.mklines[0]
	t.CheckEquals(mkline.IsVarassign(), true)
	t.CheckEquals(mkline.Varname(), "VAR")
	t.CheckEquals(mkline.Op(), opAssign)
	t.CheckEquals(mkline.Value(), "")
	t.CheckEquals(mkline.Comment(), " nothing")
}

func (s *Suite) Test_MkLineParser_parseVarassign__leading_space(c *check.C) {
	t := s.Init(c)

	_ = t.NewMkLine("rubyversion.mk", 427, " _RUBYVER=\t2.15")
	_ = t.NewMkLine("bsd.buildlink3.mk", 132, "   ok:=yes")

	// In mk/buildlink3/bsd.buildlink3.mk, the leading space is really helpful,
	// therefore no warnings for that file.
	t.CheckOutputLines(
		"WARN: rubyversion.mk:427: Makefile lines should not start with space characters.")
}

func (s *Suite) Test_MkLineParser_parseVarassign__space_around_operator(c *check.C) {
	t := s.Init(c)

	t.SetUpCommandLine("--show-autofix", "--source")
	t.NewMkLine("test.mk", 101,
		"pkgbase = package")

	t.CheckOutputLines(
		"NOTE: test.mk:101: Unnecessary space after variable name \"pkgbase\".",
		"AUTOFIX: test.mk:101: Replacing \"pkgbase =\" with \"pkgbase=\".",
		"-\tpkgbase = package",
		"+\tpkgbase= package")
}

func (s *Suite) Test_MkLineParser_parseVarassign__autofix_space_after_varname(c *check.C) {
	t := s.Init(c)

	filename := t.CreateFileLines("Makefile",
		MkCvsID,
		"VARNAME +=\t${VARNAME}",
		"VARNAME+ =\t${VARNAME+}",
		"VARNAME+ +=\t${VARNAME+}",
		"VARNAME+ ?=\t${VARNAME}",
		"pkgbase := pkglint")

	CheckFileMk(filename)

	t.CheckOutputLines(
		"NOTE: ~/Makefile:2: Unnecessary space after variable name \"VARNAME\".",

		// The assignment operators other than = and += cannot lead to ambiguities.
		"NOTE: ~/Makefile:5: Unnecessary space after variable name \"VARNAME+\".",

		"WARN: ~/Makefile:5: "+
			"Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")

	t.SetUpCommandLine("-Wall", "--autofix")

	CheckFileMk(filename)

	t.CheckOutputLines(
		"AUTOFIX: ~/Makefile:2: Replacing \"VARNAME +=\" with \"VARNAME+=\".",
		"AUTOFIX: ~/Makefile:5: Replacing \"VARNAME+ ?=\" with \"VARNAME+?=\".")
	t.CheckFileLines("Makefile",
		MkCvsID+"",
		"VARNAME+=\t${VARNAME}",
		"VARNAME+ =\t${VARNAME+}",
		"VARNAME+ +=\t${VARNAME+}",
		"VARNAME+?=\t${VARNAME}",
		"pkgbase := pkglint")
}

func (s *Suite) Test_MkLineParser_parseVarassign__append(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		"VARNAME+=value")

	t.CheckEquals(mkline.IsVarassign(), true)
	t.CheckEquals(mkline.Varname(), "VARNAME")
	t.CheckEquals(mkline.Varcanon(), "VARNAME")
	t.CheckEquals(mkline.Varparam(), "")
}

func (s *Suite) Test_MkLineParser_parseVarassign__varname_with_hash(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("Makefile", 123, "VARNAME.#=\tvalue")

	// Parse error because the # starts a comment.
	t.CheckEquals(mkline.IsVarassign(), false)

	mkline2 := t.NewMkLine("Makefile", 124, "VARNAME.\\#=\tvalue")

	t.CheckEquals(mkline2.IsVarassign(), true)
	t.CheckEquals(mkline2.Varname(), "VARNAME.#")

	t.CheckOutputLines(
		"ERROR: Makefile:123: Unknown Makefile line format: \"VARNAME.#=\\tvalue\".")
}

// Ensures that pkglint parses escaped # characters in the same way as bmake.
//
// To check that bmake parses them the same, set a breakpoint after the t.NewMkLines
// and look in t.tmpdir for the location of the file. Then run bmake with that file.
func (s *Suite) Test_MkLineParser_parseVarassign__escaped_hash_in_value(c *check.C) {
	t := s.Init(c)

	mklines := t.SetUpFileMkLines("Makefile",
		"VAR0=\tvalue#",
		"VAR1=\tvalue\\#",
		"VAR2=\tvalue\\\\#",
		"VAR3=\tvalue\\\\\\#",
		"VAR4=\tvalue\\\\\\\\#",
		"",
		"all:",
		".for var in VAR0 VAR1 VAR2 VAR3 VAR4",
		"\t@printf '%s\\n' ${${var}}''",
		".endfor")
	parsed := mklines.mklines

	t.CheckEquals(parsed[0].Value(), "value")
	t.CheckEquals(parsed[1].Value(), "value#")
	t.CheckEquals(parsed[2].Value(), "value\\\\")
	t.CheckEquals(parsed[3].Value(), "value\\\\#")
	t.CheckEquals(parsed[4].Value(), "value\\\\\\\\")

	t.CheckOutputLines(
		"WARN: ~/Makefile:1: The # character starts a Makefile comment.",
		"WARN: ~/Makefile:3: The # character starts a Makefile comment.",
		"WARN: ~/Makefile:5: The # character starts a Makefile comment.")
}

func (s *Suite) Test_MkLineParser_MatchVarassign(c *check.C) {
	t := s.Init(c)

	testLine := func(line *Line, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string, diagnostics ...string) {
		text := line.Text

		parser := NewMkLineParser()
		splitResult := parser.split(nil, text, true)
		m, actual := parser.MatchVarassign(line, text, &splitResult)

		assert(m)
		expected := mkLineAssign{
			commented:         commented,
			varname:           varname,
			varcanon:          varnameCanon(varname),
			varparam:          varnameParam(varname),
			spaceAfterVarname: spaceAfterVarname,
			op:                NewMkOperator(op),
			valueAlign:        align,
			value:             value,
			valueMk:           nil,
			valueMkRest:       "",
			fields:            nil,
		}
		t.CheckDeepEquals(*actual, expected)
		t.CheckEquals(splitResult.spaceBeforeComment, spaceAfterValue)
		t.CheckEquals(splitResult.hasComment, comment != "")
		t.CheckEquals(condStr(splitResult.hasComment, "#", "")+splitResult.comment, comment)
		t.CheckOutput(diagnostics)
	}

	test := func(text string, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string, diagnostics ...string) {
		line := t.NewLine("filename.mk", 123, text)
		testLine(line, commented, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment, diagnostics...)
	}

	testInvalid := func(text string, diagnostics ...string) {
		line := t.NewLine("filename.mk", 123, text)
		parser := NewMkLineParser()
		splitResult := parser.split(nil, text, true)
		m, _ := parser.MatchVarassign(line, text, &splitResult)
		if m {
			c.Errorf("Text %q matches variable assignment but shouldn't.", text)
		}
		t.CheckOutput(diagnostics)
	}

	lines := func(text ...string) *Line {
		mklines := t.NewMkLines("filename.mk",
			text...)
		return mklines.mklines[0].Line
	}

	test("C++=c11", false, "C+", "", "+=", "C++=", "c11", "", "")
	test("V=v", false, "V", "", "=", "V=", "v", "", "")
	test("VAR=#comment", false, "VAR", "", "=", "VAR=", "", "", "#comment")
	test("VAR=\\#comment", false, "VAR", "", "=", "VAR=", "#comment", "", "")
	test("VAR=\\\\\\##comment", false, "VAR", "", "=", "VAR=", "\\\\#", "", "#comment")
	test("VAR=\\", false, "VAR", "", "=", "VAR=", "\\", "", "")
	test("VAR += value", false, "VAR", " ", "+=", "VAR += ", "value", "", "")
	test(" VAR=value", false, "VAR", "", "=", " VAR=", "value", "", "")
	test("VAR=value #comment", false, "VAR", "", "=", "VAR=", "value", " ", "#comment")
	test("NFILES=${FILES:[#]}", false, "NFILES", "", "=", "NFILES=", "${FILES:[#]}", "", "")

	// To humans, the base variable name seems to be SITES_, being parameterized
	// with distfile-1.0.tar.gz. For pkglint though, the base variable name is
	// SITES_distfile-1.
	test("SITES_distfile-1.0.tar.gz=https://example.org/",
		false,
		"SITES_distfile-1.0.tar.gz",
		"",
		"=",
		"SITES_distfile-1.0.tar.gz=",
		"https://example.org/",
		"",
		"")

	test("SITES_${distfile}=https://example.org/",
		false,
		"SITES_${distfile}",
		"",
		"=",
		"SITES_${distfile}=",
		"https://example.org/",
		"",
		"")

	t.ExpectAssert(func() { testInvalid("\tVAR=value") })
	testInvalid("?=value")
	testInvalid("<=value")
	testInvalid("#")
	testInvalid("VAR.$$=value")

	// A commented variable assignment must start immediately after the comment character.
	// There must be no additional whitespace before the variable name.
	test("#VAR=value", true, "VAR", "", "=", "#VAR=", "value", "", "")

	// A single space is typically used for writing documentation, not for commenting out code.
	// Therefore this line doesn't count as commented variable assignment.
	testInvalid("# VAR=value")

	// Ensure that the alignment for the variable value is correct.
	test("BUILD_DIRS=\tdir1 dir2",
		false,
		"BUILD_DIRS",
		"",
		"=",
		"BUILD_DIRS=\t",
		"dir1 dir2",
		"",
		"")

	// Ensure that the alignment for the variable value is correct,
	// even if the whole line is commented.
	test("#BUILD_DIRS=\tdir1 dir2",
		true,
		"BUILD_DIRS",
		"",
		"=",
		"#BUILD_DIRS=\t",
		"dir1 dir2",
		"",
		"")

	test("MASTER_SITES=\t#none",
		false,
		"MASTER_SITES",
		"",
		"=",
		"MASTER_SITES=\t",
		"",
		"",
		"#none")

	test("MASTER_SITES=\t# none",
		false,
		"MASTER_SITES",
		"",
		"=",
		"MASTER_SITES=\t",
		"",
		"",
		"# none")

	test("EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",

		false,
		"EGDIRS",
		"",
		"=",
		"EGDIRS=\t",
		"${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
		"",
		"")

	test("VAR:=\t${VAR:M-*:[\\#]}",
		false,
		"VAR",
		"",
		":=",
		"VAR:=\t",
		"${VAR:M-*:[#]}",
		"",
		"")

	test("#VAR=value",
		true, "VAR", "", "=", "#VAR=", "value", "", "")

	testInvalid("# VAR=value")
	testInvalid("#\tVAR=value")
	testInvalid(MkCvsID)

	testLine(
		lines(
			"VAR=\t\t\t\\",
			"\tvalue"),
		false,
		"VAR",
		"",
		"=",
		"VAR=\t\t\t",
		"value",
		"",
		"")

	testLine(
		lines(
			"#VAR=\t\t\t\\",
			"#\tvalue"),
		true,
		"VAR",
		"",
		"=",
		"#VAR=\t\t\t",
		"value",
		"",
		"")
}

func (s *Suite) Test_MkLineParser_parseShellcmd(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		"\tshell command # shell comment")

	t.CheckEquals(mkline.IsShellCommand(), true)
	t.CheckEquals(mkline.ShellCommand(), "shell command # shell comment")
}

func (s *Suite) Test_MkLineParser_parseCommentOrEmpty__comment(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		"# whole line comment")

	t.CheckEquals(mkline.IsComment(), true)
}

func (s *Suite) Test_MkLineParser_parseCommentOrEmpty__empty(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101, "")

	t.CheckEquals(mkline.IsEmpty(), true)
}

func (s *Suite) Test_MkLineParser_parseDirective(c *check.C) {
	t := s.Init(c)

	test := func(input, expectedIndent, expectedDirective, expectedArgs, expectedComment string, diagnostics ...string) {
		line := t.NewLine("filename.mk", 123, input)
		parser := NewMkLineParser()
		splitResult := parser.split(line, input, true)
		mkline := parser.parseDirective(line, splitResult)
		if !c.Check(mkline, check.NotNil) {
			return
		}

		t.CheckDeepEquals(
			[]interface{}{mkline.Indent(), mkline.Directive(), mkline.Args(), mkline.DirectiveComment()},
			[]interface{}{expectedIndent, expectedDirective, expectedArgs, expectedComment})
		t.CheckOutput(diagnostics)
	}

	test(".if ${VAR} == value",
		"", "if", "${VAR} == value", "")

	test(".\tendif # comment",
		"\t", "endif", "", "comment")

	test(".if ${VAR} == \"#\"",
		"", "if", "${VAR} == \"", "\"")

	test(".if ${VAR:[#]}",
		"", "if", "${VAR:[#]}", "")

	test(".if ${VAR} == \\",
		"", "if", "${VAR} == \\", "")

	test(".if ${VAR",
		"", "if", "${VAR", "",
		"WARN: filename.mk:123: Missing closing \"}\" for \"VAR\".")
}

func (s *Suite) Test_MkLineParser_parseDirective__escaped_hash(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		".  if !empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[\\#]} == 1 # directive comment")

	t.CheckEquals(mkline.IsDirective(), true)
	t.CheckEquals(mkline.Indent(), "  ")
	t.CheckEquals(mkline.Directive(), "if")
	t.CheckEquals(mkline.Args(), "!empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[#]} == 1")
	t.CheckEquals(mkline.DirectiveComment(), "directive comment")
}

func (s *Suite) Test_MkLineParser_parseInclude(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		".    include \"../../mk/bsd.prefs.mk\" # include comment")

	t.CheckEquals(mkline.IsInclude(), true)
	t.CheckEquals(mkline.Indent(), "    ")
	t.CheckEquals(mkline.MustExist(), true)
	t.CheckEquals(mkline.IncludedFile(), NewRelPathString("../../mk/bsd.prefs.mk"))

	t.CheckEquals(mkline.IsSysinclude(), false)
}

func (s *Suite) Test_MkLineParser_parseSysinclude(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		".    include <subdir.mk> # sysinclude comment")

	t.CheckEquals(mkline.IsSysinclude(), true)
	t.CheckEquals(mkline.Indent(), "    ")
	t.CheckEquals(mkline.MustExist(), true)
	t.CheckEquals(mkline.IncludedFile(), NewRelPathString("subdir.mk"))

	t.CheckEquals(mkline.IsInclude(), false)
}

func (s *Suite) Test_MkLineParser_parseDependency(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		"target1 target2: source1 source2")

	t.CheckEquals(mkline.IsDependency(), true)
	t.CheckEquals(mkline.Targets(), "target1 target2")
	t.CheckEquals(mkline.Sources(), "source1 source2")
}

func (s *Suite) Test_MkLineParser_parseDependency__space(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		"target : source")

	t.CheckEquals(mkline.Targets(), "target")
	t.CheckEquals(mkline.Sources(), "source")
	t.CheckOutputLines(
		"NOTE: test.mk:101: Space before colon in dependency line.")
}

func (s *Suite) Test_MkLineParser_parseMergeConflict(c *check.C) {
	t := s.Init(c)

	mkline := t.NewMkLine("test.mk", 101,
		"<<<<<<<<<<<<<<<<<")

	// Merge conflicts are of neither type.
	t.CheckEquals(mkline.IsVarassign(), false)
	t.CheckEquals(mkline.IsDirective(), false)
	t.CheckEquals(mkline.IsInclude(), false)
	t.CheckEquals(mkline.IsEmpty(), false)
	t.CheckEquals(mkline.IsComment(), false)
	t.CheckEquals(mkline.IsDependency(), false)
	t.CheckEquals(mkline.IsShellCommand(), false)
	t.CheckEquals(mkline.IsSysinclude(), false)
}

func (s *Suite) Test_MkLineParser_split(c *check.C) {
	t := s.Init(c)
	b := NewMkTokenBuilder()

	varuse := b.VaruseToken
	varuseText := b.VaruseTextToken
	text := b.TextToken
	tokens := b.Tokens

	test := func(text string, expected mkLineSplitResult, diagnostics ...string) {
		line := t.NewLine("filename.mk", 123, text)
		actual := NewMkLineParser().split(line, text, true)

		t.CheckOutput(diagnostics)
		t.CheckDeepEquals([]interface{}{text, actual}, []interface{}{text, expected})
	}

	t.Use(text, varuse, varuseText, tokens)

	test(
		"",
		mkLineSplitResult{})

	test(
		"text",
		mkLineSplitResult{
			main:   "text",
			tokens: tokens(text("text")),
		})

	// Leading space is always kept.
	test(
		" text",
		mkLineSplitResult{
			main:   " text",
			tokens: tokens(text(" text")),
		})

	// Trailing space does not end up in the tokens since it is usually
	// ignored.
	test(
		"text\t",
		mkLineSplitResult{
			main:               "text",
			tokens:             tokens(text("text")),
			spaceBeforeComment: "\t",
		})

	test(
		"text\t# intended comment",
		mkLineSplitResult{
			main:               "text",
			tokens:             tokens(text("text")),
			spaceBeforeComment: "\t",
			hasComment:         true,
			comment:            " intended comment",
		})

	// Trailing space is saved in a separate field to detect accidental
	// unescaped # in the middle of a word, like the URL fragment in this
	// example.
	test(
		"url#fragment",
		mkLineSplitResult{
			main:       "url",
			tokens:     tokens(text("url")),
			hasComment: true,
			comment:    "fragment",
		})

	// The leading space from the comment is preserved to make parsing as exact
	// as possible.
	//
	// The difference between "#defined" and "# defined" is relevant in a few
	// cases, such as the API documentation of the infrastructure files.
	test("# comment",
		mkLineSplitResult{
			hasComment: true,
			comment:    " comment",
		})

	test("#\tcomment",
		mkLineSplitResult{
			hasComment: true,
			comment:    "\tcomment",
		})

	test("#   comment",
		mkLineSplitResult{
			hasComment: true,
			comment:    "   comment",
		})

	test(
		"#VAR=#value",
		mkLineSplitResult{
			hasComment: true,
			comment:    "VAR=#value"})

	// When parsing a commented variable assignment, the code assumes that
	// the whole comment is left uninterpreted.
	test(
		"#VAR=\\#value",
		mkLineSplitResult{
			hasComment: true,
			comment:    "VAR=\\#value"})

	// Other than in the shell, # also starts a comment in the middle of a word.
	test("COMMENT=\tThe C# compiler",
		mkLineSplitResult{
			main:       "COMMENT=\tThe C",
			tokens:     tokens(text("COMMENT=\tThe C")),
			hasComment: true,
			comment:    " compiler",
		})

	test("COMMENT=\tThe C\\# compiler",
		mkLineSplitResult{
			main:       "COMMENT=\tThe C# compiler",
			tokens:     tokens(text("COMMENT=\tThe C# compiler")),
			hasComment: false,
			comment:    "",
		})

	test("${TARGET}: ${SOURCES} # comment",
		mkLineSplitResult{
			main:               "${TARGET}: ${SOURCES}",
			tokens:             tokens(varuse("TARGET"), text(": "), varuse("SOURCES")),
			spaceBeforeComment: " ",
			hasComment:         true,
			comment:            " comment",
		})

	// A # starts a comment, except if it immediately follows a [.
	// This is done so that the length modifier :[#] can be written without
	// escaping the #.
	test("VAR=\t${OTHER:[#]} # comment",
		mkLineSplitResult{
			main:               "VAR=\t${OTHER:[#]}",
			tokens:             tokens(text("VAR=\t"), varuse("OTHER", "[#]")),
			spaceBeforeComment: " ",
			hasComment:         true,
			comment:            " comment",
		})

	// The # in the :[#] modifier may be escaped or not. Both forms are equivalent.
	test("VAR:=\t${VAR:M-*:[\\#]}",
		mkLineSplitResult{
			main:   "VAR:=\t${VAR:M-*:[#]}",
			tokens: tokens(text("VAR:=\t"), varuse("VAR", "M-*", "[#]")),
		})

	// A backslash always escapes the next character, be it a # for a comment
	// or something else. This makes it difficult to write a literal \# in a
	// Makefile, but that's an edge case anyway.
	test("VAR0=\t#comment",
		mkLineSplitResult{
			main:   "VAR0=",
			tokens: tokens(text("VAR0=")),
			// Later, when converting this result into a proper variable assignment,
			// this "space before comment" is reclassified as "space before the value",
			// in order to align the "#comment" with the other variable values.
			spaceBeforeComment: "\t",
			hasComment:         true,
			comment:            "comment",
		})

	test("VAR1=\t\\#no-comment",
		mkLineSplitResult{
			main:   "VAR1=\t#no-comment",
			tokens: tokens(text("VAR1=\t#no-comment")),
		})

	test("VAR2=\t\\\\#comment",
		mkLineSplitResult{
			main:       "VAR2=\t\\\\",
			tokens:     tokens(text("VAR2=\t\\\\")),
			hasComment: true,
			comment:    "comment",
		})

	// The backslash is only removed when it escapes a comment.
	// In particular, it cannot be used to escape a dollar that starts a
	// variable use.
	test("VAR0=\t$T",
		mkLineSplitResult{
			main:   "VAR0=\t$T",
			tokens: tokens(text("VAR0=\t"), varuseText("$T", "T")),
		},
		"WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")

	test("VAR1=\t\\$T",
		mkLineSplitResult{
			main:   "VAR1=\t\\$T",
			tokens: tokens(text("VAR1=\t\\"), varuseText("$T", "T")),
		},
		"WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")

	test("VAR2=\t\\\\$T",
		mkLineSplitResult{
			main:   "VAR2=\t\\\\$T",
			tokens: tokens(text("VAR2=\t\\\\"), varuseText("$T", "T")),
		},
		"WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")

	// To escape a dollar, write it twice.
	test("$$shellvar $${shellvar} \\${MKVAR} [] \\x",
		mkLineSplitResult{
			main:   "$$shellvar $${shellvar} \\${MKVAR} [] \\x",
			tokens: tokens(text("$$shellvar $${shellvar} \\"), varuse("MKVAR"), text(" [] \\x")),
		})

	// Parse errors are recorded in the rest return value.
	test("${UNCLOSED",
		mkLineSplitResult{
			main:   "${UNCLOSED",
			tokens: tokens(varuseText("${UNCLOSED", "UNCLOSED")),
		},
		"WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED\".")

	// Even if there is a parse error in the main part,
	// the comment is extracted.
	test("text before ${UNCLOSED# comment",
		mkLineSplitResult{
			main: "text before ${UNCLOSED",
			tokens: tokens(
				text("text before "),
				varuseText("${UNCLOSED", "UNCLOSED")),
			hasComment: true,
			comment:    " comment",
		},
		"WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED\".")

	// Even in case of parse errors, the space before the comment is parsed
	// correctly.
	test("text before ${UNCLOSED # comment",
		mkLineSplitResult{
			main: "text before ${UNCLOSED",
			tokens: tokens(
				text("text before "),
				// It's a bit inconsistent that the varname includes the space
				// but the text doesn't; anyway, it's an edge case.
				varuseText("${UNCLOSED", "UNCLOSED ")),
			spaceBeforeComment: " ",
			hasComment:         true,
			comment:            " comment",
		},
		"WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED \".",
		"WARN: filename.mk:123: Invalid part \" \" after variable name \"UNCLOSED\".")

	// The dollar-space refers to a normal Make variable named " ".
	// The lonely dollar at the very end refers to the variable named "",
	// which is specially protected in bmake to always contain the empty string.
	// It is heavily used in .for loops in the form ${:Uvalue}.
	//
	// TODO: The rest of pkglint assumes that the empty string is not a valid
	//  variable name, mainly because the empty variable name is not visible
	//  outside of the bmake debugging mode.
	test("Lonely $ character $",
		mkLineSplitResult{
			main: "Lonely $ character $",
			tokens: tokens(
				text("Lonely "),
				varuseText("$ " /* instead of "${ }" */, " "),
				text("character "),
				text("$")),
		})

	// The character [ prevents the following # from starting a comment, even
	// outside of variable modifiers.
	test("COMMENT=\t[#] $$\\# $$# comment",
		mkLineSplitResult{
			main:       "COMMENT=\t[#] $$# $$",
			tokens:     tokens(text("COMMENT=\t[#] $$# $$")),
			hasComment: true,
			comment:    " comment",
		})

	test("VAR2=\t\\\\#comment",
		mkLineSplitResult{
			main:       "VAR2=\t\\\\",
			tokens:     tokens(text("VAR2=\t\\\\")),
			hasComment: true,
			comment:    "comment",
		})

	// At this stage, MkLine.split doesn't know that empty(...) takes
	// a variable use. Instead it just sees ordinary characters and
	// other uses of variables.
	test(".if empty(${VAR.${tool}}:C/\\:.*$//:M${pattern})",
		mkLineSplitResult{
			main: ".if empty(${VAR.${tool}}:C/\\:.*$//:M${pattern})",
			tokens: tokens(
				text(".if empty("),
				varuse("VAR.${tool}"),
				text(":C/\\:.*"),
				text("$"),
				text("//:M"),
				varuse("pattern"),
				text(")")),
		})

	test("   # comment after spaces",
		mkLineSplitResult{
			spaceBeforeComment: "   ",
			hasComment:         true,
			comment:            " comment after spaces",
		})

	// FIXME: This theoretical edge case is interpreted differently
	//  between bmake and pkglint. Pkglint treats the # as a comment,
	//  while bmake interprets it as a regular character.
	test("\\[#",
		mkLineSplitResult{
			main:       "\\[",
			tokens:     tokens(text("\\[")),
			hasComment: true,
		})

	test("\\\\[#",
		mkLineSplitResult{
			main:   "\\\\[#",
			tokens: tokens(text("\\\\[#")),
		})
}

func (s *Suite) Test_MkLineParser_split__preserve_comment(c *check.C) {
	t := s.Init(c)
	b := NewMkTokenBuilder()

	tokens := b.Tokens
	text := b.TextToken
	varUse := b.VaruseToken

	test := func(text string, expected mkLineSplitResult, diagnostics ...string) {
		line := t.NewLine("filename.mk", 123, text)
		actual := NewMkLineParser().split(line, text, false)

		t.CheckDeepEquals(actual, expected)
		t.CheckOutput(diagnostics)
	}

	test(
		"text\t# no comment",
		mkLineSplitResult{
			main:   "text\t# no comment",
			tokens: tokens(text("text\t# no comment"))})

	test(
		"url#fragment",
		mkLineSplitResult{
			main:   "url#fragment",
			tokens: tokens(text("url#fragment"))})

	test("# no comment",
		mkLineSplitResult{
			main:   "# no comment",
			tokens: tokens(text("# no comment"))})

	// Other than in the shell, # also starts a comment in the middle of a word.
	test("The C# compiler",
		mkLineSplitResult{
			main:   "The C# compiler",
			tokens: tokens(text("The C# compiler"))})

	test("The C\\# compiler",
		mkLineSplitResult{
			main:   "The C\\# compiler",
			tokens: tokens(text("The C\\# compiler"))})

	test("# ${VAR}",
		mkLineSplitResult{
			main:   "# ${VAR}",
			tokens: tokens(text("# "), varUse("VAR"))})

	test("# ",
		mkLineSplitResult{
			main:               "#",
			tokens:             tokens(text("#")),
			spaceBeforeComment: " "})
}

func (s *Suite) Test_MkLineParser_split__unclosed_varuse(c *check.C) {
	t := s.Init(c)
	b := NewMkTokenBuilder()

	test := func(text string, expected mkLineSplitResult, diagnostics ...string) {
		line := t.NewLine("filename.mk", 123, text)

		splitResult := NewMkLineParser().split(line, text, true)

		t.CheckDeepEquals(splitResult, expected)
		t.CheckOutput(diagnostics)
	}

	test(
		"EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",

		mkLineSplitResult{
			"EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
			b.Tokens(
				b.TextToken("EGDIRS=\t"),
				b.VaruseTextToken(
					"${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
					"EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d")),
			"",
			false,
			false,
			"",
		},

		"WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/pam.d\".",
		"WARN: filename.mk:123: Invalid part \"/pam.d\" after variable name \"EGDIR\".",
		"WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/dbus-1/system.d ${EGDIR/pam.d\".",
		"WARN: filename.mk:123: Invalid part \"/dbus-1/system.d ${EGDIR/pam.d\" after variable name \"EGDIR\".",
		"WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\".",
		"WARN: filename.mk:123: Invalid part \"/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\" after variable name \"EGDIR\".")
}

func (s *Suite) Test_MkLineParser_unescapeComment(c *check.C) {
	t := s.Init(c)

	test := func(text string, main, comment string) {
		aMain, aComment := NewMkLineParser().unescapeComment(text)
		t.CheckDeepEquals(
			[]interface{}{text, aMain, aComment},
			[]interface{}{text, main, comment})
	}

	test("",
		"",
		"")
	test("text",
		"text",
		"")

	// The leading space from the comment is preserved to make parsing as exact
	// as possible.
	//
	// The difference between "#defined" and "# defined" is relevant in a few
	// cases, such as the API documentation of the infrastructure files.
	test("# comment",
		"",
		"# comment")
	test("#\tcomment",
		"",
		"#\tcomment")
	test("#   comment",
		"",
		"#   comment")

	// Other than in the shell, # also starts a comment in the middle of a word.
	test("COMMENT=\tThe C# compiler",
		"COMMENT=\tThe C",
		"# compiler")
	test("COMMENT=\tThe C\\# compiler",
		"COMMENT=\tThe C# compiler",
		"")

	test("${TARGET}: ${SOURCES} # comment",
		"${TARGET}: ${SOURCES} ",
		"# comment")

	// A # starts a comment, except if it immediately follows a [.
	// This is done so that the length modifier :[#] can be written without
	// escaping the #.
	test("VAR=\t${OTHER:[#]} # comment",
		"VAR=\t${OTHER:[#]} ",
		"# comment")

	// The # in the :[#] modifier may be escaped or not. Both forms are equivalent.
	test("VAR:=\t${VAR:M-*:[\\#]}",
		"VAR:=\t${VAR:M-*:[#]}",
		"")

	// The character [ prevents the following # from starting a comment, even
	// outside of variable modifiers.
	test("COMMENT=\t[#] $$\\# $$# comment",
		"COMMENT=\t[#] $$# $$",
		"# comment")

	// A backslash always escapes the next character, be it a # for a comment
	// or something else. This makes it difficult to write a literal \# in a
	// Makefile, but that's an edge case anyway.
	test("VAR0=\t#comment",
		"VAR0=\t",
		"#comment")
	test("VAR1=\t\\#no-comment",
		"VAR1=\t#no-comment",
		"")
	test("VAR2=\t\\\\#comment",
		"VAR2=\t\\\\",
		"#comment")

	// The backslash is only removed when it escapes a comment.
	// In particular, it cannot be used to escape a dollar that starts a
	// variable use.
	test("VAR0=\t$T",
		"VAR0=\t$T",
		"")
	test("VAR1=\t\\$T",
		"VAR1=\t\\$T",
		"")
	test("VAR2=\t\\\\$T",
		"VAR2=\t\\\\$T",
		"")

	// To escape a dollar, write it twice.
	test("$$shellvar $${shellvar} \\${MKVAR} [] \\x",
		"$$shellvar $${shellvar} \\${MKVAR} [] \\x",
		"")

	// Parse errors are recorded in the rest return value.
	test("${UNCLOSED",
		"${UNCLOSED",
		"")

	// In this early phase of parsing, unfinished variable uses are not
	// interpreted and do not influence the detection of the comment start.
	test("text before ${UNCLOSED # comment",
		"text before ${UNCLOSED ",
		"# comment")

	// The dollar-space refers to a normal Make variable named " ".
	// The lonely dollar at the very end refers to the variable named "",
	// which is specially protected in bmake to always contain the empty string.
	// It is heavily used in .for loops in the form ${:Uvalue}.
	test("Lonely $ character $",
		"Lonely $ character $",
		"")

	// An even number of backslashes does not escape the #.
	// Therefore it starts a comment here.
	test("VAR2=\t\\\\#comment",
		"VAR2=\t\\\\",
		"#comment")
}

func (s *Suite) Test_MkLineParser_getRawValueAlign__assertion(c *check.C) {
	t := s.Init(c)

	var p MkLineParser

	// This is unrealistic; just for code coverage of the assertion.
	t.ExpectAssert(func() { p.getRawValueAlign("a", "b") })
}