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

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

Revision 1.64, Sun Dec 8 22:03:38 2019 UTC (2 months, 2 weeks ago) by rillig
Branch: MAIN
Changes since 1.63: +263 -10 lines

pkgtools/pkglint: update pkglint to 19.3.15

Changes since 19.3.14:

Invalid lines in PLIST files are now reported as errors instead of
warnings. If pkglint doesn't know about it, it must be an error.

In PLIST files, all paths are validated to be canonical. That is, no
dotdot components, no absolute paths, no extra slashes, no intermediate
dot components.

Fewer notes for unexpanded variable expressions in DESCR files. Before,
the text $@ was reported as possible Makefile variable even though it
was just a Perl expression.

README files are allowed again in pkgsrc package directories. There was
no convincing argument why these should be forbidden.

A few diagnostics have been changed from NOTE to WARNING or from WARNING
to ERROR, to match their wording.

When pkglint suggests to replace :M with ==, the wording is now "can be
made" instead of "should".

package pkglint

import (
	"gopkg.in/check.v1"
)

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

	mkline := t.NewMkLine("filename.mk", 123, "")
	cv := VartypeCheck{MkLine: mkline}

	cv.Errorf("Error %q.", "message")

	t.CheckOutputLines(
		"ERROR: filename.mk:123: Error \"message\".")
}

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

	mkline := t.NewMkLine("filename.mk", 123, "")
	cv := VartypeCheck{MkLine: mkline}

	cv.Warnf("Warning %q.", "message")

	t.CheckOutputLines(
		"WARN: filename.mk:123: Warning \"message\".")
}

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

	mkline := t.NewMkLine("filename.mk", 123, "")
	cv := VartypeCheck{MkLine: mkline}

	cv.Notef("Note %q.", "message")

	t.CheckOutputLines(
		"NOTE: filename.mk:123: Note \"message\".")
}

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

	t.SetUpCommandLine("--explain")
	mkline := t.NewMkLine("filename.mk", 123, "")
	cv := VartypeCheck{MkLine: mkline}

	cv.Notef("Note %q.", "message")
	cv.Explain("Explanation.")

	t.CheckOutputLines(
		"NOTE: filename.mk:123: Note \"message\".",
		"",
		"\tExplanation.",
		"")
}

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

	mkline := t.NewMkLine("filename.mk", 123, "")
	cv := VartypeCheck{MkLine: mkline}

	t.CheckEquals(cv.Autofix(), mkline.Autofix())
}

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

	cv := VartypeCheck{
		Varname:    "OLD",
		Value:      "oldValue${VAR}",
		ValueNoVar: "oldValue",
	}

	copied := cv.WithValue("newValue${NEW_VAR}")

	t.CheckEquals(copied.Varname, "OLD")
	t.CheckEquals(copied.Value, "newValue${NEW_VAR}")
	t.CheckEquals(copied.ValueNoVar, "newValue")
	t.CheckEquals(cv.Value, "oldValue${VAR}")
	t.CheckEquals(cv.ValueNoVar, "oldValue")
}

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

	cv := VartypeCheck{
		Varname:    "OLD",
		Value:      "oldValue${VAR}",
		ValueNoVar: "oldValue",
	}

	copied := cv.WithVarnameValue("NEW", "newValue${NEW_VAR}")

	t.CheckEquals(copied.Varname, "NEW")
	t.CheckEquals(copied.Value, "newValue${NEW_VAR}")
	t.CheckEquals(copied.ValueNoVar, "newValue")
	t.CheckEquals(cv.Value, "oldValue${VAR}")
	t.CheckEquals(cv.ValueNoVar, "oldValue")
}

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

	cv := VartypeCheck{
		Varname:    "OLD",
		Op:         opAssign,
		Value:      "oldValue${VAR}",
		ValueNoVar: "oldValue",
	}

	copied := cv.WithVarnameValueMatch("NEW", "newValue${NEW_VAR}")

	t.CheckEquals(copied.Varname, "NEW")
	t.CheckEquals(copied.Op, opUseMatch)
	t.CheckEquals(copied.Value, "newValue${NEW_VAR}")
	t.CheckEquals(copied.ValueNoVar, "newValue")
	t.CheckEquals(cv.Varname, "OLD")
	t.CheckEquals(cv.Op, opAssign)
	t.CheckEquals(cv.Value, "oldValue${VAR}")
	t.CheckEquals(cv.ValueNoVar, "oldValue")
}

func (s *Suite) Test_VartypeCheck_AwkCommand(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtAwkCommand)

	vt.Varname("PRINT_PLIST_AWK")
	vt.Op(opAssignAppend)
	vt.Values(
		"{print $0}",
		"{print $$0}")
	t.DisableTracing()
	vt.Values(
		"{print $0}",
		"{print $$0}")

	// TODO: In this particular context of AWK programs, $$0 is not a shell variable.
	//  The warning should be adjusted to reflect this.

	vt.Output(
		"WARN: filename.mk:1: $0 is ambiguous. "+
			"Use ${0} if you mean a Make variable or $$0 if you mean a shell variable.",
		"WARN: filename.mk:3: $0 is ambiguous. "+
			"Use ${0} if you mean a Make variable or $$0 if you mean a shell variable.")
}

func (s *Suite) Test_VartypeCheck_BasicRegularExpression(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtBasicRegularExpression)

	vt.Varname("CHECK_FILES_SKIP")
	vt.Values(
		".*\\.pl$",
		".*\\.pl$$",
		"\u1E9E",
		"\\(capture\\)\\1",
		"\\+")

	vt.Output(
		"WARN: filename.mk:1: Internal pkglint error in MkLine.Tokenize at \"$\".",
		"WARN: filename.mk:5: In a basic regular expression, a backslash followed by \"+\" is undefined.")

	// Check for special characters that appear outside of character classes.
	vt.Values(
		"\u0007",
		" !\"\"\\#$$%&''()*+,-./09:;<=>?",
		"@AZ[\\\\]^_``az{|}~",
		"\t")

	vt.OutputEmpty()

	vt.Values(
		"?",
		"\\?",
		"\\\\?",
		"\\\\\\?")

	vt.Output(
		"WARN: filename.mk:22: In a basic regular expression, a backslash followed by \"?\" is undefined.",
		"WARN: filename.mk:24: In a basic regular expression, a backslash followed by \"?\" is undefined.")

	vt.Values(
		"package-[0-9][0-9.]*",
		"unclosed-[",
		// TODO: Warn about the unclosed character class.
		"backslash-[\\")

	vt.OutputEmpty()

	vt.Values(
		// TODO: Warn about incomplete regular expression escape
		"\\",
		"\\\\")

	vt.OutputEmpty()

	vt.Values(
		"${VAR}*",
		"\\?${VAR}\\/")

	vt.Output(
		"WARN: filename.mk:52: In a basic regular expression, a backslash followed by \"?\" is undefined.",
		"WARN: filename.mk:52: In a basic regular expression, a backslash followed by \"/\" is undefined.")
}

func (s *Suite) Test_VartypeCheck_BuildlinkDepmethod(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtBuildlinkDepmethod)

	vt.Varname("BUILDLINK_DEPMETHOD.libc")
	vt.Op(opAssignDefault)
	vt.Values(
		"full",
		"unknown",
		"${BUILDLINK_DEPMETHOD.kernel}")

	vt.Output(
		"WARN: filename.mk:2: Invalid dependency method \"unknown\". Valid methods are \"build\" or \"full\".")
}

func (s *Suite) Test_VartypeCheck_Category(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtCategory)

	t.CreateFileLines("filesyscategory/Makefile",
		"# empty")
	t.CreateFileLines("wip/Makefile",
		"# empty")

	vt.Varname("CATEGORIES")
	vt.Values(
		"chinese",
		"arabic",
		"filesyscategory",
		"wip",
		"gnome",
		"gnustep",
		"java",
		"kde",
		"korean",
		"linux",
		"local",
		"plan9",
		"R",
		"ruby",
		"scm",
		"tcl",
		"tk",
		"windowmaker",
		"xmms")

	vt.Output(
		"ERROR: filename.mk:2: Invalid category \"arabic\".",
		"ERROR: filename.mk:4: Invalid category \"wip\".")
}

func (s *Suite) Test_VartypeCheck_CFlag(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtCFlag)

	vt.tester.SetUpTool("pkg-config", "", AtRunTime)

	vt.Varname("CFLAGS")
	vt.Op(opAssignAppend)
	vt.Values(
		"-Wall",
		"/W3",
		"target:sparc64",
		"-std=c99",
		"-XX:+PrintClassHistogramAfterFullGC",
		"`pkg-config pidgin --cflags`",
		"-c99",
		"-c",
		"-no-integrated-as",
		"-pthread",
		"`pkg-config`_plus")
	vt.OutputEmpty()

	vt.Values(
		"-L${PREFIX}/lib",
		"-L${PREFIX}/lib64",
		"-lncurses",
		"-DMACRO=\\\"",
		"-DMACRO=\\'")

	vt.Output(
		"WARN: filename.mk:21: \"-L${PREFIX}/lib\" is a linker flag "+
			"and belong to LDFLAGS, LIBS or LDADD instead of CFLAGS.",
		"WARN: filename.mk:22: \"-L${PREFIX}/lib64\" is a linker flag "+
			"and belong to LDFLAGS, LIBS or LDADD instead of CFLAGS.",
		"WARN: filename.mk:23: \"-lncurses\" is a linker flag "+
			"and belong to LDFLAGS, LIBS or LDADD instead of CFLAGS.",
		"WARN: filename.mk:24: Compiler flag \"-DMACRO=\\\\\\\"\" "+
			"has unbalanced double quotes.",
		"WARN: filename.mk:25: Compiler flag \"-DMACRO=\\\\'\" "+
			"has unbalanced single quotes.")

	vt.Op(opUseMatch)
	vt.Values(
		"anything")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_Comment(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtComment)

	G.Pkg = NewPackage(t.File("category/converter"))
	G.Pkg.EffectivePkgbase = "converter"

	vt.Varname("COMMENT")
	vt.Values(
		"Versatile Programming Language",
		"TODO: Short description of the package",
		"A great package.",
		"some packages need a very very long comment to explain their basic usefulness",
		"\"Quoting the comment is wrong\"",
		"'Quoting the comment is wrong'",
		"Package is a great package",
		"Package is an awesome package",
		"The Big New Package is a great package",
		"Converter converts between measurement units",
		"Converter is a unit converter",
		"\"Official\" office suite",
		"'SQL injection fuzzer")

	vt.Output(
		"ERROR: filename.mk:2: COMMENT must be set.",
		"WARN: filename.mk:3: COMMENT should not begin with \"A\".",
		"WARN: filename.mk:3: COMMENT should not end with a period.",
		"WARN: filename.mk:4: COMMENT should start with a capital letter.",
		"WARN: filename.mk:4: COMMENT should not be longer than 70 characters.",
		"WARN: filename.mk:5: COMMENT should not be enclosed in quotes.",
		"WARN: filename.mk:6: COMMENT should not be enclosed in quotes.",
		"WARN: filename.mk:7: COMMENT should not contain \"is a\".",
		"WARN: filename.mk:8: COMMENT should not contain \"is an\".",
		"WARN: filename.mk:9: COMMENT should not contain \"is a\".",
		"WARN: filename.mk:10: COMMENT should not start with the package name.",
		"WARN: filename.mk:11: COMMENT should not start with the package name.",
		"WARN: filename.mk:11: COMMENT should not contain \"is a\".")
}

func (s *Suite) Test_VartypeCheck_ConfFiles(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtConfFiles)

	vt.Varname("CONF_FILES")
	vt.Op(opAssignAppend)
	vt.Values(
		"single/file",
		"share/etc/config ${PKG_SYSCONFDIR}/etc/config",
		"share/etc/config ${PKG_SYSCONFBASE}/etc/config file",
		"share/etc/config ${PREFIX}/etc/config share/etc/config2 ${VARBASE}/config2",
		"share/etc/bootrc /etc/bootrc")

	vt.Output(
		"WARN: filename.mk:1: Values for CONF_FILES should always be pairs of paths.",
		"WARN: filename.mk:3: Values for CONF_FILES should always be pairs of paths.",
		"WARN: filename.mk:5: The destination file \"/etc/bootrc\" should start with a variable reference.")
}

// See Test_MkParser_Dependency.
func (s *Suite) Test_VartypeCheck_Dependency(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtDependency)

	vt.Varname("CONFLICTS")
	vt.Op(opAssignAppend)

	// comparison operators
	vt.Values(
		"perl5>=5.22",
		"libkipi>=0.1.5<4.0",
		"gtk2+>=2.16")
	vt.OutputEmpty()

	// pattern matching
	vt.Values(
		"perl-5*",
		"perl5-*",
		"perl-5.22",
		"perl5-5.22.*",
		"gtksourceview-sharp-2.0-[0-9]*")
	vt.Output(
		"WARN: filename.mk:11: Please use \"5.*\" instead of \"5*\" as the version pattern.",
		"WARN: filename.mk:12: Please use \"perl5-[0-9]*\" instead of \"perl5-*\".",
		"WARN: filename.mk:13: Please use \"5.22{,nb*}\" instead of \"5.22\" as the version pattern.",
		"WARN: filename.mk:15: The version pattern \"2.0-[0-9]*\" should not contain a hyphen.")

	// nb suffix
	vt.Values(
		"perl5-5.22.*{,nb*}",
		"perl-5.22{,nb*}",
		"perl-5.22{,nb[0-9]*}",
		"mbrola-301h{,nb[0-9]*}",
		"ncurses-${NC_VERS}{,nb*}",
		"gnome-control-center>=2.20.1{,nb*}",
		"gnome-control-center>=2.20.1{,nb[0-9]*}")
	vt.Output(
		"WARN: filename.mk:26: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.",
		"WARN: filename.mk:27: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.")

	// alternative patterns, using braces or brackets
	vt.Values(
		"mpg123{,-esound,-nas}>=0.59.18",
		"mysql*-{client,server}-[0-9]*",
		"{ssh{,6}-[0-9]*,openssh-[0-9]*}",
		"libao-[a-z]*-[0-9]*")
	vt.OutputEmpty()

	// variables
	vt.Values(
		"postgresql8[0-35-9]-${module}-[0-9]*",
		"${_EMACS_CONFLICTS.${_EMACS_FLAVOR}}",
		"${PYPKGPREFIX}-sqlite3",
		"${PYPKGPREFIX}-sqlite3-${VERSION}",
		"${PYPKGPREFIX}-sqlite3-${PYSQLITE_REQD}",
		"${PYPKGPREFIX}-sqlite3>=${PYSQLITE_REQD}",
		"${EMACS_PACKAGE}>=${EMACS_MAJOR}",

		// The "*" is ambiguous. It could either continue the PKGBASE or
		// start the version number.
		"${PKGNAME_NOREV:S/jdk/jre/}*",

		// The canonical form is "{,nb*}" instead of "{nb*,}".
		// Plus, mentioning nb* is not necessary when using >=.
		"dovecot>=${PKGVERSION_NOREV}{nb*,}",

		"oxygen-icons>=${KF5VER}{,nb[0-9]*}",

		// The following pattern should have "]*}" instead of "]}*".
		"ja-vflib-lib-${VFLIB_VERSION}{,nb[0-9]}*",

		// The following pattern uses both ">=" and "*", which doesn't make sense.
		"${PYPKGPREFIX}-sphinx>=1.2.3nb1*",

		"{${NETSCAPE_PREFERRED:C/:/,/g}}-[0-9]*")

	vt.Output(
		"WARN: filename.mk:43: Invalid dependency pattern \"${PYPKGPREFIX}-sqlite3\".",
		// This pattern is invalid because the variable name doesn't contain "VER".
		"WARN: filename.mk:45: Invalid dependency pattern \"${PYPKGPREFIX}-sqlite3-${PYSQLITE_REQD}\".",
		"WARN: filename.mk:48: Invalid dependency pattern \"${PKGNAME_NOREV:S/jdk/jre/}*\".",
		"WARN: filename.mk:49: Invalid dependency pattern \"dovecot>=${PKGVERSION_NOREV}{nb*,}\".",
		"WARN: filename.mk:50: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.",
		"WARN: filename.mk:51: Invalid dependency pattern \"ja-vflib-lib-${VFLIB_VERSION}{,nb[0-9]}*\".",
		"WARN: filename.mk:52: Invalid dependency pattern \"${PYPKGPREFIX}-sphinx>=1.2.3nb1*\".")

	// invalid dependency patterns
	vt.Values(
		"Perl",
		"py-docs",
		"perl5-[5.10-5.22]*",
		"package-1.0|garbage",
		"package>=1.0:../../category/package",
		"package-1.0>=1.0.3",
		// This should be regarded as invalid since the [a-z0-9] might either
		// continue the PKGBASE or start the version number.
		"${RUBY_PKGPREFIX}-theme-[a-z0-9]*")
	vt.Output(
		"WARN: filename.mk:61: Invalid dependency pattern \"Perl\".",
		"WARN: filename.mk:62: Invalid dependency pattern \"py-docs\".",
		"WARN: filename.mk:63: Only [0-9]* is allowed in the numeric part of a dependency.",
		"WARN: filename.mk:63: The version pattern \"[5.10-5.22]*\" should not contain a hyphen.",
		"WARN: filename.mk:64: Invalid dependency pattern \"package-1.0|garbage\".",
		// TODO: Mention that the path should be removed.
		"WARN: filename.mk:65: Invalid dependency pattern \"package>=1.0:../../category/package\".",
		// TODO: Mention that version numbers in a pkgbase must be appended directly, without hyphen.
		"WARN: filename.mk:66: Invalid dependency pattern \"package-1.0>=1.0.3\".",
		"WARN: filename.mk:67: Invalid dependency pattern \"${RUBY_PKGPREFIX}-theme-[a-z0-9]*\".")
}

func (s *Suite) Test_VartypeCheck_DependencyWithPath(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtDependencyWithPath)

	t.CreateFileLines("category/package/Makefile")
	t.CreateFileLines("category/package/files/dummy")
	t.CreateFileLines("databases/py-sqlite3/Makefile")
	t.CreateFileLines("devel/gettext/Makefile")
	t.CreateFileLines("devel/gmake/Makefile")
	t.CreateFileLines("devel/py-module/Makefile")
	t.CreateFileLines("x11/alacarte/Makefile")
	G.Pkg = NewPackage(t.File("category/package"))

	vt.Varname("DEPENDS")
	vt.Op(opAssignAppend)
	vt.File(G.Pkg.File("filename.mk"))
	vt.Values(
		"Perl",
		"perl5>=5.22:../perl5",
		"perl5>=5.24:../../lang/perl5",
		"gtk2+>=2.16:../../x11/alacarte",
		"gettext-[0-9]*:../../devel/gettext",
		"gmake-[0-9]*:../../devel/gmake")

	vt.Output(
		"WARN: ~/category/package/filename.mk:1: Invalid dependency pattern with path \"Perl\".",
		"WARN: ~/category/package/filename.mk:2: Dependency paths should have the form \"../../category/package\".",
		"ERROR: ~/category/package/filename.mk:2: Relative path \"../perl5/Makefile\" does not exist.",
		"WARN: ~/category/package/filename.mk:2: \"../perl5\" is not a valid relative package directory.",
		"WARN: ~/category/package/filename.mk:2: Please use USE_TOOLS+=perl:run instead of this dependency.",
		"ERROR: ~/category/package/filename.mk:3: Relative path \"../../lang/perl5/Makefile\" does not exist.",
		"WARN: ~/category/package/filename.mk:3: Please use USE_TOOLS+=perl:run instead of this dependency.",
		"WARN: ~/category/package/filename.mk:5: Please use USE_TOOLS+=msgfmt instead of this dependency.",
		"WARN: ~/category/package/filename.mk:6: Please use USE_TOOLS+=gmake instead of this dependency.")

	vt.Values(
		"broken0.12.1:../../x11/alacarte", // missing version
		"broken[0-9]*:../../x11/alacarte", // missing version
		"broken[0-9]*../../x11/alacarte",  // missing colon
		"broken>=:../../x11/alacarte",     // incomplete comparison
		"broken=0:../../x11/alacarte",     // invalid comparison operator
		"broken=:../../x11/alacarte",      // incomplete comparison
		"broken-:../../x11/alacarte",      // incomplete pattern
		"broken>:../../x11/alacarte")      // incomplete comparison

	vt.Output(
		"WARN: ~/category/package/filename.mk:11: Invalid dependency pattern \"broken0.12.1\".",
		"WARN: ~/category/package/filename.mk:12: Invalid dependency pattern \"broken[0-9]*\".",
		"WARN: ~/category/package/filename.mk:13: Invalid dependency pattern with path \"broken[0-9]*../../x11/alacarte\".",
		"WARN: ~/category/package/filename.mk:14: Invalid dependency pattern \"broken>=\".",
		"WARN: ~/category/package/filename.mk:15: Invalid dependency pattern \"broken=0\".",
		"WARN: ~/category/package/filename.mk:16: Invalid dependency pattern \"broken=\".",
		"WARN: ~/category/package/filename.mk:17: Invalid dependency pattern \"broken-\".",
		"WARN: ~/category/package/filename.mk:18: Invalid dependency pattern \"broken>\".")

	vt.Values(
		"${PYPKGPREFIX}-sqlite3:../../${MY_PKGPATH.py-sqlite3}",
		"${PYPKGPREFIX}-sqlite3:../../databases/py-sqlite3",
		"${DEPENDS.NetBSD}",
		"${DEPENDENCY_PATTERN.py-sqlite3}:${DEPENDENCY_PATH.py-sqlite}",
		"${PYPKGPREFIX}-module>=0:../../devel/py-module",
		"${EMACS_PACKAGE}>=${EMACS_MAJOR}:${EMACS_PKGDIR}",
		"{${NETSCAPE_PREFERRED:C/:/,/g}}-[0-9]*:../../www/${NETSCAPE_PREFERRED:C/:.*//}")

	vt.Output(
		"WARN: ~/category/package/filename.mk:21: "+
			"Invalid dependency pattern \"${PYPKGPREFIX}-sqlite3\".",
		"WARN: ~/category/package/filename.mk:22: "+
			"Invalid dependency pattern \"${PYPKGPREFIX}-sqlite3\".")

	vt.Values(
		"gettext-[0-9]*:files/../../../databases/py-sqlite3")

	vt.Output(
		"WARN: ~/category/package/filename.mk:31: " +
			"\"files/../../../databases/py-sqlite3\" is " +
			"not a valid relative package directory.")
}

func (s *Suite) Test_VartypeCheck_DistSuffix(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtDistSuffix)

	vt.Varname("EXTRACT_SUFX")
	vt.Values(
		".tar.gz",
		".tar.bz2",
		".tar.gz # overrides a definition from a Makefile.common")

	vt.Output(
		"NOTE: filename.mk:1: EXTRACT_SUFX is \".tar.gz\" by default, so this definition may be redundant.")
}

func (s *Suite) Test_VartypeCheck_EmulPlatform(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtEmulPlatform)

	vt.Varname("EMUL_PLATFORM")
	vt.Values(
		"linux-i386",
		"nextbsd-8087",
		"${LINUX}")

	vt.Output(
		"WARN: filename.mk:2: \"nextbsd\" is not valid for the operating system part of EMUL_PLATFORM. "+
			"Use one of "+
			"{ bitrig bsdos cygwin darwin dragonfly freebsd haiku hpux "+
			"interix irix linux mirbsd netbsd openbsd osf1 solaris sunos "+
			"} instead.",
		"WARN: filename.mk:2: \"8087\" is not valid for the hardware architecture part of EMUL_PLATFORM. "+
			"Use one of { "+
			"aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 "+
			"cobalt coldfire convex dreamcast "+
			"earm earmeb earmhf earmhfeb earmv4 earmv4eb earmv5 earmv5eb "+
			"earmv6 earmv6eb earmv6hf earmv6hfeb "+
			"earmv7 earmv7eb earmv7hf earmv7hfeb evbarm "+
			"hpcmips hpcsh hppa hppa64 "+
			"i386 i586 i686 ia64 m68000 m68k m88k "+
			"mips mips64 mips64eb mips64el mipseb mipsel mipsn32 "+
			"mlrisc ns32k pc532 pmax powerpc powerpc64 rs6000 "+
			"s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
			"} instead.",
		"WARN: filename.mk:3: \"${LINUX}\" is not a valid emulation platform.")
}

func (s *Suite) Test_VartypeCheck_Enum(c *check.C) {
	basicType := enum("jdk1 jdk2 jdk4")
	G.Pkgsrc.vartypes.Define("JDK", basicType, UserSettable)
	vt := NewVartypeCheckTester(s.Init(c), basicType)

	vt.Varname("JDK")
	vt.Op(opUseMatch)
	vt.Values(
		"*",
		"jdk*",
		"sun-jdk*",
		"${JDKNAME}",
		"[")

	vt.Output(
		"WARN: filename.mk:3: The pattern \"sun-jdk*\" cannot match any of { jdk1 jdk2 jdk4 } for JDK.",
		"WARN: filename.mk:5: Invalid match pattern \"[\".")
}

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

	t.SetUpPkgsrc()
	t.Chdir("category/package")
	t.FinishSetUp()
	t.SetUpCommandLine("-Wall", "--explain")

	mklines := t.NewMkLines("module.mk",
		MkCvsID,
		"",
		".include \"../../mk/bsd.prefs.mk\"",
		"",
		".if !empty(MACHINE_ARCH:Mi386) || ${MACHINE_ARCH} == i386",
		".endif",
		".if !empty(PKGSRC_COMPILER:Mclang) || ${PKGSRC_COMPILER} == clang",
		".endif",
		".if ${MACHINE_ARCH:Ni386:Nx86_64:Nsparc64}",
		".endif")

	mklines.Check()

	t.CheckOutputLines(
		"NOTE: module.mk:5: MACHINE_ARCH can be "+
			"compared using the simpler \"${MACHINE_ARCH} == i386\" "+
			"instead of matching against \":Mi386\".",
		"",
		"\tThis variable has a single value, not a list of values. Therefore it",
		"\tfeels strange to apply list operators like :M and :N onto it. A more",
		"\tdirect approach is to use the == and != operators.",
		"",
		"\tAn entirely different case is when the pattern contains wildcards",
		"\tlike *, ?, []. In such a case, using the :M or :N modifiers is",
		"\tuseful and preferred.",
		"",
		"ERROR: module.mk:7: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
		"",
		"\tThe PKGSRC_COMPILER can be a list of chained compilers, e.g. \"ccache",
		"\tdistcc clang\". Therefore, comparing it using == or != leads to wrong",
		"\tresults in these cases.",
		"")
}

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

	t.SetUpPackage("category/own-master-site",
		"MASTER_SITE_OWN=\thttps://example.org/")
	t.FinishSetUp()

	vt := NewVartypeCheckTester(t, BtFetchURL)

	t.SetUpMasterSite("MASTER_SITE_GNU", "http://ftp.gnu.org/pub/gnu/")
	t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/")

	G.Pkg = NewPackage(t.File("category/own-master-site"))
	G.Pkg.load()

	vt.Varname("MASTER_SITES")
	vt.Values(
		"https://github.com/example/project/",
		"http://ftp.gnu.org/pub/gnu/bison", // Missing a slash at the end
		"${MASTER_SITE_GNU:=bison}",
		"${MASTER_SITE_INVALID:=subdir/}",
		"${MASTER_SITE_OWN}",
		"${MASTER_SITE_OWN:=subdir/}")

	vt.Output(
		"WARN: filename.mk:1: Please use ${MASTER_SITE_GITHUB:=example/} "+
			"instead of \"https://github.com/example/\" "+
			"and run \""+confMake+" help topic=github\" for further instructions.",
		"WARN: filename.mk:2: Please use ${MASTER_SITE_GNU:=bison} "+
			"instead of \"http://ftp.gnu.org/pub/gnu/bison\".",
		"ERROR: filename.mk:3: The subdirectory in MASTER_SITE_GNU must end with a slash.",
		"ERROR: filename.mk:4: The site MASTER_SITE_INVALID does not exist.")

	// PR 46570, keyword gimp-fix-ca
	vt.Values(
		"https://example.org/download.cgi?filename=filename&sha1=12341234")

	vt.Output(
		"WARN: filename.mk:11: The fetch URL \"https://example.org/download.cgi" +
			"?filename=filename&sha1=12341234\" should end with a slash.")

	vt.Values(
		"http://example.org/distfiles/",
		"http://example.org/download?filename=distfile;version=1.0",
		"http://example.org/download?filename=<distfile>;version=<version>")

	vt.Output(
		"WARN: filename.mk:22: The fetch URL \"http://example.org/download"+
			"?filename=distfile;version=1.0\" should end with a slash.",
		"WARN: filename.mk:23: \"http://example.org/download"+
			"?filename=<distfile>;version=<version>\" is not a valid URL.",
		"WARN: filename.mk:23: The fetch URL \"http://example.org/download"+
			"?filename=<distfile>;version=<version>\" should end with a slash.")

	vt.Values(
		"${MASTER_SITE_GITHUB:S,^,-,:=project/archive/${DISTFILE}}")

	// No warning that the part after the := must end with a slash,
	// since there is another modifier in the variable use, in this case :S.
	//
	// That modifier adds a hyphen at the beginning (but pkglint doesn't
	// inspect this), therefore the URL is not required to end with a slash anymore.
	vt.OutputEmpty()

	// As of June 2019, the :S modifier is not analyzed since it is unusual.
	vt.Values(
		"${MASTER_SITE_GNU:S,$,subdir,}",
		"${MASTER_SITE_GNU:S,$,subdir/,}")
	vt.OutputEmpty()

	vt.Values(
		"https://github.com/transmission/transmission-releases/raw/master/")
	vt.Output(
		"WARN: filename.mk:51: Please use ${MASTER_SITE_GITHUB:=transmission/} " +
			"instead of \"https://github.com/transmission/\" " +
			"and run \"" + confMake + " help topic=github\" for further instructions.")

	vt.Values(
		"-https://example.org/distfile.tar.gz",
		"-http://ftp.gnu.org/pub/gnu/bash-5.0.tar.gz",
		"-http://ftp.gnu.org/pub/gnu/bash/bash-5.0.tar.gz")

	vt.Output(
		"WARN: filename.mk:62: Please use ${MASTER_SITE_GNU:S,^,-,:=bash-5.0.tar.gz} "+
			"instead of \"-http://ftp.gnu.org/pub/gnu/bash-5.0.tar.gz\".",
		"WARN: filename.mk:63: Please use ${MASTER_SITE_GNU:S,^,-,:=bash/bash-5.0.tar.gz} "+
			"instead of \"-http://ftp.gnu.org/pub/gnu/bash/bash-5.0.tar.gz\".")

	vt.Values(
		"https://example.org/pub",
		"https://example.org/$@",
		"https://example.org/?f=",
		"https://example.org/download:",
		"https://example.org/download?",
		"https://example.org/$$")

	vt.Output(
		"WARN: filename.mk:71: The fetch URL \"https://example.org/pub\" should end with a slash.",
		"WARN: filename.mk:75: The fetch URL \"https://example.org/download?\" should end with a slash.",
		"WARN: filename.mk:76: \"https://example.org/$$\" is not a valid URL.",
		"WARN: filename.mk:76: The fetch URL \"https://example.org/$$\" should end with a slash.")

	// The transport protocol doesn't matter for matching the MASTER_SITEs.
	// See url2pkg.py, function adjust_site_from_sites_mk.
	vt.Values(
		"http://ftp.gnu.org/pub/gnu/bash/",
		"ftp://ftp.gnu.org/pub/gnu/bash/",
		"https://ftp.gnu.org/pub/gnu/bash/",
		"-http://ftp.gnu.org/pub/gnu/bash/bash-5.0.tar.gz",
		"-ftp://ftp.gnu.org/pub/gnu/bash/bash-5.0.tar.gz",
		"-https://ftp.gnu.org/pub/gnu/bash/bash-5.0.tar.gz")

	vt.Output(
		"WARN: filename.mk:81: Please use ${MASTER_SITE_GNU:=bash/} "+
			"instead of \"http://ftp.gnu.org/pub/gnu/bash/\".",
		"WARN: filename.mk:82: Please use ${MASTER_SITE_GNU:=bash/} "+
			"instead of \"ftp://ftp.gnu.org/pub/gnu/bash/\".",
		"WARN: filename.mk:83: Please use ${MASTER_SITE_GNU:=bash/} "+
			"instead of \"https://ftp.gnu.org/pub/gnu/bash/\".",
		"WARN: filename.mk:84: Please use ${MASTER_SITE_GNU:S,^,-,:=bash/bash-5.0.tar.gz} "+
			"instead of \"-http://ftp.gnu.org/pub/gnu/bash/bash-5.0.tar.gz\".",
		"WARN: filename.mk:85: Please use ${MASTER_SITE_GNU:S,^,-,:=bash/bash-5.0.tar.gz} "+
			"instead of \"-ftp://ftp.gnu.org/pub/gnu/bash/bash-5.0.tar.gz\".",
		"WARN: filename.mk:86: Please use ${MASTER_SITE_GNU:S,^,-,:=bash/bash-5.0.tar.gz} "+
			"instead of \"-https://ftp.gnu.org/pub/gnu/bash/bash-5.0.tar.gz\".")

	// The ${.TARGET} variable doesn't make sense at all in a URL.
	// Other variables might, and there could be checks for them.
	// As of December 2019 these are skipped completely,
	// see containsVarRef in VartypeCheck.URL.
	vt.Values(
		"https://example.org/$@")

	vt.OutputEmpty()
}

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

	vt := NewVartypeCheckTester(t, BtFetchURL)

	vt.Varname("MASTER_SITES")
	vt.Values(
		"https://github.com/example/project/",
		"${MASTER_SITE_OWN}")

	vt.Output(
		"ERROR: filename.mk:2: The site MASTER_SITE_OWN does not exist.")
}

func (s *Suite) Test_VartypeCheck_Filename(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtFilename)

	vt.Varname("JAVA_NAME")
	vt.Values(
		"Filename with spaces.docx",
		"OS/2-manual.txt")

	vt.Output(
		"WARN: filename.mk:1: The filename \"Filename with spaces.docx\" contains the invalid characters \"  \".",
		"WARN: filename.mk:2: The filename \"OS/2-manual.txt\" contains the invalid character \"/\".")

	vt.Op(opUseMatch)
	vt.Values(
		"Filename with spaces.docx")

	// There's no guarantee that a filename only contains [A-Za-z0-9.].
	// Therefore there are no useful checks in this situation.
	vt.Output(
		"WARN: filename.mk:11: The filename pattern \"Filename with spaces.docx\" " +
			"contains the invalid characters \"  \".")
}

func (s *Suite) Test_VartypeCheck_FilePattern(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtFilePattern)

	vt.Varname("PKGWILDCARD")
	vt.Values(
		"filename.txt",
		"*.txt",
		"[12345].txt",
		"[0-9].txt",
		"???.txt",
		"FilePattern with spaces.docx",
		"OS/2-manual.txt")

	vt.Output(
		"WARN: filename.mk:6: The filename pattern \"FilePattern with spaces.docx\" "+
			"contains the invalid characters \"  \".",
		"WARN: filename.mk:7: The filename pattern \"OS/2-manual.txt\" "+
			"contains the invalid character \"/\".")

	vt.Op(opUseMatch)
	vt.Values(
		"FilePattern with spaces.docx")

	// There's no guarantee that a filename only contains [A-Za-z0-9.].
	// Therefore it might be necessary to allow all characters here.
	// TODO: Investigate whether this restriction is useful in practice.
	vt.Output(
		"WARN: filename.mk:11: The filename pattern \"FilePattern with spaces.docx\" " +
			"contains the invalid characters \"  \".")
}

func (s *Suite) Test_VartypeCheck_FileMode(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtFileMode)

	vt.Varname("GAMEMODE")
	vt.Values(
		"u+rwx",
		"0600",
		"1234",
		"12345",
		"${OTHER_PERMS}",
		"")

	vt.Output(
		"WARN: filename.mk:1: Invalid file mode \"u+rwx\".",
		"WARN: filename.mk:4: Invalid file mode \"12345\".",
		"WARN: filename.mk:6: Invalid file mode \"\".")

	vt.Op(opUseMatch)
	vt.Values(
		"u+rwx")

	// There's no guarantee that a filename only contains [A-Za-z0-9.].
	// Therefore there are no useful checks in this situation.
	vt.Output(
		"WARN: filename.mk:11: Invalid file mode \"u+rwx\".")
}

func (s *Suite) Test_VartypeCheck_GccReqd(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtGccReqd)

	vt.Varname("GCC_REQD")
	vt.Op(opAssignAppend)
	vt.Values(
		"2.95",
		"3.1.5",
		"4.7",
		"4.8",
		"5.1",
		"6",
		"7.3")
	vt.Output(
		"WARN: filename.mk:5: GCC version numbers should only contain the major version (5).",
		"WARN: filename.mk:7: GCC version numbers should only contain the major version (7).")
}

func (s *Suite) Test_VartypeCheck_Homepage(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtHomepage)

	vt.Varname("HOMEPAGE")
	vt.Values(
		"http://www.pkgsrc.org/",
		"https://www.pkgsrc.org/",
		"${MASTER_SITES}")

	vt.Output(
		"WARN: filename.mk:3: HOMEPAGE should not be defined in terms of MASTER_SITEs.")

	G.Pkg = NewPackage(t.File("category/package"))

	vt.Values(
		"${MASTER_SITES}")

	// When this assignment occurs while checking a package, but the package
	// doesn't define MASTER_SITES, that variable cannot be expanded, which means
	// the warning cannot refer to its value.
	vt.Output(
		"WARN: filename.mk:11: HOMEPAGE should not be defined in terms of MASTER_SITEs.")

	delete(G.Pkg.vars.firstDef, "MASTER_SITES")
	delete(G.Pkg.vars.lastDef, "MASTER_SITES")
	G.Pkg.vars.Define("MASTER_SITES", t.NewMkLine(G.Pkg.File("Makefile"), 5,
		"MASTER_SITES=\thttps://cdn.NetBSD.org/pub/pkgsrc/distfiles/"))

	vt.Values(
		"${MASTER_SITES}")

	vt.Output(
		"WARN: filename.mk:21: HOMEPAGE should not be defined in terms of MASTER_SITEs. " +
			"Use https://cdn.NetBSD.org/pub/pkgsrc/distfiles/ directly.")

	delete(G.Pkg.vars.firstDef, "MASTER_SITES")
	delete(G.Pkg.vars.lastDef, "MASTER_SITES")
	G.Pkg.vars.Define("MASTER_SITES", t.NewMkLine(G.Pkg.File("Makefile"), 5,
		"MASTER_SITES=\t${MASTER_SITE_GITHUB}"))

	vt.Values(
		"${MASTER_SITES}")

	// When MASTER_SITES itself makes use of another variable, pkglint doesn't
	// resolve that variable and just outputs the simple variant of this warning.
	vt.Output(
		"WARN: filename.mk:31: HOMEPAGE should not be defined in terms of MASTER_SITEs.")

	delete(G.Pkg.vars.firstDef, "MASTER_SITES")
	delete(G.Pkg.vars.lastDef, "MASTER_SITES")
	G.Pkg.vars.Define("MASTER_SITES", t.NewMkLine(G.Pkg.File("Makefile"), 5,
		"MASTER_SITES=\t# none"))

	vt.Values(
		"${MASTER_SITES}")

	// When MASTER_SITES is empty, pkglint cannot extract the first of the URLs
	// for using it in the HOMEPAGE.
	vt.Output(
		"WARN: filename.mk:41: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
}

func (s *Suite) Test_VartypeCheck_Identifier(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtIdentifier)

	vt.Varname("MYSQL_CHARSET")
	vt.Values(
		"${OTHER_VAR}",
		"identifiers cannot contain spaces",
		"id/cannot/contain/slashes",
		"id-${OTHER_VAR}",
		"")

	vt.Output(
		"WARN: filename.mk:2: Invalid identifier \"identifiers cannot contain spaces\".",
		"WARN: filename.mk:3: Invalid identifier \"id/cannot/contain/slashes\".",
		"WARN: filename.mk:5: Invalid identifier \"\".")

	vt.Op(opUseMatch)
	vt.Values(
		"[A-Z]",
		"[A-Z.]",
		"${PKG_OPTIONS:Moption}",
		"A*B")

	vt.Output(
		"WARN: filename.mk:12: Invalid identifier pattern \"[A-Z.]\" for MYSQL_CHARSET.")
}

func (s *Suite) Test_VartypeCheck_Integer(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtInteger)

	vt.Varname("MAKE_JOBS")
	vt.Values(
		"${OTHER_VAR}",
		"123",
		"-13",
		"11111111111111111111111111111111111111111111111")

	vt.Output(
		"WARN: filename.mk:1: Invalid integer \"${OTHER_VAR}\".",
		"WARN: filename.mk:3: Invalid integer \"-13\".")
}

func (s *Suite) Test_VartypeCheck_LdFlag(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtLdFlag)

	vt.tester.SetUpTool("pkg-config", "", AtRunTime)

	vt.Varname("LDFLAGS")
	vt.Op(opAssignAppend)
	vt.Values(
		"-lc",
		"-L/usr/lib64",
		"`pkg-config pidgin --ldflags`",
		"-unknown",
		"no-hyphen",
		"-Wl,--rpath,/usr/lib64",
		"-pthread",
		"-static",
		"-static-something",
		"${LDFLAGS.NetBSD}",
		"-l${LIBNCURSES}",
		"`pkg-config`_plus",
		"-DMACRO",
		"-UMACRO",
		"-P",
		"-E",
		"-I${PREFIX}/include")
	vt.Op(opUseMatch)
	vt.Values(
		"anything")

	vt.Output(
		"WARN: filename.mk:6: Please use \"${COMPILER_RPATH_FLAG}\" instead of \"-Wl,--rpath\".",
		"WARN: filename.mk:13: \"-DMACRO\" is a compiler flag "+
			"and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.",
		"WARN: filename.mk:14: \"-UMACRO\" is a compiler flag "+
			"and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.",
		"WARN: filename.mk:15: \"-P\" is a compiler flag "+
			"and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.",
		"WARN: filename.mk:16: \"-E\" is a compiler flag "+
			"and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.",
		"WARN: filename.mk:17: \"-I${PREFIX}/include\" is a compiler flag "+
			"and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.")
}

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

	t.Chdir(".")
	// Adds the gnu-gpl-v2 and 2-clause-bsd licenses
	t.SetUpPackage("category/package")
	t.CreateFileLines("licenses/mit", "...")
	t.FinishSetUp()

	G.Pkg = NewPackage(t.File("category/package"))

	mklines := t.SetUpFileMkLines("perl5.mk",
		MkCvsID,
		"PERL5_LICENSE= gnu-gpl-v2 OR artistic")
	// Also registers the PERL5_LICENSE variable in the package.
	mklines.collectVariables()

	vt := NewVartypeCheckTester(t, BtLicense)

	vt.Varname("LICENSE")
	vt.Values(
		"gnu-gpl-v2",
		"AND mit",
		"${PERL5_LICENSE}", // Is properly resolved, see perl5.mk above.
		"${UNKNOWN_LICENSE}")

	vt.Output(
		"ERROR: filename.mk:2: Parse error for license condition \"AND mit\".",
		"ERROR: filename.mk:3: License file licenses/artistic does not exist.",
		"ERROR: filename.mk:4: Parse error for license condition \"${UNKNOWN_LICENSE}\".")

	vt.Op(opAssignAppend)
	vt.Values(
		"gnu-gpl-v2",
		"AND mit")

	vt.Output(
		"ERROR: filename.mk:11: Parse error for appended license condition \"gnu-gpl-v2\".")
}

func (s *Suite) Test_VartypeCheck_MachineGnuPlatform(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtMachineGnuPlatform)

	vt.Varname("MACHINE_GNU_PLATFORM")
	vt.Op(opUseMatch)
	vt.Values(
		"x86_64-pc-cygwin",
		"Cygwin-*-amd64",
		"x86_64-*",
		"*-*-*-*",
		"${OTHER_VAR}",
		"x86_64-pc") // Just for code coverage.

	vt.Output(
		"WARN: filename.mk:2: The pattern \"Cygwin\" cannot match any of "+
			"{ aarch64 aarch64_be alpha amd64 arc arm armeb armv4 armv4eb armv6 armv6eb armv7 armv7eb "+
			"cobalt convex dreamcast hpcmips hpcsh hppa hppa64 i386 i486 ia64 m5407 m68010 m68k m88k "+
			"mips mips64 mips64el mipseb mipsel mipsn32 mlrisc ns32k pc532 pmax powerpc powerpc64 "+
			"rs6000 s390 sh shle sparc sparc64 vax x86_64 "+
			"} for the hardware architecture part of MACHINE_GNU_PLATFORM.",
		"WARN: filename.mk:2: The pattern \"amd64\" cannot match any of "+
			"{ bitrig bsdos cygwin darwin dragonfly freebsd haiku hpux interix irix linux mirbsd "+
			"netbsd openbsd osf1 solaris sunos } "+
			"for the operating system part of MACHINE_GNU_PLATFORM.",
		"WARN: filename.mk:4: \"*-*-*-*\" is not a valid platform pattern.",
		"WARN: filename.mk:6: \"x86_64-pc\" is not a valid platform pattern.")
}

func (s *Suite) Test_VartypeCheck_MachinePlatform(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtMachinePlatform)

	// There is no need to test the assignment operators since the
	// only variable of this type is read-only.

	vt.Varname("MACHINE_PLATFORM")
	vt.Op(opUseMatch)
	vt.Values(
		"linux-i386",
		"nextbsd-5.0-8087",
		"netbsd-7.0-l*",
		"NetBSD-1.6.2-i386",
		"FreeBSD*",
		"FreeBSD-*",
		"${LINUX}",
		"NetBSD-[0-1]*-*")

	vt.Output(
		"WARN: filename.mk:1: \"linux-i386\" is not a valid platform pattern.",
		"WARN: filename.mk:2: The pattern \"nextbsd\" cannot match any of "+
			"{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
			"IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
			"} for the operating system part of MACHINE_PLATFORM.",
		"WARN: filename.mk:2: The pattern \"8087\" cannot match any of "+
			"{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 "+
			"cobalt coldfire convex dreamcast "+
			"earm earmeb earmhf earmhfeb earmv4 earmv4eb "+
			"earmv5 earmv5eb earmv6 earmv6eb earmv6hf "+
			"earmv6hfeb earmv7 earmv7eb earmv7hf earmv7hfeb evbarm hpcmips hpcsh hppa hppa64 "+
			"i386 i586 i686 ia64 m68000 m68k m88k "+
			"mips mips64 mips64eb mips64el mipseb mipsel mipsn32 "+
			"mlrisc ns32k pc532 pmax powerpc powerpc64 "+
			"rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
			"} for the hardware architecture part of MACHINE_PLATFORM.",
		"WARN: filename.mk:3: The pattern \"netbsd\" cannot match any of "+
			"{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
			"IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
			"} for the operating system part of MACHINE_PLATFORM.",
		"WARN: filename.mk:3: The pattern \"l*\" cannot match any of "+
			"{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 "+
			"cobalt coldfire convex dreamcast "+
			"earm earmeb earmhf earmhfeb earmv4 earmv4eb "+
			"earmv5 earmv5eb earmv6 earmv6eb earmv6hf "+
			"earmv6hfeb earmv7 earmv7eb earmv7hf earmv7hfeb evbarm hpcmips hpcsh hppa hppa64 "+
			"i386 i586 i686 ia64 m68000 m68k m88k "+
			"mips mips64 mips64eb mips64el mipseb mipsel mipsn32 "+
			"mlrisc ns32k pc532 pmax powerpc powerpc64 "+
			"rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
			"} for the hardware architecture part of MACHINE_PLATFORM.",
		"WARN: filename.mk:5: \"FreeBSD*\" is not a valid platform pattern.",
		"WARN: filename.mk:8: Please use \"[0-1].*\" instead of \"[0-1]*\" as the version pattern.")
}

func (s *Suite) Test_VartypeCheck_MachinePlatformPattern(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtMachinePlatformPattern)

	vt.Varname("ONLY_FOR_PLATFORM")
	vt.Op(opUseMatch)
	vt.Values(
		"linux-i386",
		"nextbsd-5.0-8087",
		"netbsd-7.0-l*",
		"NetBSD-1.6.2-i386",
		"FreeBSD*",
		"FreeBSD-*",
		"${LINUX}",
		"NetBSD-[0-1]*-*")

	vt.Output(
		"WARN: filename.mk:1: \"linux-i386\" is not a valid platform pattern.",
		"WARN: filename.mk:2: The pattern \"nextbsd\" cannot match any of "+
			"{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
			"IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
			"} for the operating system part of ONLY_FOR_PLATFORM.",
		"WARN: filename.mk:2: The pattern \"8087\" cannot match any of "+
			"{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 "+
			"cobalt coldfire convex dreamcast "+
			"earm earmeb earmhf earmhfeb earmv4 earmv4eb "+
			"earmv5 earmv5eb earmv6 earmv6eb earmv6hf "+
			"earmv6hfeb earmv7 earmv7eb earmv7hf earmv7hfeb evbarm hpcmips hpcsh hppa hppa64 "+
			"i386 i586 i686 ia64 m68000 m68k m88k "+
			"mips mips64 mips64eb mips64el mipseb mipsel mipsn32 "+
			"mlrisc ns32k pc532 pmax powerpc powerpc64 "+
			"rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
			"} for the hardware architecture part of ONLY_FOR_PLATFORM.",
		"WARN: filename.mk:3: The pattern \"netbsd\" cannot match any of "+
			"{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
			"IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
			"} for the operating system part of ONLY_FOR_PLATFORM.",
		"WARN: filename.mk:3: The pattern \"l*\" cannot match any of "+
			"{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 "+
			"cobalt coldfire convex dreamcast "+
			"earm earmeb earmhf earmhfeb earmv4 earmv4eb "+
			"earmv5 earmv5eb earmv6 earmv6eb earmv6hf "+
			"earmv6hfeb earmv7 earmv7eb earmv7hf earmv7hfeb evbarm hpcmips hpcsh hppa hppa64 "+
			"i386 i586 i686 ia64 m68000 m68k m88k "+
			"mips mips64 mips64eb mips64el mipseb mipsel mipsn32 "+
			"mlrisc ns32k pc532 pmax powerpc powerpc64 "+
			"rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
			"} for the hardware architecture part of ONLY_FOR_PLATFORM.",
		"WARN: filename.mk:5: \"FreeBSD*\" is not a valid platform pattern.",
		"WARN: filename.mk:8: Please use \"[0-1].*\" instead of \"[0-1]*\" as the version pattern.")
}

func (s *Suite) Test_VartypeCheck_MailAddress(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtMailAddress)

	vt.Varname("MAINTAINER")
	vt.Values(
		"pkgsrc-users@netbsd.org",
		"tech-pkg@NetBSD.org",
		"packages@NetBSD.org",
		"user1@example.org,user2@example.org")

	vt.Output(
		"WARN: filename.mk:1: Please write \"NetBSD.org\" instead of \"netbsd.org\".",
		"ERROR: filename.mk:2: This mailing list address is obsolete. Use pkgsrc-users@NetBSD.org instead.",
		"ERROR: filename.mk:3: This mailing list address is obsolete. Use pkgsrc-users@NetBSD.org instead.",
		"WARN: filename.mk:4: \"user1@example.org,user2@example.org\" is not a valid mail address.")
}

func (s *Suite) Test_VartypeCheck_Message(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtMessage)

	vt.Varname("SUBST_MESSAGE.id")
	vt.Values(
		"\"Correct paths\"",
		"Correct paths")

	vt.Output(
		"WARN: filename.mk:1: SUBST_MESSAGE.id should not be quoted.")
}

func (s *Suite) Test_VartypeCheck_Option(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtOption)

	G.Pkgsrc.PkgOptions["documented"] = "Option description"
	G.Pkgsrc.PkgOptions["undocumented"] = ""

	vt.Varname("PKG_OPTIONS.pkgbase")
	vt.Values(
		"documented",
		"undocumented",
		"unknown",
		"underscore_is_deprecated",
		"UPPER")

	vt.Output(
		"WARN: filename.mk:3: Unknown option \"unknown\".",
		"WARN: filename.mk:4: Use of the underscore character in option names is deprecated.",
		"ERROR: filename.mk:5: Invalid option name \"UPPER\". "+
			"Option names must start with a lowercase letter and be all-lowercase.")
}

func (s *Suite) Test_VartypeCheck_Pathlist(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPathlist)

	vt.Varname("PATH")
	vt.Values(
		"/usr/bin:/usr/sbin:.::${LOCALBASE}/bin:${HOMEPAGE:S,https://,,}:${TMPDIR}:${PREFIX}/!!!",
		"/directory with spaces")

	vt.Output(
		"ERROR: filename.mk:1: The component \".\" of PATH must be an absolute path.",
		"ERROR: filename.mk:1: The component \"\" of PATH must be an absolute path.",
		"WARN: filename.mk:1: \"${PREFIX}/!!!\" is not a valid pathname.",
		"WARN: filename.mk:2: \"/directory with spaces\" is not a valid pathname.")
}

func (s *Suite) Test_VartypeCheck_PathPattern(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPathPattern)

	vt.Varname("DISTDIRS")
	vt.Values(
		"/home/user/*",
		"src/*&*",
		"src/*/*")

	vt.Output(
		"WARN: filename.mk:2: The pathname pattern \"src/*&*\" contains the invalid character \"&\".")

	vt.Op(opUseMatch)
	vt.Values("any")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_Pathname(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPathname)

	vt.Varname("EGDIR")
	vt.Values(
		"${PREFIX}/*",
		"${PREFIX}/share/locale",
		"share/locale",
		"/bin")
	vt.Op(opUseMatch)
	vt.Values(
		"anything")

	vt.Output(
		"WARN: filename.mk:1: The pathname \"${PREFIX}/*\" contains the invalid character \"*\".")
}

func (s *Suite) Test_VartypeCheck_Perl5Packlist(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPerl5Packlist)

	vt.Varname("PERL5_PACKLIST")
	vt.Values(
		"${PKGBASE}",
		"anything else")

	vt.Output(
		"WARN: filename.mk:1: PERL5_PACKLIST should not depend on other variables.")
}

func (s *Suite) Test_VartypeCheck_Perms(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPerms)

	vt.Varname("CONF_FILES_PERMS")
	vt.Op(opAssignAppend)
	vt.Values(
		"root",
		"${ROOT_USER}",
		"ROOT_USER",
		"${REAL_ROOT_USER}",
		"${ROOT_GROUP}",
		"${REAL_ROOT_GROUP}")

	vt.Output(
		"ERROR: filename.mk:2: ROOT_USER must not be used in permission definitions. Use REAL_ROOT_USER instead.",
		"ERROR: filename.mk:5: ROOT_GROUP must not be used in permission definitions. Use REAL_ROOT_GROUP instead.")
}

func (s *Suite) Test_VartypeCheck_Pkgname(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPkgname)

	vt.Varname("PKGNAME")
	vt.Values(
		"pkgbase-0",
		"pkgbase-1.0",
		"pkgbase-1.1234567890",
		"pkgbase-1z",
		"pkgbase-client-11a",
		"pkgbase-client-1.a",
		"pkgbase-client-1_20180101",
		"pkgbase-z1",
		"pkgbase-3.1.4.1.5.9.2.6.5.3.5.8.9.7.9")

	vt.Output(
		"WARN: filename.mk:8: \"pkgbase-z1\" is not a valid package name.")

	vt.Values(
		"pkgbase-1.0nb17")

	vt.Output(
		"ERROR: filename.mk:11: The \"nb\" part of the version number belongs in PKGREVISION.")

	vt.Op(opUseMatch)
	vt.Values(
		"pkgbase-[0-9]*")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_PkgOptionsVar(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPkgOptionsVar)

	vt.Varname("PKG_OPTIONS_VAR")
	vt.Values(
		"PKG_OPTIONS.${PKGBASE}",
		"PKG_OPTIONS.anypkgbase",
		"PKG_OPTS.mc")

	vt.Output(
		"ERROR: filename.mk:1: PKGBASE must not be used in PKG_OPTIONS_VAR.",
		"ERROR: filename.mk:3: PKG_OPTIONS_VAR must be "+
			"of the form \"PKG_OPTIONS.*\", not \"PKG_OPTS.mc\".")
}

func (s *Suite) Test_VartypeCheck_Pkgpath(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtPkgpath)

	t.CreateFileLines("category/Makefile")
	t.CreateFileLines("category/other-package/Makefile")
	t.CreateFileLines("wip/package/Makefile")
	t.Chdir("category/package")

	vt.Varname("PKGPATH")
	vt.Values(
		"category/other-package",
		"${OTHER_VAR}",
		"invalid",
		"../../invalid/relative",
		"wip/package",
		"category",
		"&&")

	vt.Output(
		"ERROR: filename.mk:3: There is no package in \"../../invalid\".",
		"ERROR: filename.mk:4: There is no package in \"../../../../invalid/relative\".",
		"ERROR: filename.mk:5: A main pkgsrc package must not depend on a pkgsrc-wip package.",
		"ERROR: filename.mk:6: \"category\" is not a valid path to a package.",
		"WARN: filename.mk:7: The pathname \"&&\" contains the invalid characters \"&&\".",
		"ERROR: filename.mk:7: There is no package in \"../../&&\".")

	G.Wip = true

	vt.Values(
		"wip/package")

	vt.OutputEmpty()

	vt.Op(opUseMatch)

	vt.Values(
		"pattern",
		"&special&")

	vt.Output(
		"WARN: filename.mk:22: The pathname pattern \"&special&\" contains the invalid characters \"&&\".")
}

func (s *Suite) Test_VartypeCheck_Pkgrevision(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPkgrevision)

	vt.Varname("PKGREVISION")
	vt.Values(
		"3a")

	vt.Output(
		"ERROR: filename.mk:1: PKGREVISION must be a positive integer number.",
		"ERROR: filename.mk:1: PKGREVISION only makes sense directly in the package Makefile.")

	vt.File("Makefile")
	vt.Values(
		"3")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_PrefixPathname(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPrefixPathname)

	vt.Varname("PKGMANDIR")
	vt.Values(
		"man/man1",
		"share/locale",
		"/absolute")

	vt.Output(
		"WARN: filename.mk:1: "+
			"Please use \"${PKGMANDIR}/man1\" instead of \"man/man1\".",
		"ERROR: filename.mk:3: The pathname \"/absolute\" in PKGMANDIR "+
			"must be relative to ${PREFIX}.")
}

func (s *Suite) Test_VartypeCheck_PythonDependency(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtPythonDependency)

	vt.Varname("PYTHON_VERSIONED_DEPENDENCIES")
	vt.Values(
		"cairo",
		"${PYDEP}",
		"cairo,X")

	vt.Output(
		"WARN: filename.mk:2: Python dependencies should not contain variables.",
		"WARN: filename.mk:3: Invalid Python dependency \"cairo,X\".")
}

func (s *Suite) Test_VartypeCheck_RPkgName(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtRPkgName)

	vt.Varname("R_PKGNAME")
	vt.Values(
		"package",
		"${VAR}",
		"a,b,c",
		"under_score",
		"R-package")

	vt.Output(
		"WARN: filename.mk:2: The R package name should not contain variables.",
		"WARN: filename.mk:3: The R package name contains the invalid characters \",,\".",
		"WARN: filename.mk:5: The R_PKGNAME does not need the \"R-\" prefix.")

	vt.Op(opUseMatch)
	vt.Values(
		"R-package")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_RPkgVer(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtRPkgVer)

	vt.Varname("R_PKGVER")
	vt.Values(
		"1.0",
		"1-2-3",
		"${VERSION}",
		"1-:")

	vt.Output(
		"WARN: filename.mk:4: Invalid R version number \"1-:\".")

	vt.Op(opUseMatch)
	vt.Values(
		"1-:")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_RelativePkgDir(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtRelativePkgDir)

	t.CreateFileLines("category/other-package/Makefile")
	t.Chdir("category/package")

	vt.Varname("PKGDIR")
	vt.Values(
		"category/other-package",
		"../../category/other-package",
		"${OTHER_VAR}",
		"invalid",
		"../../invalid/relative",
		"/absolute")

	vt.Output(
		"ERROR: filename.mk:1: Relative path \"category/other-package/Makefile\" does not exist.",
		"WARN: filename.mk:1: \"category/other-package\" is not a valid relative package directory.",
		"ERROR: filename.mk:4: Relative path \"invalid/Makefile\" does not exist.",
		"WARN: filename.mk:4: \"invalid\" is not a valid relative package directory.",
		"ERROR: filename.mk:5: Relative path \"../../invalid/relative/Makefile\" does not exist.",
		"ERROR: filename.mk:6: The path \"/absolute\" must be relative.")
}

func (s *Suite) Test_VartypeCheck_RelativePkgPath(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtRelativePkgPath)

	t.CreateFileLines("category/other-package/Makefile")
	t.Chdir("category/package")

	vt.Varname("DISTINFO_FILE")
	vt.Values(
		"category/other-package",
		"../../category/other-package",
		"${OTHER_VAR}",
		"invalid",
		"../../invalid/relative",
		"/absolute")

	vt.Output(
		"ERROR: filename.mk:1: Relative path \"category/other-package\" does not exist.",
		"ERROR: filename.mk:4: Relative path \"invalid\" does not exist.",
		"ERROR: filename.mk:5: Relative path \"../../invalid/relative\" does not exist.",
		"ERROR: filename.mk:6: The path \"/absolute\" must be relative.")
}

func (s *Suite) Test_VartypeCheck_Restricted(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtRestricted)

	vt.Varname("NO_BIN_ON_CDROM")
	vt.Values(
		"May only be distributed free of charge")

	vt.Output(
		"WARN: filename.mk:1: The only valid value for NO_BIN_ON_CDROM is ${RESTRICTED}.")
}

func (s *Suite) Test_VartypeCheck_SedCommands(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtSedCommands)

	vt.Varname("SUBST_SED.dummy")
	vt.Values(
		"s,@COMPILER@,gcc,g",
		"-e s,a,b, -e a,b,c,",
		"-e \"s,#,comment ,\"",
		"-e \"s,\\#,comment ,\"",
		"-E",
		"-n",
		"-e 1d",
		"1d",
		"-e",
		"-i s,from,to,",
		"-e s,$${unclosedShellVar") // Just for code coverage.

	vt.Output(
		"NOTE: filename.mk:1: Please always use \"-e\" in sed commands, even if there is only one substitution.",
		"WARN: filename.mk:2: Each sed command should appear in an assignment of its own.",
		"WARN: filename.mk:3: The # character starts a Makefile comment.",
		"ERROR: filename.mk:3: Invalid shell words \"\\\"s,\" in sed commands.",
		"WARN: filename.mk:8: Unknown sed command \"1d\".",
		"ERROR: filename.mk:9: The -e option to sed requires an argument.",
		"WARN: filename.mk:10: Unknown sed command \"-i\".",
		"NOTE: filename.mk:10: Please always use \"-e\" in sed commands, even if there is only one substitution.",
		// TODO: duplicate warning
		"WARN: filename.mk:11: Unclosed shell variable starting at \"$${unclosedShellVar\".",
		"WARN: filename.mk:11: Unclosed shell variable starting at \"$${unclosedShellVar\".")
}

func (s *Suite) Test_VartypeCheck_SedCommands__experimental(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtSedCommands)
	G.Experimental = true

	vt.Varname("SUBST_SED.dummy")

	vt.Values(
		"-e s,???,questions,",
		"-e 's?from?to?g'",
		"-E -e 's,from,to,g'")

	vt.Output(
		"WARN: filename.mk:1: The \"?\" in the word \"s,???,questions,\" may lead to unintended file globbing.")

	vt.Values(
		"-e s,?,replacement,",
		"-e s,\\?,replacement,",
		"-e s,\\\\?,replacement,",
		"-e s,\\\\\\?,replacement,")

	vt.Output(
		"WARN: filename.mk:11: The \"?\" in the word \"s,?,replacement,\" may lead to unintended file globbing.",
		"WARN: filename.mk:13: The \"?\" in the word \"s,\\\\\\\\?,replacement,\" may lead to unintended file globbing.",
		"WARN: filename.mk:13: In a basic regular expression, a backslash followed by \"?\" is undefined.",
		"WARN: filename.mk:14: In a basic regular expression, a backslash followed by \"?\" is undefined.")

	vt.Values(
		"-e s/dir\\\\/file/other-file/")

	// No warning about backslash followed by "/" being undefined.
	vt.OutputEmpty()

	vt.Values(
		"-e 's, ,space,g'",
		"-e 's,\t,tab,g'")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_ShellCommand(c *check.C) {
	t := s.Init(c)
	t.SetUpVartypes()
	vt := NewVartypeCheckTester(t, BtShellCommand)

	vt.Varname("INSTALL_CMD")
	vt.Values(
		"${INSTALL_DATA} -m 0644 ${WRKDIR}/source ${DESTDIR}${PREFIX}/target")

	vt.Op(opUseMatch)
	vt.Values("*")

	vt.OutputEmpty()

	vt.Varname("CC")
	vt.Op(opAssignAppend)
	vt.Values("-ggdb")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_ShellCommands(c *check.C) {
	t := s.Init(c)
	t.SetUpVartypes()
	t.SetUpTool("echo", "ECHO", AtRunTime)
	vt := NewVartypeCheckTester(t, BtShellCommands)

	vt.Varname("GENERATE_PLIST")
	vt.Values(
		"echo bin/program",
		"echo bin/program;")

	vt.Output(
		"WARN: filename.mk:1: This shell command list should end with a semicolon.")
}

func (s *Suite) Test_VartypeCheck_ShellWord(c *check.C) {
	t := s.Init(c)
	t.SetUpVartypes()
	vt := NewVartypeCheckTester(t, BtShellWord)

	vt.Varname("PKG_FAIL_REASON")
	vt.Values(
		"The package does not work here.",
		"\"Properly quoted reason.\"")

	// At this level, there can be no warning for line 1 since each word
	// is analyzed on its own.
	//
	// See Test_MkLineChecker_checkVartype__one_per_line.
	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_Stage(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtStage)

	vt.Varname("SUBST_STAGE.dummy")
	vt.Values(
		"post-patch",
		"post-modern",
		"pre-test")

	vt.Output(
		"WARN: filename.mk:2: Invalid stage name \"post-modern\". " +
			"Use one of {pre,do,post}-{extract,patch,configure,build,test,install}.")
}

func (s *Suite) Test_VartypeCheck_Tool(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtTool)

	t.SetUpTool("tool1", "", AtRunTime)
	t.SetUpTool("tool2", "", AtRunTime)
	t.SetUpTool("tool3", "", AtRunTime)

	vt.Varname("USE_TOOLS")
	vt.Op(opAssignAppend)
	vt.Values(
		"tool3:run",
		"tool2:unknown",
		"${t}",
		"mal:formed:tool",
		"unknown")

	vt.Output(
		"ERROR: filename.mk:2: Invalid tool dependency \"unknown\". "+
			"Use one of \"bootstrap\", \"build\", \"pkgsrc\", \"run\" or \"test\".",
		"ERROR: filename.mk:4: Invalid tool dependency \"mal:formed:tool\".",
		"ERROR: filename.mk:5: Unknown tool \"unknown\".")

	vt.Varname("USE_TOOLS.NetBSD")
	vt.Op(opAssignAppend)
	vt.Values(
		"tool3:run",
		"tool2:unknown")

	vt.Output(
		"ERROR: filename.mk:12: Invalid tool dependency \"unknown\". " +
			"Use one of \"bootstrap\", \"build\", \"pkgsrc\", \"run\" or \"test\".")

	vt.Varname("TOOLS_NOOP")
	vt.Op(opAssignAppend)
	vt.Values(
		"gmake:run")

	vt.Varname("TOOLS_NOOP")
	vt.Op(opAssign) // TODO: In a Makefile, this should be equivalent to opAssignAppend.
	vt.Values(
		"gmake:run")

	vt.Output(
		"ERROR: filename.mk:31: Unknown tool \"gmake\".")

	vt.Varname("USE_TOOLS")
	vt.Op(opUseMatch)
	vt.Values(
		"tool1",
		"tool1\\:build",
		"tool1\\:*",
		"${t}\\:build")

	vt.OutputEmpty()

	vt.Op(opAssignAppend)
	vt.Values(
		"tool1:bootstrap",
		"tool1:build",
		"tool1:pkgsrc",
		"tool1:run",
		"tool1:test")

	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_Unknown(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtUnknown)

	vt.Varname("BDB185_DEFAULT")
	vt.Values(
		"# empty",
		"Something",
		"'quotes are ok'",
		"!\"#$%&/()*+,-./0-9:;<=>?@A-Z[\\]^_a-z{|}~")

	// This warning is produced as a side effect of parsing the lines.
	// It is not specific to the BtUnknown type.
	vt.Output(
		"WARN: filename.mk:4: The # character starts a Makefile comment.")
}

func (s *Suite) Test_VartypeCheck_URL(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtURL)

	vt.Varname("LATEX2HTML_ICONPATH")
	vt.Values(
		"# none",
		"${OTHER_VAR}",
		"https://www.NetBSD.org/",
		"https://www.netbsd.org/",
		"https://www.example.org",
		"ftp://example.org/pub/",
		"gopher://example.org/")

	vt.Output(
		"WARN: filename.mk:4: Please write NetBSD.org instead of www.netbsd.org.",
		"NOTE: filename.mk:5: For consistency, please add a trailing slash to \"https://www.example.org\".")

	vt.Values(
		"",
		"ftp://example.org/<",
		"gopher://example.org/<",
		"http://example.org/<",
		"https://example.org/<",
		"https://www.example.org/path with spaces",
		"httpxs://www.example.org",
		"mailto:someone@example.org",
		"string with spaces")

	vt.Output(
		"WARN: filename.mk:11: \"\" is not a valid URL.",
		"WARN: filename.mk:12: \"ftp://example.org/<\" is not a valid URL.",
		"WARN: filename.mk:13: \"gopher://example.org/<\" is not a valid URL.",
		"WARN: filename.mk:14: \"http://example.org/<\" is not a valid URL.",
		"WARN: filename.mk:15: \"https://example.org/<\" is not a valid URL.",
		"WARN: filename.mk:16: \"https://www.example.org/path with spaces\" is not a valid URL.",
		"WARN: filename.mk:17: \"httpxs://www.example.org\" is not a valid URL. Only ftp, gopher, http, and https URLs are allowed here.",
		"WARN: filename.mk:18: \"mailto:someone@example.org\" is not a valid URL.",
		"WARN: filename.mk:19: \"string with spaces\" is not a valid URL.")

	// Yes, even in 2019, some pkgsrc-wip packages really use a gopher HOMEPAGE.
	vt.Values(
		"gopher://bitreich.org/1/scm/geomyidae")
	vt.OutputEmpty()
}

func (s *Suite) Test_VartypeCheck_UserGroupName(c *check.C) {
	t := s.Init(c)
	vt := NewVartypeCheckTester(t, BtUserGroupName)

	vt.Varname("APACHE_USER")
	vt.Values(
		"user with spaces",
		"typical_username",
		"user123",
		"domain\\user",
		"${OTHER_VAR}")

	vt.Output(
		"WARN: filename.mk:1: Invalid user or group name \"user with spaces\".",
		"WARN: filename.mk:4: Invalid user or group name \"domain\\\\user\".")
}

func (s *Suite) Test_VartypeCheck_VariableName(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtVariableName)

	vt.Varname("BUILD_DEFS")
	vt.Values(
		"VARBASE",
		"VarBase",
		"PKG_OPTIONS_VAR.pkgbase",
		"${INDIRECT}")

	vt.Output(
		"WARN: filename.mk:2: \"VarBase\" is not a valid variable name.")
}

func (s *Suite) Test_VartypeCheck_VariableNamePattern(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtVariableNamePattern)

	vt.Varname("_SORTED_VARS.group")
	vt.Values(
		"VARBASE",
		"VarBase",
		"PKG_OPTIONS_VAR.pkgbase",
		"${INDIRECT}",
		"*_DIRS",
		"VAR.*",
		"***")

	vt.Output(
		"WARN: filename.mk:2: \"VarBase\" is not a valid variable name pattern.")
}

func (s *Suite) Test_VartypeCheck_Version(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtVersion)

	vt.Varname("PERL5_REQD")
	vt.Op(opAssignAppend)
	vt.Values(
		"0",
		"1.2.3.4.5.6",
		"4.1nb17",
		"4.1-SNAPSHOT",
		"4pre7",
		"${VER}")
	vt.Output(
		"WARN: filename.mk:4: Invalid version number \"4.1-SNAPSHOT\".")

	vt.Op(opUseMatch)
	vt.Values(
		"a*",
		"1.2/456",
		"4*",
		"?.??",
		"1.[234]*",
		"1.[2-7].*",
		"[0-9]*")
	vt.Output(
		"WARN: filename.mk:11: Invalid version number pattern \"a*\".",
		"WARN: filename.mk:12: Invalid version number pattern \"1.2/456\".",
		"WARN: filename.mk:13: Please use \"4.*\" instead of \"4*\" as the version pattern.",
		"WARN: filename.mk:15: Please use \"1.[234].*\" instead of \"1.[234]*\" as the version pattern.")
}

func (s *Suite) Test_VartypeCheck_WrapperReorder(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtWrapperReorder)

	vt.Varname("WRAPPER_REORDER_CMDS")
	vt.Op(opAssignAppend)
	vt.Values(
		"reorder:l:first:second",
		"reorder:l:first",
		"omit:first")
	vt.Output(
		"WARN: filename.mk:2: Unknown wrapper reorder command \"reorder:l:first\".",
		"WARN: filename.mk:3: Unknown wrapper reorder command \"omit:first\".")
}

func (s *Suite) Test_VartypeCheck_WrapperTransform(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtWrapperTransform)

	vt.Varname("WRAPPER_TRANSFORM_CMDS")
	vt.Op(opAssignAppend)
	vt.Values(
		"rm:-O3",
		"opt:-option",
		"rename:src:dst",
		"rm-optarg:-option",
		"rmdir:/usr/include",
		"rpath:/usr/lib:/usr/pkg/lib",
		"rpath:/usr/lib",
		"unknown",
		"-e 's,-Wall,-Wall -Wextra,'")
	vt.Output(
		"WARN: filename.mk:7: Unknown wrapper transform command \"rpath:/usr/lib\".",
		"WARN: filename.mk:8: Unknown wrapper transform command \"unknown\".")
}

func (s *Suite) Test_VartypeCheck_WrkdirSubdirectory(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtWrkdirSubdirectory)

	vt.Varname("WRKSRC")
	vt.Op(opAssign)
	vt.Values(
		"${WRKDIR}",
		"${WRKDIR}/",
		"${WRKDIR}/.",
		"${WRKDIR}/subdir",
		".",
		"${DISTNAME}",
		"${PKGNAME_NOREV}",
		"two words",
		"../other",
		"${WRKSRC}", // Recursive definition.
		"${PKGDIR}/files")

	// XXX: Many more consistency checks are possible here.
	vt.Output(
		"WARN: filename.mk:8: The pathname \"two words\" " +
			"contains the invalid character \" \".")
}

func (s *Suite) Test_VartypeCheck_WrksrcSubdirectory(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtWrksrcSubdirectory)

	vt.Varname("BUILD_DIRS")
	vt.Op(opAssignAppend)
	vt.Values(
		"${WRKSRC}",
		"${WRKSRC}/",
		"${WRKSRC}/.",
		"${WRKSRC}/subdir",
		"${CONFIGURE_DIRS}",
		"${WRKSRC}/directory with spaces", // This is a list of 3 directories.
		"directory with spaces",           // This is a list of 3 directories.
		"../other",
		"${WRKDIR}/sub",
		"${SRCDIR}/sub")
	vt.Output(
		"NOTE: filename.mk:1: You can use \".\" instead of \"${WRKSRC}\".",
		"NOTE: filename.mk:2: You can use \".\" instead of \"${WRKSRC}/\".",
		"NOTE: filename.mk:3: You can use \".\" instead of \"${WRKSRC}/.\".",
		"NOTE: filename.mk:4: You can use \"subdir\" instead of \"${WRKSRC}/subdir\".",
		"NOTE: filename.mk:6: You can use \"directory\" instead of \"${WRKSRC}/directory\".",
		"WARN: filename.mk:8: \"../other\" is not a valid subdirectory of ${WRKSRC}.",
		"WARN: filename.mk:9: \"${WRKDIR}/sub\" is not a valid subdirectory of ${WRKSRC}.",
		"WARN: filename.mk:10: \"${SRCDIR}/sub\" is not a valid subdirectory of ${WRKSRC}.")
}

func (s *Suite) Test_VartypeCheck_Yes(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtYes)

	vt.Varname("APACHE_MODULE")
	vt.Values(
		"yes",
		"no",
		"${YESVAR}")

	vt.Output(
		"WARN: filename.mk:2: APACHE_MODULE should be set to YES or yes.",
		"WARN: filename.mk:3: APACHE_MODULE should be set to YES or yes.")

	vt.Varname("BUILD_USES_MSGFMT")
	vt.Op(opUseMatch)
	vt.Values(
		"yes",
		"no",
		"${YESVAR}")

	vt.Output(
		"WARN: filename.mk:11: BUILD_USES_MSGFMT should only be used in a \".if defined(...)\" condition.",
		"WARN: filename.mk:12: BUILD_USES_MSGFMT should only be used in a \".if defined(...)\" condition.",
		"WARN: filename.mk:13: BUILD_USES_MSGFMT should only be used in a \".if defined(...)\" condition.")
}

func (s *Suite) Test_VartypeCheck_YesNo(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtYesNo)

	vt.Varname("PKG_DEVELOPER")
	vt.Values(
		"yes",
		"no",
		"ja",
		"${YESVAR}")

	vt.Output(
		"WARN: filename.mk:3: PKG_DEVELOPER should be set to YES, yes, NO, or no.",
		"WARN: filename.mk:4: PKG_DEVELOPER should be set to YES, yes, NO, or no.")

	vt.Op(opUseMatch)
	vt.Values(
		"yes",
		"[Yy]es",
		"[Yy][Ee][Ss]",
		"[yY][eE][sS]",
		"[Nn]o",
		"[Nn][Oo]",
		"[nN][oO]")

	vt.Output(
		"WARN: filename.mk:11: PKG_DEVELOPER should be matched against "+
			"\"[yY][eE][sS]\" or \"[nN][oO]\", not \"yes\".",
		"WARN: filename.mk:12: PKG_DEVELOPER should be matched against "+
			"\"[yY][eE][sS]\" or \"[nN][oO]\", not \"[Yy]es\".",
		"WARN: filename.mk:15: PKG_DEVELOPER should be matched against "+
			"\"[yY][eE][sS]\" or \"[nN][oO]\", not \"[Nn]o\".")
}

func (s *Suite) Test_VartypeCheck_YesNoIndirectly(c *check.C) {
	vt := NewVartypeCheckTester(s.Init(c), BtYesNoIndirectly)

	vt.Varname("IS_BUILTIN.pkgbase")
	vt.Values(
		"yes",
		"no",
		"ja",
		"${YESVAR}")

	vt.Output(
		"WARN: filename.mk:3: IS_BUILTIN.pkgbase should be set to YES, yes, NO, or no.")
}

// VartypeCheckTester helps to test the many different checks in VartypeCheck.
// It keeps track of the current variable, operator, filename, line number,
// so that the test can focus on the interesting details.
type VartypeCheckTester struct {
	tester    *Tester
	basicType *BasicType
	filename  CurrPath
	lineno    int
	varname   string
	op        MkOperator
}

// NewVartypeCheckTester starts the test with a filename of "filename", at line 1,
// with "=" as the operator. The variable has to be initialized explicitly.
func NewVartypeCheckTester(t *Tester, basicType *BasicType) *VartypeCheckTester {

	// This is necessary to know whether the variable name is a list type
	// since in such a case each value is split into the list elements.
	if G.Pkgsrc.VariableType(nil, "USE_CWRAPPERS") == nil {
		t.SetUpVartypes()
	}

	return &VartypeCheckTester{t, basicType, "filename.mk", 1, "", opAssign}
}

func (vt *VartypeCheckTester) Varname(varname string) {
	vartype := G.Pkgsrc.VariableType(nil, varname)
	assertNotNil(vartype)
	assert(vartype.basicType == vt.basicType)

	vt.varname = varname
	vt.nextSection()
}

func (vt *VartypeCheckTester) File(filename CurrPath) {
	vt.filename = filename
	vt.lineno = 1
}

// Op sets the operator for the following tests.
// The line number is advanced to the next number ending in 1, e.g. 11, 21, 31.
func (vt *VartypeCheckTester) Op(op MkOperator) {
	vt.op = op
	vt.nextSection()
}

// Values feeds each of the values to the actual check.
// Each value is interpreted as if it were written verbatim into a Makefile line.
// That is, # starts a comment.
//
// For the opUseMatch operator, all colons and closing braces must be escaped.
func (vt *VartypeCheckTester) Values(values ...string) {

	toText := func(value string) string {
		op := vt.op
		opStr := op.String()
		varname := vt.varname

		if op == opUseMatch {
			return sprintf(".if ${%s:M%s} == \"\"", varname, value)
		}

		if !contains(opStr, "=") {
			panic("Invalid operator: " + opStr)
		}

		space := condStr(hasSuffix(varname, "+") && opStr == "=", " ", "")
		return varname + space + opStr + value
	}

	test := func(mklines *MkLines, mkline *MkLine, value string) {
		varname := vt.varname
		comment := condStr(mkline.HasComment(), "#", "") + mkline.Comment()
		if mkline.IsVarassign() {
			_ = mkline.Tokenize(value, true) // Produce some warnings as side-effects.
		}

		effectiveValue := value
		if mkline.IsVarassign() {
			effectiveValue = mkline.Value()
		}

		vartype := G.Pkgsrc.VariableType(nil, varname)

		// See MkLineChecker.checkVartype.
		var lineValues []string
		if vartype == nil || !vartype.IsList() {
			lineValues = []string{effectiveValue}
		} else {
			lineValues = mkline.ValueFields(effectiveValue)
		}

		for _, lineValue := range lineValues {
			valueNovar := mkline.WithoutMakeVariables(lineValue)
			vc := VartypeCheck{mklines, mkline, varname, vt.op, lineValue, valueNovar, comment, false}
			vt.basicType.checker(&vc)
		}
	}

	for _, value := range values {
		text := toText(value)

		line := vt.tester.NewLine(vt.filename, vt.lineno, text)
		mklines := NewMkLines(NewLines(vt.filename, []*Line{line}))
		vt.lineno++

		mklines.ForEach(func(mkline *MkLine) { test(mklines, mkline, value) })
	}
}

// Output checks that the output from all previous steps is
// the same as the expectedLines.
func (vt *VartypeCheckTester) Output(expectedLines ...string) {
	vt.tester.CheckOutputLines(expectedLines...)
	vt.nextSection()
}

func (vt *VartypeCheckTester) OutputEmpty() {
	vt.tester.CheckOutputEmpty()
	vt.nextSection()
}

func (vt *VartypeCheckTester) nextSection() {
	if vt.lineno%10 != 1 {
		vt.lineno += 9 - (vt.lineno+8)%10
	}
}