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:           Sun Dec  8 00:06:38 UTC 2019

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile PLIST
        pkgsrc/pkgtools/pkglint/files: alternatives.go alternatives_test.go
            autofix.go autofix_test.go buildlink3.go buildlink3_test.go
            category.go check_test.go distinfo.go files_test.go licenses.go
            licenses_test.go line.go logging.go mklexer.go mklexer_test.go
            mkline.go mkline_test.go mklinechecker.go mklinechecker_test.go
            mklineparser.go mklineparser_test.go mklines.go mklines_test.go
            mkparser.go mkparser_test.go mktypes.go mktypes_test.go
            options_test.go package.go package_test.go patches.go
            patches_test.go path.go path_test.go pkglint.1 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 substcontext.go tools.go
            tools_test.go toplevel.go util.go util_test.go var.go
            varalignblock.go vardefs.go vardefs_test.go vargroups.go
            vargroups_test.go vartype.go vartype_test.go vartypecheck.go
            vartypecheck_test.go
        pkgsrc/pkgtools/pkglint/files/intqa: ideas.go qa.go qa_test.go
Added Files:
        pkgsrc/pkgtools/pkglint/files: lineslexer.go lineslexer_test.go
            mkassignchecker.go mkassignchecker_test.go mkcondchecker.go
            mkcondchecker_test.go mkvarusechecker.go mkvarusechecker_test.go
Removed Files:
        pkgsrc/pkgtools/pkglint/files: linelexer.go linelexer_test.go

Log Message:
pkgtools/pkglint: update to 19.3.14

Changes since 19.3.13:

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

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

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

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

Variables named *_AWK may be appended to.

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

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

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

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

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

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


To generate a diff of this commit:
cvs rdiff -u -r1.613 -r1.614 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.19 -r1.20 pkgsrc/pkgtools/pkglint/PLIST
cvs rdiff -u -r1.18 -r1.19 pkgsrc/pkgtools/pkglint/files/alternatives.go \
    pkgsrc/pkgtools/pkglint/files/mktypes_test.go
cvs rdiff -u -r1.17 -r1.18 pkgsrc/pkgtools/pkglint/files/alternatives_test.go
cvs rdiff -u -r1.33 -r1.34 pkgsrc/pkgtools/pkglint/files/autofix.go \
    pkgsrc/pkgtools/pkglint/files/autofix_test.go
cvs rdiff -u -r1.27 -r1.28 pkgsrc/pkgtools/pkglint/files/buildlink3.go \
    pkgsrc/pkgtools/pkglint/files/category.go
cvs rdiff -u -r1.37 -r1.38 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go
cvs rdiff -u -r1.57 -r1.58 pkgsrc/pkgtools/pkglint/files/check_test.go
cvs rdiff -u -r1.39 -r1.40 pkgsrc/pkgtools/pkglint/files/distinfo.go \
    pkgsrc/pkgtools/pkglint/files/mkparser.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go \
    pkgsrc/pkgtools/pkglint/files/util_test.go
cvs rdiff -u -r1.30 -r1.31 pkgsrc/pkgtools/pkglint/files/files_test.go \
    pkgsrc/pkgtools/pkglint/files/substcontext.go
cvs rdiff -u -r1.29 -r1.30 pkgsrc/pkgtools/pkglint/files/licenses.go
cvs rdiff -u -r1.26 -r1.27 pkgsrc/pkgtools/pkglint/files/licenses_test.go \
    pkgsrc/pkgtools/pkglint/files/toplevel.go \
    pkgsrc/pkgtools/pkglint/files/vardefs_test.go
cvs rdiff -u -r1.41 -r1.42 pkgsrc/pkgtools/pkglint/files/line.go
cvs rdiff -u -r1.8 -r0 pkgsrc/pkgtools/pkglint/files/linelexer.go
cvs rdiff -u -r1.5 -r0 pkgsrc/pkgtools/pkglint/files/linelexer_test.go
cvs rdiff -u -r0 -r1.1 pkgsrc/pkgtools/pkglint/files/lineslexer.go \
    pkgsrc/pkgtools/pkglint/files/lineslexer_test.go \
    pkgsrc/pkgtools/pkglint/files/mkassignchecker.go \
    pkgsrc/pkgtools/pkglint/files/mkassignchecker_test.go \
    pkgsrc/pkgtools/pkglint/files/mkcondchecker.go \
    pkgsrc/pkgtools/pkglint/files/mkcondchecker_test.go \
    pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go \
    pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go
cvs rdiff -u -r1.32 -r1.33 pkgsrc/pkgtools/pkglint/files/logging.go \
    pkgsrc/pkgtools/pkglint/files/patches_test.go
cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/mklexer.go
cvs rdiff -u -r1.2 -r1.3 pkgsrc/pkgtools/pkglint/files/mklexer_test.go
cvs rdiff -u -r1.67 -r1.68 pkgsrc/pkgtools/pkglint/files/mkline.go
cvs rdiff -u -r1.74 -r1.75 pkgsrc/pkgtools/pkglint/files/mkline_test.go
cvs rdiff -u -r1.56 -r1.57 pkgsrc/pkgtools/pkglint/files/mklinechecker.go
cvs rdiff -u -r1.51 -r1.52 \
    pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go \
    pkgsrc/pkgtools/pkglint/files/shell.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/mklineparser.go \
    pkgsrc/pkgtools/pkglint/files/var.go
cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/mklineparser_test.go
cvs rdiff -u -r1.62 -r1.63 pkgsrc/pkgtools/pkglint/files/mklines.go \
    pkgsrc/pkgtools/pkglint/files/util.go \
    pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
cvs rdiff -u -r1.53 -r1.54 pkgsrc/pkgtools/pkglint/files/mklines_test.go \
    pkgsrc/pkgtools/pkglint/files/pkglint_test.go
cvs rdiff -u -r1.36 -r1.37 pkgsrc/pkgtools/pkglint/files/mkparser_test.go
cvs rdiff -u -r1.21 -r1.22 pkgsrc/pkgtools/pkglint/files/mktypes.go \
    pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
cvs rdiff -u -r1.20 -r1.21 pkgsrc/pkgtools/pkglint/files/options_test.go \
    pkgsrc/pkgtools/pkglint/files/tools.go
cvs rdiff -u -r1.72 -r1.73 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.61 -r1.62 pkgsrc/pkgtools/pkglint/files/package_test.go
cvs rdiff -u -r1.34 -r1.35 pkgsrc/pkgtools/pkglint/files/patches.go
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/path.go \
    pkgsrc/pkgtools/pkglint/files/path_test.go \
    pkgsrc/pkgtools/pkglint/files/vargroups.go \
    pkgsrc/pkgtools/pkglint/files/vargroups_test.go
cvs rdiff -u -r1.59 -r1.60 pkgsrc/pkgtools/pkglint/files/pkglint.1 \
    pkgsrc/pkgtools/pkglint/files/shell_test.go
cvs rdiff -u -r1.68 -r1.69 pkgsrc/pkgtools/pkglint/files/pkglint.go
cvs rdiff -u -r1.45 -r1.46 pkgsrc/pkgtools/pkglint/files/pkgsrc.go
cvs rdiff -u -r1.46 -r1.47 pkgsrc/pkgtools/pkglint/files/plist.go
cvs rdiff -u -r1.40 -r1.41 pkgsrc/pkgtools/pkglint/files/plist_test.go \
    pkgsrc/pkgtools/pkglint/files/vartype.go
cvs rdiff -u -r1.9 -r1.10 pkgsrc/pkgtools/pkglint/files/redundantscope.go \
    pkgsrc/pkgtools/pkglint/files/redundantscope_test.go
cvs rdiff -u -r1.22 -r1.23 pkgsrc/pkgtools/pkglint/files/shtokenizer.go \
    pkgsrc/pkgtools/pkglint/files/vartype_test.go
cvs rdiff -u -r1.23 -r1.24 pkgsrc/pkgtools/pkglint/files/tools_test.go
cvs rdiff -u -r1.10 -r1.11 pkgsrc/pkgtools/pkglint/files/varalignblock.go
cvs rdiff -u -r1.80 -r1.81 pkgsrc/pkgtools/pkglint/files/vardefs.go
cvs rdiff -u -r1.69 -r1.70 pkgsrc/pkgtools/pkglint/files/vartypecheck.go
cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/intqa/ideas.go
cvs rdiff -u -r1.2 -r1.3 pkgsrc/pkgtools/pkglint/files/intqa/qa.go \
    pkgsrc/pkgtools/pkglint/files/intqa/qa_test.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.613 pkgsrc/pkgtools/pkglint/Makefile:1.614
--- pkgsrc/pkgtools/pkglint/Makefile:1.613      Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/Makefile    Sun Dec  8 00:06:37 2019
@@ -1,6 +1,6 @@
-# $NetBSD: Makefile,v 1.613 2019/12/02 23:32:09 rillig Exp $
+# $NetBSD: Makefile,v 1.614 2019/12/08 00:06:37 rillig Exp $
 
-PKGNAME=       pkglint-19.3.13
+PKGNAME=       pkglint-19.3.14
 CATEGORIES=    pkgtools
 DISTNAME=      tools
 MASTER_SITES=  ${MASTER_SITE_GITHUB:=golang/}

Index: pkgsrc/pkgtools/pkglint/PLIST
diff -u pkgsrc/pkgtools/pkglint/PLIST:1.19 pkgsrc/pkgtools/pkglint/PLIST:1.20
--- pkgsrc/pkgtools/pkglint/PLIST:1.19  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/PLIST       Sun Dec  8 00:06:38 2019
@@ -1,4 +1,4 @@
-@comment $NetBSD: PLIST,v 1.19 2019/12/02 23:32:09 rillig Exp $
+@comment $NetBSD: PLIST,v 1.20 2019/12/08 00:06:38 rillig Exp $
 bin/pkglint
 gopkg/pkg/${GO_PLATFORM}/netbsd.org/pkglint.a
 gopkg/pkg/${GO_PLATFORM}/netbsd.org/pkglint/getopt.a
@@ -43,12 +43,16 @@ gopkg/src/netbsd.org/pkglint/line.go
 gopkg/src/netbsd.org/pkglint/line_test.go
 gopkg/src/netbsd.org/pkglint/linechecker.go
 gopkg/src/netbsd.org/pkglint/linechecker_test.go
-gopkg/src/netbsd.org/pkglint/linelexer.go
-gopkg/src/netbsd.org/pkglint/linelexer_test.go
 gopkg/src/netbsd.org/pkglint/lines.go
 gopkg/src/netbsd.org/pkglint/lines_test.go
+gopkg/src/netbsd.org/pkglint/lineslexer.go
+gopkg/src/netbsd.org/pkglint/lineslexer_test.go
 gopkg/src/netbsd.org/pkglint/logging.go
 gopkg/src/netbsd.org/pkglint/logging_test.go
+gopkg/src/netbsd.org/pkglint/mkassignchecker.go
+gopkg/src/netbsd.org/pkglint/mkassignchecker_test.go
+gopkg/src/netbsd.org/pkglint/mkcondchecker.go
+gopkg/src/netbsd.org/pkglint/mkcondchecker_test.go
 gopkg/src/netbsd.org/pkglint/mklexer.go
 gopkg/src/netbsd.org/pkglint/mklexer_test.go
 gopkg/src/netbsd.org/pkglint/mkline.go
@@ -71,6 +75,8 @@ gopkg/src/netbsd.org/pkglint/mktokenslex
 gopkg/src/netbsd.org/pkglint/mktokenslexer_test.go
 gopkg/src/netbsd.org/pkglint/mktypes.go
 gopkg/src/netbsd.org/pkglint/mktypes_test.go
+gopkg/src/netbsd.org/pkglint/mkvarusechecker.go
+gopkg/src/netbsd.org/pkglint/mkvarusechecker_test.go
 gopkg/src/netbsd.org/pkglint/options.go
 gopkg/src/netbsd.org/pkglint/options_test.go
 gopkg/src/netbsd.org/pkglint/package.go
@@ -128,5 +134,5 @@ gopkg/src/netbsd.org/pkglint/vartype.go
 gopkg/src/netbsd.org/pkglint/vartype_test.go
 gopkg/src/netbsd.org/pkglint/vartypecheck.go
 gopkg/src/netbsd.org/pkglint/vartypecheck_test.go
-man/cat1/pkglint.0
 man/man1/pkglint.1
+@pkgdir man/cat1

Index: pkgsrc/pkgtools/pkglint/files/alternatives.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives.go:1.18 pkgsrc/pkgtools/pkglint/files/alternatives.go:1.19
--- pkgsrc/pkgtools/pkglint/files/alternatives.go:1.18  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/alternatives.go       Sun Dec  8 00:06:38 2019
@@ -11,72 +11,103 @@ func CheckFileAlternatives(filename Curr
                return
        }
 
-       var plist PlistContent
-       if G.Pkg != nil {
-               plist = G.Pkg.Plist
-       }
-
-       checkPlistWrapper := func(line *Line, wrapper Path) {
-               if plist.Files[wrapper] != nil {
-                       line.Errorf("Alternative wrapper %q must not appear in the PLIST.", wrapper)
-               }
-       }
-
-       checkPlistAlternative := func(line *Line, alternative string) {
-               relImplementation := strings.Replace(alternative, "@PREFIX@/", "", 1)
-               plistName := replaceAll(relImplementation, `@(\w+)@`, "${$1}")
-               if plist.Files[NewPath(plistName)] != nil || G.Pkg.vars.IsDefined("ALTERNATIVES_SRC") {
-                       return
-               }
-               if plist.Files[NewPath(strings.Replace(plistName, "${PKGMANDIR}", "man", 1))] != nil {
-                       return
-               }
-
-               switch {
-
-               case hasPrefix(alternative, "/"):
-                       // It's possible but unusual to refer to a fixed absolute path.
-                       // These cannot be mentioned in the PLIST since they are not part of the package.
-                       break
-
-               case plistName == alternative:
-                       line.Errorf("Alternative implementation %q must appear in the PLIST.", alternative)
-
-               default:
-                       line.Errorf("Alternative implementation %q must appear in the PLIST as %q.", alternative, plistName)
-               }
+       var ck AlternativesChecker
+       ck.Check(lines, G.Pkg)
+}
+
+type AlternativesChecker struct{}
+
+func (ck *AlternativesChecker) Check(lines *Lines, pkg *Package) {
+       var plistFiles map[RelPath]*PlistLine
+       if pkg != nil {
+               plistFiles = pkg.Plist.Files
        }
 
        for _, line := range lines.Lines {
-               m, wrapper, space, alternative := match3(line.Text, `^([^\t ]+)([ \t]+)([^\t ]+)`)
-               if !m {
-                       line.Errorf("Invalid line %q.", line.Text)
-                       line.Explain(
-                               sprintf("Run %q for more information.", bmakeHelp("alternatives")))
-                       continue
-               }
-
-               if plist.Files != nil {
-                       checkPlistWrapper(line, NewPath(wrapper))
-                       checkPlistAlternative(line, alternative)
-               }
-
-               switch {
-               case hasPrefix(alternative, "/"), hasPrefix(alternative, "@"):
-                       break
-
-               case textproc.NewLexer(alternative).NextByteSet(textproc.Alnum) != -1:
-                       fix := line.Autofix()
-                       fix.Errorf("Alternative implementation %q must be an absolute path.", alternative)
-                       fix.Explain(
-                               "It usually starts with @PREFIX@/... to refer to a path inside the installation prefix.")
-                       fix.ReplaceAfter(space, alternative, "@PREFIX@/"+alternative)
-                       fix.Apply()
-
-               default:
-                       line.Errorf("Alternative implementation %q must be an absolute path.", alternative)
-                       line.Explain(
-                               "It usually starts with @PREFIX@/... to refer to a path inside the installation prefix.")
-               }
+               ck.checkLine(line, plistFiles)
+       }
+}
+
+// checkLine checks a single line for the following format:
+//  wrapper alternative [optional arguments]
+func (ck *AlternativesChecker) checkLine(line *Line, plistFiles map[RelPath]*PlistLine) {
+       // TODO: Add $ to the regex, just for confidence
+       m, wrapper, space, alternative := match3(line.Text, `^([^\t ]+)([ \t]+)([^\t ]+)`)
+       if !m {
+               line.Errorf("Invalid line %q.", line.Text)
+               line.Explain(
+                       sprintf("Run %q for more information.", bmakeHelp("alternatives")))
+               return
+       }
+
+       if ck.checkWrapperAbs(line, NewPath(wrapper)) && plistFiles != nil {
+               ck.checkWrapperPlist(line, NewRelPathString(wrapper), plistFiles)
+       }
+       if plistFiles != nil {
+               ck.checkAlternativePlist(line, alternative, plistFiles)
+       }
+
+       ck.checkAlternativeAbs(alternative, line, space)
+}
+
+func (ck *AlternativesChecker) checkWrapperAbs(line *Line, wrapper Path) bool {
+       if !wrapper.IsAbs() {
+               return true
+       }
+
+       line.Errorf("Alternative wrapper %q must be relative to PREFIX.", wrapper.String())
+       return false
+}
+
+func (ck *AlternativesChecker) checkWrapperPlist(line *Line, wrapper RelPath,
+       plistFiles map[RelPath]*PlistLine) {
+
+       if plistFiles[wrapper] != nil {
+               line.Errorf("Alternative wrapper %q must not appear in the PLIST.", wrapper)
+       }
+}
+
+func (ck *AlternativesChecker) checkAlternativeAbs(alternative string, line *Line, space string) {
+       lex := textproc.NewLexer(alternative)
+
+       if lex.SkipByte('/') || lex.SkipByte('@') {
+               return
+       }
+
+       fix := line.Autofix()
+       fix.Errorf("Alternative implementation %q must be an absolute path.", alternative)
+       fix.Explain(
+               "It usually starts with @PREFIX@/... to refer to a path inside the installation prefix.")
+       if lex.TestByteSet(textproc.Alnum) {
+               fix.ReplaceAfter(space, alternative, "@PREFIX@/"+alternative)
+       }
+       fix.Apply()
+}
+
+func (ck *AlternativesChecker) checkAlternativePlist(line *Line, alternative string,
+       plistFiles map[RelPath]*PlistLine) {
+
+       relImplementation := strings.Replace(alternative, "@PREFIX@/", "", 1)
+       plistName := replaceAll(relImplementation, `@(\w+)@`, "${$1}")
+       if NewPath(plistName).IsAbs() {
+               // It's possible but unusual to refer to a fixed absolute path.
+               // These cannot be mentioned in the PLIST since they are not part of the package.
+               return
+       }
+
+       rel := NewRelPathString(plistName)
+       if plistFiles[rel] != nil || G.Pkg.vars.IsDefined("ALTERNATIVES_SRC") {
+               return
+       }
+       if plistFiles[rel.Replace("${PKGMANDIR}", "man")] != nil {
+               return
+       }
+
+       if plistName == alternative {
+               line.Errorf("Alternative implementation %q must appear in the PLIST.",
+                       alternative)
+       } else {
+               line.Errorf("Alternative implementation %q must appear in the PLIST as %q.",
+                       alternative, plistName)
        }
 }
Index: pkgsrc/pkgtools/pkglint/files/mktypes_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.18 pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.19
--- pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.18  Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/mktypes_test.go       Sun Dec  8 00:06:38 2019
@@ -36,7 +36,7 @@ func (MkTokenBuilder) VarUse(varname str
        for _, modifier := range modifiers {
                mods = append(mods, MkVarUseModifier{modifier})
        }
-       return &MkVarUse{varname, mods}
+       return NewMkVarUse(varname, mods...)
 }
 
 func (s *Suite) Test_MkVarUseModifier_MatchSubst(c *check.C) {
@@ -140,6 +140,21 @@ func (s *Suite) Test_MkVarUseModifier_Su
        t.CheckEquals(result, "")
 }
 
+func (s *Suite) Test_MkVarUseModifier_Subst__S_dollar_at(c *check.C) {
+       t := s.Init(c)
+
+       mod := MkVarUseModifier{"S/$@/replaced/"}
+
+       result, ok := mod.Subst("The target")
+
+       // As of December 2019, nothing is substituted. If pkglint should ever
+       // handle variables in the modifier, this test would been to provide a
+       // context in which to resolve the variables. If that happens, the
+       // .TARGET variable needs to be set to "target".
+       t.CheckEquals(ok, true)
+       t.CheckEquals(result, "The target")
+}
+
 func (s *Suite) Test_MkVarUseModifier_MatchMatch(c *check.C) {
        t := s.Init(c)
 
@@ -165,29 +180,38 @@ func (s *Suite) Test_MkVarUseModifier_Ma
        test("Npattern", false, "pattern", true)
 }
 
-func (s *Suite) Test_MkVarUseModifier_ChangesWords(c *check.C) {
+func (s *Suite) Test_MkVarUseModifier_ChangesList(c *check.C) {
        t := s.Init(c)
 
        test := func(modifier string, changes bool) {
                mod := MkVarUseModifier{modifier}
-               t.CheckEquals(mod.ChangesWords(), changes)
+               t.CheckEquals(mod.ChangesList(), changes)
        }
 
+       test("C,from,to,", true)
        test("E", false)
-       test("R", false)
+       test("H", false)
+
+       // FIXME: The :M and :N modifiers obviously change the number of words.
        test("Mpattern", false)
        test("Npattern", false)
+
+       test("O", false)
+       test("Q", true)
+       test("R", false)
        test("S,from,to,", true)
-       test("C,from,to,", true)
+       test("T", false)
+       test("invalid", true)
+       test("sh", true)
        test("tl", false)
+       test("tW", true)
        test("tu", false)
-       test("sh", true)
-
-       test("unknown", true)
+       test("tw", true)
 }
 
-// Ensures that ChangesWords cannot be called with an empty string as modifier.
-func (s *Suite) Test_MkVarUseModifier_ChangesWords__empty(c *check.C) {
+// Ensures that ChangesList cannot be called with an empty string as modifier.
+// Therefore it is safe to index text[0] without a preceding length check.
+func (s *Suite) Test_MkVarUseModifier_ChangesList__empty(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("filename.mk", 123, "\t${VAR:}")
@@ -196,7 +220,7 @@ func (s *Suite) Test_MkVarUseModifier_Ch
        mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) {
                n += 100
                for _, mod := range varUse.modifiers {
-                       mod.ChangesWords()
+                       mod.ChangesList()
                        n++
                }
        })

Index: pkgsrc/pkgtools/pkglint/files/alternatives_test.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.17 pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.17     Tue Nov 19 06:51:38 2019
+++ pkgsrc/pkgtools/pkglint/files/alternatives_test.go  Sun Dec  8 00:06:38 2019
@@ -2,7 +2,43 @@ package pkglint
 
 import "gopkg.in/check.v1"
 
-func (s *Suite) Test_CheckFileAlternatives__PLIST(c *check.C) {
+func (s *Suite) Test_CheckFileAlternatives__empty(c *check.C) {
+       t := s.Init(c)
+
+       t.Chdir("category/package")
+       t.CreateFileLines("ALTERNATIVES")
+
+       G.Pkg = NewPackage(".")
+
+       CheckFileAlternatives("ALTERNATIVES")
+
+       t.CheckOutputLines(
+               "ERROR: ALTERNATIVES: Must not be empty.")
+}
+
+func (s *Suite) Test_CheckFileAlternatives__ALTERNATIVES_SRC(c *check.C) {
+       t := s.Init(c)
+
+       // It's a strange situation, having an ALTERNATIVES file defined by
+       // the package but then referring to another package's file by means
+       // of ALTERNATIVES_SRC. As of February 2019 I don't remember if I
+       // really had this case in mind when I initially wrote the code in
+       // CheckFileAlternatives.
+       t.SetUpPackage("category/package",
+               "ALTERNATIVES_SRC=\talts")
+       t.CreateFileLines("category/package/ALTERNATIVES",
+               "bin/pgm @PREFIX@/bin/gnu-program",
+               "bin/pgm @PREFIX@/bin/nb-program")
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package"))
+
+       // The ALTERNATIVES file in this package is not checked at all.
+       // If it were, there would be an error for the repeated bin/pgm.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_AlternativesChecker_Check__PLIST(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
@@ -47,73 +83,116 @@ func (s *Suite) Test_CheckFileAlternativ
                "AUTOFIX: ALTERNATIVES:4: Replacing \"bin/vim\" with \"@PREFIX@/bin/vim\".")
 }
 
-// A file that is mentioned in the ALTERNATIVES file must appear
-// in the package's PLIST files. It may appear there conditionally,
-// assuming that manual testing will reveal inconsistencies. Or
-// that this scenario is an edge case anyway.
-func (s *Suite) Test_CheckFileAlternatives__PLIST_conditional(c *check.C) {
+func (s *Suite) Test_AlternativesChecker_checkLine(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
        t.Chdir("category/package")
        t.CreateFileLines("ALTERNATIVES",
-               "bin/wrapper1 @PREFIX@/bin/always-exists",
-               "bin/wrapper2 @PREFIX@/bin/conditional",
-               "bin/wrapper3 @PREFIX@/bin/not-found")
+               "bin/no-args @PREFIX@/bin/echo",
+               "bin/with-args @PREFIX@/bin/echo hello,",
+               "bin/with-quoted-args @PREFIX@/bin/echo \"hello, world\" \\ cowboy",
+               "bin/trailing @PREFIX@/bin/echo spaces ", // TODO: warn about this
+               "/abs-echo @PREFIX@/bin/echo")
        t.CreateFileLines("PLIST",
                PlistCvsID,
-               "bin/always-exists",
-               "${PLIST.cond}bin/conditional")
+               "bin/echo")
        t.FinishSetUp()
 
        G.Check(".")
 
        t.CheckOutputLines(
-               "ERROR: ALTERNATIVES:3: Alternative implementation \"@PREFIX@/bin/not-found\" " +
-                       "must appear in the PLIST as \"bin/not-found\".")
+               "ERROR: ALTERNATIVES:5: Alternative wrapper \"/abs-echo\" " +
+                       "must be relative to PREFIX.")
 }
 
-func (s *Suite) Test_CheckFileAlternatives__empty(c *check.C) {
+func (s *Suite) Test_AlternativesChecker_checkWrapperAbs(c *check.C) {
        t := s.Init(c)
 
-       t.Chdir("category/package")
-       t.CreateFileLines("ALTERNATIVES")
-
-       G.Pkg = NewPackage(".")
+       t.CreateFileLines("ALTERNATIVES",
+               "relative @PREFIX@/bin/echo",
+               "/absolute @PREFIX@/bin/echo")
 
-       CheckFileAlternatives("ALTERNATIVES")
+       CheckFileAlternatives(t.File("ALTERNATIVES"))
 
        t.CheckOutputLines(
-               "ERROR: ALTERNATIVES: Must not be empty.")
+               "ERROR: ~/ALTERNATIVES:2: Alternative wrapper \"/absolute\" " +
+                       "must be relative to PREFIX.")
 }
 
-func (s *Suite) Test_CheckFileAlternatives__ALTERNATIVES_SRC(c *check.C) {
+func (s *Suite) Test_AlternativesChecker_checkWrapperPlist(c *check.C) {
        t := s.Init(c)
 
-       // It's a strange situation, having an ALTERNATIVES file defined by
-       // the package but then referring to another package's file by means
-       // of ALTERNATIVES_SRC. As of February 2019 I don't remember if I
-       // really had this case in mind when I initially wrote the code in
-       // CheckFileAlternatives.
-       t.SetUpPackage("category/package",
-               "ALTERNATIVES_SRC=\talts")
-       t.CreateFileLines("category/package/ALTERNATIVES",
-               "bin/pgm @PREFIX@/bin/gnu-program",
-               "bin/pgm @PREFIX@/bin/nb-program")
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.CreateFileLines("ALTERNATIVES",
+               "bin/echo @PREFIX@/bin/gnu-echo",
+               "bin/editor @PREFIX@/bin/vim -e")
+       t.CreateFileLines("PLIST",
+               PlistCvsID,
+               "bin/echo",
+               "bin/gnu-echo",
+               "bin/vim")
        t.FinishSetUp()
 
-       G.Check(t.File("category/package"))
+       G.Check(".")
+
+       t.CheckOutputLines(
+               "ERROR: ALTERNATIVES:1: Alternative wrapper \"bin/echo\" " +
+                       "must not appear in the PLIST.")
+}
+
+func (s *Suite) Test_AlternativesChecker_checkAlternativeAbs(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.CreateFileLines("ALTERNATIVES",
+               "bin/echo bin/gnu-echo",
+               "bin/editor bin/vim -e")
+       t.CreateFileLines("PLIST",
+               PlistCvsID,
+               "bin/echo",
+               "bin/gnu-echo",
+               "bin/vim")
+       t.FinishSetUp()
 
        t.CheckOutputEmpty()
 }
 
+// A file that is mentioned in the ALTERNATIVES file must appear
+// in the package's PLIST files. It may appear there conditionally,
+// assuming that manual testing will reveal inconsistencies. Or
+// that this scenario is an edge case anyway.
+func (s *Suite) Test_AlternativesChecker_checkAlternativePlist__conditional(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.CreateFileLines("ALTERNATIVES",
+               "bin/wrapper1 @PREFIX@/bin/always-exists",
+               "bin/wrapper2 @PREFIX@/bin/conditional",
+               "bin/wrapper3 @PREFIX@/bin/not-found")
+       t.CreateFileLines("PLIST",
+               PlistCvsID,
+               "bin/always-exists",
+               "${PLIST.cond}bin/conditional")
+       t.FinishSetUp()
+
+       G.Check(".")
+
+       t.CheckOutputLines(
+               "ERROR: ALTERNATIVES:3: Alternative implementation \"@PREFIX@/bin/not-found\" " +
+                       "must appear in the PLIST as \"bin/not-found\".")
+}
+
 // When a man page is mentioned in the ALTERNATIVES file, it must use the
 // PKGMANDIR variable. In the PLIST files though, there is some magic
 // in the pkgsrc infrastructure that maps man/ to ${PKGMANDIR}, which
 // leads to a bit less typing.
 //
 // Seen in graphics/py-blockdiag.
-func (s *Suite) Test_CheckFileAlternatives__PLIST_man(c *check.C) {
+func (s *Suite) Test_AlternativesChecker_checkAlternativePlist__PKGMANDIR(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package")

Index: pkgsrc/pkgtools/pkglint/files/autofix.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix.go:1.33 pkgsrc/pkgtools/pkglint/files/autofix.go:1.34
--- pkgsrc/pkgtools/pkglint/files/autofix.go:1.33       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix.go    Sun Dec  8 00:06:38 2019
@@ -7,7 +7,13 @@ import (
        "strings"
 )
 
+type Autofixer interface {
+       Diagnoser
+       Autofix() *Autofix
+}
+
 // Autofix handles all modifications to a single line,
+// possibly spanning multiple physical lines in case of Makefile lines,
 // describes them in a human-readable form and formats the output.
 // The modifications are kept in memory only,
 // until they are written to disk by SaveAutofixChanges.
Index: pkgsrc/pkgtools/pkglint/files/autofix_test.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.33 pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.34
--- pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.33  Sat Nov 30 20:35:11 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix_test.go       Sun Dec  8 00:06:38 2019
@@ -146,7 +146,7 @@ func (s *Suite) Test_Autofix__lonely_sou
                ".include \"../../x11/xorgproto/buildlink3.mk\"")
        t.SetUpPackage("x11/xorgproto",
                "DISTNAME=\txorgproto-1.0")
-       t.CreateFileDummyBuildlink3("x11/xorgproto/buildlink3.mk")
+       t.CreateFileBuildlink3("x11/xorgproto/buildlink3.mk")
        t.CreateFileLines("x11/xorgproto/builtin.mk",
                MkCvsID,
                "",
@@ -176,6 +176,9 @@ func (s *Suite) Test_Autofix__lonely_sou
        G.Logger.verbose = false // For realistic conditions; otherwise all diagnostics are logged.
 
        t.SetUpPackage("print/tex-bibtex8",
+               "# Including bsd.prefs.mk is not necessary here since",
+               "# PKGSRC_COMPILER is evaluated lazily.",
+               "",
                "MAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}")
        t.Chdir(".")
        t.FinishSetUp()
@@ -184,13 +187,13 @@ func (s *Suite) Test_Autofix__lonely_sou
 
        t.CheckOutputLines(
                ">\tMAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}",
-               "WARN: print/tex-bibtex8/Makefile:20: Please use ${CFLAGS.${PKGSRC_COMPILER}:Q} instead of ${CFLAGS.${PKGSRC_COMPILER}}.",
+               "WARN: print/tex-bibtex8/Makefile:23: Please use ${CFLAGS.${PKGSRC_COMPILER}:Q} instead of ${CFLAGS.${PKGSRC_COMPILER}}.",
                "",
                "\tSee the pkgsrc guide, section \"Echoing a string exactly as-is\":",
                "\thttps://www.NetBSD.org/docs/pkgsrc/pkgsrc.html#echo-literal";,
                "",
                ">\tMAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}",
-               "WARN: print/tex-bibtex8/Makefile:20: The list variable PKGSRC_COMPILER should not be embedded in a word.",
+               "WARN: print/tex-bibtex8/Makefile:23: The list variable PKGSRC_COMPILER should not be embedded in a word.",
                "",
                "\tWhen a list variable has multiple elements, this expression expands",
                "\tto something unexpected:",

Index: pkgsrc/pkgtools/pkglint/files/buildlink3.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.27 pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.28
--- pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.27    Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/buildlink3.go Sun Dec  8 00:06:38 2019
@@ -106,7 +106,7 @@ func (ck *Buildlink3Checker) checkUnique
        }
 
        mkline.Errorf("Duplicate package identifier %q already appeared in %s.",
-               pkgbase, mkline.RefToLocation(*prev))
+               pkgbase, mkline.RelLocation(*prev))
        mkline.Explain(
                "Each buildlink3.mk file must have a unique identifier.",
                "These identifiers are used for multiple-inclusion guards,",
@@ -134,7 +134,7 @@ func (ck *Buildlink3Checker) checkSecond
        ucPkgbase := strings.ToUpper(strings.Replace(pkgbase, "-", "_", -1))
        if ucPkgbase != pkgupper && !containsVarRef(pkgbase) {
                pkgupperLine.Errorf("Package name mismatch between multiple-inclusion guard %q (expected %q) and package name %q (from %s).",
-                       pkgupper, ucPkgbase, pkgbase, pkgupperLine.RefTo(ck.pkgbaseLine))
+                       pkgupper, ucPkgbase, pkgbase, pkgupperLine.RelMkLine(ck.pkgbaseLine))
        }
        ck.checkPkgbaseMismatch(pkgbase)
 
@@ -156,7 +156,7 @@ func (ck *Buildlink3Checker) checkPkgbas
        }
 
        ck.pkgbaseLine.Errorf("Package name mismatch between %q in this file and %q from %s.",
-               bl3base, mkbase, ck.pkgbaseLine.RefTo(G.Pkg.EffectivePkgnameLine))
+               bl3base, mkbase, ck.pkgbaseLine.RelMkLine(G.Pkg.EffectivePkgnameLine))
 }
 
 // Third paragraph: Package information.
@@ -218,7 +218,7 @@ func (ck *Buildlink3Checker) checkVarass
        if doCheck && ck.abi != nil && ck.api != nil && ck.abi.Pkgbase != ck.api.Pkgbase {
                if !hasPrefix(ck.api.Pkgbase, "{") {
                        ck.abiLine.Warnf("Package name mismatch between ABI %q and API %q (from %s).",
-                               ck.abi.Pkgbase, ck.api.Pkgbase, ck.abiLine.RefTo(ck.apiLine))
+                               ck.abi.Pkgbase, ck.api.Pkgbase, ck.abiLine.RelMkLine(ck.apiLine))
                }
        }
 
@@ -227,7 +227,7 @@ func (ck *Buildlink3Checker) checkVarass
                        if ck.api != nil && ck.api.Lower != "" && !containsVarRef(ck.api.Lower) {
                                if pkgver.Compare(ck.abi.Lower, ck.api.Lower) < 0 {
                                        ck.abiLine.Warnf("ABI version %q should be at least API version %q (see %s).",
-                                               ck.abi.Lower, ck.api.Lower, ck.abiLine.RefTo(ck.apiLine))
+                                               ck.abi.Lower, ck.api.Lower, ck.abiLine.RelMkLine(ck.apiLine))
                                }
                        }
                }
Index: pkgsrc/pkgtools/pkglint/files/category.go
diff -u pkgsrc/pkgtools/pkglint/files/category.go:1.27 pkgsrc/pkgtools/pkglint/files/category.go:1.28
--- pkgsrc/pkgtools/pkglint/files/category.go:1.27      Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/category.go   Sun Dec  8 00:06:38 2019
@@ -49,7 +49,7 @@ func CheckdirCategory(dir CurrPath) {
        mlex.SkipEmptyOrNote()
 
        type subdir struct {
-               name Path
+               name RelPath
                line *MkLine
        }
 
@@ -73,7 +73,7 @@ func CheckdirCategory(dir CurrPath) {
                        }
 
                        if prev := seen[name]; prev != nil {
-                               mkline.Errorf("%q must only appear once, already seen in %s.", name, mkline.RefTo(prev))
+                               mkline.Errorf("%q must only appear once, already seen in %s.", name, mkline.RelMkLine(prev))
                        }
                        seen[name] = mkline
 
@@ -83,7 +83,7 @@ func CheckdirCategory(dir CurrPath) {
                                }
                        }
 
-                       mSubdirs = append(mSubdirs, subdir{NewPath(name), mkline})
+                       mSubdirs = append(mSubdirs, subdir{NewRelPathString(name), mkline})
 
                } else {
                        if !mkline.IsEmpty() {
@@ -96,8 +96,8 @@ func CheckdirCategory(dir CurrPath) {
        // To prevent unnecessary warnings about subdirectories that are
        // in one list but not in the other, generate the sets of
        // subdirs of each list.
-       fCheck := make(map[Path]bool)
-       mCheck := make(map[Path]bool)
+       fCheck := make(map[RelPath]bool)
+       mCheck := make(map[RelPath]bool)
        for _, fsub := range fSubdirs {
                fCheck[fsub] = true
        }

Index: pkgsrc/pkgtools/pkglint/files/buildlink3_test.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.37 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.38
--- pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.37       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/buildlink3_test.go    Sun Dec  8 00:06:38 2019
@@ -15,7 +15,7 @@ func (s *Suite) Test_CheckLinesBuildlink
        t.SetUpPackage("category/package",
                ".include \"../../category/dependency1/buildlink3.mk\"")
 
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                ".include \"../../category/dependency2/buildlink3.mk\"")
        t.FinishSetUp()
 
@@ -473,119 +473,12 @@ func (s *Suite) Test_CheckLinesBuildlink
                "WARN: buildlink3.mk:10: Invalid dependency pattern \"hs-X11!=1.6.1.2nb2\".")
 }
 
-func (s *Suite) Test_CheckLinesBuildlink3Mk__PKGBASE_with_variable(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklinesPhp := t.NewMkLines("x11/php-wxwidgets/buildlink3.mk",
-               MkCvsID,
-               "",
-               "BUILDLINK_TREE+=\t${PHP_PKG_PREFIX}-wxWidgets",
-               "",
-               ".if !defined(PHP_WXWIDGETS_BUILDLINK3_MK)",
-               "PHP_WXWIDGETS_BUILDLINK3_MK:=",
-               "",
-               "BUILDLINK_API_DEPENDS.${PHP_PKG_PREFIX}-wxWidgets+=\t${PHP_PKG_PREFIX}-wxWidgets>=2.6.1.0",
-               "BUILDLINK_ABI_DEPENDS.${PHP_PKG_PREFIX}-wxWidgets+=\t${PHP_PKG_PREFIX}-wxWidgets>=2.8.10.1nb26",
-               "",
-               ".endif",
-               "",
-               "BUILDLINK_TREE+=\t-${PHP_PKG_PREFIX}-wxWidgets")
-       mklinesPy := t.NewMkLines("x11/py-wxwidgets/buildlink3.mk",
-               MkCvsID,
-               "",
-               "BUILDLINK_TREE+=\t${PYPKGPREFIX}-wxWidgets",
-               "",
-               ".if !defined(PY_WXWIDGETS_BUILDLINK3_MK)",
-               "PY_WXWIDGETS_BUILDLINK3_MK:=",
-               "",
-               "BUILDLINK_API_DEPENDS.${PYPKGPREFIX}-wxWidgets+=\t${PYPKGPREFIX}-wxWidgets>=2.6.1.0",
-               "BUILDLINK_ABI_DEPENDS.${PYPKGPREFIX}-wxWidgets+=\t${PYPKGPREFIX}-wxWidgets>=2.8.10.1nb26",
-               "",
-               ".endif",
-               "",
-               "BUILDLINK_TREE+=\t-${PYPKGPREFIX}-wxWidgets")
-       mklinesRuby1 := t.NewMkLines("x11/ruby1-wxwidgets/buildlink3.mk",
-               MkCvsID,
-               "",
-               "BUILDLINK_TREE+=\t${RUBY_BASE}-wxWidgets",
-               "",
-               ".if !defined(RUBY_WXWIDGETS_BUILDLINK3_MK)",
-               "RUBY_WXWIDGETS_BUILDLINK3_MK:=",
-               "",
-               "BUILDLINK_API_DEPENDS.${RUBY_BASE}-wxWidgets+=\t${RUBY_BASE}-wxWidgets>=2.6.1.0",
-               "BUILDLINK_ABI_DEPENDS.${RUBY_BASE}-wxWidgets+=\t${RUBY_BASE}-wxWidgets>=2.8.10.1nb26",
-               "",
-               ".endif",
-               "",
-               "BUILDLINK_TREE+=\t-${RUBY_BASE}-wxWidgets")
-       mklinesRuby2 := t.NewMkLines("x11/ruby2-wxwidgets/buildlink3.mk",
-               MkCvsID,
-               "",
-               "BUILDLINK_TREE+=\t${RUBY_PKGPREFIX}-wxWidgets",
-               "",
-               ".if !defined(RUBY_WXWIDGETS_BUILDLINK3_MK)",
-               "RUBY_WXWIDGETS_BUILDLINK3_MK:=",
-               "",
-               "BUILDLINK_API_DEPENDS.${RUBY_PKGPREFIX}-wxWidgets+=\t${RUBY_PKGPREFIX}-wxWidgets>=2.6.1.0",
-               "BUILDLINK_ABI_DEPENDS.${RUBY_PKGPREFIX}-wxWidgets+=\t${RUBY_PKGPREFIX}-wxWidgets>=2.8.10.1nb26",
-               "",
-               ".endif",
-               "",
-               "BUILDLINK_TREE+=\t-${RUBY_PKGPREFIX}-wxWidgets")
-
-       CheckLinesBuildlink3Mk(mklinesPhp)
-       CheckLinesBuildlink3Mk(mklinesPy)
-       CheckLinesBuildlink3Mk(mklinesRuby1)
-       CheckLinesBuildlink3Mk(mklinesRuby2)
-
-       t.CheckOutputLines(
-               "WARN: x11/php-wxwidgets/buildlink3.mk:3: Please use \"php\" instead of \"${PHP_PKG_PREFIX}\" (also in other variables in this file).",
-               "WARN: x11/py-wxwidgets/buildlink3.mk:3: Please use \"py\" instead of \"${PYPKGPREFIX}\" (also in other variables in this file).",
-               "WARN: x11/ruby1-wxwidgets/buildlink3.mk:3: Please use \"ruby\" instead of \"${RUBY_BASE}\" (also in other variables in this file).",
-               "WARN: x11/ruby2-wxwidgets/buildlink3.mk:3: Please use \"ruby\" instead of \"${RUBY_PKGPREFIX}\" (also in other variables in this file).")
-}
-
-func (s *Suite) Test_CheckLinesBuildlink3Mk__PKGBASE_with_unknown_variable(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("buildlink3.mk",
-               MkCvsID,
-               "",
-               "BUILDLINK_TREE+=\t${LICENSE}-wxWidgets",
-               "",
-               ".if !defined(LICENSE_BUILDLINK3_MK)",
-               "LICENSE_BUILDLINK3_MK:=",
-               "",
-               "BUILDLINK_API_DEPENDS.${LICENSE}-wxWidgets+=\t${LICENSE}-wxWidgets>=2.6.1.0",
-               "BUILDLINK_ABI_DEPENDS.${LICENSE}-wxWidgets+=\t${LICENSE}-wxWidgets>=2.8.10.1nb26",
-               "",
-               ".endif",
-               "",
-               "BUILDLINK_TREE+=\t-${LICENSE}-wxWidgets")
-
-       CheckLinesBuildlink3Mk(mklines)
-
-       t.CheckOutputLines(
-               "WARN: buildlink3.mk:3: LICENSE should not be used in this file; "+
-                       "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 "+
-                       "(also in other variables in this file).")
-}
-
 // Just for branch coverage.
 func (s *Suite) Test_Buildlink3Checker_Check__no_tracing(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk")
+       t.CreateFileBuildlink3("category/package/buildlink3.mk")
        t.DisableTracing()
        t.FinishSetUp()
 
@@ -677,7 +570,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t.SetUpPackage("category/package",
                "DISTNAME=\t# empty",
                "PKGNAME=\t# empty, to force mkbase to be empty")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk")
+       t.CreateFileBuildlink3("category/package/buildlink3.mk")
        t.FinishSetUp()
 
        G.Check(t.File("category/package"))
@@ -694,7 +587,9 @@ func (s *Suite) Test_Buildlink3Checker_c
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
+               ".include \"../../mk/bsd.fast.prefs.mk\"",
+               "",
                ".if ${X11_TYPE} == modular",
                ".else",
                ".endif")
@@ -707,11 +602,15 @@ func (s *Suite) Test_Buildlink3Checker_c
 
 // Since the buildlink3 checker does not use MkLines.ForEach, it has to keep
 // track of the nesting depth of .if directives.
+//
+// TODO: Use MkLines.ForEach.
 func (s *Suite) Test_Buildlink3Checker_checkMainPart__nested_if(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
-       mklines := t.SetUpFileMkLines("category/package/buildlink3.mk",
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
+       mklines := t.SetUpFileMkLines("buildlink3.mk",
                MkCvsID,
                "",
                "BUILDLINK_TREE+=\ths-X11",
@@ -722,6 +621,8 @@ func (s *Suite) Test_Buildlink3Checker_c
                "BUILDLINK_API_DEPENDS.hs-X11+=\ths-X11>=1.6.1",
                "BUILDLINK_ABI_DEPENDS.hs-X11+=\ths-X11>=1.6.1.2nb2",
                "",
+               ".include \"../../mk/bsd.fast.prefs.mk\"",
+               "",
                ".if ${OPSYS} == NetBSD",
                ".endif",
                "",
@@ -797,7 +698,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=1.0:../../category/package",
                "BUILDLINK_API_DEPENDS.package+=\tpackage>=1.5:../../category/package")
        t.FinishSetUp()
@@ -817,7 +718,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       // t.CreateFileDummyBuildlink3() cannot be used here since it always adds an API line.
+       // t.CreateFileBuildlink3() cannot be used here since it always adds an API line.
        t.CreateFileLines("category/package/buildlink3.mk",
                MkCvsID,
                "",
@@ -846,7 +747,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=${ABI_VERSION}",
                "BUILDLINK_API_DEPENDS.package+=\tpackage>=${API_VERSION}",
                "",
@@ -864,7 +765,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=1.0",
                "BUILDLINK_API_DEPENDS.package+=\tpackage>=${API_VERSION}",
                "",
@@ -881,7 +782,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_ABI_DEPENDS.package+=\tpackage-1.*",
                "BUILDLINK_API_DEPENDS.package+=\tpackage-2.*")
        t.FinishSetUp()
@@ -897,7 +798,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=1",
                "BUILDLINK_API_DEPENDS.package+=\tpackage-1.*")
        t.FinishSetUp()
@@ -913,7 +814,7 @@ func (s *Suite) Test_Buildlink3Checker_c
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                "BUILDLINK_TREE+=\tmistake", // Wrong, but doesn't happen in practice.
                "",
                "LDFLAGS.NetBSD+=\t-ldl",
@@ -930,3 +831,167 @@ func (s *Suite) Test_Buildlink3Checker_c
                        "Only buildlink variables for \"package\", " +
                        "not \"other\" may be set in this file.")
 }
+
+func (s *Suite) Test_Buildlink3Checker_checkVaruseInPkgbase__PKGBASE_with_variable_PHP_PKG_PREFIX(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("x11/php-wxwidgets/buildlink3.mk",
+               MkCvsID,
+               "",
+               "BUILDLINK_TREE+=\t${PHP_PKG_PREFIX}-wxWidgets",
+               "",
+               ".if !defined(PHP_WXWIDGETS_BUILDLINK3_MK)",
+               "PHP_WXWIDGETS_BUILDLINK3_MK:=",
+               "",
+               "BUILDLINK_API_DEPENDS.${PHP_PKG_PREFIX}-wxWidgets+=\t${PHP_PKG_PREFIX}-wxWidgets>=2.6.1.0",
+               "BUILDLINK_ABI_DEPENDS.${PHP_PKG_PREFIX}-wxWidgets+=\t${PHP_PKG_PREFIX}-wxWidgets>=2.8.10.1nb26",
+               "",
+               ".endif",
+               "",
+               "BUILDLINK_TREE+=\t-${PHP_PKG_PREFIX}-wxWidgets")
+
+       CheckLinesBuildlink3Mk(mklines)
+
+       t.CheckOutputLines(
+               "WARN: x11/php-wxwidgets/buildlink3.mk:8: "+
+                       "To use PHP_PKG_PREFIX at load time, "+
+                       ".include \"../../mk/bsd.fast.prefs.mk\" first.",
+               "WARN: x11/php-wxwidgets/buildlink3.mk:3: "+
+                       "Please use \"php\" instead of \"${PHP_PKG_PREFIX}\" "+
+                       "(also in other variables in this file).")
+}
+
+func (s *Suite) Test_Buildlink3Checker_checkVaruseInPkgbase__PKGBASE_with_variable_PYPKGPREFIX(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("x11/py-wxwidgets/buildlink3.mk",
+               MkCvsID,
+               "",
+               "BUILDLINK_TREE+=\t${PYPKGPREFIX}-wxWidgets",
+               "",
+               ".if !defined(PY_WXWIDGETS_BUILDLINK3_MK)",
+               "PY_WXWIDGETS_BUILDLINK3_MK:=",
+               "",
+               "BUILDLINK_API_DEPENDS.${PYPKGPREFIX}-wxWidgets+=\t${PYPKGPREFIX}-wxWidgets>=2.6.1.0",
+               "BUILDLINK_ABI_DEPENDS.${PYPKGPREFIX}-wxWidgets+=\t${PYPKGPREFIX}-wxWidgets>=2.8.10.1nb26",
+               "",
+               ".endif",
+               "",
+               "BUILDLINK_TREE+=\t-${PYPKGPREFIX}-wxWidgets")
+
+       CheckLinesBuildlink3Mk(mklines)
+
+       t.CheckOutputLines(
+               "WARN: x11/py-wxwidgets/buildlink3.mk:8: "+
+                       "To use PYPKGPREFIX at load time, "+
+                       ".include \"../../mk/bsd.fast.prefs.mk\" first.",
+               "WARN: x11/py-wxwidgets/buildlink3.mk:3: "+
+                       "Please use \"py\" instead of \"${PYPKGPREFIX}\" "+
+                       "(also in other variables in this file).")
+}
+
+func (s *Suite) Test_Buildlink3Checker_checkVaruseInPkgbase__PKGBASE_with_variable_RUBY_BASE(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("x11/ruby1-wxwidgets/buildlink3.mk",
+               MkCvsID,
+               "",
+               "BUILDLINK_TREE+=\t${RUBY_BASE}-wxWidgets",
+               "",
+               ".if !defined(RUBY_WXWIDGETS_BUILDLINK3_MK)",
+               "RUBY_WXWIDGETS_BUILDLINK3_MK:=",
+               "",
+               "BUILDLINK_API_DEPENDS.${RUBY_BASE}-wxWidgets+=\t${RUBY_BASE}-wxWidgets>=2.6.1.0",
+               "BUILDLINK_ABI_DEPENDS.${RUBY_BASE}-wxWidgets+=\t${RUBY_BASE}-wxWidgets>=2.8.10.1nb26",
+               "",
+               ".endif",
+               "",
+               "BUILDLINK_TREE+=\t-${RUBY_BASE}-wxWidgets")
+
+       CheckLinesBuildlink3Mk(mklines)
+
+       t.CheckOutputLines(
+               "WARN: x11/ruby1-wxwidgets/buildlink3.mk:8: "+
+                       "To use RUBY_BASE at load time, "+
+                       ".include \"../../mk/bsd.fast.prefs.mk\" first.",
+               "WARN: x11/ruby1-wxwidgets/buildlink3.mk:3: "+
+                       "Please use \"ruby\" instead of \"${RUBY_BASE}\" "+
+                       "(also in other variables in this file).")
+}
+
+func (s *Suite) Test_Buildlink3Checker_checkVaruseInPkgbase__PKGBASE_with_variable_RUBY_PKGPREFIX(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("x11/ruby2-wxwidgets/buildlink3.mk",
+               MkCvsID,
+               "",
+               "BUILDLINK_TREE+=\t${RUBY_PKGPREFIX}-wxWidgets",
+               "",
+               ".if !defined(RUBY_WXWIDGETS_BUILDLINK3_MK)",
+               "RUBY_WXWIDGETS_BUILDLINK3_MK:=",
+               "",
+               "BUILDLINK_API_DEPENDS.${RUBY_PKGPREFIX}-wxWidgets+=\t${RUBY_PKGPREFIX}-wxWidgets>=2.6.1.0",
+               "BUILDLINK_ABI_DEPENDS.${RUBY_PKGPREFIX}-wxWidgets+=\t${RUBY_PKGPREFIX}-wxWidgets>=2.8.10.1nb26",
+               "",
+               ".endif",
+               "",
+               "BUILDLINK_TREE+=\t-${RUBY_PKGPREFIX}-wxWidgets")
+
+       CheckLinesBuildlink3Mk(mklines)
+
+       t.CheckOutputLines(
+               "WARN: x11/ruby2-wxwidgets/buildlink3.mk:8: "+
+                       "To use RUBY_PKGPREFIX at load time, "+
+                       ".include \"../../mk/bsd.fast.prefs.mk\" first.",
+               "WARN: x11/ruby2-wxwidgets/buildlink3.mk:3: "+
+                       "Please use \"ruby\" instead of \"${RUBY_PKGPREFIX}\" "+
+                       "(also in other variables in this file).")
+}
+
+func (s *Suite) Test_Buildlink3Checker_checkVaruseInPkgbase__PKGBASE_with_unknown_variable(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("buildlink3.mk",
+               MkCvsID,
+               "",
+               "BUILDLINK_TREE+=\t${LICENSE}-wxWidgets",
+               "",
+               ".if !defined(LICENSE_BUILDLINK3_MK)",
+               "LICENSE_BUILDLINK3_MK:=",
+               "",
+               "BUILDLINK_API_DEPENDS.${LICENSE}-wxWidgets+=\t${LICENSE}-wxWidgets>=2.6.1.0",
+               "BUILDLINK_ABI_DEPENDS.${LICENSE}-wxWidgets+=\t${LICENSE}-wxWidgets>=2.8.10.1nb26",
+               "",
+               ".endif",
+               "",
+               "BUILDLINK_TREE+=\t-${LICENSE}-wxWidgets")
+
+       CheckLinesBuildlink3Mk(mklines)
+
+       t.CheckOutputLines(
+               "WARN: buildlink3.mk:3: LICENSE should not be used in this file; "+
+                       "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 "+
+                       "(also in other variables in this file).")
+}

Index: pkgsrc/pkgtools/pkglint/files/check_test.go
diff -u pkgsrc/pkgtools/pkglint/files/check_test.go:1.57 pkgsrc/pkgtools/pkglint/files/check_test.go:1.58
--- pkgsrc/pkgtools/pkglint/files/check_test.go:1.57    Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/check_test.go Sun Dec  8 00:06:38 2019
@@ -104,11 +104,70 @@ func (s *Suite) TearDownTest(c *check.C)
 //  Test_${Type}_${Method}__${description_using_underscores}
 func (s *Suite) Test__qa(c *check.C) {
        ck := intqa.NewQAChecker(c.Errorf)
-       ck.Configure("*", "*", "*", -intqa.EMissingTest)
-       ck.Configure("path.go", "*", "*", +intqa.EMissingTest)
+
+       ck.Configure("autofix.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("buildlink3.go", "*", "*", -intqa.EMissingTest)     // TODO
+       ck.Configure("distinfo.go", "*", "*", -intqa.EMissingTest)       // TODO
+       ck.Configure("files.go", "*", "*", -intqa.EMissingTest)          // TODO
+       ck.Configure("licenses.go", "*", "*", -intqa.EMissingTest)       // TODO
+       ck.Configure("line.go", "*", "*", -intqa.EMissingTest)           // TODO
+       ck.Configure("linechecker.go", "*", "*", -intqa.EMissingTest)    // TODO
+       ck.Configure("lineslexer.go", "*", "*", -intqa.EMissingTest)     // TODO
+       ck.Configure("lines.go", "*", "*", -intqa.EMissingTest)          // TODO
+       ck.Configure("logging.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("mkline.go", "*", "*", -intqa.EMissingTest)         // TODO
+       ck.Configure("mklineparser.go", "*", "*", -intqa.EMissingTest)   // TODO
+       ck.Configure("mklinechecker.go", "*", "*", -intqa.EMissingTest)  // TODO
+       ck.Configure("mklines.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("mkparser.go", "*", "*", -intqa.EMissingTest)       // TODO
+       ck.Configure("mkshparser.go", "*", "*", -intqa.EMissingTest)     // TODO
+       ck.Configure("mkshtypes.go", "*", "*", -intqa.EMissingTest)      // TODO
+       ck.Configure("mkshwalker.go", "*", "*", -intqa.EMissingTest)     // TODO
+       ck.Configure("mktokenslexer.go", "*", "*", -intqa.EMissingTest)  // TODO
+       ck.Configure("mktypes.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("options.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("package.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("paragraph.go", "*", "*", -intqa.EMissingTest)      // TODO
+       ck.Configure("patches.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("pkglint.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("pkgsrc.go", "*", "*", -intqa.EMissingTest)         // TODO
+       ck.Configure("plist.go", "*", "*", -intqa.EMissingTest)          // TODO
+       ck.Configure("redundantscope.go", "*", "*", -intqa.EMissingTest) // TODO
+       ck.Configure("shell.go", "*", "*", -intqa.EMissingTest)          // TODO
+       ck.Configure("shtokenizer.go", "*", "*", -intqa.EMissingTest)    // TODO
+       ck.Configure("shtypes.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("substcontext.go", "*", "*", -intqa.EMissingTest)   // TODO
+       ck.Configure("tools.go", "*", "*", -intqa.EMissingTest)          // TODO
+       ck.Configure("util.go", "*", "*", -intqa.EMissingTest)           // TODO
+       ck.Configure("var.go", "*", "*", -intqa.EMissingTest)            // TODO
+       ck.Configure("varalignblock.go", "*", "*", -intqa.EMissingTest)  // TODO
+       ck.Configure("vardefs.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("vargroups.go", "*", "*", -intqa.EMissingTest)      // TODO
+       ck.Configure("vartype.go", "*", "*", -intqa.EMissingTest)        // TODO
+       ck.Configure("vartypecheck.go", "*", "*", -intqa.EMissingTest)   // TODO
+
+       // For now, don't require tests for all the test code.
+       // Having good coverage for the main code is more important.
+       ck.Configure("*_test.go", "*", "*", -intqa.EMissingTest)
+
+       // These helper methods are usually so simple that they don't need
+       // separate tests.
+       // They are tested indirectly by the tests of their corresponding checks.
+       ck.Configure("*", "*", "warn[A-Z]*", -intqa.EMissingTest)
+
+       // Generated code doesn't need a unit test.
+       // If any, every grammar production in the corresponding yacc file
+       // should have a unit test.
        ck.Configure("*yacc.go", "*", "*", intqa.ENone)
+
+       // Type definitions don't need a unit test.
+       // Only functions and methods do.
        ck.Configure("*", "*", "", -intqa.EMissingTest)
+
+       // The Suite type is used for testing all parts of pkglint.
+       // Therefore its test methods may be everywhere.
        ck.Configure("*.go", "Suite", "*", -intqa.EMethodsSameFile)
+
        ck.Check()
 }
 
@@ -216,6 +275,8 @@ func (t *Tester) SetUpFileLines(filename
 // The file is then read in, handling line continuations for Makefiles.
 //
 // See SetUpFileLines for loading an ordinary file.
+//
+// If the filename is irrelevant for the particular test, take filename.mk.
 func (t *Tester) SetUpFileMkLines(filename RelPath, lines ...string) *MkLines {
        abs := t.CreateFileLines(filename, lines...)
        return LoadMk(abs, MustSucceed)
@@ -328,6 +389,10 @@ func (t *Tester) SetUpPkgsrc() {
        // Category Makefiles require this file for the common definitions.
        t.CreateFileLines("mk/misc/category.mk")
 
+       // TODO
+       // assert(!t.File("mk/bsd.options.mk").IsFile())
+       // t.CreateFileLines("mk/bsd.options.mk")
+
        t.seenSetupPkgsrc++
 }
 
@@ -484,7 +549,7 @@ func (t *Tester) CreateFileDummyPatch(fi
                "+new")
 }
 
-func (t *Tester) CreateFileDummyBuildlink3(filename RelPath, customLines ...string) {
+func (t *Tester) CreateFileBuildlink3(filename RelPath, customLines ...string) {
        // Buildlink3.mk files only make sense in category/package directories.
        assert(G.Pkgsrc.ToRel(t.File(filename)).Count() == 3)
 
@@ -536,7 +601,7 @@ func (t *Tester) File(filename RelPath) 
        if t.cwd != "" {
                return NewCurrPath(filename.Clean().AsPath())
        }
-       return t.tmpdir.JoinClean(filename.AsPath())
+       return t.tmpdir.JoinClean(filename)
 }
 
 // Copy copies a file inside the temporary directory.
@@ -546,7 +611,6 @@ func (t *Tester) Copy(source, target Rel
 
        data, err := absSource.ReadString()
        assertNil(err, "Copy.Read")
-       // FIXME: consider DirNoClean
        err = os.MkdirAll(absTarget.DirClean().String(), 0777)
        assertNil(err, "Copy.MkdirAll")
        err = absTarget.WriteString(data)
@@ -579,7 +643,7 @@ func (t *Tester) Chdir(dirname RelPath) 
        assertNil(os.Chdir(absDirName.String()), "Chdir")
        t.cwd = dirname
        G.cwd = absDirName
-       G.Pkgsrc.topdir = NewCurrPath(absDirName.Rel(G.Pkgsrc.topdir))
+       G.Pkgsrc.topdir = NewCurrPath(absDirName.Rel(G.Pkgsrc.topdir).AsPath())
 }
 
 // Remove removes the file or directory from the temporary directory.
@@ -628,13 +692,11 @@ func (t *Tester) SetUpHierarchy() (
        // includePath returns the path to be used in an .include.
        //
        // This is the same mechanism that is used in Pkgsrc.Relpath.
-       includePath := func(including, included Path) Path {
-               // FIXME: consider DirNoClean
+       includePath := func(including, included RelPath) RelPath {
                fromDir := including.DirClean()
-               to := basedir.Rel(included)
-               // FIXME: consider DirNoClean
-               if fromDir == to.DirClean() {
-                       return NewPath(to.Base())
+               to := basedir.Rel(included.AsPath())
+               if fromDir == to.DirNoClean() {
+                       return NewRelPathString(to.Base())
                } else {
                        return fromDir.Rel(basedir).JoinNoClean(to).CleanDot()
                }
@@ -646,7 +708,7 @@ func (t *Tester) SetUpHierarchy() (
                relFilename := basedir.Rel(filename.AsPath())
 
                addLine := func(text string) {
-                       lines = append(lines, t.NewLine(NewCurrPath(relFilename), lineno, text))
+                       lines = append(lines, t.NewLine(NewCurrPath(relFilename.AsPath()), lineno, text))
                        lineno++
                }
 
@@ -655,7 +717,7 @@ func (t *Tester) SetUpHierarchy() (
                        case string:
                                addLine(arg)
                        case *MkLines:
-                               rel := includePath(relFilename, arg.lines.Filename.AsPath())
+                               rel := includePath(relFilename, NewRelPath(arg.lines.Filename.AsPath()))
                                addLine(sprintf(".include %q", rel))
                                lines = append(lines, arg.lines.Lines...)
                        default:
@@ -663,7 +725,7 @@ func (t *Tester) SetUpHierarchy() (
                        }
                }
 
-               mklines := NewMkLines(NewLines(NewCurrPath(relFilename), lines))
+               mklines := NewMkLines(NewLines(NewCurrPath(relFilename.AsPath()), lines))
                assertf(files[filename] == nil, "MkLines with name %q already exists.", filename)
                files[filename] = mklines
                return mklines
@@ -708,6 +770,8 @@ func (s *Suite) Test_Tester_SetUpHierarc
                "NOTE: subdir/env.mk:1: Text is: VAR= env")
 }
 
+// FinishSetup loads the pkgsrc infrastructure.
+// Later changes to the files in mk/ have no effect.
 func (t *Tester) FinishSetUp() {
        if t.seenSetupPkgsrc == 0 {
                t.InternalErrorf("Unnecessary t.FinishSetUp() since t.SetUpPkgsrc() has not been called.")
@@ -747,9 +811,15 @@ func (t *Tester) Main(args ...string) in
 
        argv := []string{"pkglint"}
        for _, arg := range args {
-               fileArg := t.File(NewRelPathString(arg))
-               if fileArg.Exists() {
-                       argv = append(argv, fileArg.String())
+               fileArg := NewCurrPathSlash(arg)
+               if fileArg.IsAbs() {
+                       argv = append(argv, arg)
+                       continue
+               }
+
+               file := t.File(NewRelPathString(arg))
+               if file.Exists() {
+                       argv = append(argv, file.String())
                } else {
                        argv = append(argv, arg)
                }
@@ -939,6 +1009,13 @@ func (t *Tester) NewLinesAt(filename Cur
 //
 // No actual file is created for the lines;
 // see SetUpFileMkLines for loading Makefile fragments with line continuations.
+//
+// After calling Tester.Chdir, NewMkLines creates the same object as
+// SetUpFileMkLines, just without anything being written to disk.
+// This can lead to strange error messages such as "Relative path %s does
+// not exist." because an intermediate directory in the path does not exist.
+//
+// If the filename is irrelevant for the particular test, take filename.mk.
 func (t *Tester) NewMkLines(filename CurrPath, lines ...string) *MkLines {
        basename := filename.Base()
        assertf(
@@ -1213,7 +1290,12 @@ func (t *Tester) CheckFileLinesDetab(fil
 // development.
 func (t *Tester) Use(...interface{}) {}
 
-func (t *Tester) Shquote(format string, rels ...Path) string {
+// Shquote renders the given paths into the message, adding shell quoting
+// around the paths if necessary.
+//
+// It is typically used to check the advertisement lines at the very end
+// of the pkglint output. ("Run \"pkglint -e %s\" to show explanations.")
+func (t *Tester) Shquote(format string, rels ...RelPath) string {
        var subs []interface{}
        for _, rel := range rels {
                quoted := shquote(t.tmpdir.JoinClean(rel).String())

Index: pkgsrc/pkgtools/pkglint/files/distinfo.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo.go:1.39 pkgsrc/pkgtools/pkglint/files/distinfo.go:1.40
--- pkgsrc/pkgtools/pkglint/files/distinfo.go:1.39      Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/distinfo.go   Sun Dec  8 00:06:38 2019
@@ -28,7 +28,7 @@ func CheckLinesDistinfo(pkg *Package, li
        distinfoIsCommitted := isCommitted(filename)
        ck := distinfoLinesChecker{
                pkg, lines, patchdir, distinfoIsCommitted,
-               nil, make(map[Path]distinfoFileInfo)}
+               nil, make(map[RelPath]distinfoFileInfo)}
        ck.parse()
        ck.check()
        CheckLinesTrailingEmptyLines(lines)
@@ -43,8 +43,8 @@ type distinfoLinesChecker struct {
        patchdir            PackagePath
        distinfoIsCommitted bool
 
-       filenames []Path // For keeping the order from top to bottom
-       infos     map[Path]distinfoFileInfo
+       filenames []RelPath // For keeping the order from top to bottom
+       infos     map[RelPath]distinfoFileInfo
 }
 
 func (ck *distinfoLinesChecker) parse() {
@@ -56,7 +56,7 @@ func (ck *distinfoLinesChecker) parse() 
        }
        llex.SkipEmptyOrNote()
 
-       prevFilename := NewPath("")
+       prevFilename := NewRelPath("")
        var hashes []distinfoHash
 
        isPatch := func() YesNoUnknown {
@@ -83,7 +83,7 @@ func (ck *distinfoLinesChecker) parse() 
                llex.Skip()
 
                m, alg, file, hash := match3(line.Text, `^(\w+) \((\w[^)]*)\) = (\S+(?: bytes)?)$`)
-               filename := NewPath(file)
+               filename := NewRelPathString(file)
                if !m {
                        line.Errorf("Invalid line: %s", line.Text)
                        continue
@@ -151,7 +151,7 @@ func (ck *distinfoLinesChecker) checkAlg
                }
 
                line.Warnf("Patch file %q does not exist in directory %q.",
-                       filename, line.PathToFile(ck.pkg.File(ck.patchdir)))
+                       filename, line.Rel(ck.pkg.File(ck.patchdir)))
                line.Explain(
                        "If the patches directory looks correct, the patch may have been",
                        "removed without updating the distinfo file.",
@@ -262,7 +262,7 @@ func (ck *distinfoLinesChecker) checkAlg
                        // Do not try to autofix anything in this situation.
                        // Wrong hashes are a serious issue.
                        line.Errorf("The %s checksum for %q is %s in distinfo, %s in %s.",
-                               alg, hash.filename, hash.hash, computed, line.PathToFile(distfile))
+                               alg, hash.filename, hash.hash, computed, line.Rel(distfile))
                        return
                }
        }
@@ -310,11 +310,11 @@ func (ck *distinfoLinesChecker) checkUnr
        }
 
        for _, file := range patchFiles {
-               patchName := NewPath(file.Name())
+               patchName := NewRelPathString(file.Name())
                if file.Mode().IsRegular() && ck.infos[patchName].isPatch != yes && patchName.HasPrefixText("patch-") {
                        line := NewLineWhole(ck.lines.Filename)
                        line.Errorf("Patch %q is not recorded. Run %q.",
-                               line.PathToFile(ck.pkg.File(ck.patchdir.JoinNoClean(patchName))),
+                               line.Rel(ck.pkg.File(ck.patchdir.JoinNoClean(patchName))),
                                bmake("makepatchsum"))
                }
        }
@@ -359,7 +359,7 @@ func (ck *distinfoLinesChecker) checkGlo
        if otherHash != nil {
                if !bytes.Equal(otherHash.hash, hashBytes) {
                        line.Errorf("The %s hash for %s is %s, which conflicts with %s in %s.",
-                               alg, filename, hash, hex.EncodeToString(otherHash.hash), line.RefToLocation(otherHash.location))
+                               alg, filename, hash, hex.EncodeToString(otherHash.hash), line.RelLocation(otherHash.location))
                }
        }
 }
@@ -373,7 +373,7 @@ func (ck *distinfoLinesChecker) checkUnc
        patchFileName := ck.patchdir.JoinNoClean(patchName)
        resolvedPatchFileName := ck.pkg.File(patchFileName)
        if ck.distinfoIsCommitted && !isCommitted(resolvedPatchFileName) {
-               line.Warnf("%s is registered in distinfo but not added to CVS.", line.PathToFile(resolvedPatchFileName))
+               line.Warnf("%s is registered in distinfo but not added to CVS.", line.Rel(resolvedPatchFileName))
        }
        if alg == "SHA1" {
                ck.checkPatchSha1(line, patchFileName, hash)
@@ -383,7 +383,7 @@ func (ck *distinfoLinesChecker) checkUnc
 func (ck *distinfoLinesChecker) checkPatchSha1(line *Line, patchFileName PackagePath, distinfoSha1Hex string) {
        lines := Load(ck.pkg.File(patchFileName), 0)
        if lines == nil {
-               line.Errorf("Patch %s does not exist.", patchFileName)
+               line.Errorf("Patch %s does not exist.", patchFileName.AsRelPath())
                return
        }
 
@@ -391,7 +391,7 @@ func (ck *distinfoLinesChecker) checkPat
        if distinfoSha1Hex != fileSha1Hex {
                fix := line.Autofix()
                fix.Errorf("SHA1 hash of %s differs (distinfo has %s, patch file has %s).",
-                       line.PathToFile(ck.pkg.File(patchFileName)), distinfoSha1Hex, fileSha1Hex)
+                       line.Rel(ck.pkg.File(patchFileName)), distinfoSha1Hex, fileSha1Hex)
                fix.Explain(
                        "To fix the hashes, either let pkglint --autofix do the work",
                        sprintf("or run %q.", bmake("makepatchsum")))
@@ -408,8 +408,8 @@ type distinfoFileInfo struct {
        hashes  []distinfoHash
 }
 
-func (info *distinfoFileInfo) filename() Path { return info.hashes[0].filename }
-func (info *distinfoFileInfo) line() *Line    { return info.hashes[0].line }
+func (info *distinfoFileInfo) filename() RelPath { return info.hashes[0].filename }
+func (info *distinfoFileInfo) line() *Line       { return info.hashes[0].line }
 
 func (info *distinfoFileInfo) algorithms() string {
        var algs []string
@@ -421,7 +421,7 @@ func (info *distinfoFileInfo) algorithms
 
 type distinfoHash struct {
        line      *Line
-       filename  Path
+       filename  RelPath
        algorithm string
        hash      string
 }
Index: pkgsrc/pkgtools/pkglint/files/mkparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser.go:1.39 pkgsrc/pkgtools/pkglint/files/mkparser.go:1.40
--- pkgsrc/pkgtools/pkglint/files/mkparser.go:1.39      Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mkparser.go   Sun Dec  8 00:06:38 2019
@@ -8,7 +8,7 @@ import (
 // MkParser wraps a Parser and provides methods for parsing
 // things related to Makefiles.
 type MkParser struct {
-       Line         *Line
+       diag         Diagnoser
        mklex        *MkLexer
        lexer        *textproc.Lexer
        EmitWarnings bool
@@ -30,9 +30,9 @@ func (p *MkParser) Rest() string {
 // The text argument is assumed to be after unescaping the # character,
 // which means the # is a normal character and does not introduce a Makefile comment.
 // For VarUse, this distinction is irrelevant.
-func NewMkParser(line *Line, text string) *MkParser {
-       mklex := NewMkLexer(text, line)
-       return &MkParser{line, mklex, mklex.lexer, line != nil}
+func NewMkParser(diag Autofixer, text string) *MkParser {
+       mklex := NewMkLexer(text, diag)
+       return &MkParser{diag, mklex, mklex.lexer, diag != nil}
 }
 
 // MkCond parses a condition like ${OPSYS} == "NetBSD".
@@ -90,10 +90,6 @@ func (p *MkParser) mkCondAnd() *MkCond {
        return &MkCond{And: atoms}
 }
 
-// mkCondLiteralChars contains the characters that may be used outside
-// quotes in a comparison condition such as ${PKGPATH} == category/package.
-var mkCondLiteralChars = textproc.NewByteSet("+---./0-9A-Z_a-z")
-
 func (p *MkParser) mkCondCompare() *MkCond {
        if trace.Tracing {
                defer trace.Call1(p.Rest())()
@@ -158,7 +154,7 @@ func (p *MkParser) mkCondCompare() *MkCo
                        return &MkCond{Compare: &MkCondCompare{*lhs, op, *rhs}}
                }
 
-               if str := lexer.NextBytesSet(mkCondLiteralChars); str != "" {
+               if str := lexer.NextBytesSet(mkCondStringLiteralUnquoted); str != "" {
                        return &MkCond{Compare: &MkCondCompare{*lhs, op, MkCondTerm{Str: str}}}
                }
        }
@@ -247,7 +243,7 @@ func (p *MkParser) mkCondFunc() *MkCond 
                if varname := p.mklex.Varname(); varname != "" {
                        modifiers := p.mklex.VarUseModifiers(varname, ')')
                        if lexer.SkipByte(')') {
-                               return &MkCond{Empty: &MkVarUse{varname, modifiers}}
+                               return &MkCond{Empty: NewMkVarUse(varname, modifiers...)}
                        }
                }
 
@@ -527,7 +523,7 @@ func (w *MkCondWalker) Walk(cond *MkCond
                if callback.VarUse != nil {
                        // This is not really a VarUse, it's more a VarUseDefined.
                        // But in practice they are similar enough to be treated the same.
-                       callback.VarUse(&MkVarUse{cond.Defined, nil})
+                       callback.VarUse(NewMkVarUse(cond.Defined))
                }
 
        case cond.Term != nil && cond.Term.Var != nil:
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.39 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.40
--- pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.39   Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go        Sun Dec  8 00:06:38 2019
@@ -180,7 +180,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
                Downgraded, "category/package", "1.2", "author7", "2018-01-07"})
 
        t.CheckOutputLines(
-               "WARN: ~/doc/CHANGES-2018:1: Year \"2015\" for category/package does not match the filename ~/doc/CHANGES-2018.",
+               "WARN: ~/doc/CHANGES-2018:1: Year \"2015\" for category/package does not match the filename CHANGES-2018.",
                "WARN: ~/doc/CHANGES-2018:6: Date \"2018-01-06\" for category/package is earlier than \"2018-01-09\" in line 5.",
                "WARN: ~/doc/CHANGES-2018:8: Invalid doc/CHANGES line: \tReworked category/package to 1.2 [author8 2018-01-08]",
                "WARN: ~/doc/CHANGES-2018:10: Invalid doc/CHANGES line: \ttoo few fields",
@@ -254,7 +254,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
                "WARN: ~/doc/CHANGES-2018:6: Date \"2018-01-01\" for sysutils/checkperms is earlier than \"2018-01-05\" in line 5.",
                "WARN: ~/doc/CHANGES-2018:7: Package changes should be indented using a single tab, not \"\\t\\t\".",
                "WARN: ~/doc/CHANGES-2018:8: Invalid doc/CHANGES line: \tInvalid pkgpath to 1.16 [rillig 2019-06-16]",
-               "WARN: ~/doc/CHANGES-2018:9: Year \"2019\" for category/package does not match the filename ~/doc/CHANGES-2018.",
+               "WARN: ~/doc/CHANGES-2018:9: Year \"2019\" for category/package does not match the filename CHANGES-2018.",
                "4 warnings found.",
                t.Shquote("(Run \"pkglint -e -Cglobal -Wall %s\" to show explanations.)", "."))
 }
@@ -688,7 +688,9 @@ func (s *Suite) Test_Pkgsrc_loadTools__B
                "\t@${ECHO} ${PKG_SYSCONFDIR} ${VARBASE}")
        t.CreateFileLines("mk/bsd.pkg.mk",
                MkCvsID,
-               "_BUILD_DEFS+=\tPKG_SYSCONFBASEDIR PKG_SYSCONFDIR")
+               "_BUILD_DEFS+=\tPKG_SYSCONFBASEDIR PKG_SYSCONFDIR",
+               "",
+               "BUILD_DEFINITIONS+=\tIGNORED\t")
        t.CreateFileLines("mk/defaults/mk.conf",
                MkCvsID,
                "",
@@ -707,6 +709,48 @@ func (s *Suite) Test_Pkgsrc_loadTools__B
                        "The user-defined variable VARBASE is used but not added to BUILD_DEFS.")
 }
 
+func (s *Suite) Test_Pkgsrc_loadTools__conditional_in_mk_tools(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/tools/defaults.mk",
+               ".if 0",
+               "USE_TOOLS+=\tcond-false",
+               ".endif",
+               ".if 1",
+               "USE_TOOLS+=\tcond-true",
+               ".endif",
+               "USE_TOOLS+=\tunconditional")
+       t.FinishSetUp()
+
+       // The above FinishSetUp implicitly and indirectly calls loadTools.
+
+       t.CheckEquals(G.Pkgsrc.Tools.ByName("cond-false").Validity, Nowhere)
+       t.CheckEquals(G.Pkgsrc.Tools.ByName("cond-true").Validity, Nowhere)
+       t.CheckEquals(G.Pkgsrc.Tools.ByName("unconditional").Validity, AtRunTime)
+}
+
+func (s *Suite) Test_Pkgsrc_loadTools__conditional_in_bsd_pkg_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/bsd.pkg.mk",
+               ".if 0",
+               "USE_TOOLS+=\tcond-false",
+               ".endif",
+               ".if 1",
+               "USE_TOOLS+=\tcond-true",
+               ".endif",
+               "USE_TOOLS+=\tunconditional")
+       t.FinishSetUp()
+
+       // The above FinishSetUp implicitly and indirectly calls loadTools.
+
+       t.CheckEquals(G.Pkgsrc.Tools.ByName("cond-false").Validity, Nowhere)
+       t.CheckEquals(G.Pkgsrc.Tools.ByName("cond-true").Validity, Nowhere)
+       t.CheckEquals(G.Pkgsrc.Tools.ByName("unconditional").Validity, AtRunTime)
+}
+
 func (s *Suite) Test_Pkgsrc_loadTools__no_tools_found(c *check.C) {
        t := s.Init(c)
 
@@ -809,6 +853,41 @@ func (s *Suite) Test_Pkgsrc_loadUntypedV
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_Pkgsrc_loadUntypedVars__loop_variable(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("mk/check/check-files.mk",
+               MkCvsID,
+               "${:U}=\t${CHECK_FILES_SKIP:@f@${f}@}",
+               "\t${/} ${} ${UNKNOWN}")
+
+       G.Pkgsrc.loadUntypedVars()
+
+       ignored := func(varname string) {
+               vartype := G.Pkgsrc.VariableType(nil, varname)
+               t.Check(vartype, check.IsNil, check.Commentf("%s", varname))
+       }
+       added := func(varname string, basicType *BasicType) {
+               vartype := G.Pkgsrc.VariableType(nil, "CHECK_FILES_SKIP")
+               if t.Check(vartype, check.NotNil) {
+                       t.CheckEquals(vartype.basicType, BtPathPattern)
+               }
+       }
+
+       added("CHECK_FILES_SKIP", BtPathPattern)
+       added("UNKNOWN", BtUnknown)
+
+       ignored("")
+       ignored("f")
+       ignored(".f.")
+       ignored("/")
+       ignored("PREFIX")
+
+       t.CheckOutputLines(
+               "WARN: ~/mk/check/check-files.mk:3: " +
+                       "Invalid part \"/\" after variable name \"\".")
+}
+
 func (s *Suite) Test_Pkgsrc_Latest__multiple_candidates(c *check.C) {
        t := s.Init(c)
 
@@ -1109,7 +1188,7 @@ func (s *Suite) Test_Pkgsrc_guessVariabl
        // aclpUseLoadtime. Therefore there should be a warning about the VarUse in
        // the .if line. As of March 2019, pkglint skips the permissions check for
        // guessed variables since that variable might have an entirely different
-       // meaning; see MkLineChecker.checkVarusePermissions.
+       // meaning; see MkVarUseChecker.checkPermissions.
        //
        // There is no warning for the += operator in line 3 since the variable type
        // (although guessed) is a list of things, and lists may be appended to.
@@ -1148,10 +1227,11 @@ func (s *Suite) Test_Pkgsrc_checkTopleve
        t.Main("-r", "-Cglobal", ".")
 
        t.CheckOutputLines(
-               "WARN: ~/category/package2/Makefile:11: License file ../../licenses/missing does not exist.",
+               "ERROR: ~/category/package2/Makefile:11: License file ../../licenses/missing does not exist.",
                "WARN: ~/licenses/gnu-gpl-v2: This license seems to be unused.", // Added by Tester.SetUpPkgsrc
                "WARN: ~/licenses/gnu-gpl-v3: This license seems to be unused.",
-               "3 warnings found.")
+               "1 error and 2 warnings found.",
+               t.Shquote("(Run \"pkglint -e -r -Cglobal %s\" to show explanations.)", "."))
 }
 
 func (s *Suite) Test_Pkgsrc_ReadDir(c *check.C) {
@@ -1181,7 +1261,7 @@ func (s *Suite) Test_Pkgsrc_Relpath(c *c
        t.Chdir(".")
        t.CheckEquals(G.Pkgsrc.topdir, NewCurrPath("."))
 
-       test := func(from, to CurrPath, result Path) {
+       test := func(from, to CurrPath, result RelPath) {
                t.CheckEquals(G.Pkgsrc.Relpath(from, to), result)
        }
 
Index: pkgsrc/pkgtools/pkglint/files/util_test.go
diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.39 pkgsrc/pkgtools/pkglint/files/util_test.go:1.40
--- pkgsrc/pkgtools/pkglint/files/util_test.go:1.39     Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/util_test.go  Sun Dec  8 00:06:38 2019
@@ -2,8 +2,11 @@ package pkglint
 
 import (
        "errors"
+       "fmt"
        "gopkg.in/check.v1"
        "os"
+       "reflect"
+       "sort"
        "testing"
        "time"
 )
@@ -104,12 +107,12 @@ func (s *Suite) Test_isEmptyDir__and_get
 
        if dir := t.File("."); true {
                t.CheckEquals(isEmptyDir(dir), true)
-               t.CheckDeepEquals(getSubdirs(dir), []Path(nil))
+               t.CheckDeepEquals(getSubdirs(dir), []RelPath(nil))
 
                t.CreateFileLines("somedir/file")
 
                t.CheckEquals(isEmptyDir(dir), false)
-               t.CheckDeepEquals(getSubdirs(dir), []Path{"somedir"})
+               t.CheckDeepEquals(getSubdirs(dir), []RelPath{"somedir"})
        }
 
        if absent := t.File("nonexistent"); true {
@@ -129,7 +132,27 @@ func (s *Suite) Test_getSubdirs(c *check
        t.CreateFileLines("empty/file")
        c.Check(os.Remove(t.File("empty/file").String()), check.IsNil)
 
-       t.CheckDeepEquals(getSubdirs(t.File(".")), []Path{"subdir"})
+       t.CheckDeepEquals(getSubdirs(t.File(".")), []RelPath{"subdir"})
+}
+
+func (s *Suite) Test_isIgnoredFilename(c *check.C) {
+       t := s.Init(c)
+
+       test := func(filename string, isIgnored bool) {
+               t.CheckEquals(isIgnoredFilename(filename), isIgnored)
+       }
+
+       test("filename.mk", false)
+       test(".gitignore", false)
+       test(".git", true)
+       test(".gitattributes", false)
+       test("CVS", true)
+       test(".svn", true)
+       test(".hg", true)
+
+       // There is actually an IDEA plugin for pkgsrc.
+       // See https://github.com/rillig/intellij-pkgsrc.
+       test(".idea", true)
 }
 
 func (s *Suite) Test_isLocallyModified(c *check.C) {
@@ -401,6 +424,38 @@ func emptyToNil(slice []string) []string
        return slice
 }
 
+func (s *Suite) Test_containsVarRef(c *check.C) {
+       t := s.Init(c)
+
+       test := func(str string, containsVar bool) {
+               // TODO: rename to containsVarUse
+               t.CheckEquals(containsVarRef(str), containsVar)
+       }
+
+       test("", false)
+       test("$", false) // A syntax error.
+
+       // See the bmake manual page.
+       test("$>", false) // FIXME: true; .ALLSRC
+       test("$!", false) // FIXME: true; .ARCHIVE
+       test("$<", false) // FIXME: true; .IMPSRC
+       test("$%", false) // FIXME: true; .MEMBER
+       test("$?", false) // FIXME: true; .OODATE
+       test("$*", false) // FIXME: true; .PREFIX
+       test("$@", false) // FIXME: true; .TARGET
+
+       test("$V", false) // FIXME: true
+       test("$v", false) // FIXME: true
+       test("${Var}", true)
+       test("${VAR.${param}}", true)
+       test("$(VAR)", true)
+
+       test("$$", false)     // An escaped dollar character.
+       test("$$(VAR)", true) // FIXME: false; An escaped dollar character; probably a subshell.
+       test("$${VAR}", true) // FIXME: false; An escaped dollar character; probably a shell variable.
+       test("$$VAR", false)  // An escaped dollar character.
+}
+
 func (s *Suite) Test_hasAlnumPrefix(c *check.C) {
        t := s.Init(c)
 
@@ -553,6 +608,10 @@ func (s *Suite) Test_Scope_FirstDefiniti
        // These sneaky variables with implicit definition are an edge
        // case that only few people actually know. It's better that way.
        t.Check(scope.FirstDefinition("SNEAKY"), check.IsNil)
+
+       t.CheckOutputLines(
+               "ERROR: fname.mk:3: Assignment modifiers like \":=\" " +
+                       "must not be used at all.")
 }
 
 func (s *Suite) Test_Scope_Commented(c *check.C) {
@@ -1092,3 +1151,22 @@ func (s *Suite) Test_LazyStringBuilder_R
        t.CheckEquals(sb.usingBuf, true)
        t.CheckDeepEquals(sb.buf, []byte("x"))
 }
+
+// sortedKeys takes the keys from an arbitrary map,
+// converts them to strings if necessary,
+// and then returns them sorted.
+//
+// It is only available during tests since it uses reflection.
+func keys(m interface{}) []string {
+       var keys []string
+       for _, key := range reflect.ValueOf(m).MapKeys() {
+               switch key := key.Interface().(type) {
+               case fmt.Stringer:
+                       keys = append(keys, key.String())
+               default:
+                       keys = append(keys, key.(string))
+               }
+       }
+       sort.Strings(keys)
+       return keys
+}

Index: pkgsrc/pkgtools/pkglint/files/files_test.go
diff -u pkgsrc/pkgtools/pkglint/files/files_test.go:1.30 pkgsrc/pkgtools/pkglint/files/files_test.go:1.31
--- pkgsrc/pkgtools/pkglint/files/files_test.go:1.30    Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/files_test.go Sun Dec  8 00:06:38 2019
@@ -48,6 +48,20 @@ func (s *Suite) Test_Load(c *check.C) {
                "FATAL: ~/empty: Must not be empty.")
 }
 
+// Up to 2019-12-04, pkglint suppressed fatal errors when it was started
+// with the --autofix option. This was another case where the clear
+// separation between diagnostics and technical errors had been confused.
+func (s *Suite) Test_Load__not_found_in_autofix_mode(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("--autofix")
+       t.Chdir(".")
+
+       t.ExpectFatal(
+               func() { Load("nonexistent", MustSucceed) },
+               "FATAL: nonexistent: Cannot be read.")
+}
+
 func (s *Suite) Test_convertToLogicalLines__no_continuation(c *check.C) {
        t := s.Init(c)
 
Index: pkgsrc/pkgtools/pkglint/files/substcontext.go
diff -u pkgsrc/pkgtools/pkglint/files/substcontext.go:1.30 pkgsrc/pkgtools/pkglint/files/substcontext.go:1.31
--- pkgsrc/pkgtools/pkglint/files/substcontext.go:1.30  Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/substcontext.go       Sun Dec  8 00:06:38 2019
@@ -148,7 +148,7 @@ func (ctx *SubstContext) Varassign(mklin
                if G.Pkg != nil && (value == "pre-configure" || value == "post-configure") {
                        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))
+                                       value, mkline.RelMkLine(noConfigureLine))
                                mkline.Explain(
                                        "To fix this properly, remove the definition of NO_CONFIGURE.")
                        }

Index: pkgsrc/pkgtools/pkglint/files/licenses.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses.go:1.29 pkgsrc/pkgtools/pkglint/files/licenses.go:1.30
--- pkgsrc/pkgtools/pkglint/files/licenses.go:1.29      Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/licenses.go   Sun Dec  8 00:06:38 2019
@@ -28,32 +28,23 @@ func (lc *LicenseChecker) checkName(lice
        if G.Pkg != nil {
                if mkline := G.Pkg.vars.FirstDefinition("LICENSE_FILE"); mkline != nil {
                        rel := mkline.ResolveVarsInRelativePath(NewRelPathString(mkline.Value()))
-                       licenseFile = G.Pkg.File(NewPackagePath(rel.AsPath()))
+                       licenseFile = G.Pkg.File(NewPackagePath(rel))
                }
        }
        if licenseFile.IsEmpty() {
-               licenseFile = G.Pkgsrc.File("licenses").JoinNoClean(NewPath(license))
+               licenseFile = G.Pkgsrc.File("licenses").JoinNoClean(NewRelPathString(license))
                G.InterPackage.UseLicense(license)
        }
 
        if !licenseFile.IsFile() {
-               lc.MkLine.Warnf("License file %s does not exist.",
-                       lc.MkLine.PathToFile(licenseFile))
-       }
-
-       switch license {
-       case "fee-based-commercial-use",
-               "no-commercial-use",
-               "no-profit",
-               "no-redistribution",
-               "shareware":
-               lc.MkLine.Errorf("License %q must not be used.", license)
+               lc.MkLine.Errorf("License file %s does not exist.",
+                       lc.MkLine.Rel(licenseFile))
                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.",
+                       sprintf("Run %q to see which licenses the package uses.",
+                               bmake("guess-license")),
                        "",
-                       seeGuide("Handling licenses", "handling-licenses"))
+                       sprintf("For more information about licenses, %s.",
+                               seeGuide("Handling licenses", "handling-licenses")))
        }
 }
 

Index: pkgsrc/pkgtools/pkglint/files/licenses_test.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.26 pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.27
--- pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.26 Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/licenses_test.go      Sun Dec  8 00:06:38 2019
@@ -22,16 +22,14 @@ func (s *Suite) Test_LicenseChecker_Chec
        }
 
        test("gpl-v2",
-               "WARN: ~/Makefile:1: License file licenses/gpl-v2 does not exist.")
+               "ERROR: ~/Makefile:1: License file licenses/gpl-v2 does not exist.")
 
        test("no-profit shareware",
                "ERROR: ~/Makefile:1: Parse error for license condition \"no-profit shareware\".")
 
        test("no-profit AND shareware",
-               "WARN: ~/Makefile:1: License file licenses/no-profit does not exist.",
-               "ERROR: ~/Makefile:1: License \"no-profit\" must not be used.",
-               "WARN: ~/Makefile:1: License file licenses/shareware does not exist.",
-               "ERROR: ~/Makefile:1: License \"shareware\" must not be used.")
+               "ERROR: ~/Makefile:1: License file licenses/no-profit does not exist.",
+               "ERROR: ~/Makefile:1: License file licenses/shareware does not exist.")
 
        test("gnu-gpl-v2",
                nil...)
Index: pkgsrc/pkgtools/pkglint/files/toplevel.go
diff -u pkgsrc/pkgtools/pkglint/files/toplevel.go:1.26 pkgsrc/pkgtools/pkglint/files/toplevel.go:1.27
--- pkgsrc/pkgtools/pkglint/files/toplevel.go:1.26      Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/toplevel.go   Sun Dec  8 00:06:38 2019
@@ -2,7 +2,7 @@ package pkglint
 
 type Toplevel struct {
        dir            CurrPath
-       previousSubdir Path
+       previousSubdir RelPath
        subdirs        []CurrPath
 }
 
@@ -36,7 +36,7 @@ func CheckdirToplevel(dir CurrPath) {
 }
 
 func (ctx *Toplevel) checkSubdir(mkline *MkLine) {
-       subdir := NewPath(mkline.Value())
+       subdir := NewRelPathString(mkline.Value())
 
        if mkline.IsCommentedVarassign() {
                if !mkline.HasComment() || mkline.Comment() == "" {
Index: pkgsrc/pkgtools/pkglint/files/vardefs_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.26 pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.27
--- pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.26  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/vardefs_test.go       Sun Dec  8 00:06:38 2019
@@ -2,6 +2,17 @@ package pkglint
 
 import "gopkg.in/check.v1"
 
+func (s *Suite) Test_VarTypeRegistry_acl__assertion(c *check.C) {
+       t := s.Init(c)
+
+       reg := NewVarTypeRegistry()
+       reg.pkg("VARNAME", BtUnknown)
+
+       t.ExpectPanic(
+               func() { reg.pkg("VARNAME", BtUnknown) },
+               "Pkglint internal error: Variable \"VARNAME\" must only be defined once.")
+}
+
 func (s *Suite) Test_VarTypeRegistry_compilerLanguages(c *check.C) {
        t := s.Init(c)
 
@@ -184,6 +195,18 @@ func (s *Suite) Test_VarTypeRegistry_enu
                        "file matching \"^(\\\\w+)\\\\.mk$\".")
 }
 
+func (s *Suite) Test_VarTypeRegistry_options__assertion(c *check.C) {
+       t := s.Init(c)
+
+       reg := NewVarTypeRegistry()
+
+       t.ExpectAssert(func() {
+               reg.options(
+                       SystemProvided,
+                       []vartypeOptions{DefinedIfInScope, NonemptyIfDefined})
+       })
+}
+
 func (s *Suite) Test_VarTypeRegistry_Init(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/line.go
diff -u pkgsrc/pkgtools/pkglint/files/line.go:1.41 pkgsrc/pkgtools/pkglint/files/line.go:1.42
--- pkgsrc/pkgtools/pkglint/files/line.go:1.41  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/line.go       Sun Dec  8 00:06:38 2019
@@ -42,14 +42,14 @@ type Location struct {
        lastLine  int32 // usually the same as firstLine, may differ in Makefiles
 }
 
-func (loc *Location) String() string {
-       return loc.Filename.String() + ":" + loc.Linenos()
-}
-
 func NewLocation(filename CurrPath, firstLine, lastLine int) Location {
        return Location{filename, int32(firstLine), int32(lastLine)}
 }
 
+func (loc *Location) String() string {
+       return loc.Filename.String() + ":" + loc.Linenos()
+}
+
 func (loc *Location) Linenos() string {
        switch {
        case loc.firstLine == -1:
@@ -63,6 +63,10 @@ func (loc *Location) Linenos() string {
        }
 }
 
+func (loc *Location) File(rel RelPath) CurrPath {
+       return loc.Filename.DirNoClean().JoinNoClean(rel)
+}
+
 // Line represents a line of text from a file.
 type Line struct {
        // TODO: Consider storing pointers to the Filename and Basename instead of strings to save memory.
@@ -102,25 +106,24 @@ func NewLineWhole(filename CurrPath) *Li
        return NewLineMulti(filename, 0, 0, "", nil)
 }
 
-// RefTo returns a reference to another line,
+// RelLine returns a reference to another line,
 // which can be in the same file or in a different file.
-func (line *Line) RefTo(other *Line) string {
-       return line.RefToLocation(other.Location)
+func (line *Line) RelLine(other *Line) string {
+       return line.RelLocation(other.Location)
 }
 
-func (line *Line) RefToLocation(other Location) string {
+func (line *Line) RelLocation(other Location) string {
        if line.Filename != other.Filename {
-               return line.PathToFile(other.Filename).String() + ":" + other.Linenos()
+               return line.Rel(other.Filename).String() + ":" + other.Linenos()
        }
        return "line " + other.Linenos()
 }
 
-// PathToFile returns the relative path from this line to the given file path.
+// Rel returns the relative path from this line to the given file path.
 // This is typically used for arguments in diagnostics, which should always be
 // relative to the line with which the diagnostic is associated.
-func (line *Line) PathToFile(filePath CurrPath) Path {
-       // FIXME: consider DirNoClean
-       return G.Pkgsrc.Relpath(line.Filename.DirClean(), filePath)
+func (line *Line) Rel(other CurrPath) RelPath {
+       return G.Pkgsrc.Relpath(line.Filename.DirNoClean(), other)
 }
 
 func (line *Line) IsMultiline() bool {

Index: pkgsrc/pkgtools/pkglint/files/logging.go
diff -u pkgsrc/pkgtools/pkglint/files/logging.go:1.32 pkgsrc/pkgtools/pkglint/files/logging.go:1.33
--- pkgsrc/pkgtools/pkglint/files/logging.go:1.32       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/logging.go    Sun Dec  8 00:06:38 2019
@@ -8,6 +8,17 @@ import (
        "strings"
 )
 
+// Diagnoser provides the standard way of producing errors, warnings
+// and notes, and explanations for them.
+//
+// For convenience, it is implemented by several types in pkglint.
+type Diagnoser interface {
+       Errorf(format string, args ...interface{})
+       Warnf(format string, args ...interface{})
+       Notef(format string, args ...interface{})
+       Explain(explanation ...string)
+}
+
 type Logger struct {
        Opts LoggerOpts
 
@@ -93,7 +104,19 @@ func (l *Logger) Explain(explanation ...
 //
 // See Logf for logging arbitrary messages.
 func (l *Logger) Diag(line *Line, level *LogLevel, format string, args ...interface{}) {
-       if l.IsAutofix() {
+       if G.Testing {
+               for _, arg := range args {
+                       switch arg.(type) {
+                       case int, string, error:
+                       default:
+                               // All paths in diagnostics must be relative to the line.
+                               // To achieve that, call line.File(currPath).
+                               _ = arg.(RelPath)
+                       }
+               }
+       }
+
+       if l.IsAutofix() && level != Fatal {
                // In these two cases, the only interesting diagnostics are those that can
                // be fixed automatically. These are logged by Autofix.Apply.
                l.suppressExpl = true
@@ -282,17 +305,20 @@ func (l *Logger) Logf(level *LogLevel, f
 
 // TechErrorf logs a technical error on the error output.
 //
-// location must be a slash-separated filename, such as the one in
-// Location.Filename. It may be followed by the usual ":123" for line numbers.
-//
 // For diagnostics, use Logf instead.
 func (l *Logger) TechErrorf(location CurrPath, format string, args ...interface{}) {
        msg := sprintf(format, args...)
+
+       locationStr := ""
+       if !location.IsEmpty() {
+               locationStr = location.String() + ": "
+       }
+
        var diag string
        if l.Opts.GccOutput {
-               diag = sprintf("%s: %s: %s\n", location, Error.GccName, msg)
+               diag = sprintf("%s%s: %s\n", locationStr, Error.GccName, msg)
        } else {
-               diag = sprintf("%s: %s: %s\n", Error.TraditionalName, location, msg)
+               diag = sprintf("%s: %s%s\n", Error.TraditionalName, locationStr, msg)
        }
        l.err.Write(escapePrintable(diag))
 }
Index: pkgsrc/pkgtools/pkglint/files/patches_test.go
diff -u pkgsrc/pkgtools/pkglint/files/patches_test.go:1.32 pkgsrc/pkgtools/pkglint/files/patches_test.go:1.33
--- pkgsrc/pkgtools/pkglint/files/patches_test.go:1.32  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/patches_test.go       Sun Dec  8 00:06:38 2019
@@ -538,6 +538,64 @@ func (s *Suite) Test_PatchChecker_Check_
                "ERROR: ~/patch-aa: Contains no patch.")
 }
 
+func (s *Suite) Test_PatchChecker_Check__add_file(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.SetUpFileLines("patch-aa",
+               CvsID,
+               "",
+               "This patch creates a new file.",
+               "",
+               "--- /dev/null",
+               "+++ added-file",
+               "@@ -0,0 +1,1 @@",
+               "+ added line")
+
+       CheckLinesPatch(lines)
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_PatchChecker_Check__delete_file(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.SetUpFileLines("patch-aa",
+               CvsID,
+               "",
+               "This patch deletes an existing file.",
+               "",
+               "--- deleted-file",
+               "+++ /dev/null",
+               "@@ -1,1 +0,0 @@",
+               "- deleted line")
+
+       CheckLinesPatch(lines)
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_PatchChecker_Check__absolute_path(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.SetUpFileLines("patch-aa",
+               CvsID,
+               "",
+               "This patch deletes an existing file.",
+               "",
+               "--- /absolute",
+               "+++ /absolute",
+               "@@ -1,1 +1,1 @@",
+               "- deleted line",
+               "+ added line")
+
+       CheckLinesPatch(lines)
+
+       // FIXME: Patches must not apply to absolute paths.
+       // The only allowed exception is /dev/null.
+       // ^(---|\+\+\+) /(?!dev/null)
+       t.CheckOutputEmpty()
+}
+
 func (s *Suite) Test_PatchChecker_checkUnifiedDiff__lines_at_end(c *check.C) {
        t := s.Init(c)
 
@@ -630,6 +688,52 @@ func (s *Suite) Test_PatchChecker_checkC
                "ERROR: ~/patch-aa:9: This code must not be included in patches.")
 }
 
+// I'm not sure whether configure.in is really relevant for this check.
+// As of December 2019, there is absolutely no package that uses
+// CONFIGURE_SCRIPTS_OVERRIDE.
+func (s *Suite) Test_PatchChecker_checkConfigure__configure_in(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.SetUpFileLines("patch-aa",
+               CvsID,
+               "",
+               "Documentation",
+               "",
+               "--- configure.in.orig",
+               "+++ configure.in",
+               "@@ -1,1 +1,1 @@",
+               "-old line",
+               "+: Avoid regenerating within pkgsrc")
+
+       CheckLinesPatch(lines)
+
+       t.CheckOutputLines(
+               "ERROR: ~/patch-aa:9: This code must not be included in patches.")
+}
+
+// I'm not sure whether configure.ac is really relevant for this check.
+// As of December 2019, there is absolutely no package that uses
+// CONFIGURE_SCRIPTS_OVERRIDE.
+func (s *Suite) Test_PatchChecker_checkConfigure__configure_ac(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.SetUpFileLines("patch-aa",
+               CvsID,
+               "",
+               "Documentation",
+               "",
+               "--- configure.ac.orig",
+               "+++ configure.ac",
+               "@@ -1,1 +1,1 @@",
+               "-old line",
+               "+: Avoid regenerating within pkgsrc")
+
+       CheckLinesPatch(lines)
+
+       t.CheckOutputLines(
+               "ERROR: ~/patch-aa:9: This code must not be included in patches.")
+}
+
 func (s *Suite) Test_PatchChecker_checktextCvsID(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/mklexer.go
diff -u pkgsrc/pkgtools/pkglint/files/mklexer.go:1.3 pkgsrc/pkgtools/pkglint/files/mklexer.go:1.4
--- pkgsrc/pkgtools/pkglint/files/mklexer.go:1.3        Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mklexer.go    Sun Dec  8 00:06:38 2019
@@ -19,11 +19,11 @@ import (
 // these edge cases anyway.
 type MkLexer struct {
        lexer *textproc.Lexer
-       line  *Line
+       diag  Autofixer
 }
 
-func NewMkLexer(text string, line *Line) *MkLexer {
-       return &MkLexer{textproc.NewLexer(text), line}
+func NewMkLexer(text string, diag Autofixer) *MkLexer {
+       return &MkLexer{textproc.NewLexer(text), diag}
 }
 
 // MkTokens splits a text like in the following example:
@@ -58,6 +58,7 @@ func (p *MkLexer) MkTokens() ([]*MkToken
        return tokens, lexer.Rest()
 }
 
+// VarUse parses a variable expression like ${VAR}, $@, ${VAR:Mpattern:Ox}.
 func (p *MkLexer) VarUse() *MkVarUse {
        rest := p.lexer.Rest()
        if len(rest) < 2 || rest[0] != '$' {
@@ -82,7 +83,7 @@ func (p *MkLexer) VarUse() *MkVarUse {
                //
                // 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}
+               return NewMkVarUse(rest[1:2])
 
        default:
                return p.varUseAlnum()
@@ -115,9 +116,9 @@ func (p *MkLexer) varUseBrace(usingRound
 
        closed := lexer.SkipByte(closing)
 
-       if p.line != nil {
+       if p.diag != nil {
                if !closed {
-                       p.line.Warnf("Missing closing %q for %q.", string(rune(closing)), varExpr)
+                       p.Warnf("Missing closing %q for %q.", string(rune(closing)), varExpr)
                }
 
                if usingRoundParen && closed {
@@ -127,18 +128,18 @@ func (p *MkLexer) varUseBrace(usingRound
                        edit[len(edit)-1] = '}'
                        bracesVaruse := string(edit)
 
-                       fix := p.line.Autofix()
+                       fix := p.Autofix()
                        fix.Warnf("Please use curly braces {} instead of round parentheses () for %s.", varExpr)
                        fix.Replace(parenVaruse, bracesVaruse)
                        fix.Apply()
                }
 
-               if len(varExpr) > len(varname) && !(&MkVarUse{varExpr, modifiers}).IsExpression() {
-                       p.line.Warnf("Invalid part %q after variable name %q.", varExpr[len(varname):], varname)
+               if len(varExpr) > len(varname) && !NewMkVarUse(varExpr, modifiers...).IsExpression() {
+                       p.Warnf("Invalid part %q after variable name %q.", varExpr[len(varname):], varname)
                }
        }
 
-       return &MkVarUse{varExpr, modifiers}
+       return NewMkVarUse(varExpr, modifiers...)
 }
 
 func (p *MkLexer) Varname() string {
@@ -173,6 +174,32 @@ func (p *MkLexer) varUseText(closing byt
        return lexer.Since(start)
 }
 
+// varUseText parses any text up to the closing mark, including any colons.
+//
+// This is used for the :from=to modifier.
+//
+// See devel/bmake/files/var.c:/eqFound = FALSE/
+func (p *MkLexer) varUseModifierSysV(closing byte) (string, string) {
+       lexer := p.lexer
+       start := lexer.Mark()
+       re := regcomp(regex.Pattern(condStr(closing == '}', `^([^$\\}]|\$\$|\\.)+`, `^([^$\\)]|\$\$|\\.)+`)))
+
+       noVars := NewLazyStringBuilder(lexer.Rest())
+       // pkglint deviates from bmake here by properly parsing nested
+       // variables. bmake only counts opening and closing characters.
+       for {
+               if p.VarUse() != nil {
+                       continue
+               }
+               m := lexer.NextRegexp(re)
+               if len(m) == 0 {
+                       break
+               }
+               noVars.WriteString(m[0])
+       }
+       return lexer.Since(start), noVars.String()
+}
+
 // VarUseModifiers parses the modifiers of a variable being used, such as :Q, :Mpattern.
 //
 // See the bmake manual page.
@@ -224,7 +251,7 @@ func (p *MkLexer) varUseModifier(varname
                }
 
                if hasPrefix(mod, "ts") {
-                       return p.varUseModifierSeparator(mod, closing, lexer, varname, mark)
+                       return p.varUseModifierTs(mod, closing, lexer, varname, mark)
                }
 
        case 'D', 'U':
@@ -255,29 +282,75 @@ func (p *MkLexer) varUseModifier(varname
                        p.varUseText(closing)
                        return lexer.Since(mark)
                }
+
+       case ':':
+               lexer.Skip(1)
+               if !lexer.SkipRegexp(regcomp(`^[!+?]?=`)) {
+                       break
+               }
+
+               // The corresponding code in bmake is much more complicated
+               // because it evaluates the expression immediately instead of
+               // only parsing it.
+               //
+               // This modifier should not be used at all since it hides
+               // variable assignments deep in a line.
+               //
+               // It could also happen that the assignment happens in an
+               // indirect variable reference, which is even more unexpected.
+               if varname == "" {
+                       p.Errorf("Assignment to the empty variable is not possible.")
+                       break
+               }
+
+               p.Errorf("Assignment modifiers like %q must not be used at all.",
+                       lexer.Since(mark))
+               p.Explain(
+                       "These modifiers modify other variables when they are evaluated.",
+                       "This makes it more difficult to understand them since all the",
+                       "other modifiers only affect the one expression that is being",
+                       "evaluated, without any long-lasting side effects.",
+                       "",
+                       "A similarly unpredictable mechanism are shell commands,",
+                       "but even these have only local consequences.")
+
+               p.varUseText(closing)
+               return lexer.Since(mark)
        }
 
+       // ${SOURCES:%.c=%.o}
        lexer.Reset(mark)
+       modifier, modifierNoVar := p.varUseModifierSysV(closing)
+       if contains(modifier, "=") {
+               if contains(modifierNoVar, ":") {
+                       unrealModifier := modifier[strings.Index(modifier, ":"):]
+                       p.Warnf("The text %q looks like a modifier but isn't.", unrealModifier)
+                       p.Explain(
+                               "The :from=to modifier consumes all the text until the end of the variable.",
+                               "There cannot be any further modifiers after it.")
+               }
+               return modifier
+       }
 
-       modifier := p.varUseText(closing)
-
-       // ${SOURCES:%.c=%.o} or ${:!uname -a!:[2]}
-       if contains(modifier, "=") || hasPrefix(modifier, "!") && hasSuffix(modifier, "!") {
+       // ${:!uname -a!:[2]}
+       lexer.Reset(mark)
+       modifier = p.varUseText(closing)
+       if hasPrefix(modifier, "!") && hasSuffix(modifier, "!") {
                return modifier
        }
 
-       if p.line != nil && modifier != "" {
-               p.line.Warnf("Invalid variable modifier %q for %q.", modifier, varname)
+       if modifier != "" {
+               p.Warnf("Invalid variable modifier %q for %q.", modifier, varname)
        }
 
        return ""
 }
 
-// varUseModifierSeparator parses the :ts modifier.
+// varUseModifierTs parses the :ts modifier.
 //
 // The API of this method is tricky.
 // It is only extracted from varUseModifier to make the latter smaller.
-func (p *MkLexer) varUseModifierSeparator(
+func (p *MkLexer) varUseModifierTs(
        mod string, closing byte, lexer *textproc.Lexer, varname string,
        mark textproc.LexerMark) string {
 
@@ -291,13 +364,11 @@ func (p *MkLexer) varUseModifierSeparato
        case matches(sep, `^\\\d+`):
                break
        default:
-               if p.line != nil {
-                       p.line.Warnf("Invalid separator %q for :ts modifier of %q.", sep, varname)
-                       p.line.Explain(
-                               "The separator for the :ts modifier must be either a single character",
-                               "or an escape sequence like \\t or \\n or an octal or decimal escape",
-                               "sequence; see the bmake man page for further details.")
-               }
+               p.Warnf("Invalid separator %q for :ts modifier of %q.", sep, varname)
+               p.Explain(
+                       "The separator for the :ts modifier must be either a single character",
+                       "or an escape sequence like \\t or \\n or an octal or decimal escape",
+                       "sequence; see the bmake man page for further details.")
        }
        return lexer.Since(mark)
 }
@@ -349,8 +420,10 @@ func (p *MkLexer) varUseModifierMatch(cl
 // varUseModifierSubst parses a :S,from,to, or a :C,from,to, modifier.
 func (p *MkLexer) 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 */)
+       regex = lexer.SkipByte('C')
+       if !regex && !lexer.SkipByte('S') {
+               return
+       }
 
        sep := lexer.PeekByte() // bmake allows _any_ separator, even letters.
        if sep == -1 || byte(sep) == closing {
@@ -430,8 +503,8 @@ func (p *MkLexer) varUseModifierAt(lexer
        for p.VarUse() != nil || lexer.SkipString("$$") || lexer.SkipRegexp(re) {
        }
 
-       if !lexer.SkipByte('@') && p.line != nil {
-               p.line.Warnf("Modifier ${%s:@%s@...@} is missing the final \"@\".", varname, loopVar)
+       if !lexer.SkipByte('@') {
+               p.Warnf("Modifier ${%s:@%s@...@} is missing the final \"@\".", varname, loopVar)
        }
 
        return true
@@ -447,23 +520,21 @@ func (p *MkLexer) varUseAlnum() *MkVarUs
 
        lexer.Skip(2)
 
-       if p.line != nil {
-               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).")
-               }
+       if len(apparentVarname) > 1 {
+               p.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.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.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.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{apparentVarname[:1], nil}
+       return NewMkVarUse(apparentVarname[:1])
 }
 
 func (p *MkLexer) EOF() bool {
@@ -473,3 +544,34 @@ func (p *MkLexer) EOF() bool {
 func (p *MkLexer) Rest() string {
        return p.lexer.Rest()
 }
+
+func (p *MkLexer) Errorf(format string, args ...interface{}) {
+       if p.HasDiag() {
+               p.diag.Errorf(format, args...)
+       }
+}
+
+func (p *MkLexer) Warnf(format string, args ...interface{}) {
+       if p.HasDiag() {
+               p.diag.Warnf(format, args...)
+       }
+}
+
+func (p *MkLexer) Notef(format string, args ...interface{}) {
+       if p.HasDiag() {
+               p.diag.Notef(format, args...)
+       }
+}
+
+func (p *MkLexer) Explain(explanation ...string) {
+       if p.HasDiag() {
+               p.diag.Explain(explanation...)
+       }
+}
+
+// Autofix must only be called if HasDiag returns true.
+func (p *MkLexer) Autofix() *Autofix {
+       return p.diag.Autofix()
+}
+
+func (p *MkLexer) HasDiag() bool { return p.diag != nil }

Index: pkgsrc/pkgtools/pkglint/files/mklexer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.2 pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.3
--- pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.2   Sat Nov 30 20:35:11 2019
+++ pkgsrc/pkgtools/pkglint/files/mklexer_test.go       Sun Dec  8 00:06:38 2019
@@ -1,6 +1,34 @@
 package pkglint
 
-import "gopkg.in/check.v1"
+import (
+       "gopkg.in/check.v1"
+       "netbsd.org/pkglint/textproc"
+)
+
+func (s *Suite) Test_NewMkLexer__with_diag(c *check.C) {
+       t := s.Init(c)
+
+       diag := t.NewLine("filename.mk", 123, "")
+
+       lex := NewMkLexer("${", diag)
+
+       use := lex.VarUse()
+       t.CheckDeepEquals(use, NewMkVarUse(""))
+       t.CheckEquals(lex.Rest(), "")
+       t.CheckOutputLines(
+               "WARN: filename.mk:123: Missing closing \"}\" for \"\".")
+}
+
+func (s *Suite) Test_NewMkLexer__without_diag(c *check.C) {
+       t := s.Init(c)
+
+       lex := NewMkLexer("${", nil)
+
+       use := lex.VarUse()
+       t.CheckDeepEquals(use, NewMkVarUse(""))
+       t.CheckEquals(lex.Rest(), "")
+       t.CheckOutputEmpty()
+}
 
 func (s *Suite) Test_MkLexer_MkTokens(c *check.C) {
        t := s.Init(c)
@@ -79,6 +107,7 @@ func (s *Suite) Test_MkLexer_VarUse(c *c
        varuse := b.VaruseToken
        varuseText := b.VaruseTextToken
 
+       // FIXME: This function does much more than necessary to test VarUse.
        testRest := func(input string, expectedTokens []*MkToken, expectedRest string, diagnostics ...string) {
                line := t.NewLines("Test_MkLexer_VarUse.mk", input).Lines[0]
                p := NewMkLexer(input, line)
@@ -179,10 +208,13 @@ func (s *Suite) Test_MkLexer_VarUse(c *c
        test("${PKGNAME:C/-[0-9].*$/-[0-9]*/}",
                varuse("PKGNAME", "C/-[0-9].*$/-[0-9]*/"))
 
-       // TODO: Does the $@ refer to ${.TARGET}, or is it just an unmatchable
-       //  regular expression?
-       test("${PKGNAME:C/$@/target?/}",
-               varuse("PKGNAME", "C/$@/target?/"))
+       // The $@ in the :S modifier refers to ${.TARGET}.
+       // When used in a target called "target",
+       // the whole expression evaluates to "-replaced-".
+       test("${:U-target-:S/$@/replaced/:Q}",
+               varuse("", "U-target-", "S/$@/replaced/", "Q"))
+       test("${:U-target-:C/$@/replaced/:Q}",
+               varuse("", "U-target-", "C/$@/replaced/", "Q"))
 
        test("${PKGNAME:S/py${_PYTHON_VERSION}/py${i}/:C/-[0-9].*$/-[0-9]*/}",
                varuse("PKGNAME", "S/py${_PYTHON_VERSION}/py${i}/", "C/-[0-9].*$/-[0-9]*/"))
@@ -295,23 +327,6 @@ func (s *Suite) Test_MkLexer_VarUse(c *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 a long octal number.
-       test("${VAR:ts\\000012}",
-               varuse("VAR", "ts\\000012"))
-
-       // Or even decimal.
-       test("${VAR:ts\\124}",
-               varuse("VAR", "ts\\124"))
-
-       // The :ts modifier only takes single-character separators.
-       test("${VAR:ts---}",
-               varuse("VAR", "ts---"),
-               "WARN: Test_MkLexer_VarUse.mk:1: Invalid separator \"---\" for :ts modifier of \"VAR\".")
-
        test("$<",
                varuseText("$<", "<")) // Same as ${.IMPSRC}
 
@@ -377,38 +392,9 @@ func (s *Suite) Test_MkLexer_VarUse(c *c
        test("${arbitrary text}",
                varuse("arbitrary text"),
                "WARN: Test_MkLexer_VarUse.mk:1: Invalid part \" text\" after variable name \"arbitrary\".")
-}
 
-func (s *Suite) Test_MkLexer_VarUse__ambiguous(c *check.C) {
-       t := s.Init(c)
-       b := NewMkTokenBuilder()
-
-       t.SetUpCommandLine("--explain")
-
-       line := t.NewLine("module.mk", 123, "\t$Varname $X")
-       p := NewMkLexer(line.Text[1:], line)
-
-       tokens, rest := p.MkTokens()
-       t.CheckDeepEquals(tokens, b.Tokens(
-               b.VaruseTextToken("$V", "V"),
-               b.TextToken("arname "),
-               b.VaruseTextToken("$X", "X")))
-       t.CheckEquals(rest, "")
-
-       t.CheckOutputLines(
-               "ERROR: module.mk:123: $Varname is ambiguous. Use ${Varname} if you mean a Make variable or $$Varname if you mean a shell variable.",
-               "",
-               "\tOnly the first letter after the dollar is the variable name.",
-               "\tEverything following it is normal text, even if it looks like a",
-               "\tvariable name to human readers.",
-               "",
-               "WARN: module.mk:123: $X is ambiguous. Use ${X} if you mean a Make variable or $$X if you mean a shell variable.",
-               "",
-               "\tIn its current form, this variable is parsed as a Make variable. For",
-               "\thuman readers though, $x looks more like a shell variable than a",
-               "\tMake variable, since Make variables are usually written using braces",
-               "\t(BSD-style) or parentheses (GNU-style).",
-               "")
+       test("${:!command!:Q}",
+               varuse("", "!command!", "Q"))
 }
 
 // Pkglint can replace $(VAR) with ${VAR}. It doesn't look at all components
@@ -520,6 +506,35 @@ func (s *Suite) Test_MkLexer_varUseText(
        test("a\\\\:a", "a\\\\")
 }
 
+func (s *Suite) Test_MkLexer_varUseModifierSysV(c *check.C) {
+       t := s.Init(c)
+
+       test := func(input string, closing byte, mod, modNoVar string, rest string, diagnostics ...string) {
+               diag := t.NewLine("filename.mk", 123, "")
+               lex := NewMkLexer(input, diag)
+
+               actualMod, actualModNoVar := lex.varUseModifierSysV(closing)
+
+               t.CheckDeepEquals(
+                       []interface{}{actualMod, actualModNoVar, lex.Rest()},
+                       []interface{}{mod, modNoVar, rest})
+               t.CheckOutput(diagnostics)
+       }
+
+       // The shortest possible SysV substitution:
+       // replace nothing with nothing.
+       test(":=}rest", '}',
+               ":=", ":=", "}rest",
+               nil...)
+
+       // Parsing the SysV modifier produces no parse error.
+       // This will be done by the surrounding VarUse when it doesn't find
+       // the closing parenthesis (in this case, or usually a brace).
+       test(":=}rest", ')',
+               ":=}rest", ":=}rest", "",
+               nil...)
+}
+
 func (s *Suite) Test_MkLexer_VarUseModifiers(c *check.C) {
        t := s.Init(c)
 
@@ -555,12 +570,38 @@ func (s *Suite) Test_MkLexer_VarUseModif
        // variable name, in this case BUILD_DIRS.
        test("${BUILD_DIRS:[3]:L}", varUse("BUILD_DIRS", "[3]", "L"))
 
-       test("${PATH:ts::Q}", varUse("PATH", "ts:", "Q"))
-
        // The :Q at the end is part of the right-hand side of the = modifier.
        // It does not quote anything.
        // See devel/bmake/files/var.c:/^VarGetPattern/.
-       test("${VAR:old=new:Q}", varUse("VAR", "old=new", "Q")) // FIXME
+       test("${VAR:old=new:Q}", varUse("VAR", "old=new:Q"),
+               "WARN: Makefile:20: The text \":Q\" looks like a modifier but isn't.")
+}
+
+func (s *Suite) Test_MkLexer_varUseModifier(c *check.C) {
+       t := s.Init(c)
+
+       p := NewMkLexer("${VAR:R:E:Ox:tA:tW:tw}", nil)
+
+       varUse := p.VarUse()
+
+       t.CheckDeepEquals(varUse.modifiers, []MkVarUseModifier{
+               {"R"}, {"E"}, {"Ox"}, {"tA"}, {"tW"}, {"tw"}})
+}
+
+func (s *Suite) Test_MkLexer_varUseModifier__S_parse_error(c *check.C) {
+       t := s.Init(c)
+
+       diag := t.NewLine("filename.mk", 123, "")
+       p := NewMkLexer("S,}", diag)
+
+       mod := p.varUseModifier("VAR", '}')
+
+       t.CheckEquals(mod, "")
+       // FIXME: The "S," has just disappeared.
+       t.CheckEquals(p.Rest(), "}")
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:123: Invalid variable modifier \"S,\" for \"VAR\".")
 }
 
 func (s *Suite) Test_MkLexer_varUseModifier__invalid_ts_modifier_with_warning(c *check.C) {
@@ -663,7 +704,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 func (s *Suite) Test_MkLexer_varUseModifier__eq_suffix_replacement(c *check.C) {
        t := s.Init(c)
 
-       test := func(input, modifier, rest string) {
+       test := func(input, modifier, rest string, diagnostics ...string) {
                line := t.NewLine("filename.mk", 123, "")
                p := NewMkLexer(input, line)
 
@@ -671,23 +712,119 @@ func (s *Suite) Test_MkLexer_varUseModif
 
                t.CheckDeepEquals(actual, modifier)
                t.CheckEquals(p.Rest(), rest)
+               t.CheckOutput(diagnostics)
        }
 
        test("%.c=%.o", "%.c=%.o", "")
-       test("%\\:c=%.o", "%\\:c=%.o", "") // FIXME: remove the escaping.
-       test("%\\:c=%.o", "%\\:c=%.o", "") // FIXME: remove the escaping.
+       test("%\\:c=%.o", "%\\:c=%.o", "", // XXX: maybe someday remove the escaping.
+               "WARN: filename.mk:123: The text \":c=%.o\" looks like a modifier but isn't.")
+       test("%\\:c=%.o", "%\\:c=%.o", "", // XXX: maybe someday remove the escaping.
+               "WARN: filename.mk:123: The text \":c=%.o\" looks like a modifier but isn't.")
 
        // The backslashes are only removed before parentheses,
        // braces and colons; see devel/bmake/files/var.c:/^VarGetPattern/
        test(".\\a\\b\\c=.abc", ".\\a\\b\\c=.abc", "")
 
        // See devel/bmake/files/var.c:/^#define IS_A_MATCH/.
-       // FIXME: The :rest must be part of the replacement.
-       test("%.c=%.o:rest", "%.c=%.o", ":rest")
+       test("%.c=%.o:rest", "%.c=%.o:rest", "",
+               "WARN: filename.mk:123: The text \":rest\" looks like a modifier but isn't.")
        test("\\}\\\\\\$=", "\\}\\\\\\$=", "")
-       // FIXME: test("\\}\\\\\\$=", "}\\$=", "")
+       // XXX: maybe someday test("\\}\\\\\\$=", "}\\$=", "")
        test("=\\}\\\\\\$\\&", "=\\}\\\\\\$\\&", "")
-       // FIXME: test("=\\}\\\\\\$\\&", "=}\\$&", "")
+       // XXX: maybe someday test("=\\}\\\\\\$\\&", "=}\\$&", "")
+
+       // The colon in the nested variable expression does not count as
+       // a separator for parsing the outer modifier.
+       test("=${VAR:D/}}", "=${VAR:D/}", "}")
+}
+
+func (s *Suite) Test_MkLexer_varUseModifier__assigment(c *check.C) {
+       t := s.Init(c)
+
+       test := func(varname, input, modifier, rest string, diagnostics ...string) {
+               line := t.NewLine("filename.mk", 123, "")
+               p := NewMkLexer(input, line)
+
+               actual := p.varUseModifier(varname, '}')
+
+               t.CheckDeepEquals(actual, modifier)
+               t.CheckEquals(p.Rest(), rest)
+               t.CheckOutput(diagnostics)
+       }
+
+       test("VAR", ":!=${OTHER}:rest", ":!=${OTHER}", ":rest",
+               "ERROR: filename.mk:123: "+
+                       "Assignment modifiers like \":!=\" must not be used at all.")
+       test("VAR", ":=${OTHER}:rest", ":=${OTHER}", ":rest",
+               "ERROR: filename.mk:123: "+
+                       "Assignment modifiers like \":=\" must not be used at all.")
+       test("VAR", ":+=${OTHER}:rest", ":+=${OTHER}", ":rest",
+               "ERROR: filename.mk:123: "+
+                       "Assignment modifiers like \":+=\" must not be used at all.")
+       test("VAR", ":?=${OTHER}:rest", ":?=${OTHER}", ":rest",
+               "ERROR: filename.mk:123: "+
+                       "Assignment modifiers like \":?=\" must not be used at all.")
+
+       // This one is not treated as an assignment operator since at this
+       // point the operators := and = are equivalent. There is no special
+       // parsing code for this case, therefore it falls back to the SysV
+       // interpretation of the :from=to modifier, which consumes all the
+       // remaining text.
+       //
+       // See devel/bmake/files/var.c:/tstr\[2\] == '='/.
+       test("VAR", "::=${OTHER}:rest", "::=${OTHER}:rest", "",
+               "WARN: filename.mk:123: The text \"::=${OTHER}:rest\" "+
+                       "looks like a modifier but isn't.")
+
+       test("", ":=value", ":=value", "",
+               "ERROR: filename.mk:123: "+
+                       "Assignment to the empty variable is not possible.",
+               "WARN: filename.mk:123: The text \":=value\" "+
+                       "looks like a modifier but isn't.")
+}
+
+func (s *Suite) Test_MkLexer_varUseModifierTs(c *check.C) {
+       t := s.Init(c)
+
+       test := func(input string, closing byte, mod string, rest string, diagnostics ...string) {
+               diag := t.NewLine("filename.mk", 123, "")
+               lex := NewMkLexer(input, diag)
+               mark := lex.lexer.Mark()
+               alnum := lex.lexer.NextBytesSet(textproc.Alnum)
+
+               actualMod := lex.varUseModifierTs(alnum, closing, lex.lexer, "VAR", mark)
+
+               t.CheckDeepEquals(
+                       []interface{}{actualMod, lex.Rest()},
+                       []interface{}{mod, rest})
+               t.CheckOutput(diagnostics)
+       }
+
+       // The separator character can be left out, which means empty.
+       test("ts}", '}',
+               "ts", "}",
+               nil...)
+
+       // The separator character can be a long octal number.
+       test("ts\\000012}", '}',
+               "ts\\000012", "}",
+               nil...)
+
+       // Or even decimal.
+       test("ts\\124}", '}',
+               "ts\\124", "}",
+               nil...)
+
+       // The :ts modifier only takes single-character separators.
+       test("ts---}", '}',
+               "ts---", "}",
+               "WARN: filename.mk:123: Invalid separator \"---\" for :ts modifier of \"VAR\".")
+
+       // Using a colon as separator looks a bit strange but works.
+       // The first colon is the separator, the second one starts the :Q.
+       test("ts::Q}", '}',
+               "ts:", ":Q}",
+               nil...)
 }
 
 func (s *Suite) Test_MkLexer_varUseModifierMatch(c *check.C) {
@@ -788,49 +925,67 @@ func (s *Suite) Test_MkLexer_varUseModif
 func (s *Suite) Test_MkLexer_varUseModifierSubst(c *check.C) {
        t := s.Init(c)
 
-       varUse := NewMkTokenBuilder().VarUse
-       test := func(text string, varUse *MkVarUse, rest string, diagnostics ...string) {
-               line := t.NewLine("Makefile", 20, "\t"+text)
-               p := NewMkLexer(text, line)
-
-               actual := p.VarUse()
+       test := func(mod string, regex bool, from, to, options, rest string, diagnostics ...string) {
+               line := t.NewLine("Makefile", 20, "")
+               p := NewMkLexer(mod, line)
+
+               ok, actualRegex, actualFrom, actualTo, actualOptions := p.varUseModifierSubst('}')
+
+               t.CheckDeepEquals(
+                       []interface{}{ok, actualRegex, actualFrom, actualTo, actualOptions, p.Rest()},
+                       []interface{}{true, regex, from, to, options, rest})
+               t.CheckOutput(diagnostics)
+       }
 
-               t.CheckDeepEquals(actual, varUse)
-               t.CheckEquals(p.Rest(), rest)
+       testFail := func(mod, rest string, diagnostics ...string) {
+               line := t.NewLine("Makefile", 20, "")
+               p := NewMkLexer(mod, line)
+
+               ok, regex, from, to, options := p.varUseModifierSubst('}')
+               if !ok {
+                       return
+               }
+               t.CheckDeepEquals(
+                       []interface{}{ok, regex, from, to, options, p.Rest()},
+                       []interface{}{false, false, "", "", "", rest})
                t.CheckOutput(diagnostics)
        }
 
-       test("${VAR:S", varUse("VAR"), "",
-               "WARN: Makefile:20: Invalid variable modifier \"S\" for \"VAR\".",
-               "WARN: Makefile:20: Missing closing \"}\" for \"VAR\".")
+       testFail("S", "S",
+               nil...)
 
-       test("${VAR:S}", varUse("VAR"), "",
-               "WARN: Makefile:20: Invalid variable modifier \"S\" for \"VAR\".")
+       testFail("S}", "S}",
+               nil...)
 
-       test("${VAR:S,}", varUse("VAR"), "",
+       testFail("S,}", "S,}",
                "WARN: Makefile:20: Invalid variable modifier \"S,\" for \"VAR\".")
 
-       test("${VAR:S,from,to}", varUse("VAR"), "",
+       testFail("S,from,to}", "",
                "WARN: Makefile:20: Invalid variable modifier \"S,from,to\" for \"VAR\".")
 
-       test("${VAR:S,from,to,}", varUse("VAR", "S,from,to,"), "")
+       // Up to 2019-12-05, these were considered valid substitutions,
+       // having [ as the separator and ss] as the rest.
+       testFail("M[Y][eE][sS]", "M[Y][eE][sS]",
+               nil...)
+       testFail("N[Y][eE][sS]", "M[Y][eE][sS]",
+               nil...)
+
+       test("S,from,to,}", false, "from", "to", "", "}")
 
-       test("${VAR:S,^from$,to,}", varUse("VAR", "S,^from$,to,"), "")
+       test("S,^from$,to,}", false, "^from$", "to", "", "}")
 
-       test("${VAR:S,@F@,${F},}", varUse("VAR", "S,@F@,${F},"), "")
+       test("S,@F@,${F},}", false, "@F@", "${F}", "", "}")
 
-       test("${VAR:S,from,to,1}", varUse("VAR", "S,from,to,1"), "")
-       test("${VAR:S,from,to,g}", varUse("VAR", "S,from,to,g"), "")
-       test("${VAR:S,from,to,W}", varUse("VAR", "S,from,to,W"), "")
+       test("S,from,to,1}", false, "from", "to", "1", "}")
+       test("S,from,to,g}", false, "from", "to", "g", "}")
+       test("S,from,to,W}", false, "from", "to", "W", "}")
 
-       test("${VAR:S,from,to,1gW}", varUse("VAR", "S,from,to,1gW"), "")
+       test("S,from,to,1gW}", false, "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"),
-               "")
+       test("C/[[:alnum:]]{2}/**/g}", true, "[[: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
@@ -838,9 +993,7 @@ func (s *Suite) Test_MkLexer_varUseModif
        //
        // 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"),
-               "")
+       test("S\\.post1\\\\1}", false, ".post1", "", "1", "}")
 }
 
 func (s *Suite) Test_MkLexer_varUseModifierAt__missing_at_after_variable_name(c *check.C) {
@@ -916,3 +1069,217 @@ func (s *Suite) Test_MkLexer_varUseModif
                varUse("PKG_GROUPS", "@g@${g:Q}:${PKG_GID.${g}:Q}@", "C/:*$//g"),
                "")
 }
+
+func (s *Suite) Test_MkLexer_varUseAlnum(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--explain")
+
+       test := func(input, varname, rest string, diagnostics ...string) {
+               lex := NewMkLexer(input, t.NewLine("filename.mk", 123, ""))
+
+               use := lex.varUseAlnum()
+
+               t.CheckDeepEquals(use, NewMkVarUse(varname))
+               t.CheckEquals(lex.Rest(), rest)
+               t.CheckOutput(diagnostics)
+       }
+
+       test("$Varname:rest",
+               "V", "arname:rest",
+
+               "ERROR: filename.mk:123: $Varname is ambiguous. "+
+                       "Use ${Varname} if you mean a Make variable "+
+                       "or $$Varname if you mean a shell variable.",
+               "",
+               "\tOnly the first letter after the dollar is the variable name.",
+               "\tEverything following it is normal text, even if it looks like a",
+               "\tvariable name to human readers.",
+               "")
+
+       test("$X:rest",
+               "X", ":rest",
+
+               "WARN: filename.mk:123: $X is ambiguous. "+
+                       "Use ${X} if you mean a Make variable "+
+                       "or $$X if you mean a shell variable.",
+               "",
+               "\tIn its current form, this variable is parsed as a Make variable. For",
+               "\thuman readers though, $x looks more like a shell variable than a",
+               "\tMake variable, since Make variables are usually written using braces",
+               "\t(BSD-style) or parentheses (GNU-style).",
+               "")
+}
+
+func (s *Suite) Test_MkLexer_EOF(c *check.C) {
+       t := s.Init(c)
+
+       test := func(input string, eof bool) {
+               lex := NewMkLexer(input, nil)
+               t.CheckEquals(lex.EOF(), eof)
+       }
+
+       test("", true)
+       test("x", false)
+       test("$$", false)
+       test("${VAR}", false)
+}
+
+func (s *Suite) Test_MkLexer_Rest(c *check.C) {
+       t := s.Init(c)
+
+       test := func(input, str, rest string) {
+               lex := NewMkLexer(input, nil)
+
+               lex.lexer.NextString(str)
+
+               t.CheckEquals(lex.Rest(), rest)
+       }
+
+       test("", "", "")
+       test("x", "", "x")
+       test("x", "x", "")
+       test("$$", "", "$$")
+       test("${VAR}rest", "${VAR}", "rest")
+}
+
+func (s *Suite) Test_MkLexer_Errorf(c *check.C) {
+       t := s.Init(c)
+
+       test := func(diag Autofixer, diagnostics ...string) {
+               lex := NewMkLexer("", diag)
+               lex.Errorf("Must %q.", "arg")
+               t.CheckOutput(diagnostics)
+       }
+
+       test(
+               nil,
+
+               nil...)
+
+       test(
+               t.NewLine("filename.mk", 123, ""),
+
+               "ERROR: filename.mk:123: Must \"arg\".")
+}
+
+func (s *Suite) Test_MkLexer_Warnf(c *check.C) {
+       t := s.Init(c)
+
+       test := func(diag Autofixer, diagnostics ...string) {
+               lex := NewMkLexer("", diag)
+               lex.Warnf("Should %q.", "arg")
+               t.CheckOutput(diagnostics)
+       }
+
+       test(
+               nil,
+
+               nil...)
+
+       test(
+               t.NewLine("filename.mk", 123, ""),
+
+               "WARN: filename.mk:123: Should \"arg\".")
+}
+
+func (s *Suite) Test_MkLexer_Notef(c *check.C) {
+       t := s.Init(c)
+
+       test := func(diag Autofixer, diagnostics ...string) {
+               lex := NewMkLexer("", diag)
+               lex.Notef("Can %q.", "arg")
+               t.CheckOutput(diagnostics)
+       }
+
+       test(
+               nil,
+
+               nil...)
+
+       test(
+               t.NewLine("filename.mk", 123, ""),
+
+               "NOTE: filename.mk:123: Can \"arg\".")
+}
+
+func (s *Suite) Test_MkLexer_Explain(c *check.C) {
+       t := s.Init(c)
+
+       test := func(option string, diag Autofixer, diagnostics ...string) {
+               t.SetUpCommandLine(option)
+               lex := NewMkLexer("", diag)
+               lex.Warnf("Should %q.", "arg")
+
+               lex.Explain(
+                       "Explanation.")
+
+               t.CheckOutput(diagnostics)
+       }
+
+       test(
+               "--explain",
+               nil,
+
+               nil...)
+
+       test(
+               "--explain=no",
+               nil,
+
+               nil...)
+
+       test(
+               "--explain",
+               t.NewLine("filename.mk", 123, ""),
+
+               "WARN: filename.mk:123: Should \"arg\".",
+               "",
+               "\tExplanation.",
+               "")
+
+       test(
+               "--explain=no",
+               t.NewLine("filename.mk", 123, ""),
+
+               "WARN: filename.mk:123: Should \"arg\".")
+}
+
+func (s *Suite) Test_MkLexer_Autofix(c *check.C) {
+       t := s.Init(c)
+
+       test := func() {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       "# before")
+               lex := NewMkLexer("", mklines.lines.Lines[0])
+
+               fix := lex.Autofix()
+               fix.Warnf("Warning.")
+               fix.Replace("before", "after")
+               fix.Apply()
+       }
+
+       t.ExpectDiagnosticsAutofix(
+               test,
+               "WARN: ~/filename.mk:1: Warning.",
+               "AUTOFIX: ~/filename.mk:1: Replacing \"before\" with \"after\".")
+}
+
+func (s *Suite) Test_MkLexer_Autofix__nil(c *check.C) {
+       t := s.Init(c)
+
+       t.ExpectPanicMatches(
+               func() { NewMkLexer("", nil).Autofix() },
+               `^runtime error: invalid memory address or nil pointer dereference`)
+}
+
+func (s *Suite) Test_MkLexer_HasDiag(c *check.C) {
+       t := s.Init(c)
+
+       test := func(diag Autofixer, hasDiag bool) {
+               t.CheckEquals(NewMkLexer("", diag).HasDiag(), hasDiag)
+       }
+
+       test(nil, false)
+       test(t.NewLine("filename", 123, ""), true)
+}

Index: pkgsrc/pkgtools/pkglint/files/mkline.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.67 pkgsrc/pkgtools/pkglint/files/mkline.go:1.68
--- pkgsrc/pkgtools/pkglint/files/mkline.go:1.67        Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline.go     Sun Dec  8 00:06:38 2019
@@ -59,7 +59,7 @@ type mkLineInclude struct {
        mustExist       bool     // for .sinclude, nonexistent files are ignored
        sys             bool     // whether the include uses <file.mk> (very rare) instead of "file.mk"
        indent          string   // the space between the leading "." and the directive
-       includedFile    Path     // the text between the <brackets> or "quotes"
+       includedFile    RelPath  // the text between the <brackets> or "quotes"
        conditionalVars []string // variables on which this inclusion depends (filled in later, as needed)
 }
 
@@ -276,13 +276,13 @@ func (mkline *MkLine) SetHasElseBranch(e
 
 func (mkline *MkLine) MustExist() bool { return mkline.data.(*mkLineInclude).mustExist }
 
-func (mkline *MkLine) IncludedFile() Path { return mkline.data.(*mkLineInclude).includedFile }
+func (mkline *MkLine) IncludedFile() RelPath { return mkline.data.(*mkLineInclude).includedFile }
 
 // IncludedFileFull returns the path to the included file.
 func (mkline *MkLine) IncludedFileFull() CurrPath {
-       // FIXME: consider DirNoClean
-       // FIXME: consider JoinNoClean
-       return mkline.Filename.DirClean().JoinClean(mkline.IncludedFile()).CleanPath()
+       dir := mkline.Filename.DirNoClean()
+       joined := dir.JoinNoClean(mkline.IncludedFile())
+       return joined.CleanPath()
 }
 
 func (mkline *MkLine) Targets() string { return mkline.data.(mkLineDependency).targets }
@@ -327,11 +327,11 @@ func (mkline *MkLine) Tokenize(text stri
        if mkline.IsVarassignMaybeCommented() && text == mkline.Value() {
                tokens, rest = mkline.ValueTokens()
        } else {
-               var line *Line
+               var diag Autofixer
                if warn {
-                       line = mkline.Line
+                       diag = mkline.Line
                }
-               p := NewMkLexer(text, line)
+               p := NewMkLexer(text, diag)
                tokens, rest = p.MkTokens()
        }
 
@@ -563,8 +563,7 @@ func (mkline *MkLine) ResolveVarsInRelat
        if G.Pkg != nil {
                basedir = G.Pkg.File(".")
        } else {
-               // FIXME: consider DirNoClean
-               basedir = mkline.Filename.DirClean()
+               basedir = mkline.Filename.DirNoClean()
        }
 
        tmp := relativePath
@@ -637,13 +636,13 @@ func (mkline *MkLine) ExplainRelativeDir
                "main pkgsrc repository.")
 }
 
-// RefTo returns a reference to another line,
+// RelMkLine returns a reference to another line,
 // which can be in the same file or in a different file.
 //
 // If there is a type mismatch when calling this function, try to add ".line" to
 // either the method receiver or the other line.
-func (mkline *MkLine) RefTo(other *MkLine) string {
-       return mkline.Line.RefTo(other.Line)
+func (mkline *MkLine) RelMkLine(other *MkLine) string {
+       return mkline.Line.RelLine(other.Line)
 }
 
 var (
@@ -750,7 +749,7 @@ func (mkline *MkLine) VariableNeedsQuoti
 
                // .for dir in ${PATH:C,:, ,g}
                for _, modifier := range varuse.modifiers {
-                       if modifier.ChangesWords() {
+                       if modifier.ChangesList() {
                                return unknown
                        }
                }
@@ -1055,7 +1054,7 @@ type indentationLevel struct {
        // pkglint will happily accept .include "fname" in both the then and
        // the else branch. This is ok since the primary job of this file list
        // is to prevent wrong pkglint warnings about missing files.
-       checkedFiles []RelPath
+       checkedFiles []PkgsrcPath
 
        // whether the line is a multiple-inclusion guard
        guard bool
@@ -1100,7 +1099,7 @@ func (ind *Indentation) Push(mkline *MkL
 //
 // Variables named *_MK are ignored since they are usually not interesting.
 func (ind *Indentation) AddVar(varname string) {
-       if hasSuffix(varname, "_MK") || ind.IsEmpty() {
+       if hasSuffix(varname, "_MK") {
                return
        }
 
@@ -1146,9 +1145,6 @@ func (ind *Indentation) Varnames() []str
        varnames := NewStringSet()
        for _, level := range ind.levels {
                for _, levelVarname := range level.conditionalVars {
-                       // multiple-inclusion guard must be filtered out earlier.
-                       assert(!hasSuffix(levelVarname, "_MK"))
-
                        varnames.Add(levelVarname)
                }
        }
@@ -1160,7 +1156,7 @@ func (ind *Indentation) Args() string {
        return ind.top().args
 }
 
-func (ind *Indentation) AddCheckedFile(filename RelPath) {
+func (ind *Indentation) AddCheckedFile(filename PkgsrcPath) {
        top := ind.top()
        top.checkedFiles = append(top.checkedFiles, filename)
 }
@@ -1168,8 +1164,7 @@ func (ind *Indentation) AddCheckedFile(f
 // HasExists returns whether the given filename has been tested in an
 // exists(filename) condition and thus may or may not exist.
 //
-// FIXME: Replace RelPath with PkgsrcPath, to make the filenames reliable.
-func (ind *Indentation) HasExists(filename RelPath) bool {
+func (ind *Indentation) HasExists(filename PkgsrcPath) bool {
        for _, level := range ind.levels {
                for _, levelFilename := range level.checkedFiles {
                        if filename == levelFilename {
@@ -1243,8 +1238,9 @@ func (ind *Indentation) TrackAfter(mklin
 
                cond.Walk(&MkCondCallback{
                        Call: func(name string, arg string) {
-                               if name == "exists" {
-                                       ind.AddCheckedFile(NewRelPathString(arg))
+                               if name == "exists" && !NewPath(arg).IsAbs() {
+                                       rel := G.Pkgsrc.ToRel(mkline.File(NewRelPathString(arg)))
+                                       ind.AddCheckedFile(rel)
                                }
                        }})
        }
@@ -1257,7 +1253,7 @@ func (ind *Indentation) CheckFinish(file
        eofLine := NewLineEOF(filename)
        for !ind.IsEmpty() {
                openingMkline := ind.top().mkline
-               eofLine.Errorf(".%s from %s must be closed.", openingMkline.Directive(), eofLine.RefTo(openingMkline.Line))
+               eofLine.Errorf(".%s from %s must be closed.", openingMkline.Directive(), eofLine.RelLine(openingMkline.Line))
                ind.Pop()
        }
 }
@@ -1280,7 +1276,7 @@ var (
        VarparamBytes = textproc.NewByteSet("A-Za-z_0-9#*+---./[")
 )
 
-func MatchMkInclude(text string) (m bool, indentation, directive string, filename Path) {
+func MatchMkInclude(text string) (m bool, indentation, directive string, filename RelPath) {
        lexer := textproc.NewLexer(text)
        if lexer.SkipByte('.') {
                indentation = lexer.NextHspace()
@@ -1294,7 +1290,7 @@ func MatchMkInclude(text string) (m bool
                                // Note: strictly speaking, the full MkVarUse would have to be parsed
                                // here. But since these usually don't contain double quotes, it has
                                // worked fine up to now.
-                               filename = NewPath(lexer.NextBytesFunc(func(c byte) bool { return c != '"' }))
+                               filename = NewRelPathString(lexer.NextBytesFunc(func(c byte) bool { return c != '"' }))
                                if !filename.IsEmpty() && lexer.SkipByte('"') {
                                        lexer.NextHspace()
                                        if lexer.EOF() {

Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.74 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.75
--- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.74   Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline_test.go        Sun Dec  8 00:06:38 2019
@@ -114,6 +114,34 @@ func (s *Suite) Test_MkLine_Cond(c *chec
        t.CheckEquals(mkline.Cond(), cond)
 }
 
+func (s *Suite) Test_MkLine_IncludedFileFull(c *check.C) {
+       t := s.Init(c)
+
+       test := func(including CurrPath, included RelPath, resolved CurrPath) {
+               text := sprintf(".include %q", included)
+               mkline := t.NewMkLine(including, 123, text)
+
+               t.CheckEquals(mkline.IncludedFileFull(), resolved)
+       }
+
+       test("Makefile", "options.mk", "options.mk")
+       test("Makefile", "../options.mk", "../options.mk")
+       test("Makefile", "../options.mk", "../options.mk")
+
+       // Keep a bit of context information in the path.
+       test(
+               t.File("subdir/Makefile"), "../options.mk",
+               t.File("subdir").JoinNoClean("../options.mk"))
+
+       t.Chdir(".")
+       test("category/package/Makefile", "../../devel/lib/buildlink3.mk",
+               "category/package/../../devel/lib/buildlink3.mk")
+
+       // Keep a bit of context information in the path.
+       test("a/b/c.mk", "../../d/e/../../f/g/h.mk",
+               "a/b/../../f/g/h.mk")
+}
+
 // Ensures that the conditional variables of a line can be set even
 // after initializing the MkLine.
 //
@@ -562,6 +590,21 @@ func (s *Suite) Test_MkLine_ResolveVarsI
                "WARN: ~/buildlink3.mk:2: PKGPATH.multimedia/totem is used but not defined.")
 }
 
+func (s *Suite) Test_MkLine_ResolveVarsInRelativePath__assertion(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.Chdir(".")
+       mkline := t.NewMkLine("a/b/c/d/e/f/g.mk", 123, "")
+
+       t.ExpectPanic(
+               func() { mkline.ResolveVarsInRelativePath("${PKGSRCDIR}") },
+               "Pkglint internal error: "+
+                       "Relative path \"../../../../../..\" for \"a/b/c/d/e/f\" is too deep "+
+                       "below the pkgsrc root \".\".")
+
+}
+
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__unknown_rhs(c *check.C) {
        t := s.Init(c)
 
@@ -569,7 +612,7 @@ func (s *Suite) Test_MkLine_VariableNeed
        t.SetUpVartypes()
 
        vuc := VarUseContext{G.Pkgsrc.VariableType(nil, "PKGNAME"), VucLoadTime, VucQuotUnknown, false}
-       nq := mkline.VariableNeedsQuoting(nil, &MkVarUse{"UNKNOWN", nil}, nil, &vuc)
+       nq := mkline.VariableNeedsQuoting(nil, NewMkVarUse("UNKNOWN"), nil, &vuc)
 
        t.CheckEquals(nq, unknown)
 }
@@ -585,11 +628,11 @@ func (s *Suite) Test_MkLine_VariableNeed
        mkline := mklines.mklines[1]
 
        vuc := VarUseContext{G.Pkgsrc.vartypes.Canon("MASTER_SITES"), VucRunTime, VucQuotPlain, false}
-       nq := mkline.VariableNeedsQuoting(nil, &MkVarUse{"HOMEPAGE", nil}, G.Pkgsrc.vartypes.Canon("HOMEPAGE"), &vuc)
+       nq := mkline.VariableNeedsQuoting(nil, NewMkVarUse("HOMEPAGE"), G.Pkgsrc.vartypes.Canon("HOMEPAGE"), &vuc)
 
        t.CheckEquals(nq, no)
 
-       MkLineChecker{mklines, mkline}.checkVarassign()
+       NewMkAssignChecker(mkline, mklines).checkVarassign()
 
        t.CheckOutputEmpty() // Up to version 5.3.6, pkglint warned about a missing :Q here, which was wrong.
 }
@@ -603,7 +646,7 @@ func (s *Suite) Test_MkLine_VariableNeed
                MkCvsID,
                "MASTER_SITES=\t${MASTER_SITE_SOURCEFORGE:=squirrel-sql/}")
 
-       MkLineChecker{mklines, mklines.mklines[1]}.checkVarassign()
+       NewMkAssignChecker(mklines.mklines[1], mklines).checkVarassign()
 
        // Assigning lists to lists is ok.
        t.CheckOutputEmpty()
@@ -612,15 +655,24 @@ func (s *Suite) Test_MkLine_VariableNeed
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__eval_shell(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
        mklines := t.NewMkLines("builtin.mk",
                MkCvsID,
+               "",
+               ".include \"../../mk/bsd.fast.prefs.mk\"",
+               "",
                "USE_BUILTIN.Xfixes!=\t${PKG_ADMIN} pmatch 'pkg-[0-9]*' ${BUILTIN_PKG.Xfixes:Q}")
 
-       MkLineChecker{mklines, mklines.mklines[1]}.checkVarassign()
+       mklines.ForEach(func(mkline *MkLine) {
+               if mkline.IsVarassign() {
+                       NewMkAssignChecker(mkline, mklines).checkVarassign()
+               }
+       })
 
        t.CheckOutputLines(
-               "NOTE: builtin.mk:2: The :Q modifier isn't necessary for ${BUILTIN_PKG.Xfixes} here.")
+               "NOTE: builtin.mk:5: The :Q modifier isn't necessary for ${BUILTIN_PKG.Xfixes} here.")
 }
 
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_single_quotes(c *check.C) {
@@ -631,7 +683,7 @@ func (s *Suite) Test_MkLine_VariableNeed
                MkCvsID,
                "SUBST_SED.hpath=\t-e 's|^\\(INSTALL[\t:]*=\\).*|\\1${INSTALL}|'")
 
-       MkLineChecker{mklines, mklines.mklines[1]}.checkVarassign()
+       NewMkAssignChecker(mklines.mklines[1], mklines).checkVarassign()
 
        t.CheckOutputLines(
                "WARN: Makefile:2: Please use ${INSTALL:Q} instead of ${INSTALL} " +
@@ -880,7 +932,7 @@ func (s *Suite) Test_MkLine_VariableNeed
                MkCvsID,
                "MASTER_SITES=\tftp://ftp.gtk.org/${PKGNAME}/ ${MASTER_SITE_GNOME:=subdir/}")
 
-       MkLineChecker{mklines, mklines.mklines[1]}.checkVarassignRightVaruse()
+       NewMkAssignChecker(mklines.mklines[1], mklines).checkVarassignRightVaruse()
 
        t.CheckOutputEmpty() // Don't warn about missing :Q modifiers.
 }
@@ -895,7 +947,7 @@ func (s *Suite) Test_MkLine_VariableNeed
                "",
                "CONFIGURE_ENV+=\tSYS_TAR_COMMAND_PATH=${TOOLS_TAR:Q}")
 
-       MkLineChecker{mklines, mklines.mklines[2]}.checkVarassignRightVaruse()
+       NewMkAssignChecker(mklines.mklines[2], mklines).checkVarassignRightVaruse()
 
        // The TOOLS_* variables only contain the path to the tool,
        // without any additional arguments that might be necessary
@@ -916,8 +968,8 @@ func (s *Suite) Test_MkLine_VariableNeed
                "COMPILE_CMD=\tcc `${CAT} ${WRKDIR}/compileflags`",
                "COMMENT_CMD=\techo `echo ${COMMENT}`")
 
-       MkLineChecker{mklines, mklines.mklines[2]}.checkVarassignRightVaruse()
-       MkLineChecker{mklines, mklines.mklines[3]}.checkVarassignRightVaruse()
+       NewMkAssignChecker(mklines.mklines[2], mklines).checkVarassignRightVaruse()
+       NewMkAssignChecker(mklines.mklines[3], mklines).checkVarassignRightVaruse()
 
        // Both CAT and WRKDIR are safe from quoting, therefore no warnings.
        // But COMMENT may contain arbitrary characters and therefore must
@@ -996,6 +1048,34 @@ func (s *Suite) Test_MkLine_VariableNeed
        t.CheckOutputEmpty()
 }
 
+// This test provides code coverage for the "switch vuc.quoting" in the case
+// that vuc.quoting is VucQuotUnknown.
+//
+// It is not possible to construct this scenario by calling mklines.Check(),
+// therefore it is specially crafted.
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_unknown_quotes(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpTool("bash", "BASH", AtRunTime)
+
+       mklines := t.SetUpFileMkLines("Makefile",
+               "\t:")
+       mkline := mklines.mklines[0]
+
+       varUse := NewMkVarUse("BASH")
+       vartype := G.Pkgsrc.VariableType(mklines, "BASH")
+       vuc := VarUseContext{
+               vartype:    NewVartype(BtShellWord, NoVartypeOptions, NewACLEntry("*", aclpAll)),
+               time:       VucRunTime,
+               quoting:    VucQuotUnknown,
+               IsWordPart: false}
+       needsQuoting := mkline.VariableNeedsQuoting(mklines, varUse, vartype, &vuc)
+
+       // This "yes" comes from the end of the "wantList != haveList" branch.
+       t.CheckEquals(needsQuoting, yes)
+}
+
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__D_and_U_modifiers(c *check.C) {
        t := s.Init(c)
 
@@ -1364,11 +1444,13 @@ func (s *Suite) Test_Indentation_Varname
        t := s.Init(c)
 
        t.SetUpPackage("category/other")
-       t.CreateFileDummyBuildlink3("category/other/buildlink3.mk")
+       t.CreateFileBuildlink3("category/other/buildlink3.mk")
        t.SetUpPackage("category/package",
                "DISTNAME=\tpackage-1.0",
                ".include \"../../category/other/buildlink3.mk\"")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
+               ".include \"../../mk/bsd.fast.prefs.mk\"",
+               "",
                ".if ${OPSYS} == NetBSD || ${OPSYS} == FreeBSD",
                ".  if ${OPSYS} == NetBSD",
                ".    include \"../../category/other/buildlink3.mk\"",
@@ -1382,7 +1464,31 @@ func (s *Suite) Test_Indentation_Varname
                "WARN: ~/category/package/Makefile:20: " +
                        "\"../../category/other/buildlink3.mk\" is included " +
                        "unconditionally here and " +
-                       "conditionally in buildlink3.mk:14 (depending on OPSYS).")
+                       "conditionally in buildlink3.mk:16 (depending on OPSYS).")
+}
+
+// Multiple-inclusion guards are too technical to be of any use on
+// the application level. Therefore they are filtered out early, in
+// Indentation.AddVar.
+func (s *Suite) Test_Indentation_Varnames__guard(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               ".if !defined(GUARD_MK)",
+               ".  if !defined(VAR)",
+               "VAR=\tvalue",
+               ".  endif",
+               ".endif")
+
+       var varnames []string
+       mklines.ForEach(func(mkline *MkLine) {
+               if mkline.IsVarassign() {
+                       varnames = mklines.indentation.Varnames()
+               }
+       })
+
+       t.CheckDeepEquals(varnames, []string{"VAR"})
 }
 
 func (s *Suite) Test_Indentation_TrackAfter__checked_files(c *check.C) {
@@ -1426,7 +1532,7 @@ func (s *Suite) Test_Indentation_TrackAf
 func (s *Suite) Test_MatchMkInclude(c *check.C) {
        t := s.Init(c)
 
-       test := func(input, expectedIndent, expectedDirective string, expectedFilename Path, expectedComment string) {
+       test := func(input, expectedIndent, expectedDirective string, expectedFilename RelPath, expectedComment string) {
                splitResult := NewMkLineParser().split(nil, input, true)
                m, indent, directive, args := MatchMkInclude(splitResult.main)
                t.CheckDeepEquals(

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.56 pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.57
--- pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.56 Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker.go      Sun Dec  8 00:06:38 2019
@@ -3,7 +3,6 @@ package pkglint
 import (
        "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/textproc"
-       "strconv"
        "strings"
 )
 
@@ -13,6 +12,10 @@ type MkLineChecker struct {
        MkLine  *MkLine
 }
 
+func NewMkLineChecker(mkLines *MkLines, mkLine *MkLine) MkLineChecker {
+       return MkLineChecker{MkLines: mkLines, MkLine: mkLine}
+}
+
 func (ck MkLineChecker) Check() {
        mkline := ck.MkLine
 
@@ -22,7 +25,7 @@ func (ck MkLineChecker) Check() {
 
        switch {
        case mkline.IsVarassign():
-               ck.checkVarassign()
+               NewMkAssignChecker(mkline, ck.MkLines).checkVarassign()
 
        case mkline.IsShellCommand():
                ck.checkShellCommand()
@@ -50,334 +53,6 @@ func (ck MkLineChecker) checkEmptyContin
        }
 }
 
-func (ck MkLineChecker) checkVarassign() {
-       ck.checkVarassignLeft()
-       ck.checkVarassignOp()
-       ck.checkVarassignRight()
-}
-
-// checkVarassignLeft checks everything to the left of the assignment operator.
-func (ck MkLineChecker) checkVarassignLeft() {
-       varname := ck.MkLine.Varname()
-       if hasPrefix(varname, "_") && !G.Infrastructure && G.Pkgsrc.vartypes.Canon(varname) == nil {
-               ck.MkLine.Warnf("Variable names starting with an underscore (%s) are reserved for internal pkgsrc use.", varname)
-       }
-
-       ck.checkVarassignLeftNotUsed()
-       ck.checkVarassignLeftDeprecated()
-       ck.checkVarassignLeftBsdPrefs()
-       if !ck.checkVarassignLeftUserSettable() {
-               ck.checkVarassignLeftPermissions()
-       }
-       ck.checkVarassignLeftRationale()
-
-       ck.checkTextVarUse(
-               ck.MkLine.Varname(),
-               NewVartype(BtVariableName, NoVartypeOptions, NewACLEntry("*", aclpAll)),
-               VucLoadTime)
-}
-
-// checkVarassignLeftNotUsed checks whether the left-hand side of a variable
-// assignment is not used. If it is unused and also doesn't have a predefined
-// data type, it may be a spelling mistake.
-func (ck MkLineChecker) checkVarassignLeftNotUsed() {
-       varname := ck.MkLine.Varname()
-       varcanon := varnameCanon(varname)
-
-       if ck.MkLine.Op() == opAssignEval && varname == strings.ToLower(varname) {
-               if trace.Tracing {
-                       trace.Step1("%s might be unused unless it is an argument to a procedure file.", varname)
-               }
-               return
-       }
-
-       if ck.MkLines.vars.IsUsedSimilar(varname) {
-               return
-       }
-
-       if G.Pkg != nil && G.Pkg.vars.IsUsedSimilar(varname) {
-               return
-       }
-
-       vartypes := G.Pkgsrc.vartypes
-       if vartypes.IsDefinedExact(varname) || vartypes.IsDefinedExact(varcanon) {
-               return
-       }
-
-       deprecated := G.Pkgsrc.Deprecated
-       if deprecated[varname] != "" || deprecated[varcanon] != "" {
-               return
-       }
-
-       if !ck.MkLines.once.FirstTimeSlice("defined but not used: ", varname) {
-               return
-       }
-
-       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.")
-}
-
-func (ck MkLineChecker) checkVarassignLeftDeprecated() {
-       varname := ck.MkLine.Varname()
-       if fix := G.Pkgsrc.Deprecated[varname]; fix != "" {
-               ck.MkLine.Warnf("Definition of %s is deprecated. %s", varname, fix)
-       } else if fix = G.Pkgsrc.Deprecated[varnameCanon(varname)]; fix != "" {
-               ck.MkLine.Warnf("Definition of %s is deprecated. %s", varname, fix)
-       }
-}
-
-func (ck MkLineChecker) checkVarassignLeftBsdPrefs() {
-       mkline := ck.MkLine
-
-       switch mkline.Varcanon() {
-       case "BUILDLINK_PKGSRCDIR.*",
-               "BUILDLINK_DEPMETHOD.*",
-               "BUILDLINK_ABI_DEPENDS.*",
-               "BUILDLINK_INCDIRS.*",
-               "BUILDLINK_LIBDIRS.*":
-               return
-       }
-
-       if !G.Opts.WarnExtra ||
-               G.Infrastructure ||
-               mkline.Op() != opAssignDefault ||
-               ck.MkLines.Tools.SeenPrefs ||
-               !ck.MkLines.once.FirstTime("include bsd.prefs.mk before using ?=") {
-               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.IsPackageSettable() {
-               return
-       }
-
-       mkline.Warnf("Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")
-       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.",
-               "This file must be included explicitly.",
-               "If a ?= operator appears before mk.conf has been included,",
-               "it will not care about the user's preferences,",
-               "which can result in unexpected behavior.",
-               "",
-               "The easiest way to include the mk.conf file is by including the",
-               "bsd.prefs.mk file, which will take care of everything.")
-}
-
-// checkVarassignLeftUserSettable checks whether a package defines a
-// variable that is marked as user-settable since it is defined in
-// mk/defaults/mk.conf.
-func (ck MkLineChecker) checkVarassignLeftUserSettable() bool {
-       mkline := ck.MkLine
-       varname := mkline.Varname()
-
-       defaultMkline := G.Pkgsrc.UserDefinedVars.Mentioned(varname)
-       if defaultMkline == nil {
-               return false
-       }
-       defaultValue := defaultMkline.Value()
-
-       // A few of the user-settable variables can also be set by packages.
-       // That's an unfortunate situation since there is no definite source
-       // of truth, but luckily only a few variables make use of it.
-       vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
-       if vartype.IsPackageSettable() {
-               return true
-       }
-
-       switch {
-       case mkline.HasComment():
-               // Assume that the comment contains a rationale for disabling
-               // this particular check.
-
-       case mkline.Op() == opAssignAppend:
-               mkline.Warnf("Packages should not append to user-settable %s.", varname)
-
-       case defaultValue != mkline.Value():
-               mkline.Warnf(
-                       "Package sets user-defined %q to %q, which differs "+
-                               "from the default value %q from mk/defaults/mk.conf.",
-                       varname, mkline.Value(), defaultValue)
-
-       case defaultMkline.IsCommentedVarassign():
-               // Since the variable assignment is commented out in
-               // mk/defaults/mk.conf, the package has to define it.
-
-       default:
-               mkline.Notef("Redundant definition for %s from mk/defaults/mk.conf.", varname)
-               if !ck.MkLines.Tools.SeenPrefs {
-                       mkline.Explain(
-                               "Instead of defining the variable redundantly, it suffices to include",
-                               "../../mk/bsd.prefs.mk, which provides all user-settable variables.")
-               }
-       }
-
-       return true
-}
-
-// checkVarassignLeftPermissions checks the permissions for the left-hand side
-// of a variable assignment line.
-//
-// See checkVarusePermissions.
-func (ck MkLineChecker) checkVarassignLeftPermissions() {
-       if !G.Opts.WarnPerm {
-               return
-       }
-       if G.Infrastructure {
-               // As long as vardefs.go doesn't explicitly define permissions for
-               // infrastructure files, skip the check completely. This avoids
-               // many wrong warnings.
-               return
-       }
-       if trace.Tracing {
-               defer trace.Call0()()
-       }
-
-       mkline := ck.MkLine
-       if ck.MkLine.Basename == "hacks.mk" {
-               return
-       }
-
-       varname := mkline.Varname()
-       op := mkline.Op()
-       vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
-       if vartype == nil {
-               return
-       }
-
-       perms := vartype.EffectivePermissions(mkline.Basename)
-
-       // E.g. USE_TOOLS:= ${USE_TOOLS:Nunwanted-tool}
-       if op == opAssignEval && perms&aclpAppend != 0 {
-               tokens, _ := mkline.ValueTokens()
-               if len(tokens) == 1 && tokens[0].Varuse != nil && tokens[0].Varuse.varname == varname {
-                       return
-               }
-       }
-
-       var needed ACLPermissions
-       switch op {
-       case opAssign, opAssignShell, opAssignEval:
-               needed = aclpSet
-       case opAssignDefault:
-               needed = aclpSetDefault
-       case opAssignAppend:
-               needed = aclpAppend
-       }
-
-       switch {
-       case perms.Contains(needed):
-               break
-       default:
-               alternativeActions := perms & aclpAllWrite
-               alternativeFiles := vartype.AlternativeFiles(needed)
-               switch {
-               case alternativeActions != 0 && alternativeFiles != "":
-                       mkline.Warnf("The variable %s should not be %s (only %s) in this file; it would be ok in %s.",
-                               varname, needed.HumanString(), alternativeActions.HumanString(), alternativeFiles)
-               case alternativeFiles != "":
-                       mkline.Warnf("The variable %s should not be %s in this file; it would be ok in %s.",
-                               varname, needed.HumanString(), alternativeFiles)
-               case alternativeActions != 0:
-                       mkline.Warnf("The variable %s should not be %s (only %s) in this file.",
-                               varname, needed.HumanString(), alternativeActions.HumanString())
-               default:
-                       mkline.Warnf("The variable %s should not be %s by any package.",
-                               varname, needed.HumanString())
-               }
-               ck.explainPermissions(varname, vartype)
-       }
-}
-
-func (ck MkLineChecker) explainPermissions(varname string, vartype *Vartype, intro ...string) {
-       if !G.Logger.Opts.Explain {
-               return
-       }
-
-       // TODO: Starting with the second explanation, omit the common part. Instead, only list the permission rules.
-
-       var expl []string
-
-       if len(intro) > 0 {
-               expl = append(expl, intro...)
-               expl = append(expl, "")
-       }
-
-       expl = append(expl,
-               "The allowed actions for a variable are determined based on the file",
-               "name in which the variable is used or defined.",
-               sprintf("The rules for %s are:", varname),
-               "")
-
-       for _, rule := range vartype.aclEntries {
-               perms := rule.permissions.HumanString()
-
-               files := rule.matcher.originalPattern
-               if files == "*" {
-                       files = "any file"
-               }
-
-               if perms != "" {
-                       expl = append(expl, sprintf("* in %s, it may be %s", files, perms))
-               } else {
-                       expl = append(expl, sprintf("* in %s, it should not be accessed at all", files))
-               }
-       }
-
-       expl = append(expl,
-               "",
-               "If these rules seem to be incorrect, please ask on the tech-pkg%NetBSD.org@localhost mailing list.")
-
-       ck.MkLine.Explain(expl...)
-}
-
-func (ck MkLineChecker) checkVarassignLeftRationale() {
-       if !G.Opts.WarnExtra {
-               return
-       }
-
-       mkline := ck.MkLine
-       vartype := G.Pkgsrc.VariableType(ck.MkLines, mkline.Varname())
-       if vartype == nil || !vartype.NeedsRationale() {
-               return
-       }
-
-       if mkline.HasRationale() {
-               return
-       }
-
-       mkline.Warnf("Setting variable %s should have a rationale.", mkline.Varname())
-       mkline.Explain(
-               "Since this variable prevents the package from being built in some situations,",
-               "the reasons for this restriction should be documented.",
-               "Otherwise it becomes too difficult to check whether these restrictions still apply",
-               "when the package is updated by someone else later.",
-               "",
-               "To add the rationale, put it in a comment at the end of this line,",
-               "or in a separate comment in the line above.",
-               "The rationale should try to answer these questions:",
-               "",
-               "* which specific aspects of the package are affected?",
-               "* if it's a dependency, is the dependency too old or too new?",
-               "* in which situations does a crash occur, if any?",
-               "* has it been reported upstream?")
-}
-
 func (ck MkLineChecker) checkTextVarUse(text string, vartype *Vartype, time VucTime) {
        if !contains(text, "$") {
                return
@@ -389,585 +64,15 @@ func (ck MkLineChecker) checkTextVarUse(
 
        tokens, _ := NewMkLexer(text, nil).MkTokens()
        for i, token := range tokens {
+               // TODO: flatten
                if token.Varuse != nil {
                        spaceLeft := i-1 < 0 || matches(tokens[i-1].Text, `[\t ]$`)
                        spaceRight := i+1 >= len(tokens) || matches(tokens[i+1].Text, `^[\t ]`)
                        isWordPart := !(spaceLeft && spaceRight)
                        vuc := VarUseContext{vartype, time, VucQuotPlain, isWordPart}
-                       ck.CheckVaruse(token.Varuse, &vuc)
-               }
-       }
-}
-
-// CheckVaruse checks a single use of a variable in a specific context.
-func (ck MkLineChecker) CheckVaruse(varuse *MkVarUse, vuc *VarUseContext) {
-       mkline := ck.MkLine
-       if trace.Tracing {
-               defer trace.Call(mkline, varuse, vuc)()
-       }
-
-       if varuse.IsExpression() {
-               return
-       }
-
-       varname := varuse.varname
-       vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
-
-       ck.checkVaruseUndefined(vartype, varname)
-       ck.checkVaruseModifiers(varuse, vartype)
-       ck.checkVarUseVarname(varuse)
-       ck.checkVarusePermissions(varname, vartype, vuc)
-       ck.checkVarUseQuoting(varuse, vartype, vuc)
-       ck.checkVarUseBuildDefs(varname)
-       ck.checkVaruseDeprecated(varuse)
-       ck.checkTextVarUse(varname, vartype, vuc.time)
-}
-
-func (ck MkLineChecker) checkVaruseUndefined(vartype *Vartype, varname string) {
-       switch {
-       case !G.Opts.WarnExtra,
-               // Well-known variables are probably defined by the infrastructure.
-               vartype != nil && !vartype.IsGuessed(),
-               ck.MkLines.vars.IsDefinedSimilar(varname),
-               ck.MkLines.forVars[varname],
-               ck.MkLines.vars.Mentioned(varname) != nil,
-               G.Pkg != nil && G.Pkg.vars.IsDefinedSimilar(varname),
-               containsVarRef(varname),
-               G.Pkgsrc.vartypes.IsDefinedCanon(varname),
-               varname == "":
-               return
-       }
-
-       if ck.MkLines.once.FirstTimeSlice("used but not defined", varname) {
-               ck.MkLine.Warnf("%s is used but not defined.", varname)
-       }
-}
-
-func (ck MkLineChecker) checkVaruseModifiers(varuse *MkVarUse, vartype *Vartype) {
-       mods := varuse.modifiers
-       if len(mods) == 0 {
-               return
-       }
-
-       ck.checkVaruseModifiersSuffix(varuse, vartype)
-       ck.checkVaruseModifiersRange(varuse)
-
-       // TODO: Add checks for a single modifier, among them:
-       // TODO: Suggest to replace ${VAR:@l@-l${l}@} with the simpler ${VAR:S,^,-l,}.
-       // TODO: Suggest to replace ${VAR:@l@${l}suffix@} with the simpler ${VAR:=suffix}.
-       // TODO: Investigate why :Q is not checked at this exact place.
-}
-
-func (ck MkLineChecker) checkVaruseModifiersSuffix(varuse *MkVarUse, vartype *Vartype) {
-       if varuse.modifiers[0].IsSuffixSubst() && vartype != nil && !vartype.IsList() {
-               ck.MkLine.Warnf("The :from=to modifier should only be used with lists, not with %s.", varuse.varname)
-               ck.MkLine.Explain(
-                       "Instead of (for example):",
-                       "\tMASTER_SITES=\t${HOMEPAGE:=repository/}",
-                       "",
-                       "Write:",
-                       "\tMASTER_SITES=\t${HOMEPAGE}repository/",
-                       "",
-                       "This is a clearer expression of the same thought.")
-       }
-}
-
-// checkVaruseModifiersRange suggests to replace
-// ${VAR:S,^,__magic__,1:M__magic__*:S,^__magic__,,} with the simpler ${VAR:[1]}.
-func (ck MkLineChecker) checkVaruseModifiersRange(varuse *MkVarUse) {
-       mods := varuse.modifiers
-
-       if len(mods) == 3 {
-               if m, _, from, to, options := mods[0].MatchSubst(); m && from == "^" && matches(to, `^\w+$`) && options == "1" {
-                       magic := to
-                       if m, positive, pattern, _ := mods[1].MatchMatch(); m && positive && pattern == magic+"*" {
-                               if m, _, from, to, options = mods[2].MatchSubst(); m && from == "^"+magic && to == "" && options == "" {
-                                       fix := ck.MkLine.Autofix()
-                                       fix.Notef("The modifier %q can be written as %q.", varuse.Mod(), ":[1]")
-                                       fix.Explain(
-                                               "The range modifier is much easier to understand than the",
-                                               "complicated regular expressions, which were needed before",
-                                               "the year 2006.")
-                                       fix.Replace(varuse.Mod(), ":[1]")
-                                       fix.Apply()
-                               }
-                       }
-               }
-       }
-}
-
-func (ck MkLineChecker) checkVarUseVarname(varuse *MkVarUse) {
-       if varuse.varname == "@" {
-               ck.MkLine.Warnf("Please use %q instead of %q.", "${.TARGET}", "$@")
-               ck.MkLine.Explain(
-                       "It is more readable and prevents confusion with the shell variable",
-                       "of the same name.")
-       }
-
-       if varuse.varname == "LOCALBASE" && !G.Infrastructure {
-               fix := ck.MkLine.Autofix()
-               fix.Warnf("Please use PREFIX instead of LOCALBASE.")
-               fix.ReplaceRegex(`\$\{LOCALBASE\b`, "${PREFIX", 1)
-               fix.Apply()
-       }
-}
-
-// checkVarusePermissions checks the permissions when a variable is used,
-// be it in a variable assignment, in a shell command, a conditional, or
-// somewhere else.
-//
-// See checkVarassignLeftPermissions.
-func (ck MkLineChecker) checkVarusePermissions(varname string, vartype *Vartype, vuc *VarUseContext) {
-       if !G.Opts.WarnPerm {
-               return
-       }
-       if G.Infrastructure {
-               // As long as vardefs.go doesn't explicitly define permissions for
-               // infrastructure files, skip the check completely. This avoids
-               // many wrong warnings.
-               return
-       }
-
-       if trace.Tracing {
-               defer trace.Call(varname, vuc)()
-       }
-
-       // This is the type of the variable that is being used. Not to
-       // be confused with vuc.vartype, which is the type of the
-       // context in which the variable is used (often a ShellCommand
-       // or, in an assignment, the type of the left hand side variable).
-       if vartype == nil {
-               if trace.Tracing {
-                       trace.Step1("No type definition found for %q.", varname)
-               }
-               return
-       }
-
-       if vartype.IsGuessed() {
-               return
-       }
-
-       // Do not warn about unknown infrastructure variables.
-       // These have all permissions to prevent warnings when they are used.
-       // But when other variables are assigned to them it would seem as if
-       // these other variables could become evaluated at load time.
-       // And this is something that most variables do not allow.
-       if vuc.vartype != nil && vuc.vartype.basicType == BtUnknown {
-               return
-       }
-
-       basename := ck.MkLine.Basename
-       if basename == "hacks.mk" {
-               return
-       }
-
-       effPerms := vartype.EffectivePermissions(basename)
-       if effPerms.Contains(aclpUseLoadtime) {
-               // Since the variable may be used at load time, it probably
-               // may be used at run time as well. If it weren't, that would
-               // be a rather strange permissions set.
-               return
-       }
-
-       // At this point the variable must not be used at load time.
-       // Now determine whether it is directly used at load time because
-       // the context already says so or, a little trickier, if it might
-       // be used at load time somewhere in the future because it is
-       // assigned to another variable, and that variable is allowed
-       // to be used at load time.
-       directly := vuc.time == VucLoadTime
-       indirectly := !directly && vuc.vartype != nil &&
-               vuc.vartype.Union().Contains(aclpUseLoadtime)
-
-       if !directly && !indirectly && effPerms.Contains(aclpUse) {
-               // At this point the variable is either used at run time, or the
-               // time is not known.
-               return
-       }
-
-       if directly || indirectly {
-               // At this point the variable is used at load time although that
-               // is not allowed by the permissions. The variable could be a tool
-               // variable, and these tool variables have special rules.
-               tool := G.ToolByVarname(ck.MkLines, varname)
-               if tool != nil {
-
-                       // Whether a tool variable may be used at load time depends on
-                       // whether bsd.prefs.mk has been included before. That file
-                       // examines the tools that have been added to USE_TOOLS up to
-                       // this point and makes their variables available for use at
-                       // load time.
-                       if !tool.UsableAtLoadTime(ck.MkLines.Tools.SeenPrefs) {
-                               ck.warnVaruseToolLoadTime(varname, tool)
-                       }
-                       return
-               }
-       }
-
-       if ck.MkLines.once.FirstTimeSlice("checkVarusePermissions", varname) {
-               ck.warnVarusePermissions(vuc.vartype, varname, vartype, directly, indirectly)
-       }
-}
-
-func (ck MkLineChecker) warnVarusePermissions(
-       vucVartype *Vartype, varname string, vartype *Vartype, directly, indirectly bool) {
-
-       mkline := ck.MkLine
-
-       anyPerms := vartype.Union()
-       if !anyPerms.Contains(aclpUse) && !anyPerms.Contains(aclpUseLoadtime) {
-               mkline.Warnf("%s should not be used in any file; it is a write-only variable.", varname)
-               ck.explainPermissions(varname, vartype)
-               return
-       }
-
-       if indirectly {
-               // Some of the guessed variables may be used at load time. But since the
-               // variable type and these permissions are guessed, pkglint should not
-               // issue the following warning, since it is often wrong.
-               if vucVartype.IsGuessed() {
-                       return
-               }
-
-               mkline.Warnf("%s should not be used indirectly at load time (via %s).",
-                       varname, mkline.Varname())
-               ck.explainPermissions(varname, vartype,
-                       "The variable on the left-hand side may be evaluated at load time,",
-                       "but the variable on the right-hand side should not.",
-                       "Because of the assignment in this line, the variable might be",
-                       "used indirectly at load time, before it is guaranteed to be",
-                       "properly initialized.")
-               return
-       }
-
-       needed := aclpUse
-       if directly {
-               needed = aclpUseLoadtime
-       }
-       alternativeFiles := vartype.AlternativeFiles(needed)
-
-       loadTimeExplanation := func() []string {
-               return []string{
-                       "Many variables, especially lists of something, get their values incrementally.",
-                       "Therefore it is generally unsafe to rely on their",
-                       "value until it is clear that it will never change again.",
-                       "This point is reached when the whole package Makefile is loaded and",
-                       "execution of the shell commands starts; in some cases earlier.",
-                       "",
-                       "Additionally, when using the \":=\" operator, each $$ is replaced",
-                       "with a single $, so variables that have references to shell",
-                       "variables or regular expressions are modified in a subtle way."}
-       }
-
-       switch {
-       case alternativeFiles == "" && directly:
-               mkline.Warnf("%s should not be used at load time in any file.", varname)
-               ck.explainPermissions(varname, vartype, loadTimeExplanation()...)
-
-       case alternativeFiles == "":
-               mkline.Warnf("%s should not be used in any file.", varname)
-               ck.explainPermissions(varname, vartype, loadTimeExplanation()...)
-
-       case directly:
-               mkline.Warnf(
-                       "%s should not be used at load time in this file; "+
-                               "it would be ok in %s.",
-                       varname, alternativeFiles)
-               ck.explainPermissions(varname, vartype, loadTimeExplanation()...)
-
-       default:
-               mkline.Warnf(
-                       "%s should not be used in this file; it would be ok in %s.",
-                       varname, alternativeFiles)
-               ck.explainPermissions(varname, vartype)
-       }
-}
-
-// warnVaruseToolLoadTime logs a warning that the tool ${varname}
-// should not be used at load time.
-func (ck MkLineChecker) warnVaruseToolLoadTime(varname string, tool *Tool) {
-       // TODO: While using a tool by its variable name may be ok at load time,
-       //  doing the same with the plain name of a tool is never ok.
-       //  "VAR!= cat" is never guaranteed to call the correct cat.
-       //  Even for shell builtins like echo and printf, bmake may decide
-       //  to skip the shell and execute the commands via execve, which
-       //  means that even echo is not a shell-builtin anymore.
-
-       // TODO: Replace "parse time" with "load time" everywhere.
-
-       if tool.Validity == AfterPrefsMk {
-               ck.MkLine.Warnf("To use the tool ${%s} at load time, bsd.prefs.mk has to be included before.", varname)
-               return
-       }
-
-       if ck.MkLine.Basename == "Makefile" {
-               pkgsrcTool := G.Pkgsrc.Tools.ByName(tool.Name)
-               if pkgsrcTool != nil && pkgsrcTool.Validity == Nowhere {
-                       // The tool must have been added too late to USE_TOOLS,
-                       // i.e. after bsd.prefs.mk has been included.
-                       ck.MkLine.Warnf("To use the tool ${%s} at load time, it has to be added to USE_TOOLS before including bsd.prefs.mk.", varname)
-                       return
-               }
-       }
-
-       ck.MkLine.Warnf("The tool ${%s} cannot be used at load time.", varname)
-       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.",
-               "Adding the tool to USE_TOOLS at any later time has no effect,",
-               "which means that the tool can only be used at run time.",
-               "That's the rule for the package Makefiles.",
-               "",
-               "Since any other .mk file can be included from anywhere else, there",
-               "is no guarantee that the tool is properly defined for using it at",
-               "load time (see above for the tricky rules).",
-               "Therefore the tools can only be used at run time,",
-               "except in the package Makefile itself.")
-}
-
-// checkVarUseWords checks whether a variable use of the form ${VAR}
-// or ${VAR:modifiers} is allowed in a certain context.
-func (ck MkLineChecker) checkVarUseQuoting(varUse *MkVarUse, vartype *Vartype, vuc *VarUseContext) {
-       if !G.Opts.WarnQuoting || vuc.quoting == VucQuotUnknown {
-               return
-       }
-
-       needsQuoting := ck.MkLine.VariableNeedsQuoting(ck.MkLines, varUse, vartype, vuc)
-       if needsQuoting == unknown {
-               return
-       }
-
-       varname := varUse.varname
-       mod := varUse.Mod()
-
-       // In GNU configure scripts, a few variables need to be passed through
-       // the :M* modifier before they reach the configure scripts. Otherwise
-       // the leading or trailing spaces will lead to strange caching errors
-       // since the GNU configure scripts cannot handle these space characters.
-       //
-       // When doing checks outside a package, the :M* modifier is needed for safety.
-       needMstar := (G.Pkg == nil || G.Pkg.vars.IsDefined("GNU_CONFIGURE")) &&
-               matches(varname, `^(?:.*_)?(?:CFLAGS|CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`)
-
-       mkline := ck.MkLine
-       if mod == ":M*:Q" && !needMstar {
-               if !vartype.IsGuessed() {
-                       mkline.Notef("The :M* modifier is not needed here.")
+                       NewMkVarUseChecker(token.Varuse, ck.MkLines, ck.MkLine).Check(&vuc)
                }
-
-       } else if needsQuoting == yes {
-               modNoQ := strings.TrimSuffix(mod, ":Q")
-               modNoM := strings.TrimSuffix(modNoQ, ":M*")
-               correctMod := modNoM + condStr(needMstar, ":M*:Q", ":Q")
-               if correctMod == mod+":Q" && vuc.IsWordPart && !vartype.IsShell() {
-
-                       isSingleWordConstant := func() bool {
-                               if G.Pkg == nil {
-                                       return false
-                               }
-
-                               varinfo := G.Pkg.redundant.vars[varname]
-                               if varinfo == nil || !varinfo.vari.IsConstant() {
-                                       return false
-                               }
-
-                               value := varinfo.vari.ConstantValue()
-                               return len(mkline.ValueFields(value)) == 1
-                       }
-
-                       if vartype.IsList() && isSingleWordConstant() {
-                               // Do not warn in this special case, which typically occurs
-                               // for BUILD_DIRS or similar package-settable variables.
-
-                       } else if vartype.IsList() {
-                               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",
-                                       "to something unexpected:",
-                                       "",
-                                       "Example: ${MASTER_SITE_SOURCEFORGE}directory/ expands to",
-                                       "",
-                                       "\thttps://mirror1.sf.net/ https://mirror2.sf.net/directory/";,
-                                       "",
-                                       "The first URL is missing the directory.",
-                                       "To fix this, write",
-                                       "\t${MASTER_SITE_SOURCEFORGE:=directory/}.",
-                                       "",
-                                       "Example: -l${LIBS} expands to",
-                                       "",
-                                       "\t-llib1 lib2",
-                                       "",
-                                       "The second library is missing the -l.",
-                                       "To fix this, write ${LIBS:S,^,-l,}.")
-                       } else {
-                               mkline.Warnf("The variable %s should be quoted as part of a shell word.", varname)
-                               mkline.Explain(
-                                       "This variable can contain spaces or other special characters.",
-                                       "Therefore it should be quoted by replacing ${VAR} with ${VAR:Q}.")
-                       }
-
-               } else if mod != correctMod {
-                       if vuc.quoting == VucQuotPlain {
-                               fix := mkline.Autofix()
-                               fix.Warnf("Please use ${%s%s} instead of ${%s%s}.", varname, correctMod, varname, mod)
-                               fix.Explain(
-                                       seeGuide("Echoing a string exactly as-is", "echo-literal"))
-                               fix.Replace("${"+varname+mod+"}", "${"+varname+correctMod+"}")
-                               fix.Apply()
-                       } else {
-                               mkline.Warnf("Please use ${%s%s} instead of ${%s%s} and make sure"+
-                                       " the variable appears outside of any quoting characters.", varname, correctMod, varname, mod)
-                               mkline.Explain(
-                                       "The :Q modifier only works reliably when it is used outside of any",
-                                       "quoting characters like 'single' or \"double\" quotes or `backticks`.",
-                                       "",
-                                       "Examples:",
-                                       "Instead of CFLAGS=\"${CFLAGS:Q}\",",
-                                       "     write CFLAGS=${CFLAGS:Q}.",
-                                       "Instead of 's,@CFLAGS@,${CFLAGS:Q},',",
-                                       "     write 's,@CFLAGS@,'${CFLAGS:Q}','.",
-                                       "",
-                                       seeGuide("Echoing a string exactly as-is", "echo-literal"))
-                       }
-
-               } else if vuc.quoting != VucQuotPlain {
-                       mkline.Warnf("Please move ${%s%s} outside of any quoting characters.", varname, mod)
-                       mkline.Explain(
-                               "The :Q modifier only works reliably when it is used outside of any",
-                               "quoting characters like 'single' or \"double\" quotes or `backticks`.",
-                               "",
-                               "Examples:",
-                               "Instead of CFLAGS=\"${CFLAGS:Q}\",",
-                               "     write CFLAGS=${CFLAGS:Q}.",
-                               "Instead of 's,@CFLAGS@,${CFLAGS:Q},',",
-                               "     write 's,@CFLAGS@,'${CFLAGS:Q}','.",
-                               "",
-                               seeGuide("Echoing a string exactly as-is", "echo-literal"))
-               }
-       }
-
-       if hasSuffix(mod, ":Q") && needsQuoting != yes {
-               bad := "${" + varname + mod + "}"
-               good := "${" + varname + strings.TrimSuffix(mod, ":Q") + "}"
-
-               fix := mkline.Line.Autofix()
-               fix.Notef("The :Q modifier isn't necessary for ${%s} here.", varname)
-               fix.Explain(
-                       "Many variables in pkgsrc do not need the :Q modifier since they",
-                       "are not expected to contain whitespace or other special characters.",
-                       "Examples for these \"safe\" variables are:",
-                       "",
-                       "\t* filenames",
-                       "\t* directory names",
-                       "\t* user and group names",
-                       "\t* tool names and tool paths",
-                       "\t* variable names",
-                       "\t* package names (but not dependency patterns like pkg>=1.2)")
-               fix.Replace(bad, good)
-               fix.Apply()
-       }
-}
-
-func (ck MkLineChecker) checkVarUseBuildDefs(varname string) {
-       if !(G.Pkgsrc.UserDefinedVars.IsDefined(varname) && !G.Pkgsrc.IsBuildDef(varname)) {
-               return
-       }
-
-       if !(!ck.MkLines.buildDefs[varname] && ck.MkLines.once.FirstTimeSlice("BUILD_DEFS", varname)) {
-               return
-       }
-
-       ck.MkLine.Warnf("The user-defined variable %s is used but not added to BUILD_DEFS.", varname)
-       ck.MkLine.Explain(
-               "When a pkgsrc package is built, many things can be configured by the",
-               "pkgsrc user in the mk.conf file.",
-               "All these configurations should be recorded in the binary package",
-               "so the package can be reliably rebuilt.",
-               "The BUILD_DEFS variable contains a list of all these",
-               "user-settable variables, so please add your variable to it, too.")
-}
-
-func (ck MkLineChecker) checkVaruseDeprecated(varuse *MkVarUse) {
-       varname := varuse.varname
-       instead := G.Pkgsrc.Deprecated[varname]
-       if instead == "" {
-               instead = G.Pkgsrc.Deprecated[varnameCanon(varname)]
-       }
-       if instead != "" {
-               ck.MkLine.Warnf("Use of %q is deprecated. %s", varname, instead)
-       }
-}
-
-func (ck MkLineChecker) checkVarassignOp() {
-       ck.checkVarassignOpShell()
-}
-
-func (ck MkLineChecker) checkVarassignOpShell() {
-       mkline := ck.MkLine
-
-       switch {
-       case mkline.Op() != opAssignShell:
-               return
-
-       case mkline.HasComment():
-               return
-
-       case mkline.Basename == "builtin.mk":
-               // These are typically USE_BUILTIN.* and BUILTIN_VERSION.*.
-               // Authors of builtin.mk files usually know what they're doing.
-               return
-
-       case G.Pkg == nil || G.Pkg.vars.IsUsedAtLoadTime(mkline.Varname()):
-               return
-       }
-
-       mkline.Notef("Consider the :sh modifier instead of != for %q.", mkline.Value())
-       mkline.Explain(
-               "For variable assignments using the != operator, the shell command",
-               "is run every time the file is parsed.",
-               "In some cases this is too early, and the command may not yet be installed.",
-               "In other cases the command is executed more often than necessary.",
-               "Most commands don't need to be executed for \"make clean\", for example.",
-               "",
-               "The :sh modifier defers execution until the variable value is actually needed.",
-               "On the other hand, this means the command is executed each time the variable",
-               "is evaluated.",
-               "",
-               "Example:",
-               "",
-               "\tEARLY_YEAR!=    date +%Y",
-               "",
-               "\tLATE_YEAR_CMD=  date +%Y",
-               "\tLATE_YEAR=      ${LATE_YEAR_CMD:sh}",
-               "",
-               "\t# or, in a single line:",
-               "\tLATE_YEAR=      ${date +%Y:L:sh}",
-               "",
-               "To suppress this note, provide an explanation in a comment at the end",
-               "of the line, or force the variable to be evaluated at load time,",
-               "by using it at the right-hand side of the := operator, or in an .if",
-               "or .for directive.")
-}
-
-// checkVarassignLeft checks everything to the right of the assignment operator.
-func (ck MkLineChecker) checkVarassignRight() {
-       mkline := ck.MkLine
-       varname := mkline.Varname()
-       op := mkline.Op()
-       value := mkline.Value()
-       comment := condStr(mkline.HasComment(), "#", "") + mkline.Comment()
-
-       if trace.Tracing {
-               defer trace.Call(varname, op, value)()
        }
-
-       ck.checkText(value)
-       ck.checkVartype(varname, op, value, comment)
-
-       ck.checkVarassignMisc()
-
-       ck.checkVarassignRightVaruse()
 }
 
 // checkText checks the given text (which is typically the right-hand side of a variable
@@ -1010,6 +115,9 @@ func (ck MkLineChecker) checkTextRpath(t
        }
 }
 
+// checkVartype checks the type of the given variable, when it is assigned the given value,
+// or if op is either opUseCompare or opUseMatch, when it is compared to the given value.
+//
 // comment is an empty string for no comment, or "#" + the actual comment otherwise.
 func (ck MkLineChecker) checkVartype(varname string, op MkOperator, value, comment string) {
        if trace.Tracing {
@@ -1020,7 +128,7 @@ func (ck MkLineChecker) checkVartype(var
        vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
 
        if op == opAssignAppend {
-               // XXX: MayBeAppendedTo also depends on the current file, see checkVarusePermissions.
+               // XXX: MayBeAppendedTo also depends on the current file, see MkVarUseChecker.checkPermissions.
                // These checks may be combined.
                if vartype != nil && !vartype.MayBeAppendedTo() {
                        mkline.Warnf("The \"+=\" operator should only be used with lists, not with %s.", varname)
@@ -1054,7 +162,8 @@ func (ck MkLineChecker) checkVartype(var
                                "Or, enclose the words in quotes to group them.")
                }
                if vartype.basicType == BtCategory {
-                       ck.checkVarassignRightCategory()
+                       mkAssignChecker := NewMkAssignChecker(mkline, ck.MkLines)
+                       mkAssignChecker.checkVarassignRightCategory()
                }
                for _, word := range words {
                        ck.CheckVartypeBasic(varname, vartype.basicType, op, word, comment, vartype.IsGuessed())
@@ -1077,183 +186,6 @@ func (ck MkLineChecker) CheckVartypeBasi
        checker.checker(&ctx)
 }
 
-func (ck MkLineChecker) checkVarassignRightCategory() {
-       mkline := ck.MkLine
-       if mkline.Op() != opAssign && mkline.Op() != opAssignDefault {
-               return
-       }
-
-       categories := mkline.ValueFields(mkline.Value())
-       actual := categories[0]
-       // FIXME: consider DirNoClean
-       // FIXME: consider DirNoClean
-       expected := G.Pkgsrc.ToRel(mkline.Filename).DirClean().DirClean().Base()
-
-       if expected == "wip" || actual == expected {
-               return
-       }
-
-       fix := mkline.Autofix()
-       fix.Warnf("The primary category should be %q, not %q.", expected, actual)
-       fix.Explain(
-               "The primary category of a package should be its location in the",
-               "pkgsrc directory tree, to make it easy to find the package.",
-               "All other categories may be added after this primary category.")
-       if len(categories) > 1 && categories[1] == expected {
-               fix.Replace(categories[0]+" "+categories[1], categories[1]+" "+categories[0])
-       }
-       fix.Apply()
-}
-
-func (ck MkLineChecker) checkVarassignMisc() {
-       mkline := ck.MkLine
-       varname := mkline.Varname()
-       value := mkline.Value()
-
-       if contains(value, "/etc/rc.d") && mkline.Varname() != "RPMIGNOREPATH" {
-               mkline.Warnf("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.")
-       }
-
-       if varname == "PYTHON_VERSIONS_ACCEPTED" {
-               ck.checkVarassignDecreasingVersions()
-       }
-
-       if mkline.Comment() == " defined" && !hasSuffix(varname, "_MK") && !hasSuffix(varname, "_COMMON") {
-               mkline.Notef("Please use \"# empty\", \"# none\" or \"# yes\" instead of \"# defined\".")
-               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",
-                       "means \"yes\", in other cases it is an empty list (which is also",
-                       "only the state of the variable), whose meaning could be described",
-                       "with \"none\".",
-                       "It is this meaning that should be described.")
-       }
-
-       switch varname {
-       case "DIST_SUBDIR", "WRKSRC", "MASTER_SITES":
-               // TODO: Replace regex with proper VarUse.
-               if m, revVarname := match1(value, `\$\{(PKGNAME|PKGVERSION)[:\}]`); m {
-                       mkline.Warnf("%s should not be used in %s as it includes the PKGREVISION. "+
-                               "Please use %[1]s_NOREV instead.", revVarname, varname)
-               }
-       }
-
-       if hasPrefix(varname, "SITES_") {
-               mkline.Warnf("SITES_* is deprecated. Please use SITES.* instead.")
-               // No autofix since it doesn't occur anymore.
-       }
-
-       if varname == "PKG_SKIP_REASON" && ck.MkLines.indentation.DependsOn("OPSYS") {
-               // TODO: Provide autofix for simple cases, like ".if ${OPSYS} == SunOS".
-               mkline.Notef("Consider setting NOT_FOR_PLATFORM instead of " +
-                       "PKG_SKIP_REASON depending on ${OPSYS}.")
-       }
-
-       ck.checkVarassignMiscRedundantInstallationDirs()
-}
-
-func (ck MkLineChecker) checkVarassignDecreasingVersions() {
-       mkline := ck.MkLine
-       strVersions := mkline.Fields()
-       intVersions := make([]int, len(strVersions))
-       for i, strVersion := range strVersions {
-               iver, err := strconv.Atoi(strVersion)
-               if err != nil || !(iver > 0) {
-                       mkline.Errorf("Value %q for %s must be a positive integer.", strVersion, mkline.Varname())
-                       return
-               }
-               intVersions[i] = iver
-       }
-
-       for i, ver := range intVersions {
-               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])
-                       mkline.Explain(
-                               "If they aren't, it may be possible that needless versions of",
-                               "packages are installed.")
-               }
-       }
-}
-
-func (ck MkLineChecker) checkVarassignMiscRedundantInstallationDirs() {
-       mkline := ck.MkLine
-       varname := mkline.Varname()
-
-       switch {
-       case G.Pkg == nil,
-               varname != "INSTALLATION_DIRS",
-               !matches(G.Pkg.vars.LastValue("AUTO_MKDIRS"), `^[Yy][Ee][Ss]$`):
-               return
-       }
-
-       for _, dir := range mkline.ValueFields(mkline.Value()) {
-               if G.Pkg.Plist.Dirs[NewPath(dir)] != nil {
-                       mkline.Notef("The directory %q is redundant in %s.", dir, varname)
-                       mkline.Explain(
-                               "This package defines AUTO_MKDIR, and the directory is contained in the PLIST.",
-                               "Therefore it will be created anyways.")
-               }
-       }
-}
-
-// checkVarassignRightVaruse checks that in a variable assignment,
-// each variable used on the right-hand side of the assignment operator
-// has the correct data type and quoting.
-func (ck MkLineChecker) checkVarassignRightVaruse() {
-       if trace.Tracing {
-               defer trace.Call0()()
-       }
-
-       mkline := ck.MkLine
-       op := mkline.Op()
-
-       time := VucRunTime
-       if op == opAssignEval || op == opAssignShell {
-               time = VucLoadTime
-       }
-
-       vartype := G.Pkgsrc.VariableType(ck.MkLines, mkline.Varname())
-       if op == opAssignShell {
-               vartype = shellCommandsType
-       }
-
-       if vartype != nil && vartype.IsShell() {
-               ck.checkVarassignVaruseShell(vartype, time)
-       } else { // XXX: This else looks as if it should be omitted.
-               ck.checkTextVarUse(ck.MkLine.Value(), vartype, time)
-       }
-}
-
-// checkVarassignVaruseShell is very similar to checkVarassignRightVaruse, they just differ
-// in the way they determine isWordPart.
-func (ck MkLineChecker) checkVarassignVaruseShell(vartype *Vartype, time VucTime) {
-       if trace.Tracing {
-               defer trace.Call(vartype, time)()
-       }
-
-       isWordPart := func(tokens []*ShAtom, i int) bool {
-               if i-1 >= 0 && tokens[i-1].Type.IsWord() {
-                       return true
-               }
-               if i+1 < len(tokens) && tokens[i+1].Type.IsWord() {
-                       return true
-               }
-               return false
-       }
-
-       mkline := ck.MkLine
-       atoms := NewShTokenizer(mkline.Line, mkline.Value(), false).ShAtoms()
-       for i, atom := range atoms {
-               if varuse := atom.VarUse(); varuse != nil {
-                       wordPart := isWordPart(atoms, i)
-                       vuc := VarUseContext{vartype, time, atom.Quoting.ToVarUseContext(), wordPart}
-                       ck.CheckVaruse(varuse, &vuc)
-               }
-       }
-}
-
 func (ck MkLineChecker) checkShellCommand() {
        mkline := ck.MkLine
 
@@ -1305,7 +237,7 @@ func (ck MkLineChecker) checkInclude() {
        if trace.Tracing {
                trace.Stepf("includingFile=%s includedFile=%s", mkline.Filename, includedFile)
        }
-       ck.CheckRelativePath(NewRelPath(includedFile), mustExist)
+       ck.CheckRelativePath(includedFile, mustExist)
 
        switch {
        case includedFile.HasBase("Makefile"):
@@ -1316,23 +248,21 @@ func (ck MkLineChecker) checkInclude() {
                        "module.mk or similar.",
                        "After that, both this one and the other package should include the newly created file.")
 
-       case IsPrefs(includedFile):
-               if mkline.Basename == "buildlink3.mk" && includedFile == "../../mk/bsd.prefs.mk" {
-                       fix := mkline.Autofix()
-                       fix.Notef("For efficiency reasons, please include bsd.fast.prefs.mk instead of bsd.prefs.mk.")
-                       fix.Replace("bsd.prefs.mk", "bsd.fast.prefs.mk")
-                       fix.Apply()
-               }
+       case mkline.Basename == "buildlink3.mk" && includedFile.Base() == "bsd.prefs.mk":
+               fix := mkline.Autofix()
+               fix.Notef("For efficiency reasons, please include bsd.fast.prefs.mk instead of bsd.prefs.mk.")
+               fix.Replace("bsd.prefs.mk", "bsd.fast.prefs.mk")
+               fix.Apply()
 
        case includedFile.HasSuffixPath("pkgtools/x11-links/buildlink3.mk"):
                fix := mkline.Autofix()
-               fix.Errorf("%s must not be included directly. Include \"../../mk/x11.buildlink3.mk\" instead.", includedFile)
+               fix.Errorf("%q must not be included directly. Include \"../../mk/x11.buildlink3.mk\" instead.", includedFile)
                fix.Replace("pkgtools/x11-links/buildlink3.mk", "mk/x11.buildlink3.mk")
                fix.Apply()
 
        case includedFile.HasSuffixPath("graphics/jpeg/buildlink3.mk"):
                fix := mkline.Autofix()
-               fix.Errorf("%s must not be included directly. Include \"../../mk/jpeg.buildlink3.mk\" instead.", includedFile)
+               fix.Errorf("%q must not be included directly. Include \"../../mk/jpeg.buildlink3.mk\" instead.", includedFile)
                fix.Replace("graphics/jpeg/buildlink3.mk", "mk/jpeg.buildlink3.mk")
                fix.Apply()
 
@@ -1342,9 +272,8 @@ func (ck MkLineChecker) checkInclude() {
        case includedFile != "builtin.mk" && includedFile.HasSuffixPath("builtin.mk"):
                if mkline.Basename != "hacks.mk" && !mkline.HasRationale() {
                        fix := mkline.Autofix()
-                       // FIXME: Use %q instead of %s.
-                       // FIXME: consider DirNoClean
-                       fix.Errorf("%s must not be included directly. Include \"%s/buildlink3.mk\" instead.", includedFile, includedFile.DirClean())
+                       fix.Errorf("%q must not be included directly. Include %q instead.",
+                               includedFile, includedFile.DirNoClean().JoinNoClean("buildlink3.mk"))
                        fix.Replace("builtin.mk", "buildlink3.mk")
                        fix.Apply()
                }
@@ -1379,15 +308,10 @@ func (ck MkLineChecker) CheckRelativePat
                return
        }
 
-       if resolvedPath.AsPath().IsAbs() {
-               mkline.Errorf("The path %q must be relative.", resolvedPath)
-               return
-       }
-
-       // FIXME: consider DirNoClean
-       abs := mkline.Filename.DirClean().JoinNoClean(resolvedPath.AsPath())
+       abs := mkline.Filename.DirNoClean().JoinNoClean(resolvedPath)
        if !abs.Exists() {
-               if mustExist && !ck.MkLines.indentation.HasExists(resolvedPath) {
+               pkgsrcPath := G.Pkgsrc.ToRel(ck.MkLine.File(resolvedPath))
+               if mustExist && !ck.MkLines.indentation.HasExists(pkgsrcPath) {
                        mkline.Errorf("Relative path %q does not exist.", resolvedPath)
                }
                return
@@ -1481,7 +405,8 @@ func (ck MkLineChecker) checkDirective(f
                }
 
        case directive == "if" || directive == "elif":
-               ck.checkDirectiveCond()
+               mkCondChecker := NewMkCondChecker(mkline, ck.MkLines)
+               mkCondChecker.checkDirectiveCond()
 
        case directive == "ifdef" || directive == "ifndef":
                mkline.Warnf("The \".%s\" directive is deprecated. Please use \".if %sdefined(%s)\" instead.",
@@ -1526,221 +451,6 @@ func (ck MkLineChecker) checkDirectiveEn
        }
 }
 
-func (ck MkLineChecker) checkDirectiveCond() {
-       mkline := ck.MkLine
-       if trace.Tracing {
-               defer trace.Call1(mkline.Args())()
-       }
-
-       p := NewMkParser(nil, mkline.Args()) // No emitWarnings here, see the code below.
-       cond := p.MkCond()
-       if !p.EOF() {
-               mkline.Warnf("Invalid condition, unrecognized part: %q.", p.Rest())
-               return
-       }
-
-       checkVarUse := func(varuse *MkVarUse) {
-               var vartype *Vartype // TODO: Insert a better type guess here.
-               vuc := VarUseContext{vartype, VucLoadTime, VucQuotPlain, false}
-               ck.CheckVaruse(varuse, &vuc)
-       }
-
-       // Skip subconditions that have already been handled as part of the !(...).
-       done := make(map[interface{}]bool)
-
-       checkNotEmpty := func(not *MkCond) {
-               empty := not.Empty
-               if empty != nil {
-                       ck.checkDirectiveCondEmpty(empty, true, true, not == cond.Not)
-                       done[empty] = true
-               }
-
-               if not.Term != nil && not.Term.Var != nil {
-                       varUse := not.Term.Var
-                       ck.checkDirectiveCondEmpty(varUse, false, false, not == cond.Not)
-                       done[varUse] = true
-               }
-       }
-
-       checkEmpty := func(empty *MkVarUse) {
-               if !done[empty] {
-                       ck.checkDirectiveCondEmpty(empty, true, false, empty == cond.Empty)
-               }
-       }
-
-       checkVar := func(varUse *MkVarUse) {
-               if !done[varUse] {
-                       ck.checkDirectiveCondEmpty(varUse, false, true, cond.Term != nil)
-               }
-       }
-
-       cond.Walk(&MkCondCallback{
-               Not:     checkNotEmpty,
-               Empty:   checkEmpty,
-               Var:     checkVar,
-               Compare: ck.checkDirectiveCondCompare,
-               VarUse:  checkVarUse})
-}
-
-// checkDirectiveCondEmpty checks a condition of the form empty(VAR),
-// empty(VAR:Mpattern) or ${VAR:Mpattern} in an .if directive.
-func (ck MkLineChecker) checkDirectiveCondEmpty(varuse *MkVarUse, fromEmpty bool, notEmpty bool, toplevel bool) {
-       varname := varuse.varname
-       if matches(varname, `^\$.*:[MN]`) {
-               ck.MkLine.Warnf("The empty() function takes a variable name as parameter, not a variable expression.")
-               ck.MkLine.Explain(
-                       "Instead of empty(${VARNAME:Mpattern}), you should write either of the following:",
-                       "",
-                       "\tempty(VARNAME:Mpattern)",
-                       "\t${VARNAME:Mpattern} == \"\"",
-                       "",
-                       "Instead of !empty(${VARNAME:Mpattern}), you should write either of the following:",
-                       "",
-                       "\t!empty(VARNAME:Mpattern)",
-                       "\t${VARNAME:Mpattern}")
-       }
-
-       ck.simplifyCondition(varuse, fromEmpty, notEmpty, toplevel)
-}
-
-// simplifyCondition replaces an unnecessarily complex condition with
-// a simpler condition that's still equivalent.
-//
-// * fromEmpty is true for the form empty(VAR...), and false for ${VAR...}.
-//
-// * notEmpty is true for the form !empty(VAR...), and false for empty(VAR...).
-// It also applies to the ${VAR} form.
-//
-// * toplevel is true for ${VAR...} and false for ${VAR...} && ${VAR2...}.
-func (ck MkLineChecker) simplifyCondition(varuse *MkVarUse, fromEmpty bool, notEmpty bool, toplevel bool) {
-
-       // replace constructs the state before and after the autofix.
-       // The before state is constructed to ensure that only very simple
-       // patterns get replaced automatically.
-       //
-       // Before putting any cases involving special characters into
-       // production, there need to be more tests for the edge cases.
-       replace := func(varname string, m bool, pattern string) (string, string) {
-               op := condStr(notEmpty == m, "==", "!=")
-
-               from := "" +
-                       condStr(notEmpty != fromEmpty, "", "!") +
-                       condStr(fromEmpty, "empty(", "${") +
-                       varname +
-                       condStr(m, ":M", ":N") +
-                       pattern +
-                       condStr(fromEmpty, ")", "}")
-
-               quote := condStr(matches(pattern, `[^\-/0-9@A-Z_a-z]`), "\"", "")
-               to := "${" + varname + "} " + op + " " + quote + pattern + quote
-               return from, to
-       }
-
-       varname := varuse.varname
-       modifiers := varuse.modifiers
-
-       for _, modifier := range modifiers {
-               m, positive, pattern, exact := modifier.MatchMatch()
-               if !m || !positive && len(modifiers) != 1 {
-                       continue
-               }
-
-               ck.checkVartype(varname, opUseMatch, pattern, "")
-
-               vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
-               switch {
-               case !exact,
-                       vartype == nil,
-                       vartype.IsList(),
-                       textproc.NewLexer(pattern).NextBytesSet(mkCondLiteralChars) != pattern:
-                       continue
-               }
-
-               // FIXME: This transformation is only valid if the variable is guaranteed to
-               //  be defined. If that's not the case, the :U modifier must be added.
-               fix := ck.MkLine.Autofix()
-               fix.Notef("%s should be compared using %s instead of matching against %q.",
-                       varname, condStr(positive == notEmpty, "==", "!="), ":"+modifier.Text)
-               fix.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.",
-                       "",
-                       "An entirely different case is when the pattern contains wildcards like ^, *, $.",
-                       "In such a case, using the :M or :N modifiers is useful and preferred.")
-               fix.Replace(replace(varname, positive, pattern))
-               fix.Apply()
-       }
-}
-
-func (ck MkLineChecker) checkDirectiveCondCompare(left *MkCondTerm, op string, right *MkCondTerm) {
-       switch {
-       case left.Var != nil && right.Var == nil && right.Num == "":
-               ck.checkDirectiveCondCompareVarStr(left.Var, op, right.Str)
-       }
-}
-
-func (ck MkLineChecker) checkDirectiveCondCompareVarStr(varuse *MkVarUse, op string, str string) {
-       varname := varuse.varname
-       varmods := varuse.modifiers
-       switch len(varmods) {
-       case 0:
-               ck.checkCompareVarStr(varname, op, str)
-
-       case 1:
-               if m, _, pattern, _ := varmods[0].MatchMatch(); m {
-                       ck.checkVartype(varname, opUseMatch, pattern, "")
-
-                       // After applying the :M or :N modifier, every expression may end up empty,
-                       // regardless of its data type. Therefore there's no point in type-checking that case.
-                       if str != "" {
-                               ck.checkVartype(varname, opUseCompare, str, "")
-                       }
-               }
-
-       default:
-               // This case covers ${VAR:Mfilter:O:u} or similar uses in conditions.
-               // To check these properly, pkglint first needs to know the most common
-               // modifiers and how they interact.
-               // As of March 2019, the modifiers are not modeled.
-               // The following tracing statement makes it easy to discover these cases,
-               // in order to decide whether checking them is worthwhile.
-               if trace.Tracing {
-                       trace.Stepf("checkCompareVarStr ${%s%s} %s %s",
-                               varuse.varname, varuse.Mod(), op, str)
-               }
-       }
-}
-
-func (ck MkLineChecker) checkCompareVarStr(varname, op, value string) {
-       ck.checkVartype(varname, opUseCompare, value, "")
-
-       if varname == "PKGSRC_COMPILER" {
-               ck.checkCompareVarStrCompiler(op, value)
-       }
-}
-
-func (ck MkLineChecker) checkCompareVarStrCompiler(op string, value string) {
-       if !matches(value, `^\w+$`) {
-               return
-       }
-
-       // It would be nice if original text of the whole comparison expression
-       // were available at this point, to avoid guessing how much whitespace
-       // the package author really used.
-
-       matchOp := condStr(op == "==", "M", "N")
-
-       fix := ck.MkLine.Autofix()
-       fix.Errorf("Use ${PKGSRC_COMPILER:%s%s} instead of the %s operator.", matchOp, value, op)
-       fix.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.")
-       fix.Replace("${PKGSRC_COMPILER} "+op+" "+value, "${PKGSRC_COMPILER:"+matchOp+value+"}")
-       fix.Replace("${PKGSRC_COMPILER} "+op+" \""+value+"\"", "${PKGSRC_COMPILER:"+matchOp+value+"}")
-       fix.Apply()
-}
-
 func (ck MkLineChecker) checkDirectiveFor(forVars map[string]bool, indentation *Indentation) {
        mkline := ck.MkLine
        args := mkline.Args()
@@ -1772,7 +482,7 @@ func (ck MkLineChecker) checkDirectiveFo
                forLoopType := NewVartype(btForLoop, List, NewACLEntry("*", aclpAllRead))
                forLoopContext := VarUseContext{forLoopType, VucLoadTime, VucQuotPlain, false}
                mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) {
-                       ck.CheckVaruse(varUse, &forLoopContext)
+                       NewMkVarUseChecker(varUse, ck.MkLines, mkline).Check(&forLoopContext)
                })
        }
 }

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.51 pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.52
--- pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.51    Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go Sun Dec  8 00:06:38 2019
@@ -131,1535 +131,6 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: ~/filename.mk:3: This line looks empty but continues the previous line.")
 }
 
-// Pkglint once interpreted all lists as consisting of shell tokens,
-// splitting this URL at the ampersand.
-func (s *Suite) Test_MkLineChecker_checkVarassign__URL_with_shell_special_characters(c *check.C) {
-       t := s.Init(c)
-
-       G.Pkg = NewPackage(t.File("graphics/gimp-fix-ca"))
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               "MASTER_SITES=\thttp://registry.gimp.org/file/fix-ca.c?action=download&id=9884&file=";)
-
-       mklines.Check()
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassign__list(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/";)
-       t.SetUpVartypes()
-       t.SetUpCommandLine("-Wall", "--explain")
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               "SITES.distfile=\t-${MASTER_SITE_GITHUB:=project/}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: filename.mk:2: The list variable MASTER_SITE_GITHUB should not be embedded in a word.",
-               "",
-               "\tWhen a list variable has multiple elements, this expression expands",
-               "\tto something unexpected:",
-               "",
-               "\tExample: ${MASTER_SITE_SOURCEFORGE}directory/ expands to",
-               "",
-               "\t\thttps://mirror1.sf.net/ https://mirror2.sf.net/directory/";,
-               "",
-               "\tThe first URL is missing the directory. To fix this, write",
-               "\t\t${MASTER_SITE_SOURCEFORGE:=directory/}.",
-               "",
-               "\tExample: -l${LIBS} expands to",
-               "",
-               "\t\t-llib1 lib2",
-               "",
-               "\tThe second library is missing the -l. To fix this, write",
-               "\t${LIBS:S,^,-l,}.",
-               "")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassign(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-
-       mklines := t.NewMkLines("Makefile",
-               MkCvsID,
-               "ac_cv_libpari_libs+=\t-L${BUILDLINK_PREFIX.pari}/lib") // From math/clisp-pari/Makefile, rev. 1.8
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: Makefile:2: ac_cv_libpari_libs is defined but not used.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeft(c *check.C) {
-       t := s.Init(c)
-
-       mklines := t.NewMkLines("module.mk",
-               MkCvsID,
-               "_VARNAME=\tvalue")
-       // Only to prevent "defined but not used".
-       mklines.vars.Use("_VARNAME", mklines.mklines[1], VucRunTime)
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: module.mk:2: Variable names starting with an underscore " +
-                       "(_VARNAME) are reserved for internal pkgsrc use.")
-}
-
-// Files from the pkgsrc infrastructure may define and use variables
-// whose name starts with an underscore.
-func (s *Suite) Test_MkLineChecker_checkVarassignLeft__infrastructure(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPkgsrc()
-       t.CreateFileLines("mk/infra.mk",
-               MkCvsID,
-               "_VARNAME=\t\tvalue",
-               "_SORTED_VARS.group=\tVARNAME")
-       t.FinishSetUp()
-
-       G.Check(t.File("mk/infra.mk"))
-
-       t.CheckOutputLines(
-               "WARN: ~/mk/infra.mk:2: _VARNAME is defined but not used.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeft__documented_underscore(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPkgsrc()
-       t.CreateFileLines("category/package/filename.mk",
-               MkCvsID,
-               "_SORTED_VARS.group=\tVARNAME")
-       t.FinishSetUp()
-
-       G.Check(t.File("category/package/filename.mk"))
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftNotUsed__procedure_call(c *check.C) {
-       t := s.Init(c)
-
-       t.CreateFileLines("mk/pkg-build-options.mk")
-       mklines := t.SetUpFileMkLines("category/package/filename.mk",
-               MkCvsID,
-               "",
-               "pkgbase := glib2",
-               ".include \"../../mk/pkg-build-options.mk\"",
-               "",
-               "VAR=\tvalue")
-
-       mklines.Check()
-
-       // There is no warning for pkgbase although it looks unused as well.
-       // The file pkg-build-options.mk is essentially a procedure call,
-       // and pkgbase is its parameter.
-       //
-       // To distinguish these parameters from ordinary variables, they are
-       // usually written with the := operator instead of the = operator.
-       // This has the added benefit that the parameter is only evaluated
-       // once, especially if it contains references to other variables.
-       t.CheckOutputLines(
-               "WARN: ~/category/package/filename.mk:6: VAR is defined but not used.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftNotUsed__procedure_call_no_tracing(c *check.C) {
-       t := s.Init(c)
-
-       t.DisableTracing() // Just for code coverage
-       t.CreateFileLines("mk/pkg-build-options.mk")
-       mklines := t.SetUpFileMkLines("category/package/filename.mk",
-               MkCvsID,
-               "",
-               "pkgbase := glib2",
-               ".include \"../../mk/pkg-build-options.mk\"")
-
-       mklines.Check()
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftNotUsed__infra(c *check.C) {
-       t := s.Init(c)
-
-       t.CreateFileLines("mk/infra.mk",
-               MkCvsID,
-               "#",
-               "# Package-settable variables:",
-               "#",
-               "# SHORT_DOCUMENTATION",
-               "#\tIf set to no, ...",
-               "#\tsecond line.",
-               "#",
-               "#",
-               ".if ${USED_IN_INFRASTRUCTURE:Uyes:tl} == yes",
-               ".endif")
-       t.SetUpPackage("category/package",
-               "USED_IN_INFRASTRUCTURE=\t${SHORT_DOCUMENTATION}",
-               "",
-               "UNUSED_INFRA=\t${UNDOCUMENTED}")
-       t.FinishSetUp()
-
-       G.Check(t.File("category/package"))
-
-       t.CheckOutputLines(
-               "WARN: ~/category/package/Makefile:22: UNUSED_INFRA is defined but not used.",
-               "WARN: ~/category/package/Makefile:22: UNDOCUMENTED is used but not defined.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftBsdPrefs__vartype_nil(c *check.C) {
-       t := s.Init(c)
-
-       mklines := t.NewMkLines("builtin.mk",
-               MkCvsID,
-               "VAR_SH?=\tvalue")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: builtin.mk:2: VAR_SH is defined but not used.",
-               "WARN: builtin.mk:2: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable(c *check.C) {
-       t := s.Init(c)
-
-       // TODO: Allow CreateFileLines before SetUpPackage, since it matches
-       //  the expected reading order of human readers.
-
-       t.SetUpPackage("category/package",
-               "ASSIGN_DIFF=\t\tpkg",          // assignment, differs from default value
-               "ASSIGN_DIFF2=\t\treally # ok", // ok because of the rationale in the comment
-               "ASSIGN_SAME=\t\tdefault",      // assignment, same value as default
-               "DEFAULT_DIFF?=\t\tpkg",        // default, differs from default value
-               "DEFAULT_SAME?=\t\tdefault",    // same value as default
-               "FETCH_USING=\t\tcurl",         // both user-settable and package-settable
-               "APPEND_DIRS+=\t\tdir3",        // appending requires a separate diagnostic
-               "COMMENTED_SAME?=\tdefault",    // commented default, same value as default
-               "COMMENTED_DIFF?=\tpkg")        // commented default, differs from default value
-       t.CreateFileLines("mk/defaults/mk.conf",
-               MkCvsID,
-               "ASSIGN_DIFF?=default",
-               "ASSIGN_DIFF2?=default",
-               "ASSIGN_SAME?=default",
-               "DEFAULT_DIFF?=\tdefault",
-               "DEFAULT_SAME?=\tdefault",
-               "FETCH_USING=\tauto",
-               "APPEND_DIRS=\tdefault",
-               "#COMMENTED_SAME?=\tdefault",
-               "#COMMENTED_DIFF?=\tdefault")
-       t.Chdir("category/package")
-       t.FinishSetUp()
-
-       G.Check(".")
-
-       t.CheckOutputLines(
-               "WARN: Makefile:20: Package sets user-defined \"ASSIGN_DIFF\" to \"pkg\", "+
-                       "which differs from the default value \"default\" from mk/defaults/mk.conf.",
-               "NOTE: Makefile:22: Redundant definition for ASSIGN_SAME from mk/defaults/mk.conf.",
-               "WARN: Makefile:23: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".",
-               "WARN: Makefile:23: Package sets user-defined \"DEFAULT_DIFF\" to \"pkg\", "+
-                       "which differs from the default value \"default\" from mk/defaults/mk.conf.",
-               "NOTE: Makefile:24: Redundant definition for DEFAULT_SAME from mk/defaults/mk.conf.",
-               "WARN: Makefile:26: Packages should not append to user-settable APPEND_DIRS.",
-               "WARN: Makefile:28: Package sets user-defined \"COMMENTED_DIFF\" to \"pkg\", "+
-                       "which differs from the default value \"default\" from mk/defaults/mk.conf.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable__before_prefs(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpCommandLine("-Wall", "--explain")
-       t.SetUpPackage("category/package",
-               "BEFORE=\tvalue",
-               ".include \"../../mk/bsd.prefs.mk\"")
-       t.CreateFileLines("mk/defaults/mk.conf",
-               MkCvsID,
-               "BEFORE?=\tvalue")
-       t.Chdir("category/package")
-       t.FinishSetUp()
-
-       G.Check(".")
-
-       t.CheckOutputLines(
-               "NOTE: Makefile:20: Redundant definition for BEFORE from mk/defaults/mk.conf.",
-               "",
-               "\tInstead of defining the variable redundantly, it suffices to include",
-               "\t../../mk/bsd.prefs.mk, which provides all user-settable variables.",
-               "")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable__after_prefs(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpCommandLine("-Wall", "--explain")
-       t.SetUpPackage("category/package",
-               ".include \"../../mk/bsd.prefs.mk\"",
-               "AFTER=\tvalue")
-       t.CreateFileLines("mk/defaults/mk.conf",
-               MkCvsID,
-               "AFTER?=\t\tvalue")
-       t.Chdir("category/package")
-       t.FinishSetUp()
-
-       G.Check(".")
-
-       t.CheckOutputLines(
-               "NOTE: Makefile:21: Redundant definition for AFTER from mk/defaults/mk.conf.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable__vartype_nil(c *check.C) {
-       t := s.Init(c)
-
-       t.CreateFileLines("category/package/vars.mk",
-               MkCvsID,
-               "#",
-               "# User-settable variables:",
-               "#",
-               "# USER_SETTABLE",
-               "#\tDocumentation for USER_SETTABLE.",
-               "",
-               ".include \"../../mk/bsd.prefs.mk\"",
-               "",
-               "USER_SETTABLE?=\tdefault")
-       t.SetUpPackage("category/package",
-               "USER_SETTABLE=\tvalue")
-       t.Chdir("category/package")
-       t.FinishSetUp()
-
-       G.Check(".")
-
-       // TODO: As of June 2019, pkglint doesn't parse the "User-settable variables"
-       //  comment. Therefore it doesn't know that USER_SETTABLE is intended to be
-       //  used by other packages. There should be no warning.
-       t.CheckOutputLines(
-               "WARN: Makefile:20: USER_SETTABLE is defined but not used.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions__hacks_mk(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-
-       mklines := t.NewMkLines("hacks.mk",
-               MkCvsID,
-               "OPSYS=\t${PKGREVISION}")
-
-       mklines.Check()
-
-       // No matter how strange the definition or use of a variable sounds,
-       // in hacks.mk it is allowed. Special problems sometimes need solutions
-       // that violate all standards.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.SetUpTool("awk", "AWK", AtRunTime)
-       G.Pkgsrc.vartypes.DefineParse("SET_ONLY", BtUnknown, NoVartypeOptions,
-               "options.mk: set")
-       G.Pkgsrc.vartypes.DefineParse("SET_ONLY_DEFAULT_ELSEWHERE", BtUnknown, NoVartypeOptions,
-               "options.mk: set",
-               "*.mk: default, set")
-       mklines := t.NewMkLines("options.mk",
-               MkCvsID,
-               "PKG_DEVELOPER?=\tyes",
-               "BUILD_DEFS?=\tVARBASE",
-               "USE_TOOLS:=\t${USE_TOOLS:Nunwanted-tool}",
-               "USE_TOOLS:=\t${MY_TOOLS}",
-               "USE_TOOLS:=\tawk",
-               "",
-               "SET_ONLY=\tset",
-               "SET_ONLY:=\teval",
-               "SET_ONLY?=\tdefault",
-               "",
-               "SET_ONLY_DEFAULT_ELSEWHERE=\tset",
-               "SET_ONLY_DEFAULT_ELSEWHERE:=\teval",
-               "SET_ONLY_DEFAULT_ELSEWHERE?=\tdefault")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: options.mk:2: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".",
-               "WARN: options.mk:2: The variable PKG_DEVELOPER should not be given a default value by any package.",
-               "WARN: options.mk:3: The variable BUILD_DEFS should not be given a default value (only appended to) in this file.",
-               "WARN: options.mk:4: USE_TOOLS should not be used at load time in this file; "+
-                       "it would be ok in Makefile.common or builtin.mk, but not buildlink3.mk or *.",
-               "WARN: options.mk:5: MY_TOOLS is used but not defined.",
-               "WARN: options.mk:10: "+
-                       "The variable SET_ONLY should not be given a default value "+
-                       "(only set) in this file.",
-               "WARN: options.mk:14: "+
-                       "The variable SET_ONLY_DEFAULT_ELSEWHERE should not be given a "+
-                       "default value (only set) in this file; it would be ok in *.mk, "+
-                       "but not options.mk.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions__no_tracing(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.DisableTracing() // Just to reach branch coverage for unknown permissions.
-       mklines := t.NewMkLines("options.mk",
-               MkCvsID,
-               "COMMENT=\tShort package description")
-
-       mklines.Check()
-}
-
-// Setting a default license is typical for big software projects
-// like GNOME or KDE that consist of many packages, or for programming
-// languages like Perl or Python that suggest certain licenses.
-//
-// The default license is typically set in a Makefile.common or module.mk.
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions__license_default(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPkgsrc()
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               "LICENSE?=\tgnu-gpl-v2")
-       t.FinishSetUp()
-
-       mklines.Check()
-
-       // 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.
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions__infrastructure(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.CreateFileLines("mk/infra.mk",
-               MkCvsID,
-               "",
-               "PKG_DEVELOPER?=\tyes")
-       t.CreateFileLines("mk/bsd.pkg.mk")
-
-       G.Check(t.File("mk/infra.mk"))
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_explainPermissions(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpCommandLine("-Wall", "--explain")
-       t.SetUpVartypes()
-
-       mklines := t.NewMkLines("buildlink3.mk",
-               MkCvsID,
-               "AUTO_MKDIRS=\tyes")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: buildlink3.mk:2: The variable AUTO_MKDIRS should not be set in this file; "+
-                       "it would be ok in Makefile, Makefile.* or *.mk, "+
-                       "but not buildlink3.mk or builtin.mk.",
-               "",
-               "\tThe allowed actions for a variable are determined based on the file",
-               "\tname in which the variable is used or defined. The rules for",
-               "\tAUTO_MKDIRS are:",
-               "",
-               "\t* in buildlink3.mk, it should not be accessed at all",
-               "\t* in builtin.mk, it should not be accessed at all",
-               "\t* in Makefile, it may be set, given a default value, or used",
-               "\t* in Makefile.*, it may be set, given a default value, or used",
-               "\t* in *.mk, it may be set, given a default value, or used",
-               // TODO: Add a check for infrastructure permissions
-               //  when the "infra:" prefix is added.
-               "",
-               "\tIf these rules seem to be incorrect, please ask on the",
-               "\ttech-pkg%NetBSD.org@localhost mailing list.",
-               "")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignLeftRationale(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-
-       testLines := func(lines []string, diagnostics ...string) {
-               mklines := t.NewMkLines("filename.mk",
-                       lines...)
-
-               mklines.Check()
-
-               t.CheckOutput(diagnostics)
-       }
-       test := func(lines []string, diagnostics ...string) {
-               testLines(append([]string{MkCvsID, ""}, lines...), diagnostics...)
-       }
-       lines := func(lines ...string) []string { return lines }
-
-       test(
-               lines(
-                       MkCvsID,
-                       "ONLY_FOR_PLATFORM=\t*-*-*", // The CVS Id above is not a rationale.
-                       "NOT_FOR_PLATFORM=\t*-*-*",  // Neither does this line have a rationale.
-               ),
-               "WARN: filename.mk:4: Setting variable ONLY_FOR_PLATFORM should have a rationale.",
-               "WARN: filename.mk:5: Setting variable NOT_FOR_PLATFORM should have a rationale.")
-
-       test(
-               lines(
-                       "ONLY_FOR_PLATFORM+=\t*-*-* # rationale in the same line"),
-               nil...)
-
-       test(
-               lines(
-                       "",
-                       "# rationale in the line above",
-                       "ONLY_FOR_PLATFORM+=\t*-*-*"),
-               nil...)
-
-       // A commented variable assignment does not count as a rationale,
-       // since it is not in plain text.
-       test(
-               lines(
-                       "#VAR=\tvalue",
-                       "ONLY_FOR_PLATFORM+=\t*-*-*"),
-               "WARN: filename.mk:4: Setting variable ONLY_FOR_PLATFORM should have a rationale.")
-
-       // Another variable assignment with comment does not count as a rationale.
-       test(
-               lines(
-                       "PKGNAME=\t\tpackage-1.0 # this is not a rationale",
-                       "ONLY_FOR_PLATFORM+=\t*-*-*"),
-               "WARN: filename.mk:4: Setting variable ONLY_FOR_PLATFORM should have a rationale.")
-
-       // A rationale applies to all variable assignments directly below it.
-       test(
-               lines(
-                       "# rationale",
-                       "BROKEN_ON_PLATFORM+=\t*-*-*",
-                       "BROKEN_ON_PLATFORM+=\t*-*-*"), // The rationale applies to this line, too.
-               nil...)
-
-       // Just for code coverage.
-       test(
-               lines(
-                       "PKGNAME=\tpackage-1.0", // Does not need a rationale.
-                       "UNKNOWN=\t${UNKNOWN}"), // Unknown type, does not need a rationale.
-               nil...)
-
-       // When a line requiring a rationale appears in the very first line
-       // or in the second line of a file, there is no index out of bounds error.
-       testLines(
-               lines(
-                       "NOT_FOR_PLATFORM=\t*-*-*",
-                       "NOT_FOR_PLATFORM=\t*-*-*"),
-               sprintf("ERROR: filename.mk:1: Expected %q.", MkCvsID),
-               "WARN: filename.mk:1: Setting variable NOT_FOR_PLATFORM should have a rationale.",
-               "WARN: filename.mk:2: Setting variable NOT_FOR_PLATFORM should have a rationale.")
-
-       // The whole rationale check is only enabled when -Wextra is given.
-       t.SetUpCommandLine()
-
-       test(
-               lines(
-                       MkCvsID,
-                       "ONLY_FOR_PLATFORM=\t*-*-*", // The CVS Id above is not a rationale.
-                       "NOT_FOR_PLATFORM=\t*-*-*",  // Neither does this line have a rationale.
-               ),
-               nil...)
-}
-
-// The ${VARNAME:=suffix} expression should only be used with lists.
-// It typically appears in MASTER_SITE definitions.
-func (s *Suite) Test_MkLineChecker_CheckVaruse__eq_nonlist(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/";)
-       mklines := t.SetUpFileMkLines("options.mk",
-               MkCvsID,
-               "WRKSRC=\t\t${WRKDIR:=/subdir}",
-               "MASTER_SITES=\t${MASTER_SITE_GITHUB:=organization/}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: ~/options.mk:2: The :from=to modifier should only be used with lists, not with WRKDIR.")
-}
-
-func (s *Suite) Test_MkLineChecker_CheckVaruse__for(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/";)
-       mklines := t.SetUpFileMkLines("options.mk",
-               MkCvsID,
-               ".for var in a b c",
-               "\t: ${var}",
-               ".endfor")
-
-       mklines.Check()
-
-       t.CheckOutputEmpty()
-}
-
-// When a parameterized variable is defined in the pkgsrc infrastructure,
-// it does not generate a warning about being "used but not defined".
-// Even if the variable parameter differs, like .Linux and .SunOS in this
-// case. This pattern is typical for pkgsrc, therefore pkglint doesn't
-// check that the variable names match exactly.
-func (s *Suite) Test_MkLineChecker_CheckVaruse__varcanon(c *check.C) {
-       t := s.Init(c)
-       b := NewMkTokenBuilder()
-
-       t.SetUpPkgsrc()
-       t.CreateFileLines("mk/sys-vars.mk",
-               MkCvsID,
-               "CPPPATH.Linux=\t/usr/bin/cpp")
-       t.FinishSetUp()
-
-       mklines := t.NewMkLines("module.mk",
-               MkCvsID,
-               "COMMENT=\t${CPPPATH.SunOS}")
-
-       ck := MkLineChecker{mklines, mklines.mklines[1]}
-
-       ck.CheckVaruse(b.VarUse("CPPPATH.SunOS"), &VarUseContext{
-               vartype: &Vartype{
-                       basicType:  BtPathname,
-                       options:    Guessed,
-                       aclEntries: nil,
-               },
-               time:       VucRunTime,
-               quoting:    VucQuotPlain,
-               IsWordPart: false,
-       })
-
-       t.CheckOutputEmpty()
-}
-
-// Any variable that is defined in the pkgsrc infrastructure in mk/**/*.mk is
-// considered defined, and no "used but not defined" warning is logged for it.
-//
-// See Pkgsrc.loadUntypedVars.
-func (s *Suite) Test_MkLineChecker_CheckVaruse__defined_in_infrastructure(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPkgsrc()
-       t.CreateFileLines("mk/deeply/nested/infra.mk",
-               MkCvsID,
-               "INFRA_VAR?=\tvalue")
-       t.FinishSetUp()
-       mklines := t.SetUpFileMkLines("category/package/module.mk",
-               MkCvsID,
-               "do-fetch:",
-               "\t: ${INFRA_VAR} ${UNDEFINED}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: ~/category/package/module.mk:3: UNDEFINED is used but not defined.")
-}
-
-func (s *Suite) Test_MkLineChecker_CheckVaruse__build_defs(c *check.C) {
-       t := s.Init(c)
-
-       // XXX: This paragraph should not be necessary since VARBASE and X11_TYPE
-       // are also defined in vardefs.go.
-       t.SetUpPkgsrc()
-       t.CreateFileLines("mk/defaults/mk.conf",
-               "VARBASE?= /usr/pkg/var")
-       t.FinishSetUp()
-
-       mklines := t.SetUpFileMkLines("options.mk",
-               MkCvsID,
-               "COMMENT=\t\t${VARBASE} ${X11_TYPE}",
-               "PKG_FAIL_REASON+=\t${VARBASE} ${X11_TYPE}",
-               "BUILD_DEFS+=\t\tX11_TYPE")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: ~/options.mk:2: The user-defined variable VARBASE is used but not added to BUILD_DEFS.",
-               "WARN: ~/options.mk:3: PKG_FAIL_REASON should only get one item per line.")
-}
-
-// The LOCALBASE variable may be defined and used in the infrastructure.
-// It is always equivalent to PREFIX and only exists for historic reasons.
-func (s *Suite) Test_MkLineChecker_CheckVaruse__LOCALBASE_in_infrastructure(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPkgsrc()
-       t.CreateFileLines("mk/infra.mk",
-               MkCvsID,
-               "LOCALBASE?=\t${PREFIX}",
-               "DEFAULT_PREFIX=\t${LOCALBASE}")
-       t.FinishSetUp()
-
-       G.Check(t.File("mk/infra.mk"))
-
-       // No warnings about LOCALBASE being used; the infrastructure files may
-       // do this. In packages though, LOCALBASE is deprecated.
-
-       // There is no warning about DEFAULT_PREFIX being "defined but not used"
-       // since Pkgsrc.loadUntypedVars calls Pkgsrc.vartypes.DefineType, which
-       // registers that variable globally.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_CheckVaruse__user_defined_variable_and_BUILD_DEFS(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPkgsrc()
-       t.CreateFileLines("mk/defaults/mk.conf",
-               "VARBASE?=\t${PREFIX}/var",
-               "PYTHON_VER?=\t36")
-       mklines := t.NewMkLines("file.mk",
-               MkCvsID,
-               "BUILD_DEFS+=\tPYTHON_VER",
-               "\t: ${VARBASE}",
-               "\t: ${VARBASE}",
-               "\t: ${PYTHON_VER}")
-       t.FinishSetUp()
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: file.mk:3: The user-defined variable VARBASE is used but not added to BUILD_DEFS.")
-}
-
-func (s *Suite) Test_MkLineChecker_CheckVaruse__deprecated_PKG_DEBUG(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       G.Pkgsrc.initDeprecatedVars()
-
-       mklines := t.NewMkLines("module.mk",
-               MkCvsID,
-               "\t${_PKG_SILENT}${_PKG_DEBUG} :")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: module.mk:2: Use of _PKG_SILENT and _PKG_DEBUG is deprecated. Use ${RUN} instead.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVaruseUndefined(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPkgsrc()
-       t.CreateFileLines("mk/infra.mk",
-               MkCvsID,
-               "#",
-               "# User-settable variables:",
-               "#",
-               "# DOCUMENTED",
-               "",
-               "ASSIGNED=\tassigned",
-               "#COMMENTED=\tcommented")
-       t.FinishSetUp()
-
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               "",
-               "do-build:",
-               "\t: ${ASSIGNED} ${COMMENTED} ${DOCUMENTED} ${UNKNOWN}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: filename.mk:4: UNKNOWN is used but not defined.")
-}
-
-// PR 46570, item "15. net/uucp/Makefile has a make loop"
-func (s *Suite) Test_MkLineChecker_checkVaruseUndefined__indirect_variables(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpTool("echo", "ECHO", AfterPrefsMk)
-       mklines := t.NewMkLines("net/uucp/Makefile",
-               MkCvsID,
-               "\techo ${UUCP_${var}}")
-
-       mklines.Check()
-
-       // No warning about UUCP_${var} being used but not defined.
-       //
-       // Normally, parameterized variables use a dot instead of an underscore as separator.
-       // This is one of the few other cases. Pkglint doesn't warn about dynamic variable
-       // names like UUCP_${var} or SITES_${distfile}.
-       //
-       // It does warn about simple variable names though, like ${var} in this example.
-       t.CheckOutputLines(
-               "WARN: net/uucp/Makefile:2: var is used but not defined.")
-}
-
-// Documented variables are declared as both defined and used since, as
-// of April 2019, pkglint doesn't yet interpret the "Package-settable
-// variables" comment.
-func (s *Suite) Test_MkLineChecker_checkVaruseUndefined__documented(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-
-       mklines := t.NewMkLines("interpreter.mk",
-               MkCvsID,
-               "#",
-               "# Package-settable variables:",
-               "#",
-               "# REPLACE_INTERP",
-               "#\tThe list of files whose interpreter will be corrected.",
-               "",
-               "REPLACE_INTERPRETER+=\tinterp",
-               "REPLACE.interp.old=\t.*/interp",
-               "REPLACE.interp.new=\t${PREFIX}/bin/interp",
-               "REPLACE_FILES.interp=\t${REPLACE_INTERP}")
-
-       mklines.Check()
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVaruseModifiersSuffix(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("file.mk",
-               MkCvsID,
-               "\t: ${HOMEPAGE:=subdir/:Q}", // wrong
-               "\t: ${BUILD_DIRS:=subdir/}", // correct
-               "\t: ${BIN_PROGRAMS:=.exe}")  // unknown since BIN_PROGRAMS doesn't have a type
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: file.mk:2: The :from=to modifier should only be used with lists, not with HOMEPAGE.",
-               "WARN: file.mk:4: BIN_PROGRAMS is used but not defined.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVaruseModifiersRange(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpCommandLine("--show-autofix", "--source")
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("mk/compiler/gcc.mk",
-               MkCvsID,
-               "CC:=\t${CC:C/^/_asdf_/1:M_asdf_*:S/^_asdf_//}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "NOTE: mk/compiler/gcc.mk:2: "+
-                       "The modifier \":C/^/_asdf_/1:M_asdf_*:S/^_asdf_//\" can be written as \":[1]\".",
-               "AUTOFIX: mk/compiler/gcc.mk:2: "+
-                       "Replacing \":C/^/_asdf_/1:M_asdf_*:S/^_asdf_//\" with \":[1]\".",
-               "-\tCC:=\t${CC:C/^/_asdf_/1:M_asdf_*:S/^_asdf_//}",
-               "+\tCC:=\t${CC:[1]}")
-
-       // Now go through all the "almost" cases, to reach full branch coverage.
-       mklines = t.NewMkLines("gcc.mk",
-               MkCvsID,
-               "\t: ${CC:M1:M2:M3}",
-               "\t: ${CC:C/^begin//:M2:M3}",                    // M1 pattern not exactly ^
-               "\t: ${CC:C/^/_asdf_/g:M2:M3}",                  // M1 options != "1"
-               "\t: ${CC:C/^/....../g:M2:M3}",                  // M1 replacement doesn't match \w+
-               "\t: ${CC:C/^/_asdf_/1:O:M3}",                   // M2 is not a match modifier
-               "\t: ${CC:C/^/_asdf_/1:N2:M3}",                  // M2 is :N instead of :M
-               "\t: ${CC:C/^/_asdf_/1:M_asdf_:M3}",             // M2 pattern is missing the * at the end
-               "\t: ${CC:C/^/_asdf_/1:Mother:M3}",              // M2 pattern differs from the M1 pattern
-               "\t: ${CC:C/^/_asdf_/1:M_asdf_*:M3}",            // M3 ist not a substitution modifier
-               "\t: ${CC:C/^/_asdf_/1:M_asdf_*:S,from,to,}",    // M3 pattern differs from the M1 pattern
-               "\t: ${CC:C/^/_asdf_/1:M_asdf_*:S,^_asdf_,to,}", // M3 replacement is not empty
-               "\t: ${CC:C/^/_asdf_/1:M_asdf_*:S,^_asdf_,,g}")  // M3 modifier has options
-
-       mklines.Check()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("options.mk",
-               MkCvsID,
-               "COMMENT=\t${GAMES_USER}",
-               "COMMENT:=\t${PKGBASE}",
-               "PYPKGPREFIX=\t${PKGBASE}")
-       G.Pkgsrc.loadDefaultBuildDefs()
-       G.Pkgsrc.UserDefinedVars.Define("GAMES_USER", mklines.mklines[0])
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: options.mk:3: PKGBASE should not be used at load time in any file.",
-               "WARN: options.mk:4: The variable PYPKGPREFIX should not be set in this file; "+
-                       "it would be ok in pyversion.mk only.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__explain(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpCommandLine("-Wall", "--explain")
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("options.mk",
-               MkCvsID,
-               "COMMENT=\t${GAMES_USER}",
-               "COMMENT:=\t${PKGBASE}",
-               "PYPKGPREFIX=\t${PKGBASE}")
-       G.Pkgsrc.loadDefaultBuildDefs()
-       G.Pkgsrc.UserDefinedVars.Define("GAMES_USER", mklines.mklines[0])
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: options.mk:3: PKGBASE 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 PKGBASE",
-               "\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.",
-               "",
-               "WARN: options.mk:4: The variable PYPKGPREFIX should not be set in this file; "+
-                       "it would be ok in pyversion.mk only.",
-               "",
-               "\tThe allowed actions for a variable are determined based on the file",
-               "\tname in which the variable is used or defined. The rules for",
-               "\tPYPKGPREFIX are:",
-               "",
-               "\t* in pyversion.mk, it may be set",
-               "\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.", "")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("options.mk",
-               MkCvsID,
-               "WRKSRC:=${.CURDIR}",
-               ".if ${PKG_SYSCONFDIR.gdm} != \"etc\"",
-               ".endif")
-
-       mklines.Check()
-
-       // Evaluating PKG_SYSCONFDIR.* at load time is probably ok,
-       // though pkglint cannot prove anything here.
-       //
-       // Evaluating .CURDIR at load time is definitely ok since it is defined from the beginning.
-       t.CheckOutputLines(
-               "NOTE: options.mk:2: This variable value should be aligned to column 17.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_in_condition(c *check.C) {
-       t := s.Init(c)
-
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtPathPattern, List,
-               "special:filename.mk: use-loadtime")
-       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtPathPattern, List,
-               "special:filename.mk: use")
-
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               ".if ${LOAD_TIME} && ${RUN_TIME}",
-               ".endif")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: filename.mk:2: RUN_TIME should not be used at load time in any file.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_in_for_loop(c *check.C) {
-       t := s.Init(c)
-
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtPathPattern, List,
-               "special:filename.mk: use-loadtime")
-       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtPathPattern, List,
-               "special:filename.mk: use")
-
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               ".for pattern in ${LOAD_TIME} ${RUN_TIME}",
-               ".endfor")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: filename.mk:2: RUN_TIME should not be used at load time in any file.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_guessed(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.SetUpTool("install", "", AtRunTime)
-       mklines := t.NewMkLines("install-docfiles.mk",
-               MkCvsID,
-               "DOCFILES=\ta b c",
-               "do-install:",
-               ".for f in ${DOCFILES}",
-               "\tinstall -c ${WRKSRC}/${f} ${DESTDIR}${PREFIX}/${f}",
-               ".endfor")
-
-       mklines.Check()
-
-       // No warning for using DOCFILES at compile-time. Since the variable
-       // name is not one of the predefined names from vardefs.go, the
-       // variable's type is guessed based on the name (see
-       // Pkgsrc.VariableType).
-       //
-       // These guessed variables are typically defined and used only in
-       // a single file, and in this context, mistakes are usually found
-       // quickly.
-       t.CheckOutputEmpty()
-}
-
-// Ensures that the warning "should not be evaluated at load time" is issued
-// only if using the variable at run time is allowed. If the latter were not
-// allowed, this warning would be confusing.
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_run_time(c *check.C) {
-       t := s.Init(c)
-
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtUnknown, NoVartypeOptions,
-               "*.mk: use, use-loadtime")
-       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtUnknown, NoVartypeOptions,
-               "*.mk: use")
-       G.Pkgsrc.vartypes.DefineParse("WRITE_ONLY", BtUnknown, NoVartypeOptions,
-               "*.mk: set")
-       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME_ELSEWHERE", BtUnknown, NoVartypeOptions,
-               "Makefile: use-loadtime",
-               "*.mk: set")
-       G.Pkgsrc.vartypes.DefineParse("RUN_TIME_ELSEWHERE", BtUnknown, NoVartypeOptions,
-               "Makefile: use",
-               "*.mk: set")
-
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               ".if ${LOAD_TIME} && ${RUN_TIME} && ${WRITE_ONLY}",
-               ".elif ${LOAD_TIME_ELSEWHERE} && ${RUN_TIME_ELSEWHERE}",
-               ".endif")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: filename.mk:2: RUN_TIME should not be used at load time in any file.",
-               "WARN: filename.mk:2: "+
-                       "WRITE_ONLY should not be used in any file; "+
-                       "it is a write-only variable.",
-               "WARN: filename.mk:3: "+
-                       "LOAD_TIME_ELSEWHERE should not be used at load time in this file; "+
-                       "it would be ok in Makefile, but not *.mk.",
-               "WARN: filename.mk:3: "+
-                       "RUN_TIME_ELSEWHERE should not be used at load time in any file.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__PKGREVISION(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("any.mk",
-               MkCvsID,
-               ".if defined(PKGREVISION)",
-               ".endif")
-
-       mklines.Check()
-
-       // Since PKGREVISION may only be set in the package Makefile directly,
-       // there is no other file that could be mentioned as "it would be ok in".
-       t.CheckOutputLines(
-               "WARN: any.mk:2: PKGREVISION should not be used in any file; it is a write-only variable.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__indirectly(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("file.mk",
-               MkCvsID,
-               "IGNORE_PKG.package=\t${ONLY_FOR_UNPRIVILEGED}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: file.mk:2: IGNORE_PKG.package should be set to YES or yes.",
-               "WARN: file.mk:2: ONLY_FOR_UNPRIVILEGED should not be used indirectly at load time (via IGNORE_PKG.package).")
-}
-
-// This test is only here for branch coverage.
-// It does not demonstrate anything useful.
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__indirectly_tool(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("file.mk",
-               MkCvsID,
-               "USE_TOOLS+=\t${PKGREVISION}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: file.mk:2: PKGREVISION should not be used in any file; it is a write-only variable.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__write_only_usable_in_other_file(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("buildlink3.mk",
-               MkCvsID,
-               "VAR=\t${VAR} ${AUTO_MKDIRS}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: buildlink3.mk:2: " +
-                       "AUTO_MKDIRS should not be used in this file; " +
-                       "it would be ok in Makefile, Makefile.* or *.mk, " +
-                       "but not buildlink3.mk or builtin.mk.")
-}
-
-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", BtFilename, NoVartypeOptions,
-               "*: set, use-loadtime")
-       mklines := t.NewMkLines("Makefile",
-               MkCvsID,
-               "VAR=\t${VAR}")
-
-       mklines.Check()
-
-       // Since the variable is usable at load time, pkglint assumes it is also
-       // usable at run time. This is not the case for VAR, but probably doesn't
-       // happen in practice anyway.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__assigned_to_infrastructure_variable(c *check.C) {
-       t := s.Init(c)
-
-       // This combination of BtUnknown and all permissions is typical for
-       // otherwise unknown variables from the pkgsrc infrastructure.
-       G.Pkgsrc.vartypes.Define("INFRA", BtUnknown, NoVartypeOptions,
-               NewACLEntry("*", aclpAll))
-       G.Pkgsrc.vartypes.DefineParse("VAR", BtUnknown, NoVartypeOptions,
-               "buildlink3.mk: none",
-               "*: use")
-       mklines := t.NewMkLines("buildlink3.mk",
-               MkCvsID,
-               "INFRA=\t${VAR}")
-
-       mklines.Check()
-
-       // Since INFRA is defined in the infrastructure and pkglint
-       // knows nothing else about this variable, it assumes that INFRA
-       // may be used at load time. This is done to prevent wrong warnings.
-       //
-       // This in turn has consequences when INFRA is used on the left-hand
-       // side of an assignment since pkglint assumes that the right-hand
-       // side may now be evaluated at load time.
-       //
-       // Therefore the check is skipped when such a variable appears at the
-       // left-hand side of an assignment.
-       //
-       // Even in this case involving an unknown infrastructure variable,
-       // it is possible to issue a warning since VAR should not be used at all,
-       // independent of any properties of INFRA.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__assigned_to_load_time(c *check.C) {
-       t := s.Init(c)
-
-       // LOAD_TIME may be used at load time in other.mk.
-       // Since VAR must not be used at load time at all, it would be dangerous
-       // 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", BtMessage, NoVartypeOptions,
-               "buildlink3.mk: set",
-               "*.mk: use-loadtime")
-       G.Pkgsrc.vartypes.DefineParse("VAR", BtUnknown, NoVartypeOptions,
-               "buildlink3.mk: none",
-               "*.mk: use")
-       mklines := t.NewMkLines("buildlink3.mk",
-               MkCvsID,
-               "LOAD_TIME=\t${VAR}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: buildlink3.mk:2: VAR should not be used indirectly " +
-                       "at load time (via LOAD_TIME).")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarusePermissions__multiple_times_per_file(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("buildlink3.mk",
-               MkCvsID,
-               "VAR=\t${VAR} ${AUTO_MKDIRS} ${AUTO_MKDIRS} ${PKGREVISION} ${PKGREVISION}",
-               "VAR=\t${VAR} ${AUTO_MKDIRS} ${AUTO_MKDIRS} ${PKGREVISION} ${PKGREVISION}")
-
-       mklines.Check()
-
-       // Since these warnings are valid for the whole file, duplicates are suppressed.
-       t.CheckOutputLines(
-               "WARN: buildlink3.mk:2: "+
-                       "AUTO_MKDIRS should not be used in this file; "+
-                       "it would be ok in Makefile, Makefile.* or *.mk, "+
-                       "but not buildlink3.mk or builtin.mk.",
-               "WARN: buildlink3.mk:2: "+
-                       "PKGREVISION should not be used in any file; "+
-                       "it is a write-only variable.")
-}
-
-func (s *Suite) Test_MkLineChecker_warnVarusePermissions__not_directly_and_no_alternative_files(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("mk-c.mk",
-               MkCvsID,
-               "",
-               "# GUESSED_FLAGS",
-               "#\tDocumented here to suppress the \"defined but not used\"",
-               "#\twarning.",
-               "",
-               "TOOL_DEPENDS+=\t${BUILDLINK_API_DEPENDS.mk-c}:${BUILDLINK_PKGSRCDIR.mk-c}",
-               "GUESSED_FLAGS+=\t${BUILDLINK_CPPFLAGS}")
-
-       mklines.Check()
-
-       toolDependsType := G.Pkgsrc.VariableType(nil, "TOOL_DEPENDS")
-       t.CheckEquals(toolDependsType.String(), "DependencyWithPath (list, package-settable)")
-       t.CheckEquals(toolDependsType.AlternativeFiles(aclpAppend), "Makefile, Makefile.* or *.mk")
-       t.CheckEquals(toolDependsType.AlternativeFiles(aclpUse), "Makefile, Makefile.* or *.mk")
-       t.CheckEquals(toolDependsType.AlternativeFiles(aclpUseLoadtime), "")
-
-       apiDependsType := G.Pkgsrc.VariableType(nil, "BUILDLINK_API_DEPENDS.*")
-       t.CheckEquals(apiDependsType.String(), "Dependency (list, package-settable)")
-       t.CheckEquals(apiDependsType.AlternativeFiles(aclpUse), "")
-       t.CheckEquals(apiDependsType.AlternativeFiles(aclpUseLoadtime), "buildlink3.mk or builtin.mk only")
-
-       t.CheckOutputLines(
-               "WARN: mk-c.mk:7: BUILDLINK_API_DEPENDS.mk-c should not be used in any file.",
-               "WARN: mk-c.mk:7: The list variable BUILDLINK_API_DEPENDS.mk-c should not be embedded in a word.",
-               "WARN: mk-c.mk:7: BUILDLINK_PKGSRCDIR.mk-c should not be used in any file.")
-}
-
-func (s *Suite) Test_MkLineChecker_warnVaruseToolLoadTime(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.SetUpTool("nowhere", "NOWHERE", Nowhere)
-       t.SetUpTool("after-prefs", "AFTER_PREFS", AfterPrefsMk)
-       t.SetUpTool("at-runtime", "AT_RUNTIME", AtRunTime)
-       mklines := t.NewMkLines("Makefile",
-               MkCvsID,
-               ".if ${NOWHERE} && ${AFTER_PREFS} && ${AT_RUNTIME} && ${MK_TOOL}",
-               ".endif",
-               "",
-               "TOOLS_CREATE+=\t\tmk-tool",
-               "_TOOLS_VARNAME.mk-tool=\tMK_TOOL")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: Makefile:2: To use the tool ${NOWHERE} at load time, "+
-                       "it has to be added to USE_TOOLS before including bsd.prefs.mk.",
-               "WARN: Makefile:2: To use the tool ${AFTER_PREFS} at load time, "+
-                       "bsd.prefs.mk has to be included before.",
-               "WARN: Makefile:2: The tool ${AT_RUNTIME} cannot be used at load time.",
-               "WARN: Makefile:2: To use the tool ${MK_TOOL} at load time, "+
-                       "bsd.prefs.mk has to be included before.",
-               "WARN: Makefile:6: Variable names starting with an underscore "+
-                       "(_TOOLS_VARNAME.mk-tool) are reserved for internal pkgsrc use.",
-               "WARN: Makefile:6: _TOOLS_VARNAME.mk-tool is defined but not used.")
-}
-
-// This somewhat unrealistic case demonstrates how there can be a tool in a
-// Makefile that is not known to the global pkgsrc.
-//
-// This test covers the "pkgsrcTool != nil" condition.
-func (s *Suite) Test_MkLineChecker_warnVaruseToolLoadTime__local_tool(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.CreateFileLines("mk/bsd.prefs.mk")
-       mklines := t.SetUpFileMkLines("category/package/Makefile",
-               MkCvsID,
-               ".include \"../../mk/bsd.prefs.mk\"",
-               "",
-               "TOOLS_CREATE+=\t\tmk-tool",
-               "_TOOLS_VARNAME.mk-tool=\tMK_TOOL",
-               "",
-               "TOOL_OUTPUT!=\t${MK_TOOL}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: ~/category/package/Makefile:5: Variable names starting with an underscore (_TOOLS_VARNAME.mk-tool) are reserved for internal pkgsrc use.",
-               "WARN: ~/category/package/Makefile:5: _TOOLS_VARNAME.mk-tool is defined but not used.",
-               "WARN: ~/category/package/Makefile:7: TOOL_OUTPUT is defined but not used.",
-               "WARN: ~/category/package/Makefile:7: The tool ${MK_TOOL} cannot be used at load time.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarUseQuoting(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.SetUpFileMkLines("options.mk",
-               MkCvsID,
-               "GOPATH=\t${WRKDIR}",
-               "",
-               "CONFIGURE_ENV+=\tNAME=${R_PKGNAME} VER=${R_PKGVER}",
-               "",
-               "do-build:",
-               "\tcd ${WRKSRC} && GOPATH=${GOPATH} PATH=${PATH} :")
-
-       mklines.Check()
-
-       // For WRKSRC and GOPATH, no quoting is necessary since pkgsrc directories by
-       // definition don't contain special characters. Therefore they don't need the
-       // :Q, not even when used as part of a shell word.
-
-       // For PATH, the quoting is necessary because it may contain directories outside
-       // of pkgsrc, and these may contain special characters.
-
-       t.CheckOutputLines(
-               "WARN: ~/options.mk:7: The variable PATH should be quoted as part of a shell word.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__mstar(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.SetUpFileMkLines("options.mk",
-               MkCvsID,
-               "CONFIGURE_ARGS+=\tCFLAGS=${CFLAGS:Q}",
-               "CONFIGURE_ARGS+=\tCFLAGS=${CFLAGS:M*:Q}",
-               "CONFIGURE_ARGS+=\tADA_FLAGS=${ADA_FLAGS:Q}",
-               "CONFIGURE_ARGS+=\tADA_FLAGS=${ADA_FLAGS:M*:Q}",
-               "CONFIGURE_ENV+=\t\tCFLAGS=${CFLAGS:Q}",
-               "CONFIGURE_ENV+=\t\tCFLAGS=${CFLAGS:M*:Q}",
-               "CONFIGURE_ENV+=\t\tADA_FLAGS=${ADA_FLAGS:Q}",
-               "CONFIGURE_ENV+=\t\tADA_FLAGS=${ADA_FLAGS:M*:Q}")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "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) {
-       t := s.Init(c)
-
-       pkg := t.SetUpPackage("category/package",
-               "MAKE_FLAGS+=\tCFLAGS=${CFLAGS:M*:Q}",
-               "MAKE_FLAGS+=\tLFLAGS=${LDFLAGS:M*:Q}")
-       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)
-
-       t.CheckOutputLines(
-               "NOTE: ~/category/package/Makefile:20: The :M* modifier is not needed here.",
-               "NOTE: ~/category/package/Makefile:21: The :M* modifier is not needed here.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__q_not_needed(c *check.C) {
-       t := s.Init(c)
-
-       pkg := t.SetUpPackage("category/package",
-               "MASTER_SITES=\t${HOMEPAGE:Q}")
-       t.FinishSetUp()
-
-       G.Check(pkg)
-
-       t.CheckOutputLines(
-               "NOTE: ~/category/package/Makefile:6: The :Q modifier isn't necessary for ${HOMEPAGE} here.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__undefined_list_in_word_in_shell_command(c *check.C) {
-       t := s.Init(c)
-
-       pkg := t.SetUpPackage("category/package",
-               "\t${ECHO} ./${DISTFILES}")
-       t.FinishSetUp()
-
-       G.Check(pkg)
-
-       // The variable DISTFILES is declared by the infrastructure.
-       // It is not defined by this package, therefore it doesn't
-       // appear in the RedundantScope.
-       t.CheckOutputLines(
-               "WARN: ~/category/package/Makefile:20: The list variable DISTFILES should not be embedded in a word.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__list_variable_with_single_constant_value(c *check.C) {
-       t := s.Init(c)
-
-       pkg := t.SetUpPackage("category/package",
-               "BUILD_DIRS=\tonly-dir",
-               "",
-               "do-install:",
-               "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/")
-       t.FinishSetUp()
-
-       G.Check(pkg)
-
-       // Don't warn here since BUILD_DIRS, although being a list
-       // variable, contains only a single value.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__list_variable_with_single_conditional_value(c *check.C) {
-       t := s.Init(c)
-
-       pkg := t.SetUpPackage("category/package",
-               "BUILD_DIRS=\tonly-dir",
-               ".if 0",
-               "BUILD_DIRS=\tother-dir",
-               ".endif",
-               "",
-               "do-install:",
-               "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/")
-       t.FinishSetUp()
-
-       G.Check(pkg)
-
-       // TODO: Don't warn here since BUILD_DIRS, although being a list
-       //  variable, contains only a single value.
-       t.CheckOutputLines(
-               "WARN: ~/category/package/Makefile:26: " +
-                       "The list variable BUILD_DIRS should not be embedded in a word.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__list_variable_with_two_constant_words(c *check.C) {
-       t := s.Init(c)
-
-       pkg := t.SetUpPackage("category/package",
-               "BUILD_DIRS=\tfirst-dir second-dir",
-               "",
-               "do-install:",
-               "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/")
-       t.FinishSetUp()
-
-       G.Check(pkg)
-
-       // Since BUILD_DIRS consists of two words, it would destroy the installation command.
-       t.CheckOutputLines(
-               "WARN: ~/category/package/Makefile:23: " +
-                       "The list variable BUILD_DIRS should not be embedded in a word.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignOpShell(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpTool("uname", "UNAME", AfterPrefsMk)
-       t.SetUpTool("echo", "", AtRunTime)
-       t.SetUpPkgsrc()
-       t.SetUpPackage("category/package",
-               ".include \"standalone.mk\"")
-       t.CreateFileLines("category/package/standalone.mk",
-               MkCvsID,
-               "",
-               ".include \"../../mk/bsd.prefs.mk\"",
-               "",
-               "OPSYS_NAME!=\t${UNAME}",
-               ".if ${OPSYS_NAME} == \"NetBSD\"",
-               ".endif",
-               "",
-               "OS_NAME!=\t${UNAME}",
-               "",
-               "MUST_BE_EARLY!=\techo 123 # must be evaluated early",
-               "",
-               "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.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"))
-
-       // There is no warning for OPSYS_NAME since that variable is used at
-       // load time. In such a case the command has to be executed anyway,
-       // and executing it exactly once is the best thing to do.
-       //
-       // There is no warning for MUST_BE_EARLY since the comment provides the
-       // reason that this command really has to be executed at load time.
-       t.CheckOutputLines(
-               "NOTE: ~/category/package/standalone.mk:9: Consider the :sh modifier instead of != for \"${UNAME}\".",
-               "",
-               "\tFor variable assignments using the != operator, the shell command is",
-               "\trun every time the file is parsed. In some cases this is too early,",
-               "\tand the command may not yet be installed. In other cases the command",
-               "\tis executed more often than necessary. Most commands don't need to",
-               "\tbe executed for \"make clean\", for example.",
-               "",
-               "\tThe :sh modifier defers execution until the variable value is",
-               "\tactually needed. On the other hand, this means the command is",
-               "\texecuted each time the variable is evaluated.",
-               "",
-               "\tExample:",
-               "",
-               "\t\tEARLY_YEAR!=    date +%Y",
-               "",
-               "\t\tLATE_YEAR_CMD=  date +%Y",
-               "\t\tLATE_YEAR=      ${LATE_YEAR_CMD:sh}",
-               "",
-               "\t\t# or, in a single line:",
-               "\t\tLATE_YEAR=      ${date +%Y:L:sh}",
-               "",
-               "\tTo suppress this note, provide an explanation in a comment at the",
-               "\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_checkText(c *check.C) {
        t := s.Init(c)
 
@@ -1723,374 +194,117 @@ func (s *Suite) Test_MkLineChecker_check
 
        mklines := t.NewMkLines("Makefile",
                MkCvsID,
-               "COMMENT=\tA nice package")
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: Makefile:2: COMMENT should not begin with \"A\".")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVartype(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               "DISTNAME=\tgcc-${GCC_VERSION}")
-
-       mklines.vars.Define("GCC_VERSION", mklines.mklines[1])
-       mklines.Check()
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVartype__append_to_non_list(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               "DISTNAME+=\tsuffix",
-               "COMMENT=\tComment for",
-               "COMMENT+=\tthe package")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: filename.mk:2: The variable DISTNAME should not be appended to "+
-                       "(only set, or given a default value) in this file.",
-               "WARN: filename.mk:2: The \"+=\" operator should only be used with lists, not with DISTNAME.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVartype__no_tracing(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               "UNKNOWN=\tvalue",
-               "CUR_DIR!=\tpwd")
-       t.DisableTracing()
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: filename.mk:2: UNKNOWN is defined but not used.",
-               "WARN: filename.mk:3: CUR_DIR is defined but not used.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVartype__one_per_line(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               "PKG_FAIL_REASON+=\tSeveral words are wrong.",
-               "PKG_FAIL_REASON+=\t\"Properly quoted\"",
-               "PKG_FAIL_REASON+=\t# none")
-       t.DisableTracing()
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: filename.mk:2: PKG_FAIL_REASON should only get one item per line.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVartype__CFLAGS_with_backticks(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("chat/pidgin-icb/Makefile",
-               MkCvsID,
-               "CFLAGS+=\t`pkg-config pidgin --cflags`")
-       mkline := mklines.mklines[1]
-
-       words := mkline.Fields()
-
-       // bmake handles backticks in the same way, treating them as ordinary characters
-       t.CheckDeepEquals(words, []string{"`pkg-config", "pidgin", "--cflags`"})
-
-       ck := MkLineChecker{mklines, mklines.mklines[1]}
-       ck.checkVartype("CFLAGS", opAssignAppend, "`pkg-config pidgin --cflags`", "")
-
-       // No warning about "`pkg-config" being an unknown CFlag.
-       // As of September 2019, there is no such check anymore in pkglint.
-       t.CheckOutputEmpty()
-}
-
-// See PR 46570, Ctrl+F "4. Shell quoting".
-// Pkglint is correct, since the shell sees this definition for
-// CPPFLAGS as three words, not one word.
-func (s *Suite) Test_MkLineChecker_checkVartype__CFLAGS(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("Makefile",
-               MkCvsID,
-               "CPPFLAGS.SunOS+=\t-DPIPECOMMAND=\\\"/usr/sbin/sendmail -bs %s\\\"")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "WARN: Makefile:2: Compiler flag \"-DPIPECOMMAND=\\\\\\\"/usr/sbin/sendmail\" has unbalanced double quotes.",
-               "WARN: Makefile:2: Compiler flag \"%s\\\\\\\"\" has unbalanced double quotes.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__none(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES=\t# none")
-       t.FinishSetUp()
-
-       G.Check(t.File("obscure/package"))
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__indirect(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES=\t${PKGPATH:C,/.*,,}")
-       t.FinishSetUp()
-
-       G.Check(t.File("obscure/package"))
-
-       // This case does not occur in practice,
-       // therefore it's ok to have these warnings.
-       t.CheckOutputLines(
-               "WARN: ~/obscure/package/Makefile:5: "+
-                       "The primary category should be \"obscure\", not \"${PKGPATH:C,/.*,,}\".",
-               "ERROR: ~/obscure/package/Makefile:5: "+
-                       "Invalid category \"${PKGPATH:C,/.*,,}\".")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__wrong(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES=\tperl5")
-       t.FinishSetUp()
-
-       G.Check(t.File("obscure/package"))
-
-       t.CheckOutputLines(
-               "WARN: ~/obscure/package/Makefile:5: The primary category should be \"obscure\", not \"perl5\".")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__wrong_in_package_directory(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES=\tperl5")
-       t.FinishSetUp()
-       t.Chdir("obscure/package")
-
-       G.Check(".")
-
-       t.CheckOutputLines(
-               "WARN: Makefile:5: The primary category should be \"obscure\", not \"perl5\".")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__append(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES+=\tperl5")
-       t.FinishSetUp()
-
-       G.Check(t.File("obscure/package"))
-
-       // Appending is ok.
-       // In this particular case, appending has the same effect as assigning,
-       // but that can be checked somewhere else.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__default(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES?=\tperl5")
-       t.FinishSetUp()
-
-       G.Check(t.File("obscure/package"))
-
-       // Default assignments set the primary category, just like simple assignments.
-       t.CheckOutputLines(
-               "WARN: ~/obscure/package/Makefile:5: The primary category should be \"obscure\", not \"perl5\".")
-}
-
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__autofix(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpCommandLine("-Wall", "--autofix")
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES=\tperl5 obscure python")
-       t.FinishSetUp()
-
-       G.Check(t.File("obscure/package"))
+               "COMMENT=\tA nice package")
+       mklines.Check()
 
        t.CheckOutputLines(
-               "AUTOFIX: ~/obscure/package/Makefile:5: " +
-                       "Replacing \"perl5 obscure\" with \"obscure perl5\".")
+               "WARN: Makefile:2: COMMENT should not begin with \"A\".")
 }
 
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__third(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES=\tperl5 python obscure")
-       t.FinishSetUp()
-
-       G.Check(t.File("obscure/package"))
-
-       t.CheckOutputLines(
-               "WARN: ~/obscure/package/Makefile:5: " +
-                       "The primary category should be \"obscure\", not \"perl5\".")
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "DISTNAME=\tgcc-${GCC_VERSION}")
 
-       t.SetUpCommandLine("-Wall", "--show-autofix")
+       mklines.vars.Define("GCC_VERSION", mklines.mklines[1])
+       mklines.Check()
 
-       G.Check(t.File("obscure/package"))
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLineChecker_checkVarassignRightCategory__other_file(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype__append_to_non_list(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpPackage("obscure/package",
-               "CATEGORIES=\tperl5 obscure python")
-       mklines := t.SetUpFileMkLines("obscure/package/module.mk",
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
                MkCvsID,
-               "",
-               "CATEGORIES=\tperl5")
-       t.FinishSetUp()
+               "DISTNAME+=\tsuffix",
+               "COMMENT=\tComment for",
+               "COMMENT+=\tthe package")
 
        mklines.Check()
 
-       // It doesn't matter in which file the CATEGORIES= line appears.
-       // If it's a plain assignment, it will end up as the primary category.
        t.CheckOutputLines(
-               "WARN: ~/obscure/package/module.mk:3: " +
-                       "The primary category should be \"obscure\", not \"perl5\".")
+               "WARN: filename.mk:2: The variable DISTNAME should not be appended to "+
+                       "(only set, or given a default value) in this file.",
+               "WARN: filename.mk:2: The \"+=\" operator should only be used with lists, not with DISTNAME.")
 }
 
-func (s *Suite) Test_MkLineChecker_checkVarassignMisc(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype__no_tracing(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpPkgsrc()
-       t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://download.github.com/";)
-
-       mklines := t.SetUpFileMkLines("module.mk",
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
                MkCvsID,
-               "EGDIR=\t\t\t${PREFIX}/etc/rc.d",
-               "RPMIGNOREPATH+=\t\t${PREFIX}/etc/rc.d",
-               "_TOOLS_VARNAME.sed=\tSED",
-               "DIST_SUBDIR=\t\t${PKGNAME}",
-               "WRKSRC=\t\t\t${PKGNAME}",
-               "SITES_distfile.tar.gz=\t${MASTER_SITE_GITHUB:=user/}",
-               "MASTER_SITES=\t\thttps://cdn.example.org/${PKGNAME}/";,
-               "MASTER_SITES=\t\thttps://cdn.example.org/distname-${PKGVERSION}/";)
-       t.FinishSetUp()
+               "UNKNOWN=\tvalue",
+               "CUR_DIR!=\tpwd")
+       t.DisableTracing()
 
        mklines.Check()
 
-       // TODO: Split this test into several, one for each topic.
        t.CheckOutputLines(
-               "WARN: ~/module.mk:2: Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.",
-               "WARN: ~/module.mk:4: Variable names starting with an underscore (_TOOLS_VARNAME.sed) are reserved for internal pkgsrc use.",
-               "WARN: ~/module.mk:4: _TOOLS_VARNAME.sed is defined but not used.",
-               "WARN: ~/module.mk:5: PKGNAME should not be used in DIST_SUBDIR as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
-               "WARN: ~/module.mk:6: PKGNAME should not be used in WRKSRC as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
-               "WARN: ~/module.mk:7: SITES_distfile.tar.gz is defined but not used.",
-               "WARN: ~/module.mk:7: SITES_* is deprecated. Please use SITES.* instead.",
-               "WARN: ~/module.mk:8: PKGNAME should not be used in MASTER_SITES as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
-               "WARN: ~/module.mk:9: PKGVERSION should not be used in MASTER_SITES as it includes the PKGREVISION. Please use PKGVERSION_NOREV instead.")
+               "WARN: filename.mk:2: UNKNOWN is defined but not used.",
+               "WARN: filename.mk:3: CUR_DIR is defined but not used.")
 }
 
-func (s *Suite) Test_MkLineChecker_checkVarassignMisc__multiple_inclusion_guards(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype__one_per_line(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpPkgsrc()
-       t.CreateFileLines("filename.mk",
-               MkCvsID,
-               ".if !defined(FILENAME_MK)",
-               "FILENAME_MK=\t# defined",
-               ".endif")
-       t.CreateFileLines("Makefile.common",
-               MkCvsID,
-               ".if !defined(MAKEFILE_COMMON)",
-               "MAKEFILE_COMMON=\t# defined",
-               "",
-               ".endif")
-       t.CreateFileLines("other.mk",
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
                MkCvsID,
-               "COMMENT=\t# defined")
-       t.FinishSetUp()
+               "PKG_FAIL_REASON+=\tSeveral words are wrong.",
+               "PKG_FAIL_REASON+=\t\"Properly quoted\"",
+               "PKG_FAIL_REASON+=\t# none")
+       t.DisableTracing()
 
-       G.Check(t.File("filename.mk"))
-       G.Check(t.File("Makefile.common"))
-       G.Check(t.File("other.mk"))
+       mklines.Check()
 
-       // For multiple-inclusion guards, the meaning of the variable value
-       // is clear, therefore they are exempted from the warnings.
        t.CheckOutputLines(
-               "NOTE: ~/other.mk:2: Please use \"# empty\", \"# none\" or \"# yes\" " +
-                       "instead of \"# defined\".")
+               "WARN: filename.mk:2: PKG_FAIL_REASON should only get one item per line.")
 }
 
-func (s *Suite) Test_MkLineChecker_checkVarassignDecreasingVersions(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype__CFLAGS_with_backticks(c *check.C) {
        t := s.Init(c)
 
        t.SetUpVartypes()
-       mklines := t.NewMkLines("Makefile",
+       mklines := t.NewMkLines("chat/pidgin-icb/Makefile",
                MkCvsID,
-               "PYTHON_VERSIONS_ACCEPTED=\t36 __future__ # rationale",
-               "PYTHON_VERSIONS_ACCEPTED=\t36 -13 # rationale",
-               "PYTHON_VERSIONS_ACCEPTED=\t36 ${PKGVERSION_NOREV} # rationale",
-               "PYTHON_VERSIONS_ACCEPTED=\t36 37 # rationale",
-               "PYTHON_VERSIONS_ACCEPTED=\t37 36 27 25 # rationale")
-
-       // TODO: All but the last of the above assignments should be flagged as
-       //  redundant by RedundantScope; as of March 2019, that check is only
-       //  implemented for package Makefiles, not for individual files.
+               "CFLAGS+=\t`pkg-config pidgin --cflags`")
+       mkline := mklines.mklines[1]
 
-       mklines.Check()
+       words := mkline.Fields()
+
+       // bmake handles backticks in the same way, treating them as ordinary characters
+       t.CheckDeepEquals(words, []string{"`pkg-config", "pidgin", "--cflags`"})
+
+       ck := MkLineChecker{mklines, mklines.mklines[1]}
+       ck.checkVartype("CFLAGS", opAssignAppend, "`pkg-config pidgin --cflags`", "")
 
-       // Half of these warnings are from VartypeCheck.Version, the
-       // other half are from checkVarassignDecreasingVersions.
-       // Strictly speaking some of them are redundant, but that would
-       // mean to reject only variable references in checkVarassignDecreasingVersions.
-       // This is probably ok.
-       // TODO: Fix this.
-       t.CheckOutputLines(
-               "WARN: Makefile:2: Invalid version number \"__future__\".",
-               "ERROR: Makefile:2: Value \"__future__\" for "+
-                       "PYTHON_VERSIONS_ACCEPTED must be a positive integer.",
-               "WARN: Makefile:3: Invalid version number \"-13\".",
-               "ERROR: Makefile:3: Value \"-13\" for "+
-                       "PYTHON_VERSIONS_ACCEPTED must be a positive integer.",
-               "ERROR: Makefile:4: Value \"${PKGVERSION_NOREV}\" for "+
-                       "PYTHON_VERSIONS_ACCEPTED must be a positive integer.",
-               "WARN: Makefile:5: The values for PYTHON_VERSIONS_ACCEPTED "+
-                       "should be in decreasing order (37 before 36).")
+       // No warning about "`pkg-config" being an unknown CFlag.
+       // As of September 2019, there is no such check anymore in pkglint.
+       t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLineChecker_checkVarassignRightVaruse(c *check.C) {
+// See PR 46570, Ctrl+F "4. Shell quoting".
+// Pkglint is correct, since the shell sees this definition for
+// CPPFLAGS as three words, not one word.
+func (s *Suite) Test_MkLineChecker_checkVartype__CFLAGS(c *check.C) {
        t := s.Init(c)
 
        t.SetUpVartypes()
-
-       mklines := t.NewMkLines("module.mk",
+       mklines := t.NewMkLines("Makefile",
                MkCvsID,
-               "PLIST_SUBST+=\tLOCALBASE=${LOCALBASE:Q}")
+               "CPPFLAGS.SunOS+=\t-DPIPECOMMAND=\\\"/usr/sbin/sendmail -bs %s\\\"")
 
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: module.mk:2: Please use PREFIX instead of LOCALBASE.",
-               "NOTE: module.mk:2: The :Q modifier isn't necessary for ${LOCALBASE} here.")
+               "WARN: Makefile:2: Compiler flag \"-DPIPECOMMAND=\\\\\\\"/usr/sbin/sendmail\" has unbalanced double quotes.",
+               "WARN: Makefile:2: Compiler flag \"%s\\\\\\\"\" has unbalanced double quotes.")
 }
 
 func (s *Suite) Test_MkLineChecker_checkShellCommand__indentation(c *check.C) {
@@ -2159,15 +373,15 @@ func (s *Suite) Test_MkLineChecker_check
 
        t.CheckOutputLines(
                "ERROR: ~/category/package/filename.mk:3: "+
-                       "../../pkgtools/x11-links/buildlink3.mk must not be included directly. "+
+                       "\"../../pkgtools/x11-links/buildlink3.mk\" must not be included directly. "+
                        "Include \"../../mk/x11.buildlink3.mk\" instead.",
                "ERROR: ~/category/package/filename.mk:4: "+
-                       "../../graphics/jpeg/buildlink3.mk must not be included directly. "+
+                       "\"../../graphics/jpeg/buildlink3.mk\" must not be included directly. "+
                        "Include \"../../mk/jpeg.buildlink3.mk\" instead.",
                "WARN: ~/category/package/filename.mk:5: "+
                        "Please write \"USE_TOOLS+= intltool\" instead of this line.",
                "ERROR: ~/category/package/filename.mk:6: "+
-                       "../../devel/intltool/builtin.mk must not be included directly. "+
+                       "\"../../devel/intltool/builtin.mk\" must not be included directly. "+
                        "Include \"../../devel/intltool/buildlink3.mk\" instead.")
 }
 
@@ -2235,7 +449,7 @@ func (s *Suite) Test_MkLineChecker_check
 
        t.CheckOutputLines(
                "ERROR: ~/category/package/Makefile:20: " +
-                       "../../category/package/builtin.mk must not be included directly. " +
+                       "\"../../category/package/builtin.mk\" must not be included directly. " +
                        "Include \"../../category/package/buildlink3.mk\" instead.")
 }
 
@@ -2271,7 +485,7 @@ func (s *Suite) Test_MkLineChecker_check
 
        t.CheckOutputLines(
                "ERROR: ~/category/package/Makefile:23: " +
-                       "../../category/package/builtin.mk must not be included directly. " +
+                       "\"../../category/package/builtin.mk\" must not be included directly. " +
                        "Include \"../../category/package/buildlink3.mk\" instead.")
 }
 
@@ -2481,7 +695,15 @@ func (s *Suite) Test_MkLineChecker_check
                "",
                ".for var in a b c",
                ".endfor",
-               ".undef var unrelated")
+               ".undef var unrelated",
+               "",
+               ".if 0",
+               ".  info Unsupported operating system",
+               ".  warning Unsupported operating system",
+               ".  error Unsupported operating system",
+               ".  export-env A",
+               ".  unexport A",
+               ".endif")
 
        mklines.Check()
 
@@ -2527,13 +749,18 @@ func (s *Suite) Test_MkLineChecker_check
                "ERROR: filename.mk:12: Invalid variable name \"${VAR}\".")
 }
 
+// TODO: Split into separate tests.
 func (s *Suite) Test_MkLineChecker_checkDirectiveEnd__ending_comments(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
        mklines := t.NewMkLines("opsys.mk",
                MkCvsID,
                "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".for i in 1 2 3 4 5",
                ".  if ${OPSYS} == NetBSD",
                ".    if ${MACHINE_ARCH} == x86_64",
@@ -2562,12 +789,13 @@ func (s *Suite) Test_MkLineChecker_check
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: opsys.mk:7: Comment \"MACHINE_ARCH\" does not match condition \"${OS_VERSION:M8.*}\".",
-               "WARN: opsys.mk:8: Comment \"OS_VERSION\" does not match condition \"${MACHINE_ARCH} == x86_64\".",
-               "WARN: opsys.mk:10: Comment \"j\" does not match loop \"i in 1 2 3 4 5\".",
-               "WARN: opsys.mk:12: Unknown option \"option\".",
-               "WARN: opsys.mk:20: Comment \"NetBSD\" does not match condition \"${OPSYS} == FreeBSD\".",
-               "WARN: opsys.mk:24: Comment \"ii\" does not match loop \"jj in 1 2\".")
+               // TODO: mention the line number of the corresponding condition.
+               "WARN: opsys.mk:9: Comment \"MACHINE_ARCH\" does not match condition \"${OS_VERSION:M8.*}\".",
+               "WARN: opsys.mk:10: Comment \"OS_VERSION\" does not match condition \"${MACHINE_ARCH} == x86_64\".",
+               "WARN: opsys.mk:12: Comment \"j\" does not match loop \"i in 1 2 3 4 5\".",
+               "WARN: opsys.mk:14: Unknown option \"option\".",
+               "WARN: opsys.mk:22: Comment \"NetBSD\" does not match condition \"${OPSYS} == FreeBSD\".",
+               "WARN: opsys.mk:26: Comment \"ii\" does not match loop \"jj in 1 2\".")
 }
 
 // After removing the dummy indentation in commit d5a926af,
@@ -2590,553 +818,6 @@ func (s *Suite) Test_MkLineChecker_check
                "ERROR: filename.mk:4: Unmatched .endif.")
 }
 
-func (s *Suite) Test_MkLineChecker_checkDirectiveCond(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-
-       test := func(cond string, output ...string) {
-               mklines := t.NewMkLines("filename.mk",
-                       cond)
-               mklines.ForEach(func(mkline *MkLine) {
-                       MkLineChecker{mklines, mkline}.checkDirectiveCond()
-               })
-               t.CheckOutput(output)
-       }
-
-       test(
-               ".if !empty(PKGSRC_COMPILER:Mmycc)",
-               "WARN: filename.mk:1: The pattern \"mycc\" cannot match any of "+
-                       "{ ccache ccc clang distcc f2c gcc hp icc ido "+
-                       "mipspro mipspro-ucode pcc sunpro xlc } for PKGSRC_COMPILER.")
-
-       test(
-               ".elif ${A} != ${B}",
-               "WARN: filename.mk:1: A is used but not defined.",
-               "WARN: filename.mk:1: B is used but not defined.")
-
-       test(".if ${HOMEPAGE} == \"mailto:someone%example.org@localhost\"";,
-               "WARN: filename.mk:1: \"mailto:someone%example.org@localhost\"; is not a valid URL.",
-               "WARN: filename.mk:1: HOMEPAGE should not be used at load time in any file.")
-
-       test(".if !empty(PKGSRC_RUN_TEST:M[Y][eE][sS])",
-               "WARN: filename.mk:1: PKGSRC_RUN_TEST should be matched "+
-                       "against \"[yY][eE][sS]\" or \"[nN][oO]\", not \"[Y][eE][sS]\".")
-
-       test(".if !empty(IS_BUILTIN.Xfixes:M[yY][eE][sS])")
-
-       test(".if !empty(${IS_BUILTIN.Xfixes:M[yY][eE][sS]})",
-               "WARN: filename.mk:1: The empty() function takes a variable name as parameter, "+
-                       "not a variable expression.")
-
-       test(".if ${PKGSRC_COMPILER} == \"msvc\"",
-               "WARN: filename.mk:1: \"msvc\" is not valid for PKGSRC_COMPILER. "+
-                       "Use one of { ccache ccc clang distcc f2c gcc hp icc ido mipspro mipspro-ucode pcc sunpro xlc } instead.",
-               "ERROR: filename.mk:1: Use ${PKGSRC_COMPILER:Mmsvc} instead of the == operator.")
-
-       test(".if ${PKG_LIBTOOL:Mlibtool}",
-               "NOTE: filename.mk:1: PKG_LIBTOOL should be compared using == instead of matching against \":Mlibtool\".",
-               "WARN: filename.mk:1: PKG_LIBTOOL should not be used at load time in any file.")
-
-       test(".if ${MACHINE_PLATFORM:MUnknownOS-*-*} || ${MACHINE_ARCH:Mx86}",
-               "WARN: filename.mk:1: "+
-                       "The pattern \"UnknownOS\" cannot match any of "+
-                       "{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
-                       "IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
-                       "} for the operating system part of MACHINE_PLATFORM.",
-               "WARN: filename.mk:1: "+
-                       "The pattern \"x86\" cannot match any of "+
-                       "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 cobalt coldfire convex dreamcast earm "+
-                       "earmeb earmhf earmhfeb earmv4 earmv4eb earmv5 earmv5eb earmv6 earmv6eb earmv6hf earmv6hfeb "+
-                       "earmv7 earmv7eb earmv7hf earmv7hfeb evbarm hpcmips hpcsh hppa hppa64 i386 i586 i686 ia64 "+
-                       "m68000 m68k m88k mips mips64 mips64eb mips64el mipseb mipsel mipsn32 mlrisc ns32k pc532 pmax "+
-                       "powerpc powerpc64 rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
-                       "} for MACHINE_ARCH.",
-               "NOTE: filename.mk:1: MACHINE_ARCH should be compared using == instead of matching against \":Mx86\".")
-
-       // Doesn't occur in practice since it is surprising that the ! applies
-       // to the comparison operator, and not to one of its arguments.
-       test(".if !${VAR} == value",
-               "WARN: filename.mk:1: VAR is used but not defined.")
-
-       // Doesn't occur in practice since this string can never be empty.
-       test(".if !\"${VAR}str\"",
-               "WARN: filename.mk:1: VAR is used but not defined.")
-
-       // Doesn't occur in practice since !${VAR} && !${VAR2} is more idiomatic.
-       test(".if !\"${VAR}${VAR2}\"",
-               "WARN: filename.mk:1: VAR is used but not defined.",
-               "WARN: filename.mk:1: VAR2 is used but not defined.")
-
-       // Just for code coverage; always evaluates to true.
-       test(".if \"string\"",
-               nil...)
-
-       // Code coverage for checkVar.
-       test(".if ${OPSYS} || ${MACHINE_ARCH}",
-               nil...)
-
-       test(".if ${VAR}",
-               "WARN: filename.mk:1: VAR is used but not defined.")
-
-       test(".if ${VAR} == 3",
-               "WARN: filename.mk:1: VAR is used but not defined.")
-
-       test(".if \"value\" == ${VAR}",
-               "WARN: filename.mk:1: VAR is used but not defined.")
-
-       test(".if ${MASTER_SITES:Mftp://*} == \"ftp://netbsd.org/\"";,
-               // FIXME: duplicate diagnostic, see MkParser.MkCond.
-               "WARN: filename.mk:1: Invalid variable modifier \"//*\" for \"MASTER_SITES\".",
-               "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.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkDirectiveCond__tracing(c *check.C) {
-       t := s.Init(c)
-
-       t.EnableTracingToLog()
-       mklines := t.NewMkLines("filename.mk",
-               ".if ${VAR:Mpattern1:Mpattern2} == comparison")
-
-       mklines.ForEach(func(mkline *MkLine) {
-               MkLineChecker{mklines, mkline}.checkDirectiveCond()
-       })
-
-       t.CheckOutputLinesMatching(`^WARN|checkCompare`,
-               "TRACE: 1   checkCompareVarStr ${VAR:Mpattern1:Mpattern2} == comparison",
-               "WARN: filename.mk:1: VAR is used but not defined.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparison_with_shell_command(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("security/openssl/Makefile",
-               MkCvsID,
-               ".if ${PKGSRC_COMPILER} == \"gcc\" && ${CC} == \"cc\"",
-               ".endif")
-
-       mklines.Check()
-
-       // Don't warn about unknown shell command "cc".
-       t.CheckOutputLines(
-               "ERROR: security/openssl/Makefile:2: Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.")
-}
-
-// The :N modifier filters unwanted values. After this filter, any variable value
-// may be compared with the empty string, regardless of the variable type.
-// Effectively, the :N modifier changes the type from T to Option(T).
-func (s *Suite) Test_MkLineChecker_checkDirectiveCond__compare_pattern_with_empty(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("filename.mk",
-               MkCvsID,
-               ".if ${X11BASE:Npattern} == \"\"",
-               ".endif",
-               "",
-               ".if ${X11BASE:N<>} == \"*\"",
-               ".endif",
-               "",
-               ".if !(${OPSYS:M*BSD} != \"\")",
-               ".endif")
-
-       mklines.Check()
-
-       // TODO: There should be a warning about "<>" containing invalid
-       //  characters for a path. See VartypeCheck.Pathname
-       t.CheckOutputLines(
-               "WARN: filename.mk:5: The pathname pattern \"<>\" contains the invalid characters \"<>\".",
-               "WARN: filename.mk:5: The pathname \"*\" contains the invalid character \"*\".")
-}
-
-func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparing_PKGSRC_COMPILER_with_eqeq(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("Makefile",
-               MkCvsID,
-               ".if ${PKGSRC_COMPILER} == \"clang\"",
-               ".elif ${PKGSRC_COMPILER} != \"gcc\"",
-               ".endif")
-
-       mklines.Check()
-
-       t.CheckOutputLines(
-               "ERROR: Makefile:2: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
-               "ERROR: Makefile:3: Use ${PKGSRC_COMPILER:Ngcc} instead of the != operator.")
-}
-
-func (s *Suite) Test_MkLineChecker_checkDirectiveCondEmpty(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.Chdir(".")
-
-       test := func(before string, diagnosticsAndAfter ...string) {
-
-               mklines := t.SetUpFileMkLines("module.mk",
-                       MkCvsID,
-                       before,
-                       ".endif")
-               ck := MkLineChecker{mklines, mklines.mklines[1]}
-
-               t.SetUpCommandLine("-Wall")
-               mklines.ForEach(func(mkline *MkLine) {
-                       if mkline == mklines.mklines[1] {
-                               ck.checkDirectiveCond()
-                       }
-               })
-
-               t.SetUpCommandLine("-Wall", "--autofix")
-               mklines.ForEach(func(mkline *MkLine) {
-                       if mkline == mklines.mklines[1] {
-                               ck.checkDirectiveCond()
-                       }
-               })
-
-               mklines.SaveAutofixChanges()
-               afterMklines := t.LoadMkInclude("module.mk")
-
-               if len(diagnosticsAndAfter) > 0 {
-                       diagLen := len(diagnosticsAndAfter)
-                       diagnostics := diagnosticsAndAfter[:diagLen-1]
-                       after := diagnosticsAndAfter[diagLen-1]
-
-                       t.CheckOutput(diagnostics)
-                       t.CheckEquals(afterMklines.mklines[1].Text, after)
-               } else {
-                       t.CheckOutputEmpty()
-               }
-       }
-
-       test(
-               ".if ${PKGPATH:Mpattern}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mpattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"${PKGPATH:Mpattern}\" with \"${PKGPATH} == pattern\".",
-
-               ".if ${PKGPATH} == pattern")
-
-       // When the pattern contains placeholders, it cannot be converted to == or !=.
-       test(
-               ".if ${PKGPATH:Mpa*n}",
-               nil...)
-
-       // The :tl modifier prevents the autofix.
-       test(
-               ".if ${PKGPATH:tl:Mpattern}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mpattern\".",
-
-               ".if ${PKGPATH:tl:Mpattern}")
-
-       test(
-               ".if ${PKGPATH:Ncategory/package}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using != instead of matching against \":Ncategory/package\".",
-               "AUTOFIX: module.mk:2: Replacing \"${PKGPATH:Ncategory/package}\" with \"${PKGPATH} != category/package\".",
-
-               ".if ${PKGPATH} != category/package")
-
-       // ${PKGPATH:None:Ntwo} is a short variant of ${PKGPATH} != "one" &&
-       // ${PKGPATH} != "two". Applying the transformation would make the
-       // condition longer than before, therefore nothing is done here.
-       test(
-               ".if ${PKGPATH:None:Ntwo}",
-               nil...)
-
-       // Note: this combination doesn't make sense since the patterns "one" and "two" don't overlap.
-       test(".if ${PKGPATH:Mone:Mtwo}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mone\".",
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mtwo\".",
-
-               ".if ${PKGPATH:Mone:Mtwo}")
-
-       test(".if !empty(PKGPATH:Mpattern)",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mpattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"!empty(PKGPATH:Mpattern)\" with \"${PKGPATH} == pattern\".",
-
-               ".if ${PKGPATH} == pattern")
-
-       test(".if empty(PKGPATH:Mpattern)",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using != instead of matching against \":Mpattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"empty(PKGPATH:Mpattern)\" with \"${PKGPATH} != pattern\".",
-
-               ".if ${PKGPATH} != pattern")
-
-       test(".if !!empty(PKGPATH:Mpattern)",
-
-               // TODO: When taking all the ! into account, this is actually a
-               //  test for emptiness, therefore the diagnostics should suggest
-               //  the != operator instead of ==.
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mpattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"!empty(PKGPATH:Mpattern)\" with \"${PKGPATH} == pattern\".",
-
-               // TODO: The ! and == could be combined into a !=.
-               //  Luckily the !! pattern doesn't occur in practice.
-               ".if !${PKGPATH} == pattern")
-
-       test(".if empty(PKGPATH:Mpattern) || 0",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using != instead of matching against \":Mpattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"empty(PKGPATH:Mpattern)\" with \"${PKGPATH} != pattern\".",
-
-               ".if ${PKGPATH} != pattern || 0")
-
-       // No note in this case since there is no implicit !empty around the varUse.
-       test(".if ${PKGPATH:Mpattern} != ${OTHER}",
-
-               "WARN: module.mk:2: OTHER is used but not defined.",
-
-               ".if ${PKGPATH:Mpattern} != ${OTHER}")
-
-       test(
-               ".if ${PKGPATH:Mpattern}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mpattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"${PKGPATH:Mpattern}\" with \"${PKGPATH} == pattern\".",
-
-               ".if ${PKGPATH} == pattern")
-
-       test(
-               ".if !${PKGPATH:Mpattern}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using != instead of matching against \":Mpattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"!${PKGPATH:Mpattern}\" with \"${PKGPATH} != pattern\".",
-
-               ".if ${PKGPATH} != pattern")
-
-       test(
-               ".if !!${PKGPATH:Mpattern}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using != instead of matching against \":Mpattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"!${PKGPATH:Mpattern}\" with \"${PKGPATH} != pattern\".",
-
-               ".if !${PKGPATH} != pattern")
-
-       // This pattern with spaces doesn't make sense at all in the :M
-       // modifier since it can never match.
-       // Or can it, if the PKGPATH contains quotes?
-       // How exactly does bmake apply the matching here, are both values unquoted?
-       test(
-               ".if ${PKGPATH:Mpattern with spaces}",
-
-               "WARN: module.mk:2: The pathname pattern \"pattern with spaces\" "+
-                       "contains the invalid characters \"  \".",
-
-               ".if ${PKGPATH:Mpattern with spaces}")
-       // TODO: ".if ${PKGPATH} == \"pattern with spaces\"")
-
-       test(
-               ".if ${PKGPATH:M'pattern with spaces'}",
-
-               "WARN: module.mk:2: The pathname pattern \"'pattern with spaces'\" "+
-                       "contains the invalid characters \"'  '\".",
-
-               ".if ${PKGPATH:M'pattern with spaces'}")
-       // TODO: ".if ${PKGPATH} == 'pattern with spaces'")
-
-       test(
-               ".if ${PKGPATH:M&&}",
-
-               "WARN: module.mk:2: The pathname pattern \"&&\" "+
-                       "contains the invalid characters \"&&\".",
-
-               ".if ${PKGPATH:M&&}")
-       // TODO: ".if ${PKGPATH} == '&&'")
-
-       // If PKGPATH is "", the condition is false.
-       // If PKGPATH is "negative-pattern", the condition is false.
-       // In all other cases, the condition is true.
-       //
-       // Therefore this condition cannot simply be transformed into
-       // ${PKGPATH} != negative-pattern, since that would produce a
-       // different result in the case where PKGPATH is empty.
-       //
-       // For system-provided variables that are guaranteed to be non-empty,
-       // such as OPSYS or PKGPATH, this replacement is valid.
-       // These variables are only guaranteed to be defined after bsd.prefs.mk
-       // has been included, like everywhere else.
-       test(
-               ".if ${PKGPATH:Nnegative-pattern}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using != instead of matching against \":Nnegative-pattern\".",
-               "AUTOFIX: module.mk:2: Replacing \"${PKGPATH:Nnegative-pattern}\" with \"${PKGPATH} != negative-pattern\".",
-
-               ".if ${PKGPATH} != negative-pattern")
-
-       // Since UNKNOWN is not a well-known system-provided variable that is
-       // guaranteed to be non-empty (see the previous example), it is not
-       // transformed at all.
-       test(
-               ".if ${UNKNOWN:Nnegative-pattern}",
-
-               "WARN: module.mk:2: UNKNOWN is used but not defined.",
-
-               ".if ${UNKNOWN:Nnegative-pattern}")
-
-       test(
-               ".if ${PKGPATH:Mpath1} || ${PKGPATH:Mpath2}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mpath1\".",
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mpath2\".",
-               "AUTOFIX: module.mk:2: Replacing \"${PKGPATH:Mpath1}\" with \"${PKGPATH} == path1\".",
-               "AUTOFIX: module.mk:2: Replacing \"${PKGPATH:Mpath2}\" with \"${PKGPATH} == path2\".",
-
-               ".if ${PKGPATH} == path1 || ${PKGPATH} == path2")
-
-       test(
-               ".if (((((${PKGPATH:Mpath})))))",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mpath\".",
-               "AUTOFIX: module.mk:2: Replacing \"${PKGPATH:Mpath}\" with \"${PKGPATH} == path\".",
-
-               ".if (((((${PKGPATH} == path)))))")
-
-       // Note: this combination doesn't make sense since the patterns "one" and "two" don't overlap.
-       test(
-               ".if ${PKGPATH:Mone:Mtwo}",
-
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mone\".",
-               "NOTE: module.mk:2: PKGPATH should be compared using == instead of matching against \":Mtwo\".",
-
-               ".if ${PKGPATH:Mone:Mtwo}")
-
-       test(
-               ".if ${MACHINE_ARCH:Mx86_64}",
-
-               "NOTE: module.mk:2: MACHINE_ARCH should be compared using == instead of matching against \":Mx86_64\".",
-               "AUTOFIX: module.mk:2: Replacing \"${MACHINE_ARCH:Mx86_64}\" with \"${MACHINE_ARCH} == x86_64\".",
-
-               ".if ${MACHINE_ARCH} == x86_64")
-}
-
-func (s *Suite) Test_MkLineChecker_checkDirectiveCondCompare(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-
-       test := func(cond string, output ...string) {
-               mklines := t.NewMkLines("filename.mk",
-                       cond)
-               mklines.ForEach(func(mkline *MkLine) {
-                       MkLineChecker{mklines, mkline}.checkDirectiveCond()
-               })
-               t.CheckOutput(output)
-       }
-
-       // As of July 2019, pkglint doesn't have specific checks for comparing
-       // variables to numbers.
-       test(".if ${VAR} > 0",
-               "WARN: filename.mk:1: VAR is used but not defined.")
-
-       // For string comparisons, the checks from vartypecheck.go are
-       // performed.
-       test(".if ${DISTNAME} == \"<>\"",
-               "WARN: filename.mk:1: The filename \"<>\" contains the invalid characters \"<>\".",
-               "WARN: filename.mk:1: DISTNAME should not be used at load time in any file.")
-
-       // This type of comparison doesn't occur in practice since it is
-       // overly verbose.
-       test(".if \"${BUILD_DIRS}str\" == \"str\"",
-               // TODO: why should it not be used? In a .for loop it sounds pretty normal.
-               "WARN: filename.mk:1: BUILD_DIRS should not be used at load time in any file.")
-
-       // This is a shorthand for defined(VAR), but it is not used in practice.
-       test(".if VAR",
-               "WARN: filename.mk:1: Invalid condition, unrecognized part: \"VAR\".")
-
-       // Calling a function with braces instead of parentheses is syntactically
-       // invalid. Pkglint is stricter than bmake in this situation.
-       //
-       // Bmake reads the "empty{VAR}" as a variable name. It then checks whether
-       // this variable is defined. It is not, of course, therefore the expression
-       // is false. The ! in front of it negates this false, which makes the whole
-       // condition true.
-       //
-       // See https://mail-index.netbsd.org/tech-pkg/2019/07/07/msg021539.html
-       test(".if !empty{VAR}",
-               "WARN: filename.mk:1: Invalid condition, unrecognized part: \"empty{VAR}\".")
-}
-
-func (s *Suite) Test_MkLineChecker_checkDirectiveCondCompareVarStr__no_tracing(c *check.C) {
-       t := s.Init(c)
-       b := NewMkTokenBuilder()
-
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("filename.mk",
-               ".if ${DISTFILES:Mpattern:O:u} == NetBSD")
-       t.DisableTracing()
-
-       ck := MkLineChecker{mklines, mklines.mklines[0]}
-       varUse := b.VarUse("DISTFILES", "Mpattern", "O", "u")
-       ck.checkDirectiveCondCompareVarStr(varUse, "==", "distfile-1.0.tar.gz")
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLineChecker_checkCompareVarStrCompiler(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpVartypes()
-       t.Chdir(".")
-
-       test := func(cond string, diagnostics ...string) {
-               mklines := t.SetUpFileMkLines("filename.mk",
-                       MkCvsID,
-                       "",
-                       ".if "+cond,
-                       ".endif")
-
-               t.SetUpCommandLine("-Wall")
-               mklines.Check()
-               t.SetUpCommandLine("-Wall", "--autofix")
-               mklines.Check()
-
-               t.CheckOutput(diagnostics)
-       }
-
-       test(
-               "${PKGSRC_COMPILER} == gcc",
-
-               "ERROR: filename.mk:3: "+
-                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.",
-               "AUTOFIX: filename.mk:3: "+
-                       "Replacing \"${PKGSRC_COMPILER} == gcc\" "+
-                       "with \"${PKGSRC_COMPILER:Mgcc}\".")
-
-       // No autofix because of missing whitespace.
-       // TODO: Provide the autofix regardless of the whitespace.
-       test(
-               "${PKGSRC_COMPILER}==gcc",
-
-               "ERROR: filename.mk:3: "+
-                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.")
-
-       // The comparison value can be with or without quotes.
-       test(
-               "${PKGSRC_COMPILER} == \"gcc\"",
-
-               "ERROR: filename.mk:3: "+
-                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.",
-               "AUTOFIX: filename.mk:3: "+
-                       "Replacing \"${PKGSRC_COMPILER} == \\\"gcc\\\"\" "+
-                       "with \"${PKGSRC_COMPILER:Mgcc}\".")
-
-       // No warning because it is not obvious what is meant here.
-       // This case probably doesn't occur in practice.
-       test(
-               "${PKGSRC_COMPILER} == \"distcc gcc\"",
-
-               nil...)
-}
-
 func (s *Suite) Test_MkLineChecker_checkDirectiveFor(c *check.C) {
        t := s.Init(c)
 
@@ -3162,8 +843,8 @@ func (s *Suite) Test_MkLineChecker_check
                // This warning is correct since PATH is separated by colons, not by spaces.
                "WARN: for.mk:5: Please use ${PATH:Q} instead of ${PATH}.",
 
-               // This warning is also correct since the :M modifier doesn't change the
-               // word boundaries.
+               // This warning is also correct since the :M modifier doesn't
+               // turn a list into a non-list or vice versa.
                "WARN: for.mk:8: Please use ${PATH:M*/bin:Q} instead of ${PATH:M*/bin}.")
 }
 
Index: pkgsrc/pkgtools/pkglint/files/shell.go
diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.51 pkgsrc/pkgtools/pkglint/files/shell.go:1.52
--- pkgsrc/pkgtools/pkglint/files/shell.go:1.51 Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/shell.go      Sun Dec  8 00:06:38 2019
@@ -211,7 +211,8 @@ func (scc *SimpleCommandChecker) checkAu
                        if m, dirname := match1(arg, `^(?:\$\{DESTDIR\})?\$\{PREFIX(?:|:Q)\}/(.*)`); m {
                                autoMkdirs := false
                                if G.Pkg != nil {
-                                       plistLine := G.Pkg.Plist.Dirs[NewPath(dirname)]
+                                       // FIXME: Add test for absolute path.
+                                       plistLine := G.Pkg.Plist.Dirs[NewRelPathString(dirname)]
                                        if plistLine != nil && !containsVarRef(plistLine.Text) {
                                                autoMkdirs = true
                                        }
@@ -317,10 +318,10 @@ func (scc *SimpleCommandChecker) Explain
        scc.mkline.Explain(explanation...)
 }
 
-// ShellLineChecker is either a line from a Makefile starting with a tab,
+// ShellLineChecker checks either a line from a Makefile starting with a tab,
 // thereby containing shell commands to be executed.
 //
-// Or it is a variable assignment line from a Makefile with a left-hand
+// Or it checks a variable assignment line from a Makefile with a left-hand
 // side variable that is of some shell-like type; see Vartype.IsShell.
 type ShellLineChecker struct {
        MkLines *MkLines
@@ -529,6 +530,8 @@ func (ck *ShellLineChecker) checkPipeExi
 }
 
 var shellCommandsType = NewVartype(BtShellCommands, NoVartypeOptions, NewACLEntry("*", aclpAllRuntime))
+
+// FIXME: Why is this called shell_Word_Vuc and not shell_Commands_Vuc?
 var shellWordVuc = &VarUseContext{shellCommandsType, VucUnknownTime, VucQuotPlain, false}
 
 func NewShellLineChecker(mklines *MkLines, mkline *MkLine) *ShellLineChecker {
@@ -588,7 +591,7 @@ func (ck *ShellLineChecker) CheckShellCo
        setE := lexer.SkipString("${RUN}")
        if !setE {
                if lexer.NextString("${_PKG_SILENT}${_PKG_DEBUG}") != "" {
-                       line.Warnf("Use of _PKG_SILENT and _PKG_DEBUG is deprecated. Use ${RUN} instead.")
+                       line.Errorf("Use of _PKG_SILENT and _PKG_DEBUG is obsolete. Use ${RUN} instead.")
                }
        }
 
@@ -707,7 +710,7 @@ func (ck *ShellLineChecker) CheckWord(to
        // to the MkLineChecker. Examples for these are ${VAR:Mpattern} or $@.
        if varuse := ToVarUse(token); varuse != nil {
                if ck.checkVarUse {
-                       MkLineChecker{ck.MkLines, ck.mkline}.CheckVaruse(varuse, shellWordVuc)
+                       NewMkVarUseChecker(varuse, ck.MkLines, ck.mkline).Check(shellWordVuc)
                }
                return
        }
@@ -782,7 +785,7 @@ outer:
 
        if trimHspace(tok.Rest()) != "" {
                ck.Warnf("Internal pkglint error in ShellLine.CheckWord at %q (quoting=%s), rest: %s",
-                       token, quoting, tok.Rest())
+                       token, quoting.String(), tok.Rest())
        }
 }
 
@@ -897,13 +900,15 @@ func (ck *ShellLineChecker) checkVaruseT
        varname := varuse.varname
 
        if varname == "@" {
+               // No autofix here since it may be a simple typo.
+               // Maybe the package developer meant the shell variable instead.
                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}
+               varuse = NewMkVarUse(varname, varuse.modifiers...)
        }
 
        switch {
@@ -942,7 +947,7 @@ func (ck *ShellLineChecker) checkVaruseT
 
        if ck.checkVarUse {
                vuc := VarUseContext{shellCommandsType, VucUnknownTime, quoting.ToVarUseContext(), true}
-               MkLineChecker{ck.MkLines, ck.mkline}.CheckVaruse(varuse, &vuc)
+               NewMkVarUseChecker(varuse, ck.MkLines, ck.mkline).Check(&vuc)
        }
 
        return true

Index: pkgsrc/pkgtools/pkglint/files/mklineparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.6 pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.7
--- pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.6   Sat Nov 30 20:35:11 2019
+++ pkgsrc/pkgtools/pkglint/files/mklineparser.go       Sun Dec  8 00:06:38 2019
@@ -248,7 +248,7 @@ func (p MkLineParser) parseSysinclude(li
                return nil
        }
 
-       return &MkLine{line, splitResult, &mkLineInclude{directive == "include", true, indent, NewPath(includedFile), nil}}
+       return &MkLine{line, splitResult, &mkLineInclude{directive == "include", true, indent, NewRelPathString(includedFile), nil}}
 }
 
 func (p MkLineParser) parseDependency(line *Line, splitResult mkLineSplitResult) *MkLine {
@@ -290,7 +290,7 @@ func (p MkLineParser) parseMergeConflict
 // but hasComment will always be false, and comment will always be empty.
 // This behavior is useful for shell commands (which are indented with a
 // single tab).
-func (MkLineParser) split(line *Line, text string, trimComment bool) mkLineSplitResult {
+func (MkLineParser) split(diag Autofixer, text string, trimComment bool) mkLineSplitResult {
        assert(!hasPrefix(text, "\t"))
 
        var mainWithSpaces, comment string
@@ -300,7 +300,7 @@ func (MkLineParser) split(line *Line, te
                mainWithSpaces = text
        }
 
-       parser := NewMkLexer(mainWithSpaces, line)
+       parser := NewMkLexer(mainWithSpaces, diag)
        lexer := parser.lexer
 
        parseOther := func() string {
Index: pkgsrc/pkgtools/pkglint/files/var.go
diff -u pkgsrc/pkgtools/pkglint/files/var.go:1.6 pkgsrc/pkgtools/pkglint/files/var.go:1.7
--- pkgsrc/pkgtools/pkglint/files/var.go:1.6    Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/var.go        Sun Dec  8 00:06:38 2019
@@ -211,7 +211,7 @@ func (v *Var) update(mkline *MkLine, upd
        case opAssignAppend:
                *update += " " + value
 
-       case opAssignShell:
+       default:
                // Ignore these for now.
                // Later it might be useful to parse the shell commands to
                // evaluate simple commands like "test && echo yes || echo no".
@@ -269,7 +269,7 @@ func (v *Var) updateConstantValue(mkline
        case opAssignAppend:
                v.constantValue += " " + value
 
-       case opAssignShell:
+       default:
                v.constantState = 3
                v.constantValue = ""
        }

Index: pkgsrc/pkgtools/pkglint/files/mklineparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklineparser_test.go:1.5 pkgsrc/pkgtools/pkglint/files/mklineparser_test.go:1.6
--- pkgsrc/pkgtools/pkglint/files/mklineparser_test.go:1.5      Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mklineparser_test.go  Sun Dec  8 00:06:38 2019
@@ -545,7 +545,7 @@ func (s *Suite) Test_MkLineParser_parseI
        t.CheckEquals(mkline.IsInclude(), true)
        t.CheckEquals(mkline.Indent(), "    ")
        t.CheckEquals(mkline.MustExist(), true)
-       t.CheckEquals(mkline.IncludedFile(), NewPath("../../mk/bsd.prefs.mk"))
+       t.CheckEquals(mkline.IncludedFile(), NewRelPathString("../../mk/bsd.prefs.mk"))
 
        t.CheckEquals(mkline.IsSysinclude(), false)
 }
@@ -559,7 +559,7 @@ func (s *Suite) Test_MkLineParser_parseS
        t.CheckEquals(mkline.IsSysinclude(), true)
        t.CheckEquals(mkline.Indent(), "    ")
        t.CheckEquals(mkline.MustExist(), true)
-       t.CheckEquals(mkline.IncludedFile(), NewPath("subdir.mk"))
+       t.CheckEquals(mkline.IncludedFile(), NewRelPathString("subdir.mk"))
 
        t.CheckEquals(mkline.IsInclude(), false)
 }

Index: pkgsrc/pkgtools/pkglint/files/mklines.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines.go:1.62 pkgsrc/pkgtools/pkglint/files/mklines.go:1.63
--- pkgsrc/pkgtools/pkglint/files/mklines.go:1.62       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines.go    Sun Dec  8 00:06:38 2019
@@ -16,6 +16,7 @@ type MkLines struct {
        indentation   *Indentation       // Indentation depth of preprocessing directives; only available during MkLines.ForEach.
        forVars       map[string]bool    // The variables currently used in .for loops; only available during MkLines.checkAll.
        once          Once
+       postLine      func(mkline *MkLine) // Custom action that is run after checking each line
 
        // TODO: Consider extracting plistVarAdded, plistVarSet, plistVarSkip into an own type.
        // TODO: Describe where each of the above fields is valid.
@@ -42,7 +43,8 @@ func NewMkLines(lines *Lines) *MkLines {
                tools,
                nil,
                make(map[string]bool),
-               Once{}}
+               Once{},
+               nil}
 }
 
 // TODO: Consider defining an interface MkLinesChecker (different name, though, since this one confuses even me)
@@ -363,62 +365,74 @@ func (mklines *MkLines) checkAll() {
        vargroupsChecker := NewVargroupsChecker(mklines)
        isHacksMk := mklines.lines.BaseName == "hacks.mk"
 
-       lineAction := func(mkline *MkLine) bool {
-               if isHacksMk {
-                       // Needs to be set here because it is reset in MkLines.ForEach.
-                       mklines.Tools.SeenPrefs = true
-               }
+       if trace.Tracing {
+               trace.Stepf("Starting main checking loop")
+       }
+       mklines.ForEachEnd(
+               func(mkline *MkLine) bool {
+                       if isHacksMk {
+                               // Needs to be set here because it is reset in MkLines.ForEach.
+                               mklines.Tools.SeenPrefs = true
+                       }
+                       mklines.checkLine(mkline, vargroupsChecker, &varalign, substContext, allowedTargets)
+                       return true
+               },
+               func(mkline *MkLine) {
+                       // This check is not done by ForEach because ForEach only
+                       // manages the iteration, not the actual checks.
+                       mklines.indentation.CheckFinish(mklines.lines.Filename)
+                       vargroupsChecker.Finish(mkline)
+               })
 
-               ck := MkLineChecker{mklines, mkline}
-               ck.Check()
-               vargroupsChecker.Check(mkline)
-
-               varalign.Process(mkline)
-               mklines.Tools.ParseToolLine(mklines, mkline, false, false)
-               substContext.Process(mkline)
+       substContext.Finish(mklines.EOFLine())
+       varalign.Finish()
 
-               switch {
+       CheckLinesTrailingEmptyLines(mklines.lines)
+}
 
-               case mkline.IsVarassign():
-                       mklines.target = ""
-                       mkline.Tokenize(mkline.Value(), true) // Just for the side-effect of the warnings.
+func (mklines *MkLines) checkLine(
+       mkline *MkLine,
+       vargroupsChecker *VargroupsChecker,
+       varalign *VaralignBlock,
+       substContext *SubstContext,
+       allowedTargets map[string]bool) {
 
-                       mklines.checkVarassignPlist(mkline)
+       ck := MkLineChecker{mklines, mkline}
+       ck.Check()
+       vargroupsChecker.Check(mkline)
 
-               case mkline.IsInclude():
-                       mklines.target = ""
-                       if G.Pkg != nil {
-                               G.Pkg.checkIncludeConditionally(mkline, mklines.indentation)
-                       }
+       varalign.Process(mkline)
+       mklines.Tools.ParseToolLine(mklines, mkline, false, false)
+       substContext.Process(mkline)
+
+       switch {
 
-               case mkline.IsDirective():
-                       ck.checkDirective(mklines.forVars, mklines.indentation)
+       case mkline.IsVarassign():
+               mklines.target = ""
+               mkline.Tokenize(mkline.Value(), true) // Just for the side-effect of the warnings.
 
-               case mkline.IsDependency():
-                       ck.checkDependencyRule(allowedTargets)
-                       mklines.target = mkline.Targets()
+               mklines.checkVarassignPlist(mkline)
 
-               case mkline.IsShellCommand():
-                       mkline.Tokenize(mkline.ShellCommand(), true) // Just for the side-effect of the warnings.
+       case mkline.IsInclude():
+               mklines.target = ""
+               if G.Pkg != nil {
+                       G.Pkg.checkIncludeConditionally(mkline, mklines.indentation)
                }
 
-               return true
-       }
+       case mkline.IsDirective():
+               ck.checkDirective(mklines.forVars, mklines.indentation)
 
-       atEnd := func(mkline *MkLine) {
-               mklines.indentation.CheckFinish(mklines.lines.Filename)
-               vargroupsChecker.Finish(mkline)
-       }
+       case mkline.IsDependency():
+               ck.checkDependencyRule(allowedTargets)
+               mklines.target = mkline.Targets()
 
-       if trace.Tracing {
-               trace.Stepf("Starting main checking loop")
+       case mkline.IsShellCommand():
+               mkline.Tokenize(mkline.ShellCommand(), true) // Just for the side-effect of the warnings.
        }
-       mklines.ForEachEnd(lineAction, atEnd)
-
-       substContext.Finish(mklines.EOFLine())
-       varalign.Finish()
 
-       CheckLinesTrailingEmptyLines(mklines.lines)
+       if mklines.postLine != nil {
+               mklines.postLine(mkline)
+       }
 }
 
 func (mklines *MkLines) checkVarassignPlist(mkline *MkLine) {
Index: pkgsrc/pkgtools/pkglint/files/util.go
diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.62 pkgsrc/pkgtools/pkglint/files/util.go:1.63
--- pkgsrc/pkgtools/pkglint/files/util.go:1.62  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/util.go       Sun Dec  8 00:06:38 2019
@@ -254,7 +254,7 @@ func isEmptyDir(filename CurrPath) bool 
                if isIgnoredFilename(name) {
                        continue
                }
-               if dirent.IsDir() && isEmptyDir(filename.JoinNoClean(NewPath(name))) {
+               if dirent.IsDir() && isEmptyDir(filename.JoinNoClean(NewRelPathString(name))) {
                        continue
                }
                return false
@@ -262,17 +262,17 @@ func isEmptyDir(filename CurrPath) bool 
        return true
 }
 
-func getSubdirs(filename CurrPath) []Path {
+func getSubdirs(filename CurrPath) []RelPath {
        dirents, err := filename.ReadDir()
        if err != nil {
                NewLineWhole(filename).Fatalf("Cannot be read: %s", err)
        }
 
-       var subdirs []Path
+       var subdirs []RelPath
        for _, dirent := range dirents {
                name := dirent.Name()
-               if dirent.IsDir() && !isIgnoredFilename(name) && !isEmptyDir(filename.JoinNoClean(NewPath(name))) {
-                       subdirs = append(subdirs, NewPath(name))
+               if dirent.IsDir() && !isIgnoredFilename(name) && !isEmptyDir(filename.JoinNoClean(NewRelPathString(name))) {
+                       subdirs = append(subdirs, NewRelPathString(name))
                }
        }
        return subdirs
@@ -280,7 +280,7 @@ func getSubdirs(filename CurrPath) []Pat
 
 func isIgnoredFilename(filename string) bool {
        switch filename {
-       case ".", "..", "CVS", ".svn", ".git", ".hg", ".idea":
+       case "CVS", ".svn", ".git", ".hg", ".idea":
                return true
        }
        return hasPrefix(filename, ".#")
@@ -431,6 +431,10 @@ func toInt(s string, def int) int {
 }
 
 // mkopSubst evaluates make(1)'s :S substitution operator.
+// It does not resolve any variables.
+// FIXME: Move this function to the MkVarUseModifier type.
+// FIXME: Clearly signal that substituting is not possible if either
+//  of the strings contains a variable reference.
 func mkopSubst(s string, left bool, from string, right bool, to string, flags string) string {
        re := regex.Pattern(condStr(left, "^", "") + regexp.QuoteMeta(from) + condStr(right, "$", ""))
        done := false
@@ -555,12 +559,14 @@ func (s *Scope) Define(varname string, m
                // see MkLines.collectDocumentedVariables.
                if mkline.IsVarassign() {
                        switch mkline.Op() {
-                       case opAssign, opAssignEval, opAssignShell:
-                               s.value[name] = mkline.Value()
                        case opAssignAppend:
                                s.value[name] += " " + mkline.Value()
                        case opAssignDefault:
                                // No change to the value.
+                       case opAssignShell:
+                               s.value[name] = mkline.Value() // FIXME: Really?
+                       default:
+                               s.value[name] = mkline.Value()
                        }
                }
        }
@@ -665,7 +671,7 @@ func (s *Scope) FirstDefinition(varname 
                lastLine := s.LastDefinition(varname)
                if trace.Tracing && lastLine != mkline {
                        trace.Stepf("%s: FirstDefinition differs from LastDefinition in %s.",
-                               mkline.String(), mkline.RefTo(lastLine))
+                               mkline.String(), mkline.RelMkLine(lastLine))
                }
                return mkline
        }
@@ -836,19 +842,30 @@ func naturalLess(str1, str2 string) bool
        return len1 < len2
 }
 
-// IsPrefs returns whether the given file, when included, loads the user
+// LoadsPrefs returns whether the given file, when included, loads the user
 // preferences.
-func IsPrefs(filename Path) bool {
+func LoadsPrefs(filename RelPath) bool {
        switch filename.Base() {
        case // See https://github.com/golang/go/issues/28057
                "bsd.prefs.mk",         // in mk/
                "bsd.fast.prefs.mk",    // in mk/
                "bsd.builtin.mk",       // in mk/buildlink3/
                "pkgconfig-builtin.mk", // in mk/buildlink3/
+               "pkg-build-options.mk", // in mk/
+               "compiler.mk",          // in mk/
+               "options.mk",           // in package directories
                "bsd.options.mk":       // in mk/
                return true
        }
-       return false
+
+       // Just assume that every pkgsrc infrastructure file includes
+       // bsd.prefs.mk, at least indirectly.
+       return filename.ContainsPath("mk")
+}
+
+func IsPrefs(filename RelPath) bool {
+       base := filename.Base()
+       return base == "bsd.prefs.mk" || base == "bsd.fast.prefs.mk"
 }
 
 // FileCache reduces the IO load for commonly loaded files by about 50%,
Index: pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.62 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.63
--- pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.62     Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go  Sun Dec  8 00:06:38 2019
@@ -115,7 +115,22 @@ func (s *Suite) Test_VartypeCheck_Catego
                "chinese",
                "arabic",
                "filesyscategory",
-               "wip")
+               "wip",
+               "gnome",
+               "gnustep",
+               "java",
+               "kde",
+               "korean",
+               "linux",
+               "local",
+               "plan9",
+               "R",
+               "ruby",
+               "scm",
+               "tcl",
+               "tk",
+               "windowmaker",
+               "xmms")
 
        vt.Output(
                "ERROR: filename.mk:2: Invalid category \"arabic\".",
@@ -481,12 +496,16 @@ func (s *Suite) Test_VartypeCheck_Enum(c
 func (s *Suite) Test_VartypeCheck_Enum__use_match(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
        t.SetUpCommandLine("-Wall", "--explain")
 
        mklines := t.NewMkLines("module.mk",
                MkCvsID,
                "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".if !empty(MACHINE_ARCH:Mi386) || ${MACHINE_ARCH} == i386",
                ".endif",
                ".if !empty(PKGSRC_COMPILER:Mclang) || ${PKGSRC_COMPILER} == clang",
@@ -497,17 +516,19 @@ func (s *Suite) Test_VartypeCheck_Enum__
        mklines.Check()
 
        t.CheckOutputLines(
-               "NOTE: module.mk:3: MACHINE_ARCH should be compared using == instead of matching against \":Mi386\".",
+               "NOTE: module.mk:5: MACHINE_ARCH "+
+                       "should be compared using \"${MACHINE_ARCH} == i386\" "+
+                       "instead of matching against \":Mi386\".",
                "",
                "\tThis variable has a single value, not a list of values. Therefore it",
                "\tfeels strange to apply list operators like :M and :N onto it. A more",
                "\tdirect approach is to use the == and != operators.",
                "",
                "\tAn entirely different case is when the pattern contains wildcards",
-               "\tlike ^, *, $. In such a case, using the :M or :N modifiers is useful",
-               "\tand preferred.",
+               "\tlike *, ?, []. In such a case, using the :M or :N modifiers is",
+               "\tuseful and preferred.",
                "",
-               "ERROR: module.mk:5: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
+               "ERROR: module.mk:7: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
                "",
                "\tThe PKGSRC_COMPILER can be a list of chained compilers, e.g. \"ccache",
                "\tdistcc clang\". Therefore, comparing it using == or != leads to wrong",
@@ -907,8 +928,9 @@ func (s *Suite) Test_VartypeCheck_Licens
        t := s.Init(c)
 
        t.Chdir(".")
-       t.SetUpPkgsrc() // Adds the gnu-gpl-v2 and 2-clause-bsd licenses
+       // Adds the gnu-gpl-v2 and 2-clause-bsd licenses
        t.SetUpPackage("category/package")
+       t.CreateFileLines("licenses/mit", "...")
        t.FinishSetUp()
 
        G.Pkg = NewPackage(t.File("category/package"))
@@ -930,7 +952,7 @@ func (s *Suite) Test_VartypeCheck_Licens
 
        vt.Output(
                "ERROR: filename.mk:2: Parse error for license condition \"AND mit\".",
-               "WARN: filename.mk:3: License file licenses/artistic does not exist.",
+               "ERROR: filename.mk:3: License file licenses/artistic does not exist.",
                "ERROR: filename.mk:4: Parse error for license condition \"${UNKNOWN_LICENSE}\".")
 
        vt.Op(opAssignAppend)
@@ -939,8 +961,7 @@ func (s *Suite) Test_VartypeCheck_Licens
                "AND mit")
 
        vt.Output(
-               "ERROR: filename.mk:11: Parse error for appended license condition \"gnu-gpl-v2\".",
-               "WARN: filename.mk:12: License file licenses/mit does not exist.")
+               "ERROR: filename.mk:11: Parse error for appended license condition \"gnu-gpl-v2\".")
 }
 
 func (s *Suite) Test_VartypeCheck_MachineGnuPlatform(c *check.C) {
@@ -1266,10 +1287,14 @@ func (s *Suite) Test_VartypeCheck_Prefix
        vt.Varname("PKGMANDIR")
        vt.Values(
                "man/man1",
-               "share/locale")
+               "share/locale",
+               "/absolute")
 
        vt.Output(
-               "WARN: filename.mk:1: Please use \"${PKGMANDIR}/man1\" instead of \"man/man1\".")
+               "WARN: filename.mk:1: "+
+                       "Please use \"${PKGMANDIR}/man1\" instead of \"man/man1\".",
+               "ERROR: filename.mk:3: The pathname \"/absolute\" in PKGMANDIR "+
+                       "must be relative to ${PREFIX}.")
 }
 
 func (s *Suite) Test_VartypeCheck_PythonDependency(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/mklines_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.53 pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.54
--- pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.53  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines_test.go       Sun Dec  8 00:06:38 2019
@@ -52,9 +52,14 @@ func (s *Suite) Test_MkLines__for_loop_m
 func (s *Suite) Test_MkLines__comparing_YesNo_variable_to_string(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("databases/gdbm_compat/builtin.mk",
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.SetUpFileMkLines("databases/gdbm_compat/builtin.mk",
                MkCvsID,
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".if ${USE_BUILTIN.gdbm} == \"no\"",
                ".endif",
                ".if ${USE_BUILTIN.gdbm:tu} == \"no\"", // Can never be true, since "no" is not uppercase.
@@ -63,7 +68,7 @@ func (s *Suite) Test_MkLines__comparing_
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: databases/gdbm_compat/builtin.mk:2: " +
+               "WARN: databases/gdbm_compat/builtin.mk:5: " +
                        "USE_BUILTIN.gdbm should be matched against \"[yY][eE][sS]\" or \"[nN][oO]\", " +
                        "not compared with \"no\".")
 }
@@ -197,9 +202,14 @@ func (s *Suite) Test_MkLines_Check__loop
 func (s *Suite) Test_MkLines_Check__PKG_SKIP_REASON_depending_on_OPSYS(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
        mklines := t.NewMkLines("Makefile",
                MkCvsID,
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                "PKG_SKIP_REASON+=\t\"Fails everywhere\"",
                ".if ${OPSYS} == \"Cygwin\"",
                "PKG_SKIP_REASON+=\t\"Fails on Cygwin\"",
@@ -208,7 +218,7 @@ func (s *Suite) Test_MkLines_Check__PKG_
        mklines.Check()
 
        t.CheckOutputLines(
-               "NOTE: Makefile:4: Consider setting NOT_FOR_PLATFORM instead of PKG_SKIP_REASON depending on ${OPSYS}.")
+               "NOTE: Makefile:7: Consider setting NOT_FOR_PLATFORM instead of PKG_SKIP_REASON depending on ${OPSYS}.")
 }
 
 func (s *Suite) Test_MkLines_Check__use_list_variable_as_part_of_word(c *check.C) {
@@ -229,9 +239,14 @@ func (s *Suite) Test_MkLines_Check__use_
 func (s *Suite) Test_MkLines_Check__absolute_pathname_depending_on_OPSYS(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
-       mklines := t.NewMkLines("games/heretic2-demo/Makefile",
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.SetUpFileMkLines("games/heretic2-demo/Makefile",
                MkCvsID,
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".if ${OPSYS} == \"DragonFly\"",
                "TAR_CMD=\t/usr/bin/bsdtar",
                ".endif",
@@ -246,15 +261,20 @@ func (s *Suite) Test_MkLines_Check__abso
        // Shell commands that are specific to an operating system are probably defined
        // and used intentionally, so even commands that are not known tools are allowed.
        t.CheckOutputLines(
-               "WARN: games/heretic2-demo/Makefile:5: Unknown shell command \"/usr/bin/bsdtar\".")
+               "WARN: games/heretic2-demo/Makefile:8: Unknown shell command \"/usr/bin/bsdtar\".")
 }
 
 func (s *Suite) Test_MkLines_Check__indentation(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
        mklines := t.NewMkLines("options.mk",
                MkCvsID,
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ". if !defined(GUARD_MK)",
                ". if ${OPSYS} == ${OPSYS}",
                ".   for i in ${FILES}",
@@ -273,27 +293,27 @@ func (s *Suite) Test_MkLines_Check__inde
        mklines.Check()
 
        t.CheckOutputLines(
-               "NOTE: options.mk:2: This directive should be indented by 0 spaces.",
-               "WARN: options.mk:2: GUARD_MK is used but not defined.",
-               "NOTE: options.mk:3: This directive should be indented by 0 spaces.",
-               "NOTE: options.mk:4: This directive should be indented by 2 spaces.",
-               "WARN: options.mk:4: FILES is used but not defined.",
-               "NOTE: options.mk:5: This directive should be indented by 4 spaces.",
-               "WARN: options.mk:5: GUARD2_MK is used but not defined.",
-               "NOTE: options.mk:6: This directive should be indented by 4 spaces.",
-               "NOTE: options.mk:7: This directive should be indented by 4 spaces.",
-               "NOTE: options.mk:8: This directive should be indented by 2 spaces.",
-               "NOTE: options.mk:9: This directive should be indented by 2 spaces.",
-               "WARN: options.mk:9: COND1 is used but not defined.",
-               "NOTE: options.mk:10: This directive should be indented by 2 spaces.",
-               "WARN: options.mk:10: COND2 is used but not defined.",
+               "NOTE: options.mk:5: This directive should be indented by 0 spaces.",
+               "WARN: options.mk:5: GUARD_MK is used but not defined.",
+               "NOTE: options.mk:6: This directive should be indented by 0 spaces.",
+               "NOTE: options.mk:7: This directive should be indented by 2 spaces.",
+               "WARN: options.mk:7: FILES is used but not defined.",
+               "NOTE: options.mk:8: This directive should be indented by 4 spaces.",
+               "WARN: options.mk:8: GUARD2_MK is used but not defined.",
+               "NOTE: options.mk:9: This directive should be indented by 4 spaces.",
+               "NOTE: options.mk:10: This directive should be indented by 4 spaces.",
                "NOTE: options.mk:11: This directive should be indented by 2 spaces.",
-               "ERROR: options.mk:11: \".else\" does not take arguments. If you meant \"else if\", use \".elif\".",
                "NOTE: options.mk:12: This directive should be indented by 2 spaces.",
-               "NOTE: options.mk:13: This directive should be indented by 0 spaces.",
-               "NOTE: options.mk:14: This directive should be indented by 0 spaces.",
-               "NOTE: options.mk:15: This directive should be indented by 0 spaces.",
-               "ERROR: options.mk:15: Unmatched .endif.")
+               "WARN: options.mk:12: COND1 is used but not defined.",
+               "NOTE: options.mk:13: This directive should be indented by 2 spaces.",
+               "WARN: options.mk:13: COND2 is used but not defined.",
+               "NOTE: options.mk:14: This directive should be indented by 2 spaces.",
+               "ERROR: options.mk:14: \".else\" does not take arguments. If you meant \"else if\", use \".elif\".",
+               "NOTE: options.mk:15: This directive should be indented by 2 spaces.",
+               "NOTE: options.mk:16: This directive should be indented by 0 spaces.",
+               "NOTE: options.mk:17: This directive should be indented by 0 spaces.",
+               "NOTE: options.mk:18: This directive should be indented by 0 spaces.",
+               "ERROR: options.mk:18: Unmatched .endif.")
 }
 
 // The .include directives do not need to be indented. They have the
@@ -303,12 +323,16 @@ func (s *Suite) Test_MkLines_Check__inde
 func (s *Suite) Test_MkLines_Check__indentation_include(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
        t.CreateFileLines("included.mk")
        mklines := t.SetUpFileMkLines("module.mk",
                MkCvsID,
                "",
-               ".if ${PKGPATH} == \"category/package\"",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
+               ".if ${PKGPATH} == \"category/nonexistent\"",
                ".include \"included.mk\"",
                ". include \"included.mk\"",
                ".  include \"included.mk\"",
@@ -318,18 +342,23 @@ func (s *Suite) Test_MkLines_Check__inde
        mklines.Check()
 
        t.CheckOutputLines(
-               "ERROR: ~/module.mk:3: There is no package in \"category/package\".",
-               "NOTE: ~/module.mk:5: This directive should be indented by 2 spaces.",
-               "NOTE: ~/module.mk:7: This directive should be indented by 2 spaces.")
+               // TODO: Use relative path for missing package.
+               "ERROR: module.mk:5: There is no package in \"../../category/nonexistent\".",
+               "NOTE: module.mk:7: This directive should be indented by 2 spaces.",
+               "NOTE: module.mk:9: This directive should be indented by 2 spaces.")
 }
 
 func (s *Suite) Test_MkLines_Check__unfinished_directives(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
        mklines := t.NewMkLines("opsys.mk",
                MkCvsID,
                "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".for i in 1 2 3 4 5",
                ".  if ${OPSYS} == NetBSD",
                ".    if ${MACHINE_ARCH} == x86_64",
@@ -338,19 +367,23 @@ func (s *Suite) Test_MkLines_Check__unfi
        mklines.Check()
 
        t.CheckOutputLines(
+               "ERROR: opsys.mk:EOF: .if from line 8 must be closed.",
+               "ERROR: opsys.mk:EOF: .if from line 7 must be closed.",
                "ERROR: opsys.mk:EOF: .if from line 6 must be closed.",
-               "ERROR: opsys.mk:EOF: .if from line 5 must be closed.",
-               "ERROR: opsys.mk:EOF: .if from line 4 must be closed.",
-               "ERROR: opsys.mk:EOF: .for from line 3 must be closed.")
+               "ERROR: opsys.mk:EOF: .for from line 5 must be closed.")
 }
 
 func (s *Suite) Test_MkLines_Check__unbalanced_directives(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
        mklines := t.NewMkLines("opsys.mk",
                MkCvsID,
                "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".for i in 1 2 3 4 5",
                ".  if ${OPSYS} == NetBSD",
                ".  endfor",
@@ -630,6 +663,49 @@ func (s *Suite) Test_MkLines_collectVari
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_MkLines_collectVariables__BUILD_DEFS(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "",
+               "BUILD_DEFS+=\t\tBD_VAR",
+               // In practice, these variables are called *_GROUP.
+               "PKG_GROUPS_VARS+=\tGRP",
+               // In practice, these variables are called *_USER.
+               "PKG_USERS_VARS+=\tUSR")
+
+       mklines.Check()
+
+       t.CheckDeepEquals(
+               keys(mklines.buildDefs),
+               []string{"BD_VAR", "GRP", "USR"})
+}
+
+func (s *Suite) Test_MkLines_collectVariables__find_files_and_headers(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("builtin.mk",
+               MkCvsID,
+               "",
+               "BUILTIN_FIND_FILES_VAR=\t\tX1_FILE X2_FILE",
+               "BUILTIN_FIND_HEADERS_VAR=\tX1_HEADER X2_HEADER")
+
+       mklines.Check()
+
+       t.CheckDeepEquals(
+               keys(mklines.vars.firstDef),
+               []string{
+                       "BUILTIN_FIND_FILES_VAR",
+                       "BUILTIN_FIND_HEADERS_VAR",
+                       "X1_FILE",
+                       "X1_HEADER",
+                       "X2_FILE",
+                       "X2_HEADER"})
+}
+
 // Ensures that during MkLines.ForEach, the conditional variables in
 // MkLines.Indentation are correctly updated for each line.
 func (s *Suite) Test_MkLines_ForEach__conditional_variables(c *check.C) {
Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.53 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.54
--- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.53  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go       Sun Dec  8 00:06:38 2019
@@ -371,7 +371,7 @@ func (s *Suite) Test_Pkglint_Check__outs
        // pkglint will exit with a fatal error message since it doesn't
        // know where to load the infrastructure files from.
        t.CheckOutputLines(
-               "ERROR: ~: Cannot determine the pkgsrc root directory for \"~\".")
+               "ERROR: Cannot determine the pkgsrc root directory for \"~\".")
 }
 
 func (s *Suite) Test_Pkglint_Check__empty_directory(c *check.C) {
@@ -1272,7 +1272,7 @@ func (s *Suite) Test_InterPackage_Bl3__s
                "PKGNAME=\t${DISTNAME:@v@${v}@}") // Make the package name non-obvious.
        t.SetUpPackage("category/package2",
                "PKGNAME=\t${DISTNAME:@v@${v}@}") // Make the package name non-obvious.
-       t.CreateFileDummyBuildlink3("category/package1/buildlink3.mk")
+       t.CreateFileBuildlink3("category/package1/buildlink3.mk")
        t.Copy("category/package1/buildlink3.mk", "category/package2/buildlink3.mk")
        t.Chdir(".")
        t.FinishSetUp()

Index: pkgsrc/pkgtools/pkglint/files/mkparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.36 pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.37
--- pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.36 Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/mkparser_test.go      Sun Dec  8 00:06:38 2019
@@ -163,6 +163,18 @@ func (s *Suite) Test_MkParser_MkCond(c *
        test("\"${VAR}str\"",
                &MkCond{Term: &MkCondTerm{Str: "${VAR}str"}})
 
+       test("commands(show-var)",
+               &MkCond{Call: &MkCondCall{"commands", "show-var"}})
+
+       test("exists(/usr/bin)",
+               &MkCond{Call: &MkCondCall{"exists", "/usr/bin"}})
+
+       test("make(show-var)",
+               &MkCond{Call: &MkCondCall{"make", "show-var"}})
+
+       test("target(do-build)",
+               &MkCond{Call: &MkCondCall{"target", "do-build"}})
+
        // Errors
 
        testRest("defined()",
@@ -248,6 +260,10 @@ func (s *Suite) Test_MkParser_MkCond(c *
        testRest("!empty{USE_CROSS_COMPILE:M[yY][eE][sS]}",
                nil,
                "empty{USE_CROSS_COMPILE:M[yY][eE][sS]}")
+
+       testRest("unknown(arg)",
+               nil,
+               "unknown(arg)")
 }
 
 func (s *Suite) Test_MkParser_mkCondCompare(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/mktypes.go
diff -u pkgsrc/pkgtools/pkglint/files/mktypes.go:1.21 pkgsrc/pkgtools/pkglint/files/mktypes.go:1.22
--- pkgsrc/pkgtools/pkglint/files/mktypes.go:1.21       Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/mktypes.go    Sun Dec  8 00:06:38 2019
@@ -7,7 +7,7 @@ import "strings"
 //
 // Example: /usr/share/${PKGNAME}/data consists of 3 tokens:
 //  1. MkToken{Text: "/usr/share/"}
-//  2. MkToken{Text: "${PKGNAME}", Varuse: &MkVarUse{varname: "PKGNAME"}}
+//  2. MkToken{Text: "${PKGNAME}", Varuse: NewMkVarUse("PKGNAME")}
 //  3. MkToken{Text: "/data"}
 //
 type MkToken struct {
@@ -29,6 +29,10 @@ type MkVarUse struct {
        modifiers []MkVarUseModifier // E.g. "Q", "S/from/to/"
 }
 
+func NewMkVarUse(varname string, modifiers ...MkVarUseModifier) *MkVarUse {
+       return &MkVarUse{varname, modifiers}
+}
+
 func (vu *MkVarUse) String() string { return sprintf("${%s%s}", vu.varname, vu.Mod()) }
 
 type MkVarUseModifier struct {
@@ -105,9 +109,10 @@ func (m MkVarUseModifier) MatchMatch() (
 
 func (m MkVarUseModifier) IsToLower() bool { return m.Text == "tl" }
 
-// ChangesWords returns true if applying this modifier to a list variable
-// may change the number of words in the list, or their boundaries.
-func (m MkVarUseModifier) ChangesWords() bool {
+// ChangesList returns true if applying this modifier to a variable
+// may change the expression from a list type to a non-list type
+// or vice versa.
+func (m MkVarUseModifier) ChangesList() bool {
        text := m.Text
 
        // See MkParser.varUseModifier for the meaning of these modifiers.
Index: pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.21 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.22
--- pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.21      Sat Nov 30 20:35:11 2019
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go   Sun Dec  8 00:06:38 2019
@@ -523,6 +523,16 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                operator(")"), space,
                text("still-subshell"), operator(";;"), space,
                text("esac"), operator(")"), operator(";"))
+
+       testRest("`echo \\${VAR}`",
+               atoms(
+                       backt(text("`")),
+                       backt(text("echo")),
+                       backt(space)),
+               "\\${VAR}`")
+       t.CheckOutputLines(
+               "WARN: filename.mk:1: Internal pkglint error " +
+                       "in ShTokenizer.ShAtom at \"\\\\${VAR}`\" (quoting=b).")
 }
 
 func (s *Suite) Test_ShTokenizer_ShAtom__quoting(c *check.C) {
@@ -562,6 +572,21 @@ func (s *Suite) Test_ShTokenizer_ShAtom_
        test("x`x\\\"x\\'x\\`x\\\\", "x`[b]x\\\"x\\'x\\`x\\\\")
 }
 
+// The switch statement in ShTokenizer.ShAtom is exhaustive.
+// If a new quoting mode is added (in which case the shell tokenizer
+// should rather be rewritten completely and correctly), it is ok
+// to panic if ShQuoting is not adjusted in the same commit.
+func (s *Suite) Test_ShTokenizer_ShAtom__internal_error(c *check.C) {
+       t := s.Init(c)
+
+       line := t.NewLine("filename.mk", 123, "\ttoken")
+       tok := NewShTokenizer(line, line.Text, true)
+       t.ExpectPanicMatches(
+               func() { tok.ShAtom(^ShQuoting(0)) },
+               // Normalize the panic message, for Go < 12 if I remember correctly.
+               `^runtime error: index out of range.*`)
+}
+
 func (s *Suite) Test_ShTokenizer_shVarUse(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/options_test.go
diff -u pkgsrc/pkgtools/pkglint/files/options_test.go:1.20 pkgsrc/pkgtools/pkglint/files/options_test.go:1.21
--- pkgsrc/pkgtools/pkglint/files/options_test.go:1.20  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/options_test.go       Sun Dec  8 00:06:38 2019
@@ -347,14 +347,15 @@ func (s *Suite) Test_CheckLinesOptionsMk
 func (s *Suite) Test_CheckLinesOptionsMk__malformed_condition(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir(".")
        t.SetUpOption("mc-charset", "")
        t.SetUpOption("ncurses", "")
        t.SetUpOption("slang", "")
        t.SetUpOption("x11", "")
-
        t.CreateFileLines("mk/bsd.options.mk",
                MkCvsID)
+       t.FinishSetUp()
 
        mklines := t.SetUpFileMkLines("category/package/options.mk",
                MkCvsID,
@@ -363,6 +364,8 @@ func (s *Suite) Test_CheckLinesOptionsMk
                "PKG_SUPPORTED_OPTIONS=\t\t# none",
                "PKG_SUGGESTED_OPTIONS=\t\t# none",
                "",
+               ".include \"../../mk/bsd.fast.prefs.mk\"",
+               "",
                "# Comments and conditionals are allowed at this point.",
                ".if ${OPSYS} == NetBSD",
                ".endif",
@@ -375,7 +378,7 @@ func (s *Suite) Test_CheckLinesOptionsMk
        CheckLinesOptionsMk(mklines)
 
        t.CheckOutputLines(
-               "WARN: ~/category/package/options.mk:13: Invalid condition, unrecognized part: \"${OPSYS} == 'Darwin'\".")
+               "WARN: category/package/options.mk:15: Invalid condition, unrecognized part: \"${OPSYS} == 'Darwin'\".")
 }
 
 func (s *Suite) Test_CheckLinesOptionsMk__PLIST_VARS_based_on_PKG_SUPPORTED_OPTIONS(c *check.C) {
@@ -571,6 +574,11 @@ func (s *Suite) Test_CheckLinesOptionsMk
        t.CreateFileLines("category/package/options.mk",
                MkCvsID,
                "",
+               "# Including bsd.prefs.mk is not necessary here since the OPSYS",
+               "# in PKG_SUPPORTED_OPTIONS is only evaluated lazily inside",
+               "# bsd.options.mk, at which point bsd.prefs.mk will be included",
+               "# as well.",
+               "",
                "PKG_OPTIONS_VAR=\tPKG_OPTIONS.package",
                "PKG_SUPPORTED_OPTIONS=\tgeneric-${OPSYS}",
                "",
Index: pkgsrc/pkgtools/pkglint/files/tools.go
diff -u pkgsrc/pkgtools/pkglint/files/tools.go:1.20 pkgsrc/pkgtools/pkglint/files/tools.go:1.21
--- pkgsrc/pkgtools/pkglint/files/tools.go:1.20 Sat Nov 23 23:35:56 2019
+++ pkgsrc/pkgtools/pkglint/files/tools.go      Sun Dec  8 00:06:38 2019
@@ -285,7 +285,7 @@ func (tr *Tools) ParseToolLine(mklines *
                }
 
        case mkline.IsInclude():
-               if IsPrefs(mkline.IncludedFile()) {
+               if LoadsPrefs(mkline.IncludedFile()) {
                        tr.SeenPrefs = true
                }
        }
@@ -345,7 +345,7 @@ func (tr *Tools) implicitTools(toolName 
 
 func (tr *Tools) validity(basename string, useTools bool) Validity {
        switch {
-       case IsPrefs(NewPath(basename)): // IsPrefs is not 100% accurate here but good enough
+       case LoadsPrefs(NewRelPathString(basename)): // LoadsPrefs is not 100% accurate here but good enough
                return AfterPrefsMk
        case basename == "Makefile" && !tr.SeenPrefs:
                return AfterPrefsMk

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.72 pkgsrc/pkgtools/pkglint/files/package.go:1.73
--- pkgsrc/pkgtools/pkglint/files/package.go:1.72       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/package.go    Sun Dec  8 00:06:38 2019
@@ -31,7 +31,13 @@ type Package struct {
        vars      Scope
        redundant *RedundantScope
 
-       bl3 map[Path]*MkLine // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included.
+       // bl3 contains the buildlink3.mk files that are included by the
+       // package, including any transitively included files.
+       //
+       // This is later compared to those buildlink3.mk files that are
+       // included by the package's own buildlink3.mk file.
+       // These included files should match.
+       bl3 map[PackagePath]*MkLine
 
        // Remembers the Makefile fragments that have already been included.
        // The key to the map is the filename relative to the package directory.
@@ -45,14 +51,26 @@ type Package struct {
        included Once
 
        // Does the package have any .includes?
+       //
+       // TODO: Be more precise about the purpose of this field.
        seenInclude bool
 
+       // During both load() and check(), tells whether bsd.prefs.mk has
+       // already been loaded directly or indirectly.
+       //
+       // At the end of load(), it is reset to false.
+       seenPrefs bool
+
+       // The first line of the package Makefile at which bsd.prefs.mk is
+       // guaranteed to be loaded.
+       prefsLine *MkLine
+
        // Files from .include lines that are nested inside .if.
        // They often depend on OPSYS or on the existence of files in the build environment.
-       conditionalIncludes map[Path]*MkLine
+       conditionalIncludes map[PackagePath]*MkLine
        // Files from .include lines that are not nested.
        // These are cross-checked with buildlink3.mk whether they are unconditional there, too.
-       unconditionalIncludes map[Path]*MkLine
+       unconditionalIncludes map[PackagePath]*MkLine
 
        IgnoreMissingPatches bool // In distinfo, don't warn about patches that cannot be found.
 
@@ -77,10 +95,10 @@ func NewPackage(dir CurrPath) *Package {
                DistinfoFile:          "${PKGDIR}/distinfo", // TODO: Redundant, see the vars.Fallback below.
                Plist:                 NewPlistContent(),
                vars:                  NewScope(),
-               bl3:                   make(map[Path]*MkLine),
+               bl3:                   make(map[PackagePath]*MkLine),
                included:              Once{},
-               conditionalIncludes:   make(map[Path]*MkLine),
-               unconditionalIncludes: make(map[Path]*MkLine),
+               conditionalIncludes:   make(map[PackagePath]*MkLine),
+               unconditionalIncludes: make(map[PackagePath]*MkLine),
        }
        pkg.vars.DefineAll(G.Pkgsrc.UserDefinedVars)
 
@@ -99,6 +117,11 @@ func NewPackage(dir CurrPath) *Package {
        return &pkg
 }
 
+func (pkg *Package) Check() {
+       files, mklines, allLines := pkg.load()
+       pkg.check(files, mklines, allLines)
+}
+
 func (pkg *Package) load() ([]CurrPath, *MkLines, *MkLines) {
        // Load the package Makefile and all included files,
        // to collect all used and defined variables and similar data.
@@ -112,7 +135,7 @@ func (pkg *Package) load() ([]CurrPath, 
                files = append(files, pkg.File(pkg.Pkgdir).ReadPaths()...)
        }
        files = append(files, pkg.File(pkg.Patchdir).ReadPaths()...)
-       if pkg.DistinfoFile != NewPackagePath(NewPath(pkg.vars.fallback["DISTINFO_FILE"])) {
+       if pkg.DistinfoFile != NewPackagePathString(pkg.vars.fallback["DISTINFO_FILE"]) {
                files = append(files, pkg.File(pkg.DistinfoFile))
        }
 
@@ -144,6 +167,7 @@ func (pkg *Package) load() ([]CurrPath, 
                }
        }
 
+       pkg.seenPrefs = false
        return files, mklines, allLines
 }
 
@@ -159,7 +183,7 @@ func (pkg *Package) loadPackageMakefile(
        }
 
        allLines := NewMkLines(NewLines("", nil))
-       if !pkg.parse(mainLines, allLines, "") {
+       if !pkg.parse(mainLines, allLines, "", true) {
                return nil, nil
        }
 
@@ -182,10 +206,10 @@ func (pkg *Package) loadPackageMakefile(
 
        allLines.collectUsedVariables()
 
-       pkg.Pkgdir = NewPackagePath(NewPath(pkg.vars.LastValue("PKGDIR")))
-       pkg.DistinfoFile = NewPackagePath(NewPath(pkg.vars.LastValue("DISTINFO_FILE")))
-       pkg.Filesdir = NewPackagePath(NewPath(pkg.vars.LastValue("FILESDIR")))
-       pkg.Patchdir = NewPackagePath(NewPath(pkg.vars.LastValue("PATCHDIR")))
+       pkg.Pkgdir = NewPackagePathString(pkg.vars.LastValue("PKGDIR"))
+       pkg.DistinfoFile = NewPackagePathString(pkg.vars.LastValue("DISTINFO_FILE"))
+       pkg.Filesdir = NewPackagePathString(pkg.vars.LastValue("FILESDIR"))
+       pkg.Patchdir = NewPackagePathString(pkg.vars.LastValue("PATCHDIR"))
 
        // See lang/php/ext.mk
        if pkg.vars.IsDefinedSimilar("PHPEXT_MK") {
@@ -213,13 +237,13 @@ func (pkg *Package) loadPackageMakefile(
 }
 
 // TODO: What is allLines used for, is it still necessary? Would it be better as a field in Package?
-func (pkg *Package) parse(mklines *MkLines, allLines *MkLines, includingFileForUsedCheck CurrPath) bool {
+func (pkg *Package) parse(mklines *MkLines, allLines *MkLines, includingFileForUsedCheck CurrPath, main bool) bool {
        if trace.Tracing {
                defer trace.Call(mklines.lines.Filename)()
        }
 
        result := mklines.ForEachEnd(
-               func(mkline *MkLine) bool { return pkg.parseLine(mklines, mkline, allLines) },
+               func(mkline *MkLine) bool { return pkg.parseLine(mklines, mkline, allLines, main) },
                func(mkline *MkLine) {})
 
        if includingFileForUsedCheck != "" {
@@ -235,14 +259,14 @@ func (pkg *Package) parse(mklines *MkLin
                builtinRel := G.Pkgsrc.Relpath(pkg.dir, builtin)
                if pkg.included.FirstTime(builtinRel.String()) && builtin.IsFile() {
                        builtinMkLines := LoadMk(builtin, MustSucceed|LogErrors)
-                       pkg.parse(builtinMkLines, allLines, "")
+                       pkg.parse(builtinMkLines, allLines, "", false)
                }
        }
 
        return result
 }
 
-func (pkg *Package) parseLine(mklines *MkLines, mkline *MkLine, allLines *MkLines) bool {
+func (pkg *Package) parseLine(mklines *MkLines, mkline *MkLine, allLines *MkLines, main bool) bool {
        allLines.mklines = append(allLines.mklines, mkline)
        allLines.lines.Lines = append(allLines.lines.Lines, mkline.Line)
 
@@ -252,7 +276,8 @@ func (pkg *Package) parseLine(mklines *M
                includedMkLines, skip := pkg.loadIncluded(mkline, includingFile)
 
                if includedMkLines == nil {
-                       if skip || mklines.indentation.HasExists(NewRelPath(includedFile)) {
+                       pkgsrcPath := G.Pkgsrc.ToRel(mkline.File(includedFile))
+                       if skip || mklines.indentation.HasExists(pkgsrcPath) {
                                return true // See https://github.com/rillig/pkglint/issues/1
                        }
                        mkline.Errorf("Cannot read %q.", includedFile)
@@ -264,9 +289,12 @@ func (pkg *Package) parseLine(mklines *M
                if dir != "" && base == "Makefile.common" && dir.String() != "../../"+pkg.Pkgpath.String()+"/" {
                        filenameForUsedCheck = includingFile
                }
-               if !pkg.parse(includedMkLines, allLines, filenameForUsedCheck) {
+               if !pkg.parse(includedMkLines, allLines, filenameForUsedCheck, false) {
                        return false
                }
+               if main && pkg.seenPrefs && pkg.prefsLine == nil {
+                       pkg.prefsLine = mkline
+               }
        }
 
        if mkline.IsVarassign() {
@@ -345,7 +373,7 @@ func (pkg *Package) loadIncluded(mkline 
        }
 
        mkline.Notef("The path to the included file should be %q.",
-               mkline.PathToFile(fullIncludedFallback))
+               mkline.Rel(fullIncludedFallback))
        mkline.Explain(
                "The .include directive first searches the file relative to the including file.",
                "And if that doesn't exist, falls back to the current directory, which in the",
@@ -359,13 +387,13 @@ func (pkg *Package) loadIncluded(mkline 
 
 // resolveIncludedFile resolves Makefile variables such as ${PKGPATH} to
 // their actual values.
-func (pkg *Package) resolveIncludedFile(mkline *MkLine, includingFilename CurrPath) Path {
+func (pkg *Package) resolveIncludedFile(mkline *MkLine, includingFilename CurrPath) RelPath {
 
        // FIXME: resolveVariableRefs uses G.Pkg implicitly. It should be made explicit.
        // TODO: Try to combine resolveVariableRefs and ResolveVarsInRelativePath.
-       resolved := mkline.ResolveVarsInRelativePath(NewRelPath(mkline.IncludedFile()))
+       resolved := mkline.ResolveVarsInRelativePath(mkline.IncludedFile())
        includedText := resolveVariableRefs(nil /* XXX: or maybe some mklines? */, resolved.String())
-       includedFile := NewPath(includedText)
+       includedFile := NewRelPathString(includedText)
        if containsVarRef(includedText) {
                if trace.Tracing && !includingFilename.ContainsPath("mk") {
                        trace.Stepf("%s:%s: Skipping unresolvable include file %q.",
@@ -376,9 +404,14 @@ func (pkg *Package) resolveIncludedFile(
 
        if mkline.Basename != "buildlink3.mk" {
                if includedFile.HasSuffixPath("buildlink3.mk") {
-                       pkg.bl3[includedFile] = mkline
+                       curr := mkline.File(includedFile)
+                       if G.Pkg != nil && !curr.ContainsText("$") && !curr.IsFile() {
+                               curr = G.Pkg.File(PackagePath(includedFile))
+                       }
+                       packagePath := pkg.Rel(curr)
+                       pkg.bl3[packagePath] = mkline
                        if trace.Tracing {
-                               trace.Step1("Buildlink3 file in package: %q", includedText)
+                               trace.Stepf("Buildlink3 file in package: %q", packagePath)
                        }
                }
        }
@@ -390,9 +423,10 @@ func (pkg *Package) resolveIncludedFile(
 //
 // The includingFile is relative to the current working directory,
 // the includedFile is taken directly from the .include directive.
-func (*Package) shouldDiveInto(includingFile CurrPath, includedFile Path) bool {
+func (pkg *Package) shouldDiveInto(includingFile CurrPath, includedFile RelPath) bool {
 
        if includedFile.HasSuffixPath("bsd.pkg.mk") || IsPrefs(includedFile) {
+               pkg.seenPrefs = true
                return false
        }
 
@@ -404,7 +438,7 @@ func (*Package) shouldDiveInto(including
        return true
 }
 
-func (pkg *Package) collectSeenInclude(mkline *MkLine, includedFile Path) {
+func (pkg *Package) collectSeenInclude(mkline *MkLine, includedFile RelPath) {
        if mkline.Basename != "Makefile" {
                return
        }
@@ -444,8 +478,8 @@ func (pkg *Package) loadPlistDirs(plistF
        lines := Load(plistFilename, MustSucceed)
        ck := PlistChecker{
                pkg,
-               make(map[Path]*PlistLine),
-               make(map[Path]*PlistLine),
+               make(map[RelPath]*PlistLine),
+               make(map[RelPath]*PlistLine),
                "",
                Once{},
                false}
@@ -544,7 +578,7 @@ func (pkg *Package) checkfilePackageMake
        if noConfigureLine := vars.FirstDefinition("NO_CONFIGURE"); noConfigureLine != nil {
                if replacePerlLine := vars.FirstDefinition("REPLACE_PERL"); replacePerlLine != nil {
                        replacePerlLine.Warnf("REPLACE_PERL is ignored when NO_CONFIGURE is set (in %s).",
-                               replacePerlLine.RefTo(noConfigureLine))
+                               replacePerlLine.RelMkLine(noConfigureLine))
                }
        }
 
@@ -558,6 +592,12 @@ func (pkg *Package) checkfilePackageMake
        }
 
        pkg.redundant = NewRedundantScope()
+       pkg.redundant.IsRelevant = func(mkline *MkLine) bool {
+               if !G.Infrastructure && !G.Opts.CheckGlobal {
+                       return !G.Pkgsrc.IsInfra(mkline.Filename)
+               }
+               return true
+       }
        pkg.redundant.Check(allLines) // Updates the variables in the scope
        pkg.checkCategories()
        pkg.checkGnuConfigureUseLanguages()
@@ -573,7 +613,7 @@ func (pkg *Package) checkfilePackageMake
        if imake := vars.FirstDefinition("USE_IMAKE"); imake != nil {
                if x11 := vars.FirstDefinition("USE_X11"); x11 != nil {
                        if !x11.Filename.HasSuffixPath("mk/x11.buildlink3.mk") {
-                               imake.Notef("USE_IMAKE makes USE_X11 in %s redundant.", imake.RefTo(x11))
+                               imake.Notef("USE_IMAKE makes USE_X11 in %s redundant.", imake.RelMkLine(x11))
                        }
                }
        }
@@ -587,6 +627,17 @@ func (pkg *Package) checkfilePackageMake
 
        allLines.collectVariables()    // To get the tool definitions
        mklines.Tools = allLines.Tools // TODO: also copy the other collected data
+
+       // TODO: Checking only mklines instead of allLines ignores the
+       //  .include lines. For example, including "options.mk" does not
+       //  set Tools.SeenPrefs, but it should.
+       //
+       // See Test_Package_checkfilePackageMakefile__options_mk.
+       mklines.postLine = func(mkline *MkLine) {
+               if mkline == pkg.prefsLine {
+                       pkg.seenPrefs = true
+               }
+       }
        mklines.Check()
 
        // This check is experimental because it's not yet clear how to
@@ -823,7 +874,7 @@ func (pkg *Package) CheckVarorder(mkline
                                        }
                                        return false
                                }
-                       case many:
+                       default:
                                for varcanon() == variable.Name {
                                        interesting = interesting[1:]
                                }
@@ -890,20 +941,25 @@ func (pkg *Package) checkCategories() {
                return
        }
 
+       // FIXME: Decide what exactly this map means.
+       //  Is it "this category has been seen somewhere",
+       //  or is it "this category has definitely been added"?
        seen := map[string]*MkLine{}
        for _, mkline := range categories.vari.WriteLocations() {
                switch mkline.Op() {
                case opAssignDefault:
                        for _, category := range mkline.ValueFields(mkline.Value()) {
+                               // FIXME: This looks wrong. It should probably be replaced by
+                               //  an "if len(seen) == 0" outside the for loop.
                                if seen[category] == nil {
                                        seen[category] = mkline
                                }
                        }
-               case opAssign, opAssignAppend:
+               default:
                        for _, category := range mkline.ValueFields(mkline.Value()) {
                                if seen[category] != nil {
                                        mkline.Notef("Category %q is already added in %s.",
-                                               category, mkline.RefTo(seen[category]))
+                                               category, mkline.RelMkLine(seen[category]))
                                }
                                if seen[category] == nil {
                                        seen[category] = mkline
@@ -952,7 +1008,7 @@ func (pkg *Package) checkGnuConfigureUse
                gnuLine.Warnf(
                        "GNU_CONFIGURE almost always needs a C compiler, "+
                                "but \"c\" is not added to USE_LANGUAGES in %s.",
-                       gnuLine.RefTo(useLine))
+                       gnuLine.RelMkLine(useLine))
        }
 }
 
@@ -1136,7 +1192,7 @@ func (pkg *Package) checkPossibleDowngra
                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)
+                               change.Version(), mkline.Line.RelLocation(change.Location), pkgversion)
                        mkline.Explain(
                                "The files in doc/CHANGES-*, in which all version changes are",
                                "recorded, have a higher version number than what the package says.",
@@ -1145,7 +1201,7 @@ func (pkg *Package) checkPossibleDowngra
 
                case cmp > 0 && !isLocallyModified(mkline.Filename):
                        mkline.Notef("Package version %q is greater than the latest %q from %s.",
-                               pkgversion, change.Version(), mkline.Line.RefToLocation(change.Location))
+                               pkgversion, change.Version(), mkline.Line.RelLocation(change.Location))
                        mkline.Explain(
                                "Each update to a package should be mentioned in the doc/CHANGES file.",
                                "That file is used for the quarterly statistics of updated packages.",
@@ -1178,7 +1234,7 @@ func (pkg *Package) checkUpdate() {
 
                mkline := pkg.EffectivePkgnameLine
                cmp := pkgver.Compare(pkg.EffectivePkgversion, suggver)
-               ref := mkline.RefToLocation(sugg.Line)
+               ref := mkline.RelLocation(sugg.Line)
                switch {
 
                case cmp < 0:
@@ -1340,14 +1396,15 @@ func (pkg *Package) checkLinesBuildlink3
        }
 
        // Collect all the included buildlink3.mk files from the file.
-       includedFiles := make(map[Path]*MkLine)
+       includedFiles := make(map[PackagePath]*MkLine)
        for _, mkline := range mklines.mklines {
                if mkline.IsInclude() {
-                       includedFile := mkline.IncludedFile()
-                       if includedFile.HasSuffixPath("buildlink3.mk") {
-                               includedFiles[includedFile] = mkline
-                               if pkg.bl3[includedFile] == nil {
-                                       mkline.Warnf("%s is included by this file but not by the package.", includedFile)
+                       included := pkg.Rel(mkline.IncludedFileFull())
+                       if included.AsPath().HasSuffixPath("buildlink3.mk") {
+                               includedFiles[included] = mkline
+                               if pkg.bl3[included] == nil {
+                                       mkline.Warnf("%s is included by this file but not by the package.",
+                                               mkline.IncludedFile())
                                }
                        }
                }
@@ -1399,7 +1456,7 @@ func (pkg *Package) checkIncludeConditio
                                        "and unconditionally in %s.",
                                mkline.IncludedFile().CleanPath(),
                                dependingOn(mkline.ConditionalVars()),
-                               mkline.RefTo(other))
+                               mkline.RelMkLine(other))
 
                        explainPkgOptions(other, mkline)
                }
@@ -1414,7 +1471,7 @@ func (pkg *Package) checkIncludeConditio
                                "%q is included unconditionally here "+
                                        "and conditionally in %s%s.",
                                mkline.IncludedFile().CleanPath(),
-                               mkline.RefTo(other),
+                               mkline.RelMkLine(other),
                                dependingOn(other.ConditionalVars()))
 
                        explainPkgOptions(mkline, other)
@@ -1444,7 +1501,7 @@ func (pkg *Package) AutofixDistinfo(oldS
 // as resolved from the package's directory.
 // Variables that are known in the package are resolved, e.g. ${PKGDIR}.
 func (pkg *Package) File(relativeFileName PackagePath) CurrPath {
-       joined := pkg.dir.JoinNoClean(relativeFileName.AsPath())
+       joined := pkg.dir.JoinNoClean(NewRelPath(relativeFileName.AsPath()))
        resolved := resolveVariableRefs(nil /* XXX: or maybe some mklines? */, joined.String())
        return NewCurrPathString(resolved).CleanPath()
 }
@@ -1455,13 +1512,13 @@ func (pkg *Package) File(relativeFileNam
 //
 // Example:
 //  NewPackage("category/package").Rel("other/package") == "../../other/package"
-func (pkg *Package) Rel(filename CurrPath) Path {
-       return G.Pkgsrc.Relpath(pkg.dir, filename)
+func (pkg *Package) Rel(filename CurrPath) PackagePath {
+       return NewPackagePath(G.Pkgsrc.Relpath(pkg.dir, filename))
 }
 
 // Returns whether the given file (relative to the package directory)
 // is included somewhere in the package, either directly or indirectly.
-func (pkg *Package) Includes(filename Path) bool {
+func (pkg *Package) Includes(filename PackagePath) bool {
        return pkg.unconditionalIncludes[filename] != nil ||
                pkg.conditionalIncludes[filename] != nil
 }
@@ -1475,12 +1532,12 @@ func (pkg *Package) Includes(filename Pa
 // 2. Ensure that the entries mentioned in the ALTERNATIVES file
 // also appear in the PLIST files.
 type PlistContent struct {
-       Dirs  map[Path]*PlistLine
-       Files map[Path]*PlistLine
+       Dirs  map[RelPath]*PlistLine
+       Files map[RelPath]*PlistLine
 }
 
 func NewPlistContent() PlistContent {
        return PlistContent{
-               make(map[Path]*PlistLine),
-               make(map[Path]*PlistLine)}
+               make(map[RelPath]*PlistLine),
+               make(map[RelPath]*PlistLine)}
 }

Index: pkgsrc/pkgtools/pkglint/files/package_test.go
diff -u pkgsrc/pkgtools/pkglint/files/package_test.go:1.61 pkgsrc/pkgtools/pkglint/files/package_test.go:1.62
--- pkgsrc/pkgtools/pkglint/files/package_test.go:1.61  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/package_test.go       Sun Dec  8 00:06:38 2019
@@ -360,7 +360,7 @@ func (s *Suite) Test_Package_load__build
 
        t.SetUpCommandLine("-Wall", "--explain")
        t.SetUpPackage("multimedia/libav")
-       t.CreateFileDummyBuildlink3("multimedia/libav/buildlink3.mk",
+       t.CreateFileBuildlink3("multimedia/libav/buildlink3.mk",
                ".include \"available.mk\"")
        t.CreateFileLines("multimedia/libav/available.mk",
                MkCvsID,
@@ -897,7 +897,7 @@ func (s *Suite) Test_Package_parse__buil
                "",
                "show-var-from-builtin: .PHONY",
                "\techo ${VAR_FROM_BUILTIN} ${OTHER_VAR}")
-       t.CreateFileDummyBuildlink3("category/lib1/buildlink3.mk")
+       t.CreateFileBuildlink3("category/lib1/buildlink3.mk")
        t.CreateFileLines("category/lib1/builtin.mk",
                MkCvsID,
                "VAR_FROM_BUILTIN=\t# defined")
@@ -919,7 +919,7 @@ func (s *Suite) Test_Package_parse__incl
                ".include \"../../devel/library/buildlink3.mk\"",
                ".include \"../../lang/language/module.mk\"")
        t.SetUpPackage("devel/library")
-       t.CreateFileDummyBuildlink3("devel/library/buildlink3.mk")
+       t.CreateFileBuildlink3("devel/library/buildlink3.mk")
        t.CreateFileLines("devel/library/builtin.mk",
                MkCvsID)
        t.CreateFileLines("lang/language/module.mk",
@@ -1015,10 +1015,10 @@ func (s *Suite) Test_Package_loadInclude
        t.SetUpPackage("x11/kde-runtime4",
                ".include \"../../x11/kde-libs4/buildlink3.mk\"")
        t.SetUpPackage("x11/kde-libs4")
-       t.CreateFileDummyBuildlink3("x11/kde-libs4/buildlink3.mk",
+       t.CreateFileBuildlink3("x11/kde-libs4/buildlink3.mk",
                ".include \"../../databases/openldap/buildlink3.mk\"")
        t.SetUpPackage("databases/openldap")
-       t.CreateFileDummyBuildlink3("databases/openldap/buildlink3.mk",
+       t.CreateFileBuildlink3("databases/openldap/buildlink3.mk",
                "VAR=\tvalue",
                "VAR=\tvalue") // Provoke a warning in this file.
        t.FinishSetUp()
@@ -1090,8 +1090,9 @@ func (s *Suite) Test_Package_shouldDiveI
        t := s.Init(c)
        t.Chdir("category/package")
 
-       test := func(including CurrPath, included Path, expected bool) {
-               actual := (*Package)(nil).shouldDiveInto(including, included)
+       test := func(including CurrPath, included RelPath, expected bool) {
+               pkg := NewPackage(".")
+               actual := pkg.shouldDiveInto(including, included)
                t.CheckEquals(actual, expected)
        }
 
@@ -1184,13 +1185,13 @@ func (s *Suite) Test_Package_loadPlistDi
        pkg := NewPackage(t.File("category/package"))
        pkg.load()
 
-       var dirs []Path
+       var dirs []RelPath
        for dir := range pkg.Plist.Dirs {
                dirs = append(dirs, dir)
        }
        sort.Slice(dirs, func(i, j int) bool { return dirs[i] < dirs[j] })
 
-       t.CheckDeepEquals(dirs, []Path{"bin"})
+       t.CheckDeepEquals(dirs, []RelPath{"bin"})
 }
 
 func (s *Suite) Test_Package_loadPlistDirs(c *check.C) {
@@ -1207,13 +1208,13 @@ func (s *Suite) Test_Package_loadPlistDi
        pkg := NewPackage(t.File("category/package"))
        pkg.load()
 
-       var dirs []Path
+       var dirs []RelPath
        for dir := range pkg.Plist.Dirs {
                dirs = append(dirs, dir)
        }
        sort.Slice(dirs, func(i, j int) bool { return dirs[i] < dirs[j] })
 
-       t.CheckDeepEquals(dirs, []Path{"bin", "dir", "dir/subdir"})
+       t.CheckDeepEquals(dirs, []RelPath{"bin", "dir", "dir/subdir"})
 }
 
 func (s *Suite) Test_Package_check__files_Makefile(c *check.C) {
@@ -1438,6 +1439,70 @@ func (s *Suite) Test_Package_checkfilePa
                        "A package that downloads files should have a distinfo file.")
 }
 
+// The fonts/t1lib package has split the options handling between the
+// package Makefile and options.mk. Make sure that this situation is
+// handled correctly by pkglint.
+//
+// See "tr.SeenPrefs = true".
+func (s *Suite) Test_Package_checkfilePackageMakefile__options_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpOption("option", "An example option")
+       t.SetUpPackage("category/package",
+               ".include \"options.mk\"",
+               "",
+               ".if ${PKG_OPTIONS:Moption}",
+               ".endif")
+       t.CreateFileLines("mk/bsd.options.mk")
+       t.CreateFileLines("category/package/options.mk",
+               MkCvsID,
+               "",
+               "PKG_OPTIONS_VAR=\tPKG_OPTIONS.package",
+               "PKG_SUPPORTED_OPTIONS=\toption",
+               "",
+               ".include \"../../mk/bsd.options.mk\"",
+               "",
+               ".if ${PKG_OPTIONS:Moption}",
+               ".endif")
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Package_checkfilePackageMakefile__prefs_indirect(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpOption("option", "An example option")
+       t.SetUpPackage("category/package",
+               ".include \"common.mk\"",
+               ".if ${OPSYS} == NetBSD",
+               ".endif")
+       t.CreateFileLines("category/package/common.mk",
+               MkCvsID,
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"")
+       t.FinishSetUp()
+       pkg := NewPackage(t.File("category/package"))
+       G.Pkg = pkg
+       defer func() { G.Pkg = nil }()
+
+       files, mklines, allLines := G.Pkg.load()
+
+       t.CheckEquals(G.Pkg.seenPrefs, false)
+       t.CheckEquals(G.Pkg.prefsLine, mklines.mklines[19])
+
+       G.Pkg.check(files, mklines, allLines)
+
+       t.CheckEquals(G.Pkg.seenPrefs, true)
+       t.CheckEquals(G.Pkg.prefsLine, mklines.mklines[19])
+
+       // Since bsd.prefs.mk is included indirectly by common.mk,
+       // OPSYS may be used at load time in line 21.
+       t.CheckOutputEmpty()
+}
+
 // When a package defines PLIST_SRC, it may or may not use the
 // PLIST file from the package directory. Therefore the check
 // is skipped completely.
@@ -1995,6 +2060,27 @@ func (s *Suite) Test_Package_checkCatego
        t.CheckOutputEmpty()
 }
 
+// The := assignment operator is equivalent to the simple = operator
+// if its right-hand side does not contain references to any variables.
+func (s *Suite) Test_Package_checkCategories__eval_assignment(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "CATEGORIES:=\tcategory",
+               ".include \"included.mk\"")
+       t.CreateFileLines("category/package/included.mk",
+               MkCvsID,
+               "CATEGORIES+=\tcategory")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       G.Check(".")
+
+       t.CheckOutputLines(
+               "NOTE: included.mk:2: " +
+                       "Category \"category\" is already added in Makefile:5.")
+}
+
 func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__no_C(c *check.C) {
        t := s.Init(c)
 
@@ -2875,7 +2961,7 @@ func (s *Suite) Test_Package_checkLinesB
 
        t.SetUpPackage("category/package",
                ".include \"../../mk/motif.buildlink3.mk\"")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                ".include \"../../mk/motif.buildlink3.mk\"")
        t.CreateFileLines("mk/motif.buildlink3.mk",
                MkCvsID)
@@ -2908,12 +2994,94 @@ func (s *Suite) Test_Package_checkLinesB
                "TRACE: - (*Package).checkLinesBuildlink3Inclusion()")
 }
 
+// The file mk/ocaml.mk uses ../.. to reach PKGSRCDIR.
+// The canonical path is .. since ocaml.mk is only one directory away
+// from PKGSRCDIR.
+// Before 2019-12-07, pkglint didn't resolve the resulting path correctly.
+func (s *Suite) Test_Package_checkLinesBuildlink3Inclusion__mk_dotdot_dotdot(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("x11/ocaml-graphics",
+               ".include \"../../mk/ocaml.mk\"")
+       t.CreateFileLines("mk/ocaml.mk",
+               MkCvsID,
+               ".include \"../../lang/ocaml/buildlink3.mk\"")
+       t.CreateFileLines("lang/ocaml/buildlink3.mk",
+               MkCvsID)
+       t.Chdir(".")
+       t.FinishSetUp()
+       pkg := NewPackage("x11/ocaml-graphics")
+       G.Pkg = pkg
+
+       files, mklines, allLines := pkg.load()
+       pkg.check(files, mklines, allLines)
+
+       t.CheckDeepEquals(
+               keys(pkg.bl3),
+               []string{"../../lang/ocaml/buildlink3.mk"})
+       t.CheckOutputEmpty()
+}
+
+// Ocaml packages include ../../mk/ocaml.mk.
+// That file uses the canonical .. to reach PKGSRCDIR,
+// not the ../.. that is typically used in packages.
+func (s *Suite) Test_Package_checkLinesBuildlink3Inclusion__mk_dotdot(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("x11/ocaml-graphics",
+               ".include \"../../mk/ocaml.mk\"")
+       t.CreateFileLines("mk/ocaml.mk",
+               MkCvsID,
+               ".include \"../lang/ocaml/buildlink3.mk\"")
+       t.CreateFileLines("lang/ocaml/buildlink3.mk",
+               MkCvsID)
+       t.Chdir(".")
+       t.FinishSetUp()
+       pkg := NewPackage("x11/ocaml-graphics")
+       G.Pkg = pkg
+
+       files, mklines, allLines := pkg.load()
+       pkg.check(files, mklines, allLines)
+
+       t.CheckDeepEquals(
+               keys(pkg.bl3),
+               []string{"../../lang/ocaml/buildlink3.mk"})
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Package_checkLinesBuildlink3Inclusion__ocaml(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("x11/ocaml-graphics",
+               ".include \"../../mk/ocaml.mk\"")
+       t.CreateFileBuildlink3("x11/ocaml-graphics/buildlink3.mk",
+               ".include \"../../lang/ocaml/buildlink3.mk\"")
+       t.CreateFileLines("mk/ocaml.mk",
+               MkCvsID,
+               // Note: this is ../.. even though .. is enough.
+               ".include \"../../lang/ocaml/buildlink3.mk\"")
+       t.CreateFileLines("lang/ocaml/buildlink3.mk",
+               MkCvsID)
+       t.Chdir(".")
+       t.FinishSetUp()
+
+       G.Check("mk/ocaml.mk")
+       G.checkdirPackage("x11/ocaml-graphics")
+
+       t.CheckOutputLines(
+               // This error is only reported if the file is checked on its own.
+               // If it is checked as part of a package, both bmake and pkglint
+               // use the package path as the fallback search path.
+               "ERROR: mk/ocaml.mk:2: Relative path " +
+                       "\"../../lang/ocaml/buildlink3.mk\" does not exist.")
+}
+
 // Just for code coverage.
 func (s *Suite) Test_Package_checkLinesBuildlink3Inclusion__no_tracing(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk")
+       t.CreateFileBuildlink3("category/package/buildlink3.mk")
        t.FinishSetUp()
 
        t.DisableTracing()
@@ -2927,6 +3095,7 @@ func (s *Suite) Test_Package_checkInclud
 
        t.SetUpOption("zlib", "")
        t.SetUpPackage("category/package",
+               ".include \"../../mk/bsd.prefs.mk\"",
                ".include \"../../devel/zlib/buildlink3.mk\"",
                ".if ${OPSYS} == \"Linux\"",
                ".include \"../../sysutils/coreutils/buildlink3.mk\"",
@@ -2953,10 +3122,10 @@ func (s *Suite) Test_Package_checkInclud
        G.checkdirPackage(".")
 
        t.CheckOutputLines(
-               "WARN: Makefile:20: \"../../devel/zlib/buildlink3.mk\" is included "+
+               "WARN: Makefile:21: \"../../devel/zlib/buildlink3.mk\" is included "+
                        "unconditionally here "+
                        "and conditionally in options.mk:9 (depending on PKG_OPTIONS).",
-               "WARN: Makefile:22: \"../../sysutils/coreutils/buildlink3.mk\" is included "+
+               "WARN: Makefile:23: \"../../sysutils/coreutils/buildlink3.mk\" is included "+
                        "conditionally here (depending on OPSYS) and "+
                        "unconditionally in options.mk:11.")
 }
@@ -2980,7 +3149,7 @@ func (s *Suite) Test_Package_checkInclud
                ".if ${PKG_OPTIONS:Mzlib}",
                ".include \"../../devel/zlib/buildlink3.mk\"",
                ".endif")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                ".include \"../../devel/zlib/buildlink3.mk\"")
        t.Chdir("category/package")
        t.FinishSetUp()
@@ -3007,10 +3176,12 @@ func (s *Suite) Test_Package_checkInclud
        t.CreateFileLines("devel/zlib/buildlink3.mk",
                MkCvsID)
        t.SetUpPackage("category/package",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".if ${OPSYS} == Linux",
                ".include \"../../devel/zlib/buildlink3.mk\"",
                ".endif")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                ".include \"../../devel/zlib/buildlink3.mk\"")
        t.Chdir("category/package")
        t.FinishSetUp()
@@ -3018,7 +3189,7 @@ func (s *Suite) Test_Package_checkInclud
        G.checkdirPackage(".")
 
        t.CheckOutputLines(
-               "WARN: Makefile:21: " +
+               "WARN: Makefile:23: " +
                        "\"../../devel/zlib/buildlink3.mk\" is included conditionally here " +
                        "(depending on OPSYS) and unconditionally in buildlink3.mk:12.")
 }
@@ -3085,7 +3256,7 @@ func (s *Suite) Test_Package_checkInclud
                ".if ${PKG_OPTIONS:Mzlib}",
                ".include \"../../devel/zlib/buildlink3.mk\"",
                ".endif")
-       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
                ".include \"../../devel/zlib/buildlink3.mk\"")
        t.Chdir("category/package")
        t.FinishSetUp()
@@ -3113,6 +3284,8 @@ func (s *Suite) Test_Package_checkInclud
        t.CreateFileLines("including.mk",
                MkCvsID,
                "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".include \"included.mk\"",
                ".if ${OPSYS} == \"Linux\"",
                ".include \"included.mk\"",
@@ -3124,14 +3297,16 @@ func (s *Suite) Test_Package_checkInclud
        G.Check(".")
 
        t.CheckOutputLines(
-               "WARN: including.mk:3: \"included.mk\" is included " +
-                       "unconditionally here and conditionally in line 5 (depending on OPSYS).")
+               "WARN: including.mk:5: \"included.mk\" is included " +
+                       "unconditionally here and conditionally in line 7 (depending on OPSYS).")
 }
 
 func (s *Suite) Test_Package_checkIncludeConditionally__only_conditionally(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".if ${OPSYS} == \"Linux\"",
                ".include \"included.mk\"",
                ".endif")
@@ -3153,6 +3328,8 @@ func (s *Suite) Test_Package_checkInclud
        t.CreateFileLines("including.mk",
                MkCvsID,
                "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".if ${OPSYS} == \"Linux\"",
                ".include \"included.mk\"",
                ".endif",
@@ -3164,8 +3341,8 @@ func (s *Suite) Test_Package_checkInclud
        G.Check(".")
 
        t.CheckOutputLines(
-               "WARN: including.mk:4: \"included.mk\" is included " +
-                       "conditionally here (depending on OPSYS) and unconditionally in line 6.")
+               "WARN: including.mk:6: \"included.mk\" is included " +
+                       "conditionally here (depending on OPSYS) and unconditionally in line 8.")
 }
 
 func (s *Suite) Test_Package_checkIncludeConditionally__included_multiple_times(c *check.C) {
@@ -3176,6 +3353,8 @@ func (s *Suite) Test_Package_checkInclud
        t.CreateFileLines("including.mk",
                MkCvsID,
                "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".include \"included.mk\"",
                ".if ${OPSYS} == \"Linux\"",
                ".include \"included.mk\"",
@@ -3192,12 +3371,12 @@ func (s *Suite) Test_Package_checkInclud
        G.Check(".")
 
        t.CheckOutputLines(
-               "WARN: including.mk:3: \"included.mk\" is included "+
-                       "unconditionally here and conditionally in line 10 (depending on OPSYS).",
                "WARN: including.mk:5: \"included.mk\" is included "+
-                       "conditionally here (depending on OPSYS) and unconditionally in line 8.",
-               "WARN: including.mk:8: \"included.mk\" is included "+
-                       "unconditionally here and conditionally in line 10 (depending on OPSYS).")
+                       "unconditionally here and conditionally in line 12 (depending on OPSYS).",
+               "WARN: including.mk:7: \"included.mk\" is included "+
+                       "conditionally here (depending on OPSYS) and unconditionally in line 10.",
+               "WARN: including.mk:10: \"included.mk\" is included "+
+                       "unconditionally here and conditionally in line 12 (depending on OPSYS).")
 }
 
 // For preferences files, it doesn't matter whether they are included

Index: pkgsrc/pkgtools/pkglint/files/patches.go
diff -u pkgsrc/pkgtools/pkglint/files/patches.go:1.34 pkgsrc/pkgtools/pkglint/files/patches.go:1.35
--- pkgsrc/pkgtools/pkglint/files/patches.go:1.34       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/patches.go    Sun Dec  8 00:06:38 2019
@@ -2,10 +2,7 @@ package pkglint
 
 // Checks for patch files.
 
-import (
-       "path"
-       "strings"
-)
+import "strings"
 
 func CheckLinesPatch(lines *Lines) {
        (&PatchChecker{lines, NewLinesLexer(lines), false, false}).Check()
@@ -41,7 +38,7 @@ func (ck *PatchChecker) Check() {
                if ck.llex.SkipRegexp(rePatchUniFileDel) {
                        if m := ck.llex.NextRegexp(rePatchUniFileAdd); m != nil {
                                ck.checkBeginDiff(line, patchedFiles)
-                               ck.checkUnifiedDiff(m[1])
+                               ck.checkUnifiedDiff(NewPath(m[1]))
                                patchedFiles++
                                continue
                        }
@@ -50,7 +47,7 @@ func (ck *PatchChecker) Check() {
                }
 
                if m := ck.llex.NextRegexp(rePatchUniFileAdd); m != nil {
-                       patchedFile := m[1]
+                       patchedFile := NewPath(m[1])
                        if ck.llex.SkipRegexp(rePatchUniFileDel) {
                                ck.checkBeginDiff(line, patchedFiles)
                                ck.llex.PreviousLine().Warnf("Unified diff headers should be first ---, then +++.")
@@ -95,7 +92,7 @@ func (ck *PatchChecker) Check() {
 }
 
 // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html
-func (ck *PatchChecker) checkUnifiedDiff(patchedFile string) {
+func (ck *PatchChecker) checkUnifiedDiff(patchedFile Path) {
        isConfigure := ck.isConfigure(patchedFile)
 
        hasHunks := false
@@ -161,7 +158,7 @@ func (ck *PatchChecker) checkUnifiedDiff
        }
 
        if !hasHunks {
-               ck.llex.CurrentLine().Errorf("No patch hunks for %q.", patchedFile)
+               ck.llex.CurrentLine().Errorf("No patch hunks for %q.", patchedFile.String())
        }
 
        if !ck.llex.EOF() {
@@ -265,8 +262,8 @@ func (ck *PatchChecker) isEmptyLine(text
                hasPrefix(text, "=============")
 }
 
-func (*PatchChecker) isConfigure(filename string) bool {
-       switch path.Base(filename) {
+func (*PatchChecker) isConfigure(filename Path) bool {
+       switch filename.Base() {
        case "configure", "configure.in", "configure.ac":
                return true
        }

Index: pkgsrc/pkgtools/pkglint/files/path.go
diff -u pkgsrc/pkgtools/pkglint/files/path.go:1.4 pkgsrc/pkgtools/pkglint/files/path.go:1.5
--- pkgsrc/pkgtools/pkglint/files/path.go:1.4   Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/path.go       Sun Dec  8 00:06:38 2019
@@ -150,11 +150,11 @@ func (p Path) Replace(from, to string) P
        return Path(strings.Replace(string(p), from, to, -1))
 }
 
-func (p Path) JoinClean(s Path) Path {
+func (p Path) JoinClean(s RelPath) Path {
        return Path(path.Join(string(p), string(s)))
 }
 
-func (p Path) JoinNoClean(s Path) Path {
+func (p Path) JoinNoClean(s RelPath) Path {
        return Path(string(p) + "/" + string(s))
 }
 
@@ -198,12 +198,17 @@ func (p Path) IsAbs() bool {
 }
 
 // Rel returns the relative path from this path to the other.
-func (p Path) Rel(other Path) Path {
+//
+// The returned path is a canonical relative path.
+// It starts with a possibly empty sequence of "../",
+// followed by a possibly empty sequence of non-dotdot directories.
+// It may have a single dot at the end, which means the path goes to a directory.
+func (p Path) Rel(other Path) RelPath {
        fp := filepath.FromSlash(p.String())
        fpOther := filepath.FromSlash(other.String())
        rel, err := filepath.Rel(fp, fpOther)
        assertNil(err, "Relpath from %q to %q", p, other)
-       return NewPath(filepath.ToSlash(rel))
+       return NewRelPath(NewPath(filepath.ToSlash(rel)))
 }
 
 // CurrPath is a path that is either absolute or relative to the current
@@ -290,15 +295,15 @@ func (p CurrPath) CleanPath() CurrPath {
        return CurrPath(p.AsPath().CleanPath())
 }
 
-func (p CurrPath) JoinNoClean(other Path) CurrPath {
+func (p CurrPath) JoinNoClean(other RelPath) CurrPath {
        return CurrPath(p.AsPath().JoinNoClean(other))
 }
 
-func (p CurrPath) JoinClean(other Path) CurrPath {
+func (p CurrPath) JoinClean(other RelPath) CurrPath {
        return NewCurrPath(p.AsPath().JoinClean(other))
 }
 
-func (p CurrPath) Rel(rel CurrPath) Path {
+func (p CurrPath) Rel(rel CurrPath) RelPath {
        return p.AsPath().Rel(rel.AsPath())
 }
 
@@ -341,7 +346,7 @@ func (p CurrPath) ReadPaths() []CurrPath
        var filenames []CurrPath
        for _, info := range infos {
                if !isIgnoredFilename(info.Name()) {
-                       joined := p.JoinNoClean(NewPath(info.Name())).CleanPath()
+                       joined := p.JoinNoClean(NewRelPathString(info.Name())).CleanPath()
                        filenames = append(filenames, joined)
                }
        }
@@ -362,12 +367,17 @@ func (p CurrPath) WriteString(s string) 
 // PkgsrcPath is a path relative to the pkgsrc root.
 type PkgsrcPath string
 
-func NewPkgsrcPath(p Path) PkgsrcPath { return PkgsrcPath(p) }
+func NewPkgsrcPath(p Path) PkgsrcPath {
+       _ = NewRelPath(p)
+       return PkgsrcPath(p)
+}
 
 func (p PkgsrcPath) String() string { return string(p) }
 
 func (p PkgsrcPath) AsPath() Path { return NewPath(string(p)) }
 
+func (p PkgsrcPath) AsRelPath() RelPath { return RelPath(p) }
+
 func (p PkgsrcPath) DirClean() PkgsrcPath {
        return NewPkgsrcPath(p.AsPath().DirClean())
 }
@@ -384,28 +394,32 @@ func (p PkgsrcPath) HasPrefixPath(prefix
        return p.AsPath().HasPrefixPath(prefix)
 }
 
-func (p PkgsrcPath) JoinNoClean(other Path) PkgsrcPath {
+func (p PkgsrcPath) JoinNoClean(other RelPath) PkgsrcPath {
        return NewPkgsrcPath(p.AsPath().JoinNoClean(other))
 }
 
-func (p PkgsrcPath) JoinRel(other RelPath) PkgsrcPath {
-       return p.JoinNoClean(other.AsPath())
-}
-
 // PackagePath is a path relative to the package directory. It is used
 // for the PATCHDIR and PKGDIR variables, as well as dependencies and
 // conflicts on other packages.
 type PackagePath string
 
-func NewPackagePath(p Path) PackagePath { return PackagePath(p) }
+func NewPackagePath(p RelPath) PackagePath {
+       return PackagePath(p)
+}
+
+func NewPackagePathString(p string) PackagePath {
+       _ = NewRelPathString(p)
+       return PackagePath(p)
+}
 
 func (p PackagePath) AsPath() Path { return Path(p) }
 
+func (p PackagePath) AsRelPath() RelPath { return RelPath(p) }
+
 func (p PackagePath) String() string { return p.AsPath().String() }
 
-// TODO: try RelPath instead of Path
-func (p PackagePath) JoinNoClean(other Path) PackagePath {
-       return NewPackagePath(p.AsPath().JoinNoClean(other))
+func (p PackagePath) JoinNoClean(other RelPath) PackagePath {
+       return NewPackagePathString(p.AsPath().JoinNoClean(other).String())
 }
 
 func (p PackagePath) IsEmpty() bool { return p.AsPath().IsEmpty() }
@@ -414,14 +428,27 @@ func (p PackagePath) IsEmpty() bool { re
 // further specified.
 type RelPath string
 
-func NewRelPath(p Path) RelPath { return RelPath(p) }
+func NewRelPath(p Path) RelPath {
+       assert(!p.IsAbs())
+       return RelPath(p)
+}
 
-func NewRelPathString(p string) RelPath { return RelPath(p) }
+func NewRelPathString(p string) RelPath {
+       assert(!NewPath(p).IsAbs())
+       return RelPath(p)
+}
 
 func (p RelPath) AsPath() Path { return NewPath(string(p)) }
 
 func (p RelPath) String() string { return p.AsPath().String() }
 
+func (p RelPath) IsEmpty() bool { return p.AsPath().IsEmpty() }
+
+func (p RelPath) Split() (RelPath, string) {
+       dir, base := p.AsPath().Split()
+       return NewRelPath(dir), base
+}
+
 func (p RelPath) DirClean() RelPath { return RelPath(p.AsPath().DirClean()) }
 
 func (p RelPath) DirNoClean() RelPath {
@@ -438,11 +465,15 @@ func (p RelPath) Count() int { return p.
 
 func (p RelPath) Clean() RelPath { return NewRelPath(p.AsPath().Clean()) }
 
+func (p RelPath) CleanDot() RelPath {
+       return NewRelPath(p.AsPath().CleanDot())
+}
+
 func (p RelPath) CleanPath() RelPath {
        return RelPath(p.AsPath().CleanPath())
 }
 
-func (p RelPath) JoinNoClean(other Path) RelPath {
+func (p RelPath) JoinNoClean(other RelPath) RelPath {
        return RelPath(p.AsPath().JoinNoClean(other))
 }
 
@@ -454,6 +485,10 @@ func (p RelPath) HasPrefixPath(prefix Pa
        return p.AsPath().HasPrefixPath(prefix)
 }
 
+func (p RelPath) HasPrefixText(prefix string) bool {
+       return p.AsPath().HasPrefixText(prefix)
+}
+
 func (p RelPath) ContainsPath(sub Path) bool {
        return p.AsPath().ContainsPath(sub)
 }
@@ -465,3 +500,9 @@ func (p RelPath) ContainsText(text strin
 func (p RelPath) HasSuffixPath(suffix Path) bool {
        return p.AsPath().HasSuffixPath(suffix)
 }
+
+func (p RelPath) HasSuffixText(suffix string) bool {
+       return p.AsPath().HasSuffixText(suffix)
+}
+
+func (p RelPath) Rel(other Path) RelPath { return p.AsPath().Rel(other) }
Index: pkgsrc/pkgtools/pkglint/files/path_test.go
diff -u pkgsrc/pkgtools/pkglint/files/path_test.go:1.4 pkgsrc/pkgtools/pkglint/files/path_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/path_test.go:1.4      Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/path_test.go  Sun Dec  8 00:06:38 2019
@@ -19,9 +19,17 @@ func (s *Suite) Test_NewPath(c *check.C)
 func (s *Suite) Test_Path_String(c *check.C) {
        t := s.Init(c)
 
-       for _, p := range []string{"", "filename", "a/b", "c\\d"} {
-               t.CheckEquals(NewPath(p).String(), p)
+       test := func(p Path) {
+               t.CheckEquals(p.String(), string(p))
        }
+
+       test("")
+       test("filename")
+       test("a/b")
+
+       // No normalization takes place here.
+       // That's what NewPathSlash is for.
+       test("c\\d")
 }
 
 func (s *Suite) Test_Path_GoString(c *check.C) {
@@ -63,7 +71,6 @@ func (s *Suite) Test_Path_DirClean(c *ch
        test("dir/filename", "dir")
        test("dir/filename\\with\\backslash", "dir")
 
-       // TODO: I didn't expect that Dir would return the cleaned path.
        test("././././dir/filename", "dir")
 }
 
@@ -374,8 +381,8 @@ func (s *Suite) Test_Path_Replace(c *che
 func (s *Suite) Test_Path_JoinClean(c *check.C) {
        t := s.Init(c)
 
-       test := func(p Path, suffix Path, result Path) {
-               t.CheckEquals(p.JoinClean(suffix), result)
+       test := func(p Path, rel RelPath, result Path) {
+               t.CheckEquals(p.JoinClean(rel), result)
        }
 
        test("dir", "file", "dir/file")
@@ -387,8 +394,8 @@ func (s *Suite) Test_Path_JoinClean(c *c
 func (s *Suite) Test_Path_JoinNoClean(c *check.C) {
        t := s.Init(c)
 
-       test := func(p, suffix Path, result Path) {
-               t.CheckEquals(p.JoinNoClean(suffix), result)
+       test := func(p, rel RelPath, result RelPath) {
+               t.CheckEquals(p.JoinNoClean(rel), result)
        }
 
        test("dir", "file", "dir/file")
@@ -500,13 +507,23 @@ func (s *Suite) Test_Path_IsAbs(c *check
 func (s *Suite) Test_Path_Rel(c *check.C) {
        t := s.Init(c)
 
-       base := NewPath(".")
-       abc := NewPath("a/b/c")
-       defFile := NewPath("d/e/f/file")
-
-       t.CheckEquals(abc.Rel(defFile), NewPath("../../../d/e/f/file"))
-       t.CheckEquals(base.Rel(base), NewPath("."))
-       t.CheckEquals(abc.Rel(base), NewPath("../../../."))
+       test := func(base Path, other Path, result RelPath) {
+               t.CheckEquals(base.Rel(other), result)
+       }
+
+       test("a/b/c", "d/e/f/file", "../../../d/e/f/file")
+       test(".", ".", ".")
+
+       // The trailing dot marks the difference between a file and a directory.
+       // This is the same behavior as with filepath.Rel.
+       test("a/b/c", ".", "../../../.")
+
+       // Intermediate dotdot components are removed.
+       test("a/../b", "c/../d", "../d")
+
+       test(".", "dir/file", "dir/file")
+       test(".", "dir/subdir/", "dir/subdir")  // FIXME: missing /. at the end
+       test(".", "dir/subdir/.", "dir/subdir") // FIXME: missing /. at the end
 }
 
 func (s *Suite) Test_NewCurrPath(c *check.C) {
@@ -531,55 +548,55 @@ func (s *Suite) Test_NewCurrPathSlash(c 
        test := func(path, curr string) {
                t.CheckEquals(NewCurrPathSlash(path).String(), curr)
        }
-       testWindows := func(path, currWindows, currOther string) {
-               t.CheckEquals(
-                       NewCurrPathSlash(path).String(),
-                       condStr(runtime.GOOS == "windows", currWindows, currOther))
-       }
 
        test("filename", "filename")
        test("dir/.///file", "dir/.///file")
-
-       testWindows("\\", "/", "\\")
 }
 
 func (s *Suite) Test_NewCurrPathSlash__windows(c *check.C) {
        t := s.Init(c)
 
-       if runtime.GOOS != "windows" {
-               return
+       test := func(path, currWindows, currOther string) {
+               t.CheckEquals(
+                       NewCurrPathSlash(path).String(),
+                       condStr(runtime.GOOS == "windows", currWindows, currOther))
        }
 
-       curr := NewCurrPathSlash("dir\\.\\\\\\file")
-
-       t.CheckEquals(curr.String(), "dir/.///file")
+       test("\\", "/", "\\")
+       test("dir\\.\\\\\\file", "dir/.///file", "dir\\.\\\\\\file")
 }
 
 func (s *Suite) Test_CurrPath_GoString(c *check.C) {
        t := s.Init(c)
 
-       // Tabs in filenames are rare, probably typos.
-       curr := NewCurrPath("dir/file\t")
+       test := func(p CurrPath, str string) {
+               t.CheckEquals(p.GoString(), str)
+       }
 
-       t.CheckEquals(curr.GoString(), "\"dir/file\\t\"")
+       // Tabs in filenames are rare, probably typos.
+       test("dir/file\t", "\"dir/file\\t\"")
 }
 
 func (s *Suite) Test_CurrPath_String(c *check.C) {
        t := s.Init(c)
 
-       // Tabs in filenames are rare, probably typos.
-       curr := NewCurrPath("dir/file\t")
+       test := func(p CurrPath, str string) {
+               t.CheckEquals(p.String(), str)
+       }
 
-       t.CheckEquals(curr.String(), "dir/file\t")
+       // Tabs in filenames are rare, probably typos.
+       test("dir/file\t", "dir/file\t")
 }
 
 func (s *Suite) Test_CurrPath_AsPath(c *check.C) {
        t := s.Init(c)
 
-       // Tabs in filenames are rare, probably typos.
-       curr := NewCurrPath("dir/file\t")
+       test := func(curr CurrPath, asPath Path) {
+               t.CheckEquals(curr.AsPath(), asPath)
+       }
 
-       t.CheckEquals(curr.AsPath(), NewPath("dir/file\t"))
+       // Tabs in filenames are rare, probably typos.
+       test("dir/file\t", "dir/file\t")
 }
 
 func (s *Suite) Test_CurrPath_IsEmpty(c *check.C) {
@@ -796,8 +813,8 @@ func (s *Suite) Test_CurrPath_CleanPath(
 func (s *Suite) Test_CurrPath_JoinNoClean(c *check.C) {
        t := s.Init(c)
 
-       test := func(curr CurrPath, other Path, joined CurrPath) {
-               t.CheckEquals(curr.JoinNoClean(other), joined)
+       test := func(curr CurrPath, rel RelPath, joined CurrPath) {
+               t.CheckEquals(curr.JoinNoClean(rel), joined)
        }
 
        test("", "", "/")
@@ -808,8 +825,8 @@ func (s *Suite) Test_CurrPath_JoinNoClea
 func (s *Suite) Test_CurrPath_JoinClean(c *check.C) {
        t := s.Init(c)
 
-       test := func(curr CurrPath, other Path, joined CurrPath) {
-               t.CheckEquals(curr.JoinClean(other), joined)
+       test := func(curr CurrPath, rel RelPath, joined CurrPath) {
+               t.CheckEquals(curr.JoinClean(rel), joined)
        }
 
        test("", "", "")
@@ -820,8 +837,8 @@ func (s *Suite) Test_CurrPath_JoinClean(
 func (s *Suite) Test_CurrPath_Rel(c *check.C) {
        t := s.Init(c)
 
-       test := func(curr, suffix CurrPath, rel Path) {
-               t.CheckEquals(curr.Rel(suffix), rel)
+       test := func(curr, rel CurrPath, result RelPath) {
+               t.CheckEquals(curr.Rel(rel), result)
        }
 
        test("dir/subdir", "dir", "..")
@@ -847,7 +864,7 @@ func (s *Suite) Test_CurrPath_Rename(c *
 func (s *Suite) Test_CurrPath_Lstat(c *check.C) {
        t := s.Init(c)
 
-       testDir := func(f CurrPath, isDir bool) {
+       test := func(f CurrPath, isDir bool) {
                st, err := f.Lstat()
                assertNil(err, "Lstat")
                t.CheckEquals(st.Mode()&os.ModeDir != 0, isDir)
@@ -856,14 +873,14 @@ func (s *Suite) Test_CurrPath_Lstat(c *c
        t.CreateFileLines("subdir/file")
        t.CreateFileLines("file")
 
-       testDir(t.File("subdir"), true)
-       testDir(t.File("file"), false)
+       test(t.File("subdir"), true)
+       test(t.File("file"), false)
 }
 
 func (s *Suite) Test_CurrPath_Stat(c *check.C) {
        t := s.Init(c)
 
-       testDir := func(f CurrPath, isDir bool) {
+       test := func(f CurrPath, isDir bool) {
                st, err := f.Stat()
                assertNil(err, "Stat")
                t.CheckEquals(st.Mode()&os.ModeDir != 0, isDir)
@@ -872,8 +889,8 @@ func (s *Suite) Test_CurrPath_Stat(c *ch
        t.CreateFileLines("subdir/file")
        t.CreateFileLines("file")
 
-       testDir(t.File("subdir"), true)
-       testDir(t.File("file"), false)
+       test(t.File("subdir"), true)
+       test(t.File("file"), false)
 }
 
 func (s *Suite) Test_CurrPath_Exists(c *check.C) {
@@ -895,22 +912,32 @@ func (s *Suite) Test_CurrPath_IsFile(c *
        t := s.Init(c)
 
        t.CreateFileLines("dir/file")
+       t.Chdir(".")
 
-       t.CheckEquals(t.File("nonexistent").IsFile(), false)
-       t.CheckEquals(t.File("dir").IsFile(), false)
-       t.CheckEquals(t.File("dir/nonexistent").IsFile(), false)
-       t.CheckEquals(t.File("dir/file").IsFile(), true)
+       test := func(curr CurrPath, isFile bool) {
+               t.CheckEquals(curr.IsFile(), isFile)
+       }
+
+       test("nonexistent", false)
+       test("dir", false)
+       test("dir/nonexistent", false)
+       test("dir/file", true)
 }
 
 func (s *Suite) Test_CurrPath_IsDir(c *check.C) {
        t := s.Init(c)
 
        t.CreateFileLines("dir/file")
+       t.Chdir(".")
 
-       t.CheckEquals(t.File("nonexistent").IsDir(), false)
-       t.CheckEquals(t.File("dir").IsDir(), true)
-       t.CheckEquals(t.File("dir/nonexistent").IsDir(), false)
-       t.CheckEquals(t.File("dir/file").IsDir(), false)
+       test := func(curr CurrPath, isFile bool) {
+               t.CheckEquals(curr.IsDir(), isFile)
+       }
+
+       test("nonexistent", false)
+       test("dir", true)
+       test("dir/nonexistent", false)
+       test("dir/file", false)
 }
 
 func (s *Suite) Test_CurrPath_Chmod(c *check.C) {
@@ -938,16 +965,26 @@ func (s *Suite) Test_CurrPath_ReadDir(c 
        t.CreateFileLines("file")
        t.CreateFileLines("CVS/Entries")
        t.CreateFileLines(".git/info/exclude")
+       t.Chdir(".")
 
-       infos, err := t.File(".").ReadDir()
-
-       assertNil(err, "ReadDir")
-       var names []string
-       for _, info := range infos {
-               names = append(names, info.Name())
-       }
-
-       t.CheckDeepEquals(names, []string{".git", "CVS", "file", "subdir"})
+       test := func(curr CurrPath, entries ...string) {
+               infos, err := curr.ReadDir()
+               assertNil(err, "ReadDir")
+
+               var names []string
+               for _, info := range infos {
+                       names = append(names, info.Name())
+               }
+
+               t.CheckDeepEquals(names, entries)
+       }
+
+       test(".",
+               ".git", "CVS", "file", "subdir")
+       test("subdir",
+               "file")
+       test("CVS",
+               "Entries")
 }
 
 func (s *Suite) Test_CurrPath_ReadPaths(c *check.C) {
@@ -956,14 +993,17 @@ func (s *Suite) Test_CurrPath_ReadPaths(
        t.CreateFileLines("dir/subdir/file")
        t.CreateFileLines("dir/CVS/Entries")
        t.CreateFileLines("dir/file")
+       t.Chdir(".")
 
-       p := t.File("dir")
+       test := func(dir CurrPath, entries ...CurrPath) {
+               t.CheckDeepEquals(dir.ReadPaths(), entries)
+       }
 
-       paths := p.ReadPaths()
+       test(".",
+               "dir")
 
-       t.CheckDeepEquals(paths, []CurrPath{
-               t.File("dir/file"),
-               t.File("dir/subdir")})
+       test("dir",
+               "dir/file", "dir/subdir")
 }
 
 func (s *Suite) Test_CurrPath_Open(c *check.C) {
@@ -972,38 +1012,61 @@ func (s *Suite) Test_CurrPath_Open(c *ch
        t.CreateFileLines("filename",
                "line 1",
                "line 2")
+       t.Chdir(".")
+
+       test := func(curr CurrPath, content string) {
+               f, err := curr.Open()
+               assertNil(err, "Open")
+               defer func() { assertNil(f.Close(), "Close") }()
 
-       f, err := t.File("filename").Open()
+               var sb strings.Builder
+               n, err := io.Copy(&sb, f)
+               assertNil(err, "Copy")
 
-       assertNil(err, "Open")
-       defer func() { assertNil(f.Close(), "Close") }()
-       var sb strings.Builder
-       n, err := io.Copy(&sb, f)
-       assertNil(err, "Copy")
-       t.CheckEquals(n, int64(14))
-       t.CheckEquals(sb.String(), "line 1\nline 2\n")
+               t.CheckEquals(n, int64(len(content)))
+               t.CheckEquals(sb.String(), content)
+       }
+
+       test("filename", "line 1\nline 2\n")
 }
 
 func (s *Suite) Test_CurrPath_ReadString(c *check.C) {
        t := s.Init(c)
 
+       t.Chdir(".")
+       t.CreateFileLines("empty")
        t.CreateFileLines("filename",
                "line 1",
                "line 2")
 
-       text, err := t.File("filename").ReadString()
+       test := func(curr CurrPath, content string) {
+               text, err := curr.ReadString()
 
-       assertNil(err, "ReadString")
-       t.CheckEquals(text, "line 1\nline 2\n")
+               assertNil(err, "ReadString")
+               t.CheckEquals(text, content)
+       }
+
+       test("empty", "")
+       test("filename", "line 1\nline 2\n")
 }
 
 func (s *Suite) Test_CurrPath_WriteString(c *check.C) {
        t := s.Init(c)
 
-       err := t.File("filename").WriteString("line 1\nline 2\n")
+       t.Chdir(".")
+
+       test := func(curr CurrPath, content string, lines ...string) {
+               err := curr.WriteString(content)
+               assertNil(err, "WriteString")
 
-       assertNil(err, "WriteString")
-       t.CheckFileLines("filename",
+               t.CheckFileLines(NewRelPath(curr.AsPath()),
+                       lines...)
+       }
+
+       test("empty", "",
+               nil...)
+
+       test("filename", "line 1\nline 2\n",
                "line 1",
                "line 2")
 }
@@ -1037,80 +1100,88 @@ func (s *Suite) Test_PkgsrcPath_AsPath(c
        t.CheckEquals(p.String(), "./category/package/Makefile")
 }
 
-func (s *Suite) Test_PkgsrcPath_DirClean(c *check.C) {
+func (s *Suite) Test_PkgsrcPath_AsRelPath(c *check.C) {
        t := s.Init(c)
 
-       pp := NewPkgsrcPath("./dir/../dir/base///.")
+       pp := NewPkgsrcPath("./category/package/Makefile")
 
-       dir := pp.DirClean()
+       rel := pp.AsRelPath()
 
-       t.CheckEquals(dir, NewPkgsrcPath("dir/base"))
+       t.CheckEquals(rel.String(), "./category/package/Makefile")
 }
 
-func (s *Suite) Test_PkgsrcPath_DirNoClean(c *check.C) {
+func (s *Suite) Test_PkgsrcPath_DirClean(c *check.C) {
        t := s.Init(c)
 
-       pp := NewPkgsrcPath("./dir/../dir/base///.")
+       test := func(pp, cleaned PkgsrcPath) {
+               t.CheckEquals(pp.DirClean(), cleaned)
+       }
+
+       test("./dir/../dir/base///.", "dir/base")
+}
+
+func (s *Suite) Test_PkgsrcPath_DirNoClean(c *check.C) {
+       t := s.Init(c)
 
-       dir := pp.DirNoClean()
+       test := func(pp, cleaned PkgsrcPath) {
+               t.CheckEquals(pp.DirNoClean(), cleaned)
+       }
 
-       t.CheckEquals(dir, NewPkgsrcPath("./dir/../dir/base"))
+       test("./dir/../dir/base///.", "./dir/../dir/base")
 }
 
 func (s *Suite) Test_PkgsrcPath_Base(c *check.C) {
        t := s.Init(c)
 
-       pp := NewPkgsrcPath("dir/base///.")
-
-       base := pp.Base()
+       test := func(pp PkgsrcPath, base string) {
+               t.CheckEquals(pp.Base(), base)
+       }
 
-       t.CheckEquals(base, ".")
+       test("./dir/../dir/base///.", ".")
 }
 
 func (s *Suite) Test_PkgsrcPath_Count(c *check.C) {
        t := s.Init(c)
 
-       pp := NewPkgsrcPath("./...////dir")
-
-       count := pp.Count()
+       test := func(pp PkgsrcPath, count int) {
+               t.CheckEquals(pp.Count(), count)
+       }
 
-       t.CheckEquals(count, 2)
+       test("./...////dir", 2)
 }
 
 func (s *Suite) Test_PkgsrcPath_HasPrefixPath(c *check.C) {
        t := s.Init(c)
 
-       pp := NewPkgsrcPath("./././///prefix/suffix")
-
-       hasPrefixPath := pp.HasPrefixPath("prefix")
+       test := func(pp PkgsrcPath, prefix Path, hasPrefixPath bool) {
+               t.CheckEquals(pp.HasPrefixPath(prefix), hasPrefixPath)
+       }
 
-       t.CheckEquals(hasPrefixPath, true)
+       test("./././///prefix/suffix", "prefix", true)
 }
 
 func (s *Suite) Test_PkgsrcPath_JoinNoClean(c *check.C) {
        t := s.Init(c)
 
-       pp := NewPkgsrcPath("base///.")
-
-       joined := pp.JoinNoClean("./../rel")
+       test := func(pp PkgsrcPath, rel RelPath, joined PkgsrcPath) {
+               t.CheckEquals(pp.JoinNoClean(rel), joined)
+       }
 
-       t.CheckEquals(joined, NewPkgsrcPath("base///././../rel"))
+       test("base///.", "./../rel", "base///././../rel")
 }
 
-func (s *Suite) Test_PkgsrcPath_JoinRel(c *check.C) {
+func (s *Suite) Test_NewPackagePath(c *check.C) {
        t := s.Init(c)
 
-       pp := NewPkgsrcPath("base///.")
-
-       joined := pp.JoinRel("./../rel")
+       p := NewPackagePath("../../category/package")
 
-       t.CheckEquals(joined, NewPkgsrcPath("base///././../rel"))
+       t.CheckEquals(p.AsPath(), NewPath("../../category/package"))
 }
 
-func (s *Suite) Test_NewPackagePath(c *check.C) {
+func (s *Suite) Test_NewPackagePathString(c *check.C) {
        t := s.Init(c)
 
-       p := NewPackagePath("../../category/package")
+       p := NewPackagePathString("../../category/package")
 
        t.CheckEquals(p.AsPath(), NewPath("../../category/package"))
 }
@@ -1125,6 +1196,16 @@ func (s *Suite) Test_PackagePath_AsPath(
        t.CheckEquals(p.String(), "../../category/package/Makefile")
 }
 
+func (s *Suite) Test_PackagePath_AsRelPath(c *check.C) {
+       t := s.Init(c)
+
+       pp := NewPackagePath("./category/package/Makefile")
+
+       rel := pp.AsRelPath()
+
+       t.CheckEquals(rel.String(), "./category/package/Makefile")
+}
+
 func (s *Suite) Test_PackagePath_String(c *check.C) {
        t := s.Init(c)
 
@@ -1138,11 +1219,13 @@ func (s *Suite) Test_PackagePath_String(
 func (s *Suite) Test_PackagePath_JoinNoClean(c *check.C) {
        t := s.Init(c)
 
-       pp := NewPackagePath("../../category/package/Makefile")
+       test := func(pp PackagePath, other RelPath, joined PackagePath) {
+               t.CheckEquals(pp.JoinNoClean(other), joined)
 
-       p := pp.JoinNoClean("patches")
+       }
 
-       t.CheckEquals(p.String(), "../../category/package/Makefile/patches")
+       test("../../category/package/patches", "patch-aa",
+               "../../category/package/patches/patch-aa")
 }
 
 func (s *Suite) Test_PackagePath_IsEmpty(c *check.C) {
@@ -1192,34 +1275,63 @@ func (s *Suite) Test_RelPath_String(c *c
        t.CheckEquals(str, ".///rel")
 }
 
-func (s *Suite) Test_RelPath_DirClean(c *check.C) {
+func (s *Suite) Test_RelPath_IsEmpty(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("./dir/../dir///./file")
+       test := func(rel RelPath, isEmpty bool) {
+               t.CheckEquals(rel.IsEmpty(), isEmpty)
+       }
 
-       dir := rel.DirClean()
+       test("", true)
+       test(".", false)
+       test("/", false)
+}
+
+func (s *Suite) Test_RelPath_Split(c *check.C) {
+       t := s.Init(c)
+
+       test := func(rel RelPath, dir RelPath, base string) {
+               actualDir, actualBase := rel.Split()
+               t.CheckEquals(actualDir, dir)
+               t.CheckEquals(actualBase, base)
+       }
+
+       test("dir/file", "dir/", "file")
+       test("././///file", "././///", "file")
+
+       t.ExpectAssert(
+               func() { test("/", "/", "") })
 
-       t.CheckEquals(dir, NewRelPath("dir"))
 }
 
-func (s *Suite) Test_RelPath_DirNoClean(c *check.C) {
+func (s *Suite) Test_RelPath_DirClean(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("./dir/../dir///./file")
+       test := func(rel RelPath, dir RelPath) {
+               t.CheckEquals(rel.DirClean(), dir)
+       }
+
+       test("./dir/../dir///./file", "dir")
+}
+
+func (s *Suite) Test_RelPath_DirNoClean(c *check.C) {
+       t := s.Init(c)
 
-       dir := rel.DirNoClean()
+       test := func(rel RelPath, dir RelPath) {
+               t.CheckEquals(rel.DirNoClean(), dir)
+       }
 
-       t.CheckEquals(dir, NewRelPath("./dir/../dir"))
+       test("./dir/../dir///./file", "./dir/../dir")
 }
 
 func (s *Suite) Test_RelPath_Base(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("./dir////./file")
-
-       base := rel.Base()
+       test := func(rel RelPath, base string) {
+               t.CheckEquals(rel.Base(), base)
+       }
 
-       t.CheckEquals(base, "file")
+       test("./dir/../dir///./file", "file")
 }
 
 func (s *Suite) Test_RelPath_HasBase(c *check.C) {
@@ -1238,100 +1350,162 @@ func (s *Suite) Test_RelPath_HasBase(c *
 func (s *Suite) Test_RelPath_Parts(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("./dir/.///base")
-
-       parts := rel.Parts()
+       test := func(rel RelPath, parts ...string) {
+               t.CheckDeepEquals(rel.Parts(), parts)
+       }
 
-       t.CheckDeepEquals(parts, []string{"dir", "base"})
+       test("./dir/.///base", "dir", "base")
 }
 
 func (s *Suite) Test_RelPath_Count(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("./dir/.///base")
-
-       count := rel.Count()
+       test := func(rel RelPath, count int) {
+               t.CheckEquals(rel.Count(), count)
+       }
 
-       t.CheckDeepEquals(count, 2)
+       test("./dir/.///base", 2)
 }
 
 func (s *Suite) Test_RelPath_Clean(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("a/b/../../c/d/../../e/../f")
+       test := func(rel RelPath, cleaned RelPath) {
+               t.CheckDeepEquals(rel.Clean(), cleaned)
+       }
 
-       cleaned := rel.Clean()
+       test("a/b/../../c/d/../.././e/../f", "f")
+}
+
+func (s *Suite) Test_RelPath_CleanDot(c *check.C) {
+       t := s.Init(c)
 
-       t.CheckEquals(cleaned, NewRelPath("f"))
+       test := func(rel RelPath, cleaned RelPath) {
+               t.CheckEquals(rel.CleanDot(), cleaned)
+       }
+
+       test("a/b/../../c/d/../.././e/../f", "a/b/../../c/d/../../e/../f")
 }
 
 func (s *Suite) Test_RelPath_CleanPath(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("a/b/../../c/d/../../e/../f")
-
-       cleaned := rel.CleanPath()
+       test := func(rel RelPath, cleaned RelPath) {
+               t.CheckEquals(rel.CleanPath(), cleaned)
+       }
 
-       t.CheckEquals(cleaned, NewRelPath("a/b/../../e/../f"))
+       test("a/b/../../c/d/../.././e/../f", "a/b/../../e/../f")
 }
 
 func (s *Suite) Test_RelPath_JoinNoClean(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("basedir/.//")
-
-       joined := rel.JoinNoClean("./other")
+       test := func(rel, other, joined RelPath) {
+               t.CheckEquals(rel.JoinNoClean(other), joined)
+       }
 
-       t.CheckEquals(joined, NewRelPath("basedir/.///./other"))
+       test("basedir/.//", "./other", "basedir/.///./other")
 }
 
 func (s *Suite) Test_RelPath_Replace(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("dir/subdir/file")
-
-       replaced := rel.Replace("/", ":")
+       test := func(rel RelPath, from, to string, result RelPath) {
+               t.CheckEquals(rel.Replace(from, to), result)
+       }
 
-       t.CheckEquals(replaced, NewRelPath("dir:subdir:file"))
+       test("dir/subdir/file", "/", ":", "dir:subdir:file")
 }
 
 func (s *Suite) Test_RelPath_HasPrefixPath(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("dir/subdir/file")
+       test := func(rel RelPath, prefix Path, hasPrefixPath bool) {
+               t.CheckEquals(rel.HasPrefixPath(prefix), hasPrefixPath)
+       }
+
+       test("dir/subdir/file", "dir", true)
+       test("dir/subdir/file", "dir/sub", false)
+       test("dir/subdir/file", "subdir", false)
+}
+
+func (s *Suite) Test_RelPath_HasPrefixText(c *check.C) {
+       t := s.Init(c)
+
+       test := func(rel RelPath, prefix string, hasPrefixPath bool) {
+               t.CheckEquals(rel.HasPrefixText(prefix), hasPrefixPath)
+       }
 
-       t.CheckEquals(rel.HasPrefixPath("dir"), true)
-       t.CheckEquals(rel.HasPrefixPath("dir/sub"), false)
-       t.CheckEquals(rel.HasPrefixPath("subdir"), false)
+       test("dir/subdir/file", "dir", true)
+       test("dir/subdir/file", "dir/sub", true)
+       test("dir/subdir/file", "subdir", false)
+       test("dir/subdir/file", "super", false)
 }
 
 func (s *Suite) Test_RelPath_ContainsPath(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("dir/subdir/file")
+       test := func(rel RelPath, prefix Path, hasPrefixPath bool) {
+               t.CheckEquals(rel.ContainsPath(prefix), hasPrefixPath)
+       }
 
-       t.CheckEquals(rel.ContainsPath("dir"), true)
-       t.CheckEquals(rel.ContainsPath("dir/sub"), false)
-       t.CheckEquals(rel.ContainsPath("subdir"), true)
+       test("dir/subdir/file", "dir", true)
+       test("dir/subdir/file", "dir/sub", false)
+       test("dir/subdir/file", "subdir", true)
 }
 
 func (s *Suite) Test_RelPath_ContainsText(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("dir/subdir/file")
+       test := func(rel RelPath, prefix string, hasPrefixPath bool) {
+               t.CheckEquals(rel.ContainsText(prefix), hasPrefixPath)
+       }
 
-       t.CheckEquals(rel.ContainsText("dir"), true)
-       t.CheckEquals(rel.ContainsText("dir/sub"), true)
-       t.CheckEquals(rel.ContainsText("subdir"), true)
-       t.CheckEquals(rel.ContainsText("super"), false)
+       test("dir/subdir/file", "dir", true)
+       test("dir/subdir/file", "dir/sub", true)
+       test("dir/subdir/file", "subdir", true)
+       test("dir/subdir/file", "super", false)
 }
 
 func (s *Suite) Test_RelPath_HasSuffixPath(c *check.C) {
        t := s.Init(c)
 
-       rel := NewRelPath("dir/subdir/file")
+       test := func(rel RelPath, prefix Path, hasPrefixPath bool) {
+               t.CheckEquals(rel.HasSuffixPath(prefix), hasPrefixPath)
+       }
+
+       test("dir/subdir/file", "dir", false)
+       test("dir/subdir/file", "file", true)
+       test("dir/subdir/file", "le", false)
+       test("dir/subdir/file", "subdir/file", true)
+       test("dir/subdir/file", "subdir", false)
+}
+
+func (s *Suite) Test_RelPath_HasSuffixText(c *check.C) {
+       t := s.Init(c)
+
+       test := func(rel RelPath, prefix string, hasPrefixPath bool) {
+               t.CheckEquals(rel.HasSuffixText(prefix), hasPrefixPath)
+       }
+
+       test("dir/subdir/file", "dir", false)
+       test("dir/subdir/file", "file", true)
+       test("dir/subdir/file", "le", true)
+       test("dir/subdir/file", "subdir/file", true)
+       test("dir/subdir/file", "subdir", false)
+}
+
+func (s *Suite) Test_RelPath_Rel(c *check.C) {
+       t := s.Init(c)
+
+       test := func(base RelPath, other Path, result RelPath) {
+               t.CheckEquals(base.Rel(other), result)
+       }
+
+       test("a/b/c", "d/e/f/file", "../../../d/e/f/file")
+       test(".", ".", ".")
 
-       t.CheckEquals(rel.HasSuffixPath("file"), true)
-       t.CheckEquals(rel.HasSuffixPath("subdir/file"), true)
-       t.CheckEquals(rel.HasSuffixPath("subdir"), false)
+       // The trailing dot marks the difference between a file and a directory.
+       // This is the same behavior as with filepath.Rel.
+       test("a/b/c", ".", "../../../.")
 }
Index: pkgsrc/pkgtools/pkglint/files/vargroups.go
diff -u pkgsrc/pkgtools/pkglint/files/vargroups.go:1.4 pkgsrc/pkgtools/pkglint/files/vargroups.go:1.5
--- pkgsrc/pkgtools/pkglint/files/vargroups.go:1.4      Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/vargroups.go  Sun Dec  8 00:06:38 2019
@@ -96,7 +96,7 @@ func (ck *VargroupsChecker) init() {
 
                        if ck.registered[varname] != nil {
                                mkline.Warnf("Duplicate variable name %s, already appeared in %s.",
-                                       varname, mkline.RefTo(ck.registered[varname]))
+                                       varname, mkline.RelMkLine(ck.registered[varname]))
                        } else {
                                ck.registered[varname] = mkline
                        }
Index: pkgsrc/pkgtools/pkglint/files/vargroups_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vargroups_test.go:1.4 pkgsrc/pkgtools/pkglint/files/vargroups_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/vargroups_test.go:1.4 Sat Oct 26 09:51:48 2019
+++ pkgsrc/pkgtools/pkglint/files/vargroups_test.go     Sun Dec  8 00:06:38 2019
@@ -214,7 +214,9 @@ func (s *Suite) Test_VargroupsChecker__u
 func (s *Suite) Test_VargroupsChecker__ignore(c *check.C) {
        t := s.Init(c)
 
-       t.SetUpVartypes()
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
 
        mklines := t.NewMkLines("Makefile",
                MkCvsID,
@@ -225,6 +227,8 @@ func (s *Suite) Test_VargroupsChecker__i
                "_IGN_VARS.group=\t.CURDIR",
                "_UNDERSCORE=\t\t_", // This is not an isVargroups name.
                "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
                ".if ${PREFER_PKGSRC:U} || ${WRKOBJDIR:U}",
                ".endif")
 
@@ -236,7 +240,7 @@ func (s *Suite) Test_VargroupsChecker__i
                        "are reserved for internal pkgsrc use.",
                "WARN: Makefile:7: _UNDERSCORE is defined but not used.",
                "WARN: Makefile:7: Variable _UNDERSCORE is defined but not mentioned in the _VARGROUPS section.",
-               "WARN: Makefile:9: Variable WRKOBJDIR is used but not mentioned in the _VARGROUPS section.")
+               "WARN: Makefile:11: Variable WRKOBJDIR is used but not mentioned in the _VARGROUPS section.")
 }
 
 func (s *Suite) Test_VargroupsChecker__private_before_public(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/pkglint.1
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.1:1.59 pkgsrc/pkgtools/pkglint/files/pkglint.1:1.60
--- pkgsrc/pkgtools/pkglint/files/pkglint.1:1.59        Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.1     Sun Dec  8 00:06:38 2019
@@ -1,4 +1,4 @@
-.\"    $NetBSD: pkglint.1,v 1.59 2019/12/02 23:32:09 rillig Exp $
+.\"    $NetBSD: pkglint.1,v 1.60 2019/12/08 00:06:38 rillig Exp $
 .\"    From FreeBSD: portlint.1,v 1.8 1997/11/25 14:53:14 itojun Exp
 .\"
 .\" Copyright (c) 1997 by Jun-ichiro Itoh <itojun%itojun.org@localhost>.
Index: pkgsrc/pkgtools/pkglint/files/shell_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shell_test.go:1.59 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.60
--- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.59    Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/shell_test.go Sun Dec  8 00:06:38 2019
@@ -5,6 +5,45 @@ import (
        "strings"
 )
 
+// When pkglint is called without -Wextra, the check for unknown shell
+// commands is disabled, as it is still unreliable. As of December 2019
+// there are around 500 warnings in pkgsrc, and several of them are wrong.
+func (s *Suite) Test_SimpleCommandChecker_checkCommandStart__unknown_default(c *check.C) {
+       t := s.Init(c)
+
+       test := func(commandLineArg string, diagnostics ...string) {
+               t.SetUpCommandLine(commandLineArg)
+               mklines := t.NewMkLines("Makefile",
+                       MkCvsID,
+                       "",
+                       "MY_TOOL.i386=\t${PREFIX}/bin/tool-i386",
+                       "MY_TOOL.x86_64=\t${PREFIX}/bin/tool-x86_64",
+                       "",
+                       "pre-configure:",
+                       "\t${MY_TOOL.amd64} -e 'print 12345'",
+                       "\t${UNKNOWN_TOOL}")
+
+               mklines.Check()
+
+               t.CheckOutput(diagnostics)
+       }
+
+       t.SetUpPackage("category/package")
+       G.Pkg = NewPackage(t.File("category/package"))
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       test(".", // Override the default -Wall option.
+               nil...)
+
+       test("-Wall,no-extra",
+               nil...)
+
+       test("-Wall",
+               "WARN: Makefile:8: Unknown shell command \"${UNKNOWN_TOOL}\".",
+               "WARN: Makefile:8: UNKNOWN_TOOL is used but not defined.")
+}
+
 func (s *Suite) Test_SimpleCommandChecker_handleForbiddenCommand(c *check.C) {
        t := s.Init(c)
 
@@ -113,6 +152,33 @@ func (s *Suite) Test_SimpleCommandChecke
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_SimpleCommandChecker_handleShellBuiltin(c *check.C) {
+       t := s.Init(c)
+
+       test := func(command string, isBuiltin bool) {
+               token := NewShToken(command, NewShAtom(shtText, command, shqPlain))
+               simpleCommand := &MkShSimpleCommand{Name: token}
+               scc := NewSimpleCommandChecker(nil, simpleCommand, RunTime)
+               t.CheckEquals(scc.handleShellBuiltin(), isBuiltin)
+       }
+
+       test(":", true)
+       test("break", true)
+       test("cd", true)
+       test("continue", true)
+       test("eval", true)
+       test("exec", true)
+       test("exit", true)
+       test("export", true)
+       test("read", true)
+       test("set", true)
+       test("shift", true)
+       test("umask", true)
+       test("unset", true)
+
+       test("git", false)
+}
+
 func (s *Suite) Test_SimpleCommandChecker_checkRegexReplace(c *check.C) {
        t := s.Init(c)
 
@@ -436,8 +502,10 @@ func (s *Suite) Test_ShellLineChecker_ca
        t.SetUpTool("dirname", "", AtRunTime)
        t.SetUpTool("echo", "", AtRunTime)
        t.SetUpTool("env", "", AtRunTime)
+       t.SetUpTool("ggrep", "", AtRunTime)
        t.SetUpTool("grep", "GREP", AtRunTime)
        t.SetUpTool("sed", "", AtRunTime)
+       t.SetUpTool("gsed", "", AtRunTime)
        t.SetUpTool("touch", "", AtRunTime)
        t.SetUpTool("tr", "tr", AtRunTime)
        t.SetUpTool("true", "TRUE", AtRunTime)
@@ -474,6 +542,30 @@ func (s *Suite) Test_ShellLineChecker_ca
        test("${ECHO_MSG} \"Message\"",
                nil...)
 
+       test("${PHASE_MSG} \"Message\"",
+               nil...)
+
+       test("${STEP_MSG} \"Message\"",
+               nil...)
+
+       test("${INFO_MSG} \"Message\"",
+               nil...)
+
+       test("${WARNING_MSG} \"Message\"",
+               nil...)
+
+       test("${ERROR_MSG} \"Message\"",
+               nil...)
+
+       test("${WARNING_CAT} \"Message\"",
+               nil...)
+
+       test("${ERROR_CAT} \"Message\"",
+               nil...)
+
+       test("${DO_NADA} \"Message\"",
+               nil...)
+
        test("${FAIL_MSG} \"Failure\"",
                "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon "+
                        "(after \"${FAIL_MSG} \\\"Failure\\\"\") to separate commands.")
@@ -491,6 +583,24 @@ func (s *Suite) Test_ShellLineChecker_ca
        test("sed s,in,out,",
                nil...)
 
+       test("gsed -e s,in,out,",
+               nil...)
+
+       test("gsed s,in,out,",
+               nil...)
+
+       test("gsed s,in,out, filename",
+               "WARN: Makefile:3: Please switch to \"set -e\" mode "+
+                       "before using a semicolon (after \"gsed s,in,out, filename\") "+
+                       "to separate commands.")
+
+       test("ggrep input",
+               nil...)
+
+       test("ggrep pattern file...",
+               "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon "+
+                       "(after \"ggrep pattern file...\") to separate commands.")
+
        test("grep input",
                nil...)
 
@@ -523,6 +633,9 @@ func (s *Suite) Test_ShellLineChecker_ca
 
        test("dirname dir/file",
                nil...)
+
+       test("tr A-Z a-z",
+               nil...)
 }
 
 func (s *Suite) Test_ShellLineChecker_checkPipeExitcode(c *check.C) {
@@ -604,7 +717,7 @@ func (s *Suite) Test_ShellLineChecker_Ch
 
                // ShellLineChecker.checkVaruseToken
                //     MkLineChecker.CheckVaruse
-               //         MkLineChecker.checkVarUseQuoting
+               //         MkVarUseChecker.checkQuoting
                "WARN: filename.mk:1: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q} "+
                        "and make sure the variable appears outside of any quoting characters.")
 
@@ -1000,8 +1113,11 @@ func (s *Suite) Test_ShellLineChecker_Ch
 func (s *Suite) Test_ShellLineChecker_checkHiddenAndSuppress(c *check.C) {
        t := s.Init(c)
 
+       t.SetUpVartypes()
        t.SetUpTool("echo", "ECHO", AtRunTime)
        t.SetUpTool("ls", "LS", AtRunTime)
+       t.SetUpTool("mkdir", "MKDIR", AtRunTime)
+       t.SetUpTool("printf", "PRINTF", AtRunTime)
        mklines := t.NewMkLines("Makefile",
                MkCvsID,
                "",
@@ -1014,13 +1130,31 @@ func (s *Suite) Test_ShellLineChecker_ch
                "\t@ls 'may be hidden'",
                "",
                "pre-configure:",
-               "\t@")
+               "\t@",
+               "\t@mkdir ${WRKSRC}",
+               "\t@${DELAYED_ERROR_MSG} 'ok'",
+               "\t@${DELAYED_WARNING_MSG} 'ok'",
+               "\t@${DO_NADA} 'ok'",
+               "\t@${ECHO} 'ok'",
+               "\t@${ECHO_MSG} 'ok'",
+               "\t@${ECHO_N} 'ok'",
+               "\t@${ERROR_CAT} 'ok'",
+               "\t@${ERROR_MSG} 'ok'",
+               "\t@${FAIL_MSG} 'ok'",
+               "\t@${INFO_MSG} 'ok'",
+               "\t@${PHASE_MSG} 'ok'",
+               "\t@${PRINTF} 'ok'",
+               "\t@${SHCOMMENT} 'ok'",
+               "\t@${STEP_MSG} 'ok'",
+               "\t@${WARNING_CAT} 'ok'",
+               "\t@${WARNING_MSG} 'ok'")
 
        mklines.Check()
 
        // No warning about the hidden ls since the target names start
        // with "show-" or end with "-message".
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "WARN: Makefile:13: The shell command \"mkdir\" should not be hidden.")
 }
 
 func (s *Suite) Test_ShellLineChecker_checkHiddenAndSuppress__no_tracing(c *check.C) {
@@ -1419,6 +1553,15 @@ func (s *Suite) Test_ShellLineChecker_un
                "WARN: filename.mk:1: Backslashes should be doubled inside backticks.",
                "WARN: filename.mk:1: Double quotes inside backticks inside double quotes are error prone.",
                "WARN: filename.mk:1: Double quotes inside backticks inside double quotes are error prone.")
+
+       // The inner shell command in the backticks is malformed since it
+       // contains an unpaired backtick.
+       test("`echo \\``rest", "echo `", "rest")
+
+       // Enclosing the inner backtick in single quotes makes it valid.
+       test("`echo '\\`'`rest", "echo '`'", "rest")
+
+       test("`echo \\$$var`rest", "echo $$var", "rest")
 }
 
 func (s *Suite) Test_ShellLineChecker_unescapeBackticks__dquotBacktDquot(c *check.C) {
@@ -1541,21 +1684,66 @@ func (s *Suite) Test_ShellLineChecker_va
 func (s *Suite) Test_ShellLineChecker_checkInstallCommand(c *check.C) {
        t := s.Init(c)
 
-       mklines := t.NewMkLines("filename.mk",
-               "\t# dummy")
-       mklines.target = "do-install"
+       test := func(lines ...string) {
+               var cmds []string
+               i := 0
+               for i < len(lines) && lines[i] != "" {
+                       cmds = append(cmds, "\t"+lines[i])
+                       i++
+               }
+               diagnostics := lines[i+1:]
 
-       ck := NewShellLineChecker(mklines, mklines.mklines[0])
+               mklines := t.NewMkLines("filename.mk", cmds...)
+               mklines.target = "do-install"
 
-       ck.checkInstallCommand("sed")
+               mklines.ForEach(func(mkline *MkLine) {
+                       ck := NewShellLineChecker(mklines, mkline)
+                       ck.checkInstallCommand(mkline.ShellCommand())
+               })
 
-       t.CheckOutputLines(
-               "WARN: filename.mk:1: The shell command \"sed\" should not be used in the install phase.")
+               t.CheckOutput(diagnostics)
+       }
 
-       ck.checkInstallCommand("cp")
+       test(
+               "sed",
+               "${SED}",
+               "",
+               "WARN: filename.mk:1: The shell command \"sed\" "+
+                       "should not be used in the install phase.",
+               "WARN: filename.mk:2: The shell command \"${SED}\" "+
+                       "should not be used in the install phase.")
 
-       t.CheckOutputLines(
-               "WARN: filename.mk:1: ${CP} should not be used to install files.")
+       test(
+               "tr",
+               "${TR}",
+               "",
+               "WARN: filename.mk:1: The shell command \"tr\" "+
+                       "should not be used in the install phase.",
+               "WARN: filename.mk:2: The shell command \"${TR}\" "+
+                       "should not be used in the install phase.")
+
+       test(
+               "cp",
+               "${CP}",
+               "",
+               "WARN: filename.mk:1: ${CP} should not be used to install files.",
+               "WARN: filename.mk:2: ${CP} should not be used to install files.")
+
+       test(
+               "${INSTALL}",
+               "${INSTALL_DATA}",
+               "${INSTALL_DATA_DIR}",
+               "${INSTALL_LIB}",
+               "${INSTALL_LIB_DIR}",
+               "${INSTALL_MAN}",
+               "${INSTALL_MAN_DIR}",
+               "${INSTALL_PROGRAM}",
+               "${INSTALL_PROGRAM_DIR}",
+               "${INSTALL_SCRIPT}",
+               "${LIBTOOL}",
+               "${LN}",
+               "${PAX}",
+               "")
 }
 
 func (s *Suite) Test_ShellLineChecker_checkMultiLineComment(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/pkglint.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.68 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.69
--- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.68       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.go    Sun Dec  8 00:06:38 2019
@@ -314,8 +314,7 @@ func (pkglint *Pkglint) checkMode(dirent
 
        dir := dirent
        if !isDir {
-               // FIXME: consider DirNoClean
-               dir = dirent.DirClean()
+               dir = dirent.DirNoClean()
        }
 
        basename := dirent.Base()
@@ -325,7 +324,9 @@ func (pkglint *Pkglint) checkMode(dirent
        pkglint.Infrastructure = pkgsrcRel.HasPrefixPath("mk")
        pkgsrcdir := findPkgsrcTopdir(dir)
        if pkgsrcdir.IsEmpty() {
-               NewLineWhole(dirent).Errorf("Cannot determine the pkgsrc root directory for %q.", dir.CleanPath())
+               G.Logger.TechErrorf("",
+                       "Cannot determine the pkgsrc root directory for %q.",
+                       dirent)
                return
        }
 
@@ -360,15 +361,12 @@ func (pkglint *Pkglint) checkdirPackage(
 
        pkglint.Pkg = NewPackage(dir)
        defer func() { pkglint.Pkg = nil }()
-       pkg := pkglint.Pkg
-
-       files, mklines, allLines := pkg.load()
-       pkg.check(files, mklines, allLines)
+       pkglint.Pkg.Check()
 }
 
 // Returns the pkgsrc top-level directory, relative to the given directory.
-func findPkgsrcTopdir(dirname CurrPath) Path {
-       for _, dir := range [...]Path{".", "..", "../..", "../../.."} {
+func findPkgsrcTopdir(dirname CurrPath) RelPath {
+       for _, dir := range [...]RelPath{".", "..", "../..", "../../.."} {
                if dirname.JoinNoClean(dir).JoinNoClean("mk/bsd.pkg.mk").IsFile() {
                        return dir
                }
@@ -786,7 +784,7 @@ func (pkglint *Pkglint) loadCvsEntries(f
 
 func (pkglint *Pkglint) Abs(filename CurrPath) CurrPath {
        if !filename.IsAbs() {
-               return pkglint.cwd.JoinNoClean(filename.AsPath()).Clean()
+               return pkglint.cwd.JoinNoClean(NewRelPath(filename.AsPath())).Clean()
        }
        return filename.Clean()
 }
@@ -806,7 +804,7 @@ func (ip *InterPackage) Enable() {
 
 func (ip *InterPackage) Enabled() bool { return ip.hashes != nil }
 
-func (ip *InterPackage) Hash(alg string, filename Path, hashBytes []byte, loc *Location) *Hash {
+func (ip *InterPackage) Hash(alg string, filename RelPath, hashBytes []byte, loc *Location) *Hash {
        key := alg + ":" + filename.String()
        if otherHash := ip.hashes[key]; otherHash != nil {
                return otherHash

Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.45 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.46
--- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.45        Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go     Sun Dec  8 00:06:38 2019
@@ -147,11 +147,11 @@ func (src *Pkgsrc) loadDocChanges() {
                NewLineWhole(docDir).Fatalf("Cannot be read for loading the package changes.")
        }
 
-       var filenames []Path
+       var filenames []RelPath
        for _, file := range files {
                filename := file.Name()
                if matches(filename, `^CHANGES-20\d\d$`) && filename >= "CHANGES-2011" { // TODO: Why 2011?
-                       filenames = append(filenames, NewPath(filename))
+                       filenames = append(filenames, NewRelPathString(filename)) // FIXME: low-level API
                }
        }
 
@@ -219,13 +219,13 @@ func (src *Pkgsrc) loadDocChangesFromFil
 
                if year != "" && change.Date[0:4] != year {
                        line.Warnf("Year %q for %s does not match the filename %s.",
-                               change.Date[0:4], change.Pkgpath, filename)
+                               change.Date[0:4], change.Pkgpath.String(), line.Rel(filename))
                }
 
                if len(changes) >= 2 && year != "" {
                        if prev := changes[len(changes)-2]; change.Date < prev.Date {
                                line.Warnf("Date %q for %s is earlier than %q in %s.",
-                                       change.Date, change.Pkgpath, prev.Date, line.RefToLocation(prev.Location))
+                                       change.Date, change.Pkgpath.String(), prev.Date, line.RelLocation(prev.Location))
                                line.Explain(
                                        "The entries in doc/CHANGES should be in chronological order, and",
                                        "all dates are assumed to be in the UTC timezone, to prevent time",
@@ -343,7 +343,7 @@ func (src *Pkgsrc) checkRemovedAfterLast
                // without the wrong text. That's only because I'm too lazy loading
                // the file again, and the original text is not lying around anywhere.
                line := NewLineMulti(change.Location.Filename, int(change.Location.firstLine), int(change.Location.lastLine), "", nil)
-               line.Errorf("Package %s must either exist or be marked as removed.", change.Pkgpath)
+               line.Errorf("Package %s must either exist or be marked as removed.", change.Pkgpath.String())
        }
 }
 
@@ -412,7 +412,7 @@ func (src *Pkgsrc) loadTools() {
                        if mkline.IsInclude() {
                                includedFile := mkline.IncludedFile()
                                if !includedFile.ContainsText("/") {
-                                       toolFiles = append(toolFiles, NewRelPath(includedFile))
+                                       toolFiles = append(toolFiles, includedFile)
                                }
                        }
                }
@@ -429,9 +429,10 @@ func (src *Pkgsrc) loadTools() {
        tools.def("true", "TRUE", true, AfterPrefsMk, nil)
 
        for _, basename := range toolFiles {
-               mklines := src.LoadMk(NewPkgsrcPath("mk/tools").JoinRel(basename), MustSucceed|NotEmpty)
+               mklines := src.LoadMk(NewPkgsrcPath("mk/tools").JoinNoClean(basename), MustSucceed|NotEmpty)
                mklines.ForEach(func(mkline *MkLine) {
-                       tools.ParseToolLine(mklines, mkline, true, !mklines.indentation.IsConditional())
+                       conditional := mklines.indentation.IsConditional()
+                       tools.ParseToolLine(mklines, mkline, true, !conditional)
                })
        }
 
@@ -443,7 +444,8 @@ func (src *Pkgsrc) loadTools() {
                                varname := mkline.Varname()
                                switch varname {
                                case "USE_TOOLS":
-                                       tools.ParseToolLine(mklines, mkline, true, !mklines.indentation.IsConditional())
+                                       conditional := mklines.indentation.IsConditional()
+                                       tools.ParseToolLine(mklines, mkline, true, !conditional)
 
                                case "_BUILD_DEFS":
                                        // TODO: Compare with src.loadDefaultBuildDefs; is it redundant?
@@ -648,9 +650,10 @@ func (src *Pkgsrc) loadUntypedVars() {
                case src.vartypes.IsDefinedCanon(varcanon):
                        // Already defined, can also be a tool.
 
-               case hasPrefix(varcanon, "_"):
-                       // Variables starting with an underscore are reserved for the
-                       // infrastructure and are not available for use by packages.
+               case !matches(varcanon, `^[A-Z]`):
+                       // This filters out several unwanted variables: empty strings,
+                       // punctuation, lowercase letters (that are used in .for loops),
+                       // dotted names (that are used in ${VAR:@f@${f}@}).
 
                case contains(varcanon, "$"):
                        // Indirect, but not the usual parameterized form. Variables of
@@ -812,7 +815,7 @@ func (src *Pkgsrc) ListVersions(category
        }
        if len(names) == 0 {
                if errorIfEmpty {
-                       NewLineWhole(src.File(category)).Errorf("Cannot find package versions of %q.", re)
+                       G.Logger.TechErrorf(src.File(category), "Cannot find package versions of %q.", string(re))
                }
                src.listVersions[cacheKey] = nil
                return nil
@@ -938,6 +941,8 @@ func (src *Pkgsrc) guessVariableType(var
        case hasSuffix(varbase, "_MK"):
                // TODO: Add BtGuard for inclusion guards, since these variables may only be checked using defined().
                return plainType(BtUnknown, aclpAll)
+       case hasSuffix(varbase, "_AWK"):
+               return plainType(BtAwkCommand, aclpAll)
        case hasSuffix(varbase, "_SKIP"):
                return listType(BtPathPattern, aclpAllRuntime)
        }
@@ -970,7 +975,7 @@ func (src *Pkgsrc) checkToplevelUnusedLi
        for _, licenseFile := range src.ReadDir("licenses") {
                licenseName := licenseFile.Name()
                if !G.InterPackage.IsLicenseUsed(licenseName) {
-                       licensePath := licensesDir.JoinNoClean(NewPath(licenseName))
+                       licensePath := licensesDir.JoinNoClean(NewRelPathString(licenseName))
                        NewLineWhole(licensePath).Warnf("This license seems to be unused.")
                }
        }
@@ -1004,7 +1009,7 @@ func (src *Pkgsrc) ReadDir(dirName Pkgsr
        var relevantFiles []os.FileInfo
        for _, dirent := range files {
                name := dirent.Name()
-               if !dirent.IsDir() || !isIgnoredFilename(name) && !isEmptyDir(dir.JoinNoClean(NewPath(name))) {
+               if !dirent.IsDir() || !isIgnoredFilename(name) && !isEmptyDir(dir.JoinNoClean(NewRelPathString(name))) {
                        relevantFiles = append(relevantFiles, dirent)
                }
        }
@@ -1045,10 +1050,7 @@ func (src *Pkgsrc) Load(filename PkgsrcP
 // another cannot be computed in another way. The preferred way is to take
 // the relative filenames directly from the .include or exists() where they
 // appear.
-//
-// TODO: Invent data types for all kinds of relative paths that occur in pkgsrc
-//  and pkglint. Make sure that these paths cannot be accidentally mixed.
-func (src *Pkgsrc) Relpath(from, to CurrPath) Path {
+func (src *Pkgsrc) Relpath(from, to CurrPath) RelPath {
        cfrom := from.Clean()
        cto := to.Clean()
 
@@ -1071,7 +1073,7 @@ func (src *Pkgsrc) Relpath(from, to Curr
        }
 
        if cfrom == "." && !cto.IsAbs() {
-               return cto.Clean().AsPath()
+               return NewRelPath(cto.Clean().AsPath())
        }
 
        absFrom := G.Abs(cfrom)
@@ -1095,7 +1097,7 @@ func (src *Pkgsrc) Relpath(from, to Curr
                                relParts = append(relParts, "..")
                        }
                        relParts = append(relParts, toParts[2:]...)
-                       return NewPath(strings.Join(relParts, "/")).CleanDot()
+                       return NewRelPath(NewPath(strings.Join(relParts, "/")).CleanDot())
                }
        }
 
@@ -1107,7 +1109,7 @@ func (src *Pkgsrc) Relpath(from, to Curr
 // Example:
 //  NewPkgsrc("/usr/pkgsrc").File("distfiles") => "/usr/pkgsrc/distfiles"
 func (src *Pkgsrc) File(relativeName PkgsrcPath) CurrPath {
-       cleaned := relativeName.AsPath().Clean()
+       cleaned := NewRelPath(relativeName.AsPath()).Clean()
        if cleaned == "." {
                return src.topdir.CleanDot()
        }
@@ -1119,8 +1121,9 @@ func (src *Pkgsrc) File(relativeName Pkg
 //
 // Example:
 //  NewPkgsrc("/usr/pkgsrc").ToRel("/usr/pkgsrc/distfiles") => "distfiles"
+// FIXME: Rename to Rel.
 func (src *Pkgsrc) ToRel(filename CurrPath) PkgsrcPath {
-       return NewPkgsrcPath(src.Relpath(src.topdir, filename))
+       return NewPkgsrcPath(src.Relpath(src.topdir, filename).AsPath())
 }
 
 // IsInfra returns whether the given filename is part of the pkgsrc

Index: pkgsrc/pkgtools/pkglint/files/plist.go
diff -u pkgsrc/pkgtools/pkglint/files/plist.go:1.46 pkgsrc/pkgtools/pkglint/files/plist.go:1.47
--- pkgsrc/pkgtools/pkglint/files/plist.go:1.46 Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/plist.go      Sun Dec  8 00:06:38 2019
@@ -31,8 +31,8 @@ func CheckLinesPlist(pkg *Package, lines
 
        ck := PlistChecker{
                pkg,
-               make(map[Path]*PlistLine),
-               make(map[Path]*PlistLine),
+               make(map[RelPath]*PlistLine),
+               make(map[RelPath]*PlistLine),
                "",
                Once{},
                false}
@@ -41,8 +41,8 @@ func CheckLinesPlist(pkg *Package, lines
 
 type PlistChecker struct {
        pkg             *Package
-       allFiles        map[Path]*PlistLine
-       allDirs         map[Path]*PlistLine
+       allFiles        map[RelPath]*PlistLine
+       allDirs         map[RelPath]*PlistLine
        lastFname       string
        once            Once
        nonAsciiAllowed bool
@@ -107,7 +107,8 @@ func (ck *PlistChecker) collectFilesAndD
                        first := text[0]
                        switch {
                        case plistLineStart.Contains(first):
-                               path := NewPath(text)
+                               // FIXME: Add test for absolute path.
+                               path := NewRelPathString(text)
                                if prev := ck.allFiles[path]; prev == nil || stringSliceLess(pline.conditions, prev.conditions) {
                                        ck.allFiles[path] = pline
                                }
@@ -119,7 +120,8 @@ func (ck *PlistChecker) collectFilesAndD
                        case first == '@':
                                if m, dirname := match1(text, `^@exec \$\{MKDIR\} %D/(.*)$`); m {
                                        // FIXME: consider DirNoClean
-                                       for dir := NewPath(dirname); dir != "."; dir = dir.DirClean() {
+                                       // FIXME: Add test for absolute path.
+                                       for dir := NewRelPathString(dirname); dir != "."; dir = dir.DirClean() {
                                                ck.allDirs[dir] = pline
                                        }
                                }
@@ -263,13 +265,13 @@ func (ck *PlistChecker) checkDuplicate(p
                return
        }
 
-       prev := ck.allFiles[NewPath(text)]
+       prev := ck.allFiles[NewRelPathString(text)]
        if prev == pline || len(prev.conditions) > 0 {
                return
        }
 
        fix := pline.Autofix()
-       fix.Errorf("Duplicate filename %q, already appeared in %s.", text, pline.RefTo(prev.Line))
+       fix.Errorf("Duplicate filename %q, already appeared in %s.", text, pline.RelLine(prev.Line))
        fix.Delete()
        fix.Apply()
 }
@@ -321,8 +323,9 @@ func (ck *PlistChecker) checkPathLib(pli
 
        if contains(basename, ".a") || contains(basename, ".so") {
                if m, noext := match1(pline.text, `^(.*)(?:\.a|\.so[0-9.]*)$`); m {
-                       if laLine := ck.allFiles[NewPath(noext+".la")]; laLine != nil {
-                               pline.Warnf("Redundant library found. The libtool library is in %s.", pline.RefTo(laLine.Line))
+                       // FIXME: Add test for absolute path.
+                       if laLine := ck.allFiles[NewRelPathString(noext+".la")]; laLine != nil {
+                               pline.Warnf("Redundant library found. The libtool library is in %s.", pline.RelLine(laLine.Line))
                        }
                }
        }
@@ -354,7 +357,7 @@ func (ck *PlistChecker) checkPathMan(pli
                pline.Warnf("Unknown section %q for manual page.", section)
        }
 
-       if catOrMan == "cat" && ck.allFiles[NewPath("man/man"+section+"/"+manpage+"."+section)] == nil {
+       if catOrMan == "cat" && ck.allFiles[NewRelPathString("man/man"+section+"/"+manpage+"."+section)] == nil {
                pline.Warnf("Preformatted manual page without unformatted one.")
        }
 

Index: pkgsrc/pkgtools/pkglint/files/plist_test.go
diff -u pkgsrc/pkgtools/pkglint/files/plist_test.go:1.40 pkgsrc/pkgtools/pkglint/files/plist_test.go:1.41
--- pkgsrc/pkgtools/pkglint/files/plist_test.go:1.40    Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/plist_test.go Sun Dec  8 00:06:38 2019
@@ -687,7 +687,7 @@ func (s *Suite) Test_PlistChecker_checkP
 func (s *Suite) Test_PlistChecker_checkPathShareIcons__using_gnome_icon_theme(c *check.C) {
        t := s.Init(c)
 
-       t.CreateFileDummyBuildlink3("graphics/gnome-icon-theme/buildlink3.mk")
+       t.CreateFileBuildlink3("graphics/gnome-icon-theme/buildlink3.mk")
        t.SetUpPackage("graphics/gnome-icon-theme-extras",
                "ICON_THEMES=\tyes",
                ".include \"../../graphics/gnome-icon-theme/buildlink3.mk\"")
@@ -715,7 +715,7 @@ func (s *Suite) Test_PlistChecker_checkP
 func (s *Suite) Test_PlistChecker_checkPathShareIcons__gnome_icon_theme_itself(c *check.C) {
        t := s.Init(c)
 
-       t.CreateFileDummyBuildlink3("graphics/gnome-icon-theme/buildlink3.mk",
+       t.CreateFileBuildlink3("graphics/gnome-icon-theme/buildlink3.mk",
                "ICON_THEMES=\tyes")
        t.SetUpPackage("graphics/gnome-icon-theme",
                ".include \"../../graphics/gnome-icon-theme/buildlink3.mk\"")
Index: pkgsrc/pkgtools/pkglint/files/vartype.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype.go:1.40 pkgsrc/pkgtools/pkglint/files/vartype.go:1.41
--- pkgsrc/pkgtools/pkglint/files/vartype.go:1.40       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/vartype.go    Sun Dec  8 00:06:38 2019
@@ -17,7 +17,7 @@ func NewVartype(basicType *BasicType, op
        return &Vartype{basicType, options, aclEntries}
 }
 
-type vartypeOptions uint8
+type vartypeOptions uint16
 
 const (
        // List is a compound type, consisting of several space-separated elements.
@@ -28,18 +28,111 @@ const (
        // and as lists of arbitrary things.
        List vartypeOptions = 1 << iota
 
+       // The variable is not defined by the pkgsrc infrastructure.
+       // It follows the common naming convention, therefore its type can be guessed.
+       // Sometimes, with files and paths, this leads to wrong decisions.
        Guessed
+
+       // The variable can, or in some cases must, be defined by the package.
+       // For several of these variables, the pkgsrc infrastructure provides
+       // a reasonable default value, either in bsd.prefs.mk or in bsd.pkg.mk.
        PackageSettable
+
+       // The variable can be defined by the pkgsrc user in mk.conf.
+       // Its value is available at load time after bsd.prefs.mk has been included.
        UserSettable
+
+       // This variable is provided by either the pkgsrc infrastructure in
+       // mk/*, or by <sys.mk>, which is included at the very beginning.
+       //
+       // FIXME: Clearly distinguish between:
+       //  * sys.mk
+       //  * bsd.prefs.mk
+       //  * bsd.pkg.mk
+       //  * other parts of the pkgsrc infrastructure
+       //  * environment variables
+       //  Having all these possibilities as boolean flags is probably not
+       //  expressive enough. This is related to the scope and lifetime of
+       //  variables and should be modelled separately.
+       //
+       // See DefinedInSysMk.
        SystemProvided
+
+       // This variable may be provided in the command line by the pkgsrc
+       // user when building a package.
+       //
+       // Since the values of these variables are not written down in any
+       // file, they must not influence the generated binary packages.
+       //
+       // See UserSettable.
        CommandLineProvided
 
        // NeedsRationale marks variables that should always contain a comment
        // describing why they are set. Typical examples are NOT_FOR_* variables.
        NeedsRationale
 
+       // When something is appended to this variable, each additional
+       // value should be on a line of its own.
        OnePerLine
 
+       // AlwaysInScope is true when the variable is always available.
+       //
+       // One possibility is that the variable is defined in <sys.mk>,
+       // which means that its value is loaded even before the package
+       // Makefile is parsed.
+       //
+       // Another possibility is that the variable is local to a target,
+       // such as .TARGET or .IMPSRC.
+       //
+       // These variables may be used at load time in .if and .for
+       // directives even before bsd.prefs.mk is included.
+       //
+       // XXX: This option is related to the lifetime of the variable.
+       //  Other aspects of the lifetime are handled by ACLPermissions,
+       //  see aclpUseLoadtime.
+       AlwaysInScope
+
+       // DefinedIfInScope is true if the variable is guaranteed to be
+       // defined, provided that it is in scope.
+       //
+       // This means the variable can be used in expressions like ${VAR}
+       // without having to add the :U modifier like in ${VAR:U}.
+       //
+       // This option is independent of the lifetime of the variable,
+       // it merely expresses "if the variable is in scope, it is defined".
+       // As of December 2019, the lifetime of variables is managed by
+       // the ACLPermissions, but is incomplete.
+       //
+       // TODO: Model the lifetime and scope separately, see SystemProvided.
+       //
+       // Examples:
+       //  MACHINE_PLATFORM (from sys.mk)
+       //  PKGPATH (from bsd.prefs.mk)
+       //  PREFIX (from bsd.pkg.mk)
+       DefinedIfInScope
+
+       // NonemptyIfDefined is true if the variable is guaranteed to be
+       // nonempty, provided that the variable is in scope and defined.
+       //
+       // This is typical for system-provided variables like PKGPATH or
+       // MACHINE_PLATFORM, as well as package-settable variables like
+       // PKGNAME.
+       //
+       // This option is independent of the lifetime of the variable,
+       // it merely expresses "if the variable is in scope, it is defined".
+       // As of December 2019, the lifetime of variables is managed by
+       // the ACLPermissions, but is incomplete.
+       //
+       // TODO: Model the lifetime and scope separately, see SystemProvided.
+       //
+       // Examples:
+       //  MACHINE_PLATFORM (from sys.mk)
+       //  PKGPATH (from bsd.prefs.mk)
+       //  PREFIX (from bsd.pkg.mk)
+       //  PKGNAME (package-settable)
+       //  X11_TYPE (user-settable)
+       NonemptyIfDefined
+
        NoVartypeOptions = 0
 )
 
@@ -104,6 +197,9 @@ func (vt *Vartype) IsSystemProvided() bo
 func (vt *Vartype) IsCommandLineProvided() bool { return vt.options&CommandLineProvided != 0 }
 func (vt *Vartype) NeedsRationale() bool        { return vt.options&NeedsRationale != 0 }
 func (vt *Vartype) IsOnePerLine() bool          { return vt.options&OnePerLine != 0 }
+func (vt *Vartype) IsAlwaysInScope() bool       { return vt.options&AlwaysInScope != 0 }
+func (vt *Vartype) IsDefinedIfInScope() bool    { return vt.options&DefinedIfInScope != 0 }
+func (vt *Vartype) IsNonemptyIfInScope() bool   { return vt.options&NonemptyIfDefined != 0 }
 
 func (vt *Vartype) EffectivePermissions(basename string) ACLPermissions {
        for _, aclEntry := range vt.aclEntries {
@@ -244,7 +340,7 @@ func (vt *Vartype) IsShell() bool {
 // NeedsQ returns whether variables of this type need the :Q
 // modifier to be safely embedded in other variables or shell programs.
 //
-// Variables that can consists only of characters like A-Za-z0-9-._
+// Variables that can consist only of characters like A-Za-z0-9-._
 // don't need the :Q modifier. All others do, for safety reasons.
 func (bt *BasicType) NeedsQ() bool {
        switch bt {
@@ -368,6 +464,7 @@ var (
        BtYesNo                  = &BasicType{"YesNo", (*VartypeCheck).YesNo}
        BtYesNoIndirectly        = &BasicType{"YesNoIndirectly", (*VartypeCheck).YesNoIndirectly}
 
+       btCond    = &BasicType{".if condition", nil /* never called */}
        btForLoop = &BasicType{".for loop", nil /* never called */}
 )
 

Index: pkgsrc/pkgtools/pkglint/files/redundantscope.go
diff -u pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.9 pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.10
--- pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.9 Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/redundantscope.go     Sun Dec  8 00:06:38 2019
@@ -16,6 +16,7 @@ package pkglint
 type RedundantScope struct {
        vars        map[string]*redundantScopeVarinfo
        includePath includePath
+       IsRelevant  func(mkline *MkLine) bool
 }
 type redundantScopeVarinfo struct {
        vari         *Var
@@ -24,7 +25,7 @@ type redundantScopeVarinfo struct {
 }
 
 func NewRedundantScope() *RedundantScope {
-       return &RedundantScope{vars: make(map[string]*redundantScopeVarinfo)}
+       return &RedundantScope{make(map[string]*redundantScopeVarinfo), includePath{}, nil}
 }
 
 func (s *RedundantScope) Check(mklines *MkLines) {
@@ -86,11 +87,6 @@ func (s *RedundantScope) handleVarassign
        effOp := mkline.Op()
        value := mkline.Value()
 
-       // FIXME: Skip the whole redundancy check if the value is not known to be constant.
-       if effOp == opAssign && info.vari.Value() == value {
-               effOp = opAssignDefault
-       }
-
        if effOp == opAssignEval && value == mkline.WithoutMakeVariables(value) {
                // Maybe add support for VAR:= ${OTHER} later. This involves evaluating
                // the OTHER variable though using the appropriate scope. Oh, wait,
@@ -101,6 +97,11 @@ func (s *RedundantScope) handleVarassign
                effOp = opAssign
        }
 
+       // FIXME: Skip the whole redundancy check if the value is not known to be constant.
+       if effOp == opAssign && info.vari.Value() == value {
+               effOp = opAssignDefault
+       }
+
        switch effOp {
 
        case opAssign: // with a different value than before
@@ -196,18 +197,24 @@ func (s *RedundantScope) access(varname 
 }
 
 func (s *RedundantScope) onRedundant(redundant *MkLine, because *MkLine) {
+       if s.IsRelevant != nil && !s.IsRelevant(redundant) {
+               return
+       }
        if redundant.Op() == opAssignDefault {
                redundant.Notef("Default assignment of %s has no effect because of %s.",
-                       because.Varname(), redundant.RefTo(because))
+                       because.Varname(), redundant.RelMkLine(because))
        } else {
                redundant.Notef("Definition of %s is redundant because of %s.",
-                       because.Varname(), redundant.RefTo(because))
+                       because.Varname(), redundant.RelMkLine(because))
        }
 }
 
 func (s *RedundantScope) onOverwrite(overwritten *MkLine, by *MkLine) {
+       if s.IsRelevant != nil && !s.IsRelevant(overwritten) {
+               return
+       }
        overwritten.Warnf("Variable %s is overwritten in %s.",
-               overwritten.Varname(), overwritten.RefTo(by))
+               overwritten.Varname(), overwritten.RelMkLine(by))
        overwritten.Explain(
                "The variable definition in this line does not have an effect since",
                "it is overwritten elsewhere.",
Index: pkgsrc/pkgtools/pkglint/files/redundantscope_test.go
diff -u pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.9 pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.10
--- pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.9    Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/redundantscope_test.go        Sun Dec  8 00:06:38 2019
@@ -25,7 +25,7 @@ func (s *Suite) Test_RedundantScope__sin
        t.CheckOutputLines(
                "NOTE: file.mk:7: Default assignment of VAR.def has no effect because of line 1.",
                "NOTE: file.mk:8: Definition of VAR.asg is redundant because of line 2.",
-               "WARN: file.mk:4: Variable VAR.evl is overwritten in line 10.")
+               "NOTE: file.mk:10: Definition of VAR.evl is redundant because of line 4.")
        // TODO: "VAR.shl: is overwritten later"
 }
 
@@ -52,7 +52,7 @@ func (s *Suite) Test_RedundantScope__sin
        t.CheckOutputLines(
                "NOTE: file.mk:7: Default assignment of VAR.def has no effect because of line 1.",
                "NOTE: file.mk:8: Definition of VAR.asg is redundant because of line 2.",
-               "WARN: file.mk:4: Variable VAR.evl is overwritten in line 10.")
+               "NOTE: file.mk:10: Definition of VAR.evl is redundant because of line 4.")
        // TODO: "VAR.shl: is overwritten later"
 }
 
@@ -107,7 +107,7 @@ func (s *Suite) Test_RedundantScope__sin
        t.CheckOutputLines(
                "NOTE: file.mk:7: Default assignment of VAR.def has no effect because of line 1.",
                "NOTE: file.mk:8: Definition of VAR.asg is redundant because of line 2.",
-               "WARN: file.mk:4: Variable VAR.evl is overwritten in line 10.")
+               "NOTE: file.mk:10: Definition of VAR.evl is redundant because of line 4.")
        // TODO: "VAR.shl: is overwritten later"
 }
 
@@ -1246,7 +1246,7 @@ func (s *Suite) Test_RedundantScope__inc
                "CONFIGURE_ARGS=         two",
                "CONFIGURE_ARGS+=        three")
        t.SetUpPackage("category/dependency")
-       t.CreateFileDummyBuildlink3("category/dependency/buildlink3.mk")
+       t.CreateFileBuildlink3("category/dependency/buildlink3.mk")
        t.CreateFileLines("category/dependency/builtin.mk",
                MkCvsID,
                "CONFIGURE_ARGS.Darwin+= darwin")
@@ -1332,7 +1332,7 @@ func (s *Suite) Test_RedundantScope__eva
        NewRedundantScope().Check(mklines)
 
        t.CheckOutputLines(
-               "WARN: filename.mk:1: Variable VAR is overwritten in line 2.",
+               "NOTE: filename.mk:2: Definition of VAR is redundant because of line 1.",
                "WARN: filename.mk:2: Variable VAR is overwritten in line 3.")
 }
 
@@ -1427,6 +1427,48 @@ func (s *Suite) Test_RedundantScope__pro
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_RedundantScope__infra(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("mk/bsd.options.mk",
+               "PKG_OPTIONS:=\t# empty",
+               "PKG_OPTIONS=\t# empty")
+       t.CreateFileLines("options.mk",
+               "OUTSIDE:=\t# empty",
+               "OUTSIDE=\t# empty",
+               ".include \"mk/bsd.options.mk\"")
+
+       test := func(diagnostics ...string) {
+               mklines := t.LoadMkInclude("options.mk")
+               scope := NewRedundantScope()
+               scope.IsRelevant = func(mkline *MkLine) bool {
+                       // See checkfilePackageMakefile.
+                       if !G.Infrastructure && !G.Opts.CheckGlobal {
+                               return !G.Pkgsrc.IsInfra(mkline.Filename)
+                       }
+                       return true
+               }
+
+               scope.Check(mklines)
+
+               // No note about the redundant variable assignment in bsd.options.mk
+               // because it is part of the infrastructure, which is filtered out.
+               t.CheckOutput(diagnostics)
+       }
+
+       test(
+               "NOTE: ~/options.mk:2: " +
+                       "Definition of OUTSIDE is redundant because of line 1.")
+
+       t.SetUpCommandLine("-Cglobal")
+
+       test(
+               "NOTE: ~/options.mk:2: "+
+                       "Definition of OUTSIDE is redundant because of line 1.",
+               "NOTE: ~/mk/bsd.options.mk:2: "+
+                       "Definition of PKG_OPTIONS is redundant because of line 1.")
+}
+
 // Branch coverage for info.vari.IsConstant(). The other tests typically
 // make a variable non-constant by adding conditional assignments between
 // .if/.endif. But there are other ways. The output of shell commands is
@@ -1522,6 +1564,21 @@ func (s *Suite) Test_RedundantScope_hand
                "NOTE: main.mk:3: Definition of VAR is redundant because of redundant.mk:1.")
 }
 
+func (s *Suite) Test_RedundantScope_handleVarassign__assign_then_eval(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("mk/bsd.options.mk",
+               "PKG_OPTIONS=\t# empty",
+               "PKG_OPTIONS:=\t# empty")
+
+       scope := NewRedundantScope()
+       scope.Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: mk/bsd.options.mk:2: " +
+                       "Definition of PKG_OPTIONS is redundant because of line 1.")
+}
+
 func (s *Suite) Test_includePath_includes(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/shtokenizer.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.22 pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.23
--- pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.22   Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer.go        Sun Dec  8 00:06:38 2019
@@ -7,13 +7,13 @@ type ShTokenizer struct {
        inWord bool
 }
 
-func NewShTokenizer(line *Line, text string, emitWarnings bool) *ShTokenizer {
+func NewShTokenizer(diag Autofixer, text string, emitWarnings bool) *ShTokenizer {
        // TODO: Switching to NewMkParser is nontrivial since emitWarnings must equal (line != nil).
        // assert((line != nil) == emitWarnings)
-       if line != nil {
+       if diag != nil {
                emitWarnings = true
        }
-       mklex := NewMkLexer(text, line)
+       mklex := NewMkLexer(text, diag)
        return &ShTokenizer{mklex, false}
 }
 
@@ -69,9 +69,11 @@ func (p *ShTokenizer) ShAtom(quoting ShQ
        if atom == nil {
                lexer.Reset(mark)
                if hasPrefix(lexer.Rest(), "$${") {
-                       p.parser.line.Warnf("Unclosed shell variable starting at %q.", shorten(lexer.Rest(), 20))
+                       p.parser.Warnf("Unclosed shell variable starting at %q.", shorten(lexer.Rest(), 20))
                } else {
-                       p.parser.line.Warnf("Internal pkglint error in ShTokenizer.ShAtom at %q (quoting=%s).", lexer.Rest(), quoting)
+                       p.parser.Warnf("Internal pkglint error in ShTokenizer.ShAtom at %q (quoting=%s).",
+                               // TODO: shorten(lexer.Rest(), 20)
+                               lexer.Rest(), quoting.String())
                }
        }
        return atom
Index: pkgsrc/pkgtools/pkglint/files/vartype_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.22 pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.23
--- pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.22  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/vartype_test.go       Sun Dec  8 00:06:38 2019
@@ -150,11 +150,36 @@ func (s *Suite) Test_Vartype_MayBeAppend
 
        t.SetUpVartypes()
 
-       t.CheckEquals(G.Pkgsrc.VariableType(nil, "COMMENT").MayBeAppendedTo(), true)
-       t.CheckEquals(G.Pkgsrc.VariableType(nil, "DEPENDS").MayBeAppendedTo(), true)
-       t.CheckEquals(G.Pkgsrc.VariableType(nil, "PKG_FAIL_REASON").MayBeAppendedTo(), true)
-       t.CheckEquals(G.Pkgsrc.VariableType(nil, "CONF_FILES").MayBeAppendedTo(), true)
+       test := func(varname string, isAppendAllowed bool) {
+               vartype := G.Pkgsrc.VariableType(nil, varname)
+
+               t.CheckEquals(vartype.MayBeAppendedTo(), isAppendAllowed)
+       }
+
+       // There are several packages that append a parenthesized addition
+       // to the comment, such as "(command-line version)".
+       test("COMMENT", true)
+
+       // Appending to a list is always ok.
+       test("DEPENDS", true)
+       test("PKG_FAIL_REASON", true)
+
+       // This type is not marked as a list since it does not support
+       // appending a single element, therefore the above rule does not apply.
+       // Whenever something is appended, it must be in pairs of two words.
+       test("CONF_FILES", true)
+
+       // By convention, all variables ending in _AWK contain AWK code.
+       // It is usual to append a single rule to it, such as:
+       //  EXAMPLE_AWK+=   /condition/ { action }
+       test("EXAMPLE_AWK", true)
+
+       // This is another variable where the appended things should always
+       // come in pairs. A typical example is:
+       //  SUBST_SED.id+=  -e s,from,to,
+       test("SUBST_SED.id", true)
 }
+
 func (s *Suite) Test_Vartype_String(c *check.C) {
        t := s.Init(c)
 
@@ -164,6 +189,54 @@ func (s *Suite) Test_Vartype_String(c *c
        t.CheckEquals(vartype.String(), "Integer (command-line-provided)")
 }
 
+func (s *Suite) Test_BasicType_NeedsQ(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       test := func(varname string, isAppendAllowed bool) {
+               vartype := G.Pkgsrc.VariableType(nil, varname)
+
+               t.CheckEquals(vartype.basicType.NeedsQ(), isAppendAllowed)
+       }
+
+       test("BUILDLINK_DEPMETHOD.pkgbase", false)
+       test("CATEGORIES", false)
+       test("EXTRACT_SUFX", false)
+       test("EMUL_PLATFORM", false)
+       test("BINMODE", false)
+
+       // Typically safe, seldom used in practice.
+       test("DISTFILES", false)
+
+       // XXX: BtIdentifier is used for several other purposes
+       test("PLIST_VARS", false)
+
+       test("MAKE_JOBS", false) // XXX: What if MAKE_JOBS is undefined?
+       test("PKGREVISION", false)
+
+       // A specific platform does not have special characters.
+       // The patterns for such platforms typically do, such as
+       // x86_64-*-*.
+       test("MACHINE_GNU_PLATFORM", false)
+
+       // A specific platform does not have special characters.
+       // The patterns for such platforms typically do, such as
+       // NetBSD-*-*.
+       test("MACHINE_PLATFORM", false)
+
+       test("PERL5_PACKLIST", false)
+       test("PKG_OPTIONS_VAR", false)
+       test("PYTHON_VERSIONED_DEPENDENCIES", false)
+       test("SUBST_STAGE.id", false)
+       test("IS_BUILTIN.pkgbase", false)
+
+       test("COMMENT", true)
+       test("PKG_FAIL_REASON", true)
+       test("SUBST_MESSAGE.id", true)
+       test("CC", true)
+}
+
 func (s *Suite) Test_BasicType_HasEnum(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/tools_test.go
diff -u pkgsrc/pkgtools/pkglint/files/tools_test.go:1.23 pkgsrc/pkgtools/pkglint/files/tools_test.go:1.24
--- pkgsrc/pkgtools/pkglint/files/tools_test.go:1.23    Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/tools_test.go Sun Dec  8 00:06:38 2019
@@ -18,6 +18,37 @@ func (s *Suite) Test_Tool_UsableAtLoadTi
        t.CheckEquals(run.UsableAtLoadTime(true), false)
 }
 
+func (s *Suite) Test_Tool_UsableAtLoadTime__pkgconfig_builtin_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--explain")
+       t.SetUpTool("tool1", "TOOL1", AfterPrefsMk)
+       t.SetUpTool("tool2", "TOOL2", AfterPrefsMk)
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("mk/buildlink3/pkgconfig-builtin.mk")
+       t.FinishSetUp()
+
+       mklines := t.SetUpFileMkLines("category/package/filename.mk",
+               MkCvsID,
+               "",
+               "OUT!=\t${TOOL1} ${OUT}",
+               "",
+               ".include \"../../mk/buildlink3/pkgconfig-builtin.mk\"",
+               "",
+               "OUT!=\t${TOOL2} ${OUT}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/filename.mk:3: " +
+                       "To use the tool ${TOOL1} at load time, " +
+                       "bsd.prefs.mk has to be included before.")
+       // Maybe an explanation might help here.
+       // There is surprisingly few feedback on any of the explanations
+       // though (about 0 in 10 years), therefore I don't even know
+       // whether anyone reads them.
+}
+
 func (s *Suite) Test_Tool_UsableAtRunTime(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/varalignblock.go
diff -u pkgsrc/pkgtools/pkglint/files/varalignblock.go:1.10 pkgsrc/pkgtools/pkglint/files/varalignblock.go:1.11
--- pkgsrc/pkgtools/pkglint/files/varalignblock.go:1.10 Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/varalignblock.go      Sun Dec  8 00:06:38 2019
@@ -429,7 +429,6 @@ func (va *VaralignBlock) realign(info *v
                assert(*indentDiffSet)
                va.realignMultiFollow(info, newWidth, *indentDiff)
        } else {
-               assert(!*indentDiffSet)
                va.realignSingle(info, newWidth)
        }
 }

Index: pkgsrc/pkgtools/pkglint/files/vardefs.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.80 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.81
--- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.80       Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/vardefs.go    Sun Dec  8 00:06:38 2019
@@ -56,7 +56,7 @@ func (reg *VarTypeRegistry) DefineType(v
 }
 
 func (reg *VarTypeRegistry) Define(varname string, basicType *BasicType, options vartypeOptions, aclEntries ...ACLEntry) {
-       m, varbase, varparam := match2(varname, `^([A-Z_.][A-Z0-9_]*|@)(|\*|\.\*)$`)
+       m, varbase, varparam := match2(varname, `^([A-Z_.][A-Z0-9_]*|@|\.newline)(|\*|\.\*)$`)
        assert(m) // invalid variable name
 
        vartype := NewVartype(basicType, options, aclEntries...)
@@ -94,7 +94,12 @@ func (reg *VarTypeRegistry) DefineParse(
 //  - why the predefined permission set is not good enough
 //  - which packages need this custom permission set.
 func (reg *VarTypeRegistry) acl(varname string, basicType *BasicType, options vartypeOptions, aclEntries ...string) {
+
+       // If this assertion fails, it usually means that
+       // the test calls SetUpVartypes redundantly.
+       // For example, it is called by SetUpPkgsrc or SetUpPackage as well.
        assertf(!reg.IsDefinedExact(varname), "Variable %q must only be defined once.", varname)
+
        reg.DefineParse(varname, basicType, options, aclEntries...)
 }
 
@@ -232,9 +237,9 @@ func (reg *VarTypeRegistry) pkglistbl3ra
 //
 // TODO: These timing issues should be handled separately from the permissions.
 //  They can be made more precise.
-func (reg *VarTypeRegistry) sys(varname string, basicType *BasicType) {
+func (reg *VarTypeRegistry) sys(varname string, basicType *BasicType, options ...vartypeOptions) {
        reg.acl(varname, basicType,
-               SystemProvided,
+               reg.options(SystemProvided, options),
                "buildlink3.mk: none",
                "*: use")
 }
@@ -290,15 +295,17 @@ func (reg *VarTypeRegistry) usrpkg(varna
 }
 
 // sysload declares a system-provided variable that may already be used at load time.
-func (reg *VarTypeRegistry) sysload(varname string, basicType *BasicType) {
+//
+// TODO: For some of these variables, bsd.prefs.mk has to be included first.
+func (reg *VarTypeRegistry) sysload(varname string, basicType *BasicType, options ...vartypeOptions) {
        reg.acl(varname, basicType,
-               SystemProvided,
+               reg.options(SystemProvided, options),
                "*: use, use-loadtime")
 }
 
-func (reg *VarTypeRegistry) sysloadlist(varname string, basicType *BasicType) {
+func (reg *VarTypeRegistry) sysloadlist(varname string, basicType *BasicType, options ...vartypeOptions) {
        reg.acl(varname, basicType,
-               List|SystemProvided,
+               reg.options(List|SystemProvided, options),
                "*: use, use-loadtime")
 }
 
@@ -313,9 +320,9 @@ func (reg *VarTypeRegistry) bl3list(varn
 
 // cmdline declares a variable that is defined on the command line. There
 // are only few variables of this type, such as PKG_DEBUG_LEVEL.
-func (reg *VarTypeRegistry) cmdline(varname string, basicType *BasicType) {
+func (reg *VarTypeRegistry) cmdline(varname string, basicType *BasicType, options ...vartypeOptions) {
        reg.acl(varname, basicType,
-               CommandLineProvided,
+               reg.options(CommandLineProvided, options),
                "buildlink3.mk, builtin.mk: none",
                "*: use, use-loadtime")
 }
@@ -471,6 +478,15 @@ func (reg *VarTypeRegistry) enumFromFile
        return enum(strings.Join(relevant, " "))
 }
 
+func (reg *VarTypeRegistry) options(base vartypeOptions, additional []vartypeOptions) vartypeOptions {
+       assert(len(additional) <= 1)
+       opts := base
+       if len(additional) > 0 {
+               opts |= additional[0]
+       }
+       return opts
+}
+
 // 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.
@@ -507,7 +523,10 @@ func (reg *VarTypeRegistry) Init(src *Pk
                `(.*)\.mk$`, "$1",
                "Cygwin DragonFly FreeBSD Linux NetBSD SunOS")
 
-       // Last synced with mk/defaults/mk.conf revision 1.300 (fe3d998769f).
+       // TODO: Only mark those variables as user-settable that actually influence
+       //  the generated packages. For example, UPDATE_TARGET doesn't.
+
+       // Last synced with mk/defaults/mk.conf revision 1.300 (abbf617a26f3).
        reg.usr("USE_CWRAPPERS", enum("yes no auto"))
        reg.usr("ALLOW_VULNERABLE_PACKAGES", BtYes)
        reg.usrlist("AUDIT_PACKAGES_FLAGS", BtShellWord)
@@ -552,7 +571,7 @@ 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.
        reg.acl("X11_TYPE", enum("modular native"),
-               UserSettable,
+               UserSettable|DefinedIfInScope|NonemptyIfDefined,
                "*: use, use-loadtime")
        reg.acl("X11BASE", BtPathname,
                UserSettable,
@@ -828,7 +847,13 @@ func (reg *VarTypeRegistry) Init(src *Pk
        //  subst, buildlink3, checks. This will make them easier to
        //  analyze and align the permissions.
 
-       reg.sysload(".CURDIR", BtPathname)
+       // TODO: Determine AlwaysInScope automatically based on sys.mk.
+       // TODO: Determine DefinedIfInScope automatically.
+       // TODO: Determine NonemptyIfDefined automatically.
+
+       reg.sysload(".newline", BtMessage, AlwaysInScope|DefinedIfInScope|NonemptyIfDefined)
+       reg.sysloadlist(".ALLSRC", BtPathname, AlwaysInScope)
+       reg.sysload(".CURDIR", BtPathname, AlwaysInScope|DefinedIfInScope|NonemptyIfDefined)
        reg.sysload(".IMPSRC", BtPathname)
        reg.sys(".TARGET", BtPathname)
        reg.sys("@", BtPathname)
@@ -836,8 +861,8 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.pkg("ALTERNATIVES_FILE", BtFilename)
        reg.pkglist("ALTERNATIVES_SRC", BtPathname)
        reg.pkg("APACHE_MODULE", BtYes)
-       reg.sys("AR", BtShellCommand)
-       reg.sys("AS", BtShellCommand)
+       reg.sys("AR", BtShellCommand, AlwaysInScope|DefinedIfInScope|NonemptyIfDefined)
+       reg.sys("AS", BtShellCommand, AlwaysInScope|DefinedIfInScope|NonemptyIfDefined)
        reg.pkglist("AUTOCONF_REQD", BtVersion)
        reg.pkglist("AUTOMAKE_OVERRIDE", BtPathPattern)
        reg.pkglist("AUTOMAKE_REQD", BtVersion)
@@ -850,9 +875,9 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.syslist("BDB_LIBS", BtLdFlag)
        reg.sys("BDB_TYPE", enum("db1 db2 db3 db4 db5 db6"))
        reg.syslist("BIGENDIANPLATFORMS", BtMachinePlatformPattern)
-       reg.sys("BINGRP", BtUserGroupName)
-       reg.sys("BINMODE", BtFileMode)
-       reg.sys("BINOWN", BtUserGroupName)
+       reg.sysload("BINGRP", BtUserGroupName, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("BINMODE", BtFileMode, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("BINOWN", BtUserGroupName, DefinedIfInScope|NonemptyIfDefined)
        reg.pkglist("BOOTSTRAP_DEPENDS", BtDependencyWithPath)
        reg.pkg("BOOTSTRAP_PKG", BtYesNo)
        reg.pkglistone("BROKEN", BtShellWord)
@@ -986,7 +1011,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.sys("BUILTIN_X11_TYPE", BtUnknown)
        reg.sys("BUILTIN_X11_VERSION", BtUnknown)
        reg.pkglist("CATEGORIES", BtCategory)
-       reg.sysload("CC_VERSION", BtMessage)
+       reg.sysload("CC_VERSION", BtMessage, DefinedIfInScope|NonemptyIfDefined)
        reg.sysload("CC", BtShellCommand)
        reg.pkglistbl3("CFLAGS", BtCFlag)   // may also be changed by the user
        reg.pkglistbl3("CFLAGS.*", BtCFlag) // may also be changed by the user
@@ -1084,9 +1109,9 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.acllist("DL_LIBS", BtLdFlag,
                PackageSettable,
                "*: append, use")
-       reg.sys("DOCOWN", BtUserGroupName)
-       reg.sys("DOCGRP", BtUserGroupName)
-       reg.sys("DOCMODE", BtFileMode)
+       reg.sysload("DOCOWN", BtUserGroupName, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("DOCGRP", BtUserGroupName, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("DOCMODE", BtFileMode, DefinedIfInScope|NonemptyIfDefined)
        reg.sys("DOWNLOADED_DISTFILE", BtPathname)
        reg.sys("DO_NADA", BtShellCommand)
        reg.pkg("DYNAMIC_SITES_CMD", BtShellCommand)
@@ -1251,17 +1276,32 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.sys("LINK.*", BtShellCommand)
        reg.sys("LINKER_RPATH_FLAG", BtShellWord)
        reg.syslist("LITTLEENDIANPLATFORMS", BtMachinePlatformPattern)
-       reg.sys("LOWER_OPSYS", BtIdentifier)
-       reg.sys("LOWER_VENDOR", BtIdentifier)
-       reg.syslist("LP64PLATFORMS", BtMachinePlatformPattern)
+       reg.sysload("LOWER_OPSYS", BtIdentifier, NonemptyIfDefined)
+       reg.sysload("LOWER_VENDOR", BtIdentifier, NonemptyIfDefined)
+       reg.sysloadlist("LP64PLATFORMS", BtMachinePlatformPattern, DefinedIfInScope|NonemptyIfDefined)
        reg.pkglist("LTCONFIG_OVERRIDE", BtPathPattern)
-       reg.sysload("MACHINE_ARCH", enumMachineArch)
-       reg.sysload("MACHINE_GNU_ARCH", enumMachineGnuArch)
-       reg.sysload("MACHINE_GNU_PLATFORM", BtMachineGnuPlatform)
-       reg.sysload("MACHINE_PLATFORM", BtMachinePlatform)
+
+       // See devel/bmake/files/main.c:/Var_Set."MACHINE_ARCH"/.
+       reg.sysload("MACHINE_ARCH", enumMachineArch, AlwaysInScope|DefinedIfInScope|NonemptyIfDefined)
+
+       // From mk/endian.mk, determined by a shell program that compiles
+       // a C program. That's just too much for pkglint to analyze.
+       reg.sysload("MACHINE_ENDIAN", enum("big little unknown"), DefinedIfInScope|NonemptyIfDefined)
+
+       reg.sysload("MACHINE_GNU_ARCH", enumMachineGnuArch, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("MACHINE_GNU_PLATFORM", BtMachineGnuPlatform, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("MACHINE_PLATFORM", BtMachinePlatform, DefinedIfInScope|NonemptyIfDefined)
        reg.pkg("MAINTAINER", BtMailAddress)
-       reg.sysload("MAKE", BtShellCommand)
+
+       // See devel/bmake/files/main.c:/Var_Set."MAKE"/.
+       reg.sysload("MAKE", BtShellCommand, AlwaysInScope|DefinedIfInScope|NonemptyIfDefined)
+
+       // System-provided, but packages may extend them.
+       // TODO: This needs a special declaration since the very first
+       //  assignment in a package must use += as well.
+       // See devel/bmake/files/main.c:/Var_Set."MAKEFLAGS"/.
        reg.pkglist("MAKEFLAGS", BtShellWord)
+
        reg.pkglistbl3("MAKEVARS", BtVariableName)
        reg.pkglist("MAKE_DIRS", BtPathname)
        reg.pkglist("MAKE_DIRS_PERMS", BtPerms)
@@ -1274,9 +1314,9 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.pkg("MAKE_PROGRAM", BtShellCommand)
        reg.pkg("MANCOMPRESSED", BtYesNo)
        reg.pkg("MANCOMPRESSED_IF_MANZ", BtYes)
-       reg.sys("MANGRP", BtUserGroupName)
-       reg.sys("MANMODE", BtFileMode)
-       reg.sys("MANOWN", BtUserGroupName)
+       reg.sysload("MANGRP", BtUserGroupName, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("MANMODE", BtFileMode, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("MANOWN", BtUserGroupName, DefinedIfInScope|NonemptyIfDefined)
        reg.pkglist("MASTER_SITES", BtFetchURL)
 
        for _, filename := range []PkgsrcPath{"mk/fetch/sites.mk", "mk/fetch/fetch.mk"} {
@@ -1322,9 +1362,10 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.pkglistrat("ONLY_FOR_COMPILER", compilers)
        reg.pkglistrat("ONLY_FOR_PLATFORM", BtMachinePlatformPattern)
        reg.pkgrat("ONLY_FOR_UNPRIVILEGED", BtYesNo)
-       reg.sysload("OPSYS", platforms)
+       reg.sysload("OPSYS", platforms, DefinedIfInScope|NonemptyIfDefined)
        reg.pkglistbl3("OPSYSVARS", BtVariableName)
        reg.pkg("OSVERSION_SPECIFIC", BtYes)
+       reg.sysload("OS_VARIANT", BtIdentifier, DefinedIfInScope)
        reg.sysload("OS_VERSION", BtVersion)
        reg.sysload("OSX_VERSION", BtVersion) // See mk/platform/Darwin.mk.
        reg.pkg("OVERRIDE_DIRDEPTH*", BtInteger)
@@ -1342,7 +1383,10 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.pkg("PATCH_DIST_STRIP*", BtShellWord)
        reg.pkglist("PATCH_SITES", BtFetchURL)
        reg.pkg("PATCH_STRIP", BtShellWord)
-       reg.sysload("PATH", BtPathlist)   // From the PATH environment variable.
+
+       // From the PATH environment variable.
+       reg.sysload("PATH", BtPathlist, AlwaysInScope|DefinedIfInScope|NonemptyIfDefined)
+
        reg.sys("PAXCTL", BtShellCommand) // See mk/pax.mk.
        reg.pkglist("PERL5_PACKLIST", BtPerl5Packlist)
        reg.pkg("PERL5_PACKLIST_DIR", BtPathname)
@@ -1394,7 +1438,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.sys("PKGLOCALEDIR", BtPathname)
        reg.pkg("PKGNAME", BtPkgname)
        reg.sys("PKGNAME_NOREV", BtPkgname)
-       reg.sysload("PKGPATH", BtPkgpath)
+       reg.sysload("PKGPATH", BtPkgpath, DefinedIfInScope|NonemptyIfDefined)
        reg.sys("PKGREPOSITORY", BtUnknown)
        // This variable is special in that it really only makes sense to
        // be set in a package Makefile.
@@ -1402,7 +1446,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.acl("PKGREVISION", BtPkgrevision,
                PackageSettable,
                "Makefile: set")
-       reg.sys("PKGSRCDIR", BtPathname)
+       reg.sysload("PKGSRCDIR", BtPathname, DefinedIfInScope|NonemptyIfDefined)
        // This definition is only valid in the top-level Makefile,
        // not in category or package Makefiles.
        reg.acl("PKGSRCTOP", BtYes,
@@ -1414,7 +1458,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.sys("PKGVERSION_NOREV", BtVersion) // Without the nb* part.
        reg.sys("PKGWILDCARD", BtFilePattern)
        reg.sysload("PKG_ADMIN", BtShellCommand)
-       reg.sys("PKG_APACHE", enum("apache24"))
+       reg.sys("PKG_APACHE", enum("apache24"), DefinedIfInScope|NonemptyIfDefined)
        reg.pkglistrat("PKG_APACHE_ACCEPTED", enum("apache24"))
        reg.usr("PKG_APACHE_DEFAULT", enum("apache24"))
        reg.sysloadlist("PKG_BUILD_OPTIONS.*", BtOption)
@@ -1444,18 +1488,19 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.acllist("PKG_HACKS", BtIdentifier,
                PackageSettable,
                "*: none")
-       reg.sysload("PKG_INFO", BtShellCommand)
+       reg.sysload("PKG_INFO", BtShellCommand, DefinedIfInScope|NonemptyIfDefined)
        reg.sys("PKG_JAVA_HOME", BtPathname)
-       reg.sys("PKG_JVM", jvms)
+       // FIXME: Add definition for PKG_DEFAULT_JVM.
+       reg.sys("PKG_JVM", jvms) // deprecated
        reg.pkglistrat("PKG_JVMS_ACCEPTED", jvms)
-       reg.pkg("PKG_LIBTOOL", BtPathname)
+       reg.sys("PKG_LIBTOOL", BtPathname)
 
        // begin PKG_OPTIONS section
        //
        // TODO: force the pkgsrc packages to only define options in the
        //  options.mk file. Most packages already do this, but some still
        //  define them in the Makefile or Makefile.common.
-       reg.sysloadlist("PKG_OPTIONS", BtOption)
+       reg.sysloadlist("PKG_OPTIONS", BtOption, DefinedIfInScope|NonemptyIfDefined)
        reg.usrlist("PKG_OPTIONS.*", BtOption)
        reg.pkgloadlist("PKG_LEGACY_OPTIONS", BtOption)
        reg.pkgloadlist("PKG_OPTIONS_DEPRECATED_WARNINGS", BtShellWord)
@@ -1500,9 +1545,11 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.pkg("PLIST_TYPE", enum("dynamic static"))
        reg.pkglistbl3("PREPEND_PATH", BtPathname)
 
-       reg.acl("PREFIX", BtPathname,
-               UserSettable,
+       // PREFIX is indeed defined late, in bsd.pkg.use.mk, included by bsd.pkg.mk.
+       // It may be used everywhere since it is a rather central variable.
+       reg.acl("PREFIX", BtPathname, SystemProvided|DefinedIfInScope,
                "*: 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.
        reg.pkg("PREV_PKGPATH", BtPathname)
@@ -1536,6 +1583,8 @@ func (reg *VarTypeRegistry) Init(src *Pk
        //  to "files".
        reg.pkg("RCD_SCRIPT_SRC.*", BtPathname)
        reg.pkg("RCD_SCRIPT_WRK.*", BtPathname)
+       reg.sysload("READLINE_TYPE", enum("editline none readline"),
+               DefinedIfInScope|NonemptyIfDefined)
        reg.usr("REAL_ROOT_USER", BtUserGroupName)
        reg.usr("REAL_ROOT_GROUP", BtUserGroupName)
 
@@ -1577,7 +1626,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
                SystemProvided,
                "special:rubyversion.mk: default, set, use",
                "*: use, use-loadtime")
-       reg.sys("RUN", BtShellCommand)
+       reg.sys("RUN", BtShellCommand, DefinedIfInScope|NonemptyIfDefined)
        reg.sys("RUN_LDCONFIG", BtYesNo)
        reg.pkg("R_PKGNAME", BtRPkgName)
        reg.pkg("R_PKGVER", BtRPkgVer)
@@ -1585,14 +1634,15 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.usrlist("SETGID_GAMES_PERMS", BtPerms)
        reg.usrlist("SETUID_ROOT_PERMS", BtPerms)
        reg.pkg("SET_LIBDIR", BtYes)
-       reg.sys("SHAREGRP", BtUserGroupName)
-       reg.sys("SHAREMODE", BtFileMode)
-       reg.sys("SHAREOWN", BtUserGroupName)
-       reg.sys("SHCOMMENT", BtShellCommand)
+       reg.sysload("SHAREGRP", BtUserGroupName, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("SHAREMODE", BtFileMode, DefinedIfInScope|NonemptyIfDefined)
+       reg.sysload("SHAREOWN", BtUserGroupName, DefinedIfInScope|NonemptyIfDefined)
+       reg.sys("SHCOMMENT", BtShellCommand, DefinedIfInScope|NonemptyIfDefined)
        reg.sys("SHLIBTOOL", BtShellCommand)
        reg.pkglist("SHLIBTOOL_OVERRIDE", BtPathPattern)
        reg.sysload("SHLIB_TYPE",
-               enum("COFF ECOFF ELF SOM XCOFF Mach-O PE PEwin a.out aixlib dylib none"))
+               enum("COFF ECOFF ELF SOM XCOFF Mach-O PE PEwin a.out aixlib dylib none"),
+               DefinedIfInScope|NonemptyIfDefined)
        reg.pkglist("SITES.*", BtFetchURL)
        reg.usr("SMF_PREFIS", BtPathname)
        reg.pkg("SMF_SRCDIR", BtPathname)
@@ -1603,7 +1653,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.pkg("SMF_METHOD_SRC.*", BtPathname)
        reg.pkg("SMF_METHOD_SHELL", BtShellCommand)
        reg.pkglist("SPECIAL_PERMS", BtPerms)
-       reg.sys("STEP_MSG", BtShellCommand)
+       reg.sys("STEP_MSG", BtShellCommand, DefinedIfInScope|NonemptyIfDefined)
        reg.sys("STRIP", BtShellCommand) // see mk/tools/strip.mk
 
        // Only valid in the top-level and the category Makefiles.
@@ -1644,7 +1694,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.usr("UNPRIVILEGED_USER", BtUserGroupName)
        reg.usr("UNPRIVILEGED_GROUP", BtUserGroupName)
        reg.pkglist("UNWRAP_FILES", BtPathPattern)
-       reg.usrlist("UPDATE_TARGET", BtIdentifier)
+       reg.cmdline("UPDATE_TARGET", BtIdentifier, List)
        reg.pkg("USERGROUP_PHASE", enum("configure build pre-install"))
        reg.usrlist("USER_ADDITIONAL_PKGS", BtPkgpath)
        reg.pkg("USE_BSD_MAKEFILE", BtYes)
@@ -1654,7 +1704,7 @@ func (reg *VarTypeRegistry) Init(src *Pk
        // this check, a package may set this variable before including the
        // corresponding buildlink3.mk file.
        reg.acl("USE_BUILTIN.*", BtYesNoIndirectly,
-               PackageSettable,
+               PackageSettable|DefinedIfInScope|NonemptyIfDefined,
                "Makefile, Makefile.*, *.mk: set, use, use-loadtime")
 
        reg.pkg("USE_CMAKE", BtYes)

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.69 pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.70
--- pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.69  Mon Dec  2 23:32:09 2019
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck.go       Sun Dec  8 00:06:38 2019
@@ -887,8 +887,9 @@ func (cv *VartypeCheck) MailAddress() {
        }
 }
 
-// Message is a plain string. It should not be enclosed in quotes since
-// that is the job of the code that uses the message.
+// Message is a plain string. When defining a message variable, it should
+// not be enclosed in quotes since that is the job of the code that uses
+// the message.
 //
 // Lists of messages use a different type since they need the quotes
 // around each message; see PKG_FAIL_REASON.
@@ -1078,19 +1079,19 @@ func (cv *VartypeCheck) Pkgpath() {
 
        pkgpath := NewPkgsrcPath(NewPath(value))
        if !G.Wip && pkgpath.HasPrefixPath("wip") {
-               cv.MkLine.Errorf("A main pkgsrc package must not depend on a pkgsrc-wip package.")
+               cv.Errorf("A main pkgsrc package must not depend on a pkgsrc-wip package.")
        }
 
        pkgdir := G.Pkgsrc.File(pkgpath)
        if !pkgdir.JoinNoClean("Makefile").IsFile() {
-               cv.MkLine.Errorf("There is no package in %q.",
-                       cv.MkLine.PathToFile(pkgdir))
+               cv.Errorf("There is no package in %q.",
+                       cv.MkLine.Rel(pkgdir))
                return
        }
 
        if !matches(value, `^([^./][^/]*/[^./][^/]*)$`) {
-               cv.MkLine.Errorf("%q is not a valid path to a package.", pkgpath)
-               cv.MkLine.Explain(
+               cv.Errorf("%q is not a valid path to a package.", pkgpath.String())
+               cv.Explain(
                        "A path to a package has the form \"category/pkgbase\".",
                        "It is relative to the pkgsrc root.")
        }
@@ -1113,6 +1114,12 @@ func (cv *VartypeCheck) Pkgrevision() {
 
 // PrefixPathname checks for a pathname relative to ${PREFIX}.
 func (cv *VartypeCheck) PrefixPathname() {
+       if NewPath(cv.Value).IsAbs() {
+               cv.Errorf("The pathname %q in %s must be relative to ${PREFIX}.",
+                       cv.Value, cv.Varname)
+               return
+       }
+
        if m, manSubdir := match1(cv.Value, `^man/(.+)`); m {
                from := "${PKGMANDIR}/" + manSubdir
                fix := cv.Autofix()
@@ -1164,6 +1171,11 @@ func (cv *VartypeCheck) RPkgVer() {
 
 // RelativePkgDir refers to a package directory, e.g. ../../category/pkgbase.
 func (cv *VartypeCheck) RelativePkgDir() {
+       if NewPath(cv.Value).IsAbs() {
+               cv.Errorf("The path %q must be relative.", cv.Value)
+               return
+       }
+
        MkLineChecker{cv.MkLines, cv.MkLine}.CheckRelativePkgdir(NewRelPathString(cv.Value))
 }
 
@@ -1172,6 +1184,11 @@ func (cv *VartypeCheck) RelativePkgDir()
 //
 // See RelativePkgDir, which requires a directory, not a file.
 func (cv *VartypeCheck) RelativePkgPath() {
+       if NewPath(cv.Value).IsAbs() {
+               cv.Errorf("The path %q must be relative.", cv.Value)
+               return
+       }
+
        MkLineChecker{cv.MkLines, cv.MkLine}.CheckRelativePath(NewRelPathString(cv.Value), true)
 }
 

Index: pkgsrc/pkgtools/pkglint/files/intqa/ideas.go
diff -u pkgsrc/pkgtools/pkglint/files/intqa/ideas.go:1.3 pkgsrc/pkgtools/pkglint/files/intqa/ideas.go:1.4
--- pkgsrc/pkgtools/pkglint/files/intqa/ideas.go:1.3    Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/intqa/ideas.go        Sun Dec  8 00:06:38 2019
@@ -7,3 +7,11 @@ package intqa
 
 // XXX: If there is a constructor for a type, only that constructor may be used
 // for constructing objects. All other forms (var x Type; x := &Type{}) should be forbidden.
+
+// Each test must call its testee, if the testee is callable at all.
+//
+// If it doesn't, the name of the test is misleading. A typical case where
+// this happens is copy-and-paste mistakes combined with incomplete test
+// cases.
+//
+// To check this, every testee must be instrumented.

Index: pkgsrc/pkgtools/pkglint/files/intqa/qa.go
diff -u pkgsrc/pkgtools/pkglint/files/intqa/qa.go:1.2 pkgsrc/pkgtools/pkglint/files/intqa/qa.go:1.3
--- pkgsrc/pkgtools/pkglint/files/intqa/qa.go:1.2       Mon Dec  2 23:32:10 2019
+++ pkgsrc/pkgtools/pkglint/files/intqa/qa.go   Sun Dec  8 00:06:38 2019
@@ -322,11 +322,16 @@ func (ck *QAChecker) checkTesteesTest() 
                }
 
                testName := "Test_" + join(testee.Type, "_", testee.Func)
+
+               testCode := code{testee.testFile(), "", testName, 0}
+               test := test{testCode, testee.fullName(), "", testee}
+               insertion := ck.insertionSuggestion(&test)
+
                ck.addError(
                        EMissingTest,
                        testee.code,
-                       "Missing unit test %q for %q.",
-                       testName, testee.fullName())
+                       "Missing unit test %q for %q. %s",
+                       testName, testee.fullName(), insertion)
        }
 }
 
@@ -460,6 +465,39 @@ func (ck *QAChecker) print() {
        }
 }
 
+func (ck *QAChecker) insertionSuggestion(newTest *test) string {
+       // Find the testee directly above the testee of newTest.
+       var before *testee
+       for _, testee := range ck.testees {
+               if testee.order > newTest.testee.order {
+                       break
+               }
+               if testee.testFile() != newTest.file {
+                       continue
+               }
+               if before != nil && before.order > testee.order {
+                       break
+               }
+               before = testee
+       }
+
+       if before == nil {
+               return fmt.Sprintf("Insert it at the top of %q.", newTest.file)
+       }
+
+       for _, test := range ck.tests {
+               if test.testee == nil || test.testee.order < newTest.testee.order {
+                       continue
+               }
+               if test.file != before.testFile() {
+                       break
+               }
+               return fmt.Sprintf("Insert it in %q, above %q.", newTest.file, test.fullName())
+       }
+
+       return fmt.Sprintf("Insert it at the bottom of %q.", newTest.file)
+}
+
 type filter struct {
        filenames string
        typeNames string
Index: pkgsrc/pkgtools/pkglint/files/intqa/qa_test.go
diff -u pkgsrc/pkgtools/pkglint/files/intqa/qa_test.go:1.2 pkgsrc/pkgtools/pkglint/files/intqa/qa_test.go:1.3
--- pkgsrc/pkgtools/pkglint/files/intqa/qa_test.go:1.2  Mon Dec  2 23:32:10 2019
+++ pkgsrc/pkgtools/pkglint/files/intqa/qa_test.go      Sun Dec  8 00:06:38 2019
@@ -140,10 +140,15 @@ func (s *Suite) Test_QAChecker_Check(c *
        ck.Check()
 
        s.CheckErrors(
-               "Missing unit test \"Test_QAChecker_addCode\" for \"QAChecker.addCode\".",
-               "Missing unit test \"Test_QAChecker_relate\" for \"QAChecker.relate\".",
-               "Missing unit test \"Test_QAChecker_isRelevant\" for \"QAChecker.isRelevant\".")
-       s.CheckSummary("3 errors.")
+               "Missing unit test \"Test_QAChecker_addCode\" for \"QAChecker.addCode\". "+
+                       "Insert it in \"qa_test.go\", above \"Suite.Test_QAChecker_isTest\".",
+               "Missing unit test \"Test_QAChecker_relate\" for \"QAChecker.relate\". "+
+                       "Insert it in \"qa_test.go\", above \"Suite.Test_QAChecker_checkTests\".",
+               "Missing unit test \"Test_QAChecker_isRelevant\" for \"QAChecker.isRelevant\". "+
+                       "Insert it in \"qa_test.go\", above \"Suite.Test_QAChecker_errorsMask\".",
+               "Missing unit test \"Test_QAChecker_insertionSuggestion\" for \"QAChecker.insertionSuggestion\". "+
+                       "Insert it in \"qa_test.go\", above \"Suite.Test_code_fullName\".")
+       s.CheckSummary("4 errors.")
 }
 
 func (s *Suite) Test_QAChecker_load__filtered_nothing(c *check.C) {
@@ -455,6 +460,7 @@ func (s *Suite) Test_QAChecker_checkTest
        ck.addTestee(code{"demo.go", "OkType", "", 0})
        ck.addTestee(code{"demo.go", "", "OkFunc", 0})
        ck.addTestee(code{"demo.go", "OkType", "Method", 0})
+       ck.addTestee(code{"demo.go", "Bottom", "Method", 0})
        ck.addTest(code{"demo_test.go", "", "Test_OkType", 0})
        ck.addTest(code{"demo_test.go", "", "Test_OkFunc", 0})
        ck.addTest(code{"demo_test.go", "", "Test_OkType_Method", 0})
@@ -463,9 +469,14 @@ func (s *Suite) Test_QAChecker_checkTest
        ck.checkTesteesTest()
 
        s.CheckErrors(
-               "Missing unit test \"Test_Type\" for \"Type\".",
-               "Missing unit test \"Test_Func\" for \"Func\".",
-               "Missing unit test \"Test_Type_Method\" for \"Type.Method\".")
+               "Missing unit test \"Test_Type\" for \"Type\". "+
+                       "Insert it in \"demo_test.go\", above \"Test_OkType\".",
+               "Missing unit test \"Test_Func\" for \"Func\". "+
+                       "Insert it in \"demo_test.go\", above \"Test_OkType\".",
+               "Missing unit test \"Test_Type_Method\" for \"Type.Method\". "+
+                       "Insert it in \"demo_test.go\", above \"Test_OkType\".",
+               "Missing unit test \"Test_Bottom_Method\" for \"Bottom.Method\". "+
+                       "Insert it at the bottom of \"demo_test.go\".")
 }
 
 func (s *Suite) Test_QAChecker_checkMethodsSameFile(c *check.C) {

Added files:

Index: pkgsrc/pkgtools/pkglint/files/lineslexer.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/lineslexer.go:1.1
--- /dev/null   Sun Dec  8 00:06:39 2019
+++ pkgsrc/pkgtools/pkglint/files/lineslexer.go Sun Dec  8 00:06:38 2019
@@ -0,0 +1,153 @@
+package pkglint
+
+import "netbsd.org/pkglint/regex"
+
+// LinesLexer records the state when checking a list of lines from top to bottom.
+type LinesLexer struct {
+       line  *Line
+       index int
+       lines *Lines
+}
+
+func NewLinesLexer(lines *Lines) *LinesLexer {
+       llex := LinesLexer{nil, 0, lines}
+       llex.setIndex(0)
+       return &llex
+}
+
+// CurrentLine returns the line that the lexer is currently looking at.
+// For the EOF, a virtual line with line number "EOF" is returned.
+func (llex *LinesLexer) CurrentLine() *Line {
+       if llex.line != nil {
+               return llex.line
+       }
+       return NewLineEOF(llex.lines.Filename)
+}
+
+func (llex *LinesLexer) PreviousLine() *Line {
+       return llex.lines.Lines[llex.index-1]
+}
+
+func (llex *LinesLexer) EOF() bool {
+       return llex.line == nil
+}
+
+// Skip skips the current line.
+func (llex *LinesLexer) Skip() bool {
+       if llex.EOF() {
+               return false
+       }
+       llex.next()
+       return true
+}
+
+func (llex *LinesLexer) Undo() {
+       llex.setIndex(llex.index - 1)
+}
+
+func (llex *LinesLexer) NextRegexp(re regex.Pattern) []string {
+       if trace.Tracing {
+               defer trace.Call(llex.CurrentLine().Text, re)()
+       }
+
+       if !llex.EOF() {
+               if m := match(llex.line.Text, re); m != nil {
+                       llex.next()
+                       return m
+               }
+       }
+       return nil
+}
+
+func (llex *LinesLexer) SkipRegexp(re regex.Pattern) bool {
+       return llex.NextRegexp(re) != nil
+}
+
+func (llex *LinesLexer) SkipPrefix(prefix string) bool {
+       if trace.Tracing {
+               defer trace.Call2(llex.CurrentLine().Text, prefix)()
+       }
+
+       if !llex.EOF() && hasPrefix(llex.line.Text, prefix) {
+               llex.next()
+               return true
+       }
+       return false
+}
+
+func (llex *LinesLexer) SkipText(text string) bool {
+       if trace.Tracing {
+               defer trace.Call2(llex.CurrentLine().Text, text)()
+       }
+
+       if !llex.EOF() && llex.line.Text == text {
+               llex.Skip()
+               return true
+       }
+       return false
+}
+
+func (llex *LinesLexer) SkipEmptyOrNote() bool {
+       if llex.SkipText("") {
+               return true
+       }
+
+       if llex.index == 0 {
+               fix := llex.CurrentLine().Autofix()
+               fix.Notef("Empty line expected before this line.")
+               fix.InsertBefore("")
+               fix.Apply()
+       } else {
+               fix := llex.PreviousLine().Autofix()
+               fix.Notef("Empty line expected after this line.")
+               fix.InsertAfter("")
+               fix.Apply()
+       }
+
+       return false
+}
+
+func (llex *LinesLexer) SkipContainsOrWarn(text string) bool {
+       result := llex.SkipText(text)
+       if !result {
+               llex.CurrentLine().Warnf("This line should contain the following text: %s", text)
+       }
+       return result
+}
+
+func (llex *LinesLexer) setIndex(index int) {
+       llex.index = index
+       if index < llex.lines.Len() {
+               llex.line = llex.lines.Lines[index]
+       } else {
+               llex.line = nil
+       }
+}
+
+func (llex *LinesLexer) next() { llex.setIndex(llex.index + 1) }
+
+// MkLinesLexer records the state when checking a list of Makefile lines from top to bottom.
+type MkLinesLexer struct {
+       mklines *MkLines
+       LinesLexer
+}
+
+func NewMkLinesLexer(mklines *MkLines) *MkLinesLexer {
+       return &MkLinesLexer{mklines, *NewLinesLexer(mklines.lines)}
+}
+
+func (mlex *MkLinesLexer) PreviousMkLine() *MkLine {
+       return mlex.mklines.mklines[mlex.index-1]
+}
+
+func (mlex *MkLinesLexer) CurrentMkLine() *MkLine {
+       return mlex.mklines.mklines[mlex.index]
+}
+
+func (mlex *MkLinesLexer) SkipIf(pred func(mkline *MkLine) bool) bool {
+       if !mlex.EOF() && pred(mlex.CurrentMkLine()) {
+               mlex.next()
+               return true
+       }
+       return false
+}
Index: pkgsrc/pkgtools/pkglint/files/lineslexer_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/lineslexer_test.go:1.1
--- /dev/null   Sun Dec  8 00:06:39 2019
+++ pkgsrc/pkgtools/pkglint/files/lineslexer_test.go    Sun Dec  8 00:06:38 2019
@@ -0,0 +1,66 @@
+package pkglint
+
+import (
+       "gopkg.in/check.v1"
+)
+
+func (s *Suite) Test_LinesLexer_SkipPrefix(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.NewLines("file.txt",
+               "line 1",
+               "line 2")
+       llex := NewLinesLexer(lines)
+
+       t.CheckEquals(llex.SkipPrefix("line 1"), true)
+       t.CheckEquals(llex.SkipPrefix("line 1"), false)
+       t.CheckEquals(llex.SkipPrefix("line 2"), true)
+       t.CheckEquals(llex.SkipPrefix("line 2"), false)
+       t.CheckEquals(llex.SkipPrefix(""), false)
+}
+
+func (s *Suite) Test_LinesLexer_SkipEmptyOrNote__beginning_of_file(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.NewLines("file.txt",
+               "line 1",
+               "line 2")
+       llex := NewLinesLexer(lines)
+
+       llex.SkipEmptyOrNote()
+
+       t.CheckOutputLines(
+               "NOTE: file.txt:1: Empty line expected before this line.")
+}
+
+func (s *Suite) Test_LinesLexer_SkipEmptyOrNote__end_of_file(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.NewLines("file.txt",
+               "line 1",
+               "line 2")
+       llex := NewLinesLexer(lines)
+
+       for llex.Skip() {
+       }
+
+       llex.SkipEmptyOrNote()
+
+       t.CheckOutputLines(
+               "NOTE: file.txt:2: Empty line expected after this line.")
+}
+
+func (s *Suite) Test_MkLinesLexer_SkipIf(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("filename.mk",
+               "# comment",
+               "VAR=\tnot a comment")
+       mlex := NewMkLinesLexer(mklines)
+
+       t.CheckEquals(mlex.SkipIf((*MkLine).IsComment), true)
+       t.CheckEquals(mlex.SkipIf((*MkLine).IsComment), false)
+       t.CheckEquals(mlex.SkipIf((*MkLine).IsVarassign), true)
+       t.CheckEquals(mlex.SkipIf((*MkLine).IsVarassign), false)
+       t.CheckEquals(mlex.EOF(), true)
+}
Index: pkgsrc/pkgtools/pkglint/files/mkassignchecker.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/mkassignchecker.go:1.1
--- /dev/null   Sun Dec  8 00:06:39 2019
+++ pkgsrc/pkgtools/pkglint/files/mkassignchecker.go    Sun Dec  8 00:06:38 2019
@@ -0,0 +1,558 @@
+package pkglint
+
+import (
+       "strconv"
+       "strings"
+)
+
+// MkAssignChecker checks a variable assignment line in a Makefile.
+type MkAssignChecker struct {
+       MkLine  *MkLine
+       MkLines *MkLines
+}
+
+func NewMkAssignChecker(mkLine *MkLine, mkLines *MkLines) *MkAssignChecker {
+       return &MkAssignChecker{MkLine: mkLine, MkLines: mkLines}
+}
+
+func (ck *MkAssignChecker) checkVarassign() {
+       ck.checkVarassignLeft()
+       ck.checkVarassignOp()
+       ck.checkVarassignRight()
+}
+
+// checkVarassignLeft checks everything to the left of the assignment operator.
+func (ck *MkAssignChecker) checkVarassignLeft() {
+       varname := ck.MkLine.Varname()
+       if hasPrefix(varname, "_") && !G.Infrastructure && G.Pkgsrc.vartypes.Canon(varname) == nil {
+               ck.MkLine.Warnf("Variable names starting with an underscore (%s) are reserved for internal pkgsrc use.", varname)
+       }
+
+       ck.checkVarassignLeftNotUsed()
+       ck.checkVarassignLeftDeprecated()
+       ck.checkVarassignLeftBsdPrefs()
+       if !ck.checkVarassignLeftUserSettable() {
+               ck.checkVarassignLeftPermissions()
+       }
+       ck.checkVarassignLeftRationale()
+
+       NewMkLineChecker(ck.MkLines, ck.MkLine).checkTextVarUse(
+               ck.MkLine.Varname(),
+               NewVartype(BtVariableName, NoVartypeOptions, NewACLEntry("*", aclpAll)),
+               VucLoadTime)
+}
+
+// checkVarassignLeftNotUsed checks whether the left-hand side of a variable
+// assignment is not used. If it is unused and also doesn't have a predefined
+// data type, it may be a spelling mistake.
+func (ck *MkAssignChecker) checkVarassignLeftNotUsed() {
+       varname := ck.MkLine.Varname()
+       varcanon := varnameCanon(varname)
+
+       if ck.MkLine.Op() == opAssignEval && varname == strings.ToLower(varname) {
+               if trace.Tracing {
+                       trace.Step1("%s might be unused unless it is an argument to a procedure file.", varname)
+               }
+               return
+       }
+
+       if ck.MkLines.vars.IsUsedSimilar(varname) {
+               return
+       }
+
+       if G.Pkg != nil && G.Pkg.vars.IsUsedSimilar(varname) {
+               return
+       }
+
+       vartypes := G.Pkgsrc.vartypes
+       if vartypes.IsDefinedExact(varname) || vartypes.IsDefinedExact(varcanon) {
+               return
+       }
+
+       deprecated := G.Pkgsrc.Deprecated
+       if deprecated[varname] != "" || deprecated[varcanon] != "" {
+               return
+       }
+
+       if !ck.MkLines.once.FirstTimeSlice("defined but not used: ", varname) {
+               return
+       }
+
+       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.")
+}
+
+func (ck *MkAssignChecker) checkVarassignLeftDeprecated() {
+       varname := ck.MkLine.Varname()
+       if fix := G.Pkgsrc.Deprecated[varname]; fix != "" {
+               ck.MkLine.Warnf("Definition of %s is deprecated. %s", varname, fix)
+       } else if fix = G.Pkgsrc.Deprecated[varnameCanon(varname)]; fix != "" {
+               ck.MkLine.Warnf("Definition of %s is deprecated. %s", varname, fix)
+       }
+}
+
+func (ck *MkAssignChecker) checkVarassignLeftBsdPrefs() {
+       mkline := ck.MkLine
+
+       switch mkline.Varcanon() {
+       case "BUILDLINK_PKGSRCDIR.*",
+               "BUILDLINK_DEPMETHOD.*",
+               "BUILDLINK_ABI_DEPENDS.*",
+               "BUILDLINK_INCDIRS.*",
+               "BUILDLINK_LIBDIRS.*":
+               return
+       }
+
+       if !G.Opts.WarnExtra ||
+               G.Infrastructure ||
+               mkline.Op() != opAssignDefault ||
+               ck.MkLines.Tools.SeenPrefs {
+               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.IsPackageSettable() {
+               return
+       }
+
+       if !ck.MkLines.once.FirstTime("include bsd.prefs.mk before using ?=") {
+               return
+       }
+       mkline.Warnf("Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")
+       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.",
+               "This file must be included explicitly.",
+               "If a ?= operator appears before mk.conf has been included,",
+               "it will not care about the user's preferences,",
+               "which can result in unexpected behavior.",
+               "",
+               "The easiest way to include the mk.conf file is by including the",
+               "bsd.prefs.mk file, which will take care of everything.")
+}
+
+// checkVarassignLeftUserSettable checks whether a package defines a
+// variable that is marked as user-settable since it is defined in
+// mk/defaults/mk.conf.
+func (ck *MkAssignChecker) checkVarassignLeftUserSettable() bool {
+       mkline := ck.MkLine
+       varname := mkline.Varname()
+
+       defaultMkline := G.Pkgsrc.UserDefinedVars.Mentioned(varname)
+       if defaultMkline == nil {
+               return false
+       }
+       defaultValue := defaultMkline.Value()
+
+       // A few of the user-settable variables can also be set by packages.
+       // That's an unfortunate situation since there is no definite source
+       // of truth, but luckily only a few variables make use of it.
+       vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
+       if vartype.IsPackageSettable() {
+               return true
+       }
+
+       switch {
+       case mkline.HasComment():
+               // Assume that the comment contains a rationale for disabling
+               // this particular check.
+
+       case mkline.Op() == opAssignAppend:
+               mkline.Warnf("Packages should not append to user-settable %s.", varname)
+
+       case defaultValue != mkline.Value():
+               mkline.Warnf(
+                       "Package sets user-defined %q to %q, which differs "+
+                               "from the default value %q from mk/defaults/mk.conf.",
+                       varname, mkline.Value(), defaultValue)
+
+       case defaultMkline.IsCommentedVarassign():
+               // Since the variable assignment is commented out in
+               // mk/defaults/mk.conf, the package has to define it.
+
+       default:
+               mkline.Notef("Redundant definition for %s from mk/defaults/mk.conf.", varname)
+               if !ck.MkLines.Tools.SeenPrefs {
+                       mkline.Explain(
+                               "Instead of defining the variable redundantly, it suffices to include",
+                               "../../mk/bsd.prefs.mk, which provides all user-settable variables.")
+               }
+       }
+
+       return true
+}
+
+// checkVarassignLeftPermissions checks the permissions for the left-hand side
+// of a variable assignment line.
+//
+// See checkPermissions.
+func (ck *MkAssignChecker) checkVarassignLeftPermissions() {
+       if !G.Opts.WarnPerm {
+               return
+       }
+       if G.Infrastructure {
+               // As long as vardefs.go doesn't explicitly define permissions for
+               // infrastructure files, skip the check completely. This avoids
+               // many wrong warnings.
+               return
+       }
+       if trace.Tracing {
+               defer trace.Call0()()
+       }
+
+       mkline := ck.MkLine
+       if ck.MkLine.Basename == "hacks.mk" {
+               return
+       }
+
+       varname := mkline.Varname()
+       op := mkline.Op()
+       vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
+       if vartype == nil {
+               return
+       }
+
+       perms := vartype.EffectivePermissions(mkline.Basename)
+
+       // E.g. USE_TOOLS:= ${USE_TOOLS:Nunwanted-tool}
+       if op == opAssignEval && perms&aclpAppend != 0 {
+               tokens, _ := mkline.ValueTokens()
+               if len(tokens) == 1 && tokens[0].Varuse != nil && tokens[0].Varuse.varname == varname {
+                       return
+               }
+       }
+
+       needed := aclpSet
+       switch op {
+       case opAssignDefault:
+               needed = aclpSetDefault
+       case opAssignAppend:
+               needed = aclpAppend
+       }
+
+       switch {
+       case perms.Contains(needed):
+               break
+       default:
+               alternativeActions := perms & aclpAllWrite
+               alternativeFiles := vartype.AlternativeFiles(needed)
+               switch {
+               case alternativeActions != 0 && alternativeFiles != "":
+                       mkline.Warnf("The variable %s should not be %s (only %s) in this file; it would be ok in %s.",
+                               varname, needed.HumanString(), alternativeActions.HumanString(), alternativeFiles)
+               case alternativeFiles != "":
+                       mkline.Warnf("The variable %s should not be %s in this file; it would be ok in %s.",
+                               varname, needed.HumanString(), alternativeFiles)
+               case alternativeActions != 0:
+                       mkline.Warnf("The variable %s should not be %s (only %s) in this file.",
+                               varname, needed.HumanString(), alternativeActions.HumanString())
+               default:
+                       mkline.Warnf("The variable %s should not be %s by any package.",
+                               varname, needed.HumanString())
+               }
+
+               // XXX: explainPermissions doesn't really belong to MkVarUseChecker.
+               (&MkVarUseChecker{nil, nil, ck.MkLines, mkline}).
+                       explainPermissions(varname, vartype)
+       }
+}
+
+func (ck *MkAssignChecker) checkVarassignLeftRationale() {
+       if !G.Opts.WarnExtra {
+               return
+       }
+
+       mkline := ck.MkLine
+       vartype := G.Pkgsrc.VariableType(ck.MkLines, mkline.Varname())
+       if vartype == nil || !vartype.NeedsRationale() {
+               return
+       }
+
+       if mkline.HasRationale() {
+               return
+       }
+
+       mkline.Warnf("Setting variable %s should have a rationale.", mkline.Varname())
+       mkline.Explain(
+               "Since this variable prevents the package from being built in some situations,",
+               "the reasons for this restriction should be documented.",
+               "Otherwise it becomes too difficult to check whether these restrictions still apply",
+               "when the package is updated by someone else later.",
+               "",
+               "To add the rationale, put it in a comment at the end of this line,",
+               "or in a separate comment in the line above.",
+               "The rationale should try to answer these questions:",
+               "",
+               "* which specific aspects of the package are affected?",
+               "* if it's a dependency, is the dependency too old or too new?",
+               "* in which situations does a crash occur, if any?",
+               "* has it been reported upstream?")
+}
+
+func (ck *MkAssignChecker) checkVarassignOp() {
+       ck.checkVarassignOpShell()
+}
+
+func (ck *MkAssignChecker) checkVarassignOpShell() {
+       mkline := ck.MkLine
+
+       switch {
+       case mkline.Op() != opAssignShell:
+               return
+
+       case mkline.HasComment():
+               return
+
+       case mkline.Basename == "builtin.mk":
+               // These are typically USE_BUILTIN.* and BUILTIN_VERSION.*.
+               // Authors of builtin.mk files usually know what they're doing.
+               return
+
+       case G.Pkg == nil || G.Pkg.vars.IsUsedAtLoadTime(mkline.Varname()):
+               return
+       }
+
+       mkline.Notef("Consider the :sh modifier instead of != for %q.", mkline.Value())
+       mkline.Explain(
+               "For variable assignments using the != operator, the shell command",
+               "is run every time the file is parsed.",
+               "In some cases this is too early, and the command may not yet be installed.",
+               "In other cases the command is executed more often than necessary.",
+               "Most commands don't need to be executed for \"make clean\", for example.",
+               "",
+               "The :sh modifier defers execution until the variable value is actually needed.",
+               "On the other hand, this means the command is executed each time the variable",
+               "is evaluated.",
+               "",
+               "Example:",
+               "",
+               "\tEARLY_YEAR!=    date +%Y",
+               "",
+               "\tLATE_YEAR_CMD=  date +%Y",
+               "\tLATE_YEAR=      ${LATE_YEAR_CMD:sh}",
+               "",
+               "\t# or, in a single line:",
+               "\tLATE_YEAR=      ${date +%Y:L:sh}",
+               "",
+               "To suppress this note, provide an explanation in a comment at the end",
+               "of the line, or force the variable to be evaluated at load time,",
+               "by using it at the right-hand side of the := operator, or in an .if",
+               "or .for directive.")
+}
+
+// checkVarassignLeft checks everything to the right of the assignment operator.
+func (ck *MkAssignChecker) checkVarassignRight() {
+       mkline := ck.MkLine
+       varname := mkline.Varname()
+       op := mkline.Op()
+       value := mkline.Value()
+       comment := condStr(mkline.HasComment(), "#", "") + mkline.Comment()
+
+       if trace.Tracing {
+               defer trace.Call(varname, op, value)()
+       }
+
+       mkLineChecker := NewMkLineChecker(ck.MkLines, ck.MkLine)
+       mkLineChecker.checkText(value)
+       mkLineChecker.checkVartype(varname, op, value, comment)
+
+       ck.checkVarassignMisc()
+
+       ck.checkVarassignRightVaruse()
+}
+
+func (ck *MkAssignChecker) checkVarassignRightCategory() {
+       mkline := ck.MkLine
+       if mkline.Op() != opAssign && mkline.Op() != opAssignDefault {
+               return
+       }
+
+       categories := mkline.ValueFields(mkline.Value())
+       actual := categories[0]
+       expected := G.Pkgsrc.ToRel(mkline.Filename).DirNoClean().DirNoClean().Base()
+
+       if expected == "wip" || actual == expected {
+               return
+       }
+
+       fix := mkline.Autofix()
+       fix.Warnf("The primary category should be %q, not %q.", expected, actual)
+       fix.Explain(
+               "The primary category of a package should be its location in the",
+               "pkgsrc directory tree, to make it easy to find the package.",
+               "All other categories may be added after this primary category.")
+       if len(categories) > 1 && categories[1] == expected {
+               fix.Replace(categories[0]+" "+categories[1], categories[1]+" "+categories[0])
+       }
+       fix.Apply()
+}
+
+func (ck *MkAssignChecker) checkVarassignMisc() {
+       mkline := ck.MkLine
+       varname := mkline.Varname()
+       value := mkline.Value()
+
+       if contains(value, "/etc/rc.d") && mkline.Varname() != "RPMIGNOREPATH" {
+               mkline.Warnf("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.")
+       }
+
+       if varname == "PYTHON_VERSIONS_ACCEPTED" {
+               ck.checkVarassignDecreasingVersions()
+       }
+
+       if mkline.Comment() == " defined" && !hasSuffix(varname, "_MK") && !hasSuffix(varname, "_COMMON") {
+               mkline.Notef("Please use \"# empty\", \"# none\" or \"# yes\" instead of \"# defined\".")
+               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",
+                       "means \"yes\", in other cases it is an empty list (which is also",
+                       "only the state of the variable), whose meaning could be described",
+                       "with \"none\".",
+                       "It is this meaning that should be described.")
+       }
+
+       switch varname {
+       case "DIST_SUBDIR", "WRKSRC", "MASTER_SITES":
+               // TODO: Replace regex with proper VarUse.
+               if m, revVarname := match1(value, `\$\{(PKGNAME|PKGVERSION)[:\}]`); m {
+                       mkline.Warnf("%s should not be used in %s as it includes the PKGREVISION. "+
+                               "Please use %[1]s_NOREV instead.", revVarname, varname)
+               }
+       }
+
+       if hasPrefix(varname, "SITES_") {
+               mkline.Warnf("SITES_* is deprecated. Please use SITES.* instead.")
+               // No autofix since it doesn't occur anymore.
+       }
+
+       if varname == "PKG_SKIP_REASON" && ck.MkLines.indentation.DependsOn("OPSYS") {
+               // TODO: Provide autofix for simple cases, like ".if ${OPSYS} == SunOS".
+               mkline.Notef("Consider setting NOT_FOR_PLATFORM instead of " +
+                       "PKG_SKIP_REASON depending on ${OPSYS}.")
+       }
+
+       ck.checkVarassignMiscRedundantInstallationDirs()
+}
+
+func (ck *MkAssignChecker) checkVarassignDecreasingVersions() {
+       mkline := ck.MkLine
+       strVersions := mkline.Fields()
+       intVersions := make([]int, len(strVersions))
+       for i, strVersion := range strVersions {
+               iver, err := strconv.Atoi(strVersion)
+               if err != nil || !(iver > 0) {
+                       mkline.Errorf("Value %q for %s must be a positive integer.", strVersion, mkline.Varname())
+                       return
+               }
+               intVersions[i] = iver
+       }
+
+       for i, ver := range intVersions {
+               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])
+                       mkline.Explain(
+                               "If they aren't, it may be possible that needless versions of",
+                               "packages are installed.")
+               }
+       }
+}
+
+func (ck *MkAssignChecker) checkVarassignMiscRedundantInstallationDirs() {
+       mkline := ck.MkLine
+       varname := mkline.Varname()
+
+       switch {
+       case G.Pkg == nil,
+               varname != "INSTALLATION_DIRS",
+               !matches(G.Pkg.vars.LastValue("AUTO_MKDIRS"), `^[Yy][Ee][Ss]$`):
+               return
+       }
+
+       for _, dir := range mkline.ValueFields(mkline.Value()) {
+               if NewPath(dir).IsAbs() {
+                       continue
+               }
+
+               rel := NewRelPathString(dir)
+               if G.Pkg.Plist.Dirs[rel] != nil {
+                       mkline.Notef("The directory %q is redundant in %s.", rel, varname)
+                       mkline.Explain(
+                               "This package defines AUTO_MKDIR, and the directory is contained in the PLIST.",
+                               "Therefore it will be created anyway.")
+               }
+       }
+}
+
+// checkVarassignRightVaruse checks that in a variable assignment,
+// each variable used on the right-hand side of the assignment operator
+// has the correct data type and quoting.
+func (ck *MkAssignChecker) checkVarassignRightVaruse() {
+       if trace.Tracing {
+               defer trace.Call0()()
+       }
+
+       mkline := ck.MkLine
+       op := mkline.Op()
+
+       time := VucRunTime
+       if op == opAssignEval || op == opAssignShell {
+               time = VucLoadTime
+       }
+
+       vartype := G.Pkgsrc.VariableType(ck.MkLines, mkline.Varname())
+       if op == opAssignShell {
+               vartype = shellCommandsType
+       }
+
+       if vartype != nil && vartype.IsShell() {
+               ck.checkVarassignVaruseShell(vartype, time)
+       } else { // XXX: This else looks as if it should be omitted.
+               mkLineChecker := NewMkLineChecker(ck.MkLines, ck.MkLine)
+               mkLineChecker.checkTextVarUse(ck.MkLine.Value(), vartype, time)
+       }
+}
+
+// checkVarassignVaruseShell is very similar to checkVarassignRightVaruse, they just differ
+// in the way they determine isWordPart.
+func (ck *MkAssignChecker) checkVarassignVaruseShell(vartype *Vartype, time VucTime) {
+       if trace.Tracing {
+               defer trace.Call(vartype, time)()
+       }
+
+       isWordPart := func(tokens []*ShAtom, i int) bool {
+               if i-1 >= 0 && tokens[i-1].Type.IsWord() {
+                       return true
+               }
+               if i+1 < len(tokens) && tokens[i+1].Type.IsWord() {
+                       return true
+               }
+               return false
+       }
+
+       mkline := ck.MkLine
+       atoms := NewShTokenizer(mkline.Line, mkline.Value(), false).ShAtoms()
+       for i, atom := range atoms {
+               if varuse := atom.VarUse(); varuse != nil {
+                       wordPart := isWordPart(atoms, i)
+                       vuc := VarUseContext{vartype, time, atom.Quoting.ToVarUseContext(), wordPart}
+                       NewMkVarUseChecker(varuse, ck.MkLines, mkline).Check(&vuc)
+               }
+       }
+}
Index: pkgsrc/pkgtools/pkglint/files/mkassignchecker_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/mkassignchecker_test.go:1.1
--- /dev/null   Sun Dec  8 00:06:39 2019
+++ pkgsrc/pkgtools/pkglint/files/mkassignchecker_test.go       Sun Dec  8 00:06:38 2019
@@ -0,0 +1,1058 @@
+package pkglint
+
+import "gopkg.in/check.v1"
+
+func (s *Suite) Test_NewMkAssignChecker(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("filename.mk",
+               "VAR=\t${OTHER}")
+
+       ck := NewMkAssignChecker(mklines.mklines[0], mklines)
+
+       ck.checkVarassign()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:1: VAR is defined but not used.",
+               "WARN: filename.mk:1: OTHER is used but not defined.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassign(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       mklines := t.NewMkLines("Makefile",
+               MkCvsID,
+               "ac_cv_libpari_libs+=\t-L${BUILDLINK_PREFIX.pari}/lib") // From math/clisp-pari/Makefile, rev. 1.8
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: Makefile:2: ac_cv_libpari_libs is defined but not used.")
+}
+
+// Pkglint once interpreted all lists as consisting of shell tokens,
+// splitting this URL at the ampersand.
+func (s *Suite) Test_MkAssignChecker_checkVarassign__URL_with_shell_special_characters(c *check.C) {
+       t := s.Init(c)
+
+       G.Pkg = NewPackage(t.File("graphics/gimp-fix-ca"))
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "MASTER_SITES=\thttp://registry.gimp.org/file/fix-ca.c?action=download&id=9884&file=";)
+
+       mklines.Check()
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassign__list(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/";)
+       t.SetUpVartypes()
+       t.SetUpCommandLine("-Wall", "--explain")
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "SITES.distfile=\t-${MASTER_SITE_GITHUB:=project/}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:2: The list variable MASTER_SITE_GITHUB should not be embedded in a word.",
+               "",
+               "\tWhen a list variable has multiple elements, this expression expands",
+               "\tto something unexpected:",
+               "",
+               "\tExample: ${MASTER_SITE_SOURCEFORGE}directory/ expands to",
+               "",
+               "\t\thttps://mirror1.sf.net/ https://mirror2.sf.net/directory/";,
+               "",
+               "\tThe first URL is missing the directory. To fix this, write",
+               "\t\t${MASTER_SITE_SOURCEFORGE:=directory/}.",
+               "",
+               "\tExample: -l${LIBS} expands to",
+               "",
+               "\t\t-llib1 lib2",
+               "",
+               "\tThe second library is missing the -l. To fix this, write",
+               "\t${LIBS:S,^,-l,}.",
+               "")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeft(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("module.mk",
+               MkCvsID,
+               "_VARNAME=\tvalue")
+       // Only to prevent "defined but not used".
+       mklines.vars.Use("_VARNAME", mklines.mklines[1], VucRunTime)
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: module.mk:2: Variable names starting with an underscore " +
+                       "(_VARNAME) are reserved for internal pkgsrc use.")
+}
+
+// Files from the pkgsrc infrastructure may define and use variables
+// whose name starts with an underscore.
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeft__infrastructure(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/infra.mk",
+               MkCvsID,
+               "_VARNAME=\t\tvalue",
+               "_SORTED_VARS.group=\tVARNAME")
+       t.FinishSetUp()
+
+       G.Check(t.File("mk/infra.mk"))
+
+       t.CheckOutputLines(
+               "WARN: ~/mk/infra.mk:2: _VARNAME is defined but not used.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeft__documented_underscore(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("category/package/filename.mk",
+               MkCvsID,
+               "_SORTED_VARS.group=\tVARNAME")
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package/filename.mk"))
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftNotUsed__procedure_call(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("mk/pkg-build-options.mk")
+       mklines := t.SetUpFileMkLines("category/package/filename.mk",
+               MkCvsID,
+               "",
+               "pkgbase := glib2",
+               ".include \"../../mk/pkg-build-options.mk\"",
+               "",
+               "VAR=\tvalue")
+
+       mklines.Check()
+
+       // There is no warning for pkgbase although it looks unused as well.
+       // The file pkg-build-options.mk is essentially a procedure call,
+       // and pkgbase is its parameter.
+       //
+       // To distinguish these parameters from ordinary variables, they are
+       // usually written with the := operator instead of the = operator.
+       // This has the added benefit that the parameter is only evaluated
+       // once, especially if it contains references to other variables.
+       t.CheckOutputLines(
+               "WARN: ~/category/package/filename.mk:6: VAR is defined but not used.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftNotUsed__procedure_call_no_tracing(c *check.C) {
+       t := s.Init(c)
+
+       t.DisableTracing() // Just for code coverage
+       t.CreateFileLines("mk/pkg-build-options.mk")
+       mklines := t.SetUpFileMkLines("category/package/filename.mk",
+               MkCvsID,
+               "",
+               "pkgbase := glib2",
+               ".include \"../../mk/pkg-build-options.mk\"")
+
+       mklines.Check()
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftNotUsed__infra(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("mk/infra.mk",
+               MkCvsID,
+               "#",
+               "# Package-settable variables:",
+               "#",
+               "# SHORT_DOCUMENTATION",
+               "#\tIf set to no, ...",
+               "#\tsecond line.",
+               "#",
+               "#",
+               ".if ${USED_IN_INFRASTRUCTURE:Uyes:tl} == yes",
+               ".endif")
+       t.SetUpPackage("category/package",
+               "USED_IN_INFRASTRUCTURE=\t${SHORT_DOCUMENTATION}",
+               "",
+               "UNUSED_INFRA=\t${UNDOCUMENTED}")
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:22: UNUSED_INFRA is defined but not used.",
+               "WARN: ~/category/package/Makefile:22: UNDOCUMENTED is used but not defined.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftDeprecated(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.FinishSetUp()
+
+       test := func(varname string, diagnostics ...string) {
+               mklines := t.NewMkLines("filename.mk",
+                       varname+"=\t# none")
+               ck := NewMkAssignChecker(mklines.mklines[0], mklines)
+
+               ck.checkVarassignLeftDeprecated()
+
+               t.CheckOutput(diagnostics)
+       }
+
+       test("FIX_RPATH",
+               "WARN: filename.mk:1: Definition of FIX_RPATH is deprecated. "+
+                       "It has been removed from pkgsrc in 2003.")
+
+       test("PKGNAME",
+               nil...)
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftBsdPrefs(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--only", "bsd.prefs.mk")
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("module.mk",
+               MkCvsID,
+               "",
+               "BUILDLINK_PKGSRCDIR.pkgbase?=\t${PREFIX}",
+               "BUILDLINK_DEPMETHOD.pkgbase?=\tfull",
+               "BUILDLINK_ABI_DEPENDS.pkgbase?=\tpkgbase>=1",
+               "BUILDLINK_INCDIRS.pkgbase?=\t# none",
+               "BUILDLINK_LIBDIRS.pkgbase?=\t# none",
+               "",
+               // User-settable, therefore bsd.prefs.mk must be included before.
+               // To avoid frightening pkgsrc developers, this is currently a
+               // warning instead of an error. An error would be better though.
+               "MYSQL_USER?=\tmysqld",
+               // Package-settable variables do not depend on the user settings,
+               // therefore it is ok to give them default values without
+               // including bsd.prefs.mk before.
+               "PKGNAME?=\tpkgname-1.0")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: module.mk:9: " +
+                       "Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")
+}
+
+// Up to 2019-12-03, pkglint didn't issue a warning if a default assignment
+// to a package-settable variable appeared before one to a user-settable
+// variable. This was a mistake.
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftBsdPrefs__first_time(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("module.mk",
+               MkCvsID,
+               "",
+               "PKGNAME?=\tpkgname-1.0",
+               "MYSQL_USER?=\tmysqld")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: module.mk:4: Please include \"../../mk/bsd.prefs.mk\" "+
+                       "before using \"?=\".",
+               "WARN: module.mk:4: The variable MYSQL_USER should not "+
+                       "be given a default value by any package.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftBsdPrefs__vartype_nil(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("builtin.mk",
+               MkCvsID,
+               "VAR_SH?=\tvalue")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: builtin.mk:2: VAR_SH is defined but not used.",
+               "WARN: builtin.mk:2: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftUserSettable(c *check.C) {
+       t := s.Init(c)
+
+       // TODO: Allow CreateFileLines before SetUpPackage, since it matches
+       //  the expected reading order of human readers.
+
+       t.SetUpPackage("category/package",
+               "ASSIGN_DIFF=\t\tpkg",          // assignment, differs from default value
+               "ASSIGN_DIFF2=\t\treally # ok", // ok because of the rationale in the comment
+               "ASSIGN_SAME=\t\tdefault",      // assignment, same value as default
+               "DEFAULT_DIFF?=\t\tpkg",        // default, differs from default value
+               "DEFAULT_SAME?=\t\tdefault",    // same value as default
+               "FETCH_USING=\t\tcurl",         // both user-settable and package-settable
+               "APPEND_DIRS+=\t\tdir3",        // appending requires a separate diagnostic
+               "COMMENTED_SAME?=\tdefault",    // commented default, same value as default
+               "COMMENTED_DIFF?=\tpkg")        // commented default, differs from default value
+       t.CreateFileLines("mk/defaults/mk.conf",
+               MkCvsID,
+               "ASSIGN_DIFF?=default",
+               "ASSIGN_DIFF2?=default",
+               "ASSIGN_SAME?=default",
+               "DEFAULT_DIFF?=\tdefault",
+               "DEFAULT_SAME?=\tdefault",
+               "FETCH_USING=\tauto",
+               "APPEND_DIRS=\tdefault",
+               "#COMMENTED_SAME?=\tdefault",
+               "#COMMENTED_DIFF?=\tdefault")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       G.Check(".")
+
+       t.CheckOutputLines(
+               "WARN: Makefile:20: Package sets user-defined \"ASSIGN_DIFF\" to \"pkg\", "+
+                       "which differs from the default value \"default\" from mk/defaults/mk.conf.",
+               "NOTE: Makefile:22: Redundant definition for ASSIGN_SAME from mk/defaults/mk.conf.",
+               "WARN: Makefile:23: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".",
+               "WARN: Makefile:23: Package sets user-defined \"DEFAULT_DIFF\" to \"pkg\", "+
+                       "which differs from the default value \"default\" from mk/defaults/mk.conf.",
+               "NOTE: Makefile:24: Redundant definition for DEFAULT_SAME from mk/defaults/mk.conf.",
+               "WARN: Makefile:26: Packages should not append to user-settable APPEND_DIRS.",
+               "WARN: Makefile:28: Package sets user-defined \"COMMENTED_DIFF\" to \"pkg\", "+
+                       "which differs from the default value \"default\" from mk/defaults/mk.conf.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftUserSettable__before_prefs(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--explain")
+       t.SetUpPackage("category/package",
+               "BEFORE=\tvalue",
+               ".include \"../../mk/bsd.prefs.mk\"")
+       t.CreateFileLines("mk/defaults/mk.conf",
+               MkCvsID,
+               "BEFORE?=\tvalue")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       G.Check(".")
+
+       t.CheckOutputLines(
+               "NOTE: Makefile:20: Redundant definition for BEFORE from mk/defaults/mk.conf.",
+               "",
+               "\tInstead of defining the variable redundantly, it suffices to include",
+               "\t../../mk/bsd.prefs.mk, which provides all user-settable variables.",
+               "")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftUserSettable__after_prefs(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--explain")
+       t.SetUpPackage("category/package",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "AFTER=\tvalue")
+       t.CreateFileLines("mk/defaults/mk.conf",
+               MkCvsID,
+               "AFTER?=\t\tvalue")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       G.Check(".")
+
+       t.CheckOutputLines(
+               "NOTE: Makefile:21: Redundant definition for AFTER from mk/defaults/mk.conf.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftUserSettable__vartype_nil(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("category/package/vars.mk",
+               MkCvsID,
+               "#",
+               "# User-settable variables:",
+               "#",
+               "# USER_SETTABLE",
+               "#\tDocumentation for USER_SETTABLE.",
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
+               "USER_SETTABLE?=\tdefault")
+       t.SetUpPackage("category/package",
+               "USER_SETTABLE=\tvalue")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       G.Check(".")
+
+       // TODO: As of June 2019, pkglint doesn't parse the "User-settable variables"
+       //  comment. Therefore it doesn't know that USER_SETTABLE is intended to be
+       //  used by other packages. There should be no warning.
+       t.CheckOutputLines(
+               "WARN: Makefile:20: USER_SETTABLE is defined but not used.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftPermissions__hacks_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       mklines := t.NewMkLines("hacks.mk",
+               MkCvsID,
+               "OPSYS=\t${PKGREVISION}")
+
+       mklines.Check()
+
+       // No matter how strange the definition or use of a variable sounds,
+       // in hacks.mk it is allowed. Special problems sometimes need solutions
+       // that violate all standards.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftPermissions(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpTool("awk", "AWK", AtRunTime)
+       G.Pkgsrc.vartypes.DefineParse("SET_ONLY", BtUnknown, NoVartypeOptions,
+               "options.mk: set")
+       G.Pkgsrc.vartypes.DefineParse("SET_ONLY_DEFAULT_ELSEWHERE", BtUnknown, NoVartypeOptions,
+               "options.mk: set",
+               "*.mk: default, set")
+       mklines := t.NewMkLines("options.mk",
+               MkCvsID,
+               "PKG_DEVELOPER?=\tyes",
+               "BUILD_DEFS?=\tVARBASE",
+               "USE_TOOLS:=\t${USE_TOOLS:Nunwanted-tool}",
+               "USE_TOOLS:=\t${MY_TOOLS}",
+               "USE_TOOLS:=\tawk",
+               "",
+               "SET_ONLY=\tset",
+               "SET_ONLY:=\teval",
+               "SET_ONLY?=\tdefault",
+               "",
+               "SET_ONLY_DEFAULT_ELSEWHERE=\tset",
+               "SET_ONLY_DEFAULT_ELSEWHERE:=\teval",
+               "SET_ONLY_DEFAULT_ELSEWHERE?=\tdefault")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: options.mk:2: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".",
+               "WARN: options.mk:2: The variable PKG_DEVELOPER should not be given a default value by any package.",
+               "WARN: options.mk:3: The variable BUILD_DEFS should not be given a default value (only appended to) in this file.",
+               "WARN: options.mk:4: USE_TOOLS should not be used at load time in this file; "+
+                       "it would be ok in Makefile.common or builtin.mk, but not buildlink3.mk or *.",
+               "WARN: options.mk:5: MY_TOOLS is used but not defined.",
+               "WARN: options.mk:10: "+
+                       "The variable SET_ONLY should not be given a default value "+
+                       "(only set) in this file.",
+               "WARN: options.mk:14: "+
+                       "The variable SET_ONLY_DEFAULT_ELSEWHERE should not be given a "+
+                       "default value (only set) in this file; it would be ok in *.mk, "+
+                       "but not options.mk.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftPermissions__no_tracing(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.DisableTracing() // Just to reach branch coverage for unknown permissions.
+       mklines := t.NewMkLines("options.mk",
+               MkCvsID,
+               "COMMENT=\tShort package description")
+
+       mklines.Check()
+}
+
+// Setting a default license is typical for big software projects
+// like GNOME or KDE that consist of many packages, or for programming
+// languages like Perl or Python that suggest certain licenses.
+//
+// The default license is typically set in a Makefile.common or module.mk.
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftPermissions__license_default(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "LICENSE?=\tgnu-gpl-v2")
+       t.FinishSetUp()
+
+       mklines.Check()
+
+       // 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.
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftPermissions__infrastructure(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.CreateFileLines("mk/infra.mk",
+               MkCvsID,
+               "",
+               "PKG_DEVELOPER?=\tyes")
+       t.CreateFileLines("mk/bsd.pkg.mk")
+
+       G.Check(t.File("mk/infra.mk"))
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignLeftRationale(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       testLines := func(lines []string, diagnostics ...string) {
+               mklines := t.NewMkLines("filename.mk",
+                       lines...)
+
+               mklines.Check()
+
+               t.CheckOutput(diagnostics)
+       }
+       test := func(lines []string, diagnostics ...string) {
+               testLines(append([]string{MkCvsID, ""}, lines...), diagnostics...)
+       }
+       lines := func(lines ...string) []string { return lines }
+
+       test(
+               lines(
+                       MkCvsID,
+                       "ONLY_FOR_PLATFORM=\t*-*-*", // The CVS Id above is not a rationale.
+                       "NOT_FOR_PLATFORM=\t*-*-*",  // Neither does this line have a rationale.
+               ),
+               "WARN: filename.mk:4: Setting variable ONLY_FOR_PLATFORM should have a rationale.",
+               "WARN: filename.mk:5: Setting variable NOT_FOR_PLATFORM should have a rationale.")
+
+       test(
+               lines(
+                       "ONLY_FOR_PLATFORM+=\t*-*-* # rationale in the same line"),
+               nil...)
+
+       test(
+               lines(
+                       "",
+                       "# rationale in the line above",
+                       "ONLY_FOR_PLATFORM+=\t*-*-*"),
+               nil...)
+
+       // A commented variable assignment does not count as a rationale,
+       // since it is not in plain text.
+       test(
+               lines(
+                       "#VAR=\tvalue",
+                       "ONLY_FOR_PLATFORM+=\t*-*-*"),
+               "WARN: filename.mk:4: Setting variable ONLY_FOR_PLATFORM should have a rationale.")
+
+       // Another variable assignment with comment does not count as a rationale.
+       test(
+               lines(
+                       "PKGNAME=\t\tpackage-1.0 # this is not a rationale",
+                       "ONLY_FOR_PLATFORM+=\t*-*-*"),
+               "WARN: filename.mk:4: Setting variable ONLY_FOR_PLATFORM should have a rationale.")
+
+       // A rationale applies to all variable assignments directly below it.
+       test(
+               lines(
+                       "# rationale",
+                       "BROKEN_ON_PLATFORM+=\t*-*-*",
+                       "BROKEN_ON_PLATFORM+=\t*-*-*"), // The rationale applies to this line, too.
+               nil...)
+
+       // Just for code coverage.
+       test(
+               lines(
+                       "PKGNAME=\tpackage-1.0", // Does not need a rationale.
+                       "UNKNOWN=\t${UNKNOWN}"), // Unknown type, does not need a rationale.
+               nil...)
+
+       // When a line requiring a rationale appears in the very first line
+       // or in the second line of a file, there is no index out of bounds error.
+       testLines(
+               lines(
+                       "NOT_FOR_PLATFORM=\t*-*-*",
+                       "NOT_FOR_PLATFORM=\t*-*-*"),
+               sprintf("ERROR: filename.mk:1: Expected %q.", MkCvsID),
+               "WARN: filename.mk:1: Setting variable NOT_FOR_PLATFORM should have a rationale.",
+               "WARN: filename.mk:2: Setting variable NOT_FOR_PLATFORM should have a rationale.")
+
+       // The whole rationale check is only enabled when -Wextra is given.
+       t.SetUpCommandLine()
+
+       test(
+               lines(
+                       MkCvsID,
+                       "ONLY_FOR_PLATFORM=\t*-*-*", // The CVS Id above is not a rationale.
+                       "NOT_FOR_PLATFORM=\t*-*-*",  // Neither does this line have a rationale.
+               ),
+               nil...)
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignOp(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpTool("uname", "UNAME", AfterPrefsMk)
+       t.SetUpTool("echo", "", AtRunTime)
+       t.SetUpPkgsrc()
+       t.SetUpPackage("category/package",
+               "OPSYS_NAME!=\t${UNAME}",
+               "",
+               "PKG_FAIL_REASON+=\t${OPSYS_NAME}")
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:20: "+
+                       "Consider the :sh modifier instead of != for \"${UNAME}\".",
+               "WARN: ~/category/package/Makefile:20: "+
+                       "To use the tool ${UNAME} at load time, "+
+                       "bsd.prefs.mk has to be included before.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignOpShell(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpTool("uname", "UNAME", AfterPrefsMk)
+       t.SetUpTool("echo", "", AtRunTime)
+       t.SetUpPkgsrc()
+       t.SetUpPackage("category/package",
+               ".include \"standalone.mk\"")
+       t.CreateFileLines("category/package/standalone.mk",
+               MkCvsID,
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
+               "OPSYS_NAME!=\t${UNAME}",
+               ".if ${OPSYS_NAME} == \"NetBSD\"",
+               ".endif",
+               "",
+               "OS_NAME!=\t${UNAME}",
+               "",
+               "MUST_BE_EARLY!=\techo 123 # must be evaluated early",
+               "",
+               "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.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"))
+
+       // There is no warning for OPSYS_NAME since that variable is used at
+       // load time. In such a case the command has to be executed anyway,
+       // and executing it exactly once is the best thing to do.
+       //
+       // There is no warning for MUST_BE_EARLY since the comment provides the
+       // reason that this command really has to be executed at load time.
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/standalone.mk:9: Consider the :sh modifier instead of != for \"${UNAME}\".",
+               "",
+               "\tFor variable assignments using the != operator, the shell command is",
+               "\trun every time the file is parsed. In some cases this is too early,",
+               "\tand the command may not yet be installed. In other cases the command",
+               "\tis executed more often than necessary. Most commands don't need to",
+               "\tbe executed for \"make clean\", for example.",
+               "",
+               "\tThe :sh modifier defers execution until the variable value is",
+               "\tactually needed. On the other hand, this means the command is",
+               "\texecuted each time the variable is evaluated.",
+               "",
+               "\tExample:",
+               "",
+               "\t\tEARLY_YEAR!=    date +%Y",
+               "",
+               "\t\tLATE_YEAR_CMD=  date +%Y",
+               "\t\tLATE_YEAR=      ${LATE_YEAR_CMD:sh}",
+               "",
+               "\t\t# or, in a single line:",
+               "\t\tLATE_YEAR=      ${date +%Y:L:sh}",
+               "",
+               "\tTo suppress this note, provide an explanation in a comment at the",
+               "\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_MkAssignChecker_checkVarassignRight(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
+               "BUILD_CMD.${UNKNOWN}=\tcd ${WRKSRC}/.. && make")
+
+       mklines.ForEach(func(mkline *MkLine) {
+               ck := NewMkAssignChecker(mkline, mklines)
+               ck.checkVarassignRight()
+       })
+
+       // No warning about the UNKNOWN variable on the left-hand side,
+       // since that is out of scope.
+       t.CheckOutputLines(
+               "WARN: filename.mk:1: Building the package should take place "+
+                       "entirely inside ${WRKSRC}, not \"${WRKSRC}/..\".",
+               "WARN: filename.mk:1: Unknown shell command \"make\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__none(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES=\t# none")
+       t.FinishSetUp()
+
+       G.Check(t.File("obscure/package"))
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__indirect(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES=\t${PKGPATH:C,/.*,,}")
+       t.FinishSetUp()
+
+       G.Check(t.File("obscure/package"))
+
+       // This case does not occur in practice,
+       // therefore it's ok to have these warnings.
+       t.CheckOutputLines(
+               "WARN: ~/obscure/package/Makefile:5: "+
+                       "The primary category should be \"obscure\", not \"${PKGPATH:C,/.*,,}\".",
+               "ERROR: ~/obscure/package/Makefile:5: "+
+                       "Invalid category \"${PKGPATH:C,/.*,,}\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__wrong(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES=\tperl5")
+       t.FinishSetUp()
+
+       G.Check(t.File("obscure/package"))
+
+       t.CheckOutputLines(
+               "WARN: ~/obscure/package/Makefile:5: The primary category should be \"obscure\", not \"perl5\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__wrong_in_package_directory(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES=\tperl5")
+       t.FinishSetUp()
+       t.Chdir("obscure/package")
+
+       G.Check(".")
+
+       t.CheckOutputLines(
+               "WARN: Makefile:5: The primary category should be \"obscure\", not \"perl5\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__append(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES+=\tperl5")
+       t.FinishSetUp()
+
+       G.Check(t.File("obscure/package"))
+
+       // Appending is ok.
+       // In this particular case, appending has the same effect as assigning,
+       // but that can be checked somewhere else.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__default(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES?=\tperl5")
+       t.FinishSetUp()
+
+       G.Check(t.File("obscure/package"))
+
+       // Default assignments set the primary category, just like simple assignments.
+       t.CheckOutputLines(
+               "WARN: ~/obscure/package/Makefile:5: The primary category should be \"obscure\", not \"perl5\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__autofix(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--autofix")
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES=\tperl5 obscure python")
+       t.FinishSetUp()
+
+       G.Check(t.File("obscure/package"))
+
+       t.CheckOutputLines(
+               "AUTOFIX: ~/obscure/package/Makefile:5: " +
+                       "Replacing \"perl5 obscure\" with \"obscure perl5\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__third(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES=\tperl5 python obscure")
+       t.FinishSetUp()
+
+       G.Check(t.File("obscure/package"))
+
+       t.CheckOutputLines(
+               "WARN: ~/obscure/package/Makefile:5: " +
+                       "The primary category should be \"obscure\", not \"perl5\".")
+
+       t.SetUpCommandLine("-Wall", "--show-autofix")
+
+       G.Check(t.File("obscure/package"))
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightCategory__other_file(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("obscure/package",
+               "CATEGORIES=\tperl5 obscure python")
+       mklines := t.SetUpFileMkLines("obscure/package/module.mk",
+               MkCvsID,
+               "",
+               "CATEGORIES=\tperl5")
+       t.FinishSetUp()
+
+       mklines.Check()
+
+       // It doesn't matter in which file the CATEGORIES= line appears.
+       // If it's a plain assignment, it will end up as the primary category.
+       t.CheckOutputLines(
+               "WARN: ~/obscure/package/module.mk:3: " +
+                       "The primary category should be \"obscure\", not \"perl5\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignMisc(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://download.github.com/";)
+
+       mklines := t.SetUpFileMkLines("module.mk",
+               MkCvsID,
+               "EGDIR=\t\t\t${PREFIX}/etc/rc.d",
+               "RPMIGNOREPATH+=\t\t${PREFIX}/etc/rc.d",
+               "_TOOLS_VARNAME.sed=\tSED",
+               "DIST_SUBDIR=\t\t${PKGNAME}",
+               "WRKSRC=\t\t\t${PKGNAME}",
+               "SITES_distfile.tar.gz=\t${MASTER_SITE_GITHUB:=user/}",
+               "MASTER_SITES=\t\thttps://cdn.example.org/${PKGNAME}/";,
+               "MASTER_SITES=\t\thttps://cdn.example.org/distname-${PKGVERSION}/";)
+       t.FinishSetUp()
+
+       mklines.Check()
+
+       // TODO: Split this test into several, one for each topic.
+       t.CheckOutputLines(
+               "WARN: ~/module.mk:2: Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.",
+               "WARN: ~/module.mk:4: Variable names starting with an underscore (_TOOLS_VARNAME.sed) are reserved for internal pkgsrc use.",
+               "WARN: ~/module.mk:4: _TOOLS_VARNAME.sed is defined but not used.",
+               "WARN: ~/module.mk:5: PKGNAME should not be used in DIST_SUBDIR as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
+               "WARN: ~/module.mk:6: PKGNAME should not be used in WRKSRC as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
+               "WARN: ~/module.mk:7: SITES_distfile.tar.gz is defined but not used.",
+               "WARN: ~/module.mk:7: SITES_* is deprecated. Please use SITES.* instead.",
+               "WARN: ~/module.mk:8: PKGNAME should not be used in MASTER_SITES as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
+               "WARN: ~/module.mk:9: PKGVERSION should not be used in MASTER_SITES as it includes the PKGREVISION. Please use PKGVERSION_NOREV instead.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignMisc__multiple_inclusion_guards(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("filename.mk",
+               MkCvsID,
+               ".if !defined(FILENAME_MK)",
+               "FILENAME_MK=\t# defined",
+               ".endif")
+       t.CreateFileLines("Makefile.common",
+               MkCvsID,
+               ".if !defined(MAKEFILE_COMMON)",
+               "MAKEFILE_COMMON=\t# defined",
+               "",
+               ".endif")
+       t.CreateFileLines("other.mk",
+               MkCvsID,
+               "COMMENT=\t# defined")
+       t.FinishSetUp()
+
+       G.Check(t.File("filename.mk"))
+       G.Check(t.File("Makefile.common"))
+       G.Check(t.File("other.mk"))
+
+       // For multiple-inclusion guards, the meaning of the variable value
+       // is clear, therefore they are exempted from the warnings.
+       t.CheckOutputLines(
+               "NOTE: ~/other.mk:2: Please use \"# empty\", \"# none\" or \"# yes\" " +
+                       "instead of \"# defined\".")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignDecreasingVersions(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("Makefile",
+               MkCvsID,
+               "PYTHON_VERSIONS_ACCEPTED=\t36 __future__ # rationale",
+               "PYTHON_VERSIONS_ACCEPTED=\t36 -13 # rationale",
+               "PYTHON_VERSIONS_ACCEPTED=\t36 ${PKGVERSION_NOREV} # rationale",
+               "PYTHON_VERSIONS_ACCEPTED=\t36 37 # rationale",
+               "PYTHON_VERSIONS_ACCEPTED=\t37 36 27 25 # rationale")
+
+       // TODO: All but the last of the above assignments should be flagged as
+       //  redundant by RedundantScope; as of March 2019, that check is only
+       //  implemented for package Makefiles, not for individual files.
+
+       mklines.Check()
+
+       // Half of these warnings are from VartypeCheck.Version, the
+       // other half are from checkVarassignDecreasingVersions.
+       // Strictly speaking some of them are redundant, but that would
+       // mean to reject only variable references in checkVarassignDecreasingVersions.
+       // This is probably ok.
+       // TODO: Fix this.
+       t.CheckOutputLines(
+               "WARN: Makefile:2: Invalid version number \"__future__\".",
+               "ERROR: Makefile:2: Value \"__future__\" for "+
+                       "PYTHON_VERSIONS_ACCEPTED must be a positive integer.",
+               "WARN: Makefile:3: Invalid version number \"-13\".",
+               "ERROR: Makefile:3: Value \"-13\" for "+
+                       "PYTHON_VERSIONS_ACCEPTED must be a positive integer.",
+               "ERROR: Makefile:4: Value \"${PKGVERSION_NOREV}\" for "+
+                       "PYTHON_VERSIONS_ACCEPTED must be a positive integer.",
+               "WARN: Makefile:5: The values for PYTHON_VERSIONS_ACCEPTED "+
+                       "should be in decreasing order (37 before 36).")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignMiscRedundantInstallationDirs__AUTO_MKDIRS_yes(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "INSTALLATION_DIRS=\tbin man ${PKGMANDIR}",
+               "AUTO_MKDIRS=\t\tyes")
+       t.CreateFileLines("category/package/PLIST",
+               PlistCvsID,
+               "bin/program",
+               "man/man1/program.1")
+       t.FinishSetUp()
+
+       G.checkdirPackage(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:20: "+
+                       "The directory \"bin\" is redundant in INSTALLATION_DIRS.",
+               "NOTE: ~/category/package/Makefile:20: "+
+                       "The directory \"man\" is redundant in INSTALLATION_DIRS.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignMiscRedundantInstallationDirs__AUTO_MKDIRS_no(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "INSTALLATION_DIRS=\tbin man ${PKGMANDIR}",
+               "AUTO_MKDIRS=\t\tno")
+       t.CreateFileLines("category/package/PLIST",
+               PlistCvsID,
+               "bin/program",
+               "man/man1/program.1")
+       t.FinishSetUp()
+
+       G.checkdirPackage(t.File("category/package"))
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignMiscRedundantInstallationDirs__absolute(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "INSTALLATION_DIRS=\t/bin",
+               "AUTO_MKDIRS=\t\tyes")
+       t.CreateFileLines("category/package/PLIST",
+               PlistCvsID,
+               "bin/program",
+               "man/man1/program.1")
+       t.FinishSetUp()
+
+       G.checkdirPackage(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/Makefile:20: " +
+                       "The pathname \"/bin\" in INSTALLATION_DIRS " +
+                       "must be relative to ${PREFIX}.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignRightVaruse(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       mklines := t.NewMkLines("module.mk",
+               MkCvsID,
+               "PLIST_SUBST+=\tLOCALBASE=${LOCALBASE:Q}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: module.mk:2: Please use PREFIX instead of LOCALBASE.",
+               "NOTE: module.mk:2: The :Q modifier isn't necessary for ${LOCALBASE} here.")
+}
+
+func (s *Suite) Test_MkAssignChecker_checkVarassignVaruseShell(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("filename.mk",
+               "EXAMPLE_CMD=\tgrep word ${EXAMPLE_FILES}; continue")
+
+       mklines.ForEach(func(mkline *MkLine) {
+               ck := NewMkAssignChecker(mkline, mklines)
+               ck.checkVarassignRight()
+       })
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:1: Unknown shell command \"grep\".",
+               "WARN: filename.mk:1: EXAMPLE_FILES is used but not defined.")
+}
Index: pkgsrc/pkgtools/pkglint/files/mkcondchecker.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/mkcondchecker.go:1.1
--- /dev/null   Sun Dec  8 00:06:39 2019
+++ pkgsrc/pkgtools/pkglint/files/mkcondchecker.go      Sun Dec  8 00:06:38 2019
@@ -0,0 +1,303 @@
+package pkglint
+
+import "netbsd.org/pkglint/textproc"
+
+// MkCondChecker checks conditions in Makefiles.
+// These conditions occur in .if and .elif clauses, as well as the
+// :? modifier.
+type MkCondChecker struct {
+       MkLine  *MkLine
+       MkLines *MkLines
+}
+
+func NewMkCondChecker(mkLine *MkLine, mkLines *MkLines) *MkCondChecker {
+       return &MkCondChecker{MkLine: mkLine, MkLines: mkLines}
+}
+
+func (ck *MkCondChecker) checkDirectiveCond() {
+       mkline := ck.MkLine
+       if trace.Tracing {
+               defer trace.Call1(mkline.Args())()
+       }
+
+       p := NewMkParser(nil, mkline.Args()) // No emitWarnings here, see the code below.
+       cond := p.MkCond()
+       if !p.EOF() {
+               mkline.Warnf("Invalid condition, unrecognized part: %q.", p.Rest())
+               return
+       }
+
+       checkVarUse := func(varuse *MkVarUse) {
+               var vartype *Vartype // TODO: Insert a better type guess here.
+               vuc := VarUseContext{vartype, VucLoadTime, VucQuotPlain, false}
+               NewMkVarUseChecker(varuse, ck.MkLines, mkline).Check(&vuc)
+       }
+
+       // Skip subconditions that have already been handled as part of the !(...).
+       done := make(map[interface{}]bool)
+
+       checkNotEmpty := func(not *MkCond) {
+               empty := not.Empty
+               if empty != nil {
+                       ck.checkDirectiveCondEmpty(empty, true, true)
+                       done[empty] = true
+               }
+
+               if not.Term != nil && not.Term.Var != nil {
+                       varUse := not.Term.Var
+                       ck.checkDirectiveCondEmpty(varUse, false, false)
+                       done[varUse] = true
+               }
+       }
+
+       checkEmpty := func(empty *MkVarUse) {
+               if !done[empty] {
+                       ck.checkDirectiveCondEmpty(empty, true, false)
+               }
+       }
+
+       checkVar := func(varUse *MkVarUse) {
+               if !done[varUse] {
+                       ck.checkDirectiveCondEmpty(varUse, false, true)
+               }
+       }
+
+       cond.Walk(&MkCondCallback{
+               Not:     checkNotEmpty,
+               Empty:   checkEmpty,
+               Var:     checkVar,
+               Compare: ck.checkDirectiveCondCompare,
+               VarUse:  checkVarUse})
+}
+
+// checkDirectiveCondEmpty checks a condition of the form empty(VAR),
+// empty(VAR:Mpattern) or ${VAR:Mpattern} in an .if directive.
+func (ck *MkCondChecker) checkDirectiveCondEmpty(varuse *MkVarUse, fromEmpty bool, neg bool) {
+       ck.checkDirectiveCondEmptyExpr(varuse)
+       ck.checkDirectiveCondEmptyType(varuse)
+       ck.simplifyCondition(varuse, fromEmpty, neg)
+}
+
+func (ck *MkCondChecker) checkDirectiveCondEmptyExpr(varuse *MkVarUse) {
+       if !matches(varuse.varname, `^\$.*:[MN]`) {
+               return
+       }
+
+       ck.MkLine.Warnf("The empty() function takes a variable name as parameter, " +
+               "not a variable expression.")
+       ck.MkLine.Explain(
+               "Instead of empty(${VARNAME:Mpattern}), you should write either of the following:",
+               "",
+               "\tempty(VARNAME:Mpattern)",
+               "\t${VARNAME:Mpattern} == \"\"",
+               "",
+               "Instead of !empty(${VARNAME:Mpattern}), you should write either of the following:",
+               "",
+               "\t!empty(VARNAME:Mpattern)",
+               "\t${VARNAME:Mpattern}")
+}
+
+func (ck *MkCondChecker) checkDirectiveCondEmptyType(varuse *MkVarUse) {
+       for _, modifier := range varuse.modifiers {
+               ok, _, pattern, _ := modifier.MatchMatch()
+               if ok {
+                       mkLineChecker := NewMkLineChecker(ck.MkLines, ck.MkLine)
+                       mkLineChecker.checkVartype(varuse.varname, opUseMatch, pattern, "")
+                       continue
+               }
+
+               switch modifier.Text {
+               default:
+                       return
+               case "O", "u":
+               }
+       }
+}
+
+// mkCondStringLiteralUnquoted contains a safe subset of the characters
+// that may be used without surrounding quotes in a comparison such as
+// ${PKGPATH} == category/package.
+var mkCondStringLiteralUnquoted = textproc.NewByteSet("+---./0-9@A-Z_a-z")
+
+// mkCondModifierPatternLiteral contains a safe subset of the characters
+// that are interpreted literally in the :M and :N modifiers.
+var mkCondModifierPatternLiteral = textproc.NewByteSet("+---./0-9<=>@A-Z_a-z")
+
+// simplifyCondition replaces an unnecessarily complex condition with
+// a simpler condition that's still equivalent.
+//
+// * fromEmpty is true for the form empty(VAR...), and false for ${VAR...}.
+//
+// * neg is true for the form !empty(VAR...), and false for empty(VAR...).
+// It also applies to the ${VAR} form.
+func (ck *MkCondChecker) simplifyCondition(varuse *MkVarUse, fromEmpty bool, neg bool) {
+       varname := varuse.varname
+       mods := varuse.modifiers
+       modifiers := mods
+
+       n := len(modifiers)
+       if n == 0 {
+               return
+       }
+       modsExceptLast := NewMkVarUse("", mods[:n-1]...).Mod()
+       vartype := G.Pkgsrc.VariableType(ck.MkLines, varname)
+
+       isDefined := func() bool {
+               if vartype.IsAlwaysInScope() && vartype.IsDefinedIfInScope() {
+                       return true
+               }
+
+               if ck.MkLines.vars.IsDefined(varname) {
+                       return true
+               }
+
+               return ck.MkLines.Tools.SeenPrefs &&
+                       vartype.Union().Contains(aclpUseLoadtime) &&
+                       vartype.IsDefinedIfInScope()
+       }
+
+       // replace constructs the state before and after the autofix.
+       // The before state is constructed to ensure that only very simple
+       // patterns get replaced automatically.
+       //
+       // Before putting any cases involving special characters into
+       // production, there need to be more tests for the edge cases.
+       replace := func(positive bool, pattern string) (bool, string, string) {
+               defined := isDefined()
+               if !defined && !positive {
+                       // TODO: This is a double negation, maybe even triple.
+                       //  There is an :N pattern, and the variable may be undefined.
+                       //  If it is indeed undefined, should the whole condition
+                       //  evaluate to true or false?
+                       //  The cases to be distinguished are: undefined, empty, filled.
+
+                       // For now, be conservative and don't suggest anything wrong.
+                       return false, "", ""
+               }
+               uMod := condStr(!defined && !varuse.HasModifier("U"), ":U", "")
+
+               op := condStr(neg == positive, "==", "!=")
+
+               from := sprintf("%s%s%s%s%s%s%s",
+                       condStr(neg != fromEmpty, "", "!"),
+                       condStr(fromEmpty, "empty(", "${"),
+                       varname,
+                       modsExceptLast,
+                       condStr(positive, ":M", ":N"),
+                       pattern,
+                       condStr(fromEmpty, ")", "}"))
+
+               needsQuotes := textproc.NewLexer(pattern).NextBytesSet(mkCondStringLiteralUnquoted) != pattern ||
+                       matches(pattern, `^\d+\.?\d*$`)
+               quote := condStr(needsQuotes, "\"", "")
+
+               to := sprintf(
+                       "${%s%s%s} %s %s%s%s",
+                       varname, uMod, modsExceptLast, op, quote, pattern, quote)
+
+               return true, from, to
+       }
+
+       modifier := modifiers[n-1]
+       ok, positive, pattern, exact := modifier.MatchMatch()
+       if !ok || !positive && n != 1 {
+               return
+       }
+
+       switch {
+       case !exact,
+               vartype == nil,
+               vartype.IsList(),
+               textproc.NewLexer(pattern).NextBytesSet(mkCondModifierPatternLiteral) != pattern:
+               return
+       }
+
+       ok, from, to := replace(positive, pattern)
+       if !ok {
+               return
+       }
+
+       fix := ck.MkLine.Autofix()
+       fix.Notef("%s should be compared using \"%s\" instead of matching against %q.",
+               varname, to, ":"+modifier.Text)
+       fix.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.",
+               "",
+               "An entirely different case is when the pattern contains",
+               "wildcards like *, ?, [].",
+               "In such a case, using the :M or :N modifiers is useful and preferred.")
+       fix.Replace(from, to)
+       fix.Apply()
+}
+
+func (ck *MkCondChecker) checkDirectiveCondCompare(left *MkCondTerm, op string, right *MkCondTerm) {
+       switch {
+       case left.Var != nil && right.Var == nil && right.Num == "":
+               ck.checkDirectiveCondCompareVarStr(left.Var, op, right.Str)
+       }
+}
+
+func (ck *MkCondChecker) checkDirectiveCondCompareVarStr(varuse *MkVarUse, op string, str string) {
+       varname := varuse.varname
+       varmods := varuse.modifiers
+       switch len(varmods) {
+       case 0:
+               ck.checkCompareVarStr(varname, op, str)
+
+       case 1:
+               if m, _, pattern, _ := varmods[0].MatchMatch(); m {
+                       mkLineChecker := NewMkLineChecker(ck.MkLines, ck.MkLine)
+                       mkLineChecker.checkVartype(varname, opUseMatch, pattern, "")
+
+                       // After applying the :M or :N modifier, every expression may end up empty,
+                       // regardless of its data type. Therefore there's no point in type-checking that case.
+                       if str != "" {
+                               mkLineChecker.checkVartype(varname, opUseCompare, str, "")
+                       }
+               }
+
+       default:
+               // This case covers ${VAR:Mfilter:O:u} or similar uses in conditions.
+               // To check these properly, pkglint first needs to know the most common
+               // modifiers and how they interact.
+               // As of March 2019, the modifiers are not modeled.
+               // The following tracing statement makes it easy to discover these cases,
+               // in order to decide whether checking them is worthwhile.
+               if trace.Tracing {
+                       trace.Stepf("checkCompareVarStr ${%s%s} %s %s",
+                               varuse.varname, varuse.Mod(), op, str)
+               }
+       }
+}
+
+func (ck *MkCondChecker) checkCompareVarStr(varname, op, value string) {
+       mkLineChecker := NewMkLineChecker(ck.MkLines, ck.MkLine)
+       mkLineChecker.checkVartype(varname, opUseCompare, value, "")
+
+       if varname == "PKGSRC_COMPILER" {
+               ck.checkCompareVarStrCompiler(op, value)
+       }
+}
+
+func (ck *MkCondChecker) checkCompareVarStrCompiler(op string, value string) {
+       if !matches(value, `^\w+$`) {
+               return
+       }
+
+       // It would be nice if original text of the whole comparison expression
+       // were available at this point, to avoid guessing how much whitespace
+       // the package author really used.
+
+       matchOp := condStr(op == "==", "M", "N")
+
+       fix := ck.MkLine.Autofix()
+       fix.Errorf("Use ${PKGSRC_COMPILER:%s%s} instead of the %s operator.", matchOp, value, op)
+       fix.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.")
+       fix.Replace("${PKGSRC_COMPILER} "+op+" "+value, "${PKGSRC_COMPILER:"+matchOp+value+"}")
+       fix.Replace("${PKGSRC_COMPILER} "+op+" \""+value+"\"", "${PKGSRC_COMPILER:"+matchOp+value+"}")
+       fix.Apply()
+}
Index: pkgsrc/pkgtools/pkglint/files/mkcondchecker_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/mkcondchecker_test.go:1.1
--- /dev/null   Sun Dec  8 00:06:39 2019
+++ pkgsrc/pkgtools/pkglint/files/mkcondchecker_test.go Sun Dec  8 00:06:38 2019
@@ -0,0 +1,1083 @@
+package pkglint
+
+import "gopkg.in/check.v1"
+
+func (s *Suite) Test_NewMkCondChecker(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID)
+
+       ck := NewMkCondChecker(mklines.mklines[0], mklines)
+
+       t.CheckEquals(ck.MkLine.Text, MkCvsID)
+       t.CheckEquals(ck.MkLines, mklines)
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCond(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       test := func(cond string, output ...string) {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       MkCvsID,
+                       ".include \"../../mk/bsd.fast.prefs.mk\"",
+                       "",
+                       cond,
+                       ".endif")
+               mklines.Check()
+               t.CheckOutput(output)
+       }
+
+       test(
+               ".if !empty(PKGSRC_COMPILER:Mmycc)",
+               "WARN: filename.mk:4: The pattern \"mycc\" cannot match any of "+
+                       "{ ccache ccc clang distcc f2c gcc hp icc ido "+
+                       "mipspro mipspro-ucode pcc sunpro xlc } for PKGSRC_COMPILER.")
+
+       test(
+               ".if ${A} != ${B}",
+               "WARN: filename.mk:4: A is used but not defined.",
+               "WARN: filename.mk:4: B is used but not defined.")
+
+       test(".if ${HOMEPAGE} == \"mailto:someone%example.org@localhost\"";,
+               "WARN: filename.mk:4: \"mailto:someone%example.org@localhost\"; is not a valid URL.",
+               "WARN: filename.mk:4: HOMEPAGE should not be used at load time in any file.")
+
+       test(".if !empty(PKGSRC_RUN_TEST:M[Y][eE][sS])",
+               "WARN: filename.mk:4: PKGSRC_RUN_TEST should be matched "+
+                       "against \"[yY][eE][sS]\" or \"[nN][oO]\", not \"[Y][eE][sS]\".")
+
+       test(".if !empty(IS_BUILTIN.Xfixes:M[yY][eE][sS])")
+
+       test(".if !empty(${IS_BUILTIN.Xfixes:M[yY][eE][sS]})",
+               "WARN: filename.mk:4: The empty() function takes a variable name as parameter, "+
+                       "not a variable expression.")
+
+       test(".if ${PKGSRC_COMPILER} == \"msvc\"",
+               "WARN: filename.mk:4: \"msvc\" is not valid for PKGSRC_COMPILER. "+
+                       "Use one of { ccache ccc clang distcc f2c gcc hp icc ido mipspro mipspro-ucode pcc sunpro xlc } instead.",
+               "ERROR: filename.mk:4: Use ${PKGSRC_COMPILER:Mmsvc} instead of the == operator.")
+
+       // PKG_LIBTOOL is only available after including bsd.pkg.mk,
+       // therefore the :U and the subsequent warning.
+       test(".if ${PKG_LIBTOOL:U:Mlibtool}",
+               "NOTE: filename.mk:4: PKG_LIBTOOL "+
+                       "should be compared using \"${PKG_LIBTOOL:U} == libtool\" "+
+                       "instead of matching against \":Mlibtool\".",
+               "WARN: filename.mk:4: PKG_LIBTOOL should not be used at load time in any file.")
+
+       test(".if ${MACHINE_PLATFORM:MUnknownOS-*-*} || ${MACHINE_ARCH:Mx86}",
+               "WARN: filename.mk:4: "+
+                       "The pattern \"UnknownOS\" cannot match any of "+
+                       "{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
+                       "IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
+                       "} for the operating system part of MACHINE_PLATFORM.",
+               "WARN: filename.mk:4: "+
+                       "The pattern \"x86\" cannot match any of "+
+                       "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 cobalt coldfire convex dreamcast earm "+
+                       "earmeb earmhf earmhfeb earmv4 earmv4eb earmv5 earmv5eb earmv6 earmv6eb earmv6hf earmv6hfeb "+
+                       "earmv7 earmv7eb earmv7hf earmv7hfeb evbarm hpcmips hpcsh hppa hppa64 i386 i586 i686 ia64 "+
+                       "m68000 m68k m88k mips mips64 mips64eb mips64el mipseb mipsel mipsn32 mlrisc ns32k pc532 pmax "+
+                       "powerpc powerpc64 rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
+                       "} for MACHINE_ARCH.",
+               "NOTE: filename.mk:4: MACHINE_ARCH "+
+                       "should be compared using \"${MACHINE_ARCH} == x86\" "+
+                       "instead of matching against \":Mx86\".")
+
+       // Doesn't occur in practice since it is surprising that the ! applies
+       // to the comparison operator, and not to one of its arguments.
+       test(".if !${VAR} == value",
+               "WARN: filename.mk:4: VAR is used but not defined.")
+
+       // Doesn't occur in practice since this string can never be empty.
+       test(".if !\"${VAR}str\"",
+               "WARN: filename.mk:4: VAR is used but not defined.")
+
+       // Doesn't occur in practice since !${VAR} && !${VAR2} is more idiomatic.
+       test(".if !\"${VAR}${VAR2}\"",
+               "WARN: filename.mk:4: VAR is used but not defined.",
+               "WARN: filename.mk:4: VAR2 is used but not defined.")
+
+       // Just for code coverage; always evaluates to true.
+       test(".if \"string\"",
+               nil...)
+
+       // Code coverage for checkVar.
+       test(".if ${OPSYS} || ${MACHINE_ARCH}",
+               nil...)
+
+       test(".if ${VAR}",
+               "WARN: filename.mk:4: VAR is used but not defined.")
+
+       test(".if ${VAR} == 3",
+               "WARN: filename.mk:4: VAR is used but not defined.")
+
+       test(".if \"value\" == ${VAR}",
+               "WARN: filename.mk:4: VAR is used but not defined.")
+
+       test(".if ${MASTER_SITES:Mftp://*} == \"ftp://netbsd.org/\"";,
+               // XXX: duplicate diagnostic, see MkParser.MkCond.
+               "WARN: filename.mk:4: Invalid variable modifier \"//*\" for \"MASTER_SITES\".",
+               "WARN: filename.mk:4: Invalid variable modifier \"//*\" for \"MASTER_SITES\".",
+               "WARN: filename.mk:4: \"ftp\" is not a valid URL.",
+               "WARN: filename.mk:4: MASTER_SITES should not be used at load time in any file.")
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCond__tracing(c *check.C) {
+       t := s.Init(c)
+
+       t.EnableTracingToLog()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               ".if ${VAR:Mpattern1:Mpattern2} == comparison",
+               ".endif")
+
+       mklines.Check()
+
+       t.CheckOutputLinesMatching(`^WARN|checkCompare`,
+               "TRACE: 1 2   checkCompareVarStr ${VAR:Mpattern1:Mpattern2} == comparison",
+               "WARN: filename.mk:2: VAR is used but not defined.")
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCond__comparison_with_shell_command(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.SetUpFileMkLines("security/openssl/Makefile",
+               MkCvsID,
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
+               ".if ${PKGSRC_COMPILER} == \"gcc\" && ${CC} == \"cc\"",
+               ".endif")
+
+       mklines.Check()
+
+       // Don't warn about unknown shell command "cc".
+       t.CheckOutputLines(
+               "ERROR: security/openssl/Makefile:5: Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.")
+}
+
+// The :N modifier filters unwanted values. After this filter, any variable value
+// may be compared with the empty string, regardless of the variable type.
+// Effectively, the :N modifier changes the type from T to Option(T).
+func (s *Suite) Test_MkCondChecker_checkDirectiveCond__compare_pattern_with_empty(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "",
+               ".include \"../../mk/bsd.fast.prefs.mk\"",
+               "",
+               ".if ${X11BASE:Npattern} == \"\"",
+               ".endif",
+               "",
+               ".if ${X11BASE:N<>} == \"*\"",
+               ".endif",
+               "",
+               ".if !(${OPSYS:M*BSD} != \"\")",
+               ".endif")
+
+       mklines.Check()
+
+       // TODO: There should be a warning about "<>" containing invalid
+       //  characters for a path. See VartypeCheck.Pathname
+       t.CheckOutputLines(
+               "WARN: filename.mk:8: The pathname pattern \"<>\" contains the invalid characters \"<>\".",
+               "WARN: filename.mk:8: The pathname \"*\" contains the invalid character \"*\".")
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCond__comparing_PKGSRC_COMPILER_with_eqeq(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("Makefile",
+               MkCvsID,
+               "",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
+               ".if ${PKGSRC_COMPILER} == \"clang\"",
+               ".elif ${PKGSRC_COMPILER} != \"gcc\"",
+               ".endif")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "ERROR: Makefile:5: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
+               "ERROR: Makefile:6: Use ${PKGSRC_COMPILER:Ngcc} instead of the != operator.")
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCondEmpty(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       // before: the directive before the condition is simplified
+       // after: the directive after the condition is simplified
+       // diagnostics: the usual ones
+       test := func(before, after string, diagnostics ...string) {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       MkCvsID,
+                       "",
+                       before,
+                       ".endif")
+
+               t.ExpectDiagnosticsAutofix(
+                       mklines.Check,
+                       diagnostics...)
+
+               afterMklines := LoadMk(t.File("filename.mk"), MustSucceed)
+               t.CheckEquals(afterMklines.mklines[2].Text, after)
+       }
+
+       test(
+               ".if !empty(OPSYS:MUnknown)",
+               ".if ${OPSYS:U} == Unknown",
+
+               "WARN: filename.mk:3: The pattern \"Unknown\" cannot match any of "+
+                       "{ Cygwin DragonFly FreeBSD Linux NetBSD SunOS } for OPSYS.",
+               "NOTE: filename.mk:3: OPSYS should be "+
+                       "compared using \"${OPSYS:U} == Unknown\" "+
+                       "instead of matching against \":MUnknown\".",
+               // TODO: Turn the bsd.prefs.mk warning into an error,
+               //  once pkglint is confident enough to get this check right.
+               "WARN: filename.mk:3: To use OPSYS at load time, "+
+                       ".include \"../../mk/bsd.prefs.mk\" first.",
+               "AUTOFIX: filename.mk:3: Replacing \"!empty(OPSYS:MUnknown)\" "+
+                       "with \"${OPSYS:U} == Unknown\".")
+
+       // The condition can only be simplified if the :M or :N modifier is
+       // the last one on the chain.
+       test(
+               ".if !empty(OPSYS:O:MUnknown:S,a,b,)",
+               ".if !empty(OPSYS:O:MUnknown:S,a,b,)",
+
+               "WARN: filename.mk:3: The pattern \"Unknown\" cannot match any of "+
+                       "{ Cygwin DragonFly FreeBSD Linux NetBSD SunOS } for OPSYS.",
+               "WARN: filename.mk:3: To use OPSYS at load time, "+
+                       ".include \"../../mk/bsd.prefs.mk\" first.")
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCondEmptyExpr(c *check.C) {
+       t := s.Init(c)
+
+       test := func(use *MkVarUse, diagnostics ...string) {
+               mklines := t.NewMkLines("filename.mk",
+                       "# dummy")
+               ck := NewMkCondChecker(mklines.mklines[0], mklines)
+
+               ck.checkDirectiveCondEmptyExpr(use)
+
+               t.CheckOutput(diagnostics)
+       }
+
+       // In some cases it makes sense to use indirection in a !empty(...)
+       // expression.
+       test(
+               NewMkVarUse("${PREFIX}"),
+
+               nil...)
+
+       // Typical examples for indirection are .for loops.
+       test(
+               NewMkVarUse("${var}"),
+
+               nil...)
+
+       // This one is obvious enough for pkglint.
+       test(
+               NewMkVarUse("${PREFIX:Mpattern}"),
+
+               "WARN: filename.mk:1: The empty() function takes a variable "+
+                       "name as parameter, not a variable expression.")
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCondEmptyType(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       test := func(line string, diagnostics ...string) {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       MkCvsID,
+                       "",
+                       line,
+                       ".endif")
+
+               mklines.ForEach(func(mkline *MkLine) {
+                       ck := NewMkCondChecker(mkline, mklines)
+                       mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) {
+                               ck.checkDirectiveCondEmptyType(varUse)
+                       })
+               })
+
+               t.CheckOutput(diagnostics)
+       }
+
+       test(".if !empty(OPSYS:Mok)",
+               "WARN: filename.mk:3: The pattern \"ok\" cannot match any of "+
+                       "{ Cygwin DragonFly FreeBSD Linux NetBSD SunOS } for OPSYS.")
+
+       // As of December 2019, pkglint doesn't analyze the :S modifier in
+       // depth and therefore simply skips the type check for the :M
+       // modifier.
+       test(".if !empty(OPSYS:S,NetBSD,ok,:Mok)",
+               nil...)
+       test(".if !empty(OPSYS:C,NetBSD,ok,:Mok)",
+               nil...)
+
+       // Several other modifiers are ok since they don't modify the
+       // individual words.
+       test(".if !empty(OPSYS:O:u:Mok)",
+               "WARN: filename.mk:3: The pattern \"ok\" cannot match any of "+
+                       "{ Cygwin DragonFly FreeBSD Linux NetBSD SunOS } for OPSYS.")
+
+       // Other modifiers can modify the words themselves. As long as
+       // pkglint doesn't actually evaluate these modifiers, suppress
+       // any warnings.
+       test(".if !empty(OPSYS:E:Mok)",
+               nil...)
+       test(".if !empty(OPSYS:H:Mok)",
+               nil...)
+       test(".if !empty(OPSYS:R:Mok)",
+               nil...)
+       test(".if !empty(OPSYS:tl:Mok)",
+               nil...)
+       test(".if !empty(OPSYS:tW:Mok)",
+               nil...)
+       test(".if !empty(OPSYS:tW:Mok)",
+               nil...)
+}
+
+func (s *Suite) Test_MkCondChecker_simplifyCondition(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       // prefs: whether to include bsd.prefs.mk before
+       // before: the directive before the condition is simplified
+       // after: the directive after the condition is simplified
+       test := func(prefs bool, before, after string, diagnostics ...string) {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       MkCvsID,
+                       condStr(prefs, ".include \"../../mk/bsd.prefs.mk\"", ""),
+                       before,
+                       ".endif")
+
+               // The high-level call MkLines.Check is used here to
+               // get MkLines.Tools.SeenPrefs correct, which decides
+               // whether the :U modifier is necessary.
+               //
+               // TODO: Replace MkLines.Check this with a more specific method.
+
+               t.ExpectDiagnosticsAutofix(
+                       mklines.Check,
+                       diagnostics...)
+
+               // TODO: Move this assertion above the assertion about the diagnostics.
+               afterMklines := LoadMk(t.File("filename.mk"), MustSucceed)
+               t.CheckEquals(afterMklines.mklines[2].Text, after)
+       }
+       testAfterPrefs := func(before, after string, diagnostics ...string) {
+               test(true, before, after, diagnostics...)
+       }
+
+       test(
+               false,
+               ".if ${PKGPATH:Mpattern}",
+               ".if ${PKGPATH:U} == pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH:U} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "WARN: filename.mk:3: To use PKGPATH at load time, "+
+                       ".include \"../../mk/bsd.prefs.mk\" first.",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Mpattern}\" "+
+                       "with \"${PKGPATH:U} == pattern\".")
+
+       testAfterPrefs(
+               ".if ${PKGPATH:Mpattern}",
+               ".if ${PKGPATH} == pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Mpattern}\" "+
+                       "with \"${PKGPATH} == pattern\".")
+
+       // When the pattern contains placeholders, it cannot be converted to == or !=.
+       testAfterPrefs(
+               ".if ${PKGPATH:Mpa*n}",
+               ".if ${PKGPATH:Mpa*n}",
+
+               nil...)
+
+       // When deciding whether to replace the expression, only the
+       // last modifier is inspected. All the others are copied.
+       testAfterPrefs(
+               ".if ${PKGPATH:tl:Mpattern}",
+               ".if ${PKGPATH:tl} == pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH:tl} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:tl:Mpattern}\" "+
+                       "with \"${PKGPATH:tl} == pattern\".")
+
+       // Negated pattern matches are supported as well,
+       // as long as the variable is guaranteed to be nonempty.
+       testAfterPrefs(
+               ".if ${PKGPATH:Ncategory/package}",
+               ".if ${PKGPATH} != category/package",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} != category/package\" "+
+                       "instead of matching against \":Ncategory/package\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Ncategory/package}\" "+
+                       "with \"${PKGPATH} != category/package\".")
+
+       // ${PKGPATH:None:Ntwo} is a short variant of ${PKGPATH} != "one" &&
+       // ${PKGPATH} != "two". Applying the transformation would make the
+       // condition longer than before, therefore nothing is done here.
+       testAfterPrefs(
+               ".if ${PKGPATH:None:Ntwo}",
+               ".if ${PKGPATH:None:Ntwo}",
+
+               nil...)
+
+       // Note: this combination doesn't make sense since the patterns
+       // "one" and "two" don't overlap.
+       testAfterPrefs(
+               ".if ${PKGPATH:Mone:Mtwo}",
+               ".if ${PKGPATH:Mone} == two",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH:Mone} == two\" "+
+                       "instead of matching against \":Mtwo\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Mone:Mtwo}\" "+
+                       "with \"${PKGPATH:Mone} == two\".")
+
+       testAfterPrefs(
+               ".if !empty(PKGPATH:Mpattern)",
+               ".if ${PKGPATH} == pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"!empty(PKGPATH:Mpattern)\" "+
+                       "with \"${PKGPATH} == pattern\".")
+
+       testAfterPrefs(
+               ".if empty(PKGPATH:Mpattern)",
+               ".if ${PKGPATH} != pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} != pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"empty(PKGPATH:Mpattern)\" "+
+                       "with \"${PKGPATH} != pattern\".")
+
+       testAfterPrefs(
+               ".if !!empty(PKGPATH:Mpattern)",
+               // TODO: The ! and == could be combined into a !=.
+               //  Luckily the !! pattern doesn't occur in practice.
+               ".if !${PKGPATH} == pattern",
+
+               // TODO: When taking all the ! into account, this is actually a
+               //  test for emptiness, therefore the diagnostics should suggest
+               //  the != operator instead of ==.
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"!empty(PKGPATH:Mpattern)\" "+
+                       "with \"${PKGPATH} == pattern\".")
+
+       testAfterPrefs(".if empty(PKGPATH:Mpattern) || 0",
+               ".if ${PKGPATH} != pattern || 0",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} != pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"empty(PKGPATH:Mpattern)\" "+
+                       "with \"${PKGPATH} != pattern\".")
+
+       // No note in this case since there is no implicit !empty around the varUse.
+       testAfterPrefs(
+               ".if ${PKGPATH:Mpattern} != ${OTHER}",
+               ".if ${PKGPATH:Mpattern} != ${OTHER}",
+
+               "WARN: filename.mk:3: OTHER is used but not defined.")
+
+       testAfterPrefs(
+               ".if ${PKGPATH:Mpattern}",
+               ".if ${PKGPATH} == pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Mpattern}\" "+
+                       "with \"${PKGPATH} == pattern\".")
+
+       testAfterPrefs(
+               ".if !${PKGPATH:Mpattern}",
+               ".if ${PKGPATH} != pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} != pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"!${PKGPATH:Mpattern}\" "+
+                       "with \"${PKGPATH} != pattern\".")
+
+       // TODO: Merge the double negation into the comparison operator.
+       testAfterPrefs(
+               ".if !!${PKGPATH:Mpattern}",
+               ".if !${PKGPATH} != pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} != pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"!${PKGPATH:Mpattern}\" "+
+                       "with \"${PKGPATH} != pattern\".")
+
+       // This pattern with spaces doesn't make sense at all in the :M
+       // modifier since it can never match.
+       // Or can it, if the PKGPATH contains quotes?
+       // How exactly does bmake apply the matching here, are both values unquoted?
+       testAfterPrefs(
+               ".if ${PKGPATH:Mpattern with spaces}",
+               ".if ${PKGPATH:Mpattern with spaces}",
+
+               "WARN: filename.mk:3: The pathname pattern \"pattern with spaces\" "+
+                       "contains the invalid characters \"  \".")
+       // TODO: ".if ${PKGPATH} == \"pattern with spaces\"")
+
+       testAfterPrefs(
+               ".if ${PKGPATH:M'pattern with spaces'}",
+               ".if ${PKGPATH:M'pattern with spaces'}",
+
+               "WARN: filename.mk:3: The pathname pattern \"'pattern with spaces'\" "+
+                       "contains the invalid characters \"'  '\".")
+       // TODO: ".if ${PKGPATH} == 'pattern with spaces'")
+
+       testAfterPrefs(
+               ".if ${PKGPATH:M&&}",
+               ".if ${PKGPATH:M&&}",
+
+               "WARN: filename.mk:3: The pathname pattern \"&&\" "+
+                       "contains the invalid characters \"&&\".")
+       // TODO: ".if ${PKGPATH} == '&&'")
+
+       // If PKGPATH is "", the condition is false.
+       // If PKGPATH is "negative-pattern", the condition is false.
+       // In all other cases, the condition is true.
+       //
+       // Therefore this condition cannot simply be transformed into
+       // ${PKGPATH} != negative-pattern, since that would produce a
+       // different result in the case where PKGPATH is empty.
+       //
+       // For system-provided variables that are guaranteed to be non-empty,
+       // such as OPSYS or PKGPATH, this replacement is valid.
+       // These variables are only guaranteed to be defined after bsd.prefs.mk
+       // has been included, like everywhere else.
+       testAfterPrefs(
+               ".if ${PKGPATH:Nnegative-pattern}",
+               ".if ${PKGPATH} != negative-pattern",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} != negative-pattern\" "+
+                       "instead of matching against \":Nnegative-pattern\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Nnegative-pattern}\" "+
+                       "with \"${PKGPATH} != negative-pattern\".")
+
+       // Since UNKNOWN is not a well-known system-provided variable that is
+       // guaranteed to be non-empty (see the previous example), it is not
+       // transformed at all.
+       test(
+               false,
+               ".if ${UNKNOWN:Nnegative-pattern}",
+               ".if ${UNKNOWN:Nnegative-pattern}",
+
+               "WARN: filename.mk:3: UNKNOWN is used but not defined.")
+
+       test(
+               true,
+               ".if ${UNKNOWN:Nnegative-pattern}",
+               ".if ${UNKNOWN:Nnegative-pattern}",
+
+               "WARN: filename.mk:3: UNKNOWN is used but not defined.")
+
+       testAfterPrefs(
+               ".if ${PKGPATH:Mpath1} || ${PKGPATH:Mpath2}",
+               ".if ${PKGPATH} == path1 || ${PKGPATH} == path2",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} == path1\" "+
+                       "instead of matching against \":Mpath1\".",
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} == path2\" "+
+                       "instead of matching against \":Mpath2\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Mpath1}\" "+
+                       "with \"${PKGPATH} == path1\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Mpath2}\" "+
+                       "with \"${PKGPATH} == path2\".")
+
+       testAfterPrefs(
+               ".if (((((${PKGPATH:Mpath})))))",
+               ".if (((((${PKGPATH} == path)))))",
+
+               "NOTE: filename.mk:3: PKGPATH "+
+                       "should be compared using \"${PKGPATH} == path\" "+
+                       "instead of matching against \":Mpath\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${PKGPATH:Mpath}\" "+
+                       "with \"${PKGPATH} == path\".")
+
+       // MACHINE_ARCH is built-in into bmake and is always available.
+       // Therefore it doesn't matter whether bsd.prefs.mk is included or not.
+       test(
+               false,
+               ".if ${MACHINE_ARCH:Mx86_64}",
+               ".if ${MACHINE_ARCH} == x86_64",
+
+               "NOTE: filename.mk:3: MACHINE_ARCH "+
+                       "should be compared using \"${MACHINE_ARCH} == x86_64\" "+
+                       "instead of matching against \":Mx86_64\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${MACHINE_ARCH:Mx86_64}\" "+
+                       "with \"${MACHINE_ARCH} == x86_64\".")
+
+       // MACHINE_ARCH is built-in into bmake and is always available.
+       // Therefore it doesn't matter whether bsd.prefs.mk is included or not.
+       test(
+               true,
+               ".if ${MACHINE_ARCH:Mx86_64}",
+               ".if ${MACHINE_ARCH} == x86_64",
+
+               "NOTE: filename.mk:3: MACHINE_ARCH "+
+                       "should be compared using \"${MACHINE_ARCH} == x86_64\" "+
+                       "instead of matching against \":Mx86_64\".",
+               "AUTOFIX: filename.mk:3: Replacing \"${MACHINE_ARCH:Mx86_64}\" "+
+                       "with \"${MACHINE_ARCH} == x86_64\".")
+
+       test(
+               false,
+               ".if !empty(OPSYS:MUnknown)",
+               ".if ${OPSYS:U} == Unknown",
+
+               // FIXME: This warning is not the job of simplifyCondition.
+               //  Therefore don't test it here.
+               "WARN: filename.mk:3: The pattern \"Unknown\" cannot match any of "+
+                       "{ Cygwin DragonFly FreeBSD Linux NetBSD SunOS } for OPSYS.",
+               "NOTE: filename.mk:3: OPSYS should be "+
+                       "compared using \"${OPSYS:U} == Unknown\" "+
+                       "instead of matching against \":MUnknown\".",
+               "WARN: filename.mk:3: To use OPSYS at load time, "+
+                       ".include \"../../mk/bsd.prefs.mk\" first.",
+               "AUTOFIX: filename.mk:3: Replacing \"!empty(OPSYS:MUnknown)\" "+
+                       "with \"${OPSYS:U} == Unknown\".")
+
+       testAfterPrefs(
+               ".if !empty(OPSYS:S,NetBSD,ok,:Mok)",
+               ".if ${OPSYS:S,NetBSD,ok,} == ok",
+
+               "NOTE: filename.mk:3: OPSYS should be "+
+                       "compared using \"${OPSYS:S,NetBSD,ok,} == ok\" "+
+                       "instead of matching against \":Mok\".",
+               "AUTOFIX: filename.mk:3: Replacing \"!empty(OPSYS:S,NetBSD,ok,:Mok)\" "+
+                       "with \"${OPSYS:S,NetBSD,ok,} == ok\".")
+
+       testAfterPrefs(
+               ".if empty(OPSYS:tl:Msunos)",
+               ".if ${OPSYS:tl} != sunos",
+
+               "NOTE: filename.mk:3: OPSYS should be "+
+                       "compared using \"${OPSYS:tl} != sunos\" "+
+                       "instead of matching against \":Msunos\".",
+               "AUTOFIX: filename.mk:3: Replacing \"empty(OPSYS:tl:Msunos)\" "+
+                       "with \"${OPSYS:tl} != sunos\".")
+
+       testAfterPrefs(
+               ".if !empty(OPSYS:O:MUnknown:S,a,b,)",
+               ".if !empty(OPSYS:O:MUnknown:S,a,b,)",
+
+               "WARN: filename.mk:3: The pattern \"Unknown\" cannot match any of "+
+                       "{ Cygwin DragonFly FreeBSD Linux NetBSD SunOS } for OPSYS.")
+
+       // The dot is just an ordinary character.
+       // It's only special when used in number literals.
+       testAfterPrefs(
+               ".if !empty(PKGPATH:Mcategory/package1.2)",
+               ".if ${PKGPATH} == category/package1.2",
+
+               "NOTE: filename.mk:3: PKGPATH should be "+
+                       "compared using \"${PKGPATH} == category/package1.2\" "+
+                       "instead of matching against \":Mcategory/package1.2\".",
+               "AUTOFIX: filename.mk:3: Replacing \"!empty(PKGPATH:Mcategory/package1.2)\" "+
+                       "with \"${PKGPATH} == category/package1.2\".")
+
+       // Numbers must be enclosed in quotes, otherwise they are compared
+       // as numbers, not as strings. The :M and :N modifiers always compare
+       // strings.
+       testAfterPrefs(
+               ".if empty(ABI:U:M64)",
+               ".if ${ABI:U} != \"64\"",
+
+               "NOTE: filename.mk:3: ABI should be compared using \"${ABI:U} != \"64\"\" "+
+                       "instead of matching against \":M64\".",
+               "AUTOFIX: filename.mk:3: Replacing \"empty(ABI:U:M64)\" "+
+                       "with \"${ABI:U} != \\\"64\\\"\".")
+
+       // Fractional numbers must also be enclosed in quotes.
+       testAfterPrefs(
+               ".if empty(PKGVERSION_NOREV:U:M19.12)",
+               ".if ${PKGVERSION_NOREV:U} != \"19.12\"",
+
+               "NOTE: filename.mk:3: PKGVERSION_NOREV should be "+
+                       "compared using \"${PKGVERSION_NOREV:U} != \"19.12\"\" "+
+                       "instead of matching against \":M19.12\".",
+               "WARN: filename.mk:3: PKGVERSION_NOREV should not be used at load time in any file.",
+               "AUTOFIX: filename.mk:3: Replacing \"empty(PKGVERSION_NOREV:U:M19.12)\" "+
+                       "with \"${PKGVERSION_NOREV:U} != \\\"19.12\\\"\".")
+
+       testAfterPrefs(
+               ".if !empty(PKG_INFO:Mpkg_info)",
+               ".if ${PKG_INFO} == pkg_info",
+
+               "NOTE: filename.mk:3: PKG_INFO should be "+
+                       "compared using \"${PKG_INFO} == pkg_info\" "+
+                       "instead of matching against \":Mpkg_info\".",
+               "AUTOFIX: filename.mk:3: "+
+                       "Replacing \"!empty(PKG_INFO:Mpkg_info)\" "+
+                       "with \"${PKG_INFO} == pkg_info\".")
+
+       t.CheckEquals(
+               G.Pkgsrc.VariableType(nil, "PKG_LIBTOOL").
+                       Union().Contains(aclpUseLoadtime),
+               false)
+       testAfterPrefs(
+               ".if !empty(PKG_LIBTOOL:Npattern)",
+               ".if !empty(PKG_LIBTOOL:Npattern)",
+
+               // No diagnostics about the :N modifier yet,
+               // see MkLineChecker.simplifyCondition.replace.
+               "WARN: filename.mk:3: PKG_LIBTOOL should not be used "+
+                       "at load time in any file.")
+
+       // TODO: Add a note that the :U is unnecessary, and explain why.
+       testAfterPrefs(
+               ".if ${PKGPATH:U:Mcategory/package}",
+               ".if ${PKGPATH:U} == category/package",
+
+               "NOTE: filename.mk:3: PKGPATH should be "+
+                       "compared using \"${PKGPATH:U} == category/package\" "+
+                       "instead of matching against \":Mcategory/package\".",
+               "AUTOFIX: filename.mk:3: "+
+                       "Replacing \"${PKGPATH:U:Mcategory/package}\" "+
+                       "with \"${PKGPATH:U} == category/package\".")
+
+       testAfterPrefs(
+               ".if ${UNKNOWN:Mpattern}",
+               ".if ${UNKNOWN:Mpattern}",
+
+               "WARN: filename.mk:3: UNKNOWN is used but not defined.")
+
+       // MAKE is AlwaysInScope and DefinedIfInScope and NonemptyIfDefined.
+       testAfterPrefs(
+               ".if ${MAKE:Mpattern}",
+               ".if ${MAKE} == pattern",
+
+               "NOTE: filename.mk:3: MAKE should be "+
+                       "compared using \"${MAKE} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:3: "+
+                       "Replacing \"${MAKE:Mpattern}\" "+
+                       "with \"${MAKE} == pattern\".")
+
+       // VarUse without any modifiers is skipped.
+       testAfterPrefs(
+               ".if ${MAKE}",
+               ".if ${MAKE}",
+
+               nil...)
+
+       // Special characters must be enclosed in quotes when they are
+       // used in string literals.
+       // As of December 2019, strings with special characters are not yet
+       // replaced automatically, see mkCondLiteralChars.
+       // TODO: Add tests for all characters that are special in string literals or patterns.
+       // TODO: Then, extend the set of characters for which the expressions are simplified.
+       testAfterPrefs(
+               ".if ${FETCH_CMD:M&&}",
+               ".if ${FETCH_CMD:M&&}",
+
+               nil...)
+
+       // The + is contained in mkCondStringLiteralUnquoted.
+       // The + is contained in mkCondModifierPatternLiteral.
+       testAfterPrefs(
+               ".if ${PKGPATH:Mcategory/gtk+}",
+               ".if ${PKGPATH} == category/gtk+",
+
+               "NOTE: filename.mk:3: PKGPATH should be "+
+                       "compared using \"${PKGPATH} == category/gtk+\" "+
+                       "instead of matching against \":Mcategory/gtk+\".",
+               "AUTOFIX: filename.mk:3: "+
+                       "Replacing \"${PKGPATH:Mcategory/gtk+}\" "+
+                       "with \"${PKGPATH} == category/gtk+\".")
+
+       // The characters <=> may be used unescaped in :M and :N patterns
+       // but not in .if conditions. There they must be enclosed in quotes.
+       testAfterPrefs(
+               ".if ${PKGPATH:M<=>}",
+               ".if ${PKGPATH} == \"<=>\"",
+
+               "WARN: filename.mk:3: The pathname pattern \"<=>\" "+
+                       "contains the invalid characters \"<=>\".",
+               "NOTE: filename.mk:3: PKGPATH should be "+
+                       "compared using \"${PKGPATH} == \"<=>\"\" "+
+                       "instead of matching against \":M<=>\".",
+               "AUTOFIX: filename.mk:3: "+
+                       "Replacing \"${PKGPATH:M<=>}\" "+
+                       "with \"${PKGPATH} == \\\"<=>\\\"\".")
+
+       // If pkglint replaces this particular pattern, the resulting string
+       // literal must be escaped properly.
+       testAfterPrefs(
+               ".if ${PKGPATH:M\"}",
+               ".if ${PKGPATH:M\"}",
+
+               // TODO: Find a better variable than PKGPATH,
+               //  to get rid of this unrelated warning.
+               "WARN: filename.mk:3: The pathname pattern \"\\\"\" "+
+                       "contains the invalid character \"\\\"\".")
+}
+
+func (s *Suite) Test_MkCondChecker_simplifyCondition__defined_in_same_file(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       // before: the directive before the condition is simplified
+       // after: the directive after the condition is simplified
+       test := func(before, after string, diagnostics ...string) {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       MkCvsID,
+                       "OK=\t\tok",
+                       "OK_DIR=\t\tok", // See Pkgsrc.guessVariableType.
+                       before,
+                       "LATER=\t\tlater",
+                       "LATER_DIR=\tlater", // See Pkgsrc.guessVariableType.
+                       ".endif",
+                       "USED=\t\t${OK} ${LATER} ${OK_DIR} ${LATER_DIR} ${USED}")
+
+               // The high-level call MkLines.Check is used here to
+               // get MkLines.Tools.SeenPrefs correct, which decides
+               // whether the :U modifier is necessary.
+               //
+               // TODO: Replace MkLines.Check this with a more specific method.
+
+               t.ExpectDiagnosticsAutofix(
+                       mklines.Check,
+                       diagnostics...)
+
+               // TODO: Move this assertion above the assertion about the diagnostics.
+               afterMklines := LoadMk(t.File("filename.mk"), MustSucceed)
+               t.CheckEquals(afterMklines.mklines[3].Text, after)
+       }
+
+       // For variables with completely unknown names, the type is nil
+       // and the complete check is skipped.
+       test(
+               ".if ${OK:Mpattern}",
+               ".if ${OK:Mpattern}",
+
+               nil...)
+
+       // For variables with completely unknown names, the type is nil
+       // and the complete check is skipped.
+       test(
+               ".if ${LATER:Mpattern}",
+               ".if ${LATER:Mpattern}",
+
+               nil...)
+
+       // OK_DIR is defined earlier than the .if condition,
+       // which is the correct order.
+       test(
+               ".if ${OK_DIR:Mpattern}",
+               ".if ${OK_DIR} == pattern",
+
+               "NOTE: filename.mk:4: OK_DIR should be "+
+                       "compared using \"${OK_DIR} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:4: "+
+                       "Replacing \"${OK_DIR:Mpattern}\" "+
+                       "with \"${OK_DIR} == pattern\".")
+
+       // LATER_DIR is defined later than the .if condition,
+       // therefore at the time of the .if statement, it is still empty.
+       test(
+               ".if ${LATER_DIR:Mpattern}",
+               ".if ${LATER_DIR} == pattern",
+
+               // FIXME: Warn that LATER_DIR is used before it is defined.
+               // FIXME: Add :U modifier since LATER_DIR is not yet defined.
+               "NOTE: filename.mk:4: LATER_DIR should be "+
+                       "compared using \"${LATER_DIR} == pattern\" "+
+                       "instead of matching against \":Mpattern\".",
+               "AUTOFIX: filename.mk:4: "+
+                       "Replacing \"${LATER_DIR:Mpattern}\" "+
+                       "with \"${LATER_DIR} == pattern\".")
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCondCompare(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       test := func(cond string, output ...string) {
+               mklines := t.NewMkLines("filename.mk",
+                       cond)
+               mklines.ForEach(func(mkline *MkLine) {
+                       NewMkCondChecker(mkline, mklines).checkDirectiveCond()
+               })
+               t.CheckOutput(output)
+       }
+
+       // As of July 2019, pkglint doesn't have specific checks for comparing
+       // variables to numbers.
+       test(".if ${VAR} > 0",
+               "WARN: filename.mk:1: VAR is used but not defined.")
+
+       // For string comparisons, the checks from vartypecheck.go are
+       // performed.
+       test(".if ${DISTNAME} == \"<>\"",
+               "WARN: filename.mk:1: The filename \"<>\" contains the invalid characters \"<>\".",
+               "WARN: filename.mk:1: DISTNAME should not be used at load time in any file.")
+
+       // This type of comparison doesn't occur in practice since it is
+       // overly verbose.
+       test(".if \"${BUILD_DIRS}str\" == \"str\"",
+               // TODO: why should it not be used? In a .for loop it sounds pretty normal.
+               "WARN: filename.mk:1: BUILD_DIRS should not be used at load time in any file.")
+
+       // This is a shorthand for defined(VAR), but it is not used in practice.
+       test(".if VAR",
+               "WARN: filename.mk:1: Invalid condition, unrecognized part: \"VAR\".")
+
+       // Calling a function with braces instead of parentheses is syntactically
+       // invalid. Pkglint is stricter than bmake in this situation.
+       //
+       // Bmake reads the "empty{VAR}" as a variable name. It then checks whether
+       // this variable is defined. It is not, of course, therefore the expression
+       // is false. The ! in front of it negates this false, which makes the whole
+       // condition true.
+       //
+       // See https://mail-index.netbsd.org/tech-pkg/2019/07/07/msg021539.html
+       test(".if !empty{VAR}",
+               "WARN: filename.mk:1: Invalid condition, unrecognized part: \"empty{VAR}\".")
+}
+
+func (s *Suite) Test_MkCondChecker_checkDirectiveCondCompareVarStr__no_tracing(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
+               ".if ${DISTFILES:Mpattern:O:u} == NetBSD")
+       t.DisableTracing()
+
+       ck := NewMkCondChecker(mklines.mklines[0], mklines)
+       varUse := b.VarUse("DISTFILES", "Mpattern", "O", "u")
+       // TODO: mklines.ForEach
+       ck.checkDirectiveCondCompareVarStr(varUse, "==", "distfile-1.0.tar.gz")
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkCondChecker_checkCompareVarStr(c *check.C) {
+       t := s.Init(c)
+
+       test := func() {
+               // FIXME
+               t.CheckEquals(true, true)
+       }
+
+       test()
+}
+
+func (s *Suite) Test_MkCondChecker_checkCompareVarStrCompiler(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       test := func(cond string, diagnostics ...string) {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       MkCvsID,
+                       "",
+                       ".include \"../../mk/bsd.fast.prefs.mk\"",
+                       "",
+                       ".if "+cond,
+                       ".endif")
+
+               t.SetUpCommandLine("-Wall")
+               mklines.Check()
+               t.SetUpCommandLine("-Wall", "--autofix")
+               mklines.Check()
+
+               t.CheckOutput(diagnostics)
+       }
+
+       test(
+               "${PKGSRC_COMPILER} == gcc",
+
+               "ERROR: filename.mk:5: "+
+                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.",
+               "AUTOFIX: filename.mk:5: "+
+                       "Replacing \"${PKGSRC_COMPILER} == gcc\" "+
+                       "with \"${PKGSRC_COMPILER:Mgcc}\".")
+
+       // No autofix because of missing whitespace.
+       // TODO: Provide the autofix regardless of the whitespace.
+       test(
+               "${PKGSRC_COMPILER}==gcc",
+
+               "ERROR: filename.mk:5: "+
+                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.")
+
+       // The comparison value can be with or without quotes.
+       test(
+               "${PKGSRC_COMPILER} == \"gcc\"",
+
+               "ERROR: filename.mk:5: "+
+                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.",
+               "AUTOFIX: filename.mk:5: "+
+                       "Replacing \"${PKGSRC_COMPILER} == \\\"gcc\\\"\" "+
+                       "with \"${PKGSRC_COMPILER:Mgcc}\".")
+
+       // No warning because it is not obvious what is meant here.
+       // This case probably doesn't occur in practice.
+       test(
+               "${PKGSRC_COMPILER} == \"distcc gcc\"",
+
+               nil...)
+}
Index: pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go:1.1
--- /dev/null   Sun Dec  8 00:06:39 2019
+++ pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go    Sun Dec  8 00:06:38 2019
@@ -0,0 +1,683 @@
+package pkglint
+
+import "strings"
+
+type MkVarUseChecker struct {
+       use     *MkVarUse
+       vartype *Vartype
+
+       MkLines *MkLines
+       MkLine  *MkLine
+}
+
+func NewMkVarUseChecker(use *MkVarUse, mklines *MkLines, mkline *MkLine) *MkVarUseChecker {
+       vartype := G.Pkgsrc.VariableType(mklines, use.varname)
+
+       return &MkVarUseChecker{use, vartype, mklines, mkline}
+}
+
+// CheckVaruse checks a single use of a variable in a specific context.
+func (ck *MkVarUseChecker) Check(vuc *VarUseContext) {
+       if ck.use.IsExpression() {
+               return
+       }
+
+       ck.checkUndefined()
+       ck.checkPermissions(vuc)
+
+       ck.checkVarname()
+       ck.checkModifiers()
+       ck.checkQuoting(vuc)
+
+       ck.checkBuildDefs()
+       ck.checkDeprecated()
+
+       NewMkLineChecker(ck.MkLines, ck.MkLine).
+               checkTextVarUse(ck.use.varname, ck.vartype, vuc.time)
+}
+
+func (ck *MkVarUseChecker) checkUndefined() {
+       varuse := ck.use
+       vartype := ck.vartype
+       varname := varuse.varname
+
+       switch {
+       case !G.Opts.WarnExtra,
+               // Well-known variables are probably defined by the infrastructure.
+               vartype != nil && !vartype.IsGuessed(),
+               ck.MkLines.vars.IsDefinedSimilar(varname),
+               ck.MkLines.forVars[varname],
+               ck.MkLines.vars.Mentioned(varname) != nil,
+               G.Pkg != nil && G.Pkg.vars.IsDefinedSimilar(varname),
+               containsVarRef(varname),
+               G.Pkgsrc.vartypes.IsDefinedCanon(varname),
+               varname == "":
+               return
+       }
+
+       if ck.MkLines.once.FirstTimeSlice("used but not defined", varname) {
+               ck.MkLine.Warnf("%s is used but not defined.", varname)
+       }
+}
+
+func (ck *MkVarUseChecker) checkModifiers() {
+       varuse := ck.use
+       mods := varuse.modifiers
+       if len(mods) == 0 {
+               return
+       }
+
+       ck.checkModifiersSuffix()
+       ck.checkModifiersRange()
+
+       // TODO: Add checks for a single modifier, among them:
+       // TODO: Suggest to replace ${VAR:@l@-l${l}@} with the simpler ${VAR:S,^,-l,}.
+       // TODO: Suggest to replace ${VAR:@l@${l}suffix@} with the simpler ${VAR:=suffix}.
+       // TODO: Investigate why :Q is not checked at this exact place.
+}
+
+func (ck *MkVarUseChecker) checkModifiersSuffix() {
+       varuse := ck.use
+       vartype := ck.vartype
+
+       if !varuse.modifiers[0].IsSuffixSubst() || vartype == nil || vartype.IsList() {
+               return
+       }
+
+       ck.MkLine.Warnf("The :from=to modifier should only be used with lists, not with %s.", varuse.varname)
+       ck.MkLine.Explain(
+               "Instead of (for example):",
+               "\tMASTER_SITES=\t${HOMEPAGE:=repository/}",
+               "",
+               "Write:",
+               "\tMASTER_SITES=\t${HOMEPAGE}repository/",
+               "",
+               "This expresses the intention of the code more clearly.")
+}
+
+// checkModifiersRange suggests to replace
+// ${VAR:S,^,__magic__,1:M__magic__*:S,^__magic__,,} with the simpler ${VAR:[1]}.
+func (ck *MkVarUseChecker) checkModifiersRange() {
+       varuse := ck.use
+       mods := varuse.modifiers
+
+       if len(mods) != 3 {
+               return
+       }
+
+       m, _, from, to, options := mods[0].MatchSubst()
+       if !m || from != "^" || !matches(to, `^\w+$`) || options != "1" {
+               return
+       }
+
+       magic := to
+       m, positive, pattern, _ := mods[1].MatchMatch()
+       if !m || !positive || pattern != magic+"*" {
+               return
+       }
+
+       m, _, from, to, options = mods[2].MatchSubst()
+       if !m || from != "^"+magic || to != "" || options != "" {
+               return
+       }
+
+       fix := ck.MkLine.Autofix()
+       fix.Notef("The modifier %q can be written as %q.", varuse.Mod(), ":[1]")
+       fix.Explain(
+               "The range modifier is much easier to understand than the",
+               "complicated regular expressions, which were needed before",
+               "the year 2006.")
+       fix.Replace(varuse.Mod(), ":[1]")
+       fix.Apply()
+}
+
+func (ck *MkVarUseChecker) checkVarname() {
+       varname := ck.use.varname
+       if varname == "@" {
+               ck.MkLine.Warnf("Please use %q instead of %q.", "${.TARGET}", "$@")
+               ck.MkLine.Explain(
+                       "It is more readable and prevents confusion with the shell variable",
+                       "of the same name.")
+       }
+
+       if varname == "LOCALBASE" && !G.Infrastructure {
+               fix := ck.MkLine.Autofix()
+               fix.Warnf("Please use PREFIX instead of LOCALBASE.")
+               fix.ReplaceRegex(`\$\{LOCALBASE\b`, "${PREFIX", 1)
+               fix.Apply()
+       }
+}
+
+// checkPermissions checks the permissions when a variable is used,
+// be it in a variable assignment, in a shell command, a conditional, or
+// somewhere else.
+//
+// See checkVarassignLeftPermissions.
+func (ck *MkVarUseChecker) checkPermissions(vuc *VarUseContext) {
+       if !G.Opts.WarnPerm {
+               return
+       }
+       if G.Infrastructure {
+               // As long as vardefs.go doesn't explicitly define permissions for
+               // infrastructure files, skip the check completely. This avoids
+               // many wrong warnings.
+               return
+       }
+
+       if trace.Tracing {
+               defer trace.Call(vuc)()
+       }
+
+       // This is the type of the variable that is being used. Not to
+       // be confused with vuc.vartype, which is the type of the
+       // context in which the variable is used (often a ShellCommand
+       // or, in an assignment, the type of the left hand side variable).
+       varname := ck.use.varname
+       vartype := ck.vartype
+       if vartype == nil {
+               if trace.Tracing {
+                       trace.Step1("No type definition found for %q.", varname)
+               }
+               return
+       }
+
+       if vartype.IsGuessed() {
+               return
+       }
+
+       // Do not warn about unknown infrastructure variables.
+       // These have all permissions to prevent warnings when they are used.
+       // But when other variables are assigned to them it would seem as if
+       // these other variables could become evaluated at load time.
+       // And this is something that most variables do not allow.
+       if vuc.vartype != nil && vuc.vartype.basicType == BtUnknown {
+               return
+       }
+
+       basename := ck.MkLine.Basename
+       if basename == "hacks.mk" {
+               return
+       }
+
+       effPerms := vartype.EffectivePermissions(basename)
+       if effPerms.Contains(aclpUseLoadtime) {
+               ck.checkUseAtLoadTime(vuc.time)
+
+               // Since the variable may be used at load time, it probably
+               // may be used at run time as well. If it weren't, that would
+               // be a rather strange permissions set.
+               return
+       }
+
+       // At this point the variable must not be used at load time.
+       // Now determine whether it is directly used at load time because
+       // the context already says so or, a little trickier, if it might
+       // be used at load time somewhere in the future because it is
+       // assigned to another variable, and that variable is allowed
+       // to be used at load time.
+       directly := vuc.time == VucLoadTime
+       indirectly := !directly && vuc.vartype != nil &&
+               vuc.vartype.Union().Contains(aclpUseLoadtime)
+
+       if !directly && !indirectly && effPerms.Contains(aclpUse) {
+               // At this point the variable is either used at run time, or the
+               // time is not known.
+               return
+       }
+
+       if directly || indirectly {
+               // At this point the variable is used at load time although that
+               // is not allowed by the permissions. The variable could be a tool
+               // variable, and these tool variables have special rules.
+               tool := G.ToolByVarname(ck.MkLines, varname)
+               if tool != nil {
+
+                       // Whether a tool variable may be used at load time depends on
+                       // whether bsd.prefs.mk has been included before. That file
+                       // examines the tools that have been added to USE_TOOLS up to
+                       // this point and makes their variables available for use at
+                       // load time.
+                       if !tool.UsableAtLoadTime(ck.MkLines.Tools.SeenPrefs) {
+                               ck.warnToolLoadTime(varname, tool)
+                       }
+                       return
+               }
+       }
+
+       if ck.MkLines.once.FirstTimeSlice("checkPermissions", varname) {
+               ck.warnPermissions(vuc.vartype, varname, vartype, directly, indirectly)
+       }
+}
+
+func (ck *MkVarUseChecker) warnPermissions(
+       vucVartype *Vartype, varname string, vartype *Vartype, directly, indirectly bool) {
+
+       mkline := ck.MkLine
+
+       anyPerms := vartype.Union()
+       if !anyPerms.Contains(aclpUse) && !anyPerms.Contains(aclpUseLoadtime) {
+               mkline.Warnf("%s should not be used in any file; it is a write-only variable.", varname)
+               ck.explainPermissions(varname, vartype)
+               return
+       }
+
+       if indirectly {
+               // Some of the guessed variables may be used at load time. But since the
+               // variable type and these permissions are guessed, pkglint should not
+               // issue the following warning, since it is often wrong.
+               if vucVartype.IsGuessed() {
+                       return
+               }
+
+               mkline.Warnf("%s should not be used indirectly at load time (via %s).",
+                       varname, mkline.Varname())
+               ck.explainPermissions(varname, vartype,
+                       "The variable on the left-hand side may be evaluated at load time,",
+                       "but the variable on the right-hand side should not.",
+                       "Because of the assignment in this line, the variable might be",
+                       "used indirectly at load time, before it is guaranteed to be",
+                       "properly initialized.")
+               return
+       }
+
+       needed := aclpUse
+       if directly {
+               needed = aclpUseLoadtime
+       }
+       alternativeFiles := vartype.AlternativeFiles(needed)
+
+       loadTimeExplanation := func() []string {
+               return []string{
+                       "Many variables, especially lists of something, get their values incrementally.",
+                       "Therefore it is generally unsafe to rely on their",
+                       "value until it is clear that it will never change again.",
+                       "This point is reached when the whole package Makefile is loaded and",
+                       "execution of the shell commands starts; in some cases earlier.",
+                       "",
+                       "Additionally, when using the \":=\" operator, each $$ is replaced",
+                       "with a single $, so variables that have references to shell",
+                       "variables or regular expressions are modified in a subtle way."}
+       }
+
+       switch {
+       case alternativeFiles == "" && directly:
+               mkline.Warnf("%s should not be used at load time in any file.", varname)
+               ck.explainPermissions(varname, vartype, loadTimeExplanation()...)
+
+       case alternativeFiles == "":
+               mkline.Warnf("%s should not be used in any file.", varname)
+               ck.explainPermissions(varname, vartype, loadTimeExplanation()...)
+
+       case directly:
+               mkline.Warnf(
+                       "%s should not be used at load time in this file; "+
+                               "it would be ok in %s.",
+                       varname, alternativeFiles)
+               ck.explainPermissions(varname, vartype, loadTimeExplanation()...)
+
+       default:
+               mkline.Warnf(
+                       "%s should not be used in this file; it would be ok in %s.",
+                       varname, alternativeFiles)
+               ck.explainPermissions(varname, vartype)
+       }
+}
+
+func (ck *MkVarUseChecker) explainPermissions(varname string, vartype *Vartype, intro ...string) {
+       if !G.Logger.Opts.Explain {
+               return
+       }
+
+       // TODO: Starting with the second explanation, omit the common part. Instead, only list the permission rules.
+
+       var expl []string
+
+       if len(intro) > 0 {
+               expl = append(expl, intro...)
+               expl = append(expl, "")
+       }
+
+       expl = append(expl,
+               "The allowed actions for a variable are determined based on the file",
+               "name in which the variable is used or defined.",
+               sprintf("The rules for %s are:", varname),
+               "")
+
+       for _, rule := range vartype.aclEntries {
+               perms := rule.permissions.HumanString()
+
+               files := rule.matcher.originalPattern
+               if files == "*" {
+                       files = "any file"
+               }
+
+               if perms != "" {
+                       expl = append(expl, sprintf("* in %s, it may be %s", files, perms))
+               } else {
+                       expl = append(expl, sprintf("* in %s, it should not be accessed at all", files))
+               }
+       }
+
+       expl = append(expl,
+               "",
+               "If these rules seem to be incorrect, please ask on the tech-pkg%NetBSD.org@localhost mailing list.")
+
+       ck.MkLine.Explain(expl...)
+}
+
+func (ck *MkVarUseChecker) checkUseAtLoadTime(time VucTime) {
+       if time != VucLoadTime {
+               return
+       }
+       if ck.vartype.IsAlwaysInScope() || ck.MkLines.Tools.SeenPrefs {
+               return
+       }
+       if G.Pkg != nil && G.Pkg.seenPrefs {
+               return
+       }
+       mkline := ck.MkLine
+       basename := mkline.Basename
+       if basename == "builtin.mk" {
+               return
+       }
+
+       if ck.vartype.IsPackageSettable() &&
+               basename != "Makefile" && basename != "options.mk" {
+
+               // For package-settable variables, the explanation doesn't
+               // make sense since it talks about completely different
+               // types of variables.
+               return
+       }
+
+       if !ck.MkLines.once.FirstTime("bsd.prefs.mk") {
+               return
+       }
+
+       include := condStr(
+               basename == "buildlink3.mk",
+               "mk/bsd.fast.prefs.mk",
+               "mk/bsd.prefs.mk")
+       currInclude := G.Pkgsrc.File(NewPkgsrcPath(NewPath(include)))
+
+       mkline.Warnf("To use %s at load time, .include %q first.",
+               ck.use.varname, mkline.Rel(currInclude))
+       mkline.Explain(
+               "The user-settable variables and several other variables",
+               "from the pkgsrc infrastructure are only available",
+               "after the preferences have been loaded.",
+               "",
+               "Before that, these variables are undefined.")
+}
+
+// warnToolLoadTime logs a warning that the tool ${varname}
+// should not be used at load time.
+func (ck *MkVarUseChecker) warnToolLoadTime(varname string, tool *Tool) {
+       // TODO: While using a tool by its variable name may be ok at load time,
+       //  doing the same with the plain name of a tool is never ok.
+       //  "VAR!= cat" is never guaranteed to call the correct cat.
+       //  Even for shell builtins like echo and printf, bmake may decide
+       //  to skip the shell and execute the commands via execve, which
+       //  means that even echo is not a shell-builtin anymore.
+
+       // TODO: Replace "parse time" with "load time" everywhere.
+
+       if tool.Validity == AfterPrefsMk {
+               ck.MkLine.Warnf("To use the tool ${%s} at load time, bsd.prefs.mk has to be included before.", varname)
+               return
+       }
+
+       if ck.MkLine.Basename == "Makefile" {
+               pkgsrcTool := G.Pkgsrc.Tools.ByName(tool.Name)
+               if pkgsrcTool != nil && pkgsrcTool.Validity == Nowhere {
+                       // The tool must have been added too late to USE_TOOLS,
+                       // i.e. after bsd.prefs.mk has been included.
+                       ck.MkLine.Warnf("To use the tool ${%s} at load time, it has to be added to USE_TOOLS before including bsd.prefs.mk.", varname)
+                       return
+               }
+       }
+
+       ck.MkLine.Warnf("The tool ${%s} cannot be used at load time.", varname)
+       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.",
+               "Adding the tool to USE_TOOLS at any later time has no effect,",
+               "which means that the tool can only be used at run time.",
+               "That's the rule for the package Makefiles.",
+               "",
+               "Since any other .mk file can be included from anywhere else, there",
+               "is no guarantee that the tool is properly defined for using it at",
+               "load time (see above for the tricky rules).",
+               "Therefore the tools can only be used at run time,",
+               "except in the package Makefile itself.")
+}
+
+// checkVarUseWords checks whether a variable use of the form ${VAR}
+// or ${VAR:modifiers} is allowed in a certain context.
+func (ck *MkVarUseChecker) checkQuoting(vuc *VarUseContext) {
+       if !G.Opts.WarnQuoting || vuc.quoting == VucQuotUnknown {
+               return
+       }
+
+       varUse := ck.use
+       vartype := ck.vartype
+
+       needsQuoting := ck.MkLine.VariableNeedsQuoting(ck.MkLines, varUse, vartype, vuc)
+       if needsQuoting == unknown {
+               return
+       }
+
+       mod := varUse.Mod()
+
+       // In GNU configure scripts, a few variables need to be passed through
+       // the :M* modifier before they reach the configure scripts. Otherwise
+       // the leading or trailing spaces will lead to strange caching errors
+       // since the GNU configure scripts cannot handle these space characters.
+       //
+       // When doing checks outside a package, the :M* modifier is needed for safety.
+       needMstar := (G.Pkg == nil || G.Pkg.vars.IsDefined("GNU_CONFIGURE")) &&
+               matches(varUse.varname, `^(?:.*_)?(?:CFLAGS|CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`)
+
+       mkline := ck.MkLine
+       if mod == ":M*:Q" && !needMstar {
+               if !vartype.IsGuessed() {
+                       mkline.Notef("The :M* modifier is not needed here.")
+               }
+
+       } else if needsQuoting == yes {
+               ck.checkQuotingQM(mod, needMstar, vuc)
+       }
+
+       if hasSuffix(mod, ":Q") && needsQuoting == no {
+               ck.warnRedundantModifierQ(mod)
+       }
+}
+
+func (ck *MkVarUseChecker) checkQuotingQM(mod string, needMstar bool, vuc *VarUseContext) {
+       vartype := ck.vartype
+       varname := ck.use.varname
+
+       modNoQ := strings.TrimSuffix(mod, ":Q")
+       modNoM := strings.TrimSuffix(modNoQ, ":M*")
+       correctMod := modNoM + condStr(needMstar, ":M*:Q", ":Q")
+
+       if correctMod == mod+":Q" && vuc.IsWordPart && !vartype.IsShell() {
+
+               isSingleWordConstant := func() bool {
+                       if G.Pkg == nil {
+                               return false
+                       }
+
+                       varinfo := G.Pkg.redundant.vars[varname]
+                       if varinfo == nil || !varinfo.vari.IsConstant() {
+                               return false
+                       }
+
+                       value := varinfo.vari.ConstantValue()
+                       return len(ck.MkLine.ValueFields(value)) == 1
+               }
+
+               if vartype.IsList() && isSingleWordConstant() {
+                       // Do not warn in this special case, which typically occurs
+                       // for BUILD_DIRS or similar package-settable variables.
+
+               } else if vartype.IsList() {
+                       ck.warnListVariableInWord()
+               } else {
+                       ck.warnMissingModifierQInWord()
+               }
+
+       } else if mod != correctMod {
+               if vuc.quoting == VucQuotPlain {
+                       ck.fixQuotingModifiers(correctMod, mod)
+               } else {
+                       ck.warnWrongQuotingModifiers(correctMod, mod)
+               }
+
+       } else if vuc.quoting != VucQuotPlain {
+               ck.warnModifierQInQuotes(mod)
+       }
+}
+
+func (ck *MkVarUseChecker) warnListVariableInWord() {
+       mkline := ck.MkLine
+
+       mkline.Warnf("The list variable %s should not be embedded in a word.",
+               ck.use.varname)
+       mkline.Explain(
+               "When a list variable has multiple elements, this expression expands",
+               "to something unexpected:",
+               "",
+               "Example: ${MASTER_SITE_SOURCEFORGE}directory/ expands to",
+               "",
+               "\thttps://mirror1.sf.net/ https://mirror2.sf.net/directory/";,
+               "",
+               "The first URL is missing the directory.",
+               "To fix this, write",
+               "\t${MASTER_SITE_SOURCEFORGE:=directory/}.",
+               "",
+               "Example: -l${LIBS} expands to",
+               "",
+               "\t-llib1 lib2",
+               "",
+               "The second library is missing the -l.",
+               "To fix this, write ${LIBS:S,^,-l,}.")
+}
+
+func (ck *MkVarUseChecker) warnMissingModifierQInWord() {
+       mkline := ck.MkLine
+
+       mkline.Warnf("The variable %s should be quoted as part of a shell word.",
+               ck.use.varname)
+       mkline.Explain(
+               "This variable can contain spaces or other special characters.",
+               "Therefore it should be quoted by replacing ${VAR} with ${VAR:Q}.")
+}
+
+func (ck *MkVarUseChecker) fixQuotingModifiers(correctMod string, mod string) {
+       varname := ck.use.varname
+
+       fix := ck.MkLine.Autofix()
+       fix.Warnf("Please use ${%s%s} instead of ${%s%s}.", varname, correctMod, varname, mod)
+       fix.Explain(
+               seeGuide("Echoing a string exactly as-is", "echo-literal"))
+       fix.Replace("${"+varname+mod+"}", "${"+varname+correctMod+"}")
+       fix.Apply()
+}
+
+func (ck *MkVarUseChecker) warnWrongQuotingModifiers(correctMod string, mod string) {
+       mkline := ck.MkLine
+       varname := ck.use.varname
+
+       mkline.Warnf("Please use ${%s%s} instead of ${%s%s} and make sure"+
+               " the variable appears outside of any quoting characters.", varname, correctMod, varname, mod)
+       mkline.Explain(
+               "The :Q modifier only works reliably when it is used outside of any",
+               "quoting characters like 'single' or \"double\" quotes or `backticks`.",
+               "",
+               "Examples:",
+               "Instead of CFLAGS=\"${CFLAGS:Q}\",",
+               "     write CFLAGS=${CFLAGS:Q}.",
+               "Instead of 's,@CFLAGS@,${CFLAGS:Q},',",
+               "     write 's,@CFLAGS@,'${CFLAGS:Q}','.",
+               "",
+               seeGuide("Echoing a string exactly as-is", "echo-literal"))
+}
+
+func (ck *MkVarUseChecker) warnModifierQInQuotes(mod string) {
+       mkline := ck.MkLine
+
+       mkline.Warnf("Please move ${%s%s} outside of any quoting characters.",
+               ck.use.varname, mod)
+       mkline.Explain(
+               "The :Q modifier only works reliably when it is used outside of any",
+               "quoting characters like 'single' or \"double\" quotes or `backticks`.",
+               "",
+               "Examples:",
+               "Instead of CFLAGS=\"${CFLAGS:Q}\",",
+               "     write CFLAGS=${CFLAGS:Q}.",
+               "Instead of 's,@CFLAGS@,${CFLAGS:Q},',",
+               "     write 's,@CFLAGS@,'${CFLAGS:Q}','.",
+               "",
+               seeGuide("Echoing a string exactly as-is", "echo-literal"))
+}
+
+func (ck *MkVarUseChecker) warnRedundantModifierQ(mod string) {
+       varname := ck.use.varname
+
+       bad := "${" + varname + mod + "}"
+       good := "${" + varname + strings.TrimSuffix(mod, ":Q") + "}"
+
+       fix := ck.MkLine.Line.Autofix()
+       fix.Notef("The :Q modifier isn't necessary for ${%s} here.", varname)
+       fix.Explain(
+               "Many variables in pkgsrc do not need the :Q modifier since they",
+               "are not expected to contain whitespace or other special characters.",
+               "Examples for these \"safe\" variables are:",
+               "",
+               "\t* filenames",
+               "\t* directory names",
+               "\t* user and group names",
+               "\t* tool names and tool paths",
+               "\t* variable names",
+               "\t* package names (but not dependency patterns like pkg>=1.2)")
+       fix.Replace(bad, good)
+       fix.Apply()
+}
+
+func (ck *MkVarUseChecker) checkBuildDefs() {
+       varname := ck.use.varname
+
+       if !G.Pkgsrc.UserDefinedVars.IsDefined(varname) || G.Pkgsrc.IsBuildDef(varname) {
+               return
+       }
+       if ck.MkLines.buildDefs[varname] {
+               return
+       }
+       if !ck.MkLines.once.FirstTimeSlice("BUILD_DEFS", varname) {
+               return
+       }
+
+       ck.MkLine.Warnf("The user-defined variable %s is used but not added to BUILD_DEFS.", varname)
+       ck.MkLine.Explain(
+               "When a pkgsrc package is built, many things can be configured by the",
+               "pkgsrc user in the mk.conf file.",
+               "All these configurations should be recorded in the binary package",
+               "so the package can be reliably rebuilt.",
+               "The BUILD_DEFS variable contains a list of all these",
+               "user-settable variables, so please add your variable to it, too.")
+}
+
+func (ck *MkVarUseChecker) checkDeprecated() {
+       varname := ck.use.varname
+       instead := G.Pkgsrc.Deprecated[varname]
+       if instead == "" {
+               instead = G.Pkgsrc.Deprecated[varnameCanon(varname)]
+       }
+       if instead == "" {
+               return
+       }
+
+       ck.MkLine.Warnf("Use of %q is deprecated. %s", varname, instead)
+}
Index: pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go:1.1
--- /dev/null   Sun Dec  8 00:06:39 2019
+++ pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go       Sun Dec  8 00:06:38 2019
@@ -0,0 +1,1138 @@
+package pkglint
+
+import "gopkg.in/check.v1"
+
+func (s *Suite) Test_NewMkVarUseChecker(c *check.C) {
+       t := s.Init(c)
+
+       t.ExpectPanicMatches(
+               func() { NewMkVarUseChecker(nil, nil, nil) },
+               `runtime error: invalid memory address or nil pointer dereference`)
+}
+
+func (s *Suite) Test_MkVarUseChecker_Check(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "",
+               "PKGNAME=\t${UNKNOWN}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:3: UNKNOWN is used but not defined.")
+}
+
+// The ${VARNAME:=suffix} expression should only be used with lists.
+// It typically appears in MASTER_SITE definitions.
+func (s *Suite) Test_MkVarUseChecker_Check__eq_nonlist(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/";)
+       mklines := t.SetUpFileMkLines("options.mk",
+               MkCvsID,
+               "WRKSRC=\t\t${WRKDIR:=/subdir}",
+               "MASTER_SITES=\t${MASTER_SITE_GITHUB:=organization/}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: ~/options.mk:2: The :from=to modifier should only be used with lists, not with WRKDIR.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_Check__for(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/";)
+       mklines := t.SetUpFileMkLines("options.mk",
+               MkCvsID,
+               ".for var in a b c",
+               "\t: ${var}",
+               ".endfor")
+
+       mklines.Check()
+
+       t.CheckOutputEmpty()
+}
+
+// When a parameterized variable is defined in the pkgsrc infrastructure,
+// it does not generate a warning about being "used but not defined".
+// Even if the variable parameter differs, like .Linux and .SunOS in this
+// case. This pattern is typical for pkgsrc, therefore pkglint doesn't
+// check that the variable names match exactly.
+func (s *Suite) Test_MkVarUseChecker_Check__varcanon(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/sys-vars.mk",
+               MkCvsID,
+               "CPPPATH.Linux=\t/usr/bin/cpp")
+       t.FinishSetUp()
+
+       mklines := t.NewMkLines("module.mk",
+               MkCvsID,
+               "COMMENT=\t${CPPPATH.SunOS}")
+       ck := NewMkVarUseChecker(b.VarUse("CPPPATH.SunOS"), mklines, mklines.mklines[1])
+
+       ck.Check(&VarUseContext{
+               vartype: &Vartype{
+                       basicType:  BtPathname,
+                       options:    Guessed,
+                       aclEntries: nil,
+               },
+               time:       VucRunTime,
+               quoting:    VucQuotPlain,
+               IsWordPart: false,
+       })
+
+       t.CheckOutputEmpty()
+}
+
+// Any variable that is defined in the pkgsrc infrastructure in mk/**/*.mk is
+// considered defined, and no "used but not defined" warning is logged for it.
+//
+// See Pkgsrc.loadUntypedVars.
+func (s *Suite) Test_MkVarUseChecker_Check__defined_in_infrastructure(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/deeply/nested/infra.mk",
+               MkCvsID,
+               "INFRA_VAR?=\tvalue")
+       t.FinishSetUp()
+       mklines := t.SetUpFileMkLines("category/package/module.mk",
+               MkCvsID,
+               "do-fetch:",
+               "\t: ${INFRA_VAR} ${UNDEFINED}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/module.mk:3: UNDEFINED is used but not defined.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_Check__build_defs(c *check.C) {
+       t := s.Init(c)
+
+       // XXX: This paragraph should not be necessary since VARBASE and X11_TYPE
+       // are also defined in vardefs.go.
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/defaults/mk.conf",
+               "VARBASE?= /usr/pkg/var")
+       t.FinishSetUp()
+
+       mklines := t.SetUpFileMkLines("options.mk",
+               MkCvsID,
+               "COMMENT=\t\t${VARBASE} ${X11_TYPE}",
+               "PKG_FAIL_REASON+=\t${VARBASE} ${X11_TYPE}",
+               "BUILD_DEFS+=\t\tX11_TYPE")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: ~/options.mk:2: The user-defined variable VARBASE is used but not added to BUILD_DEFS.",
+               "WARN: ~/options.mk:3: PKG_FAIL_REASON should only get one item per line.")
+}
+
+// The LOCALBASE variable may be defined and used in the infrastructure.
+// It is always equivalent to PREFIX and only exists for historic reasons.
+func (s *Suite) Test_MkVarUseChecker_Check__LOCALBASE_in_infrastructure(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/infra.mk",
+               MkCvsID,
+               "LOCALBASE?=\t${PREFIX}",
+               "DEFAULT_PREFIX=\t${LOCALBASE}")
+       t.FinishSetUp()
+
+       G.Check(t.File("mk/infra.mk"))
+
+       // No warnings about LOCALBASE being used; the infrastructure files may
+       // do this. In packages though, LOCALBASE is deprecated.
+
+       // There is no warning about DEFAULT_PREFIX being "defined but not used"
+       // since Pkgsrc.loadUntypedVars calls Pkgsrc.vartypes.DefineType, which
+       // registers that variable globally.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkVarUseChecker_Check__user_defined_variable_and_BUILD_DEFS(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/defaults/mk.conf",
+               "VARBASE?=\t${PREFIX}/var",
+               "PYTHON_VER?=\t36")
+       mklines := t.NewMkLines("file.mk",
+               MkCvsID,
+               "BUILD_DEFS+=\tPYTHON_VER",
+               "\t: ${VARBASE}",
+               "\t: ${VARBASE}",
+               "\t: ${PYTHON_VER}")
+       t.FinishSetUp()
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: file.mk:3: The user-defined variable VARBASE is used but not added to BUILD_DEFS.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_Check__obsolete_PKG_DEBUG(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       G.Pkgsrc.initDeprecatedVars()
+
+       mklines := t.NewMkLines("module.mk",
+               MkCvsID,
+               "\t${_PKG_SILENT}${_PKG_DEBUG} :")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "ERROR: module.mk:2: Use of _PKG_SILENT and _PKG_DEBUG is obsolete. Use ${RUN} instead.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkUndefined(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/infra.mk",
+               MkCvsID,
+               "#",
+               "# User-settable variables:",
+               "#",
+               "# DOCUMENTED",
+               "",
+               "ASSIGNED=\tassigned",
+               "#COMMENTED=\tcommented")
+       t.FinishSetUp()
+
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "",
+               "do-build:",
+               "\t: ${ASSIGNED} ${COMMENTED} ${DOCUMENTED} ${UNKNOWN}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:4: UNKNOWN is used but not defined.")
+}
+
+// PR 46570, item "15. net/uucp/Makefile has a make loop"
+func (s *Suite) Test_MkVarUseChecker_checkUndefined__indirect_variables(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpTool("echo", "ECHO", AfterPrefsMk)
+       mklines := t.NewMkLines("net/uucp/Makefile",
+               MkCvsID,
+               "\techo ${UUCP_${var}}")
+
+       mklines.Check()
+
+       // No warning about UUCP_${var} being used but not defined.
+       //
+       // Normally, parameterized variables use a dot instead of an underscore as separator.
+       // This is one of the few other cases. Pkglint doesn't warn about dynamic variable
+       // names like UUCP_${var} or SITES_${distfile}.
+       //
+       // It does warn about simple variable names though, like ${var} in this example.
+       t.CheckOutputLines(
+               "WARN: net/uucp/Makefile:2: var is used but not defined.")
+}
+
+// Documented variables are declared as both defined and used since, as
+// of April 2019, pkglint doesn't yet interpret the "Package-settable
+// variables" comment.
+func (s *Suite) Test_MkVarUseChecker_checkUndefined__documented(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       mklines := t.NewMkLines("interpreter.mk",
+               MkCvsID,
+               "#",
+               "# Package-settable variables:",
+               "#",
+               "# REPLACE_INTERP",
+               "#\tThe list of files whose interpreter will be corrected.",
+               "",
+               "REPLACE_INTERPRETER+=\tinterp",
+               "REPLACE.interp.old=\t.*/interp",
+               "REPLACE.interp.new=\t${PREFIX}/bin/interp",
+               "REPLACE_FILES.interp=\t${REPLACE_INTERP}")
+
+       mklines.Check()
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkModifiers(c *check.C) {
+       // FIXME
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkModifiersSuffix(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("file.mk",
+               MkCvsID,
+               "\t: ${HOMEPAGE:=subdir/:Q}", // wrong
+               "\t: ${BUILD_DIRS:=subdir/}", // correct
+               "\t: ${BIN_PROGRAMS:=.exe}")  // unknown since BIN_PROGRAMS doesn't have a type
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: file.mk:2: The text \":Q\" looks like a modifier but isn't.",
+               "WARN: file.mk:2: The text \":Q\" looks like a modifier but isn't.",
+               "WARN: file.mk:2: The :from=to modifier should only be used with lists, not with HOMEPAGE.",
+               "WARN: file.mk:2: The text \":Q\" looks like a modifier but isn't.",
+               "WARN: file.mk:4: BIN_PROGRAMS is used but not defined.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkModifiersRange(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("--show-autofix", "--source")
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("mk/compiler/gcc.mk",
+               MkCvsID,
+               "CC:=\t${CC:C/^/_asdf_/1:M_asdf_*:S/^_asdf_//}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "NOTE: mk/compiler/gcc.mk:2: "+
+                       "The modifier \":C/^/_asdf_/1:M_asdf_*:S/^_asdf_//\" can be written as \":[1]\".",
+               "AUTOFIX: mk/compiler/gcc.mk:2: "+
+                       "Replacing \":C/^/_asdf_/1:M_asdf_*:S/^_asdf_//\" with \":[1]\".",
+               "-\tCC:=\t${CC:C/^/_asdf_/1:M_asdf_*:S/^_asdf_//}",
+               "+\tCC:=\t${CC:[1]}")
+
+       // Now go through all the "almost" cases, to reach full branch coverage.
+       mklines = t.NewMkLines("gcc.mk",
+               MkCvsID,
+               "\t: ${CC:M1:M2:M3}",
+               "\t: ${CC:C/^begin//:M2:M3}",                    // M1 pattern not exactly ^
+               "\t: ${CC:C/^/_asdf_/g:M2:M3}",                  // M1 options != "1"
+               "\t: ${CC:C/^/....../g:M2:M3}",                  // M1 replacement doesn't match \w+
+               "\t: ${CC:C/^/_asdf_/1:O:M3}",                   // M2 is not a match modifier
+               "\t: ${CC:C/^/_asdf_/1:N2:M3}",                  // M2 is :N instead of :M
+               "\t: ${CC:C/^/_asdf_/1:M_asdf_:M3}",             // M2 pattern is missing the * at the end
+               "\t: ${CC:C/^/_asdf_/1:Mother:M3}",              // M2 pattern differs from the M1 pattern
+               "\t: ${CC:C/^/_asdf_/1:M_asdf_*:M3}",            // M3 ist not a substitution modifier
+               "\t: ${CC:C/^/_asdf_/1:M_asdf_*:S,from,to,}",    // M3 pattern differs from the M1 pattern
+               "\t: ${CC:C/^/_asdf_/1:M_asdf_*:S,^_asdf_,to,}", // M3 replacement is not empty
+               "\t: ${CC:C/^/_asdf_/1:M_asdf_*:S,^_asdf_,,g}")  // M3 modifier has options
+
+       mklines.Check()
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkVarname(c *check.C) {
+       // FIXME
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("options.mk",
+               MkCvsID,
+               "COMMENT=\t${GAMES_USER}",
+               "COMMENT:=\t${PKGBASE}",
+               "PYPKGPREFIX=\t${PKGBASE}")
+       G.Pkgsrc.loadDefaultBuildDefs()
+       G.Pkgsrc.UserDefinedVars.Define("GAMES_USER", mklines.mklines[0])
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: options.mk:3: PKGBASE should not be used at load time in any file.",
+               "WARN: options.mk:4: The variable PYPKGPREFIX should not be set in this file; "+
+                       "it would be ok in pyversion.mk only.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__explain(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--explain")
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("options.mk",
+               MkCvsID,
+               "COMMENT=\t${GAMES_USER}",
+               "COMMENT:=\t${PKGBASE}",
+               "PYPKGPREFIX=\t${PKGBASE}")
+       G.Pkgsrc.loadDefaultBuildDefs()
+       G.Pkgsrc.UserDefinedVars.Define("GAMES_USER", mklines.mklines[0])
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: options.mk:3: PKGBASE 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 PKGBASE",
+               "\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.",
+               "",
+               "WARN: options.mk:4: The variable PYPKGPREFIX should not be set in this file; "+
+                       "it would be ok in pyversion.mk only.",
+               "",
+               "\tThe allowed actions for a variable are determined based on the file",
+               "\tname in which the variable is used or defined. The rules for",
+               "\tPYPKGPREFIX are:",
+               "",
+               "\t* in pyversion.mk, it may be set",
+               "\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.", "")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__load_time(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.Chdir("category/package")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("options.mk",
+               MkCvsID,
+               "",
+               "# don't include bsd.prefs.mk here",
+               "",
+               "WRKSRC:=\t${.CURDIR}",
+               ".if ${PKG_SYSCONFDIR.gdm} != \"etc\"",
+               ".endif")
+
+       mklines.Check()
+
+       // The PKG_SYSCONFDIR.* depend on the directory layout that is
+       // specified in mk.conf, therefore bsd.prefs.mk must be included first.
+       //
+       // Evaluating .CURDIR at load time is definitely ok since it is defined
+       // internally by bmake to be AlwaysInScope.
+       t.CheckOutputLines(
+               "WARN: options.mk:6: To use PKG_SYSCONFDIR.gdm at load time, " +
+                       ".include \"../../mk/bsd.prefs.mk\" first.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__load_time_in_condition(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtPathPattern, List,
+               "special:filename.mk: use-loadtime")
+       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtPathPattern, List,
+               "special:filename.mk: use")
+       t.Chdir(".")
+       t.FinishSetUp()
+
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               ".if ${LOAD_TIME} && ${RUN_TIME}",
+               ".endif")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:2: To use LOAD_TIME at load time, "+
+                       ".include \"mk/bsd.prefs.mk\" first.",
+               "WARN: filename.mk:2: RUN_TIME should not be used at load time in any file.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__load_time_in_for_loop(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtPathPattern, List,
+               "special:filename.mk: use-loadtime")
+       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtPathPattern, List,
+               "special:filename.mk: use")
+       t.Chdir(".")
+       t.FinishSetUp()
+
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               ".for pattern in ${LOAD_TIME} ${RUN_TIME}",
+               ".endfor")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:2: To use LOAD_TIME at load time, "+
+                       ".include \"mk/bsd.prefs.mk\" first.",
+               "WARN: filename.mk:2: RUN_TIME should not be used at load time in any file.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__load_time_guessed(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpTool("install", "", AtRunTime)
+       mklines := t.NewMkLines("install-docfiles.mk",
+               MkCvsID,
+               "DOCFILES=\ta b c",
+               "do-install:",
+               ".for f in ${DOCFILES}",
+               "\tinstall -c ${WRKSRC}/${f} ${DESTDIR}${PREFIX}/${f}",
+               ".endfor")
+
+       mklines.Check()
+
+       // No warning for using DOCFILES at compile-time. Since the variable
+       // name is not one of the predefined names from vardefs.go, the
+       // variable's type is guessed based on the name (see
+       // Pkgsrc.VariableType).
+       //
+       // These guessed variables are typically defined and used only in
+       // a single file, and in this context, mistakes are usually found
+       // quickly.
+       t.CheckOutputEmpty()
+}
+
+// Ensures that the warning "should not be evaluated at load time" is issued
+// only if using the variable at run time is allowed. If the latter were not
+// allowed, this warning would be confusing.
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__load_time_run_time(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME", BtUnknown, NoVartypeOptions,
+               "*.mk: use, use-loadtime")
+       G.Pkgsrc.vartypes.DefineParse("RUN_TIME", BtUnknown, NoVartypeOptions,
+               "*.mk: use")
+       G.Pkgsrc.vartypes.DefineParse("WRITE_ONLY", BtUnknown, NoVartypeOptions,
+               "*.mk: set")
+       G.Pkgsrc.vartypes.DefineParse("LOAD_TIME_ELSEWHERE", BtUnknown, NoVartypeOptions,
+               "Makefile: use-loadtime",
+               "*.mk: set")
+       G.Pkgsrc.vartypes.DefineParse("RUN_TIME_ELSEWHERE", BtUnknown, NoVartypeOptions,
+               "Makefile: use",
+               "*.mk: set")
+       t.Chdir(".")
+       t.FinishSetUp()
+
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               ".if ${LOAD_TIME} && ${RUN_TIME} && ${WRITE_ONLY}",
+               ".elif ${LOAD_TIME_ELSEWHERE} && ${RUN_TIME_ELSEWHERE}",
+               ".endif")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:2: To use LOAD_TIME at load time, "+
+                       ".include \"mk/bsd.prefs.mk\" first.",
+               "WARN: filename.mk:2: RUN_TIME should not be used at load time in any file.",
+               "WARN: filename.mk:2: "+
+                       "WRITE_ONLY should not be used in any file; "+
+                       "it is a write-only variable.",
+               "WARN: filename.mk:3: "+
+                       "LOAD_TIME_ELSEWHERE should not be used at load time in this file; "+
+                       "it would be ok in Makefile, but not *.mk.",
+               "WARN: filename.mk:3: "+
+                       "RUN_TIME_ELSEWHERE should not be used at load time in any file.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__PKGREVISION(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("any.mk",
+               MkCvsID,
+               ".if defined(PKGREVISION)",
+               ".endif")
+
+       mklines.Check()
+
+       // Since PKGREVISION may only be set in the package Makefile directly,
+       // there is no other file that could be mentioned as "it would be ok in".
+       t.CheckOutputLines(
+               "WARN: any.mk:2: PKGREVISION should not be used in any file; it is a write-only variable.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__indirectly(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("file.mk",
+               MkCvsID,
+               "IGNORE_PKG.package=\t${ONLY_FOR_UNPRIVILEGED}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: file.mk:2: IGNORE_PKG.package should be set to YES or yes.",
+               "WARN: file.mk:2: ONLY_FOR_UNPRIVILEGED should not be used indirectly at load time (via IGNORE_PKG.package).")
+}
+
+// This test is only here for branch coverage.
+// It does not demonstrate anything useful.
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__indirectly_tool(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("file.mk",
+               MkCvsID,
+               "USE_TOOLS+=\t${PKGREVISION}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: file.mk:2: PKGREVISION should not be used in any file; it is a write-only variable.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__write_only_usable_in_other_file(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("buildlink3.mk",
+               MkCvsID,
+               "VAR=\t${VAR} ${AUTO_MKDIRS}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: buildlink3.mk:2: " +
+                       "AUTO_MKDIRS should not be used in this file; " +
+                       "it would be ok in Makefile, Makefile.* or *.mk, " +
+                       "but not buildlink3.mk or builtin.mk.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__usable_only_at_loadtime_in_other_file(c *check.C) {
+       t := s.Init(c)
+
+       G.Pkgsrc.vartypes.DefineParse("VAR", BtFilename, NoVartypeOptions,
+               "*: set, use-loadtime")
+       mklines := t.NewMkLines("Makefile",
+               MkCvsID,
+               "VAR=\t${VAR}")
+
+       mklines.Check()
+
+       // Since the variable is usable at load time, pkglint assumes it is also
+       // usable at run time. This is not the case for VAR, but probably doesn't
+       // happen in practice anyway.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__assigned_to_infrastructure_variable(c *check.C) {
+       t := s.Init(c)
+
+       // This combination of BtUnknown and all permissions is typical for
+       // otherwise unknown variables from the pkgsrc infrastructure.
+       G.Pkgsrc.vartypes.Define("INFRA", BtUnknown, NoVartypeOptions,
+               NewACLEntry("*", aclpAll))
+       G.Pkgsrc.vartypes.DefineParse("VAR", BtUnknown, NoVartypeOptions,
+               "buildlink3.mk: none",
+               "*: use")
+       mklines := t.NewMkLines("buildlink3.mk",
+               MkCvsID,
+               "INFRA=\t${VAR}")
+
+       mklines.Check()
+
+       // Since INFRA is defined in the infrastructure and pkglint
+       // knows nothing else about this variable, it assumes that INFRA
+       // may be used at load time. This is done to prevent wrong warnings.
+       //
+       // This in turn has consequences when INFRA is used on the left-hand
+       // side of an assignment since pkglint assumes that the right-hand
+       // side may now be evaluated at load time.
+       //
+       // Therefore the check is skipped when such a variable appears at the
+       // left-hand side of an assignment.
+       //
+       // Even in this case involving an unknown infrastructure variable,
+       // it is possible to issue a warning since VAR should not be used at all,
+       // independent of any properties of INFRA.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__assigned_to_load_time(c *check.C) {
+       t := s.Init(c)
+
+       // LOAD_TIME may be used at load time in other.mk.
+       // Since VAR must not be used at load time at all, it would be dangerous
+       // 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", BtMessage, NoVartypeOptions,
+               "buildlink3.mk: set",
+               "*.mk: use-loadtime")
+       G.Pkgsrc.vartypes.DefineParse("VAR", BtUnknown, NoVartypeOptions,
+               "buildlink3.mk: none",
+               "*.mk: use")
+       mklines := t.NewMkLines("buildlink3.mk",
+               MkCvsID,
+               "LOAD_TIME=\t${VAR}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: buildlink3.mk:2: VAR should not be used indirectly " +
+                       "at load time (via LOAD_TIME).")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkPermissions__multiple_times_per_file(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("buildlink3.mk",
+               MkCvsID,
+               "VAR=\t${VAR} ${AUTO_MKDIRS} ${AUTO_MKDIRS} ${PKGREVISION} ${PKGREVISION}",
+               "VAR=\t${VAR} ${AUTO_MKDIRS} ${AUTO_MKDIRS} ${PKGREVISION} ${PKGREVISION}")
+
+       mklines.Check()
+
+       // Since these warnings are valid for the whole file, duplicates are suppressed.
+       t.CheckOutputLines(
+               "WARN: buildlink3.mk:2: "+
+                       "AUTO_MKDIRS should not be used in this file; "+
+                       "it would be ok in Makefile, Makefile.* or *.mk, "+
+                       "but not buildlink3.mk or builtin.mk.",
+               "WARN: buildlink3.mk:2: "+
+                       "PKGREVISION should not be used in any file; "+
+                       "it is a write-only variable.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_warnPermissions__not_directly_and_no_alternative_files(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("mk-c.mk",
+               MkCvsID,
+               "",
+               "# GUESSED_FLAGS",
+               "#\tDocumented here to suppress the \"defined but not used\"",
+               "#\twarning.",
+               "",
+               "TOOL_DEPENDS+=\t${BUILDLINK_API_DEPENDS.mk-c}:${BUILDLINK_PKGSRCDIR.mk-c}",
+               "GUESSED_FLAGS+=\t${BUILDLINK_CPPFLAGS}")
+
+       mklines.Check()
+
+       toolDependsType := G.Pkgsrc.VariableType(nil, "TOOL_DEPENDS")
+       t.CheckEquals(toolDependsType.String(), "DependencyWithPath (list, package-settable)")
+       t.CheckEquals(toolDependsType.AlternativeFiles(aclpAppend), "Makefile, Makefile.* or *.mk")
+       t.CheckEquals(toolDependsType.AlternativeFiles(aclpUse), "Makefile, Makefile.* or *.mk")
+       t.CheckEquals(toolDependsType.AlternativeFiles(aclpUseLoadtime), "")
+
+       apiDependsType := G.Pkgsrc.VariableType(nil, "BUILDLINK_API_DEPENDS.*")
+       t.CheckEquals(apiDependsType.String(), "Dependency (list, package-settable)")
+       t.CheckEquals(apiDependsType.AlternativeFiles(aclpUse), "")
+       t.CheckEquals(apiDependsType.AlternativeFiles(aclpUseLoadtime), "buildlink3.mk or builtin.mk only")
+
+       t.CheckOutputLines(
+               "WARN: mk-c.mk:7: BUILDLINK_API_DEPENDS.mk-c should not be used in any file.",
+               "WARN: mk-c.mk:7: The list variable BUILDLINK_API_DEPENDS.mk-c should not be embedded in a word.",
+               "WARN: mk-c.mk:7: BUILDLINK_PKGSRCDIR.mk-c should not be used in any file.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_explainPermissions(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--explain")
+       t.SetUpVartypes()
+
+       mklines := t.NewMkLines("buildlink3.mk",
+               MkCvsID,
+               "AUTO_MKDIRS=\tyes")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: buildlink3.mk:2: The variable AUTO_MKDIRS should not be set in this file; "+
+                       "it would be ok in Makefile, Makefile.* or *.mk, "+
+                       "but not buildlink3.mk or builtin.mk.",
+               "",
+               "\tThe allowed actions for a variable are determined based on the file",
+               "\tname in which the variable is used or defined. The rules for",
+               "\tAUTO_MKDIRS are:",
+               "",
+               "\t* in buildlink3.mk, it should not be accessed at all",
+               "\t* in builtin.mk, it should not be accessed at all",
+               "\t* in Makefile, it may be set, given a default value, or used",
+               "\t* in Makefile.*, it may be set, given a default value, or used",
+               "\t* in *.mk, it may be set, given a default value, or used",
+               // TODO: Add a check for infrastructure permissions
+               //  when the "infra:" prefix is added.
+               "",
+               "\tIf these rules seem to be incorrect, please ask on the",
+               "\ttech-pkg%NetBSD.org@localhost mailing list.",
+               "")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkUseAtLoadTime__buildlink3_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
+               ".if ${OPSYS} == NetBSD",
+               ".endif")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       G.Check("buildlink3.mk")
+
+       t.CheckOutputLines(
+               "WARN: buildlink3.mk:12: To use OPSYS at load time, " +
+                       ".include \"../../mk/bsd.fast.prefs.mk\" first.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkUseAtLoadTime__pkg_build_options_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpOption("option", "An example option")
+       t.CreateFileLines("mk/pkg-build-options.mk",
+               MkCvsID)
+       t.SetUpPackage("category/package")
+
+       t.CreateFileBuildlink3("category/package/buildlink3.mk",
+               "pkgbase := package",
+               ".include \"../../mk/pkg-build-options.mk\"",
+               ".if ${PKG_BUILD_OPTIONS.package:Moption}",
+               ".endif")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       G.Check("buildlink3.mk")
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkUseAtLoadTime__other_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("category/package/filename.mk",
+               MkCvsID,
+               ".if ${OPSYS} == NetBSD",
+               ".endif")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+
+       G.Check("filename.mk")
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:2: To use OPSYS at load time, " +
+                       ".include \"../../mk/bsd.prefs.mk\" first.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_warnToolLoadTime(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpTool("nowhere", "NOWHERE", Nowhere)
+       t.SetUpTool("after-prefs", "AFTER_PREFS", AfterPrefsMk)
+       t.SetUpTool("at-runtime", "AT_RUNTIME", AtRunTime)
+       mklines := t.NewMkLines("Makefile",
+               MkCvsID,
+               ".if ${NOWHERE} && ${AFTER_PREFS} && ${AT_RUNTIME} && ${MK_TOOL}",
+               ".endif",
+               "",
+               "TOOLS_CREATE+=\t\tmk-tool",
+               "_TOOLS_VARNAME.mk-tool=\tMK_TOOL")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: Makefile:2: To use the tool ${NOWHERE} at load time, "+
+                       "it has to be added to USE_TOOLS before including bsd.prefs.mk.",
+               "WARN: Makefile:2: To use the tool ${AFTER_PREFS} at load time, "+
+                       "bsd.prefs.mk has to be included before.",
+               "WARN: Makefile:2: The tool ${AT_RUNTIME} cannot be used at load time.",
+               "WARN: Makefile:2: To use the tool ${MK_TOOL} at load time, "+
+                       "bsd.prefs.mk has to be included before.",
+               "WARN: Makefile:6: Variable names starting with an underscore "+
+                       "(_TOOLS_VARNAME.mk-tool) are reserved for internal pkgsrc use.",
+               "WARN: Makefile:6: _TOOLS_VARNAME.mk-tool is defined but not used.")
+}
+
+// This somewhat unrealistic case demonstrates how there can be a tool in a
+// Makefile that is not known to the global pkgsrc.
+//
+// This test covers the "pkgsrcTool != nil" condition.
+func (s *Suite) Test_MkVarUseChecker_warnToolLoadTime__local_tool(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.CreateFileLines("mk/bsd.prefs.mk")
+       mklines := t.SetUpFileMkLines("category/package/Makefile",
+               MkCvsID,
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
+               "TOOLS_CREATE+=\t\tmk-tool",
+               "_TOOLS_VARNAME.mk-tool=\tMK_TOOL",
+               "",
+               "TOOL_OUTPUT!=\t${MK_TOOL}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:5: Variable names starting with an underscore (_TOOLS_VARNAME.mk-tool) are reserved for internal pkgsrc use.",
+               "WARN: ~/category/package/Makefile:5: _TOOLS_VARNAME.mk-tool is defined but not used.",
+               "WARN: ~/category/package/Makefile:7: TOOL_OUTPUT is defined but not used.",
+               "WARN: ~/category/package/Makefile:7: The tool ${MK_TOOL} cannot be used at load time.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkQuoting(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.SetUpFileMkLines("options.mk",
+               MkCvsID,
+               "GOPATH=\t${WRKDIR}",
+               "",
+               "CONFIGURE_ENV+=\tNAME=${R_PKGNAME} VER=${R_PKGVER}",
+               "",
+               "do-build:",
+               "\tcd ${WRKSRC} && GOPATH=${GOPATH} PATH=${PATH} :")
+
+       mklines.Check()
+
+       // For WRKSRC and GOPATH, no quoting is necessary since pkgsrc directories by
+       // definition don't contain special characters. Therefore they don't need the
+       // :Q, not even when used as part of a shell word.
+
+       // For PATH, the quoting is necessary because it may contain directories outside
+       // of pkgsrc, and these may contain special characters.
+
+       t.CheckOutputLines(
+               "WARN: ~/options.mk:7: The variable PATH should be quoted as part of a shell word.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkQuoting__mstar(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.SetUpFileMkLines("options.mk",
+               MkCvsID,
+               "CONFIGURE_ARGS+=\tCFLAGS=${CFLAGS:Q}",
+               "CONFIGURE_ARGS+=\tCFLAGS=${CFLAGS:M*:Q}",
+               "CONFIGURE_ARGS+=\tADA_FLAGS=${ADA_FLAGS:Q}",
+               "CONFIGURE_ARGS+=\tADA_FLAGS=${ADA_FLAGS:M*:Q}",
+               "CONFIGURE_ENV+=\t\tCFLAGS=${CFLAGS:Q}",
+               "CONFIGURE_ENV+=\t\tCFLAGS=${CFLAGS:M*:Q}",
+               "CONFIGURE_ENV+=\t\tADA_FLAGS=${ADA_FLAGS:Q}",
+               "CONFIGURE_ENV+=\t\tADA_FLAGS=${ADA_FLAGS:M*:Q}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "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_MkVarUseChecker_checkQuoting__mstar_not_needed(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetUpPackage("category/package",
+               "MAKE_FLAGS+=\tCFLAGS=${CFLAGS:M*:Q}",
+               "MAKE_FLAGS+=\tLFLAGS=${LDFLAGS:M*:Q}")
+       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)
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:20: The :M* modifier is not needed here.",
+               "NOTE: ~/category/package/Makefile:21: The :M* modifier is not needed here.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkQuoting__q_not_needed(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetUpPackage("category/package",
+               "MASTER_SITES=\t${HOMEPAGE:Q}")
+       t.FinishSetUp()
+
+       G.Check(pkg)
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:6: The :Q modifier isn't necessary for ${HOMEPAGE} here.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkQuoting__undefined_list_in_word_in_shell_command(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetUpPackage("category/package",
+               "\t${ECHO} ./${DISTFILES}")
+       t.FinishSetUp()
+
+       G.Check(pkg)
+
+       // The variable DISTFILES is declared by the infrastructure.
+       // It is not defined by this package, therefore it doesn't
+       // appear in the RedundantScope.
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:20: The list variable DISTFILES should not be embedded in a word.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkQuoting__list_variable_with_single_constant_value(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetUpPackage("category/package",
+               "BUILD_DIRS=\tonly-dir",
+               "",
+               "do-install:",
+               "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/")
+       t.FinishSetUp()
+
+       G.Check(pkg)
+
+       // Don't warn here since BUILD_DIRS, although being a list
+       // variable, contains only a single value.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkQuoting__list_variable_with_single_conditional_value(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetUpPackage("category/package",
+               "BUILD_DIRS=\tonly-dir",
+               ".if 0",
+               "BUILD_DIRS=\tother-dir",
+               ".endif",
+               "",
+               "do-install:",
+               "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/")
+       t.FinishSetUp()
+
+       G.Check(pkg)
+
+       // TODO: Don't warn here since BUILD_DIRS, although being a list
+       //  variable, contains only a single value.
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:26: " +
+                       "The list variable BUILD_DIRS should not be embedded in a word.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkQuoting__list_variable_with_two_constant_words(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetUpPackage("category/package",
+               "BUILD_DIRS=\tfirst-dir second-dir",
+               "",
+               "do-install:",
+               "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/")
+       t.FinishSetUp()
+
+       G.Check(pkg)
+
+       // Since BUILD_DIRS consists of two words, it would destroy the installation command.
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:23: " +
+                       "The list variable BUILD_DIRS should not be embedded in a word.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkQuotingQM(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "",
+               "CONFIGURE_ENV+=\tCFLAGS=${CFLAGS:Q}",
+               "CONFIGURE_ENV+=\tCFLAGS=${CFLAGS:M*:Q}",
+               "CONFIGURE_ENV+=\tCFLAGS=${CFLAGS:N*:Q}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:3: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q}.",
+               // XXX: Doesn't matter in this case since :N* results in an empty list.
+               "WARN: filename.mk:5: Please use ${CFLAGS:N*:M*:Q} instead of ${CFLAGS:N*:Q}.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_fixQuotingModifiers(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+
+       test := func() {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       MkCvsID,
+                       "",
+                       "CONFIGURE_ENV+=\tCFLAGS=${CFLAGS:Q}",
+                       "CONFIGURE_ENV+=\tCFLAGS=${CFLAGS:M*:Q}",
+                       "CONFIGURE_ENV+=\tCFLAGS=${CFLAGS:N*:Q}")
+
+               mklines.Check()
+       }
+
+       t.ExpectDiagnosticsAutofix(
+               test,
+               "WARN: ~/filename.mk:3: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q}.",
+               "WARN: ~/filename.mk:5: Please use ${CFLAGS:N*:M*:Q} instead of ${CFLAGS:N*:Q}.",
+               "AUTOFIX: ~/filename.mk:3: Replacing \"${CFLAGS:Q}\" with \"${CFLAGS:M*:Q}\".",
+               "AUTOFIX: ~/filename.mk:5: Replacing \"${CFLAGS:N*:Q}\" with \"${CFLAGS:N*:M*:Q}\".")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkBuildDefs(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.CreateFileLines("mk/defaults/mk.conf",
+               MkCvsID,
+               "USER_SETTABLE_OK?=\tyes",
+               "USER_SETTABLE_MISSING?=\tyes")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "",
+               "BUILD_DEFS+=\tUSER_SETTABLE_OK",
+               "",
+               "\t: ${USER_SETTABLE_OK}",
+               "\t: ${USER_SETTABLE_MISSING}",
+               "\t: ${PKGNAME}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:6: The user-defined variable " +
+                       "USER_SETTABLE_MISSING is used but not added to BUILD_DEFS.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkDeprecated(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.FinishSetUp()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "",
+               "\t: ${USE_CROSSBASE}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:3: USE_CROSSBASE is used but not defined.",
+               "WARN: filename.mk:3: Use of \"USE_CROSSBASE\" is deprecated. "+
+                       "Has been removed.")
+}



Home | Main Index | Thread Index | Old Index