pkgsrc-Changes archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

CVS commit: pkgsrc/pkgtools/pkglint



Module Name:    pkgsrc
Committed By:   rillig
Date:           Sat Apr 20 17:43:25 UTC 2019

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile
        pkgsrc/pkgtools/pkglint/files: alternatives.go alternatives_test.go
            autofix.go autofix_test.go buildlink3.go buildlink3_test.go
            category.go category_test.go check_test.go distinfo.go
            distinfo_test.go files_test.go licenses.go licenses_test.go
            linechecker.go linelexer.go lines_test.go logging_test.go mkline.go
            mkline_test.go mklinechecker.go mklinechecker_test.go mklines.go
            mklines_test.go mklines_varalign_test.go mkparser.go
            mkparser_test.go mkshwalker.go mkshwalker_test.go mktypes.go
            mktypes_test.go options.go options_test.go package.go
            package_test.go patches.go pkglint.go pkglint_test.go pkgsrc.go
            pkgsrc_test.go plist.go plist_test.go redundantscope.go
            redundantscope_test.go shell.go shell_test.go shtokenizer.go
            shtokenizer_test.go shtypes.go substcontext.go substcontext_test.go
            testnames_test.go tools_test.go util.go util_test.go vardefs.go
            vardefs_test.go vartype.go vartype_test.go vartypecheck.go
            vartypecheck_test.go
        pkgsrc/pkgtools/pkglint/files/trace: tracing.go

Log Message:
pkgtools/pkglint: update to 5.7.5

Changes since 5.7.4:

* Warn about invalid variable uses in directives like
  .if and .for

* Do not warn when a package-settable variable is assigned using the ?=
  operator before including bsd.prefs.mk. This warning only makes sense
  for user-settable and system-provided variables.

* The parser for variable uses like ${VAR:@v@${v:Q}} is more robust now,
  which reduces the number of parse errors and leads to more appropriate
  diagnostics, in cases like ${URL:Mftp://*}, which should really be
  ${URL:Mftp\://*}.

* The valid values for OPSYS are now determined by the files in
  mk/platform instead of allowing arbitrary identifiers. This catches a
  few instances where "Solaris" is used instead of the correct "SunOS".

* Setting USE_LANGUAGES only has an effect if mk/compiler.mk has not yet
  been included. In all other cases, pkglint warns now.

* Missing entries in doc/CHANGES produce a note now. This will lead to
  more accurate statistics for the release notes.


To generate a diff of this commit:
cvs rdiff -u -r1.574 -r1.575 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.10 -r1.11 pkgsrc/pkgtools/pkglint/files/alternatives.go
cvs rdiff -u -r1.13 -r1.14 pkgsrc/pkgtools/pkglint/files/alternatives_test.go \
    pkgsrc/pkgtools/pkglint/files/linechecker.go \
    pkgsrc/pkgtools/pkglint/files/logging_test.go \
    pkgsrc/pkgtools/pkglint/files/mktypes.go \
    pkgsrc/pkgtools/pkglint/files/options.go \
    pkgsrc/pkgtools/pkglint/files/tools_test.go
cvs rdiff -u -r1.19 -r1.20 pkgsrc/pkgtools/pkglint/files/autofix.go \
    pkgsrc/pkgtools/pkglint/files/autofix_test.go
cvs rdiff -u -r1.20 -r1.21 pkgsrc/pkgtools/pkglint/files/buildlink3.go \
    pkgsrc/pkgtools/pkglint/files/category_test.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
cvs rdiff -u -r1.29 -r1.30 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go \
    pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go \
    pkgsrc/pkgtools/pkglint/files/patches.go \
    pkgsrc/pkgtools/pkglint/files/vartype.go
cvs rdiff -u -r1.18 -r1.19 pkgsrc/pkgtools/pkglint/files/category.go
cvs rdiff -u -r1.37 -r1.38 pkgsrc/pkgtools/pkglint/files/check_test.go \
    pkgsrc/pkgtools/pkglint/files/pkglint_test.go \
    pkgsrc/pkgtools/pkglint/files/shell.go
cvs rdiff -u -r1.30 -r1.31 pkgsrc/pkgtools/pkglint/files/distinfo.go
cvs rdiff -u -r1.27 -r1.28 pkgsrc/pkgtools/pkglint/files/distinfo_test.go
cvs rdiff -u -r1.23 -r1.24 pkgsrc/pkgtools/pkglint/files/files_test.go \
    pkgsrc/pkgtools/pkglint/files/substcontext_test.go
cvs rdiff -u -r1.22 -r1.23 pkgsrc/pkgtools/pkglint/files/licenses.go \
    pkgsrc/pkgtools/pkglint/files/licenses_test.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc.go \
    pkgsrc/pkgtools/pkglint/files/substcontext.go
cvs rdiff -u -r1.2 -r1.3 pkgsrc/pkgtools/pkglint/files/linelexer.go \
    pkgsrc/pkgtools/pkglint/files/redundantscope_test.go
cvs rdiff -u -r1.8 -r1.9 pkgsrc/pkgtools/pkglint/files/lines_test.go
cvs rdiff -u -r1.50 -r1.51 pkgsrc/pkgtools/pkglint/files/mkline.go \
    pkgsrc/pkgtools/pkglint/files/pkglint.go
cvs rdiff -u -r1.55 -r1.56 pkgsrc/pkgtools/pkglint/files/mkline_test.go
cvs rdiff -u -r1.33 -r1.34 pkgsrc/pkgtools/pkglint/files/mklinechecker.go
cvs rdiff -u -r1.45 -r1.46 pkgsrc/pkgtools/pkglint/files/mklines.go \
    pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
cvs rdiff -u -r1.40 -r1.41 pkgsrc/pkgtools/pkglint/files/mklines_test.go
cvs rdiff -u -r1.9 -r1.10 \
    pkgsrc/pkgtools/pkglint/files/mklines_varalign_test.go \
    pkgsrc/pkgtools/pkglint/files/mktypes_test.go
cvs rdiff -u -r1.26 -r1.27 pkgsrc/pkgtools/pkglint/files/mkparser.go \
    pkgsrc/pkgtools/pkglint/files/mkparser_test.go \
    pkgsrc/pkgtools/pkglint/files/util_test.go
cvs rdiff -u -r1.7 -r1.8 pkgsrc/pkgtools/pkglint/files/mkshwalker.go \
    pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go
cvs rdiff -u -r1.11 -r1.12 pkgsrc/pkgtools/pkglint/files/options_test.go
cvs rdiff -u -r1.49 -r1.50 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.42 -r1.43 pkgsrc/pkgtools/pkglint/files/package_test.go
cvs rdiff -u -r1.39 -r1.40 pkgsrc/pkgtools/pkglint/files/plist.go
cvs rdiff -u -r1.35 -r1.36 pkgsrc/pkgtools/pkglint/files/plist_test.go
cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/redundantscope.go
cvs rdiff -u -r1.43 -r1.44 pkgsrc/pkgtools/pkglint/files/shell_test.go
cvs rdiff -u -r1.15 -r1.16 pkgsrc/pkgtools/pkglint/files/shtokenizer.go
cvs rdiff -u -r1.14 -r1.15 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go \
    pkgsrc/pkgtools/pkglint/files/shtypes.go
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/testnames_test.go
cvs rdiff -u -r1.41 -r1.42 pkgsrc/pkgtools/pkglint/files/util.go
cvs rdiff -u -r1.58 -r1.59 pkgsrc/pkgtools/pkglint/files/vardefs.go
cvs rdiff -u -r1.12 -r1.13 pkgsrc/pkgtools/pkglint/files/vardefs_test.go
cvs rdiff -u -r1.17 -r1.18 pkgsrc/pkgtools/pkglint/files/vartype_test.go
cvs rdiff -u -r1.53 -r1.54 pkgsrc/pkgtools/pkglint/files/vartypecheck.go
cvs rdiff -u -r1.7 -r1.8 pkgsrc/pkgtools/pkglint/files/trace/tracing.go

Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.

Modified files:

Index: pkgsrc/pkgtools/pkglint/Makefile
diff -u pkgsrc/pkgtools/pkglint/Makefile:1.574 pkgsrc/pkgtools/pkglint/Makefile:1.575
--- pkgsrc/pkgtools/pkglint/Makefile:1.574      Tue Apr 16 18:41:18 2019
+++ pkgsrc/pkgtools/pkglint/Makefile    Sat Apr 20 17:43:24 2019
@@ -1,7 +1,6 @@
-# $NetBSD: Makefile,v 1.574 2019/04/16 18:41:18 bsiegert Exp $
+# $NetBSD: Makefile,v 1.575 2019/04/20 17:43:24 rillig Exp $
 
-PKGNAME=       pkglint-5.7.4
-PKGREVISION=   1
+PKGNAME=       pkglint-5.7.5
 CATEGORIES=    pkgtools
 DISTNAME=      tools
 MASTER_SITES=  ${MASTER_SITE_GITHUB:=golang/}

Index: pkgsrc/pkgtools/pkglint/files/alternatives.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives.go:1.10 pkgsrc/pkgtools/pkglint/files/alternatives.go:1.11
--- pkgsrc/pkgtools/pkglint/files/alternatives.go:1.10  Fri Dec 21 19:46:48 2018
+++ pkgsrc/pkgtools/pkglint/files/alternatives.go       Sat Apr 20 17:43:24 2019
@@ -48,7 +48,7 @@ func CheckFileAlternatives(filename stri
                m, wrapper, space, alternative := match3(line.Text, `^([^\t ]+)([ \t]+)([^\t ]+)`)
                if !m {
                        line.Errorf("Invalid line %q.", line.Text)
-                       G.Explain(
+                       line.Explain(
                                sprintf("Run %q for more information.", makeHelp("alternatives")))
                        continue
                }

Index: pkgsrc/pkgtools/pkglint/files/alternatives_test.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.13 pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.14
--- pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.13     Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/alternatives_test.go  Sat Apr 20 17:43:24 2019
@@ -21,6 +21,7 @@ func (s *Suite) Test_CheckFileAlternativ
                "bin/echo",
                "bin/vim",
                "sbin/sendmail.exim${EXIMVER}")
+       t.FinishSetUp()
 
        G.Check(".")
 
@@ -73,7 +74,7 @@ func (s *Suite) Test_CheckFileAlternativ
        t.CreateFileLines("category/package/ALTERNATIVES",
                "bin/pgm @PREFIX@/bin/gnu-program",
                "bin/pgm @PREFIX@/bin/nb-program")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
Index: pkgsrc/pkgtools/pkglint/files/linechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/linechecker.go:1.13 pkgsrc/pkgtools/pkglint/files/linechecker.go:1.14
--- pkgsrc/pkgtools/pkglint/files/linechecker.go:1.13   Sat Jan 26 16:31:33 2019
+++ pkgsrc/pkgtools/pkglint/files/linechecker.go        Sat Apr 20 17:43:24 2019
@@ -18,7 +18,7 @@ func (ck LineChecker) CheckLength(maxLen
        for i := 0; i < len(prefix); i++ {
                if isHspace(prefix[i]) {
                        ck.line.Warnf("Line too long (should be no more than %d characters).", maxLength)
-                       G.Explain(
+                       ck.line.Explain(
                                "Back in the old time, terminals with 80x25 characters were common.",
                                "And this is still the default size of many terminal emulators.",
                                "Moderately short lines also make reading easier.")
Index: pkgsrc/pkgtools/pkglint/files/logging_test.go
diff -u pkgsrc/pkgtools/pkglint/files/logging_test.go:1.13 pkgsrc/pkgtools/pkglint/files/logging_test.go:1.14
--- pkgsrc/pkgtools/pkglint/files/logging_test.go:1.13  Sun Mar 10 19:01:50 2019
+++ pkgsrc/pkgtools/pkglint/files/logging_test.go       Sat Apr 20 17:43:24 2019
@@ -288,10 +288,10 @@ func (s *Suite) Test_Logger_Explain__onl
 
        // Neither the warning nor the corresponding explanation are logged.
        line.Warnf("Filtered warning.")
-       G.Explain("Explanation for the above warning.")
+       line.Explain("Explanation for the above warning.")
 
        line.Notef("What an interesting line.")
-       G.Explain("This explanation is logged.")
+       line.Explain("This explanation is logged.")
 
        t.CheckOutputLines(
                "NOTE: Makefile:27: What an interesting line.",
@@ -417,7 +417,7 @@ func (s *Suite) Test_Logger_ShowSummary_
 
        // Neither the warning nor the corresponding explanation are logged.
        line.Warnf("Filtered warning.")
-       G.Explain("Explanation for the above warning.")
+       line.Explain("Explanation for the above warning.")
        G.ShowSummary()
 
        // Since the above warning is filtered out by the --only option,
@@ -429,7 +429,7 @@ func (s *Suite) Test_Logger_ShowSummary_
                "Looks fine.")
 
        line.Warnf("This warning is interesting.")
-       G.Explain("This explanation is available.")
+       line.Explain("This explanation is available.")
        G.ShowSummary()
 
        c.Check(G.explanationsAvailable, equals, true)
@@ -608,12 +608,12 @@ func (s *Suite) Test_Logger_Logf__duplic
 
        // Is logged because it is the first appearance of this warning.
        line.Warnf("The warning.")
-       G.Explain("Explanation 1")
+       line.Explain("Explanation 1")
 
        // Is suppressed because the warning is the same as above and LogVerbose
        // has been set to false for this test.
        line.Warnf("The warning.")
-       G.Explain("Explanation 2")
+       line.Explain("Explanation 2")
 
        t.CheckOutputLines(
                "WARN: README.txt:123: The warning.",
@@ -630,9 +630,9 @@ func (s *Suite) Test_Logger_Logf__duplic
 
        // In rare cases, different diagnostics may have the same explanation.
        line.Warnf("Warning 1.")
-       G.Explain("Explanation")
+       line.Explain("Explanation")
        line.Warnf("Warning 2.")
-       G.Explain("Explanation") // Is suppressed.
+       line.Explain("Explanation") // Is suppressed.
 
        t.CheckOutputLines(
                "WARN: README.txt:123: Warning 1.",
@@ -745,7 +745,7 @@ func (s *Suite) Test_Logger_Diag__source
        t.SetUpPackage("category/package2",
                "PATCHDIR=\t../../category/dependency/patches")
 
-       G.Main("pkglint", "--source", "-Wall", t.File("category/package1"), t.File("category/package2"))
+       t.Main("--source", "-Wall", t.File("category/package1"), t.File("category/package2"))
 
        t.CheckOutputLines(
                "ERROR: ~/category/package1/distinfo: "+
Index: pkgsrc/pkgtools/pkglint/files/mktypes.go
diff -u pkgsrc/pkgtools/pkglint/files/mktypes.go:1.13 pkgsrc/pkgtools/pkglint/files/mktypes.go:1.14
--- pkgsrc/pkgtools/pkglint/files/mktypes.go:1.13       Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mktypes.go    Sat Apr 20 17:43:24 2019
@@ -1,10 +1,6 @@
 package pkglint
 
-import (
-       "netbsd.org/pkglint/textproc"
-       "strings"
-       "unicode"
-)
+import "strings"
 
 // MkToken represents a contiguous string from a Makefile.
 // It is either a literal string or a variable use.
@@ -47,42 +43,8 @@ func (m MkVarUseModifier) IsSuffixSubst(
 }
 
 func (m MkVarUseModifier) MatchSubst() (ok bool, regex bool, from string, to string, options string) {
-       l := textproc.NewLexer(m.Text)
-       regex = l.PeekByte() == 'C'
-       if l.SkipByte('S') || l.SkipByte('C') {
-               separator := l.PeekByte()
-               l.Skip(1)
-               if unicode.IsPunct(rune(separator)) || separator == '|' {
-                       noSeparator := func(b byte) bool { return int(b) != separator && b != '\\' }
-                       nextToken := func() string {
-                               start := l.Mark()
-                               for {
-                                       switch {
-                                       case l.NextBytesFunc(noSeparator) != "":
-                                               continue
-                                       case l.PeekByte() == '\\' && len(l.Rest()) >= 2:
-                                               // TODO: Compare with devel/bmake for the exact behavior
-                                               l.Skip(2)
-                                       default:
-                                               return l.Since(start)
-                                       }
-                               }
-                       }
-
-                       from = nextToken()
-                       if from != "" && l.SkipByte(byte(separator)) {
-                               to = nextToken()
-                               if l.SkipByte(byte(separator)) {
-                                       options = l.NextBytesFunc(func(b byte) bool {
-                                               return b == '1' || b == 'g' || b == 'W'
-                                       })
-                                       ok = l.EOF()
-                                       return
-                               }
-                       }
-               }
-       }
-       return
+       p := NewMkParser(nil, m.Text, false)
+       return p.varUseModifierSubst('}')
 }
 
 // Subst evaluates an S/from/to/ modifier.
Index: pkgsrc/pkgtools/pkglint/files/options.go
diff -u pkgsrc/pkgtools/pkglint/files/options.go:1.13 pkgsrc/pkgtools/pkglint/files/options.go:1.14
--- pkgsrc/pkgtools/pkglint/files/options.go:1.13       Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/options.go    Sat Apr 20 17:43:24 2019
@@ -1,134 +1,180 @@
 package pkglint
 
 func CheckLinesOptionsMk(mklines MkLines) {
-       if trace.Tracing {
-               defer trace.Call1(mklines.lines.FileName)()
-       }
+       ck := OptionsLinesChecker{
+               mklines,
+               make(map[string]MkLine),
+               make(map[string]MkLine),
+               nil}
+
+       ck.Check()
+}
+
+// OptionsLinesChecker checks an options.mk file of a pkgsrc package.
+//
+// See mk/bsd.options.mk for a detailed description.
+type OptionsLinesChecker struct {
+       mklines MkLines
+
+       declaredOptions           map[string]MkLine
+       handledOptions            map[string]MkLine
+       optionsInDeclarationOrder []string
+}
+
+func (ck *OptionsLinesChecker) Check() {
+       mklines := ck.mklines
 
        mklines.Check()
 
        mlex := NewMkLinesLexer(mklines)
        mlex.SkipWhile(func(mkline MkLine) bool { return mkline.IsComment() || mkline.IsEmpty() })
 
-       if mlex.EOF() || !(mlex.CurrentMkLine().IsVarassign() && mlex.CurrentMkLine().Varname() == "PKG_OPTIONS_VAR") {
-               mlex.CurrentLine().Warnf("Expected definition of PKG_OPTIONS_VAR.")
-               G.Explain(
-                       "The input variables in an options.mk file should always be",
-                       "mentioned in the same order: PKG_OPTIONS_VAR,",
-                       "PKG_SUPPORTED_OPTIONS, PKG_SUGGESTED_OPTIONS.",
-                       "This way, the options.mk files have the same structure and are easy to understand.")
+       if !ck.lookingAtPkgOptionsVar(mlex) {
                return
        }
        mlex.Skip()
 
-       declaredOptions := make(map[string]MkLine)
-       handledOptions := make(map[string]MkLine)
-       var optionsInDeclarationOrder []string
+       upper := true
+       for !mlex.EOF() && upper {
+               upper = ck.handleUpperLine(mlex.CurrentMkLine())
+               mlex.Skip()
+       }
+
+       for !mlex.EOF() {
+               ck.handleLowerLine(mlex.CurrentMkLine())
+               mlex.Skip()
+       }
+
+       ck.checkOptionsMismatch()
 
-loop:
-       for ; !mlex.EOF(); mlex.Skip() {
+       mklines.SaveAutofixChanges()
+}
+
+func (ck *OptionsLinesChecker) lookingAtPkgOptionsVar(mlex *MkLinesLexer) bool {
+       if !mlex.EOF() {
                mkline := mlex.CurrentMkLine()
-               switch {
-               case mkline.IsComment():
-                       break
-               case mkline.IsEmpty():
-                       break
-
-               case mkline.IsVarassign():
-                       switch mkline.Varcanon() {
-                       case "PKG_SUPPORTED_OPTIONS", "PKG_OPTIONS_GROUP.*", "PKG_OPTIONS_SET.*":
-                               for _, option := range mkline.ValueFields(mkline.Value()) {
-                                       if !containsVarRef(option) {
-                                               declaredOptions[option] = mkline
-                                               optionsInDeclarationOrder = append(optionsInDeclarationOrder, option)
-                                       }
-                               }
-                       }
+               if mkline.IsVarassign() && mkline.Varname() == "PKG_OPTIONS_VAR" {
+                       return true
+               }
+       }
 
-               case mkline.IsDirective():
-                       // The conditionals are typically for OPSYS and MACHINE_ARCH.
+       line := mlex.CurrentLine()
+       line.Warnf("Expected definition of PKG_OPTIONS_VAR.")
+       line.Explain(
+               "The input variables in an options.mk file should always be",
+               "mentioned in the same order: PKG_OPTIONS_VAR,",
+               "PKG_SUPPORTED_OPTIONS, PKG_SUGGESTED_OPTIONS.",
+               "This way, the options.mk files have the same structure and are easy to understand.")
+       return false
+}
 
-               case mkline.IsInclude():
-                       if mkline.IncludedFile() == "../../mk/bsd.options.mk" {
-                               mlex.Skip()
-                               break loop
+// checkLineUpper checks a line from the upper part of an options.mk file,
+// before bsd.options.mk is included.
+func (ck *OptionsLinesChecker) handleUpperLine(mkline MkLine) bool {
+       switch {
+       case mkline.IsComment():
+               break
+       case mkline.IsEmpty():
+               break
+
+       case mkline.IsVarassign():
+               switch mkline.Varcanon() {
+               case "PKG_SUPPORTED_OPTIONS", "PKG_OPTIONS_GROUP.*", "PKG_OPTIONS_SET.*":
+                       for _, option := range mkline.ValueFields(mkline.Value()) {
+                               if !containsVarRef(option) {
+                                       ck.declaredOptions[option] = mkline
+                                       ck.optionsInDeclarationOrder = append(ck.optionsInDeclarationOrder, option)
+                               }
                        }
+               }
 
-               default:
-                       mlex.CurrentLine().Warnf("Expected inclusion of \"../../mk/bsd.options.mk\".")
-                       G.Explain(
-                               "After defining the input variables (PKG_OPTIONS_VAR, etc.),",
-                               "bsd.options.mk should be included to do the actual processing.",
-                               "No other actions should take place in this part of the file",
-                               "in order to have the same structure in all options.mk files.")
-                       return
+       case mkline.IsDirective():
+               // The conditionals are typically for OPSYS and MACHINE_ARCH.
+
+       case mkline.IsInclude():
+               if mkline.IncludedFile() == "../../mk/bsd.options.mk" {
+                       return false
                }
+
+       default:
+               line := mkline
+               line.Warnf("Expected inclusion of \"../../mk/bsd.options.mk\".")
+               line.Explain(
+                       "After defining the input variables (PKG_OPTIONS_VAR, etc.),",
+                       "bsd.options.mk should be included to do the actual processing.",
+                       "No other actions should take place in this part of the file",
+                       "in order to have the same structure in all options.mk files.")
+               return false
        }
 
-       for ; !mlex.EOF(); mlex.Skip() {
-               mkline := mlex.CurrentMkLine()
-               if mkline.IsDirective() && (mkline.Directive() == "if" || mkline.Directive() == "elif") {
+       return true
+}
+
+func (ck *OptionsLinesChecker) handleLowerLine(mkline MkLine) {
+       if mkline.IsDirective() {
+               directive := mkline.Directive()
+               if directive == "if" || directive == "elif" {
                        cond := mkline.Cond()
-                       if cond == nil {
-                               continue
+                       if cond != nil {
+                               ck.handleLowerCondition(mkline, cond)
                        }
+               }
+       }
+}
+
+func (ck *OptionsLinesChecker) handleLowerCondition(mkline MkLine, cond MkCond) {
 
-                       recordUsedOption := func(varuse *MkVarUse) {
-                               if varuse.varname == "PKG_OPTIONS" && len(varuse.modifiers) == 1 {
-                                       if m, positive, pattern := varuse.modifiers[0].MatchMatch(); m && positive {
-                                               option := pattern
-                                               if !containsVarRef(option) {
-                                                       handledOptions[option] = mkline
-                                                       optionsInDeclarationOrder = append(optionsInDeclarationOrder, option)
-                                               }
-                                       }
+       recordUsedOption := func(varuse *MkVarUse) {
+               if varuse.varname == "PKG_OPTIONS" && len(varuse.modifiers) == 1 {
+                       if m, positive, pattern := varuse.modifiers[0].MatchMatch(); m && positive {
+                               option := pattern
+                               if !containsVarRef(option) {
+                                       ck.handledOptions[option] = mkline
+                                       ck.optionsInDeclarationOrder = append(ck.optionsInDeclarationOrder, option)
                                }
                        }
-                       cond.Walk(&MkCondCallback{
-                               Empty: recordUsedOption,
-                               Var:   recordUsedOption})
-
-                       // FIXME: Is this note also issued for the following lines?
-                       //  .if empty(ANY_OTHER_VARIABLE)
-                       //  .else
-                       //  .endif
-                       if cond.Empty != nil && mkline.HasElseBranch() {
-                               mkline.Notef("The positive branch of the .if/.else should be the one where the option is set.")
-                               G.Explain(
-                                       "For consistency among packages, the upper branch of this",
-                                       ".if/.else statement should always handle the case where the",
-                                       "option is activated.",
-                                       "A missing exclamation mark at this point can easily be overlooked.",
-                                       "",
-                                       "If that seems too much to type and the exclamation mark",
-                                       "seems wrong for a positive test, switch the blocks nevertheless",
-                                       "and write the condition like this, which has the same effect",
-                                       "as the !empty(...).",
-                                       "",
-                                       "\t.if ${PKG_OPTIONS.packagename:Moption}")
-                       }
                }
        }
 
-       for _, option := range optionsInDeclarationOrder {
-               declared := declaredOptions[option]
-               handled := handledOptions[option]
+       cond.Walk(&MkCondCallback{
+               Empty: recordUsedOption,
+               Var:   recordUsedOption})
+
+       if cond.Empty != nil && cond.Empty.varname == "PKG_OPTIONS" && mkline.HasElseBranch() {
+               mkline.Notef("The positive branch of the .if/.else should be the one where the option is set.")
+               mkline.Explain(
+                       "For consistency among packages, the upper branch of this",
+                       ".if/.else statement should always handle the case where the",
+                       "option is activated.",
+                       "A missing exclamation mark at this point can easily be overlooked.",
+                       "",
+                       "If that seems too much to type and the exclamation mark",
+                       "seems wrong for a positive test, switch the blocks nevertheless",
+                       "and write the condition like this, which has the same effect",
+                       "as the !empty(...).",
+                       "",
+                       "\t.if ${PKG_OPTIONS.packagename:Moption}")
+       }
+}
+
+func (ck *OptionsLinesChecker) checkOptionsMismatch() {
+       for _, option := range ck.optionsInDeclarationOrder {
+               declared := ck.declaredOptions[option]
+               handled := ck.handledOptions[option]
 
                switch {
                case handled == nil:
                        declared.Warnf("Option %q should be handled below in an .if block.", option)
-                       G.Explain(
+                       declared.Explain(
                                "If an option is not processed in this file, it may either be a",
                                "typo, or the option does not have any effect.")
 
                case declared == nil:
                        handled.Warnf("Option %q is handled but not added to PKG_SUPPORTED_OPTIONS.", option)
-                       G.Explain(
+                       handled.Explain(
                                "This block of code will never be run since PKG_OPTIONS cannot",
                                "contain this value.",
                                "This is most probably a typo.")
                }
        }
-
-       mklines.SaveAutofixChanges()
 }
Index: pkgsrc/pkgtools/pkglint/files/tools_test.go
diff -u pkgsrc/pkgtools/pkglint/files/tools_test.go:1.13 pkgsrc/pkgtools/pkglint/files/tools_test.go:1.14
--- pkgsrc/pkgtools/pkglint/files/tools_test.go:1.13    Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/tools_test.go Sat Apr 20 17:43:25 2019
@@ -94,7 +94,7 @@ func (s *Suite) Test_Tools__USE_TOOLS_pr
                "\t${SED} < input > output",
                "\t${AWK} < input > output")
 
-       G.Main("pkglint", "-Wall", t.File("module.mk"))
+       t.Main("-Wall", t.File("module.mk"))
 
        // Since this test doesn't load the usual tool definitions via
        // G.Pkgsrc.loadTools, AWK is not known at all.
@@ -200,7 +200,7 @@ func (s *Suite) Test_Tools__package_Make
                "USE_TOOLS+=     load")
        t.CreateFileLines("mk/bsd.pkg.mk",
                "USE_TOOLS+=     run")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        tools := NewTools()
        tools.Fallback(G.Pkgsrc.Tools)
@@ -250,7 +250,7 @@ func (s *Suite) Test_Tools__builtin_mk(c
        t.CreateFileLines("mk/bsd.pkg.mk",
                "USE_TOOLS+=     run")
        t.CreateFileLines("mk/buildlink3/bsd.builtin.mk")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        // Tools that are defined by pkgsrc as load-time tools
        // may be used in any file at load time.
@@ -299,7 +299,7 @@ func (s *Suite) Test_Tools__implicit_def
        // bsd.pkg.mk and not defined earlier in mk/tools/defaults.mk, but
        // the pkglint code is even prepared for these rare cases.
        // In other words, this test is only there for the code coverage.
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        c.Check(G.Pkgsrc.Tools.ByName("run").String(), equals, "run:::AtRunTime")
 }
@@ -318,7 +318,7 @@ func (s *Suite) Test_Tools__both_prefs_a
 
        // The echo tool is mentioned in both files. The file bsd.prefs.mk
        // grants more use cases (load time + run time), therefore it wins.
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        c.Check(G.Pkgsrc.Tools.ByName("both").Validity, equals, AfterPrefsMk)
 }
@@ -336,7 +336,7 @@ func (s *Suite) Test_Tools__tools_having
        t.CreateFileLines("mk/bsd.prefs.mk",
                "USE_TOOLS+=     awk sed")
 
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        c.Check(G.Pkgsrc.Tools.ByName("awk").Validity, equals, AfterPrefsMk)
        c.Check(G.Pkgsrc.Tools.ByName("sed").Validity, equals, AfterPrefsMk)
@@ -397,7 +397,7 @@ func (s *Suite) Test_Tools__var(c *check
                "_TOOLS_VARNAME.ln=      LN")
        t.CreateFileLines("mk/bsd.pkg.mk",
                "USE_TOOLS+=             ln")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        mklines := t.NewMkLines("module.mk",
                MkRcsID,
@@ -498,7 +498,7 @@ func (s *Suite) Test_Tools__cmake(c *che
                ".if defined(USE_CMAKE)",
                "USE_TOOLS+=\tcmake cpack",
                ".endif")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -523,8 +523,7 @@ func (s *Suite) Test_Tools__gmake(c *che
        t.CreateFileLines("mk/tools/replace.mk",
                "TOOLS_CREATE+=\tgmake",
                "TOOLS_PATH.gmake=\t/usr/bin/gnu-make")
-
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 

Index: pkgsrc/pkgtools/pkglint/files/autofix.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix.go:1.19 pkgsrc/pkgtools/pkglint/files/autofix.go:1.20
--- pkgsrc/pkgtools/pkglint/files/autofix.go:1.19       Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix.go    Sat Apr 20 17:43:24 2019
@@ -299,7 +299,7 @@ func (fix *Autofix) Apply() {
                        line.showSource(G.out)
                }
                if logDiagnostic && len(fix.explanation) > 0 {
-                       G.Explain(fix.explanation...)
+                       line.Explain(fix.explanation...)
                }
                if G.Logger.Opts.ShowSource {
                        if !(G.Logger.Opts.Explain && logDiagnostic && len(fix.explanation) > 0) {
@@ -330,7 +330,8 @@ func (fix *Autofix) Realign(mkline MkLin
        {
                // Parsing the continuation marker as variable value is cheating but works well.
                text := strings.TrimSuffix(mkline.raw[0].orignl, "\n")
-               _, a := MatchVarassign(text)
+               data := MkLineParser{}.split(nil, text)
+               _, a := MkLineParser{}.MatchVarassign(mkline.Line, text, data)
                if a.value != "\\" {
                        oldWidth = tabWidth(a.valueAlign)
                }
@@ -448,11 +449,11 @@ func SaveAutofixChanges(lines Lines) (au
                G.fileCache.Evict(filename)
                changedLines := changes[filename]
                tmpName := filename + ".pkglint.tmp"
-               text := ""
+               var text strings.Builder
                for _, changedLine := range changedLines {
-                       text += changedLine
+                       text.WriteString(changedLine)
                }
-               err := ioutil.WriteFile(tmpName, []byte(text), 0666)
+               err := ioutil.WriteFile(tmpName, []byte(text.String()), 0666)
                if err != nil {
                        G.Logf(Error, tmpName, "", "Cannot write: %s", "Cannot write: "+err.Error())
                        continue
Index: pkgsrc/pkgtools/pkglint/files/autofix_test.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.19 pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.20
--- pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.19  Sun Mar 10 19:01:50 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix_test.go       Sat Apr 20 17:43:24 2019
@@ -1167,8 +1167,8 @@ func (s *Suite) Test_Autofix__lonely_sou
                "",
                ".for id in ${PRE_XORGPROTO_LIST_MISSING}",
                ".endfor")
-       G.Pkgsrc.LoadInfrastructure()
        t.Chdir(".")
+       t.FinishSetUp()
 
        G.Check("x11/xorg-cf-files")
        G.Check("x11/xorgproto")
@@ -1188,8 +1188,8 @@ func (s *Suite) Test_Autofix__lonely_sou
 
        t.SetUpPackage("print/tex-bibtex8",
                "MAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}")
-       G.Pkgsrc.LoadInfrastructure()
        t.Chdir(".")
+       t.FinishSetUp()
 
        G.Check("print/tex-bibtex8")
 

Index: pkgsrc/pkgtools/pkglint/files/buildlink3.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.20 pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.21
--- pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.20    Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/buildlink3.go Sat Apr 20 17:43:24 2019
@@ -230,7 +230,7 @@ func (ck *Buildlink3Checker) checkVaruse
                                token.Text)
                }
 
-               G.Explain(
+               pkgbaseLine.Explain(
                        "The identifiers in the BUILDLINK_TREE variable should be plain",
                        "strings that do not refer to any variable.",
                        "",
Index: pkgsrc/pkgtools/pkglint/files/category_test.go
diff -u pkgsrc/pkgtools/pkglint/files/category_test.go:1.20 pkgsrc/pkgtools/pkglint/files/category_test.go:1.21
--- pkgsrc/pkgtools/pkglint/files/category_test.go:1.20 Sun Mar 10 19:01:50 2019
+++ pkgsrc/pkgtools/pkglint/files/category_test.go      Sat Apr 20 17:43:24 2019
@@ -83,6 +83,7 @@ func (s *Suite) Test_CheckdirCategory__w
                "\t${RUN}wip-specific-command",
                "",
                ".include \"../mk/misc/category.mk\"")
+       t.FinishSetUp()
 
        G.Check(t.File("wip"))
 
@@ -117,6 +118,7 @@ func (s *Suite) Test_CheckdirCategory__s
                "#SUBDIR+=\tcommented-without-reason",
                "",
                ".include \"../mk/misc/category.mk\"")
+       t.FinishSetUp()
 
        CheckdirCategory(t.File("category"))
 
@@ -147,6 +149,7 @@ func (s *Suite) Test_CheckdirCategory__o
                "SUBDIR+=\tonly-in-makefile",
                "",
                ".include \"../mk/misc/category.mk\"")
+       t.FinishSetUp()
 
        CheckdirCategory(t.File("category"))
 
@@ -174,6 +177,7 @@ func (s *Suite) Test_CheckdirCategory__o
                "SUBDIR+=\tboth",
                "",
                ".include \"../mk/misc/category.mk\"")
+       t.FinishSetUp()
 
        CheckdirCategory(t.File("category"))
 
@@ -203,6 +207,7 @@ func (s *Suite) Test_CheckdirCategory__r
                "",
                ".include \"../mk/misc/category.mk\"")
        t.Chdir("category")
+       t.FinishSetUp()
 
        CheckdirCategory(".")
 
@@ -238,6 +243,7 @@ func (s *Suite) Test_CheckdirCategory__s
                "SUBDIR+=\tmk-and-fs",
                "",
                ".include \"../mk/misc/category.mk\"")
+       t.FinishSetUp()
 
        CheckdirCategory(t.File("category"))
 
@@ -263,6 +269,7 @@ func (s *Suite) Test_CheckdirCategory__i
                "SUBDIR+=\tpackage2",
                "",
                ".include \"../mk/misc/category.mk\"")
+       t.FinishSetUp()
 
        CheckdirCategory(t.File("category"))
 
@@ -287,6 +294,7 @@ func (s *Suite) Test_CheckdirCategory__c
                "SUBDIR+=\tpackage",
                "",
                ".include \"../mk/misc/category.mk\"")
+       t.FinishSetUp()
 
        CheckdirCategory(t.File("category"))
 
@@ -318,6 +326,7 @@ func (s *Suite) Test_CheckdirCategory__u
                "COMMENT=\tCategory comment",
                "",
                "SUBDIR+=\tpackage")
+       t.FinishSetUp()
 
        CheckdirCategory(t.File("category"))
 
@@ -333,6 +342,7 @@ func (s *Suite) Test_CheckdirCategory__n
 
        t.SetUpPkgsrc()
        t.CreateFileLines("category/other-file")
+       t.FinishSetUp()
 
        G.Check(t.File("category"))
 
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.20 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.21
--- pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.20   Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go        Sat Apr 20 17:43:24 2019
@@ -71,7 +71,7 @@ func (s *Suite) Test_Pkgsrc_checkTopleve
        t.SetUpPackage("category/package",
                "LICENSE=\t2-clause-bsd")
 
-       G.Main("pkglint", "-r", "-Cglobal", t.File("."))
+       t.Main("-r", "-Cglobal", t.File("."))
 
        t.CheckOutputLines(
                "WARN: ~/licenses/gnu-gpl-v2: This license seems to be unused.", // Added by Tester.SetUpPkgsrc
@@ -153,7 +153,7 @@ func (s *Suite) Test_Pkgsrc_loadTools__B
        t.CreateFileLines("mk/bsd.pkg.mk",
                MkRcsID,
                "_BUILD_DEFS+=\tPKG_SYSCONFBASEDIR PKG_SYSCONFDIR")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -173,7 +173,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
        t.Remove("doc")
 
        t.ExpectFatal(
-               G.Pkgsrc.loadDocChanges,
+               t.FinishSetUp,
                "FATAL: ~/doc: Cannot be read for loading the package changes.")
 }
 
@@ -241,7 +241,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
                "\tUpdated sysutils/checkperms to 1.10 [rillig 2018-01-05]",
                "\tUpdated sysutils/checkperms to 1.11 [rillig 2018-01-01]")
 
-       G.Main("pkglint", t.File("wip/package"))
+       t.Main(t.File("wip/package"))
 
        t.CheckOutputLines(
                "Looks fine.")
@@ -259,7 +259,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
                "        Updated sysutils/checkperms to 1.10 [rillig 2018-01-05]",
                "    \tUpdated sysutils/checkperms to 1.11 [rillig 2018-01-01]")
 
-       G.Main("pkglint", t.File("category/package"))
+       t.Main(t.File("category/package"))
 
        t.CheckOutputLines(
                "WARN: ~/doc/CHANGES-2018:5: Package changes should be indented using a single tab, not \"        \".",
@@ -284,7 +284,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
                "\t\tdistfile directly from GitHub [rillig 2018-01-01]",
                "\tmk/bsd.pkg.mk: Another infrastructure change [rillig 2018-01-02]")
 
-       G.Main("pkglint", t.File("category/package"))
+       t.Main(t.File("category/package"))
 
        // For pkglint's purpose, the infrastructure entries are simply ignored
        // since they do not belong to a single package.
@@ -303,7 +303,7 @@ func (s *Suite) Test_Pkgsrc_parseSuggest
                "Suggested package updates",
                "",
                "\to package-1.13 [cool new features]")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -586,14 +586,14 @@ func (s *Suite) Test_Pkgsrc_VariableType
        test("_PERL5_PACKLIST_AWK_STRIP_DESTDIR", "")
        test("SOME_DIR", "Pathname (guessed)")
        test("SOMEDIR", "Pathname (guessed)")
-       test("SEARCHPATHS", "List of Pathname (guessed)")
+       test("SEARCHPATHS", "Pathname (list, guessed)")
        test("MYPACKAGE_USER", "UserGroupName (guessed)")
        test("MYPACKAGE_GROUP", "UserGroupName (guessed)")
-       test("MY_CMD_ENV", "List of ShellWord (guessed)")
-       test("MY_CMD_ARGS", "List of ShellWord (guessed)")
-       test("MY_CMD_CFLAGS", "List of CFlag (guessed)")
-       test("MY_CMD_LDFLAGS", "List of LdFlag (guessed)")
-       test("PLIST.abcde", "Yes")
+       test("MY_CMD_ENV", "ShellWord (list, guessed)")
+       test("MY_CMD_ARGS", "ShellWord (list, guessed)")
+       test("MY_CMD_CFLAGS", "CFlag (list, guessed)")
+       test("MY_CMD_LDFLAGS", "LdFlag (list, guessed)")
+       test("PLIST.abcde", "Yes (package-settable)")
 }
 
 // Guessing the variable type works for both plain and parameterized variable names.
@@ -605,12 +605,12 @@ func (s *Suite) Test_Pkgsrc_VariableType
        t1 := G.Pkgsrc.VariableType(nil, "FONT_DIRS")
 
        c.Assert(t1, check.NotNil)
-       c.Check(t1.String(), equals, "List of PathMask (guessed)")
+       c.Check(t1.String(), equals, "PathMask (list, guessed)")
 
        t2 := G.Pkgsrc.VariableType(nil, "FONT_DIRS.ttf")
 
        c.Assert(t2, check.NotNil)
-       c.Check(t2.String(), equals, "List of PathMask (guessed)")
+       c.Check(t2.String(), equals, "PathMask (list, guessed)")
 }
 
 // Guessing the variable type also works for variables that are
@@ -630,16 +630,15 @@ func (s *Suite) Test_Pkgsrc_VariableType
                "PKGSRC_MAKE_ENV?=\t# none",
                "CPPPATH?=\tcpp",
                "OSNAME.Linux?=\tLinux")
-
        pkg := t.SetUpPackage("category/package",
                "PKGSRC_MAKE_ENV+=\tCPP=${CPPPATH:Q}",
                "PKGSRC_UNKNOWN_ENV+=\tCPP=${ABCPATH:Q}",
                "OSNAME.SunOS=\t\t${OSNAME.Other}")
 
-       G.Main("pkglint", "-Wall", pkg)
+       t.Main("-Wall", pkg)
 
        if typ := G.Pkgsrc.VariableType(nil, "PKGSRC_MAKE_ENV"); c.Check(typ, check.NotNil) {
-               c.Check(typ.String(), equals, "List of ShellWord (guessed)")
+               c.Check(typ.String(), equals, "ShellWord (list, guessed)")
        }
 
        if typ := G.Pkgsrc.VariableType(nil, "CPPPATH"); c.Check(typ, check.NotNil) {
@@ -657,7 +656,8 @@ func (s *Suite) Test_Pkgsrc_VariableType
        t.CheckOutputLines(
                "WARN: ~/category/package/Makefile:21: PKGSRC_UNKNOWN_ENV is defined but not used.",
                "WARN: ~/category/package/Makefile:21: ABCPATH is used but not defined.",
-               "0 errors and 2 warnings found.")
+               "0 errors and 2 warnings found.",
+               "(Run \"pkglint -e\" to show explanations.)")
 }
 
 func (s *Suite) Test_Pkgsrc_guessVariableType__SKIP(c *check.C) {
@@ -673,7 +673,7 @@ func (s *Suite) Test_Pkgsrc_guessVariabl
        mklines.Check()
 
        vartype := G.Pkgsrc.VariableType(mklines, "MY_CHECK_SKIP")
-       t.Check(vartype.guessed, equals, true)
+       t.Check(vartype.Guessed(), equals, true)
        t.Check(vartype.EffectivePermissions("filename.mk"), equals, aclpAllRuntime)
 
        // The permissions for MY_CHECK_SKIP say aclpAllRuntime, which excludes

Index: pkgsrc/pkgtools/pkglint/files/buildlink3_test.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.29 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.30
--- pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.29       Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/buildlink3_test.go    Sat Apr 20 17:43:24 2019
@@ -19,6 +19,7 @@ func (s *Suite) Test_CheckLinesBuildlink
 
        t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
                ".include \"../../category/dependency2/buildlink3.mk\"")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -85,6 +86,7 @@ func (s *Suite) Test_CheckLinesBuildlink
                ".endif\t# HS_X11_BUILDLINK3_MK",
                "",
                "BUILDLINK_TREE+=\t-hs-X11")
+       t.FinishSetUp()
 
        G.Check(".")
 
@@ -124,6 +126,7 @@ func (s *Suite) Test_CheckLinesBuildlink
                ".endif\t# HS_X11_BUILDLINK3_MK",
                "",
                "BUILDLINK_TREE+=\t-hs-X11")
+       t.FinishSetUp()
 
        G.Check(".")
 
@@ -150,6 +153,7 @@ func (s *Suite) Test_CheckLinesBuildlink
                ".endif\t# P5_GTK2_BUILDLINK3_MK",
                "",
                "BUILDLINK_TREE+=\t-p5-gtk2")
+       t.FinishSetUp()
 
        G.Check(t.File("x11/p5-gtk2"))
 
@@ -508,6 +512,8 @@ func (s *Suite) Test_CheckLinesBuildlink
                        "it would be ok in Makefile, Makefile.* or *.mk, but not buildlink3.mk or builtin.mk.",
                "WARN: buildlink3.mk:3: The variable LICENSE should be quoted as part of a shell word.",
                "WARN: buildlink3.mk:8: The variable LICENSE should be quoted as part of a shell word.",
+               "WARN: buildlink3.mk:8: The variable LICENSE should be quoted as part of a shell word.",
+               "WARN: buildlink3.mk:9: The variable LICENSE should be quoted as part of a shell word.",
                "WARN: buildlink3.mk:9: The variable LICENSE should be quoted as part of a shell word.",
                "WARN: buildlink3.mk:13: The variable LICENSE should be quoted as part of a shell word.",
                "WARN: buildlink3.mk:3: Please replace \"${LICENSE}\" with a simple string "+
@@ -523,6 +529,7 @@ func (s *Suite) Test_Buildlink3Checker_c
                ".if ${X11_TYPE} == modular",
                ".else",
                ".endif")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -537,6 +544,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=1.0:../../category/package",
                "BUILDLINK_API_DEPENDS.package+=\tpackage>=1.5:../../category/package")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -570,6 +578,7 @@ func (s *Suite) Test_Buildlink3Checker_c
                ".endif # PACKAGE_BUILDLINK3_MK",
                "",
                "BUILDLINK_TREE+=\t-package")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -589,6 +598,7 @@ func (s *Suite) Test_Buildlink3Checker_c
                "",
                "ABI_VERSION=\t1.0",
                "API_VERSION=\t1.5")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -606,6 +616,7 @@ func (s *Suite) Test_Buildlink3Checker_c
                "BUILDLINK_API_DEPENDS.package+=\tpackage>=${API_VERSION}",
                "",
                "API_VERSION=\t1.5")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -621,6 +632,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_ABI_DEPENDS.package+=\tpackage-1.*",
                "BUILDLINK_API_DEPENDS.package+=\tpackage-2.*")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -637,6 +649,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=1",
                "BUILDLINK_API_DEPENDS.package+=\tpackage-1.*")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -658,6 +671,7 @@ func (s *Suite) Test_Buildlink3Checker_c
                "BUILDLINK_DEPMETHOD.other+=\tbuild",
                "",
                "BUILDLINK_API_DEPENDS.other+=\tother>=3")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -674,6 +688,7 @@ func (s *Suite) Test_Buildlink3Checker_C
        t.SetUpPackage("category/package")
        t.CreateFileDummyBuildlink3("category/package/buildlink3.mk")
        t.DisableTracing()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package/buildlink3.mk"))
 
@@ -687,6 +702,7 @@ func (s *Suite) Test_Buildlink3Checker_c
                "DISTNAME=\t# empty",
                "PKGNAME=\t# empty, to force mkbase to be empty")
        t.CreateFileDummyBuildlink3("category/package/buildlink3.mk")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
Index: pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.29 pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.30
--- pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.29    Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go Sat Apr 20 17:43:24 2019
@@ -66,7 +66,7 @@ func (s *Suite) Test_MkLineChecker_check
                "USED_IN_INFRASTRUCTURE=\t${SHORT_DOCUMENTATION}",
                "",
                "UNUSED_INFRA=\t${UNDOCUMENTED}")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -84,6 +84,7 @@ func (s *Suite) Test_MkLineChecker_check
        t.CreateFileLines("mk/infra.mk",
                MkRcsID,
                "_VARNAME=\tvalue")
+       t.FinishSetUp()
 
        G.Check(t.File("mk/infra.mk"))
 
@@ -185,6 +186,7 @@ func (s *Suite) Test_MkLineChecker_check
        t.SetUpPackage("category/package",
                ".include \"../../other/existing/Makefile\"",
                ".include \"../../other/not-found/Makefile\"")
+       t.FinishSetUp()
 
        G.checkdirPackage(t.File("category/package"))
 
@@ -321,12 +323,9 @@ func (s *Suite) Test_MkLineChecker_check
        mklines.Check()
 
        t.CheckOutputLines(
-               // FIXME: PATH may actually be used at load time.
-               "WARN: for.mk:2: PATH should not be used at load time in any file.",
-
-               // No warning about :Q in line 2 since the :C modifier converts the
-               // colon-separated list into a space-separated list, as required by
-               // the .for loop.
+               // No warning about a missing :Q in line 2 since the :C modifier
+               // converts the colon-separated list into a space-separated list,
+               // as required by the .for loop.
 
                // This warning is correct since PATH is separated by colons, not by spaces.
                "WARN: for.mk:5: Please use ${PATH:Q} instead of ${PATH}.",
@@ -347,6 +346,7 @@ func (s *Suite) Test_MkLineChecker_check
                "",
                ".for _i_ in 1 2 3", // Underscores are only allowed in infrastructure files.
                ".endfor")
+       t.FinishSetUp()
 
        G.Check(t.File("mk/file.mk"))
 
@@ -386,8 +386,8 @@ func (s *Suite) Test_MkLineChecker_check
 
        c.Assert(vartype, check.NotNil)
        c.Check(vartype.basicType.name, equals, "Comment")
-       c.Check(vartype.guessed, equals, false)
-       c.Check(vartype.kindOfList, equals, lkNone)
+       c.Check(vartype.Guessed(), equals, false)
+       c.Check(vartype.List(), equals, false)
 
        mklines := t.NewMkLines("Makefile",
                MkRcsID,
@@ -511,10 +511,10 @@ func (s *Suite) Test_MkLineChecker_check
                "NOTE: filename.mk:1: MACHINE_ARCH should be compared using == instead of matching against \":Mx86\".")
 
        test(".if ${MASTER_SITES:Mftp://*} == \"ftp://netbsd.org/\"";,
-               // FIXME: Indeed, indeed, the :M modifier ends at the colon.
-               //  Why doesn't pkglint complain loudly about the unknown "//*" modifier?
+               "WARN: filename.mk:1: Invalid variable modifier \"//*\" for \"MASTER_SITES\".",
                "WARN: filename.mk:1: \"ftp\" is not a valid URL.",
-               "WARN: filename.mk:1: MASTER_SITES should not be used at load time in any file.")
+               "WARN: filename.mk:1: MASTER_SITES should not be used at load time in any file.",
+               "WARN: filename.mk:1: Invalid variable modifier \"//*\" for \"MASTER_SITES\".")
 
        // The only interesting line from the below tracing output is the one
        // containing "checkCompareVarStr".
@@ -563,9 +563,9 @@ func (s *Suite) Test_MkLineChecker_check
 
        t.SetUpVartypes()
        t.SetUpTool("awk", "AWK", AtRunTime)
-       G.Pkgsrc.vartypes.DefineParse("SET_ONLY", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("SET_ONLY", BtUnknown, NoVartypeOptions,
                "options.mk: set")
-       G.Pkgsrc.vartypes.DefineParse("SET_ONLY_DEFAULT_ELSEWHERE", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("SET_ONLY_DEFAULT_ELSEWHERE", BtUnknown, NoVartypeOptions,
                "options.mk: set",
                "*.mk: default, set")
        mklines := t.NewMkLines("options.mk",
@@ -627,15 +627,15 @@ func (s *Suite) Test_MkLineChecker_check
        mklines := t.NewMkLines("filename.mk",
                MkRcsID,
                "LICENSE?=\tgnu-gpl-v2")
+       t.FinishSetUp()
 
        mklines.Check()
 
-       // FIXME: LICENSE is a package-settable variable. Therefore bsd.prefs.mk
-       //  does not need to be included before setting a default for this
-       //  variable. Including bsd.prefs.mk is only necessary when setting a
-       //  default value for user-settable or system-defined variables.
-       t.CheckOutputLines(
-               "WARN: filename.mk:2: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")
+       // LICENSE is a package-settable variable. Therefore bsd.prefs.mk
+       // does not need to be included before setting a default for this
+       // variable. Including bsd.prefs.mk is only necessary when setting a
+       // default value for user-settable or system-defined variables.
+       t.CheckOutputEmpty()
 }
 
 // Don't check the permissions for infrastructure files since they have their own rules.
@@ -678,13 +678,16 @@ func (s *Suite) Test_MkLineChecker_check
                "show-package-vars: .PHONY",
                "\techo OS_NAME=${OS_NAME:Q}",
                "\techo MUST_BE_EARLY=${MUST_BE_EARLY:Q}")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package/standalone.mk"))
 
        // There is no warning about any variable since no package is currently
        // being checked, therefore pkglint cannot decide whether the variable
        // is used a load time.
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "WARN: ~/category/package/standalone.mk:14: Please use \"${ECHO}\" instead of \"echo\".",
+               "WARN: ~/category/package/standalone.mk:15: Please use \"${ECHO}\" instead of \"echo\".")
 
        t.SetUpCommandLine("-Wall", "--explain")
        G.Check(t.File("category/package"))
@@ -722,7 +725,9 @@ func (s *Suite) Test_MkLineChecker_check
                "\tend of the line, or force the variable to be evaluated at load time,",
                "\tby using it at the right-hand side of the := operator, or in an .if",
                "\tor .for directive.",
-               "")
+               "",
+               "WARN: ~/category/package/standalone.mk:14: Please use \"${ECHO}\" instead of \"echo\".",
+               "WARN: ~/category/package/standalone.mk:15: Please use \"${ECHO}\" instead of \"echo\".")
 }
 
 func (s *Suite) Test_MkLineChecker_checkVarassignRightVaruse(c *check.C) {
@@ -736,11 +741,8 @@ func (s *Suite) Test_MkLineChecker_check
 
        mklines.Check()
 
-       // TODO: Duplicate diagnostics mean twice the work being done.
        t.CheckOutputLines(
                "WARN: module.mk:2: Please use PREFIX instead of LOCALBASE.",
-               "NOTE: module.mk:2: The :Q operator isn't necessary for ${LOCALBASE} here.",
-               "WARN: module.mk:2: Please use PREFIX instead of LOCALBASE.",
                "NOTE: module.mk:2: The :Q operator isn't necessary for ${LOCALBASE} here.")
 }
 
@@ -873,9 +875,9 @@ func (s *Suite) Test_MkLineChecker_check
 func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_in_condition(c *check.C) {
        t := s.Init(c)
 
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", lkShell, BtPathmask,
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtPathmask, List,
                "special:filename.mk: use-loadtime")
-       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", lkShell, BtPathmask,
+       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtPathmask, List,
                "special:filename.mk: use")
 
        mklines := t.NewMkLines("filename.mk",
@@ -892,9 +894,9 @@ func (s *Suite) Test_MkLineChecker_check
 func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_in_for_loop(c *check.C) {
        t := s.Init(c)
 
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", lkShell, BtPathmask,
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtPathmask, List,
                "special:filename.mk: use-loadtime")
-       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", lkShell, BtPathmask,
+       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtPathmask, List,
                "special:filename.mk: use")
 
        mklines := t.NewMkLines("filename.mk",
@@ -940,16 +942,16 @@ func (s *Suite) Test_MkLineChecker_check
 func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_run_time(c *check.C) {
        t := s.Init(c)
 
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtUnknown, NoVartypeOptions,
                "*.mk: use, use-loadtime")
-       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtUnknown, NoVartypeOptions,
                "*.mk: use")
-       G.Pkgsrc.vartypes.DefineParse("WRITE_ONLY", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("WRITE_ONLY", BtUnknown, NoVartypeOptions,
                "*.mk: set")
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME_ELSEWHERE", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME_ELSEWHERE", BtUnknown, NoVartypeOptions,
                "Makefile: use-loadtime",
                "*.mk: set")
-       G.Pkgsrc.vartypes.DefineParse("RUN_TIME_ELSEWHERE", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("RUN_TIME_ELSEWHERE", BtUnknown, NoVartypeOptions,
                "Makefile: use",
                "*.mk: set")
 
@@ -1041,7 +1043,7 @@ func (s *Suite) Test_MkLineChecker_check
 func (s *Suite) Test_MkLineChecker_checkVarusePermissions__usable_only_at_loadtime_in_other_file(c *check.C) {
        t := s.Init(c)
 
-       G.Pkgsrc.vartypes.DefineParse("VAR", lkNone, BtFileName,
+       G.Pkgsrc.vartypes.DefineParse("VAR", BtFileName, NoVartypeOptions,
                "*: set, use-loadtime")
        mklines := t.NewMkLines("Makefile",
                MkRcsID,
@@ -1060,9 +1062,9 @@ func (s *Suite) Test_MkLineChecker_check
 
        // This combination of BtUnknown and all permissions is typical for
        // otherwise unknown variables from the pkgsrc infrastructure.
-       G.Pkgsrc.vartypes.Define("INFRA", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.Define("INFRA", BtUnknown, NoVartypeOptions,
                ACLEntry{"*", aclpAll})
-       G.Pkgsrc.vartypes.DefineParse("VAR", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("VAR", BtUnknown, NoVartypeOptions,
                "buildlink3.mk: none",
                "*: use")
        mklines := t.NewMkLines("buildlink3.mk",
@@ -1096,10 +1098,10 @@ func (s *Suite) Test_MkLineChecker_check
        // to use its value in LOAD_TIME, as the latter might be evaluated later
        // at load time, and at that point VAR would be evaluated as well.
 
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", lkNone, BtMessage,
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtMessage, NoVartypeOptions,
                "buildlink3.mk: set",
                "*.mk: use-loadtime")
-       G.Pkgsrc.vartypes.DefineParse("VAR", lkNone, BtUnknown,
+       G.Pkgsrc.vartypes.DefineParse("VAR", BtUnknown, NoVartypeOptions,
                "buildlink3.mk: none",
                "*.mk: use")
        mklines := t.NewMkLines("buildlink3.mk",
@@ -1247,24 +1249,30 @@ func (s *Suite) Test_MkLineChecker_Check
        t := s.Init(c)
 
        t.CreateFileLines("other/package/Makefile")
-       // Must be in the filesystem because of directory references.
-       mklines := t.SetUpFileMkLines("category/package/Makefile",
-               "# dummy")
 
-       mklines.ForEach(func(mkline MkLine) {
-               ck := MkLineChecker{mklines, mkline}
+       test := func(relativePkgdir string, diagnostics ...string) {
+               // Must be in the filesystem because of directory references.
+               mklines := t.SetUpFileMkLines("category/package/Makefile",
+                       "# dummy")
+
+               checkRelativePkgdir := func(mkline MkLine) {
+                       MkLineChecker{mklines, mkline}.CheckRelativePkgdir(relativePkgdir)
+               }
 
-               ck.CheckRelativePkgdir("../pkgbase")
-               ck.CheckRelativePkgdir("../../other/package")
-               ck.CheckRelativePkgdir("../../other/does-not-exist")
-       })
+               mklines.ForEach(checkRelativePkgdir)
 
-       // FIXME: The diagnostics for does-not-exist are redundant.
-       t.CheckOutputLines(
-               "ERROR: ~/category/package/Makefile:1: Relative path \"../pkgbase\" does not exist.",
-               "WARN: ~/category/package/Makefile:1: \"../pkgbase\" is not a valid relative package directory.",
-               "ERROR: ~/category/package/Makefile:1: Relative path \"../../other/does-not-exist\" does not exist.",
-               "ERROR: ~/category/package/Makefile:1: There is no package in \"other/does-not-exist\".")
+               t.CheckOutput(diagnostics)
+       }
+
+       test("../pkgbase",
+               "ERROR: ~/category/package/Makefile:1: Relative path \"../pkgbase/Makefile\" does not exist.",
+               "WARN: ~/category/package/Makefile:1: \"../pkgbase\" is not a valid relative package directory.")
+
+       test("../../other/package",
+               nil...)
+
+       test("../../other/does-not-exist",
+               "ERROR: ~/category/package/Makefile:1: Relative path \"../../other/does-not-exist/Makefile\" does not exist.")
 }
 
 // PR pkg/46570, item 2
@@ -1278,12 +1286,14 @@ func (s *Suite) Test_MkLineChecker__uncl
        mklines.Check()
 
        t.CheckOutputLines(
+               "WARN: Makefile:2: Missing closing \"}\" for \"EGDIR/pam.d\".",
+               "WARN: Makefile:2: Invalid part \"/pam.d\" after variable name \"EGDIR\".",
+               "WARN: Makefile:2: Missing closing \"}\" for \"EGDIR/dbus-1/system.d ${EGDIR/pam.d\".",
+               "WARN: Makefile:2: Invalid part \"/dbus-1/system.d ${EGDIR/pam.d\" after variable name \"EGDIR\".",
+               "WARN: Makefile:2: Missing closing \"}\" for \"EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\".",
+               "WARN: Makefile:2: Invalid part \"/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\" after variable name \"EGDIR\".",
                "WARN: Makefile:2: EGDIRS is defined but not used.",
-               "WARN: Makefile:2: Unclosed Make variable starting at \"${EGDIR/apparmor.d $...\".",
-
-               // XXX: This warning is redundant because of the "Unclosed" warning above.
-               "WARN: Makefile:2: Internal pkglint error in MkLine.Tokenize at "+
-                       "\"${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\".")
+               "WARN: Makefile:2: EGDIR/pam.d is used but not defined.")
 }
 
 func (s *Suite) Test_MkLineChecker_Check__varuse_modifier_L(c *check.C) {
@@ -1304,13 +1314,11 @@ func (s *Suite) Test_MkLineChecker_Check
        // In line 2 the :L modifier is missing, therefore ${XKBBASE}/xkbcomp is the
        // name of another variable, and that variable is not known. Only XKBBASE is known.
        //
-       // FIXME: The below warnings are wrong because the MkParser does not recognize the
-       //  slash as part of a variable name. Because of that, parsing stops before the $.
-       //  The warning "Unclosed Make variable" wrongly assumes that any parse error from
-       //  a variable use is because of unclosed braces, which it isn't in this case.
+       // In line 2, warn about the invalid "/" as part of the variable name.
        t.CheckOutputLines(
-               "WARN: x11/xkeyboard-config/Makefile:2: Unclosed Make variable starting at \"${${XKBBASE}/xkbcomp...\".",
-               "WARN: x11/xkeyboard-config/Makefile:2: Unclosed Make variable starting at \"${${XKBBASE}/xkbcomp...\".")
+               "WARN: x11/xkeyboard-config/Makefile:2: "+
+                       "Invalid part \"/xkbcomp\" after variable name \"${XKBBASE}\".",
+               "WARN: x11/xkeyboard-config/Makefile:2: XKBBASE is used but not defined.")
 }
 
 func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparison_with_shell_command(c *check.C) {
@@ -1362,9 +1370,6 @@ func (s *Suite) Test_MkLineChecker_check
        mkline := t.NewMkLine("module.mk", 123, ".if ${PKGPATH} == \"category/package\"")
        ck := MkLineChecker{nil, mkline}
 
-       // FIXME: checkDirectiveCondEmpty cannot know whether it is empty(...) or !empty(...).
-       //  It must know that to generate the proper diagnostics.
-
        ck.checkDirectiveCondEmpty(NewMkVarUse("PKGPATH", "Mpattern"))
 
        // When the pattern contains placeholders, it cannot be converted to == or !=.
@@ -1546,21 +1551,21 @@ func (s *Suite) Test_MkLineChecker_check
        t.SetUpVartypes()
        mklines := t.SetUpFileMkLines("options.mk",
                MkRcsID,
-               "CONFIGURE_ARGS+=        ${CFLAGS:Q}",
-               "CONFIGURE_ARGS+=        ${CFLAGS:M*:Q}",
-               "CONFIGURE_ARGS+=        ${ADA_FLAGS:Q}",
-               "CONFIGURE_ARGS+=        ${ADA_FLAGS:M*:Q}",
-               "CONFIGURE_ENV+=         ${CFLAGS:Q}",
-               "CONFIGURE_ENV+=         ${CFLAGS:M*:Q}",
-               "CONFIGURE_ENV+=         ${ADA_FLAGS:Q}",
-               "CONFIGURE_ENV+=         ${ADA_FLAGS:M*:Q}")
+               "CONFIGURE_ARGS+=        CFLAGS=${CFLAGS:Q}",
+               "CONFIGURE_ARGS+=        CFLAGS=${CFLAGS:M*:Q}",
+               "CONFIGURE_ARGS+=        ADA_FLAGS=${ADA_FLAGS:Q}",
+               "CONFIGURE_ARGS+=        ADA_FLAGS=${ADA_FLAGS:M*:Q}",
+               "CONFIGURE_ENV+=         CFLAGS=${CFLAGS:Q}",
+               "CONFIGURE_ENV+=         CFLAGS=${CFLAGS:M*:Q}",
+               "CONFIGURE_ENV+=         ADA_FLAGS=${ADA_FLAGS:Q}",
+               "CONFIGURE_ENV+=         ADA_FLAGS=${ADA_FLAGS:M*:Q}")
 
        mklines.Check()
 
-       // FIXME: There should be some notes and warnings about missing :M*;
-       //  these are prevented by the PERL5 case in VariableNeedsQuoting.
        t.CheckOutputLines(
-               "WARN: ~/options.mk:4: ADA_FLAGS is used but not defined.")
+               "WARN: ~/options.mk:2: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q}.",
+               "WARN: ~/options.mk:4: ADA_FLAGS is used but not defined.",
+               "WARN: ~/options.mk:6: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q}.")
 }
 
 func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__mstar_not_needed(c *check.C) {
@@ -1570,18 +1575,14 @@ func (s *Suite) Test_MkLineChecker_check
        pkg := t.SetUpPackage("category/package",
                "MAKE_FLAGS+=\tCFLAGS=${CFLAGS:M*:Q}",
                "MAKE_FLAGS+=\tLFLAGS=${LDFLAGS:M*:Q}")
-       G.Pkgsrc.LoadInfrastructure()
-       // FIXME: It is too easy to forget this important call.
+       t.FinishSetUp()
 
        // This package is guaranteed to not use GNU_CONFIGURE.
        // Since the :M* hack is only needed for GNU_CONFIGURE, it is not necessary here.
        G.Check(pkg)
 
-       // FIXME: Duplicate diagnostics.
        t.CheckOutputLines(
                "NOTE: ~/category/package/Makefile:20: The :M* modifier is not needed here.",
-               "NOTE: ~/category/package/Makefile:20: The :M* modifier is not needed here.",
-               "NOTE: ~/category/package/Makefile:21: The :M* modifier is not needed here.",
                "NOTE: ~/category/package/Makefile:21: The :M* modifier is not needed here.")
 }
 
@@ -1590,7 +1591,7 @@ func (s *Suite) Test_MkLineChecker_check
 
        pkg := t.SetUpPackage("category/package",
                "MASTER_SITES=\t${HOMEPAGE:Q}")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -1645,16 +1646,15 @@ func (s *Suite) Test_MkLineChecker_Check
        t.CreateFileLines("mk/sys-vars.mk",
                MkRcsID,
                "CPPPATH.Linux=\t/usr/bin/cpp")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        ck := MkLineChecker{nil, t.NewMkLine("module.mk", 101, "COMMENT=\t${CPPPATH.SunOS}")}
 
        ck.CheckVaruse(NewMkVarUse("CPPPATH.SunOS"), &VarUseContext{
                vartype: &Vartype{
-                       kindOfList: lkNone,
                        basicType:  BtPathname,
+                       options:    Guessed,
                        aclEntries: nil,
-                       guessed:    true,
                },
                time:       vucTimeRun,
                quoting:    VucQuotPlain,
@@ -1676,7 +1676,7 @@ func (s *Suite) Test_MkLineChecker_Check
        t.CreateFileLines("mk/deeply/nested/infra.mk",
                MkRcsID,
                "INFRA_VAR?=\tvalue")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
        mklines := t.SetUpFileMkLines("category/package/module.mk",
                MkRcsID,
                "do-fetch:",
@@ -1696,10 +1696,9 @@ func (s *Suite) Test_MkLineChecker_Check
        t.SetUpPkgsrc()
        t.CreateFileLines("mk/defaults/mk.conf",
                "VARBASE?= /usr/pkg/var")
-       G.Pkgsrc.LoadInfrastructure()
-
        t.SetUpCommandLine("-Wall,no-space")
-       t.SetUpVartypes()
+       t.FinishSetUp()
+
        mklines := t.SetUpFileMkLines("options.mk",
                MkRcsID,
                "COMMENT=                ${VARBASE} ${X11_TYPE}",
@@ -1722,7 +1721,7 @@ func (s *Suite) Test_MkLineChecker_Check
                MkRcsID,
                "LOCALBASE?=\t${PREFIX}",
                "DEFAULT_PREFIX=\t${LOCALBASE}")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("mk/infra.mk"))
 
@@ -1743,13 +1742,13 @@ func (s *Suite) Test_MkLineChecker_Check
        t.CreateFileLines("mk/defaults/mk.conf",
                "VARBASE?=\t${PREFIX}/var",
                "PYTHON_VER?=\t36")
-       G.Pkgsrc.LoadInfrastructure()
        mklines := t.NewMkLines("file.mk",
                MkRcsID,
                "BUILD_DEFS+=\tPYTHON_VER",
                "\t: ${VARBASE}",
                "\t: ${VARBASE}",
                "\t: ${PYTHON_VER}")
+       t.FinishSetUp()
 
        mklines.Check()
 
@@ -1784,7 +1783,6 @@ func (s *Suite) Test_MkLineChecker_check
 
        MkLineChecker{nil, mkline}.Check()
 
-       // FIXME: The check is called two times, even though it only produces a single NOTE.
        t.CheckOutputLines(
                "NOTE: mk/compiler/gcc.mk:150: "+
                        "The modifier \":C/^/_asdf_/1:M_asdf_*:S/^_asdf_//\" can be written as \":[1]\".",
@@ -1853,9 +1851,7 @@ func (s *Suite) Test_MkLineChecker_check
        t := s.Init(c)
 
        t.SetUpPkgsrc()
-       G.Pkgsrc.LoadInfrastructure()
        t.SetUpCommandLine("-Wall,no-space")
-       t.SetUpVartypes()
        mklines := t.SetUpFileMkLines("module.mk",
                MkRcsID,
                "EGDIR=                  ${PREFIX}/etc/rc.d",
@@ -1864,6 +1860,7 @@ func (s *Suite) Test_MkLineChecker_check
                "DIST_SUBDIR=            ${PKGNAME}",
                "WRKSRC=                 ${PKGNAME}",
                "SITES_distfile.tar.gz=  ${MASTER_SITE_GITHUB:=user/}")
+       t.FinishSetUp()
 
        mklines.Check()
 
@@ -1897,6 +1894,7 @@ func (s *Suite) Test_MkLineChecker_check
        t.CreateFileLines("other.mk",
                MkRcsID,
                "COMMENT=\t# defined")
+       t.FinishSetUp()
 
        G.Check(t.File("filename.mk"))
        G.Check(t.File("Makefile.common"))
@@ -1913,20 +1911,18 @@ func (s *Suite) Test_MkLineChecker_check
        t := s.Init(c)
 
        t.SetUpPkgsrc()
-       G.Pkgsrc.LoadInfrastructure()
 
        t.SetUpCommandLine("-Wall,no-space")
        mklines := t.SetUpFileMkLines("module.mk",
                MkRcsID,
                "CFLAGS+=                -Wl,--rpath,${PREFIX}/lib",
                "PKG_FAIL_REASON+=       \"Group ${GAMEGRP} doesn't exist.\"")
+       t.FinishSetUp()
 
        mklines.Check()
 
-       // FIXME: Duplicate diagnostics.
        t.CheckOutputLines(
                "WARN: ~/module.mk:2: Please use ${COMPILER_RPATH_FLAG} instead of \"-Wl,--rpath,\".",
-               "WARN: ~/module.mk:3: Use of \"GAMEGRP\" is deprecated. Use GAMES_GROUP instead.",
                "WARN: ~/module.mk:3: Use of \"GAMEGRP\" is deprecated. Use GAMES_GROUP instead.")
 }
 
@@ -1964,7 +1960,6 @@ func (s *Suite) Test_MkLineChecker_Check
        t := s.Init(c)
 
        t.SetUpPkgsrc()
-       G.Pkgsrc.LoadInfrastructure()
        t.CreateFileLines("wip/package/Makefile")
        t.CreateFileLines("wip/package/module.mk")
        mklines := t.SetUpFileMkLines("category/package/module.mk",
@@ -1979,6 +1974,7 @@ func (s *Suite) Test_MkLineChecker_Check
                ".include \"../../category/../category/package/module.mk\"", // Oops
                ".include \"../../mk/bsd.prefs.mk\"",
                ".include \"../package/module.mk\"")
+       t.FinishSetUp()
 
        mklines.Check()
 
@@ -1998,10 +1994,10 @@ func (s *Suite) Test_MkLineChecker_Check
        absPath := absDir + "0f5c2d56-8a7a-4c9d-9caa-859b52bbc8c7"
 
        t.SetUpPkgsrc()
-       G.Pkgsrc.LoadInfrastructure()
        mklines := t.SetUpFileMkLines("category/package/module.mk",
                MkRcsID,
                "DISTINFO_FILE=\t"+absPath)
+       t.FinishSetUp()
 
        mklines.Check()
 
@@ -2031,7 +2027,7 @@ func (s *Suite) Test_MkLineChecker_Check
                MkRcsID)
        t.SetUpPackage("wip/package",
                ".include \"../mk/git-package.mk\"")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("wip/package"))
 
Index: pkgsrc/pkgtools/pkglint/files/patches.go
diff -u pkgsrc/pkgtools/pkglint/files/patches.go:1.29 pkgsrc/pkgtools/pkglint/files/patches.go:1.30
--- pkgsrc/pkgtools/pkglint/files/patches.go:1.29       Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/patches.go    Sat Apr 20 17:43:24 2019
@@ -187,7 +187,7 @@ func (ck *PatchChecker) checkUnifiedDiff
                line := ck.llex.CurrentLine()
                if !ck.isEmptyLine(line.Text) && !matches(line.Text, rePatchUniFileDel) {
                        line.Warnf("Empty line or end of file expected.")
-                       G.Explain(
+                       line.Explain(
                                "This line is not part of the patch anymore, although it may look so.",
                                "To make this situation clear, there should be an",
                                "empty line before this line.",
@@ -203,7 +203,7 @@ func (ck *PatchChecker) checkBeginDiff(l
 
        if !ck.seenDocumentation && patchedFiles == 0 {
                line.Errorf("Each patch must be documented.")
-               G.Explain(
+               line.Explain(
                        "Pkgsrc tries to have as few patches as possible.",
                        "Therefore, each patch must document why it is necessary.",
                        "Typical reasons are portability or security.",
@@ -252,7 +252,7 @@ func (ck *PatchChecker) checklineAdded(a
        case ftConfigure:
                if hasSuffix(addedText, ": Avoid regenerating within pkgsrc") {
                        line.Errorf("This code must not be included in patches.")
-                       G.Explain(
+                       line.Explain(
                                "It is generated automatically by pkgsrc after the patch phase.",
                                "",
                                "For more details, look for \"configure-scripts-override\" in",
Index: pkgsrc/pkgtools/pkglint/files/vartype.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype.go:1.29 pkgsrc/pkgtools/pkglint/files/vartype.go:1.30
--- pkgsrc/pkgtools/pkglint/files/vartype.go:1.29       Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/vartype.go    Sat Apr 20 17:43:25 2019
@@ -1,28 +1,35 @@
 package pkglint
 
-import "path"
+import (
+       "path"
+       "strings"
+)
 
 // Vartype is a combination of a data type and a permission specification.
 // See vardefs.go for examples, and vartypecheck.go for the implementation.
 type Vartype struct {
-       kindOfList KindOfList
        basicType  *BasicType
+       options    vartypeOptions
        aclEntries []ACLEntry
-       guessed    bool
 }
 
-type KindOfList uint8
+type vartypeOptions uint8
 
 const (
-       // lkNone is a plain data type, no list at all.
-       lkNone KindOfList = iota
-
-       // lkShell is a compound type, consisting of several space-separated elements.
-       // Elements can have embedded spaces by enclosing them in quotes, like in the shell.
+       // List is a compound type, consisting of several space-separated elements.
+       // Elements can have embedded spaces by enclosing them in double or single
+       // quotes, like in the shell.
        //
        // These lists are used in the :M, :S modifiers, in .for loops,
        // and as lists of arbitrary things.
-       lkShell
+       List vartypeOptions = 1 << iota
+
+       Guessed
+       PackageSettable
+       UserSettable
+       SystemProvided
+       CommandLineProvided
+       NoVartypeOptions = 0
 )
 
 type ACLEntry struct {
@@ -74,6 +81,13 @@ func (perms ACLPermissions) HumanString(
                ifelseStr(perms.Contains(aclpUse), "used", ""))
 }
 
+func (vt *Vartype) List() bool                { return vt.options&List != 0 }
+func (vt *Vartype) Guessed() bool             { return vt.options&Guessed != 0 }
+func (vt *Vartype) PackageSettable() bool     { return vt.options&PackageSettable != 0 }
+func (vt *Vartype) UserSettable() bool        { return vt.options&UserSettable != 0 }
+func (vt *Vartype) SystemProvided() bool      { return vt.options&SystemProvided != 0 }
+func (vt *Vartype) CommandLineProvided() bool { return vt.options&CommandLineProvided != 0 }
+
 func (vt *Vartype) EffectivePermissions(basename string) ACLPermissions {
        for _, aclEntry := range vt.aclEntries {
                if m, _ := path.Match(aclEntry.glob, basename); m {
@@ -153,26 +167,16 @@ func (vt *Vartype) AlternativeFiles(perm
        return positive + ", but not " + negative
 }
 
-// IsConsideredList returns whether the type is considered a list.
-//
-// FIXME: Explain why this method is necessary. IsList is clear, and MayBeAppendedTo also,
-//  but this in-between state needs a decent explanation.
-//  Probably MkLineChecker.checkVartype needs to be revisited completely.
-func (vt *Vartype) IsConsideredList() bool {
-       if vt.kindOfList == lkShell {
+func (vt *Vartype) MayBeAppendedTo() bool {
+       if vt.List() {
                return true
        }
+
        switch vt.basicType {
        case BtAwkCommand, BtSedCommands, BtShellCommand, BtShellCommands, BtConfFiles:
                return true
        }
-       return false
-}
 
-func (vt *Vartype) MayBeAppendedTo() bool {
-       if vt.kindOfList != lkNone || vt.IsConsideredList() {
-               return true
-       }
        switch vt.basicType {
        case BtComment, BtLicense:
                return true
@@ -181,9 +185,32 @@ func (vt *Vartype) MayBeAppendedTo() boo
 }
 
 func (vt *Vartype) String() string {
-       listPrefix := [...]string{"", "List of "}[vt.kindOfList]
-       guessedSuffix := ifelseStr(vt.guessed, " (guessed)", "")
-       return listPrefix + vt.basicType.name + guessedSuffix
+       var opts []string
+       if vt.List() {
+               opts = append(opts, "list")
+       }
+       if vt.Guessed() {
+               opts = append(opts, "guessed")
+       }
+       if vt.PackageSettable() {
+               opts = append(opts, "package-settable")
+       }
+       if vt.UserSettable() {
+               opts = append(opts, "user-settable")
+       }
+       if vt.SystemProvided() {
+               opts = append(opts, "system-provided")
+       }
+       if vt.CommandLineProvided() {
+               opts = append(opts, "command-line-provided")
+       }
+
+       optsSuffix := ""
+       if len(opts) > 0 {
+               optsSuffix = " (" + strings.Join(opts, ", ") + ")"
+       }
+
+       return vt.basicType.name + optsSuffix
 }
 
 func (vt *Vartype) IsShell() bool {

Index: pkgsrc/pkgtools/pkglint/files/category.go
diff -u pkgsrc/pkgtools/pkglint/files/category.go:1.18 pkgsrc/pkgtools/pkglint/files/category.go:1.19
--- pkgsrc/pkgtools/pkglint/files/category.go:1.18      Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/category.go   Sat Apr 20 17:43:24 2019
@@ -1,6 +1,10 @@
 package pkglint
 
-import "netbsd.org/pkglint/textproc"
+import (
+       "fmt"
+       "netbsd.org/pkglint/textproc"
+       "strings"
+)
 
 func CheckdirCategory(dir string) {
        if trace.Tracing {
@@ -25,18 +29,18 @@ func CheckdirCategory(dir string) {
                lex := textproc.NewLexer(mkline.Value())
                valid := textproc.NewByteSet("--- '(),/0-9A-Za-z")
                invalid := valid.Inverse()
-               uni := ""
+               var uni strings.Builder
 
                for !lex.EOF() {
                        _ = lex.NextBytesSet(valid)
                        ch := lex.NextByteSet(invalid)
                        if ch != -1 {
-                               uni += sprintf(" %U", ch)
+                               _, _ = fmt.Fprintf(&uni, " %U", ch)
                        }
                }
 
-               if uni != "" {
-                       mkline.Warnf("%s contains invalid characters (%s).", mkline.Varname(), uni[1:])
+               if uni.Len() > 0 {
+                       mkline.Warnf("%s contains invalid characters (%s).", mkline.Varname(), uni.String()[1:])
                }
 
        } else {

Index: pkgsrc/pkgtools/pkglint/files/check_test.go
diff -u pkgsrc/pkgtools/pkglint/files/check_test.go:1.37 pkgsrc/pkgtools/pkglint/files/check_test.go:1.38
--- pkgsrc/pkgtools/pkglint/files/check_test.go:1.37    Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/check_test.go Sat Apr 20 17:43:24 2019
@@ -56,7 +56,7 @@ func (s *Suite) Init(c *check.C) *Tester
 }
 
 func (s *Suite) SetUpTest(c *check.C) {
-       t := Tester{c: c}
+       t := Tester{c: c, testName: c.TestName()}
        s.Tester = &t
 
        G = NewPkglint()
@@ -89,7 +89,11 @@ func (s *Suite) TearDownTest(c *check.C)
        t.c = nil // No longer usable; see https://github.com/go-check/check/issues/22
 
        if err := os.Chdir(t.prevdir); err != nil {
-               _, _ = fmt.Fprintf(os.Stderr, "Cannot chdir back to previous dir: %s", err)
+               t.Errorf("Cannot chdir back to previous dir: %s", err)
+       }
+
+       if t.seenSetupPkgsrc > 0 && !t.seenFinish && !t.seenMain {
+               t.Errorf("After t.SetupPkgsrc(), t.FinishSetUp() or t.Main() must be called.")
        }
 
        if out := t.Output(); out != "" {
@@ -121,12 +125,18 @@ func Test(t *testing.T) { check.TestingT
 // all the test methods, which makes it difficult to find
 // a method by auto-completion.
 type Tester struct {
+       c        *check.C // Only usable during the test method itself
+       testName string
+
        stdout  bytes.Buffer
        stderr  bytes.Buffer
        tmpdir  string
-       c       *check.C // Only usable during the test method itself
-       prevdir string   // The current working directory before the test started
-       relCwd  string   // See Tester.Chdir
+       prevdir string // The current working directory before the test started
+       relCwd  string // See Tester.Chdir
+
+       seenSetupPkgsrc int
+       seenFinish      bool
+       seenMain        bool
 }
 
 // SetUpCommandLine simulates a command line for the remainder of the test.
@@ -294,6 +304,8 @@ func (t *Tester) SetUpPkgsrc() {
 
        // Category Makefiles require this file for the common definitions.
        t.CreateFileLines("mk/misc/category.mk")
+
+       t.seenSetupPkgsrc++
 }
 
 // SetUpCategory makes the given category valid by creating a dummy Makefile.
@@ -316,7 +328,8 @@ func (t *Tester) SetUpCategory(name stri
 // Returns the path to the package, ready to be used with Pkglint.Check.
 //
 // After calling this method, individual files can be overwritten as necessary.
-// Then, G.Pkgsrc.LoadInfrastructure should be called to load all the files.
+// At the end of the setup phase, t.FinishSetUp() must be called to load all
+// the files.
 func (t *Tester) SetUpPackage(pkgpath string, makefileLines ...string) string {
 
        category := path.Dir(pkgpath)
@@ -375,7 +388,7 @@ func (t *Tester) SetUpPackage(pkgpath st
 line:
        for _, line := range makefileLines {
                if m, prefix := match1(line, `^#?(\w+=)`); m {
-                       for i, existingLine := range mlines {
+                       for i, existingLine := range mlines[:19] {
                                if hasPrefix(strings.TrimPrefix(existingLine, "#"), prefix) {
                                        mlines[i] = line
                                        continue line
@@ -619,6 +632,37 @@ func (s *Suite) Test_Tester_SetUpHierarc
                "NOTE: subdir/env.mk:1: Text is: VAR= env")
 }
 
+func (t *Tester) FinishSetUp() {
+       if t.seenSetupPkgsrc == 0 {
+               t.Errorf("Unnecessary t.FinishSetUp() since t.SetUpPkgsrc() has not been called.")
+       }
+
+       if !t.seenFinish {
+               t.seenFinish = true
+               G.Pkgsrc.LoadInfrastructure()
+       } else {
+               t.Errorf("Redundant t.FinishSetup() since it was called multiple times.")
+       }
+}
+
+// Main runs the pkglint main program with the given command line arguments.
+func (t *Tester) Main(args ...string) int {
+       if t.seenFinish && !t.seenMain {
+               t.Errorf("Calling t.FinishSetup() before t.Main() is redundant " +
+                       "since t.Main() loads the pkgsrc infrastructure.")
+       }
+
+       t.seenMain = true
+
+       // Reset the logger, for tests where t.Main is called multiple times.
+       G.errors = 0
+       G.warnings = 0
+       G.logged = Once{}
+
+       argv := append([]string{"pkglint"}, args...)
+       return G.Main(argv...)
+}
+
 // Check delegates a check to the check.Check function.
 // Thereby, there is no need to distinguish between c.Check and t.Check
 // in the test code.
@@ -626,6 +670,10 @@ func (t *Tester) Check(obj interface{}, 
        return t.c.Check(obj, checker, args...)
 }
 
+func (t *Tester) Errorf(format string, args ...interface{}) {
+       _, _ = fmt.Fprintf(os.Stderr, "In %s: %s\n", t.testName, sprintf(format, args...))
+}
+
 // ExpectFatal runs the given action and expects that this action calls
 // Line.Fatalf or uses some other way to panic with a pkglintFatal.
 //
@@ -722,7 +770,7 @@ func (t *Tester) NewMkLine(filename stri
                hasSuffix(basename, ".mk") || basename == "Makefile" || hasPrefix(basename, "Makefile."),
                "filename %q must be realistic, otherwise the variable permissions are wrong", filename)
 
-       return NewMkLine(t.NewLine(filename, lineno, text))
+       return MkLineParser{}.Parse(t.NewLine(filename, lineno, text))
 }
 
 func (t *Tester) NewShellLineChecker(mklines MkLines, filename string, lineno int, text string) *ShellLineChecker {
@@ -890,3 +938,11 @@ func (t *Tester) CheckFileLinesDetab(rel
 
        t.Check(detabbedLines, deepEquals, lines)
 }
+
+// Use marks all passed functions as used for the Go compiler.
+//
+// This means that the test cases that follow do not have to use each of them,
+// and this in turn allows uninteresting test cases to be deleted during
+// development.
+func (t *Tester) Use(functions ...interface{}) {
+}
Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.37 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.38
--- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.37  Mon Apr 15 06:11:32 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go       Sat Apr 20 17:43:24 2019
@@ -13,7 +13,7 @@ import (
 func (s *Suite) Test_Pkglint_Main__help(c *check.C) {
        t := s.Init(c)
 
-       exitCode := G.Main("pkglint", "-h")
+       exitCode := t.Main("-h")
 
        c.Check(exitCode, equals, 0)
        t.CheckOutputLines(
@@ -58,7 +58,7 @@ func (s *Suite) Test_Pkglint_Main__help(
 func (s *Suite) Test_Pkglint_Main__version(c *check.C) {
        t := s.Init(c)
 
-       exitcode := G.Main("pkglint", "--version")
+       exitcode := t.Main("--version")
 
        c.Check(exitcode, equals, 0)
        t.CheckOutputLines(
@@ -68,7 +68,7 @@ func (s *Suite) Test_Pkglint_Main__versi
 func (s *Suite) Test_Pkglint_Main__no_args(c *check.C) {
        t := s.Init(c)
 
-       exitcode := G.Main("pkglint")
+       exitcode := t.Main()
 
        // The "." from the error message is the implicit argument added in Pkglint.Main.
        c.Check(exitcode, equals, 1)
@@ -76,7 +76,7 @@ func (s *Suite) Test_Pkglint_Main__no_ar
                "FATAL: \".\" must be inside a pkgsrc tree.")
 }
 
-func (s *Suite) Test_Pkglint_Main__only(c *check.C) {
+func (s *Suite) Test_Pkglint_ParseCommandLine__only(c *check.C) {
        t := s.Init(c)
 
        exitcode := G.ParseCommandLine([]string{"pkglint", "-Wall", "--only", ":Q", "--version"})
@@ -92,7 +92,7 @@ func (s *Suite) Test_Pkglint_Main__only(
 func (s *Suite) Test_Pkglint_Main__unknown_option(c *check.C) {
        t := s.Init(c)
 
-       exitcode := G.Main("pkglint", "--unknown-option")
+       exitcode := t.Main("--unknown-option")
 
        c.Check(exitcode, equals, 1)
        c.Check(t.Output(), check.Matches,
@@ -112,7 +112,7 @@ func (s *Suite) Test_Pkglint_Main__panic
        G.out = nil // Force an error that cannot happen in practice.
 
        c.Check(
-               func() { G.Main("pkglint", pkg) },
+               func() { t.Main(pkg) },
                check.PanicMatches, `(?s).*\bnil pointer\b.*`)
 }
 
@@ -131,8 +131,6 @@ func (s *Suite) Test_Pkglint_Main__compl
        // This is typical of the pkglint tests.
        t.SetUpPkgsrc()
 
-       // FIXME: pkglint should warn that the latest version in this file
-       // (1.10) doesn't match the current version in the package (1.11).
        t.CreateFileLines("doc/CHANGES-2018",
                RcsID,
                "",
@@ -230,9 +228,12 @@ func (s *Suite) Test_Pkglint_Main__compl
                "Size (checkperms-1.12.tar.gz) = 6621 bytes",
                "SHA1 (patch-checkperms.c) = asdfasdf") // Invalid SHA-1 checksum
 
-       G.Main("pkglint", "-Wall", "-Call", t.File("sysutils/checkperms"))
+       t.Main("-Wall", "-Call", t.File("sysutils/checkperms"))
 
        t.CheckOutputLines(
+               "NOTE: ~/sysutils/checkperms/Makefile:3: "+
+                       "Package version \"1.11\" is greater than the latest \"1.10\" "+
+                       "from ../../doc/CHANGES-2018:5.",
                "WARN: ~/sysutils/checkperms/Makefile:3: "+
                        "This package should be updated to 1.13 ([supports more file formats]).",
                "ERROR: ~/sysutils/checkperms/Makefile:4: Invalid category \"tools\".",
@@ -270,6 +271,7 @@ func (s *Suite) Test_Pkglint_Main__compl
 //
 // See https://github.com/rillig/gobco for the tool to measure the branch coverage.
 func (s *Suite) Test_Pkglint__realistic(c *check.C) {
+       t := s.Init(c)
 
        if cwd := os.Getenv("PKGLINT_TESTDIR"); cwd != "" {
                err := os.Chdir(cwd)
@@ -281,7 +283,7 @@ func (s *Suite) Test_Pkglint__realistic(
                G.out = NewSeparatorWriter(os.Stdout)
                G.err = NewSeparatorWriter(os.Stderr)
                trace.Out = os.Stdout
-               G.Main(append([]string{"pkglint"}, strings.Fields(cmdline)...)...)
+               t.Main(strings.Fields(cmdline)...)
        }
 }
 
@@ -308,6 +310,7 @@ func (s *Suite) Test_Pkglint_Check__empt
 
        t.SetUpPkgsrc()
        t.CreateFileLines("category/package/CVS/Entries")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -320,6 +323,7 @@ func (s *Suite) Test_Pkglint_Check__file
 
        t.SetUpPkgsrc()
        t.CreateFileLines("category/package/files/README.md")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package/files"))
 
@@ -333,6 +337,7 @@ func (s *Suite) Test_Pkglint_Check__patc
 
        t.SetUpPkgsrc()
        t.CreateFileDummyPatch("category/package/patches/patch-README.md")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package/patches"))
 
@@ -348,6 +353,7 @@ func (s *Suite) Test_Pkglint_Check__manu
        t.SetUpPackage("category/package")
        t.CreateFileLines("category/package/patches/unknown-file")
        t.CreateFileLines("category/package/patches/manual-configure")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -361,6 +367,7 @@ func (s *Suite) Test_Pkglint_Check__doc_
        t := s.Init(c)
 
        t.SetUpPkgsrc()
+       t.FinishSetUp()
 
        G.Check(G.Pkgsrc.File("doc/TODO"))
 
@@ -569,7 +576,7 @@ func (s *Suite) Test_Pkglint_checkReg__a
        lines := t.SetUpFileLines("category/package/ALTERNATIVES",
                "bin/tar bin/gnu-tar")
 
-       G.Main("pkglint", lines.FileName)
+       t.Main(lines.FileName)
 
        t.CheckOutputLines(
                "ERROR: ~/category/package/ALTERNATIVES:1: Alternative implementation \"bin/gnu-tar\" must be an absolute path.",
@@ -583,7 +590,7 @@ func (s *Suite) Test_Pkglint__profiling(
        t.SetUpPkgsrc()
        t.Chdir(".")
 
-       G.Main("pkglint", "--profiling")
+       t.Main("--profiling")
 
        // Pkglint always writes the profiling data into the current directory.
        // TODO: Make the location of the profiling log a mandatory parameter.
@@ -602,11 +609,10 @@ func (s *Suite) Test_Pkglint__profiling(
 func (s *Suite) Test_Pkglint__profiling_error(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpPkgsrc()
        t.Chdir(".")
        t.CreateFileLines("pkglint.pprof/file")
 
-       exitcode := G.Main("pkglint", "--profiling")
+       exitcode := t.Main("--profiling")
 
        c.Check(exitcode, equals, 1)
        c.Check(t.Output(), check.Matches,
@@ -620,7 +626,7 @@ func (s *Suite) Test_Pkglint_checkReg__i
        t.Chdir("category/package")
        t.CreateFileLines("log")
 
-       G.Main("pkglint")
+       t.Main()
 
        t.CheckOutputLines(
                "WARN: log: Unexpected file found.",
@@ -750,6 +756,7 @@ func (s *Suite) Test_Pkglint_checkReg__o
                "#! /bin/sh")
        t.CreateFileLines("category/package/DEINSTALL",
                "#! /bin/sh")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -765,6 +772,7 @@ func (s *Suite) Test_Pkglint_Check__inva
        t.CreateFileLines("category/package/Makefile~")
        t.CreateFileLines("category/package/Makefile.orig")
        t.CreateFileLines("category/package/Makefile.rej")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -782,7 +790,7 @@ func (s *Suite) Test_Pkglint_checkDirent
        t.SetUpPkgsrc()
        t.CreateFileLines("category/package/files/subdir/file")
        t.CreateFileLines("category/package/files/subdir/subsub/file")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.checkDirent(t.File("category/package/options.mk"), 0444)
        G.checkDirent(t.File("category/package/files/subdir"), 0555|os.ModeDir)
@@ -805,7 +813,7 @@ func (s *Suite) Test_Pkglint_checkDirent
                MkRcsID)
        t.CreateFileLines("category/package/unexpected.txt",
                RcsID)
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.checkDirent(t.File("doc/CHANGES-2018"), 0444)
        G.checkDirent(t.File("category/package/buildlink3.mk"), 0444)
@@ -861,21 +869,16 @@ func (s *Suite) Test_Pkglint_checkReg__r
        c.Check(err, check.IsNil)
 
        t.SetUpPkgsrc()
-       G.Pkgsrc.LoadInfrastructure()
        t.Chdir(".")
 
-       G.Main("pkglint", "category/package", "wip/package")
+       t.Main("category/package", "wip/package")
 
        t.CheckOutputLines(
                "ERROR: category/package/README: Packages in main pkgsrc must not have a README file.",
                "ERROR: category/package/TODO: Packages in main pkgsrc must not have a TODO file.",
                "2 errors and 0 warnings found.")
 
-       // FIXME: Do this resetting properly
-       G.errors = 0
-       G.warnings = 0
-       G.logged = Once{}
-       G.Main("pkglint", "--import", "category/package", "wip/package")
+       t.Main("--import", "category/package", "wip/package")
 
        t.CheckOutputLines(
                "ERROR: category/package/README: Packages in main pkgsrc must not have a README file.",
@@ -983,6 +986,7 @@ func (s *Suite) Test_Pkglint_checkdirPac
                "COMMENT=\tComment",
                "LICENSE=\t2-clause-bsd",
                "PKGDIR=\t\t../../other/package")
+       t.FinishSetUp()
 
        // DISTINFO_FILE is resolved relative to PKGDIR,
        // the other locations are resolved relative to the package base directory.
@@ -998,6 +1002,7 @@ func (s *Suite) Test_Pkglint_checkdirPac
        pkg := t.SetUpPackage("category/package")
        t.CreateFileDummyPatch("category/package/patches/patch-aa")
        t.Remove("category/package/distinfo")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -1034,13 +1039,14 @@ func (s *Suite) Test_Pkglint_checkdirPac
        pkg := t.SetUpPackage("category/package",
                ".include \"../../mk/bsd.prefs.mk\"",
                "",
-               "RUBY_VERSIONS_ACCEPTED=\t22 24 25 26", // As of 2018.
+               "RUBY_VERSIONS_ACCEPTED=\t22 23 24 25", // As of 2018.
                ".for rv in ${RUBY_VERSIONS_ACCEPTED}",
                "RUBY_VER?=\t\t${rv}",
                ".endfor",
                "",
                "RUBY_PKGDIR=\t../../lang/ruby-${RUBY_VER}-base",
                "DISTINFO_FILE=\t${RUBY_PKGDIR}/distinfo")
+       t.FinishSetUp()
 
        // As of January 2019, pkglint cannot resolve the location of DISTINFO_FILE completely
        // because the variable \"rv\" comes from a .for loop.
@@ -1059,6 +1065,7 @@ func (s *Suite) Test_Pkglint_checkdirPac
        pkg := t.SetUpPackage("category/package")
        t.CreateFileLines("category/package/ALTERNATIVES",
                "bin/wrapper bin/wrapper-impl")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -1074,7 +1081,7 @@ func (s *Suite) Test_Pkglint_checkdirPac
 
        t.SetUpPackage("category/package",
                "DISTINFO_FILE=\tnonexistent")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -1146,6 +1153,7 @@ func (s *Suite) Test_Main(c *check.C) {
 
        t.SetUpPackage("category/package")
        t.Chdir("category/package")
+       t.FinishSetUp()
 
        runMain := func(out *os.File, commandLine ...string) {
                args := os.Args
Index: pkgsrc/pkgtools/pkglint/files/shell.go
diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.37 pkgsrc/pkgtools/pkglint/files/shell.go:1.38
--- pkgsrc/pkgtools/pkglint/files/shell.go:1.37 Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/shell.go      Sat Apr 20 17:43:24 2019
@@ -18,13 +18,24 @@ import (
 type ShellLineChecker struct {
        MkLines MkLines
        mkline  MkLine
+
+       // checkVarUse is set to false when checking a single shell word
+       // in order to skip duplicate warnings in variable assignments.
+       checkVarUse bool
 }
 
 func NewShellLineChecker(mklines MkLines, mkline MkLine) *ShellLineChecker {
-       return &ShellLineChecker{mklines, mkline}
+       return &ShellLineChecker{mklines, mkline, true}
+}
+
+func (ck *ShellLineChecker) Warnf(format string, args ...interface{}) {
+       ck.mkline.Warnf(format, args...)
+}
+func (ck *ShellLineChecker) Explain(explanation ...string) {
+       ck.mkline.Explain(explanation...)
 }
 
-var shellCommandsType = &Vartype{lkNone, BtShellCommands, []ACLEntry{{"*", aclpAllRuntime}}, false}
+var shellCommandsType = &Vartype{BtShellCommands, NoVartypeOptions, []ACLEntry{{"*", aclpAllRuntime}}}
 var shellWordVuc = &VarUseContext{shellCommandsType, vucTimeUnknown, VucQuotPlain, false}
 
 func (ck *ShellLineChecker) CheckWord(token string, checkQuoting bool, time ToolTime) {
@@ -42,7 +53,9 @@ func (ck *ShellLineChecker) CheckWord(to
        // to the MkLineChecker. Examples for these are ${VAR:Mpattern} or $@.
        p := NewMkParser(nil, token, false)
        if varuse := p.VarUse(); varuse != nil && p.EOF() {
-               MkLineChecker{ck.MkLines, ck.mkline}.CheckVaruse(varuse, shellWordVuc)
+               if ck.checkVarUse {
+                       MkLineChecker{ck.MkLines, ck.mkline}.CheckVaruse(varuse, shellWordVuc)
+               }
                return
        }
 
@@ -58,8 +71,7 @@ func (ck *ShellLineChecker) CheckWord(to
 }
 
 func (ck *ShellLineChecker) checkWordQuoting(token string, checkQuoting bool, time ToolTime) {
-       line := ck.mkline.Line
-       tok := NewShTokenizer(line, token, false)
+       tok := NewShTokenizer(ck.mkline.Line, token, false)
 
        atoms := tok.ShAtoms()
        quoting := shqPlain
@@ -93,8 +105,8 @@ outer:
                                ck.checkShVarUsePlain(atom, checkQuoting)
 
                        case atom.Type == shtSubshell:
-                               line.Warnf("Invoking subshells via $(...) is not portable enough.")
-                               G.Explain(
+                               ck.Warnf("Invoking subshells via $(...) is not portable enough.")
+                               ck.Explain(
                                        "The Solaris /bin/sh does not know this way to execute a command in a subshell.",
                                        "Please use backticks (`...`) as a replacement.")
 
@@ -116,21 +128,20 @@ outer:
        }
 
        if trimHspace(tok.Rest()) != "" {
-               line.Warnf("Internal pkglint error in ShellLine.CheckWord at %q (quoting=%s), rest: %s",
+               ck.Warnf("Internal pkglint error in ShellLine.CheckWord at %q (quoting=%s), rest: %s",
                        token, quoting, tok.Rest())
        }
 }
 
 func (ck *ShellLineChecker) checkShVarUsePlain(atom *ShAtom, checkQuoting bool) {
-       line := ck.mkline.Line
        shVarname := atom.ShVarname()
 
        if shVarname == "@" {
-               line.Warnf("The $@ shell variable should only be used in double quotes.")
+               ck.Warnf("The $@ shell variable should only be used in double quotes.")
 
        } else if G.Opts.WarnQuoting && checkQuoting && ck.variableNeedsQuoting(shVarname) {
-               line.Warnf("Unquoted shell variable %q.", shVarname)
-               G.Explain(
+               ck.Warnf("Unquoted shell variable %q.", shVarname)
+               ck.Explain(
                        "When a shell variable contains whitespace, it is expanded (split into multiple words)",
                        "when it is written as $variable in a shell script.",
                        "If that is not intended, it should be surrounded by quotation marks, like \"$variable\".",
@@ -146,7 +157,7 @@ func (ck *ShellLineChecker) checkShVarUs
        }
 
        if shVarname == "?" {
-               line.Warnf("The $? shell variable is often not available in \"set -e\" mode.")
+               ck.Warnf("The $? shell variable is often not available in \"set -e\" mode.")
                // TODO: Explain how to properly fix this warning.
                // TODO: Make sure the warning is only shown when applicable.
        }
@@ -162,10 +173,11 @@ func (ck *ShellLineChecker) checkVaruseT
        varname := varuse.varname
 
        if varname == "@" {
-               ck.mkline.Warnf("Please use \"${.TARGET}\" instead of \"$@\".")
-               G.Explain(
+               ck.Warnf("Please use \"${.TARGET}\" instead of \"$@\".")
+               ck.Explain(
                        "The variable $@ can easily be confused with the shell variable of",
                        "the same name, which has a completely different meaning.")
+
                varname = ".TARGET"
                varuse = &MkVarUse{varname, varuse.modifiers}
        }
@@ -179,13 +191,15 @@ func (ck *ShellLineChecker) checkVaruseT
 
        case quoting == shqDquot && varuse.IsQ():
                ck.mkline.Warnf("The :Q modifier should not be used inside double quotes.")
-               G.Explain(
+               ck.mkline.Explain(
                        "To fix this warning, either remove the :Q or the double quotes.",
                        "In most cases, it is more appropriate to remove the double quotes.")
        }
 
-       vuc := VarUseContext{shellCommandsType, vucTimeUnknown, quoting.ToVarUseContext(), true}
-       MkLineChecker{ck.MkLines, ck.mkline}.CheckVaruse(varuse, &vuc)
+       if ck.checkVarUse {
+               vuc := VarUseContext{shellCommandsType, vucTimeUnknown, quoting.ToVarUseContext(), true}
+               MkLineChecker{ck.MkLines, ck.mkline}.CheckVaruse(varuse, &vuc)
+       }
 
        return true
 }
@@ -237,7 +251,7 @@ func (ck *ShellLineChecker) unescapeBack
                // pkglint has a real parser for all shell constructs.
                if atom.Quoting == shqDquotBackt && matches(atom.MkText, `(^|[^\\])"`) {
                        line.Warnf("Double quotes inside backticks inside double quotes are error prone.")
-                       G.Explain(
+                       line.Explain(
                                "According to the SUSv3, they produce undefined results.",
                                "",
                                "See the paragraph starting \"Within the backquoted ...\" in",
@@ -275,7 +289,7 @@ func (ck *ShellLineChecker) CheckShellCo
        // TODO: Now that a shell command parser is available, be more precise in the condition.
        if contains(shelltext, "${SED}") && contains(shelltext, "${MV}") {
                line.Notef("Please use the SUBST framework instead of ${SED} and ${MV}.")
-               G.Explain(
+               line.Explain(
                        "Using the SUBST framework instead of explicit commands is easier",
                        "to understand, since all the complexity of using sed and mv is",
                        "hidden behind the scenes.",
@@ -283,7 +297,7 @@ func (ck *ShellLineChecker) CheckShellCo
                        // TODO: Provide a copy-and-paste example.
                        sprintf("Run %q for more information.", makeHelp("subst")))
                if contains(shelltext, "#") {
-                       G.Explain(
+                       line.Explain(
                                "When migrating to the SUBST framework, pay attention to \"#\" characters.",
                                "In shell commands, make(1) does not interpret them as",
                                "comment character, but in variable assignments it does.",
@@ -348,7 +362,7 @@ func (ck *ShellLineChecker) CheckShellCo
                }
        }
        walker.Callback.Pipeline = func(pipeline *MkShPipeline) {
-               spc.checkPipeExitcode(line, pipeline)
+               spc.checkPipeExitcode(pipeline)
        }
        walker.Callback.Word = func(word *ShToken) {
                // TODO: Try to replace false with true here; it had been set to false
@@ -397,7 +411,7 @@ func (ck *ShellLineChecker) checkHiddenA
                                break
                        default:
                                ck.mkline.Warnf("The shell command %q should not be hidden.", cmd)
-                               G.Explain(
+                               ck.mkline.Explain(
                                        "Hidden shell commands do not appear on the terminal",
                                        "or in the log file when they are executed.",
                                        "When they fail, the error message cannot be related to the command,",
@@ -411,7 +425,7 @@ func (ck *ShellLineChecker) checkHiddenA
 
        if contains(hiddenAndSuppress, "-") {
                ck.mkline.Warnf("Using a leading \"-\" to suppress errors is deprecated.")
-               G.Explain(
+               ck.mkline.Explain(
                        "If you really want to ignore any errors from this command, append \"|| ${TRUE}\" to the command.",
                        "This is more visible than a single hyphen, and it should be.")
        }
@@ -473,7 +487,7 @@ func (scc *SimpleCommandChecker) checkCo
        default:
                if G.Opts.WarnExtra && !(scc.MkLines != nil && scc.MkLines.indentation.DependsOn("OPSYS")) {
                        scc.mkline.Warnf("Unknown shell command %q.", shellword)
-                       G.Explain(
+                       scc.mkline.Explain(
                                "To make the package portable to all platforms that pkgsrc supports,",
                                "it should only use shell commands that are covered by the tools framework.",
                                "",
@@ -512,8 +526,8 @@ func (scc *SimpleCommandChecker) handleF
        shellword := scc.strcmd.Name
        switch path.Base(shellword) {
        case "mktexlsr", "texconfig":
-               scc.mkline.Errorf("%q must not be used in Makefiles.", shellword)
-               G.Explain(
+               scc.Errorf("%q must not be used in Makefiles.", shellword)
+               scc.Explain(
                        "This command may only appear in INSTALL scripts, not in the package Makefile,",
                        "so that the package also works if it is installed as a binary package.")
                return true
@@ -587,7 +601,7 @@ func (scc *SimpleCommandChecker) handleC
        }
 
        if semicolon || multiline {
-               G.Explain(
+               scc.Explain(
                        "When a shell command is split into multiple lines that are",
                        "continued with a backslash, they will nevertheless be converted to",
                        "a single line before the shell sees them.",
@@ -615,8 +629,8 @@ func (scc *SimpleCommandChecker) checkRe
        isSubst := false
        for _, arg := range scc.strcmd.Args {
                if G.Testing && isSubst && !matches(arg, `"^[\"\'].*[\"\']$`) {
-                       scc.mkline.Warnf("Substitution commands like %q should always be quoted.", arg)
-                       G.Explain(
+                       scc.Warnf("Substitution commands like %q should always be quoted.", arg)
+                       scc.Explain(
                                "Usually these substitution commands contain characters like '*' or",
                                "other shell metacharacters that might lead to lookup of matching",
                                "filenames and then expand to more than one word.")
@@ -648,8 +662,8 @@ func (scc *SimpleCommandChecker) checkAu
                if !contains(arg, "$$") && !matches(arg, `\$\{[_.]*[a-z]`) {
                        if m, dirname := match1(arg, `^(?:\$\{DESTDIR\})?\$\{PREFIX(?:|:Q)\}/(.*)`); m {
                                if G.Pkg != nil && G.Pkg.Plist.Dirs[dirname] {
-                                       scc.mkline.Notef("You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= %s\" instead of %q.", dirname, cmdname)
-                                       G.Explain(
+                                       scc.Notef("You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= %s\" instead of %q.", dirname, cmdname)
+                                       scc.Explain(
                                                "Many packages include a list of all needed directories in their",
                                                "PLIST file.",
                                                "In such a case, you can just set AUTO_MKDIRS=yes and be done.",
@@ -662,8 +676,8 @@ func (scc *SimpleCommandChecker) checkAu
                                                "of the many INSTALL_*_DIR variables is appropriate, since",
                                                "INSTALLATION_DIRS takes care of that.")
                                } else {
-                                       scc.mkline.Notef("You can use \"INSTALLATION_DIRS+= %s\" instead of %q.", dirname, cmdname)
-                                       G.Explain(
+                                       scc.Notef("You can use \"INSTALLATION_DIRS+= %s\" instead of %q.", dirname, cmdname)
+                                       scc.Explain(
                                                "To create directories during installation, it is easier to just",
                                                "list them in INSTALLATION_DIRS than to execute the commands",
                                                "explicitly.",
@@ -694,7 +708,7 @@ func (scc *SimpleCommandChecker) checkIn
                        default:
                                if prevdir != "" {
                                        scc.mkline.Warnf("The INSTALL_*_DIR commands can only handle one directory at a time.")
-                                       G.Explain(
+                                       scc.mkline.Explain(
                                                "Many implementations of install(1) can handle more, but pkgsrc aims",
                                                "at maximum portability.")
                                        return
@@ -711,8 +725,8 @@ func (scc *SimpleCommandChecker) checkPa
        }
 
        if (scc.strcmd.Name == "${PAX}" || scc.strcmd.Name == "pax") && scc.strcmd.HasOption("-pe") {
-               scc.mkline.Warnf("Please use the -pp option to pax(1) instead of -pe.")
-               G.Explain(
+               scc.Warnf("Please use the -pp option to pax(1) instead of -pe.")
+               scc.mkline.Explain(
                        "The -pe option tells pax to preserve the ownership of the files.",
                        "",
                        "When extracting distfiles as root user, this means that whatever numeric uid was",
@@ -734,6 +748,19 @@ func (scc *SimpleCommandChecker) checkEc
        }
 }
 
+func (scc *SimpleCommandChecker) Errorf(format string, args ...interface{}) {
+       scc.mkline.Errorf(format, args...)
+}
+func (scc *SimpleCommandChecker) Warnf(format string, args ...interface{}) {
+       scc.mkline.Warnf(format, args...)
+}
+func (scc *SimpleCommandChecker) Notef(format string, args ...interface{}) {
+       scc.mkline.Notef(format, args...)
+}
+func (scc *SimpleCommandChecker) Explain(explanation ...string) {
+       scc.mkline.Explain(explanation...)
+}
+
 type ShellProgramChecker struct {
        *ShellLineChecker
 }
@@ -756,8 +783,8 @@ func (spc *ShellProgramChecker) checkCon
 
        checkConditionalCd := func(cmd *MkShSimpleCommand) {
                if NewStrCommand(cmd).Name == "cd" {
-                       spc.mkline.Errorf("The Solaris /bin/sh cannot handle \"cd\" inside conditionals.")
-                       G.Explain(
+                       spc.Errorf("The Solaris /bin/sh cannot handle \"cd\" inside conditionals.")
+                       spc.Explain(
                                "When the Solaris shell is in \"set -e\" mode and \"cd\" fails, the",
                                "shell will exit, no matter if it is protected by an \"if\" or the",
                                "\"||\" operator.")
@@ -781,8 +808,8 @@ func (spc *ShellProgramChecker) checkCon
        }
        walker.Callback.Pipeline = func(pipeline *MkShPipeline) {
                if pipeline.Negated {
-                       spc.mkline.Warnf("The Solaris /bin/sh does not support negation of shell commands.")
-                       G.Explain(
+                       spc.Warnf("The Solaris /bin/sh does not support negation of shell commands.")
+                       spc.Explain(
                                "The GNU Autoconf manual has many more details of what shell",
                                "features to avoid for portable programs.",
                                "It can be read at:",
@@ -792,7 +819,7 @@ func (spc *ShellProgramChecker) checkCon
        walker.Walk(list)
 }
 
-func (spc *ShellProgramChecker) checkPipeExitcode(line Line, pipeline *MkShPipeline) {
+func (spc *ShellProgramChecker) checkPipeExitcode(pipeline *MkShPipeline) {
        if trace.Tracing {
                defer trace.Call0()()
        }
@@ -812,11 +839,11 @@ func (spc *ShellProgramChecker) checkPip
        if G.Opts.WarnExtra && len(pipeline.Cmds) > 1 {
                if canFail, cmd := canFail(); canFail {
                        if cmd != "" {
-                               line.Warnf("The exitcode of %q at the left of the | operator is ignored.", cmd)
+                               spc.Warnf("The exitcode of %q at the left of the | operator is ignored.", cmd)
                        } else {
-                               line.Warnf("The exitcode of the command at the left of the | operator is ignored.")
+                               spc.Warnf("The exitcode of the command at the left of the | operator is ignored.")
                        }
-                       G.Explain(
+                       spc.Explain(
                                "In a shell command like \"cat *.txt | grep keyword\", if the command",
                                "on the left side of the \"|\" fails, this failure is ignored.",
                                "",
@@ -916,7 +943,7 @@ func (spc *ShellProgramChecker) checkSet
 
        line.Warnf("Please switch to \"set -e\" mode before using a semicolon (after %q) to separate commands.",
                NewStrCommand(command.Simple).String())
-       G.Explain(
+       line.Explain(
                "Normally, when a shell command fails (returns non-zero),",
                "the remaining commands are still executed.",
                "For example, the following commands would remove",
@@ -934,6 +961,16 @@ func (spc *ShellProgramChecker) checkSet
                "* use \"&&\" instead of \";\" to separate the commands")
 }
 
+func (spc *ShellProgramChecker) Errorf(format string, args ...interface{}) {
+       spc.mkline.Errorf(format, args...)
+}
+func (spc *ShellProgramChecker) Warnf(format string, args ...interface{}) {
+       spc.mkline.Warnf(format, args...)
+}
+func (spc *ShellProgramChecker) Explain(explanation ...string) {
+       spc.mkline.Explain(explanation...)
+}
+
 // Some shell commands should not be used in the install phase.
 func (ck *ShellLineChecker) checkInstallCommand(shellcmd string) {
        if trace.Tracing {
@@ -961,14 +998,14 @@ func (ck *ShellLineChecker) checkInstall
                "tr", "${TR}":
                // TODO: Pkglint should not complain when sed and tr are used to transform filenames.
                line.Warnf("The shell command %q should not be used in the install phase.", shellcmd)
-               G.Explain(
+               line.Explain(
                        "In the install phase, the only thing that should be done is to",
                        "install the prepared files to their final location.",
                        "The file's contents should not be changed anymore.")
 
        case "cp", "${CP}":
                line.Warnf("${CP} should not be used to install files.")
-               G.Explain(
+               line.Explain(
                        "The ${CP} command is highly platform dependent and cannot overwrite read-only files.",
                        "Please use ${PAX} instead.",
                        "",
@@ -990,39 +1027,16 @@ func splitIntoShellTokens(line Line, tex
        // TODO: Check whether this function is used correctly by all callers.
        //  It may be better to use a proper shell parser instead of this tokenizer.
 
-       word := ""
-       rest = text
        p := NewShTokenizer(line, text, false)
-       emit := func() {
-               if word != "" {
-                       tokens = append(tokens, word)
-                       word = ""
-               }
-               rest = p.parser.Rest()
-       }
-
-       q := shqPlain
-       var prevAtom *ShAtom
        for {
-               atom := p.ShAtom(q)
-               if atom == nil {
-                       if prevAtom == nil || prevAtom.Quoting == shqPlain {
-                               emit()
-                       }
+               token := p.ShToken()
+               if token == nil {
                        break
                }
-
-               q = atom.Quoting
-               prevAtom = atom
-               if atom.Type == shtSpace && q == shqPlain {
-                       emit()
-               } else if atom.Type.IsWord() || atom.Quoting != shqPlain {
-                       word += atom.MkText
-               } else {
-                       emit()
-                       tokens = append(tokens, atom.MkText)
-               }
+               tokens = append(tokens, token.MkText)
        }
 
+       rest = p.parser.Rest()
+
        return
 }

Index: pkgsrc/pkgtools/pkglint/files/distinfo.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo.go:1.30 pkgsrc/pkgtools/pkglint/files/distinfo.go:1.31
--- pkgsrc/pkgtools/pkglint/files/distinfo.go:1.30      Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/distinfo.go   Sat Apr 20 17:43:24 2019
@@ -153,7 +153,7 @@ func (ck *distinfoLinesChecker) checkAlg
 
                line.Warnf("Patch file %q does not exist in directory %q.",
                        filename, line.PathToFile(ck.pkg.File(ck.patchdir)))
-               G.Explain(
+               line.Explain(
                        "If the patches directory looks correct, the patch may have been",
                        "removed without updating the distinfo file.",
                        "In such a case please update the distinfo file.",

Index: pkgsrc/pkgtools/pkglint/files/distinfo_test.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.27 pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.28
--- pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.27 Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/distinfo_test.go      Sat Apr 20 17:43:24 2019
@@ -113,6 +113,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "",
                "MD5 (patch-aa) = 12345678901234567890123456789012",
                "SHA1 (patch-aa) = 1234567890123456789012345678901234567890")
+       t.FinishSetUp()
 
        G.Check(".")
 
@@ -176,7 +177,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "",
                ".include \"../mk/misc/category.mk\"")
 
-       G.Main("pkglint", "-r", "-Wall", "-Call", t.File("."))
+       t.Main("-r", "-Wall", "-Call", t.File("."))
 
        t.CheckOutputLines(
                "ERROR: ~/category/package1/distinfo:3: "+
@@ -240,6 +241,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "SHA512 (patch-aa) = ...",
                "Size (patch-aa) = ... bytes")
        t.CreateFileDummyPatch("category/package/patches/patch-aa")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -265,6 +267,7 @@ func (s *Suite) Test_distinfoLinesChecke
                RcsID,
                "",
                "RMD160 (patch-aa) = ...")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -288,6 +291,7 @@ func (s *Suite) Test_distinfoLinesChecke
                RcsID,
                "",
                "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac")
+       t.FinishSetUp()
 
        G.checkdirPackage(".")
 
@@ -309,6 +313,7 @@ func (s *Suite) Test_distinfoLinesChecke
                RcsID,
                "",
                "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac")
+       t.FinishSetUp()
 
        G.checkdirPackage(".")
 
@@ -330,6 +335,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "RMD160 (distfile.tar.gz) = ...",
                "SHA512 (distfile.tar.gz) = ...",
                "Size (distfile.tar.gz) = 1024 bytes")
+       t.FinishSetUp()
 
        G.checkdirPackage(".")
 
@@ -357,6 +363,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "SHA1 (patch-aa) = ...",
                "SHA1 (patch-only-in-distinfo) = ...")
        t.Chdir("category/package")
+       t.FinishSetUp()
 
        G.checkdirPackage(".")
 
@@ -387,6 +394,7 @@ func (s *Suite) Test_CheckLinesDistinfo_
                "SHA1 (patch-aa) = ...",
                "SHA1 (patch-only-in-distinfo) = ...")
        t.Chdir("category/package")
+       t.FinishSetUp()
 
        G.checkdirPackage(".")
 
@@ -465,6 +473,7 @@ func (s *Suite) Test_CheckLinesDistinfo_
                "",
                ".include \"../../lang/php/ext.mk\"",
                ".include \"../../mk/bsd.pkg.mk\"")
+       t.FinishSetUp()
 
        G.Check(t.File("archivers/php-bz2"))
 
@@ -508,7 +517,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "CRC32 (package-1.0.txt) = asdf")
        t.CreateFileLines("distfiles/package-1.0.txt",
                "hello, world")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        // This run is only used to verify that the RMD160 hash is correct, and if
        // it should ever differ, the correct hash will appear in an error message.
@@ -584,7 +593,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "RMD160 (package-1.0.txt) = 1234wrongHash1234")
        t.CreateFileLines("distfiles/package-1.0.txt",
                "hello, world")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -607,7 +616,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "MD5 (package-1.0.txt) = 1234wrongHash1234")
        t.CreateFileLines("distfiles/package-1.0.txt",
                "hello, world")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -629,7 +638,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "Size (package-1.0.txt) = 13 bytes")
        t.CreateFileLines("distfiles/package-1.0.txt",
                "hello, world")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -652,7 +661,7 @@ func (s *Suite) Test_distinfoLinesChecke
                "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a")
        t.CreateFileLines("distfiles/package-1.0.txt",
                "hello, world")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -691,7 +700,7 @@ func (s *Suite) Test_distinfoLinesChecke
 
        t.CreateFileLines("distfiles/package-1.0.txt",
                "hello, world")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -716,7 +725,7 @@ func (s *Suite) Test_distinfoLinesChecke
 
        t.CreateFileLines("distfiles/package-1.0.txt",
                "hello, world")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 

Index: pkgsrc/pkgtools/pkglint/files/files_test.go
diff -u pkgsrc/pkgtools/pkglint/files/files_test.go:1.23 pkgsrc/pkgtools/pkglint/files/files_test.go:1.24
--- pkgsrc/pkgtools/pkglint/files/files_test.go:1.23    Sat Jan 26 16:31:33 2019
+++ pkgsrc/pkgtools/pkglint/files/files_test.go Sat Apr 20 17:43:24 2019
@@ -98,6 +98,22 @@ func (s *Suite) Test_convertToLogicalLin
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_convertToLogicalLines__commented_multi(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("filename.mk",
+               "#COMMENTED= \\",
+               "#\tcontinuation 1 \\",
+               "#\tcontinuation 2")
+       mkline := mklines.mklines[0]
+
+       // FIXME: It is more pragmatic to strip the leading comments from the
+       //  continuation lines as well, so that the variable value is "continuation 1 continuation 2".
+       //  See nextLogicalLine.
+       t.Check(mkline.Value(), equals, "")
+       t.Check(mkline.VarassignComment(), equals, "#\tcontinuation 1 #\tcontinuation 2")
+}
+
 func (s *Suite) Test_convertToLogicalLines__missing_newline_at_eof(c *check.C) {
        t := s.Init(c)
 
Index: pkgsrc/pkgtools/pkglint/files/substcontext_test.go
diff -u pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.23 pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.24
--- pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.23     Sun Mar 10 19:01:50 2019
+++ pkgsrc/pkgtools/pkglint/files/substcontext_test.go  Sat Apr 20 17:43:25 2019
@@ -255,6 +255,7 @@ func (s *Suite) Test_SubstContext__pre_c
                "SUBST_SED.os=           -e s,@OPSYS@,Darwin,",
                "",
                "NO_CONFIGURE=           yes")
+       t.FinishSetUp()
 
        G.Check(pkg)
 

Index: pkgsrc/pkgtools/pkglint/files/licenses.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses.go:1.22 pkgsrc/pkgtools/pkglint/files/licenses.go:1.23
--- pkgsrc/pkgtools/pkglint/files/licenses.go:1.22      Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/licenses.go   Sat Apr 20 17:43:24 2019
@@ -48,7 +48,7 @@ func (lc *LicenseChecker) checkName(lice
                "no-redistribution",
                "shareware":
                lc.MkLine.Errorf("License %q must not be used.", license)
-               G.Explain(
+               lc.MkLine.Explain(
                        "Instead of using these deprecated licenses, extract the actual",
                        "license from the package into the pkgsrc/licenses/ directory",
                        "and define LICENSE to that filename.",
@@ -65,7 +65,7 @@ func (lc *LicenseChecker) checkNode(cond
 
        if cond.And && cond.Or {
                lc.MkLine.Errorf("AND and OR operators in license conditions can only be combined using parentheses.")
-               G.Explain(
+               lc.MkLine.Explain(
                        "Examples for valid license conditions are:",
                        "",
                        "\tlicense1 AND license2 AND (license3 OR license4)",
Index: pkgsrc/pkgtools/pkglint/files/licenses_test.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.22 pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.23
--- pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.22 Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/licenses_test.go      Sat Apr 20 17:43:24 2019
@@ -55,7 +55,7 @@ func (s *Suite) Test_LicenseChecker_chec
        t.CreateFileLines("category/package/my-license",
                "An individual license file.")
 
-       G.Main("pkglint", t.File("category/package"))
+       t.Main(t.File("category/package"))
 
        // There is no warning about the unusual file name in the package directory.
        // If it were not mentioned in LICENSE_FILE, the file named my-license
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.22 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.23
--- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.22        Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go     Sat Apr 20 17:43:24 2019
@@ -344,7 +344,7 @@ func (src *Pkgsrc) loadTools() {
 func (src *Pkgsrc) loadUntypedVars() {
 
        // Setting guessed to false prevents the vartype.guessed case in MkLineChecker.CheckVaruse.
-       unknownType := Vartype{lkNone, BtUnknown, []ACLEntry{{"*", aclpAll}}, false}
+       unknownType := Vartype{BtUnknown, NoVartypeOptions, []ACLEntry{{"*", aclpAll}}}
 
        define := func(varcanon string, mkline MkLine) {
                switch {
@@ -909,12 +909,12 @@ func (src *Pkgsrc) VariableType(mklines 
                if tool.Validity == AfterPrefsMk && mklines.Tools.SeenPrefs {
                        perms |= aclpUseLoadtime
                }
-               return &Vartype{lkNone, BtShellCommand, []ACLEntry{{"*", perms}}, false}
+               return &Vartype{BtShellCommand, NoVartypeOptions, []ACLEntry{{"*", perms}}}
        }
 
        if m, toolVarname := match1(varname, `^TOOLS_(.*)`); m {
                if tool := G.ToolByVarname(mklines, toolVarname); tool != nil {
-                       return &Vartype{lkNone, BtPathname, []ACLEntry{{"*", aclpUse}}, false}
+                       return &Vartype{BtPathname, NoVartypeOptions, []ACLEntry{{"*", aclpUse}}}
                }
        }
 
@@ -930,37 +930,37 @@ func (src *Pkgsrc) guessVariableType(var
        var gtype *Vartype
        switch {
        case hasSuffix(varbase, "DIRS"):
-               gtype = &Vartype{lkShell, BtPathmask, allowRuntime, true}
+               gtype = &Vartype{BtPathmask, List | Guessed, allowRuntime}
        case hasSuffix(varbase, "DIR") && !hasSuffix(varbase, "DESTDIR"), hasSuffix(varname, "_HOME"):
                // TODO: hasSuffix(varbase, "BASE")
-               gtype = &Vartype{lkNone, BtPathname, allowRuntime, true}
+               gtype = &Vartype{BtPathname, Guessed, allowRuntime}
        case hasSuffix(varbase, "FILES"):
-               gtype = &Vartype{lkShell, BtPathmask, allowRuntime, true}
+               gtype = &Vartype{BtPathmask, List | Guessed, allowRuntime}
        case hasSuffix(varbase, "FILE"):
-               gtype = &Vartype{lkNone, BtPathname, allowRuntime, true}
+               gtype = &Vartype{BtPathname, Guessed, allowRuntime}
        case hasSuffix(varbase, "PATH"):
-               gtype = &Vartype{lkNone, BtPathlist, allowRuntime, true}
+               gtype = &Vartype{BtPathlist, Guessed, allowRuntime}
        case hasSuffix(varbase, "PATHS"):
-               gtype = &Vartype{lkShell, BtPathname, allowRuntime, true}
+               gtype = &Vartype{BtPathname, List | Guessed, allowRuntime}
        case hasSuffix(varbase, "_USER"):
-               gtype = &Vartype{lkNone, BtUserGroupName, allowAll, true}
+               gtype = &Vartype{BtUserGroupName, Guessed, allowAll}
        case hasSuffix(varbase, "_GROUP"):
-               gtype = &Vartype{lkNone, BtUserGroupName, allowAll, true}
+               gtype = &Vartype{BtUserGroupName, Guessed, allowAll}
        case hasSuffix(varbase, "_ENV"):
-               gtype = &Vartype{lkShell, BtShellWord, allowRuntime, true}
+               gtype = &Vartype{BtShellWord, List | Guessed, allowRuntime}
        case hasSuffix(varbase, "_CMD"):
-               gtype = &Vartype{lkNone, BtShellCommand, allowRuntime, true}
+               gtype = &Vartype{BtShellCommand, Guessed, allowRuntime}
        case hasSuffix(varbase, "_ARGS"):
-               gtype = &Vartype{lkShell, BtShellWord, allowRuntime, true}
+               gtype = &Vartype{BtShellWord, List | Guessed, allowRuntime}
        case hasSuffix(varbase, "_CFLAGS"), hasSuffix(varname, "_CPPFLAGS"), hasSuffix(varname, "_CXXFLAGS"):
-               gtype = &Vartype{lkShell, BtCFlag, allowRuntime, true}
+               gtype = &Vartype{BtCFlag, List | Guessed, allowRuntime}
        case hasSuffix(varname, "_LDFLAGS"):
-               gtype = &Vartype{lkShell, BtLdFlag, allowRuntime, true}
+               gtype = &Vartype{BtLdFlag, List | Guessed, allowRuntime}
        case hasSuffix(varbase, "_MK"):
                // TODO: Add BtGuard for inclusion guards, since these variables may only be checked using defined().
-               gtype = &Vartype{lkNone, BtUnknown, allowAll, true}
+               gtype = &Vartype{BtUnknown, Guessed, allowAll}
        case hasSuffix(varbase, "_SKIP"):
-               gtype = &Vartype{lkShell, BtPathmask, allowRuntime, true}
+               gtype = &Vartype{BtPathmask, List | Guessed, allowRuntime}
        }
 
        if gtype == nil {
Index: pkgsrc/pkgtools/pkglint/files/substcontext.go
diff -u pkgsrc/pkgtools/pkglint/files/substcontext.go:1.22 pkgsrc/pkgtools/pkglint/files/substcontext.go:1.23
--- pkgsrc/pkgtools/pkglint/files/substcontext.go:1.22  Sun Mar 10 19:01:50 2019
+++ pkgsrc/pkgtools/pkglint/files/substcontext.go       Sat Apr 20 17:43:25 2019
@@ -140,7 +140,7 @@ func (ctx *SubstContext) Varassign(mklin
                        if noConfigureLine := G.Pkg.vars.FirstDefinition("NO_CONFIGURE"); noConfigureLine != nil {
                                mkline.Warnf("SUBST_STAGE %s has no effect when NO_CONFIGURE is set (in %s).",
                                        value, mkline.RefTo(noConfigureLine))
-                               G.Explain(
+                               mkline.Explain(
                                        "To fix this properly, remove the definition of NO_CONFIGURE.")
                        }
                }

Index: pkgsrc/pkgtools/pkglint/files/linelexer.go
diff -u pkgsrc/pkgtools/pkglint/files/linelexer.go:1.2 pkgsrc/pkgtools/pkglint/files/linelexer.go:1.3
--- pkgsrc/pkgtools/pkglint/files/linelexer.go:1.2      Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/linelexer.go  Sat Apr 20 17:43:24 2019
@@ -12,6 +12,8 @@ func NewLinesLexer(lines Lines) *LinesLe
        return &LinesLexer{lines, 0}
 }
 
+// CurrentLine returns the line that the lexer is currently looking at.
+// If it is at the end of file, the line number of the line is EOF.
 func (llex *LinesLexer) CurrentLine() Line {
        if llex.index < llex.lines.Len() {
                return llex.lines.Lines[llex.index]
Index: pkgsrc/pkgtools/pkglint/files/redundantscope_test.go
diff -u pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.2 pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.3
--- pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.2    Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/redundantscope_test.go        Sat Apr 20 17:43:24 2019
@@ -1022,7 +1022,7 @@ func (s *Suite) Test_RedundantScope__pro
                "CHECK_BUILTIN.gettext:= yes",
                ".include \"builtin.mk\"",
                "CHECK_BUILTIN.gettext:= no")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        // Checking x11/Xaos instead of devel/gettext-lib avoids warnings
        // about the minimal buildlink3.mk file.
@@ -1049,7 +1049,7 @@ func (s *Suite) Test_RedundantScope__pro
                "CHECK_BUILTIN.gettext?= no",
                ".if !empty(CHECK_BUILTIN.gettext:M[nN][oO])",
                ".endif")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("x11/alacarte"))
 
@@ -1142,6 +1142,7 @@ func (s *Suite) Test_RedundantScope__inc
        t.CreateFileLines("category/dependency/builtin.mk",
                MkRcsID,
                "CONFIGURE_ARGS.Darwin+= darwin")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 

Index: pkgsrc/pkgtools/pkglint/files/lines_test.go
diff -u pkgsrc/pkgtools/pkglint/files/lines_test.go:1.8 pkgsrc/pkgtools/pkglint/files/lines_test.go:1.9
--- pkgsrc/pkgtools/pkglint/files/lines_test.go:1.8     Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/lines_test.go Sat Apr 20 17:43:24 2019
@@ -42,6 +42,7 @@ func (s *Suite) Test_Lines_CheckRcsID__w
                "# $"+"Id$")
        t.CreateFileLines("wip/package/file5.mk",
                "# $"+"FreeBSD$")
+       t.FinishSetUp()
 
        G.Check(t.File("wip/package"))
 

Index: pkgsrc/pkgtools/pkglint/files/mkline.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.50 pkgsrc/pkgtools/pkglint/files/mkline.go:1.51
--- pkgsrc/pkgtools/pkglint/files/mkline.go:1.50        Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline.go     Sat Apr 20 17:43:24 2019
@@ -62,57 +62,101 @@ type mkLineDependency struct {
        sources string
 }
 
-// NewMkLine parses the text of a Makefile line to see what kind of line
+type MkLineParser struct{}
+
+// Parse parses the text of a Makefile line to see what kind of line
 // it is: variable assignment, include, comment, etc.
 //
 // See devel/bmake/parse.c:/^Parse_File/
-func NewMkLine(line Line) *MkLineImpl {
+func (p MkLineParser) Parse(line Line) *MkLineImpl {
        text := line.Text
 
        // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
        if hasPrefix(text, " ") && line.Basename != "bsd.buildlink3.mk" {
                line.Warnf("Makefile lines should not start with space characters.")
-               G.Explain(
+               line.Explain(
                        "If this line should be a shell command connected to a target, use a tab character for indentation.",
                        "Otherwise remove the leading whitespace.")
        }
 
-       if m, a := MatchVarassign(text); m {
-               if a.spaceAfterVarname != "" {
-                       varname := a.varname
-                       op := a.op
-                       switch {
-                       case hasSuffix(varname, "+") && (op == opAssign || op == opAssignAppend):
-                               break
-                       case matches(varname, `^[a-z]`) && op == opAssignEval:
-                               break
-                       default:
-                               // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
-                               fix := line.Autofix()
-                               fix.Notef("Unnecessary space after variable name %q.", varname)
-                               fix.Replace(varname+a.spaceAfterVarname+op.String(), varname+op.String())
-                               fix.Apply()
-                       }
-               }
+       data := p.split(line, text)
 
-               // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
-               if a.comment != "" && a.value != "" && a.spaceAfterValue == "" {
-                       line.Warnf("The # character starts a Makefile comment.")
-                       G.Explain(
-                               "In a variable assignment, an unescaped # starts a comment that",
-                               "continues until the end of the line.",
-                               "To escape the #, write \\#.")
-               }
+       // Check for shell commands first because these cannot have comments
+       // at the end of the line.
+       if hasPrefix(text, "\t") {
+               return p.parseShellcmd(line)
+       }
 
-               return &MkLineImpl{line, a}
+       if mkline := p.parseVarassign(line, data); mkline != nil {
+               return mkline
+       }
+       if mkline := p.parseCommentOrEmpty(line); mkline != nil {
+               return mkline
+       }
+       if mkline := p.parseDirective(line, data); mkline != nil {
+               return mkline
+       }
+       if mkline := p.parseInclude(line); mkline != nil {
+               return mkline
+       }
+       if mkline := p.parseSysinclude(line); mkline != nil {
+               return mkline
+       }
+       if mkline := p.parseDependency(line); mkline != nil {
+               return mkline
+       }
+       if mkline := p.parseMergeConflict(line); mkline != nil {
+               return mkline
        }
 
-       if hasPrefix(text, "\t") {
-               shellcmd := text[1:]
-               return &MkLineImpl{line, mkLineShell{shellcmd}}
+       // The %q is deliberate here since it shows possible strange characters.
+       line.Errorf("Unknown Makefile line format: %q.", text)
+       return &MkLineImpl{line, nil}
+}
+
+func (p MkLineParser) parseVarassign(line Line, data mkLineSplitResult) MkLine {
+       m, a := p.MatchVarassign(line, line.Text, data)
+       if !m {
+               return nil
+       }
+
+       if a.spaceAfterVarname != "" {
+               varname := a.varname
+               op := a.op
+               switch {
+               case hasSuffix(varname, "+") && (op == opAssign || op == opAssignAppend):
+                       break
+               case matches(varname, `^[a-z]`) && op == opAssignEval:
+                       break
+               default:
+                       fix := line.Autofix()
+                       fix.Notef("Unnecessary space after variable name %q.", varname)
+                       fix.Replace(varname+a.spaceAfterVarname+op.String(), varname+op.String())
+                       fix.Apply()
+               }
        }
 
-       trimmedText := trimHspace(text)
+       if a.comment != "" && a.value != "" && a.spaceAfterValue == "" {
+               line.Warnf("The # character starts a Makefile comment.")
+               line.Explain(
+                       "In a variable assignment, an unescaped # starts a comment that",
+                       "continues until the end of the line.",
+                       "To escape the #, write \\#.",
+                       "",
+                       "If this # character intentionally starts a comment,",
+                       "it should be preceded by a space in order to make it more visible.")
+       }
+
+       return &MkLineImpl{line, a}
+}
+
+func (p MkLineParser) parseShellcmd(line Line) MkLine {
+       return &MkLineImpl{line, mkLineShell{line.Text[1:]}}
+}
+
+func (p MkLineParser) parseCommentOrEmpty(line Line) MkLine {
+       trimmedText := trimHspace(line.Text)
+
        if strings.HasPrefix(trimmedText, "#") {
                return &MkLineImpl{line, mkLineComment{}}
        }
@@ -121,40 +165,46 @@ func NewMkLine(line Line) *MkLineImpl {
                return &MkLineImpl{line, mkLineEmpty{}}
        }
 
-       if m, indent, directive, args, comment := matchMkDirective(text); m {
-
-               // In .if and .endif lines the space surrounding the comment is irrelevant.
-               // Especially for checking that the .endif comment matches the .if condition,
-               // it must be trimmed.
-               trimmedComment := trimHspace(comment)
+       return nil
+}
 
-               return &MkLineImpl{line, &mkLineDirectiveImpl{indent, directive, args, trimmedComment, nil, nil, nil}}
+func (p MkLineParser) parseInclude(line Line) MkLine {
+       m, indent, directive, includedFile := MatchMkInclude(line.Text)
+       if !m {
+               return nil
        }
 
-       if m, indent, directive, includedFile := MatchMkInclude(text); m {
-               return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", false, indent, includedFile, nil}}
-       }
+       return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", false, indent, includedFile, nil}}
+}
 
-       if m, indent, directive, includedFile := match3(text, `^\.([\t ]*)(s?include)[\t ]+<([^>]+)>[\t ]*(?:#.*)?$`); m {
-               return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", true, indent, includedFile, nil}}
+func (p MkLineParser) parseSysinclude(line Line) MkLine {
+       m, indent, directive, includedFile := match3(line.Text, `^\.([\t ]*)(s?include)[\t ]+<([^>]+)>[\t ]*(?:#.*)?$`)
+       if !m {
+               return nil
        }
 
+       return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", true, indent, includedFile, nil}}
+}
+
+func (p MkLineParser) parseDependency(line Line) MkLine {
        // XXX: Replace this regular expression with proper parsing.
        // There might be a ${VAR:M*.c} in these variables, which the below regular expression cannot handle.
-       if m, targets, whitespace, sources := match3(text, `^([^\t :]+(?:[\t ]*[^\t :]+)*)([\t ]*):[\t ]*([^#]*?)(?:[\t ]*#.*)?$`); m {
-               // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
-               if whitespace != "" {
-                       line.Notef("Space before colon in dependency line.")
-               }
-               return &MkLineImpl{line, mkLineDependency{targets, sources}}
+       m, targets, whitespace, sources := match3(line.Text, `^([^\t :]+(?:[\t ]*[^\t :]+)*)([\t ]*):[\t ]*([^#]*?)(?:[\t ]*#.*)?$`)
+       if !m {
+               return nil
+       }
+
+       if whitespace != "" {
+               line.Notef("Space before colon in dependency line.")
        }
+       return &MkLineImpl{line, mkLineDependency{targets, sources}}
+}
 
-       if matches(text, `^(<<<<<<<|=======|>>>>>>>)`) {
-               return &MkLineImpl{line, nil}
+func (p MkLineParser) parseMergeConflict(line Line) MkLine {
+       if !matches(line.Text, `^(<<<<<<<|=======|>>>>>>>)`) {
+               return nil
        }
 
-       // The %q is deliberate here since it shows possible strange characters.
-       line.Errorf("Unknown Makefile line format: %q.", text)
        return &MkLineImpl{line, nil}
 }
 
@@ -295,7 +345,7 @@ func (mkline *MkLineImpl) Args() string 
 func (mkline *MkLineImpl) Cond() MkCond {
        cond := mkline.data.(mkLineDirective).cond
        if cond == nil {
-               cond = NewMkParser(nil, mkline.Args(), false).MkCond()
+               cond = NewMkParser(mkline.Line, mkline.Args(), true).MkCond()
                mkline.data.(mkLineDirective).cond = cond
        }
        return cond
@@ -452,23 +502,24 @@ func (mkline *MkLineImpl) ValueFields(va
                atoms = atoms[1:]
        }
 
-       word := ""
+       var word strings.Builder
        var words []string
        for _, atom := range atoms {
                if atom.Type == shtSpace && atom.Quoting == shqPlain {
-                       words = append(words, word)
-                       word = ""
+                       words = append(words, word.String())
+                       word.Reset()
                } else {
-                       word += atom.MkText
+                       word.WriteString(atom.MkText)
                }
        }
-       if word != "" && atoms[len(atoms)-1].Quoting == shqPlain {
-               words = append(words, word)
-               word = ""
+       if word.Len() > 0 && atoms[len(atoms)-1].Quoting == shqPlain {
+               words = append(words, word.String())
+               word.Reset()
        }
 
        // TODO: Handle parse errors
-       rest := word + p.parser.Rest()
+       word.WriteString(p.parser.Rest())
+       rest := word.String()
        _ = rest
 
        return words
@@ -485,7 +536,9 @@ func (mkline *MkLineImpl) ValueTokens() 
                return assign.valueMk, assign.valueMkRest
        }
 
-       p := NewMkParser(mkline.Line, value, true)
+       // No error checking here since all this has already been done when the
+       // whole line was parsed in MkLineParser.Parse.
+       p := NewMkParser(nil, value, false)
        assign.valueMk = p.MkTokens()
        assign.valueMkRest = p.Rest()
        return assign.valueMk, assign.valueMkRest
@@ -527,13 +580,13 @@ func (mkline *MkLineImpl) Fields() []str
 }
 
 func (mkline *MkLineImpl) WithoutMakeVariables(value string) string {
-       valueNovar := ""
+       var valueNovar strings.Builder
        for _, token := range NewMkParser(nil, value, false).MkTokens() {
                if token.Varuse == nil {
-                       valueNovar += token.Text
+                       valueNovar.WriteString(token.Text)
                }
        }
-       return valueNovar
+       return valueNovar.String()
 }
 
 func (mkline *MkLineImpl) ResolveVarsInRelativePath(relativePath string) string {
@@ -612,7 +665,7 @@ func (mkline *MkLineImpl) ResolveVarsInR
 }
 
 func (mkline *MkLineImpl) ExplainRelativeDirs() {
-       G.Explain(
+       mkline.Explain(
                "Directories in the form \"../../category/package\" make it easier to",
                "move a package around in pkgsrc, for example from pkgsrc-wip to the",
                "main pkgsrc repository.")
@@ -633,7 +686,7 @@ var (
        unescapeMkCommentSafeChars = textproc.NewByteSet("\\#[$").Inverse()
 )
 
-// unescapeMkComment takes a Makefile line, as written in a file, and splits
+// unescapeComment takes a Makefile line, as written in a file, and splits
 // it into the main part and the comment.
 //
 // The comment starts at the first #. Except if it is preceded by an odd number
@@ -644,7 +697,7 @@ var (
 //
 // The comment is returned including the leading "#", if any. If the line has
 // no comment, it is an empty string.
-func unescapeMkComment(text string) (main, comment string) {
+func (p MkLineParser) unescapeComment(text string) (main, comment string) {
        var sb strings.Builder
 
        lexer := textproc.NewLexer(text)
@@ -682,13 +735,21 @@ again:
                        return main, lexer.Rest()
                }
 
-               G.Assertf(lexer.EOF(), "unescapeMkComment(%q): sb = %q, rest = %q", text, main, lexer.Rest())
+               G.Assertf(lexer.EOF(), "unescapeComment(%q): sb = %q, rest = %q", text, main, lexer.Rest())
                return main, ""
        }
 
        goto again
 }
 
+type mkLineSplitResult struct {
+       main               string
+       tokens             []*MkToken
+       spaceBeforeComment string
+       hasComment         bool
+       comment            string
+}
+
 // splitMkLine parses a logical line from a Makefile (that is, after joining
 // the lines that end in a backslash) into two parts: the main part and the
 // comment.
@@ -696,12 +757,12 @@ again:
 // This applies to all line types except those starting with a tab, which
 // contain the shell commands to be associated with make targets. These cannot
 // have comments.
-func splitMkLine(text string) (main string, tokens []*MkToken, rest string, spaceBeforeComment string, hasComment bool, comment string) {
+func (p MkLineParser) split(line Line, text string) mkLineSplitResult {
 
-       main, comment = unescapeMkComment(text)
+       main, comment := p.unescapeComment(text)
 
-       p := NewMkParser(nil, main, false)
-       lexer := p.lexer
+       parser := NewMkParser(line, main, line != nil)
+       lexer := parser.lexer
 
        rtrimHspace := func(s string) string {
                end := len(s)
@@ -711,7 +772,7 @@ func splitMkLine(text string) (main stri
                return s[:end]
        }
 
-       parseToken := func() string {
+       parseOther := func() string {
                var sb strings.Builder
 
                for !lexer.EOF() {
@@ -731,56 +792,53 @@ func splitMkLine(text string) (main stri
                return sb.String()
        }
 
+       var tokens []*MkToken
        for !lexer.EOF() {
                mark := lexer.Mark()
 
-               if varUse := p.VarUse(); varUse != nil {
+               if varUse := parser.VarUse(); varUse != nil {
                        tokens = append(tokens, &MkToken{lexer.Since(mark), varUse})
 
-               } else if token := parseToken(); token != "" {
-                       tokens = append(tokens, &MkToken{token, nil})
+               } else if other := parseOther(); other != "" {
+                       tokens = append(tokens, &MkToken{other, nil})
 
                } else {
-                       break
+                       G.Assertf(lexer.SkipByte('$'), "Parse error for %q.", text)
+                       tokens = append(tokens, &MkToken{"$", nil})
                }
        }
 
-       if comment != "" {
-               hasComment = true
+       hasComment := comment != ""
+       if hasComment {
                comment = comment[1:]
        }
-       rest = lexer.Rest()
-       main = main[:len(main)-len(rest)]
 
-       if rest == "" {
-               mainWithSpaces := main
-               main = rtrimHspace(main)
-               spaceBeforeComment = mainWithSpaces[len(main):]
-       } else {
-               restWithoutSpace := strings.TrimRightFunc(rest, func(r rune) bool { return isHspace(byte(r)) })
-               if len(restWithoutSpace) < len(rest) {
-                       spaceBeforeComment = rest[len(restWithoutSpace):]
-                       rest = restWithoutSpace
+       G.Assertf(lexer.Rest() == "", "Parse error for %q.", text)
+
+       mainWithSpaces := main
+       main = rtrimHspace(main)
+       spaceBeforeComment := ifelseStr(true, mainWithSpaces[len(main):], "")
+       if spaceBeforeComment != "" && len(tokens) > 0 {
+               tokenText := &tokens[len(tokens)-1].Text
+               *tokenText = rtrimHspace(*tokenText)
+               if *tokenText == "" {
+                       tokens = tokens[:len(tokens)-1]
                }
        }
 
-       return
+       return mkLineSplitResult{main, tokens, spaceBeforeComment, hasComment, comment}
 }
 
-func matchMkDirective(text string) (m bool, indent, directive, args, comment string) {
+func (p MkLineParser) parseDirective(line Line, data mkLineSplitResult) MkLine {
+       text := line.Text
        if !hasPrefix(text, ".") {
-               return
-       }
-
-       main, _, rest, _, hasComment, trailingComment := splitMkLine(text)
-       if rest != "" {
-               return
+               return nil
        }
 
-       lexer := textproc.NewLexer(main[1:])
+       lexer := textproc.NewLexer(data.main[1:])
 
-       indent = lexer.NextHspace()
-       directive = lexer.NextBytesSet(LowerDash)
+       indent := lexer.NextHspace()
+       directive := lexer.NextBytesSet(LowerDash)
        switch directive {
        case "if", "else", "elif", "endif",
                "ifdef", "ifndef",
@@ -790,19 +848,19 @@ func matchMkDirective(text string) (m bo
                break
        default:
                // Intentionally not supported are: ifmake ifnmake elifdef elifndef elifmake elifnmake.
-               return
+               return nil
        }
 
        lexer.SkipHspace()
 
-       args = lexer.Rest()
+       args := lexer.Rest()
 
-       if hasComment {
-               comment = trailingComment
-       }
+       // In .if and .endif lines the space surrounding the comment is irrelevant.
+       // Especially for checking that the .endif comment matches the .if condition,
+       // it must be trimmed.
+       trimmedComment := trimHspace(data.comment)
 
-       m = true
-       return
+       return &MkLineImpl{line, &mkLineDirectiveImpl{indent, directive, args, trimmedComment, nil, nil, nil}}
 }
 
 // VariableNeedsQuoting determines whether the given variable needs the :Q operator
@@ -825,8 +883,8 @@ func (mkline *MkLineImpl) VariableNeedsQ
        }
 
        if !vartype.basicType.NeedsQ() {
-               if vartype.kindOfList == lkNone {
-                       if vartype.guessed {
+               if !vartype.List() {
+                       if vartype.Guessed() {
                                return unknown
                        }
                        return no
@@ -838,14 +896,14 @@ func (mkline *MkLineImpl) VariableNeedsQ
 
        // A shell word may appear as part of a shell word, for example COMPILER_RPATH_FLAG.
        if vuc.IsWordPart && vuc.quoting == VucQuotPlain {
-               if vartype.kindOfList == lkNone && vartype.basicType == BtShellWord {
+               if !vartype.List() && vartype.basicType == BtShellWord {
                        return no
                }
        }
 
        // Determine whether the context expects a list of shell words or not.
-       wantList := vucVartype.IsConsideredList()
-       haveList := vartype.IsConsideredList()
+       wantList := vucVartype.MayBeAppendedTo()
+       haveList := vartype.MayBeAppendedTo()
        if trace.Tracing {
                trace.Stepf("wantList=%v, haveList=%v", wantList, haveList)
        }
@@ -1382,16 +1440,26 @@ var (
        VarparamBytes = textproc.NewByteSet("A-Za-z_0-9#*+---.[")
 )
 
-func MatchVarassign(text string) (m bool, assignment mkLineAssign) {
-       commented := hasPrefix(text, "#")
+func (p MkLineParser) MatchVarassign(line Line, text string, asdfData mkLineSplitResult) (m bool, assignment mkLineAssign) {
+
+       // A commented variable assignment does not have leading whitespace.
+       // Otherwise line 1 of almost every Makefile fragment would need to
+       // be scanned for a variable assignment even though it only contains
+       // the $NetBSD CVS Id.
+       clex := textproc.NewLexer(text)
+       commented := clex.SkipByte('#')
+       if commented && clex.SkipHspace() || clex.EOF() {
+               return false, nil
+       }
+
        withoutLeadingComment := text
        if commented {
                withoutLeadingComment = withoutLeadingComment[1:]
        }
 
-       main, tokens, rest, spaceBeforeComment, hasComment, comment := splitMkLine(withoutLeadingComment)
+       data := p.split(nil, withoutLeadingComment)
 
-       lexer := NewMkTokensLexer(tokens)
+       lexer := NewMkTokensLexer(data.tokens)
        mainStart := lexer.Mark()
 
        for !commented && lexer.SkipByte(' ') {
@@ -1401,7 +1469,7 @@ func MatchVarassign(text string) (m bool
        // TODO: duplicated code in MkParser.Varname
        for lexer.NextBytesSet(VarbaseBytes) != "" || lexer.NextVarUse() != nil {
        }
-       if lexer.SkipByte('.') || hasPrefix(main, "SITES_") {
+       if lexer.SkipByte('.') || hasPrefix(data.main, "SITES_") {
                for lexer.NextBytesSet(VarparamBytes) != "" || lexer.NextVarUse() != nil {
                }
        }
@@ -1431,11 +1499,13 @@ func MatchVarassign(text string) (m bool
 
        lexer.SkipHspace()
 
-       value := trimHspace(lexer.Rest() + rest)
+       value := trimHspace(lexer.Rest())
+       valueAlign := ifelseStr(commented, "#", "") + lexer.Since(mainStart)
+       spaceBeforeComment := data.spaceBeforeComment
        if value == "" {
+               valueAlign += spaceBeforeComment
                spaceBeforeComment = ""
        }
-       valueAlign := ifelseStr(commented, "#", "") + lexer.Since(mainStart)
 
        return true, &mkLineAssignImpl{
                commented:         commented,
@@ -1450,7 +1520,7 @@ func MatchVarassign(text string) (m bool
                valueMkRest:       "",  // filled in lazily
                fields:            nil, // filled in lazily
                spaceAfterValue:   spaceBeforeComment,
-               comment:           ifelseStr(hasComment, "#", "") + comment,
+               comment:           ifelseStr(data.hasComment, "#", "") + data.comment,
        }
 }
 
Index: pkgsrc/pkgtools/pkglint/files/pkglint.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.50 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.51
--- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.50       Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.go    Sat Apr 20 17:43:24 2019
@@ -521,8 +521,9 @@ func CheckLinesMessage(lines Lines) {
        }
 
        if lines.Len() < 3 {
-               lines.LastLine().Warnf("File too short.")
-               G.Explain(explanation()...)
+               line := lines.LastLine()
+               line.Warnf("File too short.")
+               line.Explain(explanation()...)
                return
        }
 

Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.55 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.56
--- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.55   Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline_test.go        Sat Apr 20 17:43:24 2019
@@ -1,8 +1,11 @@
 package pkglint
 
-import "gopkg.in/check.v1"
+import (
+       "gopkg.in/check.v1"
+       "strings"
+)
 
-func (s *Suite) Test_NewMkLine__varassign(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__varassign(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -17,7 +20,7 @@ func (s *Suite) Test_NewMkLine__varassig
        c.Check(mkline.VarassignComment(), equals, "# varassign comment")
 }
 
-func (s *Suite) Test_NewMkLine__varassign_space_around_operator(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__varassign_space_around_operator(c *check.C) {
        t := s.Init(c)
 
        t.SetUpCommandLine("--show-autofix", "--source")
@@ -31,7 +34,7 @@ func (s *Suite) Test_NewMkLine__varassig
                "+\tpkgbase= package")
 }
 
-func (s *Suite) Test_NewMkLine__shellcmd(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__shellcmd(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -41,7 +44,7 @@ func (s *Suite) Test_NewMkLine__shellcmd
        c.Check(mkline.ShellCommand(), equals, "shell command # shell comment")
 }
 
-func (s *Suite) Test_NewMkLine__comment(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__comment(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -50,7 +53,7 @@ func (s *Suite) Test_NewMkLine__comment(
        c.Check(mkline.IsComment(), equals, true)
 }
 
-func (s *Suite) Test_NewMkLine__empty(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__empty(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101, "")
@@ -58,7 +61,7 @@ func (s *Suite) Test_NewMkLine__empty(c 
        c.Check(mkline.IsEmpty(), equals, true)
 }
 
-func (s *Suite) Test_NewMkLine__directive(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__directive(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -71,7 +74,7 @@ func (s *Suite) Test_NewMkLine__directiv
        c.Check(mkline.DirectiveComment(), equals, "directive comment")
 }
 
-func (s *Suite) Test_NewMkLine__include(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__include(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -85,7 +88,7 @@ func (s *Suite) Test_NewMkLine__include(
        c.Check(mkline.IsSysinclude(), equals, false)
 }
 
-func (s *Suite) Test_NewMkLine__sysinclude(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__sysinclude(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -99,7 +102,7 @@ func (s *Suite) Test_NewMkLine__sysinclu
        c.Check(mkline.IsInclude(), equals, false)
 }
 
-func (s *Suite) Test_NewMkLine__dependency(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__dependency(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -110,7 +113,7 @@ func (s *Suite) Test_NewMkLine__dependen
        c.Check(mkline.Sources(), equals, "source1 source2")
 }
 
-func (s *Suite) Test_NewMkLine__dependency_space(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__dependency_space(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -122,7 +125,7 @@ func (s *Suite) Test_NewMkLine__dependen
                "NOTE: test.mk:101: Space before colon in dependency line.")
 }
 
-func (s *Suite) Test_NewMkLine__varassign_append(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__varassign_append(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -134,7 +137,7 @@ func (s *Suite) Test_NewMkLine__varassig
        c.Check(mkline.Varparam(), equals, "")
 }
 
-func (s *Suite) Test_NewMkLine__merge_conflict(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__merge_conflict(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("test.mk", 101,
@@ -151,7 +154,7 @@ func (s *Suite) Test_NewMkLine__merge_co
        c.Check(mkline.IsSysinclude(), equals, false)
 }
 
-func (s *Suite) Test_NewMkLine__autofix_space_after_varname(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__autofix_space_after_varname(c *check.C) {
        t := s.Init(c)
 
        t.SetUpCommandLine("-Wspace")
@@ -187,7 +190,7 @@ func (s *Suite) Test_NewMkLine__autofix_
                "pkgbase := pkglint")
 }
 
-func (s *Suite) Test_NewMkLine__varname_with_hash(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__varname_with_hash(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("Makefile", 123, "VARNAME.#=\tvalue")
@@ -208,7 +211,7 @@ func (s *Suite) Test_NewMkLine__varname_
 //
 // 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_NewMkLine__escaped_hash_in_value(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__escaped_hash_in_value(c *check.C) {
        t := s.Init(c)
 
        mklines := t.SetUpFileMkLines("Makefile",
@@ -278,13 +281,13 @@ func (s *Suite) Test_VarUseContext_Strin
        vartype := G.Pkgsrc.VariableType(nil, "PKGNAME")
        vuc := VarUseContext{vartype, vucTimeUnknown, VucQuotBackt, false}
 
-       c.Check(vuc.String(), equals, "(Pkgname time:unknown quoting:backt wordpart:false)")
+       c.Check(vuc.String(), equals, "(Pkgname (package-settable) time:unknown quoting:backt wordpart:false)")
 }
 
 // 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_NewMkLine__number_sign(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__number_sign(c *check.C) {
        t := s.Init(c)
 
        mklineVarassignEscaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,\\#,hash,g'")
@@ -309,7 +312,7 @@ func (s *Suite) Test_NewMkLine__number_s
                "WARN: filename.mk:1: The # character starts a Makefile comment.")
 }
 
-func (s *Suite) Test_NewMkLine__varassign_leading_space(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__varassign_leading_space(c *check.C) {
        t := s.Init(c)
 
        _ = t.NewMkLine("rubyversion.mk", 427, " _RUBYVER=\t2.15")
@@ -327,7 +330,7 @@ func (s *Suite) Test_NewMkLine__varassig
 // be able to parse and check the infrastructure files as well.
 //
 // See Pkgsrc.loadUntypedVars.
-func (s *Suite) Test_NewMkLine__infrastructure(c *check.C) {
+func (s *Suite) Test_MkLineParser_Parse__infrastructure(c *check.C) {
        t := s.Init(c)
 
        mklines := t.NewMkLines("infra.mk",
@@ -413,7 +416,6 @@ func (s *Suite) Test_MkLine_VariableNeed
        MkLineChecker{nil, mkline}.checkVarassign()
 
        t.CheckOutputLines(
-               "WARN: builtin.mk:3: PKG_ADMIN should not be used at load time in any file.",
                "NOTE: builtin.mk:3: The :Q operator isn't necessary for ${BUILTIN_PKG.Xfixes} here.")
 }
 
@@ -841,8 +843,7 @@ func (s *Suite) Test_MkLine_VariableNeed
                "\tname in which the variable is used or defined. The rules for PATH",
                "\tare:",
                "",
-               "\t* in buildlink3.mk, it should not be accessed at all",
-               "\t* in any file, it may be used",
+               "\t* in any file, it may be used at load time, or used",
                "",
                "\tIf these rules seem to be incorrect, please ask on the",
                "\ttech-pkg%NetBSD.org@localhost mailing list.",
@@ -867,28 +868,6 @@ func (s *Suite) Test_MkLine_VariableNeed
                "",
                "\tIf these rules seem to be incorrect, please ask on the",
                "\ttech-pkg%NetBSD.org@localhost mailing list.",
-               "",
-               "WARN: ~/Makefile:6: PATH should not be used at load time in any file.",
-               "",
-               "\tMany variables, especially lists of something, get their values",
-               "\tincrementally. Therefore it is generally unsafe to rely on their",
-               "\tvalue until it is clear that it will never change again. This point",
-               "\tis reached when the whole package Makefile is loaded and execution",
-               "\tof the shell commands starts; in some cases earlier.",
-               "",
-               "\tAdditionally, when using the \":=\" operator, each $$ is replaced with",
-               "\ta single $, so variables that have references to shell variables or",
-               "\tregular expressions are modified in a subtle way.",
-               "",
-               "\tThe allowed actions for a variable are determined based on the file",
-               "\tname in which the variable is used or defined. The rules for PATH",
-               "\tare:",
-               "",
-               "\t* in buildlink3.mk, it should not be accessed at all",
-               "\t* in any file, it may be used",
-               "",
-               "\tIf these rules seem to be incorrect, please ask on the",
-               "\ttech-pkg%NetBSD.org@localhost mailing list.",
                "")
 
        // Just for branch coverage.
@@ -1142,56 +1121,87 @@ func (s *Suite) Test_MkLine_ValueFields_
 func (s *Suite) Test_MkLine_ValueTokens(c *check.C) {
        t := s.Init(c)
 
-       testTokens := func(value string, expected ...*MkToken) {
+       text := func(text string) *MkToken { return &MkToken{text, nil} }
+       varUseText := func(text string, varname string, modifiers ...string) *MkToken {
+               return &MkToken{text, NewMkVarUse(varname, modifiers...)}
+       }
+       tokens := func(tokens ...*MkToken) []*MkToken { return tokens }
+       test := func(value string, expected []*MkToken, diagnostics ...string) {
                mkline := t.NewMkLine("Makefile", 1, "PATH=\t"+value)
-               tokens, _ := mkline.ValueTokens()
-               c.Check(tokens, deepEquals, expected)
+               actualTokens, _ := mkline.ValueTokens()
+               c.Check(actualTokens, deepEquals, expected)
+               t.CheckOutput(diagnostics)
        }
 
-       testTokens("#empty",
-               []*MkToken(nil)...)
+       t.Use(text, varUseText, tokens, test)
 
-       testTokens("value",
-               &MkToken{"value", nil})
+       test("#empty",
+               tokens())
 
-       testTokens("value ${VAR} rest",
-               &MkToken{"value ", nil},
-               &MkToken{"${VAR}", NewMkVarUse("VAR")},
-               &MkToken{" rest", nil})
+       test("value",
+               tokens(text("value")))
 
-       testTokens("value ${UNFINISHED",
-               &MkToken{"value ", nil})
+       test("value ${VAR} rest",
+               tokens(
+                       text("value "),
+                       varUseText("${VAR}", "VAR"),
+                       text(" rest")))
+
+       test("value # comment",
+               tokens(
+                       text("value")))
+
+       test("value ${UNFINISHED",
+               tokens(
+                       text("value "),
+                       varUseText("${UNFINISHED", "UNFINISHED")),
+
+               "WARN: Makefile:1: Missing closing \"}\" for \"UNFINISHED\".")
 }
 
 func (s *Suite) Test_MkLine_ValueTokens__caching(c *check.C) {
        t := s.Init(c)
 
+       tokens := func(tokens ...*MkToken) []*MkToken { return tokens }
+
        mkline := t.NewMkLine("Makefile", 1, "PATH=\tvalue ${UNFINISHED")
-       tokens, rest := mkline.ValueTokens()
+       valueTokens, rest := mkline.ValueTokens()
 
-       c.Check(tokens, deepEquals, []*MkToken{{"value ", nil}})
-       c.Check(rest, equals, "${UNFINISHED")
+       c.Check(valueTokens, deepEquals,
+               tokens(
+                       &MkToken{"value ", nil},
+                       &MkToken{"${UNFINISHED", NewMkVarUse("UNFINISHED")}))
+       c.Check(rest, equals, "")
+       t.CheckOutputLines(
+               "WARN: Makefile:1: Missing closing \"}\" for \"UNFINISHED\".")
 
-       tokens2, rest2 := mkline.ValueTokens() // This time the slice is taken from the cache.
+       // This time the slice is taken from the cache.
+       tokens2, rest2 := mkline.ValueTokens()
 
-       // In Go, it's not possible to compare slices for reference equality.
-       c.Check(tokens2, deepEquals, tokens)
+       c.Check(&tokens2[0], equals, &valueTokens[0])
        c.Check(rest2, equals, rest)
 }
 
 func (s *Suite) Test_MkLine_ValueTokens__caching_parse_error(c *check.C) {
        t := s.Init(c)
 
+       tokens := func(tokens ...*MkToken) []*MkToken { return tokens }
+       varuseText := func(text, varname string, modifiers ...string) *MkToken {
+               return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
+       }
+
        mkline := t.NewMkLine("Makefile", 1, "PATH=\t${UNFINISHED")
-       tokens, rest := mkline.ValueTokens()
+       valueTokens, rest := mkline.ValueTokens()
 
-       c.Check(tokens, check.IsNil)
-       c.Check(rest, equals, "${UNFINISHED")
+       c.Check(valueTokens, deepEquals, tokens(varuseText("${UNFINISHED", "UNFINISHED")))
+       c.Check(rest, equals, "")
+       t.CheckOutputLines(
+               "WARN: Makefile:1: Missing closing \"}\" for \"UNFINISHED\".")
 
-       tokens2, rest2 := mkline.ValueTokens() // This time the slice is taken from the cache.
+       // This time the slice is taken from the cache.
+       tokens2, rest2 := mkline.ValueTokens()
 
-       // In Go, it's not possible to compare slices for reference equality.
-       c.Check(tokens2, deepEquals, tokens)
+       c.Check(&tokens2[0], equals, &valueTokens[0])
        c.Check(rest2, equals, rest)
 }
 
@@ -1266,14 +1276,16 @@ func (s *Suite) Test_MkLine_ResolveVarsI
                "WARN: ~/multimedia/totem/bla.mk:2: "+
                        "The variable BUILDLINK_PKGSRCDIR.totem should not be given a default value in this file; "+
                        "it would be ok in buildlink3.mk.",
-               "ERROR: ~/multimedia/totem/bla.mk:2: There is no package in \"multimedia/totem\".")
+               "ERROR: ~/multimedia/totem/bla.mk:2: Relative path \"../../multimedia/totem/Makefile\" does not exist.")
 }
 
-func (s *Suite) Test_MatchVarassign(c *check.C) {
-       s.Init(c)
+func (s *Suite) Test_MkLineParser_MatchVarassign(c *check.C) {
+       t := s.Init(c)
 
-       test := func(text string, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string) {
-               m, actual := MatchVarassign(text)
+       test := func(text string, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string, diagnostics ...string) {
+               line := t.NewLine("filename.mk", 123, text)
+               data := MkLineParser{}.split(line, text)
+               m, actual := MkLineParser{}.MatchVarassign(line, text, data)
                if !m {
                        c.Errorf("Text %q doesn't match variable assignment", text)
                        return
@@ -1295,13 +1307,17 @@ func (s *Suite) Test_MatchVarassign(c *c
                        comment:           comment,
                }
                c.Check(*actual, deepEquals, expected)
+               t.CheckOutput(diagnostics)
        }
 
-       testInvalid := func(text string) {
-               m, _ := MatchVarassign(text)
+       testInvalid := func(text string, diagnostics ...string) {
+               line := t.NewLine("filename.mk", 123, text)
+               data := MkLineParser{}.split(nil, text)
+               m, _ := MkLineParser{}.MatchVarassign(line, text, data)
                if m {
                        c.Errorf("Text %q matches variable assignment but shouldn't.", text)
                }
+               t.CheckOutput(diagnostics)
        }
 
        test("C++=c11", false, "C+", "", "+=", "C++=", "c11", "", "")
@@ -1396,6 +1412,7 @@ func (s *Suite) Test_MatchVarassign(c *c
                "# none")
 
        test("EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
+
                false,
                "EGDIRS",
                "",
@@ -1403,7 +1420,14 @@ func (s *Suite) Test_MatchVarassign(c *c
                "EGDIRS=\t",
                "${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
                "",
-               "")
+               "",
+
+               "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\".")
 
        test("VAR:=\t${VAR:M-*:[\\#]}",
                false,
@@ -1414,6 +1438,13 @@ func (s *Suite) Test_MatchVarassign(c *c
                "${VAR:M-*:[#]}",
                "",
                "")
+
+       test("#VAR=value",
+               true, "VAR", "", "=", "#VAR=", "value", "", "")
+
+       testInvalid("# VAR=value")
+       testInvalid("#\tVAR=value")
+       testInvalid(MkRcsID)
 }
 
 func (s *Suite) Test_NewMkOperator(c *check.C) {
@@ -1532,6 +1563,7 @@ func (s *Suite) Test_Indentation_Varname
                ".    include \"../../category/other/buildlink3.mk\"",
                ".  endif",
                ".endif")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -1590,6 +1622,9 @@ func (s *Suite) Test_MkLine_ForEachUsed(
                "run <",
                "run @",
                "run x"})
+       t.CheckOutputLines(
+               "WARN: Makefile:12: Please use curly braces {} instead of round parentheses () for ROUND_PARENTHESES.",
+               "WARN: Makefile:14: $x is ambiguous. Use ${x} if you mean a Make variable or $$x if you mean a shell variable.")
 }
 
 func (s *Suite) Test_MkLine_UnquoteShell(c *check.C) {
@@ -1621,11 +1656,11 @@ func (s *Suite) Test_MkLine_UnquoteShell
        test("`", "`")
 }
 
-func (s *Suite) Test_unescapeMkComment(c *check.C) {
+func (s *Suite) Test_MkLineParser_unescapeComment(c *check.C) {
        t := s.Init(c)
 
        test := func(text string, main, comment string) {
-               aMain, aComment := unescapeMkComment(text)
+               aMain, aComment := MkLineParser{}.unescapeComment(text)
                t.Check(
                        []interface{}{text, aMain, aComment},
                        deepEquals,
@@ -1741,16 +1776,19 @@ func (s *Suite) Test_unescapeMkComment(c
                "#comment")
 }
 
-func (s *Suite) Test_splitMkLine(c *check.C) {
+func (s *Suite) Test_MkLineParser_split(c *check.C) {
        t := s.Init(c)
 
        varuse := func(varname string, modifiers ...string) *MkToken {
-               text := "${" + varname
+               var text strings.Builder
+               text.WriteString("${")
+               text.WriteString(varname)
                for _, modifier := range modifiers {
-                       text += ":" + modifier
+                       text.WriteString(":")
+                       text.WriteString(modifier)
                }
-               text += "}"
-               return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
+               text.WriteString("}")
+               return &MkToken{Text: text.String(), Varuse: NewMkVarUse(varname, modifiers...)}
        }
        varuseText := func(text, varname string, modifiers ...string) *MkToken {
                return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
@@ -1761,30 +1799,67 @@ func (s *Suite) Test_splitMkLine(c *chec
        tokens := func(tokens ...*MkToken) []*MkToken {
                return tokens
        }
-       _, _, _, _ = text, varuse, varuseText, tokens
 
-       test := func(text string, main string, tokens []*MkToken, rest string, spaceBeforeComment string, hasComment bool, comment string) {
-               aMain, aTokens, aRest, aSpaceBeforeComment, aHasComment, aComment := splitMkLine(text)
-               t.Check(
-                       []interface{}{text, aTokens, aMain, aRest, aSpaceBeforeComment, aHasComment, aComment},
-                       deepEquals,
-                       []interface{}{text, tokens, main, rest, spaceBeforeComment, hasComment, comment})
+       test := func(text string, data mkLineSplitResult, diagnostics ...string) {
+               line := t.NewLine("filename.mk", 123, text)
+               actualData := MkLineParser{}.split(line, text)
+
+               t.CheckOutput(diagnostics)
+               t.Check([]interface{}{text, actualData}, deepEquals, []interface{}{text, data})
        }
 
-       test("",
-               "",
-               tokens(),
-               "",
+       t.Use(text, varuse, varuseText, tokens)
+
+       test(
                "",
-               false,
-               "")
-       test("text",
+               mkLineSplitResult{})
+
+       test(
                "text",
-               tokens(text("text")),
-               "",
-               "",
-               false,
-               "")
+               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.
@@ -1792,161 +1867,165 @@ func (s *Suite) Test_splitMkLine(c *chec
        // The difference between "#defined" and "# defined" is relevant in a few
        // cases, such as the API documentation of the infrastructure files.
        test("# comment",
-               "",
-               tokens(),
-               "",
-               "",
-               true,
-               " comment")
+               mkLineSplitResult{
+                       hasComment: true,
+                       comment:    " comment",
+               })
+
        test("#\tcomment",
-               "",
-               tokens(),
-               "",
-               "",
-               true,
-               "\tcomment")
+               mkLineSplitResult{
+                       hasComment: true,
+                       comment:    "\tcomment",
+               })
+
        test("#   comment",
-               "",
-               tokens(),
-               "",
-               "",
-               true,
-               "   comment")
+               mkLineSplitResult{
+                       hasComment: true,
+                       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",
-               tokens(text("COMMENT=\tThe C")),
-               "",
-               "",
-               true,
-               " compiler")
+               mkLineSplitResult{
+                       main:       "COMMENT=\tThe C",
+                       tokens:     tokens(text("COMMENT=\tThe C")),
+                       hasComment: true,
+                       comment:    " compiler",
+               })
+
        test("COMMENT=\tThe C\\# compiler",
-               "COMMENT=\tThe C# compiler",
-               tokens(text("COMMENT=\tThe C# compiler")),
-               "",
-               "",
-               false,
-               "")
+               mkLineSplitResult{
+                       main:       "COMMENT=\tThe C# compiler",
+                       tokens:     tokens(text("COMMENT=\tThe C# compiler")),
+                       hasComment: false,
+                       comment:    "",
+               })
 
        test("${TARGET}: ${SOURCES} # comment",
-               "${TARGET}: ${SOURCES}",
-               tokens(varuse("TARGET"), text(": "), varuse("SOURCES"), text(" ")),
-               "",
-               " ",
-               true,
-               " 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",
-               "VAR=\t${OTHER:[#]}",
-               tokens(text("VAR=\t"), varuse("OTHER", "[#]"), text(" ")),
-               "",
-               " ",
-               true,
-               " 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-*:[\\#]}",
-               "VAR:=\t${VAR:M-*:[#]}",
-               tokens(text("VAR:=\t"), varuse("VAR", "M-*", "[#]")),
-               "",
-               "",
-               false,
-               "")
+               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",
-               "VAR0=",
-               tokens(text("VAR0=\t")),
-               "",
-               // 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.
-               "\t",
-               true,
-               "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",
-               "VAR1=\t#no-comment",
-               tokens(text("VAR1=\t#no-comment")),
-               "",
-               "",
-               false,
-               "")
+               mkLineSplitResult{
+                       main:   "VAR1=\t#no-comment",
+                       tokens: tokens(text("VAR1=\t#no-comment")),
+               })
+
        test("VAR2=\t\\\\#comment",
-               "VAR2=\t\\\\",
-               tokens(text("VAR2=\t\\\\")),
-               "",
-               "",
-               true,
-               "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",
-               "VAR0=\t$T",
-               tokens(text("VAR0=\t"), varuseText("$T", "T")),
-               "",
-               "",
-               false,
-               "")
+               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",
-               "VAR1=\t\\$T",
-               tokens(text("VAR1=\t\\"), varuseText("$T", "T")),
-               "",
-               "",
-               false,
-               "")
+               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",
-               "VAR2=\t\\\\$T",
-               tokens(text("VAR2=\t\\\\"), varuseText("$T", "T")),
-               "",
-               "",
-               false,
-               "")
+               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",
-               "$$shellvar $${shellvar} \\${MKVAR} [] \\x",
-               tokens(text("$$shellvar $${shellvar} \\"), varuse("MKVAR"), text(" [] \\x")),
-               "",
-               "",
-               false,
-               "")
+               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",
-               "",
-               tokens(),
-               "${UNCLOSED",
-               "",
-               false,
-               "")
+               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",
-               "text before ",
-               tokens(text("text before ")),
-               "${UNCLOSED",
-               "",
-               true,
-               " 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",
-               "text before ",
-               tokens(text("text before ")),
-               "${UNCLOSED",
-               " ",
-               true,
-               " 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 "",
@@ -1957,58 +2036,73 @@ func (s *Suite) Test_splitMkLine(c *chec
        //  variable name, mainly because the empty variable name is not visible
        //  outside of the bmake debugging mode.
        test("Lonely $ character $",
-               "Lonely $ character ",
-               tokens(
-                       text("Lonely "),
-                       varuseText("$ " /* instead of "${ }" */, " "),
-                       text("character ")),
-               "$",
-               "",
-               false,
-               "")
+               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",
-               "COMMENT=\t[#] $$# $$",
-               tokens(text("COMMENT=\t[#] $$# $$")),
-               "",
-               "",
-               true,
-               " comment")
+               mkLineSplitResult{
+                       main:       "COMMENT=\t[#] $$# $$",
+                       tokens:     tokens(text("COMMENT=\t[#] $$# $$")),
+                       hasComment: true,
+                       comment:    " comment",
+               })
 
        test("VAR2=\t\\\\#comment",
-               "VAR2=\t\\\\",
-               tokens(text("VAR2=\t\\\\")),
-               "",
-               "",
-               true,
-               "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(")")),
+               })
 }
 
-func (s *Suite) Test_matchMkDirective(c *check.C) {
+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)
+               data := MkLineParser{}.split(line, input)
+               mkline := MkLineParser{}.parseDirective(line, data)
+               if !c.Check(mkline, check.NotNil) {
+                       return
+               }
 
-       test := func(input, expectedIndent, expectedDirective, expectedArgs, expectedComment string) {
-               m, indent, directive, args, comment := matchMkDirective(input)
                c.Check(
-                       []interface{}{m, indent, directive, args, comment},
+                       []interface{}{mkline.Indent(), mkline.Directive(), mkline.Args(), mkline.DirectiveComment()},
                        deepEquals,
-                       []interface{}{true, expectedIndent, expectedDirective, expectedArgs, expectedComment})
-       }
-
-       testFail := func(input string) {
-               m, indent, directive, args, comment := matchMkDirective(input)
-               if m {
-                       c.Errorf("The line %q could be parsed as directive (%q, %q, %q, %q) but shouldn't.",
-                               indent, directive, args, comment)
-               }
+                       []interface{}{expectedIndent, expectedDirective, expectedArgs, expectedComment})
+               t.CheckOutput(diagnostics)
        }
 
        test(".if ${VAR} == value",
                "", "if", "${VAR} == value", "")
 
        test(".\tendif # comment",
-               "\t", "endif", "", " comment")
+               "\t", "endif", "", "comment")
 
        test(".if ${VAR} == \"#\"",
                "", "if", "${VAR} == \"", "\"")
@@ -2019,8 +2113,9 @@ func (s *Suite) Test_matchMkDirective(c 
        test(".if ${VAR} == \\",
                "", "if", "${VAR} == \\", "")
 
-       // Unclosed variable
-       testFail(".if ${VAR")
+       test(".if ${VAR",
+               "", "if", "${VAR", "",
+               "WARN: filename.mk:123: Missing closing \"}\" for \"VAR\".")
 }
 
 func (s *Suite) Test_MatchMkInclude(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.33 pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.34
--- pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.33 Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker.go      Sat Apr 20 17:43:24 2019
@@ -84,7 +84,7 @@ func (ck MkLineChecker) checkInclude() {
        switch {
        case hasSuffix(includedFile, "/Makefile"):
                mkline.Errorf("Other Makefiles must not be included directly.")
-               G.Explain(
+               mkline.Explain(
                        "To include portions of another Makefile, extract the common parts",
                        "and put them into a Makefile.common or a Makefile fragment called",
                        "module.mk or similar.",
@@ -222,7 +222,7 @@ func (ck MkLineChecker) checkDirectiveFo
                // The guessed flag could also be determined more correctly. As of November 2018,
                // running pkglint over the whole pkgsrc tree did not produce any different result
                // whether guessed was true or false.
-               forLoopType := Vartype{lkShell, btForLoop, []ACLEntry{{"*", aclpAllRead}}, false}
+               forLoopType := Vartype{btForLoop, List, []ACLEntry{{"*", aclpAllRead}}}
                forLoopContext := VarUseContext{&forLoopType, vucTimeParse, VucQuotPlain, false}
                mkline.ForEachUsed(func(varUse *MkVarUse, time vucTime) {
                        ck.CheckVaruse(varUse, &forLoopContext)
@@ -271,7 +271,7 @@ func (ck MkLineChecker) checkDependencyR
 
                } else if !allowedTargets[target] {
                        mkline.Warnf("Undeclared target %q.", target)
-                       G.Explain(
+                       mkline.Explain(
                                "To define a custom target in a package, declare it like this:",
                                "",
                                "\t.PHONY: my-target",
@@ -353,6 +353,8 @@ func (ck MkLineChecker) explainPermissio
                return
        }
 
+       // TODO: Starting with the second explanation, omit the common part. Instead, only list the permission rules.
+
        var expl []string
 
        if len(intro) > 0 {
@@ -385,7 +387,7 @@ func (ck MkLineChecker) explainPermissio
                "",
                "If these rules seem to be incorrect, please ask on the tech-pkg%NetBSD.org@localhost mailing list.")
 
-       G.Explain(expl...)
+       ck.MkLine.Explain(expl...)
 }
 
 // CheckVaruse checks a single use of a variable in a specific context.
@@ -415,7 +417,7 @@ func (ck MkLineChecker) CheckVaruse(varu
 func (ck MkLineChecker) checkVarUseVarname(varuse *MkVarUse) {
        if varuse.varname == "@" {
                ck.MkLine.Warnf("Please use %q instead of %q.", "${.TARGET}", "$@")
-               G.Explain(
+               ck.MkLine.Explain(
                        "It is more readable and prevents confusion with the shell variable",
                        "of the same name.")
        }
@@ -453,7 +455,7 @@ func (ck MkLineChecker) checkVaruseUndef
        case !G.Opts.WarnExtra:
                return
 
-       case vartype != nil && !vartype.guessed:
+       case vartype != nil && !vartype.Guessed():
                // Well-known variables are probably defined by the infrastructure.
                return
 
@@ -492,9 +494,9 @@ func (ck MkLineChecker) checkVaruseModif
 }
 
 func (ck MkLineChecker) checkVaruseModifiersSuffix(varuse *MkVarUse, vartype *Vartype) {
-       if varuse.modifiers[0].IsSuffixSubst() && vartype != nil && !vartype.IsConsideredList() {
+       if varuse.modifiers[0].IsSuffixSubst() && vartype != nil && !vartype.List() {
                ck.MkLine.Warnf("The :from=to modifier should only be used with lists, not with %s.", varuse.varname)
-               G.Explain(
+               ck.MkLine.Explain(
                        "Instead of (for example):",
                        "\tMASTER_SITES=\t${HOMEPAGE:=repository/}",
                        "",
@@ -570,7 +572,7 @@ func (ck MkLineChecker) checkVarusePermi
        indirectly := !directly && vuc.vartype != nil &&
                vuc.vartype.Union().Contains(aclpUseLoadtime)
 
-       if vartype.guessed {
+       if vartype.Guessed() {
                return
        }
 
@@ -710,7 +712,7 @@ func (ck MkLineChecker) warnVaruseToolLo
        }
 
        ck.MkLine.Warnf("The tool ${%s} cannot be used at load time.", varname)
-       G.Explain(
+       ck.MkLine.Explain(
                "To use a tool at load time, it must be declared in the package",
                "Makefile by adding it to USE_TOOLS.",
                "After that, bsd.prefs.mk must be included.",
@@ -758,7 +760,7 @@ func (ck MkLineChecker) checkVarUseQuoti
                modNoM := strings.TrimSuffix(modNoQ, ":M*")
                correctMod := modNoM + ifelseStr(needMstar, ":M*:Q", ":Q")
                if correctMod == mod+":Q" && vuc.IsWordPart && !vartype.IsShell() {
-                       if vartype.IsConsideredList() {
+                       if vartype.List() {
                                mkline.Warnf("The list variable %s should not be embedded in a word.", varname)
                                mkline.Explain(
                                        "When a list variable has multiple elements, this expression expands",
@@ -876,7 +878,7 @@ func (ck MkLineChecker) checkVarassignDe
                if i > 0 && ver >= intVersions[i-1] {
                        mkline.Warnf("The values for %s should be in decreasing order (%d before %d).",
                                mkline.Varname(), ver, intVersions[i-1])
-                       G.Explain(
+                       mkline.Explain(
                                "If they aren't, it may be possible that needless versions of",
                                "packages are installed.")
                }
@@ -903,7 +905,7 @@ func (ck MkLineChecker) checkVarassignLe
 
        ck.checkTextVarUse(
                ck.MkLine.Varname(),
-               &Vartype{lkNone, BtVariableName, []ACLEntry{{"*", aclpAll}}, false},
+               &Vartype{BtVariableName, NoVartypeOptions, []ACLEntry{{"*", aclpAll}}},
                vucTimeParse)
 }
 
@@ -1023,10 +1025,15 @@ func (ck MkLineChecker) checkVarassignLe
                return
        }
 
-       // FIXME: Explain how to fix this warning.
-       //  For files like module.mk that are used by other packages,
-       //  documenting the variable already makes the warning disappear.
        ck.MkLine.Warnf("%s is defined but not used.", varname)
+       ck.MkLine.Explain(
+               "This might be a simple typo.",
+               "",
+               "If a package provides a file containing several related variables",
+               "(such as module.mk, app.mk, extension.mk), that file may define",
+               "variables that look unused since they are only used by other packages.",
+               "These variables should be documented at the head of the file;",
+               "see mk/subst.mk for an example of such a documentation comment.")
 }
 
 // checkVarassignRightVaruse checks that in a variable assignment,
@@ -1121,7 +1128,7 @@ func (ck MkLineChecker) checkVarassignMi
 
        if mkline.VarassignComment() == "# defined" && !hasSuffix(varname, "_MK") && !hasSuffix(varname, "_COMMON") {
                mkline.Notef("Please use \"# empty\", \"# none\" or \"# yes\" instead of \"# defined\".")
-               G.Explain(
+               mkline.Explain(
                        "The value #defined says something about the state of the variable,",
                        "but not what that _means_.",
                        "In some cases a variable that is defined",
@@ -1171,8 +1178,20 @@ func (ck MkLineChecker) checkVarassignLe
                return
        }
 
+       // Package-settable variables may use the ?= operator before including
+       // bsd.prefs.mk in situations like the following:
+       //
+       //  Makefile:  LICENSE=       package-license
+       //             .include "module.mk"
+       //  module.mk: LICENSE?=      default-license
+       //
+       vartype := G.Pkgsrc.VariableType(nil, mkline.Varname())
+       if vartype != nil && vartype.PackageSettable() {
+               return
+       }
+
        mkline.Warnf("Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")
-       G.Explain(
+       mkline.Explain(
                "The ?= operator is used to provide a default value to a variable.",
                "In pkgsrc, many variables can be set by the pkgsrc user in the",
                "mk.conf file.",
@@ -1212,8 +1231,8 @@ func (ck MkLineChecker) checkVartype(var
                        trace.Step1("Unchecked use of !=: %q", value)
                }
 
-       case vartype.kindOfList == lkNone:
-               ck.CheckVartypeBasic(varname, vartype.basicType, op, value, comment, vartype.guessed)
+       case !vartype.List():
+               ck.CheckVartypeBasic(varname, vartype.basicType, op, value, comment, vartype.Guessed())
 
        case value == "":
                break
@@ -1221,7 +1240,7 @@ func (ck MkLineChecker) checkVartype(var
        default:
                words := mkline.ValueFields(value)
                for _, word := range words {
-                       ck.CheckVartypeBasic(varname, vartype.basicType, op, word, comment, vartype.guessed)
+                       ck.CheckVartypeBasic(varname, vartype.basicType, op, word, comment, vartype.Guessed())
                }
        }
 }
@@ -1344,7 +1363,7 @@ func (ck MkLineChecker) checkDirectiveCo
        varname := varuse.varname
        if matches(varname, `^\$.*:[MN]`) {
                ck.MkLine.Warnf("The empty() function takes a variable name as parameter, not a variable expression.")
-               G.Explain(
+               ck.MkLine.Explain(
                        "Instead of empty(${VARNAME:Mpattern}), you should write either of the following:",
                        "",
                        "\tempty(VARNAME:Mpattern)",
@@ -1362,10 +1381,10 @@ func (ck MkLineChecker) checkDirectiveCo
                        ck.checkVartype(varname, opUseMatch, pattern, "")
 
                        vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
-                       if matches(pattern, `^[\w-/]+$`) && vartype != nil && !vartype.IsConsideredList() {
+                       if matches(pattern, `^[\w-/]+$`) && vartype != nil && !vartype.List() {
                                ck.MkLine.Notef("%s should be compared using %s instead of matching against %q.",
                                        varname, ifelseStr(positive, "==", "!="), ":"+modifier.Text)
-                               G.Explain(
+                               ck.MkLine.Explain(
                                        "This variable has a single value, not a list of values.",
                                        "Therefore it feels strange to apply list operators like :M and :N onto it.",
                                        "A more direct approach is to use the == and != operators.",
@@ -1383,7 +1402,7 @@ func (ck MkLineChecker) checkCompareVarS
 
        if varname == "PKGSRC_COMPILER" {
                ck.MkLine.Warnf("Use ${PKGSRC_COMPILER:%s%s} instead of the %s operator.", ifelseStr(op == "==", "M", "N"), value, op)
-               G.Explain(
+               ck.MkLine.Explain(
                        "The PKGSRC_COMPILER can be a list of chained compilers, e.g. \"ccache distcc clang\".",
                        "Therefore, comparing it using == or != leads to wrong results in these cases.")
        }
@@ -1406,18 +1425,12 @@ func (ck MkLineChecker) CheckRelativePkg
        }
 
        mkline := ck.MkLine
-       ck.CheckRelativePath(pkgdir, true)
+       ck.CheckRelativePath(pkgdir+"/Makefile", true)
        pkgdir = mkline.ResolveVarsInRelativePath(pkgdir)
 
-       // XXX: Is the leading "./" realistic?
-       if m, otherpkgpath := match1(pkgdir, `^(?:\./)?\.\./\.\./([^/]+/[^/]+)$`); m {
-               if !fileExists(G.Pkgsrc.File(otherpkgpath + "/Makefile")) {
-                       mkline.Errorf("There is no package in %q.", otherpkgpath)
-               }
-
-       } else if !containsVarRef(pkgdir) {
+       if !matches(pkgdir, `^\.\./\.\./([^./][^/]*/[^./][^/]*)$`) && !containsVarRef(pkgdir) {
                mkline.Warnf("%q is not a valid relative package directory.", pkgdir)
-               G.Explain(
+               mkline.Explain(
                        "A relative pathname always starts with \"../../\", followed",
                        "by a category, a slash and a the directory name of the package.",
                        "For example, \"../../misc/screen\" is a valid relative pathname.")

Index: pkgsrc/pkgtools/pkglint/files/mklines.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines.go:1.45 pkgsrc/pkgtools/pkglint/files/mklines.go:1.46
--- pkgsrc/pkgtools/pkglint/files/mklines.go:1.45       Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines.go    Sat Apr 20 17:43:24 2019
@@ -28,7 +28,7 @@ type MkLinesImpl struct {
 func NewMkLines(lines Lines) MkLines {
        mklines := make([]MkLine, lines.Len())
        for i, line := range lines.Lines {
-               mklines[i] = NewMkLine(line)
+               mklines[i] = MkLineParser{}.Parse(line)
        }
 
        tools := NewTools()
@@ -459,7 +459,7 @@ func (mklines *MkLinesImpl) SaveAutofixC
 }
 
 func (mklines *MkLinesImpl) EOFLine() MkLine {
-       return NewMkLine(mklines.lines.EOFLine())
+       return MkLineParser{}.Parse(mklines.lines.EOFLine())
 }
 
 // VaralignBlock checks that all variable assignments from a paragraph
@@ -531,7 +531,8 @@ func (va *VaralignBlock) processVarassig
        if mkline.IsMultiline() {
                // Parsing the continuation marker as variable value is cheating but works well.
                text := strings.TrimSuffix(mkline.raw[0].orignl, "\n")
-               m, a := MatchVarassign(text)
+               data := MkLineParser{}.split(nil, text)
+               m, a := MkLineParser{}.MatchVarassign(mkline.Line, text, data)
                continuation = m && a.value == "\\"
        }
 
Index: pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.45 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.46
--- pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.45     Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go  Sat Apr 20 17:43:25 2019
@@ -308,11 +308,10 @@ func (s *Suite) Test_VartypeCheck_Depend
        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\" does not exist.",
+               "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\" does not exist.",
-               "ERROR: ~/category/package/filename.mk:3: There is no package in \"lang/perl5\".",
+               "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.")
@@ -704,9 +703,11 @@ func (s *Suite) Test_VartypeCheck_LdFlag
 
 func (s *Suite) Test_VartypeCheck_License(c *check.C) {
        t := s.Init(c)
-       t.SetUpPkgsrc() // Adds the gnu-gpl-v2 and 2-clause-bsd licenses
 
+       t.SetUpPkgsrc() // Adds the gnu-gpl-v2 and 2-clause-bsd licenses
        t.SetUpPackage("category/package")
+       t.FinishSetUp()
+
        G.Pkg = NewPackage(t.File("category/package"))
 
        mklines := t.NewMkLines("perl5.mk",
@@ -914,7 +915,6 @@ func (s *Suite) Test_VartypeCheck_Pathna
        vt.Values(
                "anything")
 
-       // FIXME: Warn about the absolute pathname in line 4.
        vt.Output(
                "WARN: filename.mk:1: \"${PREFIX}/*\" is not a valid pathname.")
 }
@@ -1004,9 +1004,9 @@ func (s *Suite) Test_VartypeCheck_PkgPat
                "../../invalid/relative")
 
        vt.Output(
-               "ERROR: filename.mk:3: Relative path \"../../invalid\" does not exist.",
+               "ERROR: filename.mk:3: Relative path \"../../invalid/Makefile\" does not exist.",
                "WARN: filename.mk:3: \"../../invalid\" is not a valid relative package directory.",
-               "ERROR: filename.mk:4: Relative path \"../../../../invalid/relative\" does not exist.",
+               "ERROR: filename.mk:4: Relative path \"../../../../invalid/relative/Makefile\" does not exist.",
                "WARN: filename.mk:4: \"../../../../invalid/relative\" is not a valid relative package directory.")
 }
 
@@ -1112,6 +1112,8 @@ func (s *Suite) Test_VartypeCheck_SedCom
                "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\".")
 }
 
@@ -1209,9 +1211,9 @@ func (s *Suite) Test_VartypeCheck_Tool(c
        vt.Op(opUseMatch)
        vt.Values(
                "tool1",
-               "tool1:build",
-               "tool1:*",
-               "${t}:build")
+               "tool1\\:build",
+               "tool1\\:*",
+               "${t}\\:build")
 
        vt.OutputEmpty()
 }
@@ -1485,7 +1487,9 @@ func (vt *VartypeCheckTester) Op(op MkOp
 
 // 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, and for the opUseMatch operator, all closing braces must be escaped.
+// 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 {
@@ -1522,7 +1526,7 @@ func (vt *VartypeCheckTester) Values(val
 
                // See MkLineChecker.checkVartype.
                var lineValues []string
-               if vartype == nil || vartype.kindOfList == lkNone {
+               if vartype == nil || !vartype.List() {
                        lineValues = []string{effectiveValue}
                } else {
                        lineValues = mkline.ValueFields(effectiveValue)

Index: pkgsrc/pkgtools/pkglint/files/mklines_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.40 pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.41
--- pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.40  Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines_test.go       Sat Apr 20 17:43:24 2019
@@ -35,7 +35,6 @@ func (s *Suite) Test_MkLines__quoting_LD
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: Makefile:3: Please use ${X11_LDFLAGS:M*:Q} instead of ${X11_LDFLAGS:Q}.",
                "WARN: Makefile:3: Please use ${X11_LDFLAGS:M*:Q} instead of ${X11_LDFLAGS:Q}.")
 }
 
@@ -312,7 +311,6 @@ func (s *Suite) Test_MkLines_collectDefi
        t.SetUpPkgsrc()
        t.CreateFileLines("mk/tools/defaults.mk",
                "USE_TOOLS+=     autoconf autoconf213")
-       G.Pkgsrc.LoadInfrastructure()
        mklines := t.NewMkLines("determine-defined-variables.mk",
                MkRcsID,
                "",
@@ -330,6 +328,7 @@ func (s *Suite) Test_MkLines_collectDefi
                "pre-configure:",
                "\t${RUN} autoreconf; autoheader-2.13",
                "\t${ECHO} ${OSV:Q}")
+       t.FinishSetUp()
 
        mklines.Check()
 
@@ -357,7 +356,7 @@ func (s *Suite) Test_MkLines_collectDefi
                "",
                ".if ${H_XFT2:N__nonexistent__} && ${H_UNDEF:N__nonexistent__}",
                ".endif")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        mklines.Check()
 
@@ -925,7 +924,6 @@ func (s *Suite) Test_MkLines_Check__shel
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: x11/lablgtk1/Makefile:2: Please use ${CC:Q} instead of ${CC}.",
                "WARN: x11/lablgtk1/Makefile:2: Please use ${CC:Q} instead of ${CC}.")
 }
 

Index: pkgsrc/pkgtools/pkglint/files/mklines_varalign_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines_varalign_test.go:1.9 pkgsrc/pkgtools/pkglint/files/mklines_varalign_test.go:1.10
--- pkgsrc/pkgtools/pkglint/files/mklines_varalign_test.go:1.9  Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines_varalign_test.go      Sat Apr 20 17:43:24 2019
@@ -1012,20 +1012,38 @@ func (s *Suite) Test_Varalign__realign_c
        vt.Run()
 }
 
-// FIXME: The diagnostic does not correspond to the autofix; see "if oldWidth == 8".
+// The VAR2 line is a continuation line that starts in column 9, just like
+// the VAR1 line. Therefore the alignment is correct.
+//
+// Its continuation line is indented using effectively tab-tab-space, and
+// this relative indentation compared to the VAR2 line is preserved since
+// it is often used for indenting AWK or shell programs.
 func (s *Suite) Test_Varalign__mixed_indentation(c *check.C) {
        vt := NewVaralignTester(s, c)
        vt.Input(
                "VAR1=\tvalue1",
                "VAR2=\tvalue2 \\",
                " \t \t value2 continued")
-       vt.Diagnostics(
-       /*"NOTE: ~/Makefile:2--3: This line should be aligned with \"\\t\"."*/ )
-       vt.Autofixes(
-       /*"AUTOFIX: ~/Makefile:3: Replacing indentation \" \\t \\t \" with \"\\t\\t \"."*/ )
+       vt.Diagnostics()
+       vt.Autofixes()
        vt.Fixed(
                "VAR1=   value1",
                "VAR2=   value2 \\",
                "                 value2 continued")
        vt.Run()
 }
+
+func (s *Suite) Test_Varalign__eol_comment(c *check.C) {
+       vt := NewVaralignTester(s, c)
+       vt.Input(
+               "VAR1=\tdefined",
+               "VAR2=\t# defined",
+               "VAR3=\t#empty")
+       vt.Diagnostics()
+       vt.Autofixes()
+       vt.Fixed(
+               "VAR1=   defined",
+               "VAR2=   # defined",
+               "VAR3=   #empty")
+       vt.Run()
+}
Index: pkgsrc/pkgtools/pkglint/files/mktypes_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.9 pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.10
--- pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.9   Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mktypes_test.go       Sat Apr 20 17:43:24 2019
@@ -13,9 +13,17 @@ func NewMkVarUse(varname string, modifie
 }
 
 func (s *Suite) Test_MkVarUse_Mod(c *check.C) {
-       varuse := NewMkVarUse("varname", "Q")
+       t := s.Init(c)
+
+       test := func(varUseText string, mod string) {
+               line := t.NewLine("filename.mk", 123, "")
+               varUse := NewMkParser(line, varUseText, true).VarUse()
+               t.CheckOutputEmpty()
+               c.Check(varUse.Mod(), equals, mod)
+       }
 
-       c.Check(varuse.Mod(), equals, ":Q")
+       test("${varname:Q}", ":Q")
+       test("${PATH:ts::Q}", ":ts::Q")
 }
 
 // AddCommand adds a command directly to a list of commands,
@@ -92,6 +100,24 @@ func (s *Suite) Test_MkVarUseModifier_Ma
        c.Check(options, equals, "")
 }
 
+// Some pkgsrc users really explore the darkest corners of bmake by using
+// the backslash as the separator in the :S modifier. Sure, it works, it
+// just looks totally unexpected to the average pkgsrc reader.
+//
+// Using the backslash as separator means that it cannot be used for anything
+// else, not even for escaping other characters.
+func (s *Suite) Test_MkVarUseModifier_MatchSubst__backslash_as_separator(c *check.C) {
+       mod := MkVarUseModifier{"S\\.post1\\\\1"}
+
+       ok, regex, from, to, options := mod.MatchSubst()
+
+       c.Check(ok, equals, true)
+       c.Check(regex, equals, false)
+       c.Check(from, equals, ".post1")
+       c.Check(to, equals, "")
+       c.Check(options, equals, "1")
+}
+
 // As of 2019-03-24, pkglint doesn't know how to handle complicated
 // :C modifiers.
 func (s *Suite) Test_MkVarUseModifier_Subst__regexp(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/mkparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser.go:1.26 pkgsrc/pkgtools/pkglint/files/mkparser.go:1.27
--- pkgsrc/pkgtools/pkglint/files/mkparser.go:1.26      Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mkparser.go   Sat Apr 20 17:43:24 2019
@@ -66,109 +66,115 @@ func (p *MkParser) MkTokens() []*MkToken
 }
 
 func (p *MkParser) VarUse() *MkVarUse {
-       lexer := p.lexer
+       rest := p.lexer.Rest()
+       if len(rest) < 2 || rest[0] != '$' {
+               return nil
+       }
+
+       switch rest[1] {
+       case '{', '(':
+               return p.varUseBrace(rest[1] == '(')
 
-       if lexer.PeekByte() != '$' {
+       case '$':
+               // This is an escaped dollar character and not a variable use.
                return nil
+
+       case '@', '<', ' ':
+               // These variable names are known to exist.
+               //
+               // Many others are also possible but not used in practice.
+               // In particular, when parsing the :C or :S modifier,
+               // the $ must not be interpreted as a variable name,
+               // even when it looks like $/ could refer to the "/" variable.
+               //
+               // TODO: Find out whether $" is a variable use when it appears in the :M modifier.
+               p.lexer.Skip(2)
+               return &MkVarUse{rest[1:2], nil}
+
+       default:
+               return p.varUseAlnum()
        }
+}
 
-       mark := lexer.Mark()
-       lexer.Skip(1)
+// varUseBrace parses:
+//  ${VAR}
+//  ${arbitrary text:L}
+//  ${variable with invalid chars}
+//  $(PARENTHESES)
+//  ${VAR:Mpattern:C,:,colon,g:Q:Q:Q}
+func (p *MkParser) varUseBrace(usingRoundParen bool) *MkVarUse {
+       lexer := p.lexer
 
-       if lexer.SkipByte('{') || lexer.SkipByte('(') {
-               usingRoundParen := lexer.Since(mark)[1] == '('
+       beforeDollar := lexer.Mark()
+       lexer.Skip(2)
 
-               closing := byte('}')
-               if usingRoundParen {
-                       closing = ')'
-               }
+       closing := byte('}')
+       if usingRoundParen {
+               closing = ')'
+       }
 
-               varnameMark := lexer.Mark()
-               varname := p.Varname()
+       beforeVarname := lexer.Mark()
+       varname := p.Varname()
+       p.varUseText(closing)
+       varExpr := lexer.Since(beforeVarname)
 
-               modifiers := p.VarUseModifiers(varname, closing)
-               if lexer.SkipByte(closing) {
-                       if usingRoundParen && p.EmitWarnings {
-                               parenVaruse := lexer.Since(mark)
-                               edit := []byte(parenVaruse)
-                               edit[1] = '{'
-                               edit[len(edit)-1] = '}'
-                               bracesVaruse := string(edit)
+       modifiers := p.VarUseModifiers(varExpr, closing)
 
-                               fix := p.Line.Autofix()
-                               fix.Warnf("Please use curly braces {} instead of round parentheses () for %s.", varname)
-                               fix.Replace(parenVaruse, bracesVaruse)
-                               fix.Apply()
-                       }
+       closed := lexer.SkipByte(closing)
 
-                       return &MkVarUse{varname, modifiers}
+       if p.EmitWarnings {
+               if !closed {
+                       p.Line.Warnf("Missing closing %q for %q.", string(rune(closing)), varExpr)
                }
 
-               // This code path parses ${arbitrary text :L} and ${expression :? true-branch : false-branch }.
-               // The text in front of the :L or :? modifier doesn't have to be a variable name.
+               if usingRoundParen && closed {
+                       parenVaruse := lexer.Since(beforeDollar)
+                       edit := []byte(parenVaruse)
+                       edit[1] = '{'
+                       edit[len(edit)-1] = '}'
+                       bracesVaruse := string(edit)
 
-               re := G.res.Compile(regex.Pattern(ifelseStr(usingRoundParen, `^(?:[^$:)]|\$\$)+`, `^(?:[^$:}]|\$\$)+`)))
-               for p.VarUse() != nil || lexer.SkipRegexp(re) {
+                       fix := p.Line.Autofix()
+                       fix.Warnf("Please use curly braces {} instead of round parentheses () for %s.", varExpr)
+                       fix.Replace(parenVaruse, bracesVaruse)
+                       fix.Apply()
                }
 
-               rest := p.Rest()
-               if hasPrefix(rest, ":L") || hasPrefix(rest, ":?") {
-                       varexpr := lexer.Since(varnameMark)
-                       modifiers := p.VarUseModifiers(varexpr, closing)
-                       if lexer.SkipByte(closing) {
-                               return &MkVarUse{varexpr, modifiers}
-                       }
+               if len(varExpr) > len(varname) && !(&MkVarUse{varExpr, modifiers}).IsExpression() {
+                       p.Line.Warnf("Invalid part %q after variable name %q.", varExpr[len(varname):], varname)
                }
+       }
+
+       return &MkVarUse{varExpr, modifiers}
+}
+
+func (p *MkParser) varUseAlnum() *MkVarUse {
+       lexer := p.lexer
 
-               lexer.Reset(mark)
+       apparentVarname := textproc.NewLexer(lexer.Rest()[1:]).NextBytesSet(textproc.AlnumU)
+       if apparentVarname == "" {
                return nil
        }
 
-       varname := lexer.NextByteSet(textproc.AlnumU)
-       if varname != -1 {
+       lexer.Skip(2)
 
-               if p.EmitWarnings {
-                       varnameRest := lexer.Copy().NextBytesSet(textproc.AlnumU)
-                       if varnameRest != "" {
-                               p.Line.Errorf("$%[1]s is ambiguous. Use ${%[1]s} if you mean a Make variable or $$%[1]s if you mean a shell variable.",
-                                       sprintf("%c%s", varname, varnameRest))
-                               p.Line.Explain(
-                                       "Only the first letter after the dollar is the variable name.",
-                                       "Everything following it is normal text, even if it looks like a variable name to human readers.")
-                       } else {
-                               p.Line.Warnf("$%[1]c is ambiguous. Use ${%[1]c} if you mean a Make variable or $$%[1]c if you mean a shell variable.", varname)
-                               p.Line.Explain(
-                                       "In its current form, this variable is parsed as a Make variable.",
-                                       "For human readers though, $x looks more like a shell variable than a Make variable,",
-                                       "since Make variables are usually written using braces (BSD-style) or parentheses (GNU-style).")
-                       }
-               }
-
-               return &MkVarUse{sprintf("%c", varname), nil}
-       }
-
-       if !lexer.EOF() {
-               symbol := lexer.Rest()[:1]
-               switch symbol {
-               case "$":
-                       // This is an escaped dollar character and not a variable use.
-
-               case "@", "<", " ":
-                       // These variable names are known to exist.
-                       //
-                       // Many others are also possible but not used in practice.
-                       // In particular, when parsing the :C or :S modifier,
-                       // the $ must not be interpreted as a variable name,
-                       // even when it looks like $/ could refer to the "/" variable.
-                       //
-                       // TODO: Find out whether $" is a variable use when it appears in the :M modifier.
-                       lexer.Skip(1)
-                       return &MkVarUse{symbol, nil}
+       if p.EmitWarnings {
+               if len(apparentVarname) > 1 {
+                       p.Line.Errorf("$%[1]s is ambiguous. Use ${%[1]s} if you mean a Make variable or $$%[1]s if you mean a shell variable.",
+                               apparentVarname)
+                       p.Line.Explain(
+                               "Only the first letter after the dollar is the variable name.",
+                               "Everything following it is normal text, even if it looks like a variable name to human readers.")
+               } else {
+                       p.Line.Warnf("$%[1]s is ambiguous. Use ${%[1]s} if you mean a Make variable or $$%[1]s if you mean a shell variable.", apparentVarname)
+                       p.Line.Explain(
+                               "In its current form, this variable is parsed as a Make variable.",
+                               "For human readers though, $x looks more like a shell variable than a Make variable,",
+                               "since Make variables are usually written using braces (BSD-style) or parentheses (GNU-style).")
                }
        }
 
-       lexer.Reset(mark)
-       return nil
+       return &MkVarUse{apparentVarname[:1], nil}
 }
 
 // VarUseModifiers parses the modifiers of a variable being used, such as :Q, :Mpattern.
@@ -177,6 +183,8 @@ func (p *MkParser) VarUse() *MkVarUse {
 func (p *MkParser) VarUseModifiers(varname string, closing byte) []MkVarUseModifier {
        lexer := p.lexer
 
+       // TODO: Split into VarUseModifier for parsing a single modifier.
+
        var modifiers []MkVarUseModifier
        appendModifier := func(s string) { modifiers = append(modifiers, MkVarUseModifier{s}) }
 
@@ -213,16 +221,18 @@ func (p *MkParser) VarUseModifiers(varna
 
                        case "ts":
                                // See devel/bmake/files/var.c:/case 't'
-                               rest := lexer.Rest()
+                               sep := p.varUseText(closing)
                                switch {
-                               case len(rest) >= 2 && (rest[1] == closing || rest[1] == ':'):
-                                       lexer.Skip(1)
-                               case len(rest) >= 1 && (rest[0] == closing || rest[0] == ':'):
+                               case sep == "":
+                                       lexer.SkipString(":")
+                               case len(sep) == 1:
                                        break
-                               case lexer.SkipRegexp(G.res.Compile(`^\\\d+`)):
+                               case matches(sep, `^\\\d+`):
                                        break
                                default:
-                                       continue
+                                       if p.EmitWarnings {
+                                               p.Line.Warnf("Invalid separator %q for :ts modifier of %q.", sep, varname)
+                                       }
                                }
                                appendModifier(lexer.Since(modifierMark))
                                continue
@@ -238,14 +248,14 @@ func (p *MkParser) VarUseModifiers(varna
                        continue
 
                case 'C', 'S':
-                       if p.varUseModifierSubst(lexer, closing) {
+                       if ok, _, _, _, _ := p.varUseModifierSubst(closing); ok {
                                appendModifier(lexer.Since(modifierMark))
                                mayOmitColon = true
                                continue
                        }
 
                case '@':
-                       if p.varUseModifierAt(lexer, closing, varname) {
+                       if p.varUseModifierAt(lexer, varname) {
                                appendModifier(lexer.Since(modifierMark))
                                continue
                        }
@@ -258,12 +268,9 @@ func (p *MkParser) VarUseModifiers(varna
 
                case '?':
                        lexer.Skip(1)
-                       re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^$:}]|\$\$)+`, `^([^$:)]|\$\$)+`)))
-                       for p.VarUse() != nil || lexer.SkipRegexp(re) {
-                       }
+                       p.varUseText(closing)
                        if lexer.SkipByte(':') {
-                               for p.VarUse() != nil || lexer.SkipRegexp(re) {
-                               }
+                               p.varUseText(closing)
                                appendModifier(lexer.Since(modifierMark))
                                continue
                        }
@@ -290,52 +297,76 @@ func (p *MkParser) VarUseModifiers(varna
        return modifiers
 }
 
+// varUseText parses any text up to the next colon or closing mark.
+// Nested variable uses are parsed as well.
+//
+// This is used for the :L and :? modifiers since they accept arbitrary
+// text as the "variable name" and effectively interpret it as the variable
+// value instead.
+func (p *MkParser) varUseText(closing byte) string {
+       lexer := p.lexer
+       start := lexer.Mark()
+       re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^$:}]|\$\$)+`, `^([^$:)]|\$\$)+`)))
+       for p.VarUse() != nil || lexer.SkipRegexp(re) {
+       }
+       return lexer.Since(start)
+}
+
 // varUseModifierSubst parses a :S,from,to, or a :C,from,to, modifier.
-func (p *MkParser) varUseModifierSubst(lexer *textproc.Lexer, closing byte) bool {
+func (p *MkParser) varUseModifierSubst(closing byte) (ok bool, regex bool, from string, to string, options string) {
+       lexer := p.lexer
+       regex = lexer.PeekByte() == 'C'
        lexer.Skip(1 /* the initial S or C */)
 
        sep := lexer.PeekByte() // bmake allows _any_ separator, even letters.
        if sep == -1 || byte(sep) == closing {
-               return false
+               return
        }
 
        lexer.Skip(1)
        separator := byte(sep)
 
        isOther := func(b byte) bool {
-               return b != separator && b != '$' && b != closing && b != '\\'
+               return b != separator && b != '$' && b != '\\'
        }
 
        skipOther := func() {
                for p.VarUse() != nil ||
                        lexer.SkipString("$$") ||
-                       (len(lexer.Rest()) >= 2 && lexer.PeekByte() == '\\' && lexer.Skip(2)) ||
+                       (len(lexer.Rest()) >= 2 && lexer.PeekByte() == '\\' && separator != '\\' && lexer.Skip(2)) ||
                        lexer.NextBytesFunc(isOther) != "" {
                }
        }
 
+       fromStart := lexer.Mark()
        lexer.SkipByte('^')
        skipOther()
        lexer.SkipByte('$')
+       from = lexer.Since(fromStart)
 
        if !lexer.SkipByte(separator) {
-               return false
+               return
        }
 
+       toStart := lexer.Mark()
        skipOther()
+       to = lexer.Since(toStart)
 
        if !lexer.SkipByte(separator) {
-               return false
+               return
        }
 
+       optionsStart := lexer.Mark()
        lexer.NextBytesFunc(func(b byte) bool { return b == '1' || b == 'g' || b == 'W' })
+       options = lexer.Since(optionsStart)
 
-       return true
+       ok = true
+       return
 }
 
 // varUseModifierAt parses a variable modifier like ":@v@echo ${v};@",
 // which expands the variable value in a loop.
-func (p *MkParser) varUseModifierAt(lexer *textproc.Lexer, closing byte, varname string) bool {
+func (p *MkParser) varUseModifierAt(lexer *textproc.Lexer, varname string) bool {
        lexer.Skip(1 /* the initial @ */)
 
        loopVar := lexer.NextBytesSet(AlnumDot)
@@ -343,7 +374,7 @@ func (p *MkParser) varUseModifierAt(lexe
                return false
        }
 
-       re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^$:@}\\]|\\.)+`, `^([^$:@)\\]|\\.)+`)))
+       re := G.res.Compile(`^([^$@\\]|\\.)+`)
        for p.VarUse() != nil || lexer.SkipString("$$") || lexer.SkipRegexp(re) {
        }
 
@@ -448,16 +479,19 @@ func (p *MkParser) mkCondAtom() MkCond {
                }
 
                if lhs != nil {
-                       if m := lexer.NextRegexp(G.res.Compile(`^[\t ]*(<|<=|==|!=|>=|>)[\t ]*(0x[0-9A-Fa-f]+|\d+(?:\.\d+)?)`)); m != nil {
+                       lexer.SkipHspace()
+
+                       if m := lexer.NextRegexp(G.res.Compile(`^(<|<=|==|!=|>=|>)[\t ]*(0x[0-9A-Fa-f]+|\d+(?:\.\d+)?)`)); m != nil {
                                return &mkCond{CompareVarNum: &MkCondCompareVarNum{lhs, m[1], m[2]}}
                        }
 
-                       m := lexer.NextRegexp(G.res.Compile(`^[\t ]*(<|<=|==|!=|>=|>)[\t ]*`))
+                       m := lexer.NextRegexp(G.res.Compile(`^(?:<|<=|==|!=|>=|>)`))
                        if m == nil {
                                return &mkCond{Var: lhs} // See devel/bmake/files/cond.c:/\* For \.if \$/
                        }
+                       lexer.SkipHspace()
 
-                       op := m[1]
+                       op := m[0]
                        if op == "==" || op == "!=" {
                                if mrhs := lexer.NextRegexp(G.res.Compile(`^"([^"\$\\]*)"`)); mrhs != nil {
                                        return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, mrhs[1]}}
Index: pkgsrc/pkgtools/pkglint/files/mkparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.26 pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.27
--- pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.26 Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mkparser_test.go      Sat Apr 20 17:43:24 2019
@@ -86,7 +86,7 @@ func (s *Suite) Test_MkParser_MkTokens(c
 func (s *Suite) Test_MkParser_VarUse(c *check.C) {
        t := s.Init(c)
 
-       testRest := func(input string, expectedTokens []*MkToken, expectedRest string) {
+       testRest := func(input string, expectedTokens []*MkToken, expectedRest string, diagnostics ...string) {
                line := t.NewLines("Test_MkParser_VarUse.mk", input).Lines[0]
                p := NewMkParser(line, input, true)
                actualTokens := p.MkTokens()
@@ -98,9 +98,11 @@ func (s *Suite) Test_MkParser_VarUse(c *
                        }
                }
                c.Check(p.Rest(), equals, expectedRest)
+               t.CheckOutput(diagnostics)
        }
-       test := func(input string, expectedToken *MkToken) {
-               testRest(input, []*MkToken{expectedToken}, "")
+       tokens := func(tokens ...*MkToken) []*MkToken { return tokens }
+       test := func(input string, expectedToken *MkToken, diagnostics ...string) {
+               testRest(input, []*MkToken{expectedToken}, "", diagnostics...)
        }
        varuse := func(varname string, modifiers ...string) *MkToken {
                text := "${" + varname
@@ -114,6 +116,8 @@ func (s *Suite) Test_MkParser_VarUse(c *
                return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
        }
 
+       t.Use(testRest, tokens, test, varuse, varuseText)
+
        test("${VARIABLE}",
                varuse("VARIABLE"))
 
@@ -304,44 +308,63 @@ func (s *Suite) Test_MkParser_VarUse(c *
        test("${VAR:Sahara}",
                varuse("VAR", "Sahara"))
 
+       // The separator character can be left out, which means empty.
        test("${VAR:ts}",
-               varuse("VAR", "ts")) // The separator character can be left out, which means empty.
+               varuse("VAR", "ts"))
 
+       // The separator character can be a long octal number.
        test("${VAR:ts\\000012}",
-               varuse("VAR", "ts\\000012")) // The separator character can be a long octal number.
+               varuse("VAR", "ts\\000012"))
 
+       // Or even decimal.
        test("${VAR:ts\\124}",
-               varuse("VAR", "ts\\124")) // Or even decimal.
+               varuse("VAR", "ts\\124"))
 
-       testRest("${VAR:ts---}", nil, "${VAR:ts---}") // The :ts modifier only takes single-character separators.
+       // The :ts modifier only takes single-character separators.
+       test("${VAR:ts---}",
+               varuse("VAR", "ts---"),
+               "WARN: Test_MkParser_VarUse.mk:1: Invalid separator \"---\" for :ts modifier of \"VAR\".")
 
        test("$<",
                varuseText("$<", "<")) // Same as ${.IMPSRC}
 
        test("$(GNUSTEP_USER_ROOT)",
-               varuseText("$(GNUSTEP_USER_ROOT)", "GNUSTEP_USER_ROOT"))
-
-       t.CheckOutputLines(
+               varuseText("$(GNUSTEP_USER_ROOT)", "GNUSTEP_USER_ROOT"),
                "WARN: Test_MkParser_VarUse.mk:1: Please use curly braces {} instead of round parentheses () for GNUSTEP_USER_ROOT.")
 
-       testRest("${VAR)", nil, "${VAR)") // Opening brace, closing parenthesis
-       testRest("$(VAR}", nil, "$(VAR}") // Opening parenthesis, closing brace
-       t.CheckOutputEmpty()              // Warnings are only printed for balanced expressions.
+       // Opening brace, closing parenthesis.
+       // Warnings are only printed for balanced expressions.
+       test("${VAR)",
+               varuseText("${VAR)", "VAR)"),
+               "WARN: Test_MkParser_VarUse.mk:1: Missing closing \"}\" for \"VAR)\".",
+               "WARN: Test_MkParser_VarUse.mk:1: Invalid part \")\" after variable name \"VAR\".")
+
+       // Opening parenthesis, closing brace
+       // Warnings are only printed for balanced expressions.
+       test("$(VAR}",
+               varuseText("$(VAR}", "VAR}"),
+               "WARN: Test_MkParser_VarUse.mk:1: Missing closing \")\" for \"VAR}\".",
+               "WARN: Test_MkParser_VarUse.mk:1: Invalid part \"}\" after variable name \"VAR\".")
 
        test("${PLIST_SUBST_VARS:@var@${var}=${${var}:Q}@}",
                varuse("PLIST_SUBST_VARS", "@var@${var}=${${var}:Q}@"))
 
        test("${PLIST_SUBST_VARS:@var@${var}=${${var}:Q}}",
-               varuse("PLIST_SUBST_VARS", "@var@${var}=${${var}:Q}")) // Missing @ at the end
-
-       t.CheckOutputLines(
-               "WARN: Test_MkParser_VarUse.mk:1: Modifier ${PLIST_SUBST_VARS:@var@...@} is missing the final \"@\".")
+               varuseText("${PLIST_SUBST_VARS:@var@${var}=${${var}:Q}}",
+                       "PLIST_SUBST_VARS", "@var@${var}=${${var}:Q}}"),
+               "WARN: Test_MkParser_VarUse.mk:1: Modifier ${PLIST_SUBST_VARS:@var@...@} is missing the final \"@\".",
+               "WARN: Test_MkParser_VarUse.mk:1: Missing closing \"}\" for \"PLIST_SUBST_VARS\".")
 
        // Unfinished variable use
-       testRest("${", nil, "${")
+       test("${",
+               varuseText("${", ""),
+               "WARN: Test_MkParser_VarUse.mk:1: Missing closing \"}\" for \"\".")
 
        // Unfinished nested variable use
-       testRest("${${", nil, "${${")
+       test("${${",
+               varuseText("${${", "${"),
+               "WARN: Test_MkParser_VarUse.mk:1: Missing closing \"}\" for \"\".",
+               "WARN: Test_MkParser_VarUse.mk:1: Missing closing \"}\" for \"${\".")
 }
 
 func (s *Suite) Test_MkParser_VarUse__ambiguous(c *check.C) {
@@ -349,8 +372,8 @@ func (s *Suite) Test_MkParser_VarUse__am
 
        t.SetUpCommandLine("--explain")
 
-       mkline := t.NewMkLine("module.mk", 123, "\t$Varname $X")
-       p := NewMkParser(mkline.Line, mkline.ShellCommand(), true)
+       line := t.NewLine("module.mk", 123, "\t$Varname $X")
+       p := NewMkParser(line, line.Text[1:], true)
 
        tokens := p.MkTokens()
        c.Check(tokens, deepEquals, []*MkToken{
@@ -375,6 +398,8 @@ func (s *Suite) Test_MkParser_VarUse__am
 }
 
 func (s *Suite) Test_MkParser_MkCond(c *check.C) {
+       t := s.Init(c)
+
        testRest := func(input string, expectedTree MkCond, expectedRest string) {
                p := NewMkParser(nil, input, false)
                actualTree := p.MkCond()
@@ -386,6 +411,8 @@ func (s *Suite) Test_MkParser_MkCond(c *
        }
        varuse := NewMkVarUse
 
+       t.Use(testRest, test, varuse)
+
        test("${OPSYS:MNetBSD}",
                &mkCond{Var: varuse("OPSYS", "MNetBSD")})
 
@@ -515,7 +542,15 @@ func (s *Suite) Test_MkParser_MkCond(c *
                nil,
                "\"unfinished string literal")
 
-       // Not even the ${VAR} gets through here, although that can be expected. FIXME: Why?
+       // Parsing stops before the variable since the comparison between
+       // a variable and a string is one of the smallest building blocks.
+       // Letting the ${VAR} through and stopping at the == operator would
+       // be misleading.
+       //
+       // Another possibility would be to fix the unfinished string literal
+       // and continue parsing. As of April 2019, the error handling is not
+       // robust enough to support this approach; magically fixing parse
+       // errors might lead to wrong conclusions and warnings.
        testRest("${VAR} == \"unfinished string literal",
                nil,
                "${VAR} == \"unfinished string literal")
@@ -551,14 +586,14 @@ func (s *Suite) Test_MkParser_VarUseModi
        t := s.Init(c)
 
        varUse := NewMkVarUse
-       test := func(text string, varUse *MkVarUse, rest string, diagnostics ...string) {
+       test := func(text string, varUse *MkVarUse, diagnostics ...string) {
                mkline := t.NewMkLine("Makefile", 20, "\t"+text)
                p := NewMkParser(mkline.Line, mkline.ShellCommand(), true)
 
                actual := p.VarUse()
 
                t.Check(actual, deepEquals, varUse)
-               t.Check(p.Rest(), equals, rest)
+               t.Check(p.Rest(), equals, "")
                t.CheckOutput(diagnostics)
        }
 
@@ -566,21 +601,27 @@ func (s *Suite) Test_MkParser_VarUseModi
        // check whether the command is actually valid.
        // At least not while parsing the modifier since at this point it might
        // be still unknown which of the commands can be used and which cannot.
-       test("${VAR:!command!}", varUse("VAR", "!command!"), "")
+       test("${VAR:!command!}", varUse("VAR", "!command!"))
 
-       test("${VAR:!command}", varUse("VAR"), "",
+       test("${VAR:!command}", varUse("VAR"),
+               // FIXME: duplicate diagnostic
+               "WARN: Makefile:20: Invalid variable modifier \"!command\" for \"VAR\".",
                "WARN: Makefile:20: Invalid variable modifier \"!command\" for \"VAR\".")
 
-       test("${VAR:command!}", varUse("VAR"), "",
+       test("${VAR:command!}", varUse("VAR"),
+               // FIXME: duplicate diagnostic
+               "WARN: Makefile:20: Invalid variable modifier \"command!\" for \"VAR\".",
                "WARN: Makefile:20: Invalid variable modifier \"command!\" for \"VAR\".")
 
        // The :L modifier makes the variable value "echo hello", and the :[1]
        // modifier extracts the "echo".
-       test("${echo hello:L:[1]}", varUse("echo hello", "L", "[1]"), "")
+       test("${echo hello:L:[1]}", varUse("echo hello", "L", "[1]"))
 
        // bmake ignores the :[3] modifier, and the :L modifier just returns the
        // variable name, in this case BUILD_DIRS.
-       test("${BUILD_DIRS:[3]:L}", varUse("BUILD_DIRS", "[3]", "L"), "")
+       test("${BUILD_DIRS:[3]:L}", varUse("BUILD_DIRS", "[3]", "L"))
+
+       test("${PATH:ts::Q}", varUse("PATH", "ts:", "Q"))
 }
 
 func (s *Suite) Test_MkParser_varUseModifierSubst(c *check.C) {
@@ -588,8 +629,8 @@ func (s *Suite) Test_MkParser_varUseModi
 
        varUse := NewMkVarUse
        test := func(text string, varUse *MkVarUse, rest string, diagnostics ...string) {
-               mkline := t.NewMkLine("Makefile", 20, "\t"+text)
-               p := NewMkParser(mkline.Line, mkline.ShellCommand(), true)
+               line := t.NewLine("Makefile", 20, "\t"+text)
+               p := NewMkParser(line, text, true)
 
                actual := p.VarUse()
 
@@ -598,8 +639,9 @@ func (s *Suite) Test_MkParser_varUseModi
                t.CheckOutput(diagnostics)
        }
 
-       test("${VAR:S", nil, "${VAR:S",
-               "WARN: Makefile:20: Invalid variable modifier \"S\" for \"VAR\".")
+       test("${VAR:S", varUse("VAR"), "",
+               "WARN: Makefile:20: Invalid variable modifier \"S\" for \"VAR\".",
+               "WARN: Makefile:20: Missing closing \"}\" for \"VAR\".")
 
        test("${VAR:S}", varUse("VAR"), "",
                "WARN: Makefile:20: Invalid variable modifier \"S\" for \"VAR\".")
@@ -621,6 +663,23 @@ func (s *Suite) Test_MkParser_varUseModi
        test("${VAR:S,from,to,W}", varUse("VAR", "S,from,to,W"), "")
 
        test("${VAR:S,from,to,1gW}", varUse("VAR", "S,from,to,1gW"), "")
+
+       // Inside the :S or :C modifiers, neither a colon nor the closing
+       // brace need to be escaped. Otherwise these patterns would become
+       // too difficult to read and write.
+       test("${VAR:C/[[:alnum:]]{2}/**/g}",
+               varUse("VAR", "C/[[:alnum:]]{2}/**/g"),
+               "")
+
+       // Some pkgsrc users really explore the darkest corners of bmake by using
+       // the backslash as the separator in the :S modifier. Sure, it works, it
+       // just looks totally unexpected to the average pkgsrc reader.
+       //
+       // Using the backslash as separator means that it cannot be used for anything
+       // else, not even for escaping other characters.
+       test("${VAR:S\\.post1\\\\1}",
+               varUse("VAR", "S\\.post1\\\\1"),
+               "")
 }
 
 func (s *Suite) Test_MkParser_varUseModifierAt(c *check.C) {
@@ -628,8 +687,8 @@ func (s *Suite) Test_MkParser_varUseModi
 
        varUse := NewMkVarUse
        test := func(text string, varUse *MkVarUse, rest string, diagnostics ...string) {
-               mkline := t.NewMkLine("Makefile", 20, "\t"+text)
-               p := NewMkParser(mkline.Line, mkline.ShellCommand(), true)
+               line := t.NewLine("Makefile", 20, "\t"+text)
+               p := NewMkParser(line, text, true)
 
                actual := p.VarUse()
 
@@ -638,13 +697,21 @@ func (s *Suite) Test_MkParser_varUseModi
                t.CheckOutput(diagnostics)
        }
 
-       test("${VAR:@", nil, "${VAR:@",
-               "WARN: Makefile:20: Invalid variable modifier \"@\" for \"VAR\".")
+       test("${VAR:@",
+               varUse("VAR"),
+               "",
+               "WARN: Makefile:20: Invalid variable modifier \"@\" for \"VAR\".",
+               "WARN: Makefile:20: Missing closing \"}\" for \"VAR\".")
 
-       test("${VAR:@i@${i}}", varUse("VAR", "@i@${i}"), "",
-               "WARN: Makefile:20: Modifier ${VAR:@i@...@} is missing the final \"@\".")
+       test("${VAR:@i@${i}}", varUse("VAR", "@i@${i}}"), "",
+               "WARN: Makefile:20: Modifier ${VAR:@i@...@} is missing the final \"@\".",
+               "WARN: Makefile:20: Missing closing \"}\" for \"VAR\".")
 
        test("${VAR:@i@${i}@}", varUse("VAR", "@i@${i}@"), "")
+
+       test("${PKG_GROUPS:@g@${g:Q}:${PKG_GID.${g}:Q}@:C/:*$//g}",
+               varUse("PKG_GROUPS", "@g@${g:Q}:${PKG_GID.${g}:Q}@", "C/:*$//g"),
+               "")
 }
 
 func (s *Suite) Test_MkParser_PkgbasePattern(c *check.C) {
Index: pkgsrc/pkgtools/pkglint/files/util_test.go
diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.26 pkgsrc/pkgtools/pkglint/files/util_test.go:1.27
--- pkgsrc/pkgtools/pkglint/files/util_test.go:1.26     Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/util_test.go  Sat Apr 20 17:43:25 2019
@@ -302,6 +302,15 @@ func emptyToNil(slice []string) []string
        return slice
 }
 
+func (s *Suite) Test_trimHspace(c *check.C) {
+       t := s.Init(c)
+
+       t.Check(trimHspace("a b"), equals, "a b")
+       t.Check(trimHspace(" a b "), equals, "a b")
+       t.Check(trimHspace("\ta b\t"), equals, "a b")
+       t.Check(trimHspace(" \t a b\t \t"), equals, "a b")
+}
+
 func (s *Suite) Test_isLocallyModified(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/mkshwalker.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.7 pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.8
--- pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.7     Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mkshwalker.go Sat Apr 20 17:43:24 2019
@@ -72,6 +72,9 @@ func (w *MkShWalker) Path() string {
        var path []string
        for _, level := range w.Context {
                typeName := reflect.TypeOf(level.Element).Elem().Name()
+               if typeName == "" && reflect.TypeOf(level.Element).Kind() == reflect.Slice {
+                       typeName = "[]" + reflect.TypeOf(level.Element).Elem().Elem().Name()
+               }
                abbreviated := strings.TrimPrefix(typeName, "MkSh")
                if level.Index == -1 {
                        // TODO: This form should also be used if index == 0 and len == 1.
@@ -146,10 +149,10 @@ func (w *MkShWalker) walkCommand(index i
                w.walkSimpleCommand(-1, command.Simple)
        case command.Compound != nil:
                w.walkCompoundCommand(-1, command.Compound)
-               w.walkRedirects(-1, command.Redirects)
+               w.walkRedirects(command.Redirects)
        case command.FuncDef != nil:
                w.walkFunctionDefinition(-1, command.FuncDef)
-               w.walkRedirects(-1, command.Redirects)
+               w.walkRedirects(command.Redirects)
        }
 
        w.pop()
@@ -167,7 +170,7 @@ func (w *MkShWalker) walkSimpleCommand(i
                w.walkWord(-1, command.Name)
        }
        w.walkWords(1, command.Args)
-       w.walkRedirects(-1, command.Redirections)
+       w.walkRedirects(command.Redirections)
 
        w.pop()
 }
@@ -290,26 +293,25 @@ func (w *MkShWalker) walkWord(index int,
        w.pop()
 }
 
-func (w *MkShWalker) walkRedirects(index int, redirects []*MkShRedirection) {
+func (w *MkShWalker) walkRedirects(redirects []*MkShRedirection) {
        if len(redirects) == 0 {
                return
        }
 
-       w.push(index, redirects)
+       w.push(-1, redirects)
 
        if callback := w.Callback.Redirects; callback != nil {
                callback(redirects)
        }
 
        for i, redirect := range redirects {
-               // FIXME: The w.push/w.pop is missing here.
-               //  How does the path look like?
-               //  Are there ambiguities?
+               w.push(i, redirect)
                if callback := w.Callback.Redirect; callback != nil {
                        callback(redirect)
                }
 
                w.walkWord(i, redirect.Target)
+               w.pop()
        }
 
        w.pop()
Index: pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.7 pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.7        Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go    Sat Apr 20 17:43:24 2019
@@ -3,27 +3,39 @@ package pkglint
 import "gopkg.in/check.v1"
 
 func (s *Suite) Test_MkShWalker_Walk(c *check.C) {
-       list, err := parseShellProgram(dummyLine, ""+
-               "if condition; then action; else case selector in pattern) case-item-action ;; esac; fi; "+
-               "set -e; "+
-               "cd ${WRKSRC}/locale; "+
-               "for lang in *.po; do "+
-               "  [ \"$${lang}\" = \"wxstd.po\" ] && continue; "+
-               "  ${TOOLS_PATH.msgfmt} -c -o \"$${lang%.po}.mo\" \"$${lang}\"; "+
-               "done; "+
-               "while :; do fun() { :; } 1>&2; done")
 
-       if c.Check(err, check.IsNil) && c.Check(list, check.NotNil) {
+       pathFor := map[string]bool{}
+
+       outputPathFor := func(kinds ...string) {
+               for key := range pathFor {
+                       pathFor[key] = false
+               }
+               for _, kind := range kinds {
+                       pathFor[kind] = true
+               }
+       }
+
+       test := func(program string, output ...string) {
+               list, err := parseShellProgram(dummyLine, program)
+
+               if !c.Check(err, check.IsNil) || !c.Check(list, check.NotNil) {
+                       return
+               }
+
+               walker := NewMkShWalker()
                var commands []string
+
                add := func(kind string, format string, args ...interface{}) {
                        if format != "" && !contains(format, "%") {
                                panic(format)
                        }
                        detail := sprintf(format, args...)
                        commands = append(commands, sprintf("%16s %s", kind, detail))
+                       if pathFor[kind] {
+                               commands = append(commands, sprintf("%16s %s", "Path", walker.Path()))
+                       }
                }
 
-               walker := NewMkShWalker()
                callback := &walker.Callback
                callback.List = func(list *MkShList) { add("List", "with %d andOrs", len(list.AndOrs)) }
                callback.AndOr = func(andor *MkShAndOr) { add("AndOr", "with %d pipelines", len(andor.Pipes)) }
@@ -31,7 +43,6 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                callback.Command = func(command *MkShCommand) { add("Command", "") }
                callback.SimpleCommand = func(command *MkShSimpleCommand) {
                        add("SimpleCommand", "%s", NewStrCommand(command).String())
-                       add("Path", "%s", walker.Path())
                }
                callback.CompoundCommand = func(command *MkShCompoundCommand) { add("CompoundCommand", "") }
                callback.Case = func(caseClause *MkShCase) { add("Case", "with %d items", len(caseClause.Cases)) }
@@ -58,131 +69,174 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                //    Case with 1 item(s)
                //      ...
 
-               c.Check(commands, deepEquals, []string{
-                       "            List with 5 andOrs",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       " CompoundCommand ",
-                       "              If with 1 then-branches",
-                       "            List with 1 andOrs",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand condition",
-                       "            Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.If.List[0].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word condition",
-                       "            List with 1 andOrs",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand action",
-                       "            Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.If.List[1].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word action",
-                       "            List with 1 andOrs",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       " CompoundCommand ",
-                       "            Case with 1 items",
-                       "            Word selector",
-                       "        CaseItem with 1 patterns",
-                       "           Words with 1 words",
-                       "            Word pattern",
-                       "            List with 1 andOrs",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand case-item-action",
-                       "            Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.If." +
-                               "List[2].AndOr[0].Pipeline[0].Command[0].CompoundCommand.Case.CaseItem[0]." +
-                               "List.AndOr[0].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word case-item-action",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand set -e",
-                       "            Path List.AndOr[1].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word set",
-                       "           Words with 1 words",
-                       "            Word -e",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand cd ${WRKSRC}/locale",
-                       "            Path List.AndOr[2].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word cd",
-                       "           Words with 1 words",
-                       "            Word ${WRKSRC}/locale",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       " CompoundCommand ",
-                       "             For variable lang",
-                       "         Varname lang",
-                       "           Words with 1 words",
-                       "            Word *.po",
-                       "            List with 2 andOrs",
-                       "           AndOr with 2 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand [ \"$${lang}\" = \"wxstd.po\" ]",
-                       "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.For.List.AndOr[0].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word [",
-                       "           Words with 4 words",
-                       "            Word \"$${lang}\"",
-                       "            Word =",
-                       "            Word \"wxstd.po\"",
-                       "            Word ]",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand continue",
-                       "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.For.List.AndOr[0].Pipeline[1].Command[0].SimpleCommand",
-                       "            Word continue",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand ${TOOLS_PATH.msgfmt} -c -o \"$${lang%.po}.mo\" \"$${lang}\"",
-                       "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.For.List.AndOr[1].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word ${TOOLS_PATH.msgfmt}",
-                       "           Words with 4 words",
-                       "            Word -c",
-                       "            Word -o",
-                       "            Word \"$${lang%.po}.mo\"",
-                       "            Word \"$${lang}\"",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       " CompoundCommand ",
-                       "            Loop ",
-                       "            List with 1 andOrs",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand :",
-                       "            Path List.AndOr[4].Pipeline[0].Command[0].CompoundCommand.Loop.List[0].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word :",
-                       "            List with 1 andOrs",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "     FunctionDef for fun",
-                       " CompoundCommand ",
-                       "            List with 1 andOrs",
-                       "           AndOr with 1 pipelines",
-                       "        Pipeline with 1 commands",
-                       "         Command ",
-                       "   SimpleCommand :",
-                       "            Path List.AndOr[4].Pipeline[0].Command[0].CompoundCommand.Loop." +
-                               "List[1].AndOr[0].Pipeline[0].Command[0].FunctionDefinition.CompoundCommand." +
-                               "List.AndOr[0].Pipeline[0].Command[0].SimpleCommand",
-                       "            Word :",
-                       "       Redirects with 1 redirects",
-                       "        Redirect >&",
-                       "            Word 2"})
+               c.Check(commands, deepEquals, output)
 
                // After parsing, there is not a single level of indentation,
                // therefore even Parent(0) returns nil.
+               //
+               // This ensures that the w.push/w.pop calls are balanced.
                c.Check(walker.Parent(0), equals, nil)
        }
+
+       outputPathFor("SimpleCommand")
+       test(""+
+               "if condition; then action; else case selector in pattern) case-item-action ;; esac; fi; "+
+               "set -e; "+
+               "cd ${WRKSRC}/locale; "+
+               "for lang in *.po; do "+
+               "  [ \"$${lang}\" = \"wxstd.po\" ] && continue; "+
+               "  ${TOOLS_PATH.msgfmt} -c -o \"$${lang%.po}.mo\" \"$${lang}\"; "+
+               "done; "+
+               "while :; do fun() { :; } 1>&2; done",
+
+               "            List with 5 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               " CompoundCommand ",
+               "              If with 1 then-branches",
+               "            List with 1 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand condition",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.If.List[0].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
+               "            Word condition",
+               "            List with 1 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand action",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.If.List[1].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
+               "            Word action",
+               "            List with 1 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               " CompoundCommand ",
+               "            Case with 1 items",
+               "            Word selector",
+               "        CaseItem with 1 patterns",
+               "           Words with 1 words",
+               "            Word pattern",
+               "            List with 1 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand case-item-action",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.If."+
+                       "List[2].AndOr[0].Pipeline[0].Command[0].CompoundCommand.Case.CaseItem[0]."+
+                       "List.AndOr[0].Pipeline[0].Command[0].SimpleCommand",
+               "            Word case-item-action",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand set -e",
+               "            Path List.AndOr[1].Pipeline[0].Command[0].SimpleCommand",
+               "            Word set",
+               "           Words with 1 words",
+               "            Word -e",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand cd ${WRKSRC}/locale",
+               "            Path List.AndOr[2].Pipeline[0].Command[0].SimpleCommand",
+               "            Word cd",
+               "           Words with 1 words",
+               "            Word ${WRKSRC}/locale",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               " CompoundCommand ",
+               "             For variable lang",
+               "         Varname lang",
+               "           Words with 1 words",
+               "            Word *.po",
+               "            List with 2 andOrs",
+               "           AndOr with 2 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand [ \"$${lang}\" = \"wxstd.po\" ]",
+               "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.For.List.AndOr[0].Pipeline[0].Command[0].SimpleCommand",
+               "            Word [",
+               "           Words with 4 words",
+               "            Word \"$${lang}\"",
+               "            Word =",
+               "            Word \"wxstd.po\"",
+               "            Word ]",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand continue",
+               "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.For.List.AndOr[0].Pipeline[1].Command[0].SimpleCommand",
+               "            Word continue",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand ${TOOLS_PATH.msgfmt} -c -o \"$${lang%.po}.mo\" \"$${lang}\"",
+               "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.For.List.AndOr[1].Pipeline[0].Command[0].SimpleCommand",
+               "            Word ${TOOLS_PATH.msgfmt}",
+               "           Words with 4 words",
+               "            Word -c",
+               "            Word -o",
+               "            Word \"$${lang%.po}.mo\"",
+               "            Word \"$${lang}\"",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               " CompoundCommand ",
+               "            Loop ",
+               "            List with 1 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand :",
+               "            Path List.AndOr[4].Pipeline[0].Command[0].CompoundCommand.Loop.List[0].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
+               "            Word :",
+               "            List with 1 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "     FunctionDef for fun",
+               " CompoundCommand ",
+               "            List with 1 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand :",
+               "            Path List.AndOr[4].Pipeline[0].Command[0].CompoundCommand.Loop."+
+                       "List[1].AndOr[0].Pipeline[0].Command[0].FunctionDefinition.CompoundCommand."+
+                       "List.AndOr[0].Pipeline[0].Command[0].SimpleCommand",
+               "            Word :",
+               "       Redirects with 1 redirects",
+               "        Redirect >&",
+               "            Word 2")
+
+       outputPathFor("Redirects", "Redirect", "Word")
+       test(""+
+               "echo 'hello world' 1>/dev/null 2>&1 0</dev/random",
+
+               "            List with 1 andOrs",
+               "           AndOr with 1 pipelines",
+               "        Pipeline with 1 commands",
+               "         Command ",
+               "   SimpleCommand echo 'hello world'",
+               "            Word echo",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.ShToken",
+               "           Words with 1 words",
+               "            Word 'hello world'",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]ShToken[1].ShToken[0]",
+               "       Redirects with 3 redirects",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]MkShRedirection",
+               "        Redirect >",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]MkShRedirection.Redirection[0]",
+               "            Word /dev/null",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]MkShRedirection.Redirection[0].ShToken[0]",
+               "        Redirect >&",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]MkShRedirection.Redirection[1]",
+               "            Word 1",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]MkShRedirection.Redirection[1].ShToken[1]",
+               "        Redirect <",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]MkShRedirection.Redirection[2]",
+               "            Word /dev/random",
+               "            Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]MkShRedirection.Redirection[2].ShToken[2]")
 }

Index: pkgsrc/pkgtools/pkglint/files/options_test.go
diff -u pkgsrc/pkgtools/pkglint/files/options_test.go:1.11 pkgsrc/pkgtools/pkglint/files/options_test.go:1.12
--- pkgsrc/pkgtools/pkglint/files/options_test.go:1.11  Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/options_test.go       Sat Apr 20 17:43:24 2019
@@ -221,6 +221,7 @@ func (s *Suite) Test_CheckLinesOptionsMk
                "PLIST.three=\tyes",
                ".endif")
        t.Chdir("category/package")
+       t.FinishSetUp()
 
        G.Check(".")
 
@@ -233,3 +234,33 @@ func (s *Suite) Test_CheckLinesOptionsMk
                        "\"two\" is added to PLIST_VARS, but PLIST.two is not defined in this file.",
                "WARN: options.mk:5: Option \"two\" should be handled below in an .if block.")
 }
+
+// Up to April 2019, pkglint logged a wrong note saying that OTHER_VARIABLE
+// should have the positive branch first. That note was only ever intended
+// for PKG_OPTIONS.
+func (s *Suite) Test_OptionsLinesChecker_handleLowerCondition__foreign_variable(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpOption("opt", "")
+       t.CreateFileLines("mk/bsd.options.mk")
+       t.SetUpPackage("category/package",
+               ".include \"options.mk\"")
+       t.CreateFileLines("category/package/options.mk",
+               MkRcsID,
+               "",
+               "PKG_OPTIONS_VAR=\tPKG_OPTIONS.package",
+               "PKG_SUPPORTED_OPTIONS=\topt",
+               "",
+               ".include \"../../mk/bsd.options.mk\"",
+               "",
+               ".if empty(OTHER_VARIABLE)",
+               ".else",
+               ".endif")
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/options.mk:8: OTHER_VARIABLE is used but not defined.",
+               "WARN: ~/category/package/options.mk:4: Option \"opt\" should be handled below in an .if block.")
+}

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.49 pkgsrc/pkgtools/pkglint/files/package.go:1.50
--- pkgsrc/pkgtools/pkglint/files/package.go:1.49       Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/package.go    Sat Apr 20 17:43:24 2019
@@ -85,7 +85,11 @@ func NewPackage(dir string) *Package {
        pkg.vars.Fallback("PATCHDIR", "patches")
        pkg.vars.Fallback("KRB5_TYPE", "heimdal")
        pkg.vars.Fallback("PGSQL_VERSION", "95")
-       pkg.vars.Fallback(".CURDIR", ".") // FIXME: In reality, this is an absolute pathname.
+
+       // In reality, this is an absolute pathname. Since this variable is
+       // typically used in the form ${.CURDIR}/../../somewhere, this doesn't
+       // matter much.
+       pkg.vars.Fallback(".CURDIR", ".")
 
        return &pkg
 }
@@ -118,17 +122,27 @@ func (pkg *Package) checkPossibleDowngra
        }
 
        if change.Action == "Updated" {
-               changeVersion := replaceAll(change.Version, `nb\d+$`, "")
-               if pkgver.Compare(pkgversion, changeVersion) < 0 {
+               pkgversionNorev := replaceAll(pkgversion, `nb\d+$`, "")
+               changeNorev := replaceAll(change.Version, `nb\d+$`, "")
+               cmp := pkgver.Compare(pkgversionNorev, changeNorev)
+               switch {
+               case cmp < 0:
                        mkline.Warnf("The package is being downgraded from %s (see %s) to %s.",
                                change.Version, mkline.Line.RefToLocation(change.Location), pkgversion)
-                       G.Explain(
+                       mkline.Explain(
                                "The files in doc/CHANGES-*, in which all version changes are",
                                "recorded, have a higher version number than what the package says.",
                                "This is unusual, since packages are typically upgraded instead of",
                                "downgraded.")
 
-                       // TODO: Check whether the current version is mentioned in doc/CHANGES.
+               case cmp > 0:
+                       mkline.Notef("Package version %q is greater than the latest %q from %s.",
+                               pkgversion, change.Version, mkline.Line.RefToLocation(change.Location))
+                       mkline.Explain(
+                               "Each update to a package should be mentioned in the doc/CHANGES file.",
+                               "To do this after updating a package, run",
+                               sprintf("%q,", bmake("cce")),
+                               "which is the abbreviation for commit-changes-entry.")
                }
        }
 }
@@ -567,6 +581,7 @@ func (pkg *Package) checkfilePackageMake
        scope := NewRedundantScope()
        scope.Check(allLines) // Updates the variables in the scope
        pkg.checkGnuConfigureUseLanguages(scope)
+       pkg.checkUseLanguagesCompilerMk(allLines)
 
        pkg.determineEffectivePkgVars()
        pkg.checkPossibleDowngrade()
@@ -761,7 +776,7 @@ func (pkg *Package) checkUpdate() {
                case cmp < 0:
                        pkgnameLine.Warnf("This package should be updated to %s%s.",
                                sugg.Version, comment)
-                       G.Explain(
+                       pkgnameLine.Explain(
                                "The wishlist for package updates in doc/TODO mentions that a newer",
                                "version of this package is available.")
 
@@ -989,7 +1004,7 @@ func (pkg *Package) CheckVarorder(mkline
        //  except if they are helpful for locating the mistakes.
        mkline := relevantLines[0]
        mkline.Warnf("The canonical order of the variables is %s.", strings.Join(canonical, ", "))
-       G.Explain(
+       mkline.Explain(
                "In simple package Makefiles, some common variables should be",
                "arranged in a specific order.",
                "",
@@ -1026,19 +1041,21 @@ func (pkg *Package) checkLocallyModified
                return
        }
 
-       if !isLocallyModified(filename) {
+       if !isLocallyModified(filename) || !fileExists(filename) {
                return
        }
 
        if owner != "" {
-               NewLineWhole(filename).Warnf("Don't commit changes to this file without asking the OWNER, %s.", owner)
-               G.Explain(
+               line := NewLineWhole(filename)
+               line.Warnf("Don't commit changes to this file without asking the OWNER, %s.", owner)
+               line.Explain(
                        seeGuide("Package components, Makefile", "components.Makefile"))
        }
 
        if maintainer != "" {
-               NewLineWhole(filename).Notef("Please only commit changes that %s would approve.", maintainer)
-               G.Explain(
+               line := NewLineWhole(filename)
+               line.Notef("Please only commit changes that %s would approve.", maintainer)
+               line.Explain(
                        "See the pkgsrc guide, section \"Package components\",",
                        "keyword \"maintainer\", for more information.")
        }
@@ -1107,6 +1124,53 @@ func (pkg *Package) AutofixDistinfo(oldS
        }
 }
 
+// checkUseLanguagesCompilerMk checks that after including mk/compiler.mk
+// or mk/endian.mk for the first time, there are no more changes to
+// USE_LANGUAGES, as these would be ignored by the pkgsrc infrastructure.
+func (pkg *Package) checkUseLanguagesCompilerMk(mklines MkLines) {
+
+       var seen Once
+
+       handleVarassign := func(mkline MkLine) {
+               if mkline.Varname() != "USE_LANGUAGES" {
+                       return
+               }
+
+               if !seen.Seen("../../mk/compiler.mk") && !seen.Seen("../../mk/endian.mk") {
+                       return
+               }
+
+               if mkline.Basename == "compiler.mk" {
+                       if relpath(pkg.dir, mkline.Filename) == "../../mk/compiler.mk" {
+                               return
+                       }
+               }
+
+               mkline.Warnf("Modifying USE_LANGUAGES after including ../../mk/compiler.mk has no effect.")
+               mkline.Explain(
+                       "The file compiler.mk guards itself against multiple inclusion.")
+       }
+
+       handleInclude := func(mkline MkLine) {
+               dirname, _ := path.Split(mkline.Filename)
+               dirname = cleanpath(dirname)
+               fullIncluded := dirname + "/" + mkline.IncludedFile()
+               relIncludedFile := relpath(pkg.dir, fullIncluded)
+
+               seen.FirstTime(relIncludedFile)
+       }
+
+       mklines.ForEach(func(mkline MkLine) {
+               switch {
+               case mkline.IsVarassign():
+                       handleVarassign(mkline)
+
+               case mkline.IsInclude():
+                       handleInclude(mkline)
+               }
+       })
+}
+
 type PlistContent struct {
        Dirs  map[string]bool
        Files map[string]bool

Index: pkgsrc/pkgtools/pkglint/files/package_test.go
diff -u pkgsrc/pkgtools/pkglint/files/package_test.go:1.42 pkgsrc/pkgtools/pkglint/files/package_test.go:1.43
--- pkgsrc/pkgtools/pkglint/files/package_test.go:1.42  Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/package_test.go       Sat Apr 20 17:43:24 2019
@@ -49,31 +49,51 @@ func (s *Suite) Test_Package_checkLinesB
 func (s *Suite) Test_Package_pkgnameFromDistname(c *check.C) {
        t := s.Init(c)
 
-       pkg := NewPackage(t.File("category/package"))
-       pkg.vars.Define("PKGNAME", t.NewMkLine("Makefile", 5, "PKGNAME=dummy"))
-
-       test := func(pkgname, distname, expectedPkgname string) {
-               merged, ok := pkg.pkgnameFromDistname(pkgname, distname)
-               if !ok {
-                       merged = ""
+       var once Once
+       test := func(pkgname, distname, expectedPkgname string, diagnostics ...string) {
+               t.SetUpPackage("category/package",
+                       "PKGNAME=\t"+pkgname,
+                       "DISTNAME=\t"+distname)
+               if once.FirstTime("called") {
+                       t.FinishSetUp()
                }
-               c.Check(merged, equals, expectedPkgname)
+
+               pkg := NewPackage(t.File("category/package"))
+               pkg.loadPackageMakefile()
+               pkg.determineEffectivePkgVars()
+               t.Check(pkg.EffectivePkgname, equals, expectedPkgname)
+               t.CheckOutput(diagnostics)
        }
 
        test("pkgname-1.0", "whatever", "pkgname-1.0")
-       test("${DISTNAME}", "distname-1.0", "distname-1.0")
+
+       test("${DISTNAME}", "distname-1.0", "distname-1.0",
+               "NOTE: ~/category/package/Makefile:4: This assignment is probably redundant since PKGNAME is ${DISTNAME} by default.")
+
        test("${DISTNAME:S/dist/pkg/}", "distname-1.0", "pkgname-1.0")
+
        test("${DISTNAME:S|a|b|g}", "panama-0.13", "pbnbmb-0.13")
-       test("${DISTNAME:S|^lib||}", "libncurses", "ncurses")
-       test("${DISTNAME:S|^lib||}", "mylib", "mylib")
+
+       // The substitution succeeds, but the substituted value is missing
+       // the package version. Therefore it is discarded completely.
+       test("${DISTNAME:S|^lib||}", "libncurses", "")
+
+       // The substitution succeeds, but the substituted value is missing
+       // the package version. Therefore it is discarded completely.
+       test("${DISTNAME:S|^lib||}", "mylib", "")
+
        test("${DISTNAME:tl:S/-/./g:S/he/-/1}", "SaxonHE9-5-0-1J", "saxon-9.5.0.1j")
+
        test("${DISTNAME:C/beta/.0./}", "fspanel-0.8beta1", "fspanel-0.8.0.1")
+
        test("${DISTNAME:C/Gtk2/p5-gtk2/}", "Gtk2-1.0", "p5-gtk2-1.0")
+
        test("${DISTNAME:S/-0$/.0/1}", "aspell-af-0.50-0", "aspell-af-0.50.0")
+
        test("${DISTNAME:M*.tar.gz:C,\\..*,,}", "aspell-af-0.50-0", "")
 
-       // FIXME: Should produce a parse error since the :S modifier is malformed; see Test_MkParser_MkTokens.
-       test("${DISTNAME:S,a,b,c,d}", "aspell-af-0.50-0", "bspell-af-0.50-0")
+       test("${DISTNAME:S,a,b,c,d}", "aspell-af-0.50-0", "bspell-af-0.50-0",
+               "WARN: ~/category/package/Makefile:4: Invalid variable modifier \"c,d\" for \"DISTNAME\".")
 
        test("${DISTFILE:C,\\..*,,}", "aspell-af-0.50-0", "")
 }
@@ -356,6 +376,7 @@ func (s *Suite) Test_Package_determineEf
        pkg := t.SetUpPackage("category/package",
                "DISTNAME=\tdistname-1.0",
                "PKGNAME=\tdistname-1.0")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -370,6 +391,7 @@ func (s *Suite) Test_Package_determineEf
        pkg := t.SetUpPackage("category/package",
                "DISTNAME=\tdistname-1.0",
                "PKGNAME=\t${DISTNAME}")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -383,6 +405,7 @@ func (s *Suite) Test_Package_determineEf
 
        pkg := t.SetUpPackage("category/package",
                "DISTNAME=\tpkgname-version")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -397,9 +420,10 @@ func (s *Suite) Test_Package_determineEf
        t.SetUpPackage("x11/p5-gtk2",
                "DISTNAME=\tGtk2-1.0",
                "PKGNAME=\t${DISTNAME:C:Gtk2:p5-gtk2:}")
+       t.FinishSetUp()
        pkg := NewPackage(t.File("x11/p5-gtk2"))
-
        files, mklines, allLines := pkg.load()
+
        pkg.check(files, mklines, allLines)
 
        t.Check(pkg.EffectivePkgname, equals, "p5-gtk2-1.0")
@@ -415,9 +439,10 @@ func (s *Suite) Test_Package_determineEf
        t.SetUpPackage("category/package",
                "DISTNAME=\tdistname-1.0",
                "PKGNAME=\t${DISTNAME:C:does_not_match:replacement:}")
+       t.FinishSetUp()
        pkg := NewPackage(t.File("category/package"))
-
        files, mklines, allLines := pkg.load()
+
        pkg.check(files, mklines, allLines)
 
        t.Check(pkg.EffectivePkgname, equals, "distname-1.0")
@@ -474,6 +499,7 @@ func (s *Suite) Test_Package_loadPackage
                "LICENSE=\t2-clause-bsd")
        // TODO: There is no .include line at the end of the Makefile.
        //  This should always be checked though.
+       t.FinishSetUp()
 
        G.checkdirPackage(t.File("category/package"))
 
@@ -556,7 +582,8 @@ func (s *Suite) Test_Package__varuse_at_
                ".include \"../../mk/bsd.pkg.mk\"")
 
        t.SetUpCommandLine("-q", "-Wall,no-space")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
+
        G.Check(t.File("category/pkgbase"))
 
        t.CheckOutputLines(
@@ -608,7 +635,7 @@ func (s *Suite) Test_Package__relative_i
                "PKGNAME=\tpkgname-1.67",
                "DISTNAME=\tdistfile_1_67",
                ".include \"../../category/package/other.mk\"")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -647,6 +674,7 @@ func (s *Suite) Test_Package_loadPackage
        pkg := t.SetUpPackage("category/package",
                "PECL_VERSION=\t1.1.2",
                ".include \"../../lang/php/ext.mk\"")
+       t.FinishSetUp()
 
        G.Check(pkg)
 }
@@ -672,6 +700,7 @@ func (s *Suite) Test_Package_checkInclud
                ".endif",
                ".include \"../../sysutils/coreutils/buildlink3.mk\"")
        t.Chdir("category/package")
+       t.FinishSetUp()
 
        G.checkdirPackage(".")
 
@@ -692,6 +721,7 @@ func (s *Suite) Test_Package__include_wi
        t.SetUpVartypes()
        t.SetUpPackage("category/package",
                ".include \"options.mk\"")
+       t.FinishSetUp()
 
        G.checkdirPackage(t.File("category/package"))
 
@@ -708,6 +738,7 @@ func (s *Suite) Test_Package__include_af
                ".if exists(options.mk)",
                ".  include \"options.mk\"",
                ".endif")
+       t.FinishSetUp()
 
        G.checkdirPackage(t.File("category/package"))
 
@@ -724,6 +755,7 @@ func (s *Suite) Test_Package_readMakefil
                ".if exists(options.mk)",
                ".  include \"another.mk\"",
                ".endif")
+       t.FinishSetUp()
 
        G.checkdirPackage(t.File("category/package"))
 
@@ -755,7 +787,7 @@ func (s *Suite) Test_Package__redundant_
                "",
                ".include \"../../math/R/Makefile.extension\"",
                ".include \"../../mk/bsd.pkg.mk\"")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        // See Package.checkfilePackageMakefile
        G.checkdirPackage(t.File("math/R-date"))
@@ -791,9 +823,9 @@ func (s *Suite) Test_Package_checkUpdate
                "\t"+"o package1-1.0",
                "\t"+"o package2-2.0 [nice new features]",
                "\t"+"o package3-3.0 [security update]")
-
        t.Chdir(".")
-       G.Main("pkglint", "-Wall,no-space", "category/pkg1", "category/pkg2", "category/pkg3")
+
+       t.Main("-Wall,no-space", "category/pkg1", "category/pkg2", "category/pkg3")
 
        t.CheckOutputLines(
                "WARN: category/pkg1/../../doc/TODO:3: Invalid line format \"\".",
@@ -812,6 +844,7 @@ func (s *Suite) Test_NewPackage(c *check
        t.SetUpPkgsrc()
        t.CreateFileLines("category/Makefile",
                MkRcsID)
+       t.FinishSetUp()
 
        c.Check(
                func() { NewPackage("category") },
@@ -844,6 +877,7 @@ func (s *Suite) Test__distinfo_from_othe
                RcsID,
                "",
                "SHA1 (patch-aa) = 1234")
+       t.FinishSetUp()
 
        G.Check("x11/gst-x11")
 
@@ -861,6 +895,7 @@ func (s *Suite) Test_Package_checkfilePa
        pkg := t.SetUpPackage("category/package",
                "GNU_CONFIGURE=\tyes",
                "USE_LANGUAGES=\t#")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -877,6 +912,7 @@ func (s *Suite) Test_Package_checkfilePa
        pkg := t.SetUpPackage("category/package",
                "GNU_CONFIGURE=\tyes",
                "USE_LANGUAGES=\t# none, really")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -890,6 +926,7 @@ func (s *Suite) Test_Package_checkfilePa
        pkg := t.SetUpPackage("category/package",
                "REPLACE_PERL=\t*.pl",
                "NO_CONFIGURE=\tyes")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -902,6 +939,7 @@ func (s *Suite) Test_Package_checkfilePa
 
        pkg := t.SetUpPackage("category/package",
                "META_PACKAGE=\tyes")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -916,6 +954,7 @@ func (s *Suite) Test_Package_checkfilePa
        pkg := t.SetUpPackage("category/package",
                "USE_X11=\tyes",
                "USE_IMAKE=\tyes")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -931,7 +970,7 @@ func (s *Suite) Test_Package_checkGnuCon
                "USE_LANGUAGES+=\tc++14",
                "USE_LANGUAGES+=\tada",
                "GNU_CONFIGURE=\tyes")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -955,7 +994,7 @@ func (s *Suite) Test_Package_checkGnuCon
                "USE_LANGUAGES+=\tc99",
                "USE_LANGUAGES+=\tada",
                "GNU_CONFIGURE=\tyes")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -981,7 +1020,7 @@ func (s *Suite) Test_Package_checkGnuCon
                "USE_LANGUAGES?=\tc",
                "USE_LANGUAGES+=\tc",
                "USE_LANGUAGES+=\tc++")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -1012,7 +1051,7 @@ func (s *Suite) Test_Package_checkGnuCon
 
        t.SetUpPackage("category/package",
                "GNU_CONFIGURE=\tyes")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
@@ -1025,14 +1064,14 @@ func (s *Suite) Test_Package_checkGnuCon
        t.SetUpPackage("category/package",
                "GNU_CONFIGURE=\tyes",
                "USE_LANGUAGES=\tc++ objc")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_Package__USE_LANGUAGES_too_late(c *check.C) {
+func (s *Suite) Test_Package_checkUseLanguagesCompilerMk__too_late(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package",
@@ -1040,12 +1079,36 @@ func (s *Suite) Test_Package__USE_LANGUA
                "USE_LANGUAGES=\tc c99 fortran ada c++14")
        t.CreateFileLines("mk/compiler.mk",
                MkRcsID)
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
-       // FIXME: There must be a warning "USE_LANGUAGES must be added before including compiler.mk."
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:21: " +
+                       "Modifying USE_LANGUAGES after including ../../mk/compiler.mk has no effect.")
+}
+
+func (s *Suite) Test_Package_checkUseLanguagesCompilerMk__compiler_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               ".include \"compiler.mk\"",
+               "USE_LANGUAGES=\tc c99 fortran ada c++14",
+               ".include \"../../mk/compiler.mk\"",
+               "USE_LANGUAGES=\tc c99 fortran ada c++14")
+       t.CreateFileLines("category/package/compiler.mk",
+               MkRcsID)
+       t.CreateFileLines("mk/compiler.mk",
+               MkRcsID)
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:23: "+
+                       "Definition of USE_LANGUAGES is redundant because of line 21.",
+               "WARN: ~/category/package/Makefile:23: "+
+                       "Modifying USE_LANGUAGES after including ../../mk/compiler.mk has no effect.")
 }
 
 func (s *Suite) Test_Package_readMakefile__skipping(c *check.C) {
@@ -1054,6 +1117,7 @@ func (s *Suite) Test_Package_readMakefil
        t.SetUpCommandLine("-Wall,no-space")
        pkg := t.SetUpPackage("category/package",
                ".include \"${MYSQL_PKGSRCDIR:S/-client$/-server/}/buildlink3.mk\"")
+       t.FinishSetUp()
 
        t.EnableTracingToLog()
        G.Check(pkg)
@@ -1086,6 +1150,7 @@ func (s *Suite) Test_Package_readMakefil
                ".include \"../../devel/zlib/buildlink3.mk\"")
        t.CreateFileLines("devel/zlib/buildlink3.mk",
                ".include \"../../enoent/enoent/buildlink3.mk\"")
+       t.FinishSetUp()
 
        G.checkdirPackage(pkg)
 
@@ -1100,6 +1165,7 @@ func (s *Suite) Test_Package_readMakefil
                MkRcsID)
        pkg := t.SetUpPackage("category/package",
                ".include \"../package/extra.mk\"")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -1127,10 +1193,12 @@ func (s *Suite) Test_Package_readMakefil
        t.CreateFileLines("category/lib1/builtin.mk",
                MkRcsID,
                "VAR_FROM_BUILTIN=\t# defined")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 
        t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:23: Please use \"${ECHO}\" instead of \"echo\".",
                "WARN: ~/category/package/Makefile:23: OTHER_VAR is used but not defined.")
 }
 
@@ -1151,6 +1219,7 @@ func (s *Suite) Test_Package_readMakefil
                ".include \"version.mk\"")
        t.CreateFileLines("lang/language/version.mk",
                MkRcsID)
+       t.FinishSetUp()
        pkg := NewPackage(t.File("category/package"))
 
        pkg.loadPackageMakefile()
@@ -1182,6 +1251,7 @@ func (s *Suite) Test_Package_checkLocall
 
        pkg := t.SetUpPackage("category/package",
                "MAINTAINER=\tpkgsrc-users%NetBSD.org@localhost")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
@@ -1248,16 +1318,13 @@ func (s *Suite) Test_Package_checkLocall
                RcsID,
                "",
                "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac")
+       t.FinishSetUp()
 
        G.Check(pkg)
 
        t.CheckOutputLines(
-               "NOTE: ~/category/package/Makefile: "+
-                       "Please only commit changes that "+
-                       "maintainer%example.org@localhost would approve.",
-               // FIXME: There must be no warning for directories.
-               "NOTE: ~/category/package/patches: "+
-                       "Please only commit changes that "+
+               "NOTE: ~/category/package/Makefile: " +
+                       "Please only commit changes that " +
                        "maintainer%example.org@localhost would approve.")
 }
 
@@ -1269,6 +1336,7 @@ func (s *Suite) Test_Package_AutofixDist
 
        t.SetUpPkgsrc()
        G.Pkg = NewPackage(t.File("category/package"))
+       t.FinishSetUp()
 
        G.Pkg.AutofixDistinfo("old", "new")
 
@@ -1291,7 +1359,7 @@ func (s *Suite) Test_Package__using_comm
                RcsID,
                "",
                "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("security/pinentry"))
 
@@ -1317,7 +1385,7 @@ func (s *Suite) Test_Package__redundant_
        t.CreateFileLines("lang/python/egg.mk",
                MkRcsID,
                "PY_PATCHPLIST=\tyes")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
 
        G.Check(t.File("databases/py-trytond-ldap-authentication"))
 
@@ -1349,6 +1417,7 @@ func (s *Suite) Test_Package_readMakefil
                ".include \"pthread.builtin.mk\"")
        t.CreateFileLines("mk/pthread.builtin.mk",
                "# This should be included by pthread.buildlink3.mk")
+       t.FinishSetUp()
 
        G.Check(t.File("category/package"))
 

Index: pkgsrc/pkgtools/pkglint/files/plist.go
diff -u pkgsrc/pkgtools/pkglint/files/plist.go:1.39 pkgsrc/pkgtools/pkglint/files/plist.go:1.40
--- pkgsrc/pkgtools/pkglint/files/plist.go:1.39 Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/plist.go      Sat Apr 20 17:43:24 2019
@@ -15,8 +15,9 @@ func CheckLinesPlist(pkg *Package, lines
        lines.CheckRcsID(0, `@comment `, "@comment ")
 
        if lines.Len() == 1 {
-               lines.Lines[0].Warnf("PLIST files shouldn't be empty.")
-               G.Explain(
+               line := lines.Lines[0]
+               line.Warnf("PLIST files shouldn't be empty.")
+               line.Explain(
                        "One reason for empty PLISTs is that this is a newly created package",
                        sprintf("and that the author didn't run %q after installing the files.", bmake("print-PLIST")),
                        "",
@@ -200,7 +201,7 @@ func (ck *PlistChecker) checkPath(pline 
        }
        if hasSuffix(text, "/perllocal.pod") {
                pline.Warnf("The perllocal.pod file should not be in the PLIST.")
-               G.Explain(
+               pline.Explain(
                        "This file is handled automatically by the INSTALL/DEINSTALL scripts",
                        "since its contents depends on more than one package.")
        }
@@ -242,7 +243,7 @@ func (ck *PlistChecker) checkSorted(plin
                if ck.lastFname != "" {
                        if ck.lastFname > text && !G.Logger.Opts.Autofix {
                                pline.Warnf("%q should be sorted before %q.", text, ck.lastFname)
-                               G.Explain(
+                               pline.Explain(
                                        "The files in the PLIST should be sorted alphabetically.",
                                        "This allows human readers to quickly see whether a file is included or not.")
                        }
@@ -271,7 +272,7 @@ func (ck *PlistChecker) checkDuplicate(p
 func (ck *PlistChecker) checkPathBin(pline *PlistLine, dirname, basename string) {
        if contains(dirname, "/") {
                pline.Warnf("The bin/ directory should not have subdirectories.")
-               G.Explain(
+               pline.Explain(
                        "The programs in bin/ are collected there to be executable by the",
                        "user without having to type an absolute path.",
                        "This advantage does not apply to programs in subdirectories of bin/.",
@@ -389,7 +390,7 @@ func (ck *PlistChecker) checkPathShare(p
 
                if text == "share/icons/hicolor/icon-theme.cache" && pkg.Pkgpath != "graphics/hicolor-icon-theme" {
                        pline.Errorf("The file icon-theme.cache must not appear in any PLIST file.")
-                       G.Explain(
+                       pline.Explain(
                                "Remove this line and add the following line to the package Makefile.",
                                "",
                                ".include \"../../graphics/hicolor-icon-theme/buildlink3.mk\"")
@@ -399,7 +400,7 @@ func (ck *PlistChecker) checkPathShare(p
                        f := "../../graphics/gnome-icon-theme/buildlink3.mk"
                        if !pkg.included.Seen(f) {
                                pline.Errorf("The package Makefile must include %q.", f)
-                               G.Explain(
+                               pline.Explain(
                                        "Packages that install GNOME icons must maintain the icon theme",
                                        "cache.")
                        }
@@ -418,7 +419,7 @@ func (ck *PlistChecker) checkPathShare(p
 
        case hasPrefix(text, "share/info/"):
                pline.Warnf("Info pages should be installed into info/, not share/info/.")
-               G.Explain(
+               pline.Explain(
                        "To fix this, add INFO_FILES=yes to the package Makefile.")
 
        case hasPrefix(text, "share/man/"):
@@ -429,7 +430,7 @@ func (ck *PlistChecker) checkPathShare(p
 func (pline *PlistLine) CheckTrailingWhitespace() {
        if hasSuffix(pline.text, " ") || hasSuffix(pline.text, "\t") {
                pline.Errorf("Pkgsrc does not support filenames ending in whitespace.")
-               G.Explain(
+               pline.Explain(
                        "Each character in the PLIST is relevant, even trailing whitespace.")
        }
 }
@@ -458,7 +459,7 @@ func (pline *PlistLine) CheckDirective(c
 
        case "dirrm":
                pline.Warnf("@dirrm is obsolete. Please remove this line.")
-               G.Explain(
+               pline.Explain(
                        "Directories are removed automatically when they are empty.",
                        "When a package needs an empty directory, it can use the @pkgdir",
                        "command in the PLIST.")
@@ -482,7 +483,7 @@ func (pline *PlistLine) CheckDirective(c
 
 func (pline *PlistLine) warnImakeMannewsuffix() {
        pline.Warnf("IMAKE_MANNEWSUFFIX is not meant to appear in PLISTs.")
-       G.Explain(
+       pline.Explain(
                "This is the result of a print-PLIST call that has not been edited",
                "manually by the package maintainer.",
                "Please replace the IMAKE_MANNEWSUFFIX with:",

Index: pkgsrc/pkgtools/pkglint/files/plist_test.go
diff -u pkgsrc/pkgtools/pkglint/files/plist_test.go:1.35 pkgsrc/pkgtools/pkglint/files/plist_test.go:1.36
--- pkgsrc/pkgtools/pkglint/files/plist_test.go:1.35    Sun Mar 24 13:58:38 2019
+++ pkgsrc/pkgtools/pkglint/files/plist_test.go Sat Apr 20 17:43:24 2019
@@ -631,7 +631,7 @@ func (s *Suite) Test_PlistChecker_checkP
                PlistRcsID,
                "share/icons/gnome/16x16/devices/media-optical-cd-audio.png",
                "share/icons/gnome/16x16/devices/media-optical-dvd.png")
-       G.Pkgsrc.LoadInfrastructure()
+       t.FinishSetUp()
        t.Chdir(".")
 
        // This variant is typically run interactively.

Index: pkgsrc/pkgtools/pkglint/files/redundantscope.go
diff -u pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.3 pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.4
--- pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.3 Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/redundantscope.go     Sat Apr 20 17:43:24 2019
@@ -204,7 +204,7 @@ func (s *RedundantScope) onRedundant(red
 func (s *RedundantScope) onOverwrite(overwritten MkLine, by MkLine) {
        overwritten.Warnf("Variable %s is overwritten in %s.",
                overwritten.Varname(), overwritten.RefTo(by))
-       G.Explain(
+       overwritten.Explain(
                "The variable definition in this line does not have an effect since",
                "it is overwritten elsewhere.",
                "This typically happens because of a typo (writing = instead of +=)",

Index: pkgsrc/pkgtools/pkglint/files/shell_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shell_test.go:1.43 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.44
--- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.43    Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/shell_test.go Sat Apr 20 17:43:24 2019
@@ -680,10 +680,10 @@ func (s *Suite) Test_ShellLineChecker_Ch
        t.SetUpTool("cp", "CP", AtRunTime)
        t.SetUpTool("mv", "MV", AtRunTime)
        t.SetUpTool("sed", "SED", AtRunTime)
-       text := "\tfor f in *.pl; do ${SED} s,@PREFIX@,${PREFIX}, < $f > $f.tmp && ${MV} $f.tmp $f; done"
+       text := "for f in *.pl; do ${SED} s,@PREFIX@,${PREFIX}, < $f > $f.tmp && ${MV} $f.tmp $f; done"
 
-       ck := t.NewShellLineChecker(nil, "Makefile", 3, text)
-       ck.mkline.Tokenize(ck.mkline.ShellCommand(), true)
+       ck := t.NewShellLineChecker(nil, "Makefile", 3, "\t# dummy")
+       ck.mkline.Tokenize(text, true)
        ck.CheckShellCommandLine(text)
 
        t.CheckOutputLines(
@@ -1091,6 +1091,7 @@ func (s *Suite) Test_SimpleCommandChecke
        t.CreateFileLines("category/package/extra.mk",
                MkRcsID,
                "PYTHON_BIN=\tmy_cmd")
+       t.FinishSetUp()
 
        G.Check(pkg)
 

Index: pkgsrc/pkgtools/pkglint/files/shtokenizer.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.15 pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.16
--- pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.15   Sun Jan 13 19:55:53 2019
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer.go        Sat Apr 20 17:43:24 2019
@@ -53,6 +53,8 @@ func (p *ShTokenizer) ShAtom(quoting ShQ
                atom = p.shAtomSubshDquot()
        case shqSubshSquot:
                atom = p.shAtomSubshSquot()
+       case shqSubshBackt:
+               atom = p.shAtomSubshBackt()
        case shqDquotBacktDquot:
                atom = p.shAtomDquotBacktDquot()
        case shqDquotBacktSquot:
@@ -61,12 +63,9 @@ func (p *ShTokenizer) ShAtom(quoting ShQ
 
        if atom == nil {
                lexer.Reset(mark)
-               switch {
-               case hasPrefix(lexer.Rest(), "${"):
-                       p.parser.Line.Warnf("Unclosed Make variable starting at %q.", shorten(lexer.Rest(), 20))
-               case hasPrefix(lexer.Rest(), "$${"):
+               if hasPrefix(lexer.Rest(), "$${") {
                        p.parser.Line.Warnf("Unclosed shell variable starting at %q.", shorten(lexer.Rest(), 20))
-               default:
+               } else {
                        p.parser.Line.Warnf("Internal pkglint error in ShTokenizer.ShAtom at %q (quoting=%s).", lexer.Rest(), quoting)
                }
        }
@@ -158,12 +157,19 @@ func (p *ShTokenizer) shAtomSubsh() *ShA
        case lexer.SkipByte('\''):
                return &ShAtom{shtText, lexer.Since(mark), shqSubshSquot, nil}
        case lexer.SkipByte('`'):
-               // FIXME: return &ShAtom{shtText, lexer.Since(mark), shqBackt, nil}
+               return &ShAtom{shtText, lexer.Since(mark), shqSubshBackt, nil}
        case lexer.SkipRegexp(G.res.Compile(`^#[^)]*`)):
                return &ShAtom{shtComment, lexer.Since(mark), q, nil}
        case lexer.SkipByte(')'):
-               // shtText instead of shtOperator because this atom belongs to a shtText token.
-               return &ShAtom{shtText, lexer.Since(mark), shqPlain, nil}
+               // The closing parenthesis can have multiple meanings:
+               // - end of a subshell, such as (echo "in a subshell")
+               // - end of a subshell variable expression, such as var=$$(echo "from a subshell")
+               // - end of a case pattern
+               // In the "subshell variable expression" case, the atom type
+               // could be shtText since it is part of a text node. On the
+               // other hand, pkglint doesn't tokenize shell programs correctly
+               // anyway. This needs to be fixed someday.
+               return &ShAtom{shtOperator, lexer.Since(mark), shqPlain, nil}
        }
        if op := p.shOperator(q); op != nil {
                return op
@@ -237,6 +243,19 @@ func (p *ShTokenizer) shAtomSubshSquot()
        return p.shAtomInternal(q, false, true)
 }
 
+func (p *ShTokenizer) shAtomSubshBackt() *ShAtom {
+       const q = shqSubshBackt
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
+       switch {
+       case lexer.SkipByte('`'):
+               return &ShAtom{shtOperator, lexer.Since(mark), shqSubsh, nil}
+       case lexer.SkipHspace():
+               return &ShAtom{shtSpace, lexer.Since(mark), q, nil}
+       }
+       return p.shAtomInternal(q, false, false)
+}
+
 func (p *ShTokenizer) shAtomDquotBacktDquot() *ShAtom {
        const q = shqDquotBacktDquot
        lexer := p.parser.lexer
@@ -391,11 +410,13 @@ func (p *ShTokenizer) ShAtoms() []*ShAto
 func (p *ShTokenizer) ShToken() *ShToken {
        var curr *ShAtom
        q := shqPlain
+       prevQ := q
 
        peek := func() *ShAtom {
                if curr == nil {
                        curr = p.ShAtom(q)
                        if curr != nil {
+                               prevQ = q
                                q = curr.Quoting
                        }
                }
@@ -414,17 +435,18 @@ func (p *ShTokenizer) ShToken() *ShToken
                initialMark = lexer.Mark()
        }
 
-       if peek() == nil {
+       if curr == nil {
                return nil
        }
-       if atom := peek(); !atom.Type.IsWord() {
+
+       if atom := peek(); !atom.Type.IsWord() && atom.Quoting != shqSubsh {
                return NewShToken(atom.MkText, atom)
        }
 
        for {
                mark := lexer.Mark()
                atom := peek()
-               if atom != nil && (atom.Type.IsWord() || atom.Quoting != shqPlain) {
+               if atom != nil && (atom.Type.IsWord() || q != shqPlain || prevQ == shqSubsh) {
                        skip()
                        atoms = append(atoms, atom)
                        continue
@@ -433,6 +455,11 @@ func (p *ShTokenizer) ShToken() *ShToken
                break
        }
 
+       if q != shqPlain {
+               lexer.Reset(initialMark)
+               return nil
+       }
+
        G.Assertf(len(atoms) > 0, "ShTokenizer.ShToken")
        return NewShToken(lexer.Since(initialMark), atoms...)
 }

Index: pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.14 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.15
--- pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.14      Sat Jan 26 16:31:33 2019
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go   Sat Apr 20 17:43:25 2019
@@ -10,21 +10,28 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
 
        // testRest ensures that the given string is parsed to the expected
        // atoms, and returns the remaining text.
-       testRest := func(s string, expectedAtoms ...*ShAtom) string {
+       testRest := func(s string, expectedAtoms []*ShAtom, expectedRest string) {
                p := NewShTokenizer(dummyLine, s, false)
-               q := shqPlain
-               for _, expectedAtom := range expectedAtoms {
-                       c.Check(p.ShAtom(q), deepEquals, expectedAtom)
-                       q = expectedAtom.Quoting
+
+               actualAtoms := p.ShAtoms()
+
+               t.Check(p.Rest(), equals, expectedRest)
+               c.Check(len(actualAtoms), equals, len(expectedAtoms))
+
+               for i, actualAtom := range actualAtoms {
+                       if i < len(expectedAtoms) {
+                               c.Check(actualAtom, deepEquals, expectedAtoms[i])
+                       } else {
+                               c.Check(actualAtom, deepEquals, nil)
+                       }
                }
-               return p.Rest()
        }
+       atoms := func(atoms ...*ShAtom) []*ShAtom { return atoms }
 
        // test ensures that the given string is parsed to the expected
        // atoms, and that the text is completely consumed by the parser.
        test := func(str string, expected ...*ShAtom) {
-               rest := testRest(str, expected...)
-               c.Check(rest, equals, "")
+               testRest(str, expected, "")
                t.CheckOutputEmpty()
        }
 
@@ -52,31 +59,35 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
        pipe := operator("|")
        subshell := atom(shtSubshell, "$$(")
 
-       q := func(q ShQuoting, atom *ShAtom) *ShAtom {
-               return &ShAtom{atom.Type, atom.MkText, q, atom.data}
+       q := func(q ShQuoting) func(atom *ShAtom) *ShAtom {
+               return func(atom *ShAtom) *ShAtom {
+                       return &ShAtom{atom.Type, atom.MkText, q, atom.data}
+               }
        }
-       backt := func(atom *ShAtom) *ShAtom { return q(shqBackt, atom) }
-       dquot := func(atom *ShAtom) *ShAtom { return q(shqDquot, atom) }
-       squot := func(atom *ShAtom) *ShAtom { return q(shqSquot, atom) }
-       subsh := func(atom *ShAtom) *ShAtom { return q(shqSubsh, atom) }
-       backtDquot := func(atom *ShAtom) *ShAtom { return q(shqBacktDquot, atom) }
-       backtSquot := func(atom *ShAtom) *ShAtom { return q(shqBacktSquot, atom) }
-       dquotBackt := func(atom *ShAtom) *ShAtom { return q(shqDquotBackt, atom) }
-       subshDquot := func(atom *ShAtom) *ShAtom { return q(shqSubshDquot, atom) }
-       subshSquot := func(atom *ShAtom) *ShAtom { return q(shqSubshSquot, atom) }
-       dquotBacktDquot := func(atom *ShAtom) *ShAtom { return q(shqDquotBacktDquot, atom) }
-       dquotBacktSquot := func(atom *ShAtom) *ShAtom { return q(shqDquotBacktSquot, atom) }
+       backt := q(shqBackt)
+       dquot := q(shqDquot)
+       squot := q(shqSquot)
+       subsh := q(shqSubsh)
+       backtDquot := q(shqBacktDquot)
+       backtSquot := q(shqBacktSquot)
+       dquotBackt := q(shqDquotBackt)
+       subshDquot := q(shqSubshDquot)
+       subshSquot := q(shqSubshSquot)
+       subshBackt := q(shqSubshBackt)
+       dquotBacktDquot := q(shqDquotBacktDquot)
+       dquotBacktSquot := q(shqDquotBacktSquot)
 
        // Ignore unused functions; useful for deleting some of the tests during debugging.
        use := func(args ...interface{}) {}
-       use(testRest, test)
+       use(testRest, test, atoms)
        use(operator, comment, mkvar, text, whitespace)
-       use(space, semicolon, pipe, subshell)
+       use(space, semicolon, pipe, subshell, shvar)
        use(backt, dquot, squot, subsh)
        use(backtDquot, backtSquot, dquotBackt, subshDquot, subshSquot)
        use(dquotBacktDquot, dquotBacktSquot)
 
-       test("" /* none */)
+       test("",
+               nil...)
 
        test("$$var",
                shvar("$$var", "var"))
@@ -94,8 +105,8 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                squot(text("single-quoted")),
                text("'"))
 
-       rest := testRest("\"" /* none */)
-       c.Check(rest, equals, "\"")
+       test("\"",
+               dquot(text("\"")))
 
        test("$${file%.c}.o",
                shvar("$${file%.c}", "file"),
@@ -279,15 +290,16 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                semicolon,
                shvar("$$-", "-"))
 
-       rest = testRest("COMMENT=\t\\Make $$$$ fast\"",
+       test("COMMENT=\t\\Make $$$$ fast\"",
+
                text("COMMENT="),
                whitespace("\t"),
                text("\\Make"),
                space,
                shvar("$$$$", "$"),
                space,
-               text("fast"))
-       c.Check(rest, equals, "\"")
+               text("fast"),
+               dquot(text("\"")))
 
        test("var=`echo;echo|echo&echo||echo&&echo>echo`",
                text("var="),
@@ -359,7 +371,7 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
        test("$$(cat)",
                subsh(subshell),
                subsh(text("cat")),
-               text(")"))
+               operator(")"))
 
        test("$$(cat 'file')",
                subsh(subshell),
@@ -368,12 +380,12 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                subshSquot(text("'")),
                subshSquot(text("file")),
                subsh(text("'")),
-               text(")"))
+               operator(")"))
 
        test("$$(# comment) arg",
                subsh(subshell),
                subsh(comment("# comment")),
-               text(")"),
+               operator(")"),
                space,
                text("arg"))
 
@@ -388,7 +400,18 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                subshSquot(text("'")),
                subshSquot(text("second")),
                subsh(text("'")),
-               text(")"))
+               operator(")"))
+
+       test("$$(echo `echo nested-subshell`)",
+               subsh(subshell),
+               subsh(text("echo")),
+               subsh(space),
+               subshBackt(text("`")),
+               subshBackt(text("echo")),
+               subshBackt(space),
+               subshBackt(text("nested-subshell")),
+               subsh(operator("`")),
+               operator(")"))
 }
 
 func (s *Suite) Test_ShTokenizer_ShAtom__quoting(c *check.C) {
@@ -488,6 +511,9 @@ func (s *Suite) Test_ShTokenizer_ShToken
                "PATH=${PATH:Q}",
                "true")
 
+       test("id=$$(id)",
+               "id=$$(id)")
+
        test("id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)",
                "id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)")
 
@@ -552,72 +578,70 @@ func (s *Suite) Test_ShTokenizer_shVarUs
 func (s *Suite) Test_ShTokenizer__examples_from_fuzzing(c *check.C) {
        t := s.Init(c)
 
-       mklines := t.NewMkLines("fuzzing.mk",
-               MkRcsID,
-               "",
-               "pre-configure:",
-
-               // Covers shAtomBacktDquot: return nil.
-               // These are nested backticks with double quotes,
-               // which should be avoided since POSIX marks them as unspecified.
-               "\t"+"`\"`",
-
-               // Covers shAtomBacktSquot: return nil
-               "\t"+"`'$`",
-
-               // Covers shAtomDquotBacktSquot: return nil
-               "\t"+"\"`'`y",
-
-               // Covers shAtomDquotBackt: return nil
-               // FIXME: Pkglint must parse unescaped dollar in the same way, everywhere.
-               "\t"+"\"`$|",
-
-               // Covers shAtomDquotBacktDquot: return nil
-               // FIXME: Pkglint must support unlimited nesting.
-               "\t"+"\"`\"`",
-
-               // Covers shAtomSubshDquot: return nil
-               "\t"+"$$(\"'",
-
-               // Covers shAtomSubsh: case lexer.AdvanceStr("`")
-               "\t"+"$$(`",
-
-               // Covers shAtomSubshSquot: return nil
-               "\t"+"$$('$)",
-
-               // Covers shAtomDquotBackt: case lexer.AdvanceRegexp("^#[^`]*")
-               "\t"+"\"`# comment")
-
-       mklines.Check()
-
-       // Just good that these redundant error messages don't occur every day.
-       t.CheckOutputLines(
-               "WARN: fuzzing.mk:4: Internal pkglint error in ShTokenizer.ShAtom at \"`\" (quoting=bd).",
-               "WARN: fuzzing.mk:4: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"`\\\"`\"",
-
-               "WARN: fuzzing.mk:5: Internal pkglint error in ShTokenizer.ShAtom at \"$`\" (quoting=bs).",
-               "WARN: fuzzing.mk:5: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"`'$`\"",
-               "WARN: fuzzing.mk:5: Internal pkglint error in MkLine.Tokenize at \"$`\".",
-
-               "WARN: fuzzing.mk:6: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"\\\"`'`y\"",
-
-               "WARN: fuzzing.mk:7: Internal pkglint error in ShTokenizer.ShAtom at \"$|\" (quoting=db).",
-               "WARN: fuzzing.mk:7: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"\\\"`$|\"",
-               "WARN: fuzzing.mk:7: Internal pkglint error in MkLine.Tokenize at \"$|\".",
-
-               "WARN: fuzzing.mk:8: Internal pkglint error in ShTokenizer.ShAtom at \"`\" (quoting=dbd).",
-               "WARN: fuzzing.mk:8: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"\\\"`\\\"`\"",
-
-               "WARN: fuzzing.mk:9: Invoking subshells via $(...) is not portable enough.",
-
-               "WARN: fuzzing.mk:10: Internal pkglint error in ShTokenizer.ShAtom at \"`\" (quoting=S).",
-               "WARN: fuzzing.mk:10: Invoking subshells via $(...) is not portable enough.",
-
-               "WARN: fuzzing.mk:11: Internal pkglint error in ShTokenizer.ShAtom at \"$)\" (quoting=Ss).",
-               "WARN: fuzzing.mk:11: Invoking subshells via $(...) is not portable enough.",
-               "WARN: fuzzing.mk:11: Internal pkglint error in MkLine.Tokenize at \"$)\".",
-
-               "WARN: fuzzing.mk:12: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"\\\"`# comment\"")
+       test := func(input string, diagnostics ...string) {
+               mklines := t.NewMkLines("filename.mk",
+                       MkRcsID,
+                       "\t"+input)
+               mklines.Check()
+               t.CheckOutput(diagnostics)
+       }
+
+       // Covers shAtomBacktDquot: return nil.
+       // These are nested backticks with double quotes,
+       // which should be avoided since POSIX marks them as unspecified.
+       test(
+               "`\"`",
+               "WARN: filename.mk:2: Internal pkglint error in ShTokenizer.ShAtom at \"`\" (quoting=bd).",
+               "WARN: filename.mk:2: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"`\\\"`\"")
+
+       // Covers shAtomBacktSquot: return nil
+       test(
+               "`'$`",
+               "WARN: filename.mk:2: Internal pkglint error in ShTokenizer.ShAtom at \"$`\" (quoting=bs).",
+               "WARN: filename.mk:2: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"`'$`\"",
+               "WARN: filename.mk:2: Internal pkglint error in MkLine.Tokenize at \"$`\".")
+
+       // Covers shAtomDquotBacktSquot: return nil
+       test(
+               "\"`'`y",
+               "WARN: filename.mk:2: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"\\\"`'`y\"")
+
+       // Covers shAtomDquotBackt: return nil
+       // FIXME: Pkglint must parse unescaped dollar in the same way, everywhere.
+       test(
+               "\"`$|",
+               "WARN: filename.mk:2: Internal pkglint error in ShTokenizer.ShAtom at \"$|\" (quoting=db).",
+               "WARN: filename.mk:2: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"\\\"`$|\"",
+               "WARN: filename.mk:2: Internal pkglint error in MkLine.Tokenize at \"$|\".")
+
+       // Covers shAtomDquotBacktDquot: return nil
+       // FIXME: Pkglint must support unlimited nesting.
+       test(
+               "\"`\"`",
+               "WARN: filename.mk:2: Internal pkglint error in ShTokenizer.ShAtom at \"`\" (quoting=dbd).",
+               "WARN: filename.mk:2: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"\\\"`\\\"`\"")
+
+       // Covers shAtomSubshDquot: return nil
+       test(
+               "$$(\"'",
+               "WARN: filename.mk:2: Invoking subshells via $(...) is not portable enough.")
+
+       // Covers shAtomSubsh: case lexer.AdvanceStr("`")
+       test(
+               "$$(`",
+               "WARN: filename.mk:2: Invoking subshells via $(...) is not portable enough.")
+
+       // Covers shAtomSubshSquot: return nil
+       test(
+               "$$('$)",
+               "WARN: filename.mk:2: Internal pkglint error in ShTokenizer.ShAtom at \"$)\" (quoting=Ss).",
+               "WARN: filename.mk:2: Invoking subshells via $(...) is not portable enough.",
+               "WARN: filename.mk:2: Internal pkglint error in MkLine.Tokenize at \"$)\".")
+
+       // Covers shAtomDquotBackt: case lexer.AdvanceRegexp("^#[^`]*")
+       test(
+               "\"`# comment",
+               "WARN: filename.mk:2: Pkglint ShellLine.CheckShellCommand: splitIntoShellTokens couldn't parse \"\\\"`# comment\"")
 }
 
 // In order to get 100% code coverage for the shell tokenizer, a panic() statement has been
Index: pkgsrc/pkgtools/pkglint/files/shtypes.go
diff -u pkgsrc/pkgtools/pkglint/files/shtypes.go:1.14 pkgsrc/pkgtools/pkglint/files/shtypes.go:1.15
--- pkgsrc/pkgtools/pkglint/files/shtypes.go:1.14       Sat Jan 26 16:31:33 2019
+++ pkgsrc/pkgtools/pkglint/files/shtypes.go    Sat Apr 20 17:43:25 2019
@@ -84,6 +84,7 @@ const (
        shqBacktSquot                       // e.g. `'word'`
        shqSubshDquot                       // e.g. $("word")
        shqSubshSquot                       // e.g. $('word')
+       shqSubshBackt                       // e.g. $(`word`)
        shqDquotBacktDquot                  // e.g. "`"word"`"
        shqDquotBacktSquot                  // e.g. "`'word'`"
 )
@@ -95,7 +96,7 @@ func (q ShQuoting) String() string {
        return [...]string{
                "plain",
                "d", "s", "b", "S",
-               "db", "bd", "bs", "Sd", "Ss",
+               "db", "bd", "bs", "Sd", "Ss", "Sb",
                "dbd", "dbs",
        }[q]
 }

Index: pkgsrc/pkgtools/pkglint/files/testnames_test.go
diff -u pkgsrc/pkgtools/pkglint/files/testnames_test.go:1.4 pkgsrc/pkgtools/pkglint/files/testnames_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/testnames_test.go:1.4 Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/testnames_test.go     Sat Apr 20 17:43:25 2019
@@ -18,6 +18,7 @@ func (s *Suite) Test__test_names(c *chec
                "comparing_YesNo_variable_to_string",
                "enumFrom",
                "enumFromDirs",
+               "enumFromFiles",
                "dquotBacktDquot",
                "and_getSubdirs",
                "SilentAutofixFormat")

Index: pkgsrc/pkgtools/pkglint/files/util.go
diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.41 pkgsrc/pkgtools/pkglint/files/util.go:1.42
--- pkgsrc/pkgtools/pkglint/files/util.go:1.41  Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/util.go       Sat Apr 20 17:43:25 2019
@@ -244,15 +244,15 @@ func tabWidth(s string) int {
 }
 
 func detab(s string) string {
-       detabbed := ""
+       var detabbed strings.Builder
        for _, r := range s {
                if r == '\t' {
-                       detabbed += "        "[:8-len(detabbed)%8]
+                       detabbed.WriteString("        "[:8-detabbed.Len()%8])
                } else {
-                       detabbed += string(r)
+                       detabbed.WriteString(string(r))
                }
        }
-       return detabbed
+       return detabbed.String()
 }
 
 func shorten(s string, maxChars int) string {

Index: pkgsrc/pkgtools/pkglint/files/vardefs.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.58 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.59
--- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.58       Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/vardefs.go    Sat Apr 20 17:43:25 2019
@@ -56,11 +56,11 @@ func (reg *VarTypeRegistry) DefineType(v
        reg.types[varcanon] = vartype
 }
 
-func (reg *VarTypeRegistry) Define(varname string, kindOfList KindOfList, basicType *BasicType, aclEntries ...ACLEntry) {
+func (reg *VarTypeRegistry) Define(varname string, basicType *BasicType, options vartypeOptions, aclEntries ...ACLEntry) {
        m, varbase, varparam := match2(varname, `^([A-Z_.][A-Z0-9_]*|@)(|\*|\.\*)$`)
        G.Assertf(m, "invalid variable name")
 
-       vartype := Vartype{kindOfList, basicType, aclEntries, false}
+       vartype := Vartype{basicType, options, aclEntries}
 
        if varparam == "" || varparam == "*" {
                reg.types[varbase] = &vartype
@@ -81,15 +81,12 @@ func (reg *VarTypeRegistry) Define(varna
 // TODO: To be implemented: when prefixed with "infra:", the entry only
 //  applies to files within the pkgsrc infrastructure. Without this prefix,
 //  the pattern only applies to files outside the pkgsrc infrastructure.
-//
-// FIXME: Force the permissions to always be in the same order:
-//  default, set, append, use, use-loadtime.
-func (reg *VarTypeRegistry) DefineParse(varname string, kindOfList KindOfList, basicType *BasicType, aclEntries ...string) {
+func (reg *VarTypeRegistry) DefineParse(varname string, basicType *BasicType, options vartypeOptions, aclEntries ...string) {
        parsedEntries := reg.parseACLEntries(varname, aclEntries...)
-       reg.Define(varname, kindOfList, basicType, parsedEntries...)
+       reg.Define(varname, basicType, options, parsedEntries...)
 }
 
-// InitVartypes initializes the long list of predefined pkgsrc variables.
+// Init initializes the long list of predefined pkgsrc variables.
 // After this is done, PKGNAME, MAKE_ENV and all the other variables
 // can be used in Makefiles without triggering warnings about typos.
 func (reg *VarTypeRegistry) Init(src *Pkgsrc) {
@@ -102,9 +99,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        //  - how this individual permission set differs
        //  - why the predefined permission set is not good enough
        //  - which packages need this custom permission set.
-       acl := func(varname string, basicType *BasicType, aclEntries ...string) {
-               reg.DefineParse(varname, lkNone, basicType, aclEntries...)
-       }
+       acl := reg.DefineParse
 
        // acllist defines the permissions of a list variable by listing
        // the permissions individually.
@@ -114,20 +109,22 @@ func (reg *VarTypeRegistry) Init(src *Pk
        //  - how this individual permission set differs
        //  - why the predefined permission set is not good enough
        //  - which packages need this custom permission set.
-       acllist := func(varname string, basicType *BasicType, aclEntries ...string) {
-               reg.DefineParse(varname, lkShell, basicType, aclEntries...)
+       acllist := func(varname string, basicType *BasicType, options vartypeOptions, aclEntries ...string) {
+               reg.DefineParse(varname, basicType, options|List, aclEntries...)
        }
 
        // A package-settable variable may be set in all Makefiles except buildlink3.mk and builtin.mk.
        pkg := func(varname string, basicType *BasicType) {
                acl(varname, basicType,
+                       PackageSettable,
                        "buildlink3.mk, builtin.mk: none",
                        "Makefile, Makefile.*, *.mk: default, set, use")
        }
 
        // pkgload is the same as pkg, except that the variable may be accessed at load time.
        pkgload := func(varname string, basicType *BasicType) {
-               reg.DefineParse(varname, lkNone, basicType,
+               reg.DefineParse(varname, basicType,
+                       PackageSettable,
                        "buildlink3.mk: none",
                        "builtin.mk: use, use-loadtime",
                        "Makefile, Makefile.*, *.mk: default, set, use, use-loadtime")
@@ -140,6 +137,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // catch it.
        pkglist := func(varname string, basicType *BasicType) {
                acllist(varname, basicType,
+                       List|PackageSettable,
                        "buildlink3.mk, builtin.mk: none",
                        "Makefile, Makefile.*, *.mk: default, set, append, use")
        }
@@ -157,11 +155,13 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // suffix.
        pkgappend := func(varname string, basicType *BasicType) {
                acl(varname, basicType,
+                       PackageSettable,
                        "buildlink3.mk, builtin.mk: none",
                        "Makefile, Makefile.*, *.mk: default, set, append, use")
        }
        pkgappendbl3 := func(varname string, basicType *BasicType) {
                acl(varname, basicType,
+                       PackageSettable,
                        "Makefile, Makefile.*, *.mk: default, set, append, use")
        }
 
@@ -169,13 +169,15 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // These variables are typically related to compiling and linking files
        // from C and related languages.
        pkgbl3 := func(varname string, basicType *BasicType) {
-               reg.DefineParse(varname, lkNone, basicType,
+               reg.DefineParse(varname, basicType,
+                       PackageSettable,
                        "Makefile, Makefile.*, *.mk: default, set, use")
        }
        // Some package-defined lists may also be modified in buildlink3.mk files,
        // for example platform-specific CFLAGS and LDFLAGS.
        pkglistbl3 := func(varname string, basicType *BasicType) {
-               reg.DefineParse(varname, lkShell, basicType,
+               reg.DefineParse(varname, basicType,
+                       List|PackageSettable,
                        "Makefile, Makefile.*, *.mk: default, set, append, use")
        }
 
@@ -190,17 +192,20 @@ func (reg *VarTypeRegistry) Init(src *Pk
        //  They can be made more precise.
        sys := func(varname string, basicType *BasicType) {
                acl(varname, basicType,
+                       SystemProvided,
                        "buildlink3.mk: none",
                        "*: use")
        }
 
        sysbl3 := func(varname string, basicType *BasicType) {
                acl(varname, basicType,
+                       SystemProvided,
                        "*: use")
        }
 
        syslist := func(varname string, basicType *BasicType) {
                acllist(varname, basicType,
+                       List|SystemProvided,
                        "buildlink3.mk: none",
                        "*: use")
        }
@@ -209,6 +214,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        usr := func(varname string, basicType *BasicType) {
                acl(varname, basicType,
                        // TODO: why is builtin.mk missing here?
+                       UserSettable,
                        "buildlink3.mk: none",
                        "*: use, use-loadtime")
        }
@@ -217,25 +223,29 @@ func (reg *VarTypeRegistry) Init(src *Pk
        usrlist := func(varname string, basicType *BasicType) {
                acllist(varname, basicType,
                        // TODO: why is builtin.mk missing here?
+                       List|UserSettable,
                        "buildlink3.mk: none",
                        "*: use, use-loadtime")
        }
 
        // sysload declares a system-provided variable that may already be used at load time.
        sysload := func(varname string, basicType *BasicType) {
-               reg.DefineParse(varname, lkNone, basicType,
+               reg.DefineParse(varname, basicType,
+                       SystemProvided,
                        "*: use, use-loadtime")
        }
 
        sysloadlist := func(varname string, basicType *BasicType) {
-               reg.DefineParse(varname, lkShell, basicType,
+               reg.DefineParse(varname, basicType,
+                       List|SystemProvided,
                        "*: use, use-loadtime")
        }
 
        // bl3list declares a list variable that is defined by buildlink3.mk and
        // builtin.mk and can later be used by the package.
        bl3list := func(varname string, basicType *BasicType) {
-               reg.DefineParse(varname, lkShell, basicType,
+               reg.DefineParse(varname, basicType,
+                       List, // not PackageSettable since the package uses it more than setting it.
                        "buildlink3.mk, builtin.mk: append",
                        "*: use")
        }
@@ -243,7 +253,8 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // cmdline declares a variable that is defined on the command line. There
        // are only few variables of this type, such as PKG_DEBUG_LEVEL.
        cmdline := func(varname string, basicType *BasicType) {
-               reg.DefineParse(varname, lkNone, basicType,
+               reg.DefineParse(varname, basicType,
+                       CommandLineProvided,
                        "buildlink3.mk, builtin.mk: none",
                        "*: use, use-loadtime")
        }
@@ -251,6 +262,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // Only for infrastructure files; see mk/misc/show.mk
        infralist := func(varname string, basicType *BasicType) {
                acllist(varname, basicType,
+                       List,
                        "*: append")
        }
 
@@ -348,6 +360,25 @@ func (reg *VarTypeRegistry) Init(src *Pk
                return enum(strings.Join(versions, " "))
        }
 
+       // enumFromFiles reads the files from the given base directory,
+       // filtering it through the regular expression and the replacement.
+       //
+       // If no files are found, the allowed values are taken
+       // from defval. This should only happen in the pkglint tests.
+       enumFromFiles := func(basedir string, re regex.Pattern, repl string, defval string) *BasicType {
+               var relevant []string
+               for _, filename := range dirglob(G.Pkgsrc.File(basedir)) {
+                       basename := path.Base(filename)
+                       if matches(basename, re) {
+                               relevant = append(relevant, replaceAll(basename, re, repl))
+                       }
+               }
+               if len(relevant) == 0 {
+                       return enum(defval)
+               }
+               return enum(strings.Join(relevant, " "))
+       }
+
        compilers := enumFrom(
                "mk/compiler.mk",
                "ccache ccc clang distcc f2c gcc hp icc ido mipspro mipspro-ucode pcc sunpro xlc",
@@ -419,8 +450,10 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // X11_TYPE and X11BASE may be used in buildlink3.mk as well, which the
        // standard sysload doesn't allow.
        acl("X11_TYPE", enum("modular native"),
+               UserSettable,
                "*: use, use-loadtime")
        acl("X11BASE", BtPathname,
+               UserSettable,
                "*: use, use-loadtime")
 
        usr("MOTIFBASE", BtPathname)
@@ -483,6 +516,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // TODO: parse all the below information directly from mk/defaults/mk.conf.
        usrpkg := func(varname string, basicType *BasicType) {
                acl(varname, basicType,
+                       PackageSettable|UserSettable,
                        "Makefile: default, set, use, use-loadtime",
                        "buildlink3.mk, builtin.mk: none",
                        "Makefile.*, *.mk: default, set, use, use-loadtime",
@@ -490,6 +524,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        }
        usrpkglist := func(varname string, basicType *BasicType) {
                acllist(varname, basicType,
+                       List|PackageSettable|UserSettable,
                        "Makefile: default, set, use, use-loadtime",
                        "buildlink3.mk, builtin.mk: none",
                        "Makefile.*, *.mk: default, set, use, use-loadtime",
@@ -754,75 +789,99 @@ func (reg *VarTypeRegistry) Init(src *Pk
        syslist("BSD_MAKE_ENV", BtShellWord)
        // TODO: Align the permissions of the various BUILDLINK_*.* variables with each other.
        acllist("BUILDLINK_ABI_DEPENDS.*", BtDependency,
+               PackageSettable,
                "buildlink3.mk, builtin.mk: append, use-loadtime",
                "*: append")
        acllist("BUILDLINK_API_DEPENDS.*", BtDependency,
+               PackageSettable,
                "buildlink3.mk, builtin.mk: append, use-loadtime",
                "*: append")
        acl("BUILDLINK_AUTO_DIRS.*", BtYesNo,
+               PackageSettable,
                "buildlink3.mk: append",
                "Makefile: set")
        syslist("BUILDLINK_CFLAGS", BtCFlag)
        bl3list("BUILDLINK_CFLAGS.*", BtCFlag)
        acl("BUILDLINK_CONTENTS_FILTER.*", BtShellCommand,
+               PackageSettable,
                "buildlink3.mk: set")
        syslist("BUILDLINK_CPPFLAGS", BtCFlag)
        bl3list("BUILDLINK_CPPFLAGS.*", BtCFlag)
        acllist("BUILDLINK_DEPENDS", BtIdentifier,
+               PackageSettable,
                "buildlink3.mk: append")
        acllist("BUILDLINK_DEPMETHOD.*", BtBuildlinkDepmethod,
+               PackageSettable,
                "buildlink3.mk: default, append, use",
                "Makefile, Makefile.*, *.mk: default, set, append")
        acl("BUILDLINK_DIR", BtPathname,
+               PackageSettable,
                "*: use")
        bl3list("BUILDLINK_FILES.*", BtPathmask)
        pkgbl3("BUILDLINK_FILES_CMD.*", BtShellCommand)
        acllist("BUILDLINK_INCDIRS.*", BtPathname,
+               PackageSettable,
                "buildlink3.mk: default, append",
                "Makefile, Makefile.*, *.mk: use")
        acl("BUILDLINK_JAVA_PREFIX.*", BtPathname,
+               PackageSettable,
                "buildlink3.mk: set, use")
        acllist("BUILDLINK_LDADD.*", BtLdFlag,
+               PackageSettable,
                "builtin.mk: default, set, append, use",
                "buildlink3.mk: append, use",
                "Makefile, Makefile.*, *.mk: use")
        acllist("BUILDLINK_LDFLAGS", BtLdFlag,
+               PackageSettable,
                "*: use")
        bl3list("BUILDLINK_LDFLAGS.*", BtLdFlag)
        acllist("BUILDLINK_LIBDIRS.*", BtPathname,
+               PackageSettable,
                "buildlink3.mk, builtin.mk: append",
                "Makefile, Makefile.*, *.mk: use")
        acllist("BUILDLINK_LIBS.*", BtLdFlag,
+               PackageSettable,
                "buildlink3.mk: append",
                "Makefile, Makefile.*, *.mk: set, append, use")
        acllist("BUILDLINK_PASSTHRU_DIRS", BtPathname,
+               PackageSettable,
                "Makefile, Makefile.*, *.mk: append")
        acllist("BUILDLINK_PASSTHRU_RPATHDIRS", BtPathname,
+               PackageSettable,
                "Makefile, Makefile.*, *.mk: append")
        acl("BUILDLINK_PKGSRCDIR.*", BtRelativePkgDir,
+               PackageSettable,
                "buildlink3.mk: default, use-loadtime")
        acl("BUILDLINK_PREFIX.*", BtPathname,
+               PackageSettable,
                "builtin.mk: set, use",
                "Makefile, Makefile.*, *.mk: use")
        acllist("BUILDLINK_RPATHDIRS.*", BtPathname,
+               PackageSettable,
                "buildlink3.mk: append")
        acllist("BUILDLINK_TARGETS", BtIdentifier,
+               PackageSettable,
                "Makefile, Makefile.*, *.mk: append")
        acl("BUILDLINK_FNAME_TRANSFORM.*", BtSedCommands,
+               PackageSettable,
                "Makefile, buildlink3.mk, builtin.mk, hacks.mk, options.mk: append")
        acllist("BUILDLINK_TRANSFORM", BtWrapperTransform,
+               PackageSettable,
                "*: append")
        acllist("BUILDLINK_TRANSFORM.*", BtWrapperTransform,
+               PackageSettable,
                "*: append")
        acllist("BUILDLINK_TREE", BtIdentifier,
+               PackageSettable,
                "buildlink3.mk: append")
        acl("BUILDLINK_X11_DIR", BtPathname,
+               PackageSettable,
                "*: use")
        acllist("BUILD_DEFS", BtVariableName,
+               PackageSettable,
                "Makefile, Makefile.*, *.mk: append")
        pkglist("BUILD_DEFS_EFFECTS", BtVariableName)
-       acllist("BUILD_DEPENDS", BtDependencyWithPath,
-               "Makefile, Makefile.*, *.mk: append")
+       pkglistbl3("BUILD_DEPENDS", BtDependencyWithPath)
        pkglist("BUILD_DIRS", BtWrksrcSubdirectory)
        pkglist("BUILD_ENV", BtShellWord)
        sys("BUILD_MAKE_CMD", BtShellCommand)
@@ -831,19 +890,25 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkglist("BUILD_TARGET.*", BtIdentifier)
        pkg("BUILD_USES_MSGFMT", BtYes)
        acl("BUILTIN_PKG", BtIdentifier,
+               PackageSettable,
                "builtin.mk: set, use, use-loadtime",
                "Makefile, Makefile.*, *.mk: use, use-loadtime")
        acl("BUILTIN_PKG.*", BtPkgName,
+               PackageSettable,
                "builtin.mk: set, use, use-loadtime")
        pkglistbl3("BUILTIN_FIND_FILES_VAR", BtVariableName)
        pkglistbl3("BUILTIN_FIND_FILES.*", BtPathname)
        acl("BUILTIN_FIND_GREP.*", BtUnknown,
+               PackageSettable,
                "builtin.mk: set")
        acllist("BUILTIN_FIND_HEADERS_VAR", BtVariableName,
+               PackageSettable,
                "builtin.mk: set")
        acllist("BUILTIN_FIND_HEADERS.*", BtPathname,
+               PackageSettable,
                "builtin.mk: set")
        acllist("BUILTIN_FIND_LIBS", BtPathname,
+               PackageSettable,
                "builtin.mk: set")
        sys("BUILTIN_X11_TYPE", BtUnknown)
        sys("BUILTIN_X11_VERSION", BtUnknown)
@@ -853,9 +918,11 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkglistbl3("CFLAGS", BtCFlag)   // may also be changed by the user
        pkglistbl3("CFLAGS.*", BtCFlag) // may also be changed by the user
        acl("CHECK_BUILTIN", BtYesNo,
+               PackageSettable,
                "builtin.mk: default",
                "Makefile: set")
        acl("CHECK_BUILTIN.*", BtYesNo,
+               PackageSettable,
                "Makefile, options.mk, buildlink3.mk: set",
                "builtin.mk: default, use-loadtime",
                "*: use-loadtime")
@@ -942,6 +1009,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkg("DLOPEN_REQUIRE_PTHREADS", BtYesNo)
        pkg("DL_AUTO_VARS", BtYes)
        acllist("DL_LIBS", BtLdFlag,
+               PackageSettable,
                "*: append, use")
        sys("DOCOWN", BtUserGroupName)
        sys("DOCGRP", BtUserGroupName)
@@ -1031,6 +1099,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // GNU_CONFIGURE needs to be tested in some buildlink3.mk files,
        // such as lang/vala.
        acl("GNU_CONFIGURE", BtYes,
+               PackageSettable,
                "buildlink3.mk: none",
                "builtin.mk: use, use-loadtime",
                "Makefile, Makefile.*, *.mk: default, set, use, use-loadtime")
@@ -1045,6 +1114,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkg("HOMEPAGE", BtHomepage)
        pkg("ICON_THEMES", BtYes)
        acl("IGNORE_PKG.*", BtYes,
+               PackageSettable,
                "*: set, use-loadtime")
        sys("IMAKE", BtShellCommand)
        pkglistbl3("INCOMPAT_CURSES", BtMachinePlatformPattern)
@@ -1076,6 +1146,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkgload("INSTALL_UNSTRIPPED", BtYesNo)
        pkglist("INTERACTIVE_STAGE", enum("fetch extract configure build test install"))
        acl("IS_BUILTIN.*", BtYesNoIndirectly,
+               PackageSettable,
                // These two differ from the standard,
                // they are needed for devel/ncursesw.
                "buildlink3.mk: use, use-loadtime",
@@ -1208,7 +1279,8 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkglist("ONLY_FOR_COMPILER", compilers)
        pkglist("ONLY_FOR_PLATFORM", BtMachinePlatformPattern)
        pkg("ONLY_FOR_UNPRIVILEGED", BtYesNo)
-       sysload("OPSYS", BtIdentifier)
+       sysload("OPSYS", enumFromFiles("mk/platform", `(.*)\.mk$`, "$1",
+               "Cygwin DragonFly FreeBSD Linux NetBSD SunOS"))
        pkglistbl3("OPSYSVARS", BtVariableName)
        pkg("OSVERSION_SPECIFIC", BtYes)
        sysload("OS_VERSION", BtVersion)
@@ -1228,7 +1300,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkg("PATCH_DIST_STRIP*", BtShellWord)
        pkglist("PATCH_SITES", BtFetchURL)
        pkg("PATCH_STRIP", BtShellWord)
-       sys("PATH", BtPathlist)       // From the PATH environment variable.
+       sysload("PATH", BtPathlist)   // From the PATH environment variable.
        sys("PAXCTL", BtShellCommand) // See mk/pax.mk.
        pkglist("PERL5_PACKLIST", BtPerl5Packlist)
        pkg("PERL5_PACKLIST_DIR", BtPathname)
@@ -1252,6 +1324,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkg("PERL5_USE_PACKLIST", BtYesNo)
        sys("PGSQL_PREFIX", BtPathname)
        acllist("PGSQL_VERSIONS_ACCEPTED", pgsqlVersions,
+               PackageSettable,
                // The "set" is necessary for databases/postgresql-postgis2.
                "Makefile, Makefile.*, *.mk: default, set, append, use")
        usr("PGSQL_VERSION_DEFAULT", BtVersion)
@@ -1263,11 +1336,13 @@ func (reg *VarTypeRegistry) Init(src *Pk
        usr("PHP_VERSION_REQD", BtVersion)
        acl("PHP_PKG_PREFIX",
                enumFromDirs("lang", `^php(\d+)$`, "php$1", "php56 php71 php72 php73"),
+               SystemProvided,
                "special:phpversion.mk: set",
                "*: use, use-loadtime")
        sys("PKGBASE", BtIdentifier)
        // Despite its name, this is actually a list of filenames.
        acllist("PKGCONFIG_FILE.*", BtPathname,
+               PackageSettable,
                "builtin.mk: set, append",
                "special:pkgconfig-builtin.mk: use-loadtime")
        pkglist("PKGCONFIG_OVERRIDE", BtPathmask)
@@ -1283,18 +1358,20 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // be set in a package Makefile.
        // See VartypeCheck.PkgRevision for details.
        acl("PKGREVISION", BtPkgRevision,
+               PackageSettable,
                "Makefile: set")
        sys("PKGSRCDIR", BtPathname)
        // This definition is only valid in the top-level Makefile,
        // not in category or package Makefiles.
        acl("PKGSRCTOP", BtYes,
+               PackageSettable,
                "Makefile: set")
        sys("PKGSRC_SETENV", BtShellCommand)
        syslist("PKGTOOLS_ENV", BtShellWord)
        sys("PKGVERSION", BtVersion)
        sys("PKGVERSION_NOREV", BtVersion) // Without the nb* part.
        sys("PKGWILDCARD", BtFileMask)
-       sys("PKG_ADMIN", BtShellCommand)
+       sysload("PKG_ADMIN", BtShellCommand)
        sys("PKG_APACHE", enum("apache24"))
        pkglist("PKG_APACHE_ACCEPTED", enum("apache24"))
        usr("PKG_APACHE_DEFAULT", enum("apache24"))
@@ -1322,6 +1399,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        //
        // TODO: Is it possible to include hacks.mk files from the dependencies?
        acllist("PKG_HACKS", BtIdentifier,
+               PackageSettable,
                "hacks.mk: append")
        sys("PKG_INFO", BtShellCommand)
        sys("PKG_JAVA_HOME", BtPathname)
@@ -1362,6 +1440,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // The special exception for buildlink3.mk is only here because
        // of textproc/xmlcatmgr.
        acl("PKG_SYSCONFDIR*", BtPathname,
+               PackageSettable,
                "Makefile: set, use, use-loadtime",
                "buildlink3.mk, builtin.mk: use-loadtime",
                "Makefile.*, *.mk: default, set, use, use-loadtime")
@@ -1381,11 +1460,13 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkglistbl3("PREPEND_PATH", BtPathname)
 
        acl("PREFIX", BtPathname,
+               UserSettable,
                "*: use")
        // BtPathname instead of BtPkgPath since the original package doesn't exist anymore.
        // It would be more precise to check for a PkgPath that doesn't exist anymore.
        pkg("PREV_PKGPATH", BtPathname)
        acl("PRINT_PLIST_AWK", BtAwkCommand,
+               PackageSettable,
                "*: append")
        pkglist("PRIVILEGED_STAGES", enum("build install package clean"))
        pkgbl3("PTHREAD_AUTO_VARS", BtYesNo)
@@ -1397,6 +1478,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkg("PY_PATCHPLIST", BtYes)
        acl("PYPKGPREFIX",
                enumFromDirs("lang", `^python(\d+)$`, "py$1", "py27 py36"),
+               SystemProvided,
                "special:pyversion.mk: set",
                "*: use, use-loadtime")
        // See lang/python/pyversion.mk
@@ -1446,11 +1528,13 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkglist("RPMIGNOREPATH", BtPathmask)
        acl("RUBY_BASE",
                enumFromDirs("lang", `^ruby(\d+)$`, "ruby$1", "ruby22 ruby23 ruby24 ruby25"),
+               SystemProvided,
                "special:rubyversion.mk: set",
                "*: use, use-loadtime")
        usr("RUBY_VERSION_REQD", BtVersion)
        acl("RUBY_PKGPREFIX",
                enumFromDirs("lang", `^ruby(\d+)$`, "ruby$1", "ruby22 ruby23 ruby24 ruby25"),
+               SystemProvided,
                "special:rubyversion.mk: default, set, use",
                "*: use, use-loadtime")
        sys("RUN", BtShellCommand)
@@ -1479,8 +1563,10 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkglist("SPECIAL_PERMS", BtPerms)
        sys("STEP_MSG", BtShellCommand)
        sys("STRIP", BtShellCommand) // see mk/tools/strip.mk
+
        // Only valid in the top-level and the category Makefiles.
        acllist("SUBDIR", BtFileName,
+               PackageSettable,
                "Makefile: append")
 
        pkglistbl3("SUBST_CLASSES", BtIdentifier)
@@ -1519,12 +1605,15 @@ func (reg *VarTypeRegistry) Init(src *Pk
        pkg("USERGROUP_PHASE", enum("configure build pre-install"))
        usrlist("USER_ADDITIONAL_PKGS", BtPkgPath)
        pkg("USE_BSD_MAKEFILE", BtYes)
+
        // USE_BUILTIN.* is usually set by the builtin.mk file, after checking
        // whether the package is available in the base system. To override
        // this check, a package may set this variable before including the
        // corresponding buildlink3.mk file.
        acl("USE_BUILTIN.*", BtYesNoIndirectly,
+               PackageSettable,
                "Makefile, Makefile.*, *.mk: set, use, use-loadtime")
+
        pkg("USE_CMAKE", BtYes)
        usr("USE_DESTDIR", BtYes)
        pkglist("USE_FEATURES", BtIdentifier)
@@ -1561,11 +1650,13 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // The use-loadtime is only for devel/ncurses/Makefile.common, which
        // removes tbl from USE_TOOLS.
        acllist("USE_TOOLS", BtTool,
+               PackageSettable,
                "special:Makefile.common: set, append, use, use-loadtime",
                "buildlink3.mk: append",
                "builtin.mk: append, use-loadtime",
                "*: set, append, use")
        acllist("USE_TOOLS.*", BtTool, // OPSYS-specific
+               PackageSettable,
                "buildlink3.mk, builtin.mk: append",
                "*: set, append, use")
 

Index: pkgsrc/pkgtools/pkglint/files/vardefs_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.12 pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.13
--- pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.12  Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/vardefs_test.go       Sat Apr 20 17:43:25 2019
@@ -52,11 +52,11 @@ func (s *Suite) Test_VarTypeRegistry_Ini
                c.Check(vartype, equals, values)
        }
 
-       test("EMACS_VERSIONS_ACCEPTED", "List of enum: emacs29 emacs31 ")
-       test("PKG_JVM", "enum: jdk16 openjdk7 openjdk8 oracle-jdk8 sun-jdk6 sun-jdk7 ")
-       test("USE_LANGUAGES", "List of enum: ada c c++ c++03 c++0x c++11 c++14 c99 "+
-               "fortran fortran77 gnu++03 gnu++0x gnu++11 gnu++14 java obj-c++ objc ")
-       test("PKGSRC_COMPILER", "List of enum: ccache distcc f2c g95 gcc ido mipspro-ucode sunpro ")
+       test("EMACS_VERSIONS_ACCEPTED", "enum: emacs29 emacs31  (list, package-settable)")
+       test("PKG_JVM", "enum: jdk16 openjdk7 openjdk8 oracle-jdk8 sun-jdk6 sun-jdk7  (system-provided)")
+       test("USE_LANGUAGES", "enum: ada c c++ c++03 c++0x c++11 c++14 c99 "+
+               "fortran fortran77 gnu++03 gnu++0x gnu++11 gnu++14 java obj-c++ objc  (list, package-settable)")
+       test("PKGSRC_COMPILER", "enum: ccache distcc f2c g95 gcc ido mipspro-ucode sunpro  (list, user-settable)")
 }
 
 func (s *Suite) Test_VarTypeRegistry_Init__enumFromDirs(c *check.C) {
@@ -74,7 +74,25 @@ func (s *Suite) Test_VarTypeRegistry_Ini
                c.Check(vartype, equals, values)
        }
 
-       test("PYPKGPREFIX", "enum: py28 py33 ")
+       test("PYPKGPREFIX", "enum: py28 py33  (system-provided)")
+}
+
+func (s *Suite) Test_VarTypeRegistry_Init__enumFromFiles(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("mk/platform/NetBSD.mk")
+       t.CreateFileLines("mk/platform/README")
+       t.CreateFileLines("mk/platform/SunOS.mk")
+       t.CreateFileLines("mk/platform/SunOS.mk~")
+
+       t.SetUpVartypes()
+
+       test := func(varname, values string) {
+               vartype := G.Pkgsrc.VariableType(nil, varname).String()
+               c.Check(vartype, equals, values)
+       }
+
+       test("OPSYS", "enum: NetBSD SunOS  (system-provided)")
 }
 
 func (s *Suite) Test_VarTypeRegistry_parseACLEntries__invalid_arguments(c *check.C) {
@@ -120,6 +138,7 @@ func (s *Suite) Test_VarTypeRegistry_Ini
 
        pkg := t.SetUpPackage("category/package",
                "BROKEN_ON_PLATFORM=\t${LP64PLATFORMS}")
+       t.FinishSetUp()
 
        G.Check(pkg)
 

Index: pkgsrc/pkgtools/pkglint/files/vartype_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.17 pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.17  Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/vartype_test.go       Sat Apr 20 17:43:25 2019
@@ -28,7 +28,7 @@ func (s *Suite) Test_Vartype_Alternative
        // test generates the files description for the "set" permission.
        test := func(rules []string, alternatives string) {
                aclEntries := (*VarTypeRegistry).parseACLEntries(nil, "", rules...)
-               vartype := Vartype{lkNone, BtYesNo, aclEntries, false}
+               vartype := Vartype{BtYesNo, NoVartypeOptions, aclEntries}
 
                alternativeFiles := vartype.AlternativeFiles(aclpSet)
 
@@ -116,6 +116,15 @@ func (s *Suite) Test_Vartype_Alternative
                "builtin.mk, but not buildlink3.mk, Makefile or *.mk")
 }
 
+func (s *Suite) Test_Vartype_String(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       vartype := G.Pkgsrc.VariableType(nil, "PKG_DEBUG_LEVEL")
+       t.Check(vartype.String(), equals, "Integer (command-line-provided)")
+}
+
 func (s *Suite) Test_BasicType_HasEnum(c *check.C) {
        vc := enum("start middle end")
 
@@ -151,13 +160,13 @@ func (s *Suite) Test_ACLPermissions_Huma
                equals, "set, given a default value, appended to, used at load time, or used")
 }
 
-func (s *Suite) Test_Vartype_IsConsideredList(c *check.C) {
+func (s *Suite) Test_Vartype_MayBeAppendedTo(c *check.C) {
        t := s.Init(c)
 
        t.SetUpVartypes()
 
-       c.Check(G.Pkgsrc.VariableType(nil, "COMMENT").IsConsideredList(), equals, false)
-       c.Check(G.Pkgsrc.VariableType(nil, "DEPENDS").IsConsideredList(), equals, true)
-       c.Check(G.Pkgsrc.VariableType(nil, "PKG_FAIL_REASON").IsConsideredList(), equals, true)
-       c.Check(G.Pkgsrc.VariableType(nil, "CONF_FILES").IsConsideredList(), equals, true)
+       c.Check(G.Pkgsrc.VariableType(nil, "COMMENT").MayBeAppendedTo(), equals, true)
+       c.Check(G.Pkgsrc.VariableType(nil, "DEPENDS").MayBeAppendedTo(), equals, true)
+       c.Check(G.Pkgsrc.VariableType(nil, "PKG_FAIL_REASON").MayBeAppendedTo(), equals, true)
+       c.Check(G.Pkgsrc.VariableType(nil, "CONF_FILES").MayBeAppendedTo(), equals, true)
 }

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.53 pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.54
--- pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.53  Wed Apr  3 21:49:51 2019
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck.go       Sat Apr 20 17:43:25 2019
@@ -238,7 +238,7 @@ func (cv *VartypeCheck) Comment() {
                pkgbase := G.Pkg.EffectivePkgbase
                if hasPrefix(strings.ToLower(value), strings.ToLower(pkgbase+" ")) {
                        cv.Warnf("COMMENT should not start with the package name.")
-                       G.Explain(
+                       cv.Explain(
                                "The COMMENT is usually displayed together with the package name.",
                                "Therefore it does not need to repeat the package name but should",
                                "provide additional information instead.")
@@ -251,7 +251,7 @@ func (cv *VartypeCheck) Comment() {
 
        if m, isA := match1(value, `\b(is an?)\b`); m {
                cv.Warnf("COMMENT should not contain %q.", isA)
-               G.Explain(
+               cv.Explain(
                        "The words \"package is a\" are redundant.",
                        "Since every package comment could start with them,",
                        "it is better to remove this redundancy in all cases.")
@@ -286,7 +286,7 @@ func (cv *VartypeCheck) ConfFiles() {
 
                if i%2 == 1 && !hasPrefix(word, "${") {
                        cv.Warnf("The destination file %q should start with a variable reference.", word)
-                       G.Explain(
+                       cv.Explain(
                                "Since pkgsrc can be installed in different locations, the",
                                "configuration files will also end up in different locations.",
                                "Typical variables that are used for configuration files are",
@@ -304,7 +304,7 @@ func (cv *VartypeCheck) Dependency() {
 
        if deppat != nil && deppat.Wildcard == "" && (rest == "{,nb*}" || rest == "{,nb[0-9]*}") {
                cv.Warnf("Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.")
-               G.Explain(
+               cv.Explain(
                        "The \"{,nb*}\" extension is only necessary for dependencies of the",
                        "form \"pkgbase-1.2\", since the pattern \"pkgbase-1.2\" doesn't match",
                        "the version \"pkgbase-1.2nb5\".",
@@ -318,7 +318,7 @@ func (cv *VartypeCheck) Dependency() {
 
        } else if deppat == nil || rest != "" {
                cv.Warnf("Invalid dependency pattern %q.", value)
-               G.Explain(
+               cv.Explain(
                        "Typical dependencies have the following forms:",
                        "",
                        "\tpackage>=2.5",
@@ -332,7 +332,7 @@ func (cv *VartypeCheck) Dependency() {
        if m, inside := match1(wildcard, `^\[(.*)\]\*$`); m {
                if inside != "0-9" {
                        cv.Warnf("Only [0-9]* is allowed in the numeric part of a dependency.")
-                       G.Explain(
+                       cv.Explain(
                                "The pattern -[0-9] means any version.",
                                "All other version patterns should be expressed using",
                                "the comparison operators like < or >= or even >=2<3.",
@@ -345,21 +345,21 @@ func (cv *VartypeCheck) Dependency() {
        } else if m, ver, suffix := match2(wildcard, `^(\d\w*(?:\.\w+)*)(\.\*|\{,nb\*\}|\{,nb\[0-9\]\*\}|\*|)$`); m {
                if suffix == "" {
                        cv.Warnf("Please use %q instead of %q as the version pattern.", ver+"{,nb*}", ver)
-                       G.Explain(
+                       cv.Explain(
                                "Without the \"{,nb*}\" suffix, this version pattern only matches",
                                "package versions that don't have a PKGREVISION (which is the part",
                                "after the \"nb\").")
                }
                if suffix == "*" {
                        cv.Warnf("Please use %q instead of %q as the version pattern.", ver+".*", ver+"*")
-                       G.Explain(
+                       cv.Explain(
                                "For example, the version \"1*\" also matches \"10.0.0\", which is",
                                "probably not intended.")
                }
 
        } else if wildcard == "*" {
                cv.Warnf("Please use \"%[1]s-[0-9]*\" instead of \"%[1]s-*\".", deppat.Pkgbase)
-               G.Explain(
+               cv.Explain(
                        "If you use a * alone, the package specification may match other",
                        "packages that have the same prefix but a longer name.",
                        "For example, foo-* matches foo-1.2 but also",
@@ -369,7 +369,7 @@ func (cv *VartypeCheck) Dependency() {
        withoutCharClasses := replaceAll(wildcard, `\[[\d-]+\]`, "")
        if contains(withoutCharClasses, "-") {
                cv.Warnf("The version pattern %q should not contain a hyphen.", wildcard)
-               G.Explain(
+               cv.Explain(
                        "Pkgsrc interprets package names with version numbers like this:",
                        "",
                        "\t\"foo-2.0-2.1.x\" => pkgbase \"foo\", version \"2.0-2.1.x\"",
@@ -418,7 +418,7 @@ func (cv *VartypeCheck) DependencyWithPa
        }
 
        cv.Warnf("Invalid dependency pattern with path %q.", value)
-       G.Explain(
+       cv.Explain(
                "Examples for valid dependency patterns with path are:",
                "  package-[0-9]*:../../category/package",
                "  package>=3.41:../../category/package",
@@ -451,7 +451,7 @@ func (cv *VartypeCheck) EmulPlatform() {
                enumEmulArch.checker(archCv)
        } else {
                cv.Warnf("%q is not a valid emulation platform.", cv.Value)
-               G.Explain(
+               cv.Explain(
                        "An emulation platform has the form <OPSYS>-<MACHINE_ARCH>.",
                        "OPSYS is the lower-case name of the operating system, and",
                        "MACHINE_ARCH is the hardware architecture.",
@@ -706,7 +706,7 @@ func (cv *VartypeCheck) MachineGnuPlatfo
 
        } else {
                cv.Warnf("%q is not a valid platform pattern.", cv.Value)
-               G.Explain(
+               cv.Explain(
                        "A platform pattern has the form <OPSYS>-<OS_VERSION>-<MACHINE_ARCH>.",
                        "Each of these components may use wildcards.",
                        "",
@@ -745,7 +745,7 @@ func (cv *VartypeCheck) Message() {
 
        if matches(value, `^[\"'].*[\"']$`) {
                cv.Warnf("%s should not be quoted.", varname)
-               G.Explain(
+               cv.Explain(
                        "The quoting is only needed for variables which are interpreted as",
                        "multiple words (or, generally speaking, a list of something).",
                        "A single text message does not belong to this class,",
@@ -769,7 +769,7 @@ func (cv *VartypeCheck) Option() {
                // There's a difference between empty and absent here.
                if _, found := G.Pkgsrc.PkgOptions[optname]; !found {
                        cv.Warnf("Unknown option %q.", optname)
-                       G.Explain(
+                       cv.Explain(
                                "This option is not documented in the mk/defaults/options.description file.",
                                "Please think of a brief but precise description and either",
                                "update that file yourself or suggest a description for this option",
@@ -806,7 +806,7 @@ func (cv *VartypeCheck) Pathlist() {
 
                if !hasPrefix(dir, "/") && !hasPrefix(dir, "${") {
                        cv.Errorf("The component %q of %s must be an absolute path.", dir, cv.Varname)
-                       G.Explain(
+                       cv.Explain(
                                "Relative paths in the PATH variable are a security risk.",
                                "They also make the execution unreliable since they are",
                                "evaluated relative to the current directory of the process,",
@@ -864,7 +864,7 @@ func (cv *VartypeCheck) Pkgname() {
 
        if cv.Op != opUseMatch && value == cv.ValueNoVar && !matches(value, rePkgname) {
                cv.Warnf("%q is not a valid package name.", value)
-               G.Explain(
+               cv.Explain(
                        "A valid package name has the form packagename-version, where version",
                        "consists only of digits, letters and dots.")
        }
@@ -876,7 +876,7 @@ func (cv *VartypeCheck) PkgOptionsVar() 
        // TODO: Replace regex with proper VarUse.
        if matches(cv.Value, `\$\{PKGBASE[:\}]`) {
                cv.Errorf("PKGBASE must not be used in PKG_OPTIONS_VAR.")
-               G.Explain(
+               cv.Explain(
                        "PKGBASE is defined in bsd.pkg.mk, which is included as the",
                        "very last file, but PKG_OPTIONS_VAR is evaluated earlier.",
                        "Use ${PKGNAME:C/-[0-9].*//} instead.")
@@ -902,7 +902,7 @@ func (cv *VartypeCheck) PkgRevision() {
        }
        if cv.MkLine.Basename != "Makefile" {
                cv.Errorf("%s only makes sense directly in the package Makefile.", cv.Varname)
-               G.Explain(
+               cv.Explain(
                        "Usually, different packages using the same Makefile.common have",
                        "different dependencies and will be bumped at different times (e.g.",
                        "for shlib major bumps) and thus the PKGREVISIONs must be in the",
@@ -941,7 +941,7 @@ func (cv *VartypeCheck) MachinePlatformP
 
        } else {
                cv.Warnf("%q is not a valid platform pattern.", cv.Value)
-               G.Explain(
+               cv.Explain(
                        "A platform pattern has the form <OPSYS>-<OS_VERSION>-<MACHINE_ARCH>.",
                        "Each of these components may be a shell globbing expression.",
                        "",
@@ -968,7 +968,7 @@ func (cv *VartypeCheck) PythonDependency
                cv.Warnf("Python dependencies should not contain variables.")
        } else if !matches(cv.ValueNoVar, `^[+\-.0-9A-Z_a-z]+(?:|:link|:build)$`) {
                cv.Warnf("Invalid Python dependency %q.", cv.Value)
-               G.Explain(
+               cv.Explain(
                        "Python dependencies must be an identifier for a package, as",
                        "specified in lang/python/versioned_dependencies.mk.",
                        "This identifier may be followed by :build for a build-time only",
@@ -992,7 +992,7 @@ func (cv *VartypeCheck) RelativePkgPath(
 func (cv *VartypeCheck) Restricted() {
        if cv.Value != "${RESTRICTED}" {
                cv.Warnf("The only valid value for %s is ${RESTRICTED}.", cv.Varname)
-               G.Explain(
+               cv.Explain(
                        "These variables are used to control which files may be mirrored on",
                        "FTP servers or CD-ROM collections.",
                        "They are not intended to mark",
@@ -1005,7 +1005,7 @@ func (cv *VartypeCheck) SedCommands() {
        if rest != "" {
                if contains(cv.MkLine.Text, "#") {
                        cv.Errorf("Invalid shell words %q in sed commands.", rest)
-                       G.Explain(
+                       cv.Explain(
                                "When sed commands have embedded \"#\" characters, they need to be",
                                "escaped with a backslash, otherwise make(1) will interpret them as a",
                                "comment, even if they occur in single or double quotes or whatever.")
@@ -1027,7 +1027,7 @@ func (cv *VartypeCheck) SedCommands() {
                                ncommands++
                                if ncommands > 1 {
                                        cv.Notef("Each sed command should appear in an assignment of its own.")
-                                       G.Explain(
+                                       cv.Explain(
                                                "For example, instead of",
                                                "    SUBST_SED.foo+=        -e s,command1,, -e s,command2,,",
                                                "use",
@@ -1059,16 +1059,22 @@ func (cv *VartypeCheck) ShellCommand() {
                return
        }
        setE := true
-       NewShellLineChecker(cv.MkLines, cv.MkLine).CheckShellCommand(cv.Value, &setE, RunTime)
+       ck := NewShellLineChecker(cv.MkLines, cv.MkLine)
+       ck.checkVarUse = false
+       ck.CheckShellCommand(cv.Value, &setE, RunTime)
 }
 
 // ShellCommands checks for zero or more shell commands, each terminated with a semicolon.
 func (cv *VartypeCheck) ShellCommands() {
-       NewShellLineChecker(cv.MkLines, cv.MkLine).CheckShellCommands(cv.Value, RunTime)
+       ck := NewShellLineChecker(cv.MkLines, cv.MkLine)
+       ck.checkVarUse = false
+       ck.CheckShellCommands(cv.Value, RunTime)
 }
 
 func (cv *VartypeCheck) ShellWord() {
-       NewShellLineChecker(cv.MkLines, cv.MkLine).CheckWord(cv.Value, true, RunTime)
+       ck := NewShellLineChecker(cv.MkLines, cv.MkLine)
+       ck.checkVarUse = false
+       ck.CheckWord(cv.Value, true, RunTime)
 }
 
 func (cv *VartypeCheck) Stage() {
@@ -1097,7 +1103,7 @@ func (cv *VartypeCheck) Tool() {
 
        } else if cv.Op != opUseMatch && cv.Value == cv.ValueNoVar {
                cv.Errorf("Invalid tool dependency %q.", cv.Value)
-               G.Explain(
+               cv.Explain(
                        "A tool dependency typically looks like \"sed\" or \"sed:run\".")
        }
 }
@@ -1156,7 +1162,7 @@ func (cv *VartypeCheck) UserGroupName() 
 func (cv *VartypeCheck) VariableName() {
        if cv.Value == cv.ValueNoVar && !matches(cv.Value, `^[A-Z_][0-9A-Z_]*(?:[.].*)?$`) {
                cv.Warnf("%q is not a valid variable name.", cv.Value)
-               G.Explain(
+               cv.Explain(
                        "Variable names are restricted to only uppercase letters and the",
                        "underscore in the basename, and arbitrary characters in the",
                        "parameterized part, following the dot.",
@@ -1181,7 +1187,7 @@ func (cv *VartypeCheck) Version() {
                if m, ver, suffix := match2(value, `^(`+digit+alnum+`*(?:\.`+alnum+`+)*)(\.\*|\*|)$`); m {
                        if suffix == "*" && ver != "[0-9]" {
                                cv.Warnf("Please use %q instead of %q as the version pattern.", ver+".*", ver+"*")
-                               G.Explain(
+                               cv.Explain(
                                        "For example, the version \"1*\" also matches \"10.0.0\", which is",
                                        "probably not intended.")
                        }
@@ -1259,7 +1265,7 @@ func (cv *VartypeCheck) Yes() {
        switch cv.Op {
        case opUseMatch:
                cv.Warnf("%s should only be used in a \".if defined(...)\" condition.", cv.Varname)
-               G.Explain(
+               cv.Explain(
                        "This variable can have only two values: defined or undefined.",
                        "When it is defined, it means \"yes\", even when its value is",
                        "\"no\" or the empty string.",
@@ -1270,7 +1276,7 @@ func (cv *VartypeCheck) Yes() {
        default:
                if !matches(cv.Value, `^(?:YES|yes)(?:[\t ]+#.*)?$`) {
                        cv.Warnf("%s should be set to YES or yes.", cv.Varname)
-                       G.Explain(
+                       cv.Explain(
                                "This variable means \"yes\" if it is defined, and \"no\" if it is undefined.",
                                "Even when it has the value \"no\", this means \"yes\".",
                                "Therefore when it is defined, its value should correspond to its meaning.")
@@ -1301,7 +1307,7 @@ func (cv *VartypeCheck) YesNo() {
                }
        } else if cv.Op == opUseCompare {
                cv.Warnf("%s should be matched against %q or %q, not compared with %q.", cv.Varname, yes1, no1, cv.Value)
-               G.Explain(
+               cv.Explain(
                        "The yes/no value can be written in either upper or lower case, and",
                        "both forms are actually used.",
                        "As long as this is the case, when checking the variable value,",

Index: pkgsrc/pkgtools/pkglint/files/trace/tracing.go
diff -u pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.7 pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.8
--- pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.7  Sun Mar 10 19:01:51 2019
+++ pkgsrc/pkgtools/pkglint/files/trace/tracing.go      Sat Apr 20 17:43:25 2019
@@ -87,18 +87,18 @@ func isNil(a interface{}) bool {
 }
 
 func argsStr(args []interface{}) string {
-       rv := ""
+       var rv strings.Builder
        for _, arg := range args {
-               if rv != "" {
-                       rv += ", "
+               if rv.Len() > 0 {
+                       rv.WriteString(", ")
                }
                if str, ok := arg.(fmt.Stringer); ok && !isNil(str) {
-                       rv += str.String()
+                       rv.WriteString(str.String())
                } else {
-                       rv += fmt.Sprintf("%#v", arg)
+                       _, _ = fmt.Fprintf(&rv, "%#v", arg)
                }
        }
-       return rv
+       return rv.String()
 }
 
 func (t *Tracer) traceIndent() string {



Home | Main Index | Thread Index | Old Index