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 Mar 10 19:01:51 UTC 2019

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile PLIST
        pkgsrc/pkgtools/pkglint/files: autofix.go autofix_test.go
            buildlink3_test.go category_test.go check_test.go distinfo.go
            distinfo_test.go line.go lines.go logging_test.go mkline.go
            mkline_test.go mklinechecker.go mklinechecker_test.go mklines.go
            mklines_test.go mkparser.go mkparser_test.go package.go
            package_test.go pkglint.1 pkglint.go pkglint_test.go pkgsrc.go
            pkgsrc_test.go plist.go plist_test.go shell.go shell_test.go
            substcontext.go substcontext_test.go util.go util_test.go var.go
            var_test.go vardefs.go vartype.go vartypecheck.go
            vartypecheck_test.go
        pkgsrc/pkgtools/pkglint/files/textproc: lexer.go
        pkgsrc/pkgtools/pkglint/files/trace: tracing.go
Added Files:
        pkgsrc/pkgtools/pkglint/files: mktokenslexer.go mktokenslexer_test.go
            redundantscope.go redundantscope_test.go

Log Message:
pkgtools/pkglint: update to 5.7.2

Changes since 5.7.1:

* Fixed detection of GNU_CONFIGURE=yes combined with USE_LANGUAGES
  missing c. This combination tends to fail in the configure phase.

* When the distinfo doesn't contain all hashes for the downloaded
  distfiles (typically SHA512 is missing) and the distfiles are actually
  downloaded to ${PKGSRCDIR}/distfiles, pkglint can now add the missing
  hashes. It only does this if there is at least one existing hash
  and if all existing hashes are correct.

* The check for redundant variables has been improved considerably.
  Before there were several situations in which pkglint didn't get the
  redundant variable definitions right because its internal model only
  mimicked reality. The model has been improved and so have the
  diagnostics.

* Pkglint only warns about wrong permissions (for defining or using
  a variable) when it knows the type of the variable and the permissions
  for the current file. Before, it had also warned if the permissions
  for the current file were not explicitly defined.

* CFLAGS and LDFLAGS may be appended in buildlink3.mk files. This
  had been disallowed before, for no apparent reason. There are several
  places in pkgsrc where especially CFLAGS.${OPSYS} is appended to.

* Cleaned up internal handling of relative paths. Previously pkglint
  sometimes resolved relative paths using the wrong base directory,
  which led to all kinds of wrong warnings and strange behavior.

* Fixed lots of edge cases when parsing Makefile lines. These cases
  don't occur often but experience tells that the most fundamental code
  must be as correct as possible (see the handling of relative paths
  above).

* Lots of refactoring and housekeeping, as always.


To generate a diff of this commit:
cvs rdiff -u -r1.569 -r1.570 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.10 -r1.11 pkgsrc/pkgtools/pkglint/PLIST
cvs rdiff -u -r1.17 -r1.18 pkgsrc/pkgtools/pkglint/files/autofix.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
cvs rdiff -u -r1.18 -r1.19 pkgsrc/pkgtools/pkglint/files/autofix_test.go
cvs rdiff -u -r1.27 -r1.28 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go \
    pkgsrc/pkgtools/pkglint/files/vartype.go
cvs rdiff -u -r1.19 -r1.20 pkgsrc/pkgtools/pkglint/files/category_test.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc.go
cvs rdiff -u -r1.34 -r1.35 pkgsrc/pkgtools/pkglint/files/check_test.go \
    pkgsrc/pkgtools/pkglint/files/shell.go
cvs rdiff -u -r1.28 -r1.29 pkgsrc/pkgtools/pkglint/files/distinfo.go
cvs rdiff -u -r1.25 -r1.26 pkgsrc/pkgtools/pkglint/files/distinfo_test.go
cvs rdiff -u -r1.33 -r1.34 pkgsrc/pkgtools/pkglint/files/line.go \
    pkgsrc/pkgtools/pkglint/files/pkglint_test.go \
    pkgsrc/pkgtools/pkglint/files/plist_test.go
cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/lines.go
cvs rdiff -u -r1.12 -r1.13 pkgsrc/pkgtools/pkglint/files/logging_test.go
cvs rdiff -u -r1.47 -r1.48 pkgsrc/pkgtools/pkglint/files/mkline.go \
    pkgsrc/pkgtools/pkglint/files/pkglint.go
cvs rdiff -u -r1.52 -r1.53 pkgsrc/pkgtools/pkglint/files/mkline_test.go
cvs rdiff -u -r1.30 -r1.31 pkgsrc/pkgtools/pkglint/files/mklinechecker.go
cvs rdiff -u -r1.26 -r1.27 \
    pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go
cvs rdiff -u -r1.42 -r1.43 pkgsrc/pkgtools/pkglint/files/mklines.go \
    pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
cvs rdiff -u -r1.37 -r1.38 pkgsrc/pkgtools/pkglint/files/mklines_test.go \
    pkgsrc/pkgtools/pkglint/files/plist.go
cvs rdiff -u -r1.24 -r1.25 pkgsrc/pkgtools/pkglint/files/mkparser.go
cvs rdiff -u -r1.23 -r1.24 pkgsrc/pkgtools/pkglint/files/mkparser_test.go \
    pkgsrc/pkgtools/pkglint/files/util_test.go
cvs rdiff -u -r0 -r1.1 pkgsrc/pkgtools/pkglint/files/mktokenslexer.go \
    pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go \
    pkgsrc/pkgtools/pkglint/files/redundantscope.go \
    pkgsrc/pkgtools/pkglint/files/redundantscope_test.go
cvs rdiff -u -r1.46 -r1.47 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.39 -r1.40 pkgsrc/pkgtools/pkglint/files/package_test.go
cvs rdiff -u -r1.54 -r1.55 pkgsrc/pkgtools/pkglint/files/pkglint.1
cvs rdiff -u -r1.40 -r1.41 pkgsrc/pkgtools/pkglint/files/shell_test.go
cvs rdiff -u -r1.21 -r1.22 pkgsrc/pkgtools/pkglint/files/substcontext.go
cvs rdiff -u -r1.22 -r1.23 pkgsrc/pkgtools/pkglint/files/substcontext_test.go
cvs rdiff -u -r1.38 -r1.39 pkgsrc/pkgtools/pkglint/files/util.go
cvs rdiff -u -r1.1 -r1.2 pkgsrc/pkgtools/pkglint/files/var.go \
    pkgsrc/pkgtools/pkglint/files/var_test.go
cvs rdiff -u -r1.55 -r1.56 pkgsrc/pkgtools/pkglint/files/vardefs.go
cvs rdiff -u -r1.50 -r1.51 pkgsrc/pkgtools/pkglint/files/vartypecheck.go
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/textproc/lexer.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/trace/tracing.go

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

Modified files:

Index: pkgsrc/pkgtools/pkglint/Makefile
diff -u pkgsrc/pkgtools/pkglint/Makefile:1.569 pkgsrc/pkgtools/pkglint/Makefile:1.570
--- pkgsrc/pkgtools/pkglint/Makefile:1.569      Sat Mar  9 10:05:10 2019
+++ pkgsrc/pkgtools/pkglint/Makefile    Sun Mar 10 19:01:50 2019
@@ -1,7 +1,6 @@
-# $NetBSD: Makefile,v 1.569 2019/03/09 10:05:10 bsiegert Exp $
+# $NetBSD: Makefile,v 1.570 2019/03/10 19:01:50 rillig Exp $
 
-PKGNAME=       pkglint-5.7.1
-PKGREVISION=   1
+PKGNAME=       pkglint-5.7.2
 CATEGORIES=    pkgtools
 DISTNAME=      tools
 MASTER_SITES=  ${MASTER_SITE_GITHUB:=golang/}
@@ -80,4 +79,5 @@ do-install-man: .PHONY
 BUILDLINK_DEPMETHOD.go-check=  full
 
 .include "../../devel/go-check/buildlink3.mk"
+.include "../../security/go-crypto/buildlink3.mk"
 .include "../../mk/bsd.pkg.mk"

Index: pkgsrc/pkgtools/pkglint/PLIST
diff -u pkgsrc/pkgtools/pkglint/PLIST:1.10 pkgsrc/pkgtools/pkglint/PLIST:1.11
--- pkgsrc/pkgtools/pkglint/PLIST:1.10  Sat Jan 26 16:31:33 2019
+++ pkgsrc/pkgtools/pkglint/PLIST       Sun Mar 10 19:01:50 2019
@@ -1,4 +1,4 @@
-@comment $NetBSD: PLIST,v 1.10 2019/01/26 16:31:33 rillig Exp $
+@comment $NetBSD: PLIST,v 1.11 2019/03/10 19:01:50 rillig Exp $
 bin/pkglint
 gopkg/pkg/${GO_PLATFORM}/netbsd.org/pkglint.a
 gopkg/pkg/${GO_PLATFORM}/netbsd.org/pkglint/getopt.a
@@ -63,6 +63,8 @@ gopkg/src/netbsd.org/pkglint/mkshtypes.g
 gopkg/src/netbsd.org/pkglint/mkshtypes_test.go
 gopkg/src/netbsd.org/pkglint/mkshwalker.go
 gopkg/src/netbsd.org/pkglint/mkshwalker_test.go
+gopkg/src/netbsd.org/pkglint/mktokenslexer.go
+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/options.go
@@ -81,6 +83,8 @@ gopkg/src/netbsd.org/pkglint/pkgver/verc
 gopkg/src/netbsd.org/pkglint/pkgver/vercmp_test.go
 gopkg/src/netbsd.org/pkglint/plist.go
 gopkg/src/netbsd.org/pkglint/plist_test.go
+gopkg/src/netbsd.org/pkglint/redundantscope.go
+gopkg/src/netbsd.org/pkglint/redundantscope_test.go
 gopkg/src/netbsd.org/pkglint/regex/regex.go
 gopkg/src/netbsd.org/pkglint/shell.go
 gopkg/src/netbsd.org/pkglint/shell.y

Index: pkgsrc/pkgtools/pkglint/files/autofix.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix.go:1.17 pkgsrc/pkgtools/pkglint/files/autofix.go:1.18
--- pkgsrc/pkgtools/pkglint/files/autofix.go:1.17       Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix.go    Sun Mar 10 19:01:50 2019
@@ -328,9 +328,9 @@ func (fix *Autofix) Realign(mkline MkLin
        {
                // Parsing the continuation marker as variable value is cheating but works well.
                text := strings.TrimSuffix(mkline.raw[0].orignl, "\n")
-               _, _, _, _, _, valueAlign, value, _, _ := MatchVarassign(text)
-               if value != "\\" {
-                       oldWidth = tabWidth(valueAlign)
+               _, a := MatchVarassign(text)
+               if a.value != "\\" {
+                       oldWidth = tabWidth(a.valueAlign)
                }
        }
 
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.17 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.17   Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go        Sun Mar 10 19:01:50 2019
@@ -659,3 +659,26 @@ func (s *Suite) Test_Pkgsrc_VariableType
                "WARN: ~/category/package/Makefile:21: ABCPATH is used but not defined.",
                "0 errors and 2 warnings found.")
 }
+
+func (s *Suite) Test_Pkgsrc_guessVariableType__SKIP(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("filename.mk",
+               MkRcsID,
+               "MY_CHECK_SKIP=\t*.c \"bad*pathname\"",
+               "MY_CHECK_SKIP+=\t*.cpp",
+               ".if ${MY_CHECK_SKIP}",
+               ".endif")
+
+       mklines.Check()
+
+       // FIXME: The permissions in guessVariableType say allRuntime, which excludes
+       //  aclpUseLoadtime. Therefore there should be a warning about the VarUse in
+       //  the .if line.
+       //  The check in MkLineChecker.checkVarusePermissions is disabled for guessed types.
+       //
+       // 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.
+       t.CheckOutputLines(
+               "WARN: filename.mk:2: \"\\\"bad*pathname\\\"\" is not a valid pathname mask.")
+}

Index: pkgsrc/pkgtools/pkglint/files/autofix_test.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.18 pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.19
--- pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.18  Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix_test.go       Sun Mar 10 19:01:50 2019
@@ -161,7 +161,6 @@ func (s *Suite) Test_Autofix_ReplaceRege
        fix.Apply()
 
        t.CheckOutputLines(
-               "",
                "AUTOFIX: ~/Makefile:2: Replacing \"X\" with \"Y\".",
                "-\tline2",
                "+\tYXXe2")

Index: pkgsrc/pkgtools/pkglint/files/buildlink3_test.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.27 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.28
--- pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.27       Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/buildlink3_test.go    Sun Mar 10 19:01:50 2019
@@ -474,22 +474,13 @@ func (s *Suite) Test_CheckLinesBuildlink
        t.CheckOutputLines(
                "WARN: buildlink3.mk:3: LICENSE may not be used in any file; it is a write-only variable.",
                "WARN: buildlink3.mk:3: The variable LICENSE should be quoted as part of a shell word.",
-
                "WARN: buildlink3.mk:8: LICENSE should not be evaluated at load time.",
-               "WARN: buildlink3.mk:8: LICENSE may not be used in any file; it is a write-only variable.",
                "WARN: buildlink3.mk:8: LICENSE should not be evaluated indirectly at load time.",
-               "WARN: buildlink3.mk:8: LICENSE may not be used in any file; it is a write-only variable.",
                "WARN: buildlink3.mk:8: The variable LICENSE should be quoted as part of a shell word.",
-
                "WARN: buildlink3.mk:9: LICENSE should not be evaluated at load time.",
-               "WARN: buildlink3.mk:9: LICENSE may not be used in any file; it is a write-only variable.",
                "WARN: buildlink3.mk:9: LICENSE should not be evaluated indirectly at load time.",
-               "WARN: buildlink3.mk:9: LICENSE may not be used in any file; it is a write-only variable.",
                "WARN: buildlink3.mk:9: The variable LICENSE should be quoted as part of a shell word.",
-
-               "WARN: buildlink3.mk:13: LICENSE may not be used in any file; it is a write-only variable.",
                "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).")
 }
 
@@ -640,13 +631,9 @@ func (s *Suite) Test_Buildlink3Checker_c
 
        G.Check(t.File("category/package"))
 
-       // FIXME: Why is appending to LDFLAGS forbidden? It sounds useful.
        t.CheckOutputLines(
-               "WARN: ~/category/package/buildlink3.mk:14: "+
-                       "The variable LDFLAGS.NetBSD may not be appended to in this file; "+
-                       "it would be ok in Makefile, Makefile.common, options.mk or *.mk.",
-               "WARN: ~/category/package/buildlink3.mk:16: "+
-                       "Only buildlink variables for \"package\", "+
+               "WARN: ~/category/package/buildlink3.mk:16: " +
+                       "Only buildlink variables for \"package\", " +
                        "not \"other\" may be set in this file.")
 }
 
Index: pkgsrc/pkgtools/pkglint/files/vartype.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype.go:1.27 pkgsrc/pkgtools/pkglint/files/vartype.go:1.28
--- pkgsrc/pkgtools/pkglint/files/vartype.go:1.27       Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/vartype.go    Sun Mar 10 19:01:50 2019
@@ -38,6 +38,10 @@ const (
        aclpAppend                                 // VAR += value
        aclpUseLoadtime                            // OTHER := ${VAR}, OTHER != ${VAR}
        aclpUse                                    // OTHER = ${VAR}
+
+       // TODO: Try what happens if this constant is removed.
+       //  All variables should have proper permission definitions for all files.
+       //  Missing permission definitions could also count as "none".
        aclpUnknown
        aclpAllWrite   = aclpSet | aclpSetDefault | aclpAppend
        aclpAllRead    = aclpUseLoadtime | aclpUse

Index: pkgsrc/pkgtools/pkglint/files/category_test.go
diff -u pkgsrc/pkgtools/pkglint/files/category_test.go:1.19 pkgsrc/pkgtools/pkglint/files/category_test.go:1.20
--- pkgsrc/pkgtools/pkglint/files/category_test.go:1.19 Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/category_test.go      Sun Mar 10 19:01:50 2019
@@ -290,8 +290,11 @@ func (s *Suite) Test_CheckdirCategory__c
 
        CheckdirCategory(t.File("category"))
 
-       // FIXME: Wow. These are quite a few warnings and errors, just because there is
-       //  an additional comment above the COMMENT definition.
+       // These are quite a few warnings and errors, just because there is
+       // an additional comment above the COMMENT definition.
+       // On the other hand, the category Makefiles are so simple and their
+       // structure has been fixed for at least 20 years, therefore this case
+       // is rather exotic anyway.
        t.CheckOutputLines(
                "ERROR: ~/category/Makefile:3: COMMENT= line expected.",
                "NOTE: ~/category/Makefile:2: Empty line expected after this line.",
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.19 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.20
--- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.19        Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go     Sun Mar 10 19:01:50 2019
@@ -814,6 +814,13 @@ func (src *Pkgsrc) ToRel(filename string
        return relpath(src.topdir, filename)
 }
 
+// IsInfra returns whether the given filename (relative to the pkglint
+// working directory) is part of the pkgsrc infrastructure.
+func (src *Pkgsrc) IsInfra(filename string) bool {
+       rel := src.ToRel(filename)
+       return hasPrefix(rel, "mk/") || hasPrefix(rel, "wip/mk/")
+}
+
 func (src *Pkgsrc) addBuildDefs(varnames ...string) {
        for _, varname := range varnames {
                src.buildDefs[varname] = true
@@ -953,6 +960,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().
                gtype = &Vartype{lkNone, BtUnknown, allowAll, true}
+       case hasSuffix(varbase, "_SKIP"):
+               gtype = &Vartype{lkShell, BtPathmask, allowRuntime, true}
        }
 
        if gtype == nil {

Index: pkgsrc/pkgtools/pkglint/files/check_test.go
diff -u pkgsrc/pkgtools/pkglint/files/check_test.go:1.34 pkgsrc/pkgtools/pkglint/files/check_test.go:1.35
--- pkgsrc/pkgtools/pkglint/files/check_test.go:1.34    Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/check_test.go Sun Mar 10 19:01:50 2019
@@ -92,7 +92,6 @@ func (s *Suite) TearDownTest(c *check.C)
                _, _ = fmt.Fprintf(os.Stderr, "Cannot chdir back to previous dir: %s", err)
        }
 
-       G = Pkglint{} // unusable because of missing Logger.out and Logger.err
        if out := t.Output(); out != "" {
                var msg strings.Builder
                msg.WriteString("\n")
@@ -106,8 +105,11 @@ func (s *Suite) TearDownTest(c *check.C)
                _, _ = fmt.Fprintf(&msg, "\n")
                _, _ = os.Stderr.WriteString(msg.String())
        }
+
        t.tmpdir = ""
        t.DisableTracing()
+
+       G = Pkglint{} // unusable because of missing Logger.out and Logger.err
 }
 
 var _ = check.Suite(new(Suite))
@@ -196,6 +198,36 @@ func (t *Tester) SetUpFileMkLines(relati
        return LoadMk(filename, MustSucceed)
 }
 
+// LoadMkInclude loads the given Makefile fragment and all the files it includes,
+// merging all the lines into a single MkLines object.
+//
+// This is useful for testing code related to Package.readMakefile.
+func (t *Tester) LoadMkInclude(relativeFileName string) MkLines {
+       var lines []Line
+
+       // TODO: Include files with multiple-inclusion guard only once.
+       // TODO: Include files without multiple-inclusion guard as often as needed.
+       // TODO: Set an upper limit, to prevent denial of service.
+
+       var load func(filename string)
+       load = func(filename string) {
+               for _, mkline := range NewMkLines(Load(filename, MustSucceed)).mklines {
+                       lines = append(lines, mkline.Line)
+
+                       if mkline.IsInclude() {
+                               included := cleanpath(path.Dir(filename) + "/" + mkline.IncludedFile())
+                               load(included)
+                       }
+               }
+       }
+
+       load(t.File(relativeFileName))
+
+       // This assumes that the test files do not contain parse errors.
+       // Otherwise the diagnostics would appear twice.
+       return NewMkLines(NewLines(t.File(relativeFileName), lines))
+}
+
 // SetUpPkgsrc sets up a minimal but complete pkgsrc installation in the
 // temporary folder, so that pkglint runs without any errors.
 // Individual files may be overwritten by calling other SetUp* methods.
@@ -257,6 +289,8 @@ func (t *Tester) SetUpPkgsrc() {
        // used at load time by packages.
        t.CreateFileLines("mk/bsd.prefs.mk",
                MkRcsID)
+       t.CreateFileLines("mk/bsd.fast.prefs.mk",
+               MkRcsID)
 
        // Category Makefiles require this file for the common definitions.
        t.CreateFileLines("mk/misc/category.mk")
@@ -486,6 +520,69 @@ func (t *Tester) Remove(relativeFileName
        G.fileCache.Evict(filename)
 }
 
+// SetUpHierarchy provides a function for creating hierarchies of MkLines
+// that include each other.
+// The hierarchy is created only in memory, nothing is written to disk.
+//
+//  include, get := t.SetUpHierarchy()
+//
+//  include("including.mk",
+//      include("other.mk",
+//          "VAR= other"),
+//      include("module.mk",
+//          "VAR= module",
+//          include("version.mk",
+//              "VAR= version"),
+//          include("env.mk",
+//              "VAR= env")))
+//
+//  mklines := get("including.mk")
+//  module := get("module.mk")
+func (t *Tester) SetUpHierarchy() (
+       include func(filename string, args ...interface{}) MkLines,
+       get func(string) MkLines) {
+
+       files := map[string]MkLines{}
+
+       // FIXME: Define where the filename is relative to: to the file, or to the current directory.
+       include = func(filename string, args ...interface{}) MkLines {
+               var lines []Line
+               lineno := 1
+
+               addLine := func(text string) {
+                       lines = append(lines, t.NewLine(filename, lineno, text))
+                       lineno++
+               }
+
+               for _, arg := range args {
+                       switch arg := arg.(type) {
+                       case string:
+                               addLine(arg)
+                       case MkLines:
+                               text := sprintf(".include %q", arg.lines.FileName)
+                               addLine(text)
+                               lines = append(lines, arg.lines.Lines...)
+                       default:
+                               panic("invalid type")
+                       }
+               }
+
+               mklines := NewMkLines(NewLines(filename, lines))
+               // FIXME: This filename must be relative to the including file.
+               G.Assertf(files[filename] == nil, "MkLines with name %q already exist.", filename)
+               // FIXME: This filename must be relative to the base directory.
+               files[filename] = mklines
+               return mklines
+       }
+
+       get = func(filename string) MkLines {
+               G.Assertf(files[filename] != nil, "MkLines with name %q doesn't exist.", filename)
+               return files[filename]
+       }
+
+       return
+}
+
 // Check delegates a check to the check.Check function.
 // Thereby, there is no need to distinguish between c.Check and t.Check
 // in the test code.
@@ -584,6 +681,11 @@ func (t *Tester) NewLine(filename string
 
 // NewMkLine creates an in-memory line in the Makefile format with the given text.
 func (t *Tester) NewMkLine(filename string, lineno int, text string) MkLine {
+       basename := path.Base(filename)
+       G.Assertf(
+               hasSuffix(basename, ".mk") || basename == "Makefile" || hasPrefix(basename, "Makefile."),
+               "filename %q must be realistic, otherwise the variable permissions are wrong", filename)
+
        return NewMkLine(t.NewLine(filename, lineno, text))
 }
 
@@ -616,6 +718,11 @@ func (t *Tester) NewLinesAt(filename str
 // No actual file is created for the lines;
 // see SetUpFileMkLines for loading Makefile fragments with line continuations.
 func (t *Tester) NewMkLines(filename string, lines ...string) MkLines {
+       basename := path.Base(filename)
+       G.Assertf(
+               hasSuffix(basename, ".mk") || basename == "Makefile" || hasPrefix(basename, "Makefile."),
+               "filename %q must be realistic, otherwise the variable permissions are wrong", filename)
+
        var rawText strings.Builder
        for _, line := range lines {
                rawText.WriteString(line)
@@ -633,13 +740,18 @@ func (t *Tester) Output() string {
        t.stdout.Reset()
        t.stderr.Reset()
        G.Logger.logged = Once{}
+       if G.Logger.out != nil { // Necessary because Main resets the G variable.
+               G.Logger.out.state = 0 // Prevent an empty line at the beginning of the next output.
+               G.Logger.err.state = 0
+       }
 
+       G.Assertf(t.tmpdir != "", "Tester must be initialized before checking the output.")
        output := stdout + stderr
-       if t.tmpdir != "" {
-               output = strings.Replace(output, t.tmpdir, "~", -1)
-       } else {
-               panic("asdfgsfas")
-       }
+       // TODO: The explanations are wrapped. Because of this it can happen
+       //  that t.tmpdir is spread among multiple lines if that directory
+       //  name contains spaces, which is common on Windows. A temporary
+       //  workaround is to set TMP=/path/without/spaces.
+       output = strings.Replace(output, t.tmpdir, "~", -1)
        return output
 }
 
Index: pkgsrc/pkgtools/pkglint/files/shell.go
diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.34 pkgsrc/pkgtools/pkglint/files/shell.go:1.35
--- pkgsrc/pkgtools/pkglint/files/shell.go:1.34 Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/shell.go      Sun Mar 10 19:01:50 2019
@@ -530,14 +530,6 @@ func (scc *SimpleCommandChecker) handleC
        if varuse := parser.VarUse(); varuse != nil && parser.EOF() {
                varname := varuse.varname
 
-               if tool := G.ToolByVarname(varname); tool != nil {
-                       if tool.Validity == Nowhere {
-                               scc.shline.mkline.Warnf("The %q tool is used but not added to USE_TOOLS.", tool.Name)
-                       }
-                       scc.shline.checkInstallCommand(shellword)
-                       return true
-               }
-
                if vartype := G.Pkgsrc.VariableType(varname); vartype != nil && vartype.basicType.name == "ShellCommand" {
                        scc.shline.checkInstallCommand(shellword)
                        return true

Index: pkgsrc/pkgtools/pkglint/files/distinfo.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo.go:1.28 pkgsrc/pkgtools/pkglint/files/distinfo.go:1.29
--- pkgsrc/pkgtools/pkglint/files/distinfo.go:1.28      Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/distinfo.go   Sun Mar 10 19:01:50 2019
@@ -3,9 +3,13 @@ package pkglint
 import (
        "bytes"
        "crypto/sha1"
+       "crypto/sha512"
        "encoding/hex"
+       "golang.org/x/crypto/ripemd160"
+       "hash"
+       "io"
        "io/ioutil"
-       "path"
+       "os"
        "strings"
 )
 
@@ -26,106 +30,273 @@ func CheckLinesDistinfo(lines Lines) {
        distinfoIsCommitted := isCommitted(filename)
        ck := distinfoLinesChecker{
                lines, patchdir, distinfoIsCommitted,
-               make(map[string]bool), "", nil, unknown, nil}
-       ck.checkLines(lines)
+               nil, make(map[string]distinfoFileInfo)}
+       ck.parse()
+       ck.check()
        CheckLinesTrailingEmptyLines(lines)
        ck.checkUnrecordedPatches()
+
        SaveAutofixChanges(lines)
 }
 
-// XXX: Maybe an approach that first groups the lines by filename
-// is easier to understand.
-
 type distinfoLinesChecker struct {
-       distinfoLines       Lines
+       lines               Lines
        patchdir            string // Relative to G.Pkg
        distinfoIsCommitted bool
 
-       // All patches that are mentioned in the distinfo file.
-       patches map[string]bool // "patch-aa" => true
-
-       currentFileName  string
-       currentFirstLine Line         // The first line of the currentFileName group
-       isPatch          YesNoUnknown // Whether currentFileName is a patch, as opposed to a distfile
-       algorithms       []string     // The algorithms seen for currentFileName
+       filenames []string // For keeping the order from top to bottom
+       infos     map[string]distinfoFileInfo
 }
 
-func (ck *distinfoLinesChecker) checkLines(lines Lines) {
-       lines.CheckRcsID(0, ``, "")
-       if 1 < len(lines.Lines) && lines.Lines[1].Text != "" {
-               lines.Lines[1].Notef("Empty line expected.")
-       }
+func (ck *distinfoLinesChecker) parse() {
+       lines := ck.lines
 
-       for i, line := range lines.Lines {
-               if i < 2 {
-                       continue
+       llex := NewLinesLexer(lines)
+       if lines.CheckRcsID(0, ``, "") {
+               llex.Skip()
+       }
+       llex.SkipEmptyOrNote()
+
+       prevFilename := ""
+       var hashes []distinfoHash
+
+       isPatch := func() YesNoUnknown {
+               switch {
+               case !hasPrefix(prevFilename, "patch-"):
+                       return no
+               case G.Pkg == nil:
+                       return unknown
+               case fileExists(G.Pkg.File(ck.patchdir + "/" + prevFilename)):
+                       return yes
+               default:
+                       return no
                }
-               m, alg, filename, hash := match3(line.Text, `^(\w+) \((\w[^)]*)\) = (.*)(?: bytes)?$`)
+       }
+
+       finishGroup := func() {
+               ck.filenames = append(ck.filenames, prevFilename)
+               ck.infos[prevFilename] = distinfoFileInfo{isPatch(), hashes}
+               hashes = nil
+       }
+
+       for !llex.EOF() {
+               line := llex.CurrentLine()
+               llex.Skip()
+
+               m, alg, filename, hash := match3(line.Text, `^(\w+) \((\w[^)]*)\) = (\S+(?: bytes)?)$`)
                if !m {
                        line.Errorf("Invalid line: %s", line.Text)
                        continue
                }
 
-               if filename != ck.currentFileName {
-                       ck.onFilenameChange(line, filename)
+               if prevFilename != "" && filename != prevFilename {
+                       finishGroup()
                }
-               ck.algorithms = append(ck.algorithms, alg)
+               prevFilename = filename
 
-               ck.checkGlobalDistfileMismatch(line, filename, alg, hash)
-               ck.checkUncommittedPatch(line, filename, alg, hash)
+               hashes = append(hashes, distinfoHash{line, filename, alg, hash})
        }
-       ck.onFilenameChange(ck.distinfoLines.EOFLine(), "")
-}
 
-func (ck *distinfoLinesChecker) onFilenameChange(line Line, nextFname string) {
-       if ck.currentFileName != "" {
-               ck.checkAlgorithms(line)
+       if prevFilename != "" {
+               finishGroup()
        }
+}
 
-       if !hasPrefix(nextFname, "patch-") {
-               ck.isPatch = no
-       } else if G.Pkg == nil {
-               ck.isPatch = unknown
-       } else if fileExists(G.Pkg.File(ck.patchdir + "/" + nextFname)) {
-               ck.isPatch = yes
-       } else {
-               ck.isPatch = no
+func (ck *distinfoLinesChecker) check() {
+       for _, filename := range ck.filenames {
+               info := ck.infos[filename]
+
+               ck.checkAlgorithms(info)
+               for _, hash := range info.hashes {
+                       ck.checkGlobalDistfileMismatch(hash)
+                       if info.isPatch == yes {
+                               ck.checkUncommittedPatch(hash)
+                       }
+               }
        }
-
-       ck.currentFileName = nextFname
-       ck.currentFirstLine = line
-       ck.algorithms = nil
 }
 
-func (ck *distinfoLinesChecker) checkAlgorithms(line Line) {
-       filename := ck.currentFileName
-       algorithms := strings.Join(ck.algorithms, ", ")
+func (ck *distinfoLinesChecker) checkAlgorithms(info distinfoFileInfo) {
+       filename := info.filename()
+       algorithms := info.algorithms()
+       line := info.line()
+
+       isPatch := info.isPatch
 
        switch {
+       case algorithms == "SHA1" && isPatch != no:
+               return
 
-       case ck.isPatch == yes:
-               if algorithms != "SHA1" {
-                       line.Errorf("Expected SHA1 hash for %s, got %s.", filename, algorithms)
-               }
+       case algorithms == "SHA1, RMD160, SHA512, Size" && isPatch != yes:
+               return
+       }
 
-       case ck.isPatch == unknown:
-               break
+       switch {
+       case isPatch == yes:
+               line.Errorf("Expected SHA1 hash for %s, got %s.", filename, algorithms)
 
-       case G.Pkg != nil && G.Pkg.IgnoreMissingPatches:
-               break
+       case isPatch == unknown:
+               line.Errorf("Wrong checksum algorithms %s for %s.", algorithms, filename)
+               line.Explain(
+                       "Distfiles that are downloaded from external sources must have the",
+                       "checksum algorithms SHA1, RMD160, SHA512, Size.",
+                       "",
+                       "Patch files from pkgsrc must have only the SHA1 hash.")
+
+       // At this point, the file is either a missing patch file or a distfile.
 
        case hasPrefix(filename, "patch-") && algorithms == "SHA1":
-               pathToPatchdir := relpath(path.Dir(ck.currentFirstLine.Filename), G.Pkg.File(ck.patchdir))
-               ck.currentFirstLine.Warnf("Patch file %q does not exist in directory %q.", filename, pathToPatchdir)
+               if G.Pkg.IgnoreMissingPatches {
+                       break
+               }
+
+               line.Warnf("Patch file %q does not exist in directory %q.",
+                       filename, line.PathToFile(G.Pkg.File(ck.patchdir)))
                G.Explain(
                        "If the patches directory looks correct, the patch may have been",
                        "removed without updating the distinfo file.",
                        "In such a case please update the distinfo file.",
                        "",
-                       "If the patches directory looks wrong, pkglint needs to be improved.")
+                       "In rare cases, pkglint cannot determine the correct location of the patches directory.",
+                       "In that case, see the pkglint man page for contact information.")
 
-       case algorithms != "SHA1, RMD160, SHA512, Size":
-               line.Errorf("Expected SHA1, RMD160, SHA512, Size checksums for %q, got %s.", filename, algorithms)
+       default:
+               ck.checkAlgorithmsDistfile(info)
+       }
+}
+
+// checkAlgorithmsDistfile checks whether some of the standard algorithms are
+// missing. If so and the downloaded distfile exists, they are calculated and
+// added to the distinfo file via an autofix.
+func (ck *distinfoLinesChecker) checkAlgorithmsDistfile(info distinfoFileInfo) {
+       line := info.line()
+       line.Errorf("Expected SHA1, RMD160, SHA512, Size checksums for %q, got %s.", info.filename(), info.algorithms())
+
+       algorithms := [...]string{"SHA1", "RMD160", "SHA512", "Size"}
+
+       missing := map[string]bool{}
+       for _, alg := range algorithms {
+               missing[alg] = true
+       }
+       seen := map[string]distinfoHash{}
+
+       for _, hash := range info.hashes {
+               alg := hash.algorithm
+               if missing[alg] {
+                       seen[alg] = hash
+                       delete(missing, alg)
+               }
+       }
+
+       if len(missing) == 0 || len(seen) == 0 {
+               return
+       }
+
+       distdir := G.Pkgsrc.File("distfiles")
+       distSubdir := ""
+       if G.Pkg != nil {
+               distSubdir = G.Pkg.vars.LastValue("DIST_SUBDIR")
+       }
+
+       // It's a rare situation that the explanation is generated
+       // this far from the corresponding diagnostic.
+       // This explanation only makes sense when there are some
+       // hashes missing that can be automatically added by pkglint.
+       line.Explain(
+               "To add the missing lines to the distinfo file, run",
+               sprintf("\t%s", bmake("distinfo")),
+               "for each variant of the package until all distfiles are downloaded to",
+               sprintf("%q.", cleanpath("${PKGSRCDIR}/distfiles/"+distSubdir)),
+               "",
+               "The variants are typically selected by setting EMUL_PLATFORM",
+               "or similar variables in the command line.",
+               "",
+               "After that, run",
+               sprintf("%q", "cvs update -C distinfo"),
+               "to revert the distinfo file to the previous state, since the above",
+               "commands have removed some of the entries.",
+               "",
+               "After downloading all possible distfiles, run",
+               sprintf("%q,", "pkglint --autofix"),
+               "which will find the downloaded distfiles and add the missing",
+               "hashes to the distinfo file.")
+
+       distfile := cleanpath(distdir + "/" + distSubdir + "/" + info.filename())
+       if !fileExists(distfile) {
+               return
+       }
+
+       computeHash := func(hasher hash.Hash) string {
+               f, err := os.Open(distfile)
+               G.AssertNil(err, "Opening distfile")
+
+               // Don't load the distfile into memory since some of them
+               // are hundreds of MB in size.
+               _, err = io.Copy(hasher, f)
+               G.AssertNil(err, "Computing hash of distfile")
+
+               hexHash := hex.EncodeToString(hasher.Sum(nil))
+
+               err = f.Close()
+               G.AssertNil(err, "Closing distfile")
+
+               return hexHash
+       }
+
+       compute := func(alg string) string {
+               switch alg {
+               case "SHA1":
+                       return computeHash(sha1.New())
+               case "RMD160":
+                       return computeHash(ripemd160.New())
+               case "SHA512":
+                       return computeHash(sha512.New())
+               default:
+                       fileInfo, err := os.Lstat(distfile)
+                       G.AssertNil(err, "Inaccessible distfile info")
+                       return sprintf("%d bytes", fileInfo.Size())
+               }
+       }
+
+       for alg, hash := range seen {
+               computed := compute(alg)
+
+               if computed != hash.hash {
+                       // 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))
+                       return
+               }
+       }
+
+       // At this point, all the existing hash algorithms are correct,
+       // and there is at least one hash algorithm. This is evidence enough
+       // that the distfile is the expected one. Now generate the missing hashes
+       // and insert them, in the correct order.
+
+       var insertion Line
+       var remainingHashes = info.hashes
+       for _, alg := range algorithms {
+               if missing[alg] {
+                       computed := compute(alg)
+
+                       if insertion == nil {
+                               fix := line.Autofix()
+                               fix.Errorf("Missing %s hash for %s.", alg, info.filename())
+                               fix.InsertBefore(sprintf("%s (%s) = %s", alg, info.filename(), computed))
+                               fix.Apply()
+                       } else {
+                               fix := insertion.Autofix()
+                               fix.Errorf("Missing %s hash for %s.", alg, info.filename())
+                               fix.InsertAfter(sprintf("%s (%s) = %s", alg, info.filename(), computed))
+                               fix.Apply()
+                       }
+
+               } else if remainingHashes[0].algorithm == alg {
+                       insertion = remainingHashes[0].line
+                       remainingHashes = remainingHashes[1:]
+               }
        }
 }
 
@@ -143,16 +314,26 @@ func (ck *distinfoLinesChecker) checkUnr
 
        for _, file := range patchFiles {
                patchName := file.Name()
-               if file.Mode().IsRegular() && !ck.patches[patchName] && hasPrefix(patchName, "patch-") {
-                       ck.distinfoLines.Errorf("Patch %q is not recorded. Run %q.",
-                               cleanpath(relpath(path.Dir(ck.distinfoLines.FileName), G.Pkg.File(ck.patchdir+"/"+patchName))),
+               if file.Mode().IsRegular() && ck.infos[patchName].isPatch != yes && hasPrefix(patchName, "patch-") {
+                       line := NewLineWhole(ck.lines.FileName)
+                       line.Errorf("Patch %q is not recorded. Run %q.",
+                               line.PathToFile(G.Pkg.File(ck.patchdir+"/"+patchName)),
                                bmake("makepatchsum"))
                }
        }
 }
 
 // Inter-package check for differing distfile checksums.
-func (ck *distinfoLinesChecker) checkGlobalDistfileMismatch(line Line, filename, alg, hash string) {
+func (ck *distinfoLinesChecker) checkGlobalDistfileMismatch(info distinfoHash) {
+       hashes := G.Hashes
+       if hashes == nil {
+               return
+       }
+
+       filename := info.filename
+       alg := info.algorithm
+       hash := info.hash
+       line := info.line
 
        // Intentionally checking the filename instead of ck.isPatch.
        // Missing the few distfiles that actually start with patch-*
@@ -161,11 +342,6 @@ func (ck *distinfoLinesChecker) checkGlo
                return
        }
 
-       hashes := G.Hashes
-       if hashes == nil {
-               return
-       }
-
        // The Size hash is not encoded in hex, therefore it would trigger wrong error messages below.
        // Since the Size hash is targeted towards humans and not really useful for detecting duplicates,
        // omitting the check here is ok. Any mismatches will be reliably detected because the other
@@ -194,17 +370,19 @@ func (ck *distinfoLinesChecker) checkGlo
        }
 }
 
-func (ck *distinfoLinesChecker) checkUncommittedPatch(line Line, patchName, alg, hash string) {
-       if ck.isPatch == yes {
-               patchFileName := ck.patchdir + "/" + patchName
-               resolvedPatchFileName := G.Pkg.File(patchFileName)
-               if ck.distinfoIsCommitted && !isCommitted(resolvedPatchFileName) {
-                       line.Warnf("%s is registered in distinfo but not added to CVS.", line.PathToFile(resolvedPatchFileName))
-               }
-               if alg == "SHA1" {
-                       ck.checkPatchSha1(line, patchFileName, hash)
-               }
-               ck.patches[patchName] = true
+func (ck *distinfoLinesChecker) checkUncommittedPatch(info distinfoHash) {
+       patchName := info.filename
+       alg := info.algorithm
+       hash := info.hash
+       line := info.line
+
+       patchFileName := ck.patchdir + "/" + patchName
+       resolvedPatchFileName := G.Pkg.File(patchFileName)
+       if ck.distinfoIsCommitted && !isCommitted(resolvedPatchFileName) {
+               line.Warnf("%s is registered in distinfo but not added to CVS.", line.PathToFile(resolvedPatchFileName))
+       }
+       if alg == "SHA1" {
+               ck.checkPatchSha1(line, patchFileName, hash)
        }
 }
 
@@ -226,6 +404,32 @@ func (ck *distinfoLinesChecker) checkPat
        }
 }
 
+type distinfoFileInfo struct {
+       //  yes     = the patch file exists
+       //  unknown = distinfo file is checked without a pkgsrc package
+       //  no      = distfile or nonexistent patch file
+       isPatch YesNoUnknown
+       hashes  []distinfoHash
+}
+
+func (info *distinfoFileInfo) filename() string { return info.hashes[0].filename }
+func (info *distinfoFileInfo) line() Line       { return info.hashes[0].line }
+
+func (info *distinfoFileInfo) algorithms() string {
+       var algs []string
+       for _, hash := range info.hashes {
+               algs = append(algs, hash.algorithm)
+       }
+       return strings.Join(algs, ", ")
+}
+
+type distinfoHash struct {
+       line      Line
+       filename  string
+       algorithm string
+       hash      string
+}
+
 // Same as in mk/checksum/distinfo.awk:/function patchsum/
 func computePatchSha1Hex(patchFilename string) (string, error) {
        patchBytes, err := ioutil.ReadFile(patchFilename)

Index: pkgsrc/pkgtools/pkglint/files/distinfo_test.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.25 pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.26
--- pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.25 Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/distinfo_test.go      Sun Mar 10 19:01:50 2019
@@ -2,7 +2,7 @@ package pkglint
 
 import "gopkg.in/check.v1"
 
-func (s *Suite) Test_CheckLinesDistinfo(c *check.C) {
+func (s *Suite) Test_CheckLinesDistinfo__parse_errors(c *check.C) {
        t := s.Init(c)
 
        t.Chdir("category/package")
@@ -27,14 +27,16 @@ func (s *Suite) Test_CheckLinesDistinfo(
 
        t.CheckOutputLines(
                "ERROR: distinfo:1: Expected \"$"+"NetBSD$\".",
-               "NOTE: distinfo:2: Empty line expected.",
-               "ERROR: distinfo:5: Expected SHA1, RMD160, SHA512, Size checksums for \"distfile.tar.gz\", got MD5, SHA1.",
-               "ERROR: distinfo:7: Expected SHA1 hash for patch-aa, got SHA1, Size.",
+               "NOTE: distinfo:1: Empty line expected before this line.",
+               "ERROR: distinfo:1: Invalid line: should be the RCS ID",
+               "ERROR: distinfo:2: Invalid line: should be empty",
                "ERROR: distinfo:8: Invalid line: Another invalid line",
+               "ERROR: distinfo:3: Expected SHA1, RMD160, SHA512, Size checksums for \"distfile.tar.gz\", got MD5, SHA1.",
+               "ERROR: distinfo:5: Expected SHA1 hash for patch-aa, got SHA1, Size.",
                "WARN: distinfo:9: Patch file \"patch-nonexistent\" does not exist in directory \"patches\".")
 }
 
-func (s *Suite) Test_CheckLinesDistinfo__nonexistent_distfile_called_patch(c *check.C) {
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__nonexistent_distfile_called_patch(c *check.C) {
        t := s.Init(c)
 
        t.Chdir("category/package")
@@ -51,11 +53,11 @@ func (s *Suite) Test_CheckLinesDistinfo_
        // a patch, it is a normal distfile because it has other hash algorithms
        // than exactly SHA1.
        t.CheckOutputLines(
-               "ERROR: distinfo:EOF: Expected SHA1, RMD160, SHA512, Size checksums " +
+               "ERROR: distinfo:3: Expected SHA1, RMD160, SHA512, Size checksums " +
                        "for \"patch-5.3.tar.gz\", got MD5, SHA1.")
 }
 
-func (s *Suite) Test_CheckLinesDistinfo__wrong_distfile_algorithms(c *check.C) {
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__wrong_distfile_algorithms(c *check.C) {
        t := s.Init(c)
 
        t.Chdir("category/package")
@@ -68,11 +70,39 @@ func (s *Suite) Test_CheckLinesDistinfo_
        CheckLinesDistinfo(lines)
 
        t.CheckOutputLines(
-               "ERROR: distinfo:EOF: Expected SHA1, RMD160, SHA512, Size checksums " +
+               "ERROR: distinfo:3: Expected SHA1, RMD160, SHA512, Size checksums " +
                        "for \"distfile.tar.gz\", got MD5, SHA1.")
 }
 
-func (s *Suite) Test_CheckLinesDistinfo__wrong_patch_algorithms(c *check.C) {
+// This case only happens when a distinfo file is checked on its own,
+// without any reference to a pkgsrc package. Additionally the distfile
+// must start with the patch- prefix and the algorithms must be wrong
+// for both distfile or patch.
+//
+// This test only demonstrates the edge case.
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__ambiguous_distfile(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("--explain")
+       t.Chdir("category/package")
+       lines := t.SetUpFileLines("distinfo",
+               RcsID,
+               "",
+               "MD5 (patch-4.2.tar.gz) = 12345678901234567890123456789012")
+
+       CheckLinesDistinfo(lines)
+
+       t.CheckOutputLines(
+               "ERROR: distinfo:3: Wrong checksum algorithms MD5 for patch-4.2.tar.gz.",
+               "",
+               "\tDistfiles that are downloaded from external sources must have the",
+               "\tchecksum algorithms SHA1, RMD160, SHA512, Size.",
+               "",
+               "\tPatch files from pkgsrc must have only the SHA1 hash.",
+               "")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__wrong_patch_algorithms(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
@@ -87,10 +117,23 @@ func (s *Suite) Test_CheckLinesDistinfo_
        G.Check(".")
 
        t.CheckOutputLines(
+               "ERROR: distinfo:3: Expected SHA1 hash for patch-aa, got MD5, SHA1.",
                "ERROR: distinfo:4: SHA1 hash of patches/patch-aa differs "+
                        "(distinfo has 1234567890123456789012345678901234567890, "+
-                       "patch file has ebbf34b0641bcb508f17d5a27f2bf2a536d810ac).",
-               "ERROR: distinfo:EOF: Expected SHA1 hash for patch-aa, got MD5, SHA1.")
+                       "patch file has ebbf34b0641bcb508f17d5a27f2bf2a536d810ac).")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_parse__empty(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.SetUpFileLines("distinfo",
+               RcsID,
+               "")
+
+       CheckLinesDistinfo(lines)
+
+       t.CheckOutputLines(
+               "NOTE: ~/distinfo:2: Trailing empty lines.")
 }
 
 // When checking the complete pkgsrc tree, pkglint has all information it needs
@@ -109,7 +152,8 @@ func (s *Suite) Test_distinfoLinesChecke
                RcsID,
                "",
                "SHA512 (distfile-1.0.tar.gz) = 1234567811111111",
-               "SHA512 (distfile-1.1.tar.gz) = 1111111111111111")
+               "SHA512 (distfile-1.1.tar.gz) = 1111111111111111",
+               "SHA512 (patch-4.2.tar.gz) = 1234567812345678")
        t.CreateFileLines("category/package2/distinfo",
                RcsID,
                "",
@@ -135,29 +179,104 @@ func (s *Suite) Test_distinfoLinesChecke
        G.Main("pkglint", "-r", "-Wall", "-Call", t.File("."))
 
        t.CheckOutputLines(
-               "ERROR: ~/category/package1/distinfo:4: "+
+               "ERROR: ~/category/package1/distinfo:3: "+
                        "Expected SHA1, RMD160, SHA512, Size checksums for \"distfile-1.0.tar.gz\", got SHA512.",
-               "ERROR: ~/category/package1/distinfo:EOF: "+
+               "ERROR: ~/category/package1/distinfo:4: "+
                        "Expected SHA1, RMD160, SHA512, Size checksums for \"distfile-1.1.tar.gz\", got SHA512.",
-               "ERROR: ~/category/package2/distinfo:3: The SHA512 hash for distfile-1.0.tar.gz is 1234567822222222, "+
-                       "which conflicts with 1234567811111111 in ../package1/distinfo:3.",
-               "ERROR: ~/category/package2/distinfo:4: "+
+               "ERROR: ~/category/package1/distinfo:5: "+
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"patch-4.2.tar.gz\", got SHA512.",
+
+               "ERROR: ~/category/package2/distinfo:3: "+
                        "Expected SHA1, RMD160, SHA512, Size checksums for \"distfile-1.0.tar.gz\", got SHA512.",
-               "ERROR: ~/category/package2/distinfo:5: "+
+               "ERROR: ~/category/package2/distinfo:3: "+
+                       "The SHA512 hash for distfile-1.0.tar.gz is 1234567822222222, "+
+                       "which conflicts with 1234567811111111 in ../../category/package1/distinfo:3.",
+               "ERROR: ~/category/package2/distinfo:4: "+
                        "Expected SHA1, RMD160, SHA512, Size checksums for \"distfile-1.1.tar.gz\", got SHA512.",
                "ERROR: ~/category/package2/distinfo:5: "+
-                       "The SHA512 hash for encoding-error.tar.gz contains a non-hex character.",
-               "ERROR: ~/category/package2/distinfo:EOF: "+
                        "Expected SHA1, RMD160, SHA512, Size checksums for \"encoding-error.tar.gz\", got SHA512.",
+               "ERROR: ~/category/package2/distinfo:5: "+
+                       "The SHA512 hash for encoding-error.tar.gz contains a non-hex character.",
+
                "WARN: ~/licenses/gnu-gpl-v2: This license seems to be unused.",
-               "7 errors and 1 warning found.")
+               "8 errors and 1 warning found.",
+               "(Run \"pkglint -e\" to show explanations.)")
 
        // Ensure that hex.DecodeString does not waste memory here.
        t.Check(len(G.Hashes["SHA512:distfile-1.0.tar.gz"].hash), equals, 8)
        t.Check(cap(G.Hashes["SHA512:distfile-1.0.tar.gz"].hash), equals, 8)
 }
 
-func (s *Suite) Test_CheckLinesDistinfo__uncommitted_patch(c *check.C) {
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__missing_patch_with_distfile_checksums(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.SetUpFileLines("distinfo",
+               RcsID,
+               "",
+               "SHA1 (patch-aa) = ...",
+               "RMD160 (patch-aa) = ...",
+               "SHA512 (patch-aa) = ...",
+               "Size (patch-aa) = ... bytes")
+
+       CheckLinesDistinfo(lines)
+
+       // The file name certainly looks like a pkgsrc patch, but there
+       // is no corresponding file in the file system, and there is no
+       // current package to correctly determine the PATCHDIR. Therefore
+       // pkglint doesn't know whether this is a distfile or a missing
+       // patch file and doesn't warn at all.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__existing_patch_with_distfile_checksums(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "SHA1 (patch-aa) = ...",
+               "RMD160 (patch-aa) = ...",
+               "SHA512 (patch-aa) = ...",
+               "Size (patch-aa) = ... bytes")
+       t.CreateFileDummyPatch("category/package/patches/patch-aa")
+
+       G.Check(t.File("category/package"))
+
+       // Even though the checksums in the distinfo file look as if they
+       // refer to a distfile, there is a patch file in the file system
+       // that matches the distinfo lines. When checking a pkgsrc package
+       // (as opposed to checking a distinfo file on its own), this means
+       // that the distinfo lines clearly refer to that patch file and not
+       // to a distfile.
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: "+
+                       "Expected SHA1 hash for patch-aa, got SHA1, RMD160, SHA512, Size.",
+               "ERROR: ~/category/package/distinfo:3: "+
+                       "SHA1 hash of patches/patch-aa differs (distinfo has ..., "+
+                       "patch file has ebbf34b0641bcb508f17d5a27f2bf2a536d810ac).")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__missing_patch_with_wrong_algorithms(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.SetUpFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "RMD160 (patch-aa) = ...")
+
+       G.Check(t.File("category/package"))
+
+       // Patch files usually have the SHA1 hash or none at all if they are fresh.
+       // In all other cases pkglint assumes that the file is a distfile,
+       // therefore it requires the usual distfile checksum algorithms here.
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: " +
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"patch-aa\", got RMD160.")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkUncommittedPatch__bad(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
@@ -176,7 +295,27 @@ func (s *Suite) Test_CheckLinesDistinfo_
                "WARN: distinfo:3: patches/patch-aa is registered in distinfo but not added to CVS.")
 }
 
-func (s *Suite) Test_CheckLinesDistinfo__unrecorded_patches(c *check.C) {
+func (s *Suite) Test_distinfoLinesChecker_checkUncommittedPatch__good(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.CreateFileDummyPatch("patches/patch-aa")
+       t.CreateFileLines("CVS/Entries",
+               "/distinfo/...")
+       t.CreateFileLines("patches/CVS/Entries",
+               "/patch-aa/...")
+       t.SetUpFileLines("distinfo",
+               RcsID,
+               "",
+               "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac")
+
+       G.checkdirPackage(".")
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkUnrecordedPatches(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package")
@@ -202,7 +341,7 @@ func (s *Suite) Test_CheckLinesDistinfo_
 // The distinfo file and the patches are usually placed in the package
 // directory. By defining PATCHDIR or DISTINFO_FILE, a package can define
 // that they are somewhere else in pkgsrc.
-func (s *Suite) Test_CheckLinesDistinfo__relative_path_in_distinfo(c *check.C) {
+func (s *Suite) Test_distinfoLinesChecker_checkPatchSha1__relative_path_in_distinfo(c *check.C) {
        t := s.Init(c)
 
        t.SetUpPackage("category/package",
@@ -340,35 +479,251 @@ func (s *Suite) Test_CheckLinesDistinfo_
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_distinfoLinesChecker_checkUncommittedPatch(c *check.C) {
+func (s *Suite) Test_distinfoLinesChecker_checkPatchSha1(c *check.C) {
+       t := s.Init(c)
+
+       G.Pkg = NewPackage(t.File("category/package"))
+       distinfoLine := t.NewLine(t.File("category/package/distinfo"), 5, "")
+
+       checker := distinfoLinesChecker{}
+       checker.checkPatchSha1(distinfoLine, "patch-nonexistent", "distinfo-sha1")
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:5: Patch patch-nonexistent does not exist.")
+}
+
+// When there is at least one correct hash for a distfile, running
+// pkglint --autofix adds the missing hashes, provided the distfile has been
+// downloaded to pkgsrc/distfiles, which is the standard distfiles location.
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__add_missing_hashes(c *check.C) {
        t := s.Init(c)
 
+       t.SetUpCommandLine("-Wall", "--explain")
        t.SetUpPackage("category/package")
-       t.Chdir("category/package")
-       t.CreateFileDummyPatch("patches/patch-aa")
-       t.CreateFileLines("CVS/Entries",
-               "/distinfo/...")
-       t.CreateFileLines("patches/CVS/Entries",
-               "/patch-aa/...")
-       t.SetUpFileLines("distinfo",
+       t.CreateFileLines("category/package/distinfo",
                RcsID,
                "",
-               "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac")
+               "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a",
+               "Size (package-1.0.txt) = 13 bytes",
+               "CRC32 (package-1.0.txt) = asdf")
+       t.CreateFileLines("distfiles/package-1.0.txt",
+               "hello, world")
+       G.Pkgsrc.LoadInfrastructure()
 
-       G.checkdirPackage(".")
+       // This run is only used to verify that the RMD160 hash is correct, and if
+       // it should ever differ, the correct hash will appear in an error message.
+       G.Check(t.File("category/package"))
 
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: "+
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"package-1.0.txt\", "+
+                       "got RMD160, Size, CRC32.",
+               "",
+               "\tTo add the missing lines to the distinfo file, run",
+               "\t\t"+confMake+" distinfo",
+               "\tfor each variant of the package until all distfiles are downloaded",
+               "\tto \"${PKGSRCDIR}/distfiles\".",
+               "",
+               "\tThe variants are typically selected by setting EMUL_PLATFORM or",
+               "\tsimilar variables in the command line.",
+               "",
+               "\tAfter that, run \"cvs update -C distinfo\" to revert the distinfo file",
+               "\tto the previous state, since the above commands have removed some of",
+               "\tthe entries.",
+               "",
+               "\tAfter downloading all possible distfiles, run \"pkglint --autofix\",",
+               "\twhich will find the downloaded distfiles and add the missing hashes",
+               "\tto the distinfo file.",
+               "",
+               "ERROR: ~/category/package/distinfo:3: Missing SHA1 hash for package-1.0.txt.",
+               "ERROR: ~/category/package/distinfo:3: Missing SHA512 hash for package-1.0.txt.")
+
+       t.SetUpCommandLine("-Wall", "--autofix", "--show-autofix", "--source")
+
+       G.Check(t.File("category/package"))
+
+       // Since the file exists in the distfiles directory, pkglint checks the
+       // hash right away. It also adds the missing hashes since this file is
+       // not a patch file.
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: Missing SHA1 hash for package-1.0.txt.",
+               "AUTOFIX: ~/category/package/distinfo:3: "+
+                       "Inserting a line \"SHA1 (package-1.0.txt) "+
+                       "= cd50d19784897085a8d0e3e413f8612b097c03f1\" "+
+                       "before this line.",
+               "+\tSHA1 (package-1.0.txt) = cd50d19784897085a8d0e3e413f8612b097c03f1",
+               ">\tRMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a",
+               "",
+               "ERROR: ~/category/package/distinfo:3: Missing SHA512 hash for package-1.0.txt.",
+               "AUTOFIX: ~/category/package/distinfo:3: "+
+                       "Inserting a line \"SHA512 (package-1.0.txt) "+
+                       "= f65f341b35981fda842b09b2c8af9bcdb7602a4c2e6fa1f7d41f0974d3e3122f"+
+                       "268fc79d5a4af66358f5133885cd1c165c916f80ab25e5d8d95db46f803c782c\" after this line.",
+               "+\tSHA1 (package-1.0.txt) = cd50d19784897085a8d0e3e413f8612b097c03f1",
+               ">\tRMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a",
+               "+\tSHA512 (package-1.0.txt) = f65f341b35981fda842b09b2c8af9bcdb7602a4c2e6fa1f7d41f0974d3e3122f"+
+                       "268fc79d5a4af66358f5133885cd1c165c916f80ab25e5d8d95db46f803c782c")
+
+       t.SetUpCommandLine("-Wall")
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: " +
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"package-1.0.txt\", " +
+                       "got SHA1, RMD160, SHA512, Size, CRC32.")
 }
 
-func (s *Suite) Test_distinfoLinesChecker_checkPatchSha1(c *check.C) {
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__wrong_distfile_hash(c *check.C) {
        t := s.Init(c)
 
-       G.Pkg = NewPackage(t.File("category/package"))
-       distinfoLine := t.NewLine(t.File("category/package/distinfo"), 5, "")
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "RMD160 (package-1.0.txt) = 1234wrongHash1234")
+       t.CreateFileLines("distfiles/package-1.0.txt",
+               "hello, world")
+       G.Pkgsrc.LoadInfrastructure()
 
-       checker := distinfoLinesChecker{}
-       checker.checkPatchSha1(distinfoLine, "patch-nonexistent", "distinfo-sha1")
+       G.Check(t.File("category/package"))
 
        t.CheckOutputLines(
-               "ERROR: ~/category/package/distinfo:5: Patch patch-nonexistent does not exist.")
+               "ERROR: ~/category/package/distinfo:3: "+
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"package-1.0.txt\", "+
+                       "got RMD160.",
+               "ERROR: ~/category/package/distinfo:3: "+
+                       "The RMD160 checksum for \"package-1.0.txt\" is 1234wrongHash1234 in distinfo, "+
+                       "1a88147a0344137404c63f3b695366eab869a98a in ../../distfiles/package-1.0.txt.")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__no_usual_algorithm(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "MD5 (package-1.0.txt) = 1234wrongHash1234")
+       t.CreateFileLines("distfiles/package-1.0.txt",
+               "hello, world")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: " +
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"package-1.0.txt\", " +
+                       "got MD5.")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__top_algorithms_missing(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "SHA512 (package-1.0.txt) = f65f341b35981fda842b09b2c8af9bcdb7602a4c2e6fa1f7"+
+                       "d41f0974d3e3122f268fc79d5a4af66358f5133885cd1c165c916f80ab25e5d8d95db46f803c782c",
+               "Size (package-1.0.txt) = 13 bytes")
+       t.CreateFileLines("distfiles/package-1.0.txt",
+               "hello, world")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: "+
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"package-1.0.txt\", "+
+                       "got SHA512, Size.",
+               "ERROR: ~/category/package/distinfo:3: Missing SHA1 hash for package-1.0.txt.",
+               "ERROR: ~/category/package/distinfo:3: Missing RMD160 hash for package-1.0.txt.")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__bottom_algorithms_missing(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "SHA1 (package-1.0.txt) = cd50d19784897085a8d0e3e413f8612b097c03f1",
+               "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a")
+       t.CreateFileLines("distfiles/package-1.0.txt",
+               "hello, world")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: "+
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"package-1.0.txt\", "+
+                       "got SHA1, RMD160.",
+               "ERROR: ~/category/package/distinfo:4: Missing SHA512 hash for package-1.0.txt.",
+               "ERROR: ~/category/package/distinfo:4: Missing Size hash for package-1.0.txt.")
+
+       t.SetUpCommandLine("-Wall", "--autofix")
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "AUTOFIX: ~/category/package/distinfo:4: "+
+                       "Inserting a line \"SHA512 (package-1.0.txt) = f65f341b35981fda842b"+
+                       "09b2c8af9bcdb7602a4c2e6fa1f7d41f0974d3e3122f268fc79d5a4af66358f513"+
+                       "3885cd1c165c916f80ab25e5d8d95db46f803c782c\" after this line.",
+               "AUTOFIX: ~/category/package/distinfo:4: "+
+                       "Inserting a line \"Size (package-1.0.txt) = 13 bytes\" after this line.")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__algorithms_in_wrong_order(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a",
+               "SHA1 (package-1.0.txt) = cd50d19784897085a8d0e3e413f8612b097c03f1",
+               "Size (package-1.0.txt) = 13 bytes",
+               "SHA512 (package-1.0.txt) = f65f341b35981fda842b09b2c8af9bcdb7602a4c2e6fa1f7"+
+                       "d41f0974d3e3122f268fc79d5a4af66358f5133885cd1c165c916f80ab25e5d8d95db46f803c782c")
+
+       t.CreateFileLines("distfiles/package-1.0.txt",
+               "hello, world")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       // This case doesn't happen in practice, therefore there's no autofix for it.
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: " +
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"package-1.0.txt\", " +
+                       "got RMD160, SHA1, Size, SHA512.")
+}
+
+func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__some_algorithms_in_wrong_order(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package")
+       t.CreateFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a",
+               "Size (package-1.0.txt) = 13 bytes",
+               "SHA512 (package-1.0.txt) = f65f341b35981fda842b09b2c8af9bcdb7602a4c2e6fa1f7"+
+                       "d41f0974d3e3122f268fc79d5a4af66358f5133885cd1c165c916f80ab25e5d8d95db46f803c782c")
+
+       t.CreateFileLines("distfiles/package-1.0.txt",
+               "hello, world")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       // This case doesn't happen in practice, therefore there's no autofix for it.
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/distinfo:3: "+
+                       "Expected SHA1, RMD160, SHA512, Size checksums for \"package-1.0.txt\", "+
+                       "got RMD160, Size, SHA512.",
+               "ERROR: ~/category/package/distinfo:3: Missing SHA1 hash for package-1.0.txt.")
 }

Index: pkgsrc/pkgtools/pkglint/files/line.go
diff -u pkgsrc/pkgtools/pkglint/files/line.go:1.33 pkgsrc/pkgtools/pkglint/files/line.go:1.34
--- pkgsrc/pkgtools/pkglint/files/line.go:1.33  Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/line.go       Sun Mar 10 19:01:50 2019
@@ -105,15 +105,12 @@ func NewLineWhole(filename string) Line 
 // RefTo returns a reference to another line,
 // which can be in the same file or in a different file.
 func (line *LineImpl) RefTo(other Line) string {
-       if line.Filename != other.Filename {
-               return cleanpath(relpath(path.Dir(line.Filename), other.Filename)) + ":" + other.Linenos()
-       }
-       return "line " + other.Linenos()
+       return line.RefToLocation(other.Location)
 }
 
 func (line *LineImpl) RefToLocation(other Location) string {
        if line.Filename != other.Filename {
-               return cleanpath(relpath(path.Dir(line.Filename), other.Filename)) + ":" + other.Linenos()
+               return line.PathToFile(other.Filename) + ":" + other.Linenos()
        }
        return "line " + other.Linenos()
 }
Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.33 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.34
--- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.33  Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go       Sun Mar 10 19:01:50 2019
@@ -419,7 +419,7 @@ func (s *Suite) Test_Pkglint_Check(c *ch
 func (s *Suite) Test_resolveVariableRefs__circular_reference(c *check.C) {
        t := s.Init(c)
 
-       mkline := t.NewMkLine("filename", 1, "GCC_VERSION=${GCC_VERSION}")
+       mkline := t.NewMkLine("filename.mk", 1, "GCC_VERSION=${GCC_VERSION}")
        G.Pkg = NewPackage(t.File("category/pkgbase"))
        G.Pkg.vars.Define("GCC_VERSION", mkline)
 
@@ -433,9 +433,9 @@ func (s *Suite) Test_resolveVariableRefs
 func (s *Suite) Test_resolveVariableRefs__multilevel(c *check.C) {
        t := s.Init(c)
 
-       mkline1 := t.NewMkLine("filename", 10, "FIRST=\t${SECOND}")
-       mkline2 := t.NewMkLine("filename", 11, "SECOND=\t${THIRD}")
-       mkline3 := t.NewMkLine("filename", 12, "THIRD=\tgot it")
+       mkline1 := t.NewMkLine("filename.mk", 10, "FIRST=\t${SECOND}")
+       mkline2 := t.NewMkLine("filename.mk", 11, "SECOND=\t${THIRD}")
+       mkline3 := t.NewMkLine("filename.mk", 12, "THIRD=\tgot it")
        G.Pkg = NewPackage(t.File("category/pkgbase"))
        defineVar(mkline1, "FIRST")
        defineVar(mkline2, "SECOND")
@@ -455,7 +455,7 @@ func (s *Suite) Test_resolveVariableRefs
 func (s *Suite) Test_resolveVariableRefs__special_chars(c *check.C) {
        t := s.Init(c)
 
-       mkline := t.NewMkLine("filename", 10, "_=x11")
+       mkline := t.NewMkLine("filename.mk", 10, "_=x11")
        G.Pkg = NewPackage(t.File("category/pkg"))
        G.Pkg.vars.Define("GST_PLUGINS0.10_TYPE", mkline)
 
@@ -551,6 +551,15 @@ func (s *Suite) Test_CheckLinesMessage__
                "===========================================================================")
 }
 
+func (s *Suite) Test_CheckLinesMessage__common(c *check.C) {
+       t := s.Init(c)
+
+       // FIXME: If there is a MESSAGE.common, it is combined with MESSAGE.
+       //  See meta-pkgs/ruby-redmine-plugins for an example.
+
+       t.CheckOutputEmpty()
+}
+
 // Demonstrates that an ALTERNATIVES file can be tested individually,
 // without any dependencies on a whole package or a PLIST file.
 func (s *Suite) Test_Pkglint_checkReg__alternatives(c *check.C) {
Index: pkgsrc/pkgtools/pkglint/files/plist_test.go
diff -u pkgsrc/pkgtools/pkglint/files/plist_test.go:1.33 pkgsrc/pkgtools/pkglint/files/plist_test.go:1.34
--- pkgsrc/pkgtools/pkglint/files/plist_test.go:1.33    Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/plist_test.go Sun Mar 10 19:01:50 2019
@@ -566,6 +566,34 @@ func (s *Suite) Test_PlistChecker_checkP
                "WARN: ~/PLIST:6: Man pages should be installed into man/, not share/man/.")
 }
 
+func (s *Suite) Test_PlistChecker_checkPathShare__gnome_icon_theme(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileDummyBuildlink3("graphics/gnome-icon-theme/buildlink3.mk")
+       t.SetUpPackage("graphics/gnome-icon-theme-extras",
+               "ICON_THEMES=\tyes",
+               ".include \"../../graphics/gnome-icon-theme/buildlink3.mk\"")
+       t.CreateFileLines("graphics/gnome-icon-theme-extras/PLIST",
+               PlistRcsID,
+               "share/icons/gnome/16x16/devices/media-optical-cd-audio.png",
+               "share/icons/gnome/16x16/devices/media-optical-dvd.png")
+       G.Pkgsrc.LoadInfrastructure()
+       t.Chdir(".")
+
+       // This variant is typically run interactively.
+       G.Check("graphics/gnome-icon-theme-extras")
+
+       t.CheckOutputEmpty()
+
+       // Note the leading "./".
+       // This variant is typical for recursive runs of pkglint.
+       G.Check("./graphics/gnome-icon-theme-extras")
+
+       // Up to March 2019, a bug in relpath produced different behavior
+       // depending on the leading dot.
+       t.CheckOutputEmpty()
+}
+
 func (s *Suite) Test_PlistLine_CheckTrailingWhitespace(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/lines.go
diff -u pkgsrc/pkgtools/pkglint/files/lines.go:1.5 pkgsrc/pkgtools/pkglint/files/lines.go:1.6
--- pkgsrc/pkgtools/pkglint/files/lines.go:1.5  Sat Jan 26 16:31:33 2019
+++ pkgsrc/pkgtools/pkglint/files/lines.go      Sun Mar 10 19:01:50 2019
@@ -35,6 +35,7 @@ func (ls *LinesImpl) SaveAutofixChanges(
        return SaveAutofixChanges(ls)
 }
 
+// CheckRcsID returns true if the expected RCS Id was found.
 func (ls *LinesImpl) CheckRcsID(index int, prefixRe regex.Pattern, suggestedPrefix string) bool {
        if trace.Tracing {
                defer trace.Call(prefixRe, suggestedPrefix)()

Index: pkgsrc/pkgtools/pkglint/files/logging_test.go
diff -u pkgsrc/pkgtools/pkglint/files/logging_test.go:1.12 pkgsrc/pkgtools/pkglint/files/logging_test.go:1.13
--- pkgsrc/pkgtools/pkglint/files/logging_test.go:1.12  Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/logging_test.go       Sun Mar 10 19:01:50 2019
@@ -749,7 +749,7 @@ func (s *Suite) Test_Logger_Diag__source
 
        t.CheckOutputLines(
                "ERROR: ~/category/package1/distinfo: "+
-                       "Patch \"../dependency/patches/patch-aa\" is not recorded. "+
+                       "Patch \"../../category/dependency/patches/patch-aa\" is not recorded. "+
                        "Run \""+confMake+" makepatchsum\".",
                "",
                ">\t--- old file",
@@ -757,7 +757,7 @@ func (s *Suite) Test_Logger_Diag__source
                        "Each patch must be documented.",
                "",
                "ERROR: ~/category/package2/distinfo: "+
-                       "Patch \"../dependency/patches/patch-aa\" is not recorded. "+
+                       "Patch \"../../category/dependency/patches/patch-aa\" is not recorded. "+
                        "Run \""+confMake+" makepatchsum\".",
                "",
                "3 errors and 0 warnings found.",

Index: pkgsrc/pkgtools/pkglint/files/mkline.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.47 pkgsrc/pkgtools/pkglint/files/mkline.go:1.48
--- pkgsrc/pkgtools/pkglint/files/mkline.go:1.47        Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline.go     Sun Mar 10 19:01:50 2019
@@ -22,17 +22,19 @@ type MkLineImpl struct {
 }
 type mkLineAssign = *mkLineAssignImpl // See https://github.com/golang/go/issues/28045
 type mkLineAssignImpl struct {
-       commented   bool       // Whether the whole variable assignment is commented out
-       varname     string     // e.g. "HOMEPAGE", "SUBST_SED.perl"
-       varcanon    string     // e.g. "HOMEPAGE", "SUBST_SED.*"
-       varparam    string     // e.g. "", "perl"
-       op          MkOperator //
-       valueAlign  string     // The text up to and including the assignment operator, e.g. VARNAME+=\t
-       value       string     // The trimmed value
-       valueMk     []*MkToken // The value, sent through splitIntoMkWords
-       valueMkRest string     // nonempty in case of parse errors
-       fields      []string   // The value, space-separated according to shell quoting rules
-       comment     string
+       commented         bool   // Whether the whole variable assignment is commented out
+       varname           string // e.g. "HOMEPAGE", "SUBST_SED.perl"
+       varcanon          string // e.g. "HOMEPAGE", "SUBST_SED.*"
+       varparam          string // e.g. "", "perl"
+       spaceAfterVarname string
+       op                MkOperator //
+       valueAlign        string     // The text up to and including the assignment operator, e.g. VARNAME+=\t
+       value             string     // The trimmed value
+       valueMk           []*MkToken // The value, sent through splitIntoMkWords
+       valueMkRest       string     // nonempty in case of parse errors
+       fields            []string   // The value, space-separated according to shell quoting rules
+       spaceAfterValue   string
+       comment           string
 }
 type mkLineShell struct {
        command string
@@ -77,24 +79,26 @@ func NewMkLine(line Line) *MkLineImpl {
                        "Otherwise remove the leading whitespace.")
        }
 
-       if m, commented, varname, spaceAfterVarname, op, valueAlign, value, spaceAfterValue, comment := MatchVarassign(text); m {
-               if spaceAfterVarname != "" {
+       if m, a := MatchVarassign(text); m {
+               if a.spaceAfterVarname != "" {
+                       varname := a.varname
+                       op := a.op
                        switch {
-                       case hasSuffix(varname, "+") && op == "=":
+                       case hasSuffix(varname, "+") && (op == opAssign || op == opAssignAppend):
                                break
-                       case matches(varname, `^[a-z]`) && op == ":=":
+                       case matches(varname, `^[a-z]`) && op == opAssignEval:
                                break
                        default:
                                // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
                                fix := line.Autofix()
                                fix.Notef("Unnecessary space after variable name %q.", varname)
-                               fix.Replace(varname+spaceAfterVarname+op, varname+op)
+                               fix.Replace(varname+a.spaceAfterVarname+op.String(), varname+op.String())
                                fix.Apply()
                        }
                }
 
                // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
-               if comment != "" && value != "" && spaceAfterValue == "" {
+               if a.comment != "" && a.value != "" && a.spaceAfterValue == "" {
                        line.Warnf("The # character starts a Makefile comment.")
                        G.Explain(
                                "In a variable assignment, an unescaped # starts a comment that",
@@ -102,18 +106,7 @@ func NewMkLine(line Line) *MkLineImpl {
                                "To escape the #, write \\#.")
                }
 
-               return &MkLineImpl{line, &mkLineAssignImpl{
-                       commented,
-                       varname,
-                       varnameCanon(varname),
-                       varnameParam(varname),
-                       NewMkOperator(op),
-                       valueAlign,
-                       strings.Replace(value, "\\#", "#", -1),
-                       nil,
-                       "",
-                       nil,
-                       comment}}
+               return &MkLineImpl{line, a}
        }
 
        if hasPrefix(text, "\t") {
@@ -131,7 +124,13 @@ func NewMkLine(line Line) *MkLineImpl {
        }
 
        if m, indent, directive, args, comment := matchMkDirective(text); m {
-               return &MkLineImpl{line, &mkLineDirectiveImpl{indent, directive, args, comment, nil, nil, nil}}
+
+               // In .if and .endif lines the space surrounding the comment is irrelevant.
+               // Especially for checking that the .endif comment matches the .if condition,
+               // it must be trimmed.
+               trimmedComment := trimHspace(comment)
+
+               return &MkLineImpl{line, &mkLineDirectiveImpl{indent, directive, args, trimmedComment, nil, nil, nil}}
        }
 
        if m, indent, directive, includedFile := MatchMkInclude(text); m {
@@ -161,6 +160,7 @@ func NewMkLine(line Line) *MkLineImpl {
        return &MkLineImpl{line, nil}
 }
 
+// String returns the filename and line numbers.
 func (mkline *MkLineImpl) String() string {
        return sprintf("%s:%s", mkline.Filename, mkline.Linenos())
 }
@@ -423,6 +423,8 @@ func (mkline *MkLineImpl) ValueSplit(val
        return split
 }
 
+var notSpace = textproc.Space.Inverse()
+
 // ValueFields splits the given value, taking care of variable references.
 // Example:
 //
@@ -456,7 +458,7 @@ func (mkline *MkLineImpl) ValueFields(va
                                for lexer.NextBytesSet(textproc.Space) != "" {
                                        cont = false
                                }
-                               if word := lexer.NextBytesSet(textproc.Space.Inverse()); word != "" {
+                               if word := lexer.NextBytesSet(notSpace); word != "" {
                                        out(word)
                                        cont = true
                                }
@@ -529,6 +531,9 @@ func (mkline *MkLineImpl) WithoutMakeVar
 }
 
 func (mkline *MkLineImpl) ResolveVarsInRelativePath(relativePath string) string {
+       if !contains(relativePath, "$") {
+               return cleanpath(relativePath)
+       }
 
        var basedir string
        if G.Pkg != nil {
@@ -551,8 +556,22 @@ func (mkline *MkLineImpl) ResolveVarsInR
                }
                tmp = strings.Replace(tmp, "${PKGSRCDIR}", pkgsrcdir, -1)
        }
-       tmp = strings.Replace(tmp, "${.CURDIR}", ".", -1)   // TODO: Replace with the "typical" os.Getwd().
-       tmp = strings.Replace(tmp, "${.PARSEDIR}", ".", -1) // FIXME
+
+       // Strictly speaking, the .CURDIR should be replaced with the basedir.
+       // Depending on whether pkglint is executed with a relative or an absolute
+       // path, this would produce diagnostics that "this relative path must not
+       // be absolute". Since ${.CURDIR} is usually used in package Makefiles and
+       // followed by "../.." anyway, the exact directory doesn't matter.
+       tmp = strings.Replace(tmp, "${.CURDIR}", ".", -1)
+
+       // TODO: Add test for exists(${.PARSEDIR}/file).
+       // TODO: Add test for evaluating ${.PARSEDIR} in an included package.
+       // TODO: Add test for including ${.PARSEDIR}/other.mk.
+       // TODO: Add test for evaluating ${.PARSEDIR} in the infrastructure.
+       //  This is the only practically relevant use case since the category
+       //  directories don't contain any *.mk files that could be included.
+       // TODO: Add test that suggests ${.PARSEDIR} in .include to be omitted.
+       tmp = strings.Replace(tmp, "${.PARSEDIR}", ".", -1)
 
        replaceLatest := func(varuse, category string, pattern regex.Pattern, replacement string) {
                if contains(tmp, varuse) {
@@ -603,16 +622,151 @@ func (mkline *MkLineImpl) RefTo(other Mk
 }
 
 var (
-       LowerDash = textproc.NewByteSet("a-z---")
-       AlnumDot  = textproc.NewByteSet("A-Za-z0-9_.")
+       LowerDash                  = textproc.NewByteSet("a-z---")
+       AlnumDot                   = textproc.NewByteSet("A-Za-z0-9_.")
+       unescapeMkCommentSafeChars = textproc.NewByteSet("\\#[$").Inverse()
 )
 
-func matchMkDirective(text string) (m bool, indent, directive, args, comment string) {
+// unescapeMkComment takes a Makefile line, as written in a file, and splits
+// it into the main part and the comment.
+//
+// The comment starts at the first #. Except if it is preceded by an odd number
+// of backslashes. Or by an opening bracket.
+//
+// The main text is returned including leading and trailing whitespace. Any
+// escaped # is returned in its unescaped form, that is, \# becomes #.
+//
+// The comment is returned including the leading "#", if any. If the line has
+// no comment, it is an empty string.
+func unescapeMkComment(text string) (main, comment string) {
+       var sb strings.Builder
+
        lexer := textproc.NewLexer(text)
-       if !lexer.SkipByte('.') {
+
+again:
+       if plain := lexer.NextBytesSet(unescapeMkCommentSafeChars); plain != "" {
+               sb.WriteString(plain)
+               goto again
+       }
+
+       switch {
+       case lexer.SkipByte('$'):
+               sb.WriteByte('$')
+
+       case lexer.SkipString("\\#"):
+               sb.WriteByte('#')
+
+       case lexer.PeekByte() == '\\' && len(lexer.Rest()) >= 2:
+               sb.WriteString(lexer.Rest()[:2])
+               lexer.Skip(2)
+
+       case lexer.SkipByte('\\'):
+               sb.WriteByte('\\')
+
+       case lexer.SkipString("[#"):
+               // See devel/bmake/files/parse.c:/as in modifier/
+               sb.WriteString("[#")
+
+       case lexer.SkipByte('['):
+               sb.WriteByte('[')
+
+       default:
+               main = sb.String()
+               if lexer.PeekByte() == '#' {
+                       return main, lexer.Rest()
+               }
+
+               G.Assertf(lexer.EOF(), "unescapeMkComment(%q): sb = %q, rest = %q", text, main, lexer.Rest())
+               return main, ""
+       }
+
+       goto again
+}
+
+// splitMkLine parses a logical line from a Makefile (that is, after joining
+// the lines that end in a backslash) into two parts: the main part and the
+// comment.
+//
+// This applies to all line types except those starting with a tab, which
+// contain the shell commands to be associated with make targets. These cannot
+// have comments.
+func splitMkLine(text string) (main string, tokens []*MkToken, rest string, spaceBeforeComment string, hasComment bool, comment string) {
+
+       main, comment = unescapeMkComment(text)
+
+       p := NewMkParser(nil, main, false)
+       lexer := p.lexer
+
+       rtrimHspace := func(s string) string {
+               end := len(s)
+               for end > 0 && isHspace(s[end-1]) {
+                       end--
+               }
+               return s[:end]
+       }
+
+       parseToken := func() string {
+               var sb strings.Builder
+
+               for !lexer.EOF() {
+                       if lexer.SkipString("$$") {
+                               sb.WriteString("$$")
+                               continue
+                       }
+
+                       other := lexer.NextBytesFunc(func(b byte) bool { return b != '$' })
+                       if other == "" {
+                               break
+                       }
+
+                       sb.WriteString(other)
+               }
+
+               return sb.String()
+       }
+
+       for !lexer.EOF() {
+               mark := lexer.Mark()
+
+               if varUse := p.VarUse(); varUse != nil {
+                       tokens = append(tokens, &MkToken{lexer.Since(mark), varUse})
+
+               } else if token := parseToken(); token != "" {
+                       tokens = append(tokens, &MkToken{token, nil})
+
+               } else {
+                       break
+               }
+       }
+
+       if comment != "" {
+               hasComment = true
+               comment = comment[1:]
+       }
+       rest = lexer.Rest()
+       main = main[:len(main)-len(rest)]
+
+       if rest == "" {
+               mainWithSpaces := main
+               main = rtrimHspace(main)
+               spaceBeforeComment = mainWithSpaces[len(main):]
+       }
+
+       return
+}
+
+func matchMkDirective(text string) (m bool, indent, directive, args, comment string) {
+       if !hasPrefix(text, ".") {
+               return
+       }
+
+       main, _, rest, _, hasComment, trailingComment := splitMkLine(text)
+       if rest != "" {
                return
        }
 
+       lexer := textproc.NewLexer(main[1:])
+
        indent = lexer.NextHspace()
        directive = lexer.NextBytesSet(LowerDash)
        switch directive {
@@ -629,27 +783,10 @@ func matchMkDirective(text string) (m bo
 
        lexer.SkipHspace()
 
-       argsStart := lexer.Mark()
-       for !lexer.EOF() && lexer.PeekByte() != '#' {
-               switch {
-               case lexer.SkipString("[#"):
-                       // See devel/bmake/files/parse.c:/as in modifier/
-
-               case lexer.PeekByte() == '\\' && len(lexer.Rest()) > 1:
-                       lexer.Skip(2)
+       args = lexer.Rest()
 
-               default:
-                       lexer.Skip(1)
-               }
-       }
-       args = lexer.Since(argsStart)
-       args = strings.TrimFunc(args, func(r rune) bool { return isHspace(byte(r)) })
-       args = strings.Replace(args, "\\#", "#", -1)
-
-       if !lexer.EOF() {
-               lexer.Skip(1)
-               lexer.SkipHspace()
-               comment = lexer.Rest()
+       if hasComment {
+               comment = trailingComment
        }
 
        m = true
@@ -1204,48 +1341,55 @@ func (ind *Indentation) CheckFinish(file
        }
 }
 
-// VarnameBytes contains characters that may be used in variable names.
-// The bracket is included only for the tool of the same name, e.g. "TOOLS_PATH.[".
+// VarbaseBytes contains characters that may be used in the main part of variable names.
+// VarparamBytes contains characters that may be used in the parameter part of variable names.
+//
+// For example, TOOLS_PATH.[ is a valid variable name but [ alone isn't since
+// the opening bracket is only allowed in the parameter part of variable names.
 //
 // This approach differs from the one in devel/bmake/files/parse.c:/^Parse_IsVar,
 // but in practice it works equally well. Luckily there aren't many situations
 // where a complicated variable name contains unbalanced parentheses or braces,
 // which would confuse the devel/bmake parser.
-var VarnameBytes = textproc.NewByteSet("A-Za-z_0-9*+---.[")
+//
+// TODO: The allowed characters differ between the basename and the parameter
+//  of the variable. The square bracket is only allowed in the parameter part.
+var (
+       VarbaseBytes  = textproc.NewByteSet("A-Za-z_0-9+---")
+       VarparamBytes = textproc.NewByteSet("A-Za-z_0-9#*+---.[")
+)
 
-func MatchVarassign(text string) (m, commented bool, varname, spaceAfterVarname, op, valueAlign, value, spaceAfterValue, comment string) {
-       lexer := textproc.NewLexer(text)
+func MatchVarassign(text string) (m bool, assignment mkLineAssign) {
+       commented := hasPrefix(text, "#")
+       withoutLeadingComment := text
+       if commented {
+               withoutLeadingComment = withoutLeadingComment[1:]
+       }
+
+       main, tokens, rest, spaceBeforeComment, hasComment, comment := splitMkLine(withoutLeadingComment)
+
+       lexer := NewMkTokensLexer(tokens)
+       mainStart := lexer.Mark()
 
-       commented = lexer.SkipByte('#')
        for !commented && lexer.SkipByte(' ') {
        }
 
        varnameStart := lexer.Mark()
-       for !lexer.EOF() {
-               switch {
-
-               case lexer.NextBytesSet(VarnameBytes) != "":
-                       continue
-
-               case lexer.PeekByte() == '$':
-                       parser := NewMkParser(nil, lexer.Rest(), false)
-                       varuse := parser.VarUse()
-                       if varuse == nil {
-                               return
-                       }
-                       varuseLen := len(lexer.Rest()) - len(parser.Rest())
-                       lexer.Skip(varuseLen)
-                       continue
+       // TODO: duplicated code in MkParser.Varname
+       for lexer.NextBytesSet(VarbaseBytes) != "" || lexer.NextVarUse() != nil {
+       }
+       if lexer.SkipByte('.') || hasPrefix(main, "SITES_") {
+               for lexer.NextBytesSet(VarparamBytes) != "" || lexer.NextVarUse() != nil {
                }
-               break
        }
-       varname = lexer.Since(varnameStart)
+
+       varname := lexer.Since(varnameStart)
 
        if varname == "" {
                return
        }
 
-       spaceAfterVarname = lexer.NextHspace()
+       spaceAfterVarname := lexer.NextHspace()
 
        opStart := lexer.Mark()
        switch lexer.PeekByte() {
@@ -1255,37 +1399,36 @@ func MatchVarassign(text string) (m, com
        if !lexer.SkipByte('=') {
                return
        }
-       op = lexer.Since(opStart)
+       op := NewMkOperator(lexer.Since(opStart))
 
-       if hasSuffix(varname, "+") && op == "=" && spaceAfterVarname == "" {
+       if hasSuffix(varname, "+") && op == opAssign && spaceAfterVarname == "" {
                varname = varname[:len(varname)-1]
-               op = "+="
+               op = opAssignAppend
        }
 
        lexer.SkipHspace()
 
-       valueAlign = text[:len(text)-len(lexer.Rest())]
-       valueStart := lexer.Mark()
-       // FIXME: This is the same code as in matchMkDirective.
-       for !lexer.EOF() && lexer.PeekByte() != '#' {
-               switch {
-               case lexer.SkipString("[#"):
-                       break
-
-               case lexer.PeekByte() == '\\' && len(lexer.Rest()) > 1:
-                       lexer.Skip(2)
-
-               default:
-                       lexer.Skip(1)
-               }
+       value := trimHspace(lexer.Rest() + rest)
+       if value == "" {
+               spaceBeforeComment = ""
        }
-       rawValueWithSpace := lexer.Since(valueStart)
-       spaceAfterValue = rawValueWithSpace[len(strings.TrimRight(rawValueWithSpace, " \t")):]
-       value = trimHspace(strings.Replace(lexer.Since(valueStart), "\\#", "#", -1))
-       comment = lexer.Rest()
+       valueAlign := ifelseStr(commented, "#", "") + lexer.Since(mainStart)
 
-       m = true
-       return
+       return true, &mkLineAssignImpl{
+               commented:         commented,
+               varname:           varname,
+               varcanon:          varnameCanon(varname),
+               varparam:          varnameParam(varname),
+               spaceAfterVarname: spaceAfterVarname,
+               op:                op,
+               valueAlign:        valueAlign,
+               value:             value,
+               valueMk:           nil, // filled in lazily
+               valueMkRest:       "",  // filled in lazily
+               fields:            nil, // filled in lazily
+               spaceAfterValue:   spaceBeforeComment,
+               comment:           ifelseStr(hasComment, "#", "") + comment,
+       }
 }
 
 func MatchMkInclude(text string) (m bool, indentation, directive, filename string) {
Index: pkgsrc/pkgtools/pkglint/files/pkglint.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.47 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.48
--- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.47       Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.go    Sun Mar 10 19:01:50 2019
@@ -380,7 +380,7 @@ func (pkglint *Pkglint) checkdirPackage(
 
        // Load the package Makefile and all included files,
        // to collect all used and defined variables and similar data.
-       mklines := pkg.loadPackageMakefile()
+       mklines, allLines := pkg.loadPackageMakefile()
        if mklines == nil {
                return
        }
@@ -437,7 +437,7 @@ func (pkglint *Pkglint) checkdirPackage(
 
                case path.Base(filename) == "Makefile":
                        pkglint.checkExecutable(filename, st.Mode())
-                       pkg.checkfilePackageMakefile(filename, mklines)
+                       pkg.checkfilePackageMakefile(filename, mklines, allLines)
 
                default:
                        pkglint.checkDirent(filename, st.Mode())

Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.52 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.53
--- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.52   Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline_test.go        Sun Mar 10 19:01:50 2019
@@ -165,23 +165,19 @@ func (s *Suite) Test_NewMkLine__autofix_
        CheckFileMk(filename)
 
        t.CheckOutputLines(
-               "NOTE: ~/Makefile:2: Unnecessary space after variable name \"VARNAME\".",
-               // FIXME: Don't say anything here because the spaced form is clearer that the compressed form.
-               "NOTE: ~/Makefile:4: Unnecessary space after variable name \"VARNAME+\".")
+               "NOTE: ~/Makefile:2: Unnecessary space after variable name \"VARNAME\".")
 
        t.SetUpCommandLine("-Wspace", "--autofix")
 
        CheckFileMk(filename)
 
        t.CheckOutputLines(
-               "AUTOFIX: ~/Makefile:2: Replacing \"VARNAME +=\" with \"VARNAME+=\".",
-               // FIXME: Don't fix anything here because the spaced form is clearer that the compressed form.
-               "AUTOFIX: ~/Makefile:4: Replacing \"VARNAME+ +=\" with \"VARNAME++=\".")
+               "AUTOFIX: ~/Makefile:2: Replacing \"VARNAME +=\" with \"VARNAME+=\".")
        t.CheckFileLines("Makefile",
                MkRcsID+"",
                "VARNAME+=\t${VARNAME}",
                "VARNAME+ =\t${VARNAME+}",
-               "VARNAME++=\t${VARNAME+}",
+               "VARNAME+ +=\t${VARNAME+}",
                "pkgbase := pkglint")
 }
 
@@ -193,14 +189,45 @@ func (s *Suite) Test_NewMkLine__varname_
        // Parse error because the # starts a comment.
        c.Check(mkline.IsVarassign(), equals, false)
 
-       mkline2 := t.NewMkLine("Makefile", 123, "VARNAME.\\#=\tvalue")
+       mkline2 := t.NewMkLine("Makefile", 124, "VARNAME.\\#=\tvalue")
 
-       // FIXME: Varname() should be "VARNAME.#".
-       c.Check(mkline2.IsVarassign(), equals, false)
+       c.Check(mkline2.IsVarassign(), equals, true)
+       c.Check(mkline2.Varname(), equals, "VARNAME.#")
 
        t.CheckOutputLines(
-               "ERROR: Makefile:123: Unknown Makefile line format: \"VARNAME.#=\\tvalue\".",
-               "ERROR: Makefile:123: Unknown Makefile line format: \"VARNAME.\\\\#=\\tvalue\".")
+               "ERROR: Makefile:123: Unknown Makefile line format: \"VARNAME.#=\\tvalue\".")
+}
+
+// Ensures that pkglint parses escaped # characters in the same way as bmake.
+//
+// To check that bmake parses them the same, set a breakpoint after the t.NewMkLines
+// and look in t.tmpdir for the location of the file. Then run bmake with that file.
+func (s *Suite) Test_NewMkLine__escaped_hash_in_value(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("Makefile",
+               "VAR0=\tvalue#",
+               "VAR1=\tvalue\\#",
+               "VAR2=\tvalue\\\\#",
+               "VAR3=\tvalue\\\\\\#",
+               "VAR4=\tvalue\\\\\\\\#",
+               "",
+               "all:",
+               ".for var in VAR0 VAR1 VAR2 VAR3 VAR4",
+               "\t@printf '%s\\n' ${${var}}''",
+               ".endfor")
+       parsed := mklines.mklines
+
+       c.Check(parsed[0].Value(), equals, "value")
+       c.Check(parsed[1].Value(), equals, "value#")
+       c.Check(parsed[2].Value(), equals, "value\\\\")
+       c.Check(parsed[3].Value(), equals, "value\\\\#")
+       c.Check(parsed[4].Value(), equals, "value\\\\\\\\")
+
+       t.CheckOutputLines(
+               "WARN: ~/Makefile:1: The # character starts a Makefile comment.",
+               "WARN: ~/Makefile:3: The # character starts a Makefile comment.",
+               "WARN: ~/Makefile:5: The # character starts a Makefile comment.")
 }
 
 func (s *Suite) Test_MkLine_Varparam(c *check.C) {
@@ -254,26 +281,26 @@ func (s *Suite) Test_VarUseContext_Strin
 func (s *Suite) Test_NewMkLine__number_sign(c *check.C) {
        t := s.Init(c)
 
-       mklineVarassignEscaped := t.NewMkLine("filename", 1, "SED_CMD=\t's,\\#,hash,g'")
+       mklineVarassignEscaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,\\#,hash,g'")
 
        c.Check(mklineVarassignEscaped.Varname(), equals, "SED_CMD")
        c.Check(mklineVarassignEscaped.Value(), equals, "'s,#,hash,g'")
 
-       mklineCommandEscaped := t.NewMkLine("filename", 1, "\tsed -e 's,\\#,hash,g'")
+       mklineCommandEscaped := t.NewMkLine("filename.mk", 1, "\tsed -e 's,\\#,hash,g'")
 
        c.Check(mklineCommandEscaped.ShellCommand(), equals, "sed -e 's,\\#,hash,g'")
 
        // From shells/zsh/Makefile.common, rev. 1.78
-       mklineCommandUnescaped := t.NewMkLine("filename", 1, "\t# $ sha1 patches/patch-ac")
+       mklineCommandUnescaped := t.NewMkLine("filename.mk", 1, "\t# $ sha1 patches/patch-ac")
 
        c.Check(mklineCommandUnescaped.ShellCommand(), equals, "# $ sha1 patches/patch-ac")
        t.CheckOutputEmpty() // No warning about parsing the lonely dollar sign.
 
-       mklineVarassignUnescaped := t.NewMkLine("filename", 1, "SED_CMD=\t's,#,hash,'")
+       mklineVarassignUnescaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,#,hash,'")
 
        c.Check(mklineVarassignUnescaped.Value(), equals, "'s,")
        t.CheckOutputLines(
-               "WARN: filename:1: The # character starts a Makefile comment.")
+               "WARN: filename.mk:1: The # character starts a Makefile comment.")
 }
 
 func (s *Suite) Test_NewMkLine__varassign_leading_space(c *check.C) {
@@ -331,7 +358,7 @@ func (s *Suite) Test_NewMkLine__infrastr
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__unknown_rhs(c *check.C) {
        t := s.Init(c)
 
-       mkline := t.NewMkLine("filename", 1, "PKGNAME:= ${UNKNOWN}")
+       mkline := t.NewMkLine("filename.mk", 1, "PKGNAME:= ${UNKNOWN}")
        t.SetUpVartypes()
 
        vuc := VarUseContext{G.Pkgsrc.VariableType("PKGNAME"), vucTimeParse, VucQuotUnknown, false}
@@ -728,7 +755,7 @@ func (s *Suite) Test_MkLine_VariableNeed
 
        t.CheckOutputLines(
                "NOTE: ~/Makefile:6: The substitution command \"s:@LINKER_RPATH_FLAG@:${LINKER_RPATH_FLAG}:g\" " +
-                       "can be replaced with \"SUBST_VARS.class+= LINKER_RPATH_FLAG\".")
+                       "can be replaced with \"SUBST_VARS.class= LINKER_RPATH_FLAG\".")
 }
 
 // Tools, when used in a shell command, must not be quoted.
@@ -1050,6 +1077,14 @@ func (s *Suite) Test_MkLine_ValueTokens_
                "WARN: Makefile:2: Please use curly braces {} instead of round parentheses () for ROUND.")
 }
 
+func (s *Suite) Test_MkLine_Tokenize__commented_varassign(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("filename.mk", 123, "#VAR=\tvalue ${VAR} suffix text")
+
+       t.Check(mkline.Tokenize(mkline.Value(), false), check.HasLen, 3)
+}
+
 func (s *Suite) Test_MkLine_ResolveVarsInRelativePath(c *check.C) {
        t := s.Init(c)
 
@@ -1103,25 +1138,32 @@ func (s *Suite) Test_MatchVarassign(c *c
        s.Init(c)
 
        test := func(text string, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string) {
-               type VarAssign struct {
-                       commented                  bool
-                       varname, spaceAfterVarname string
-                       op, align                  string
-                       value, spaceAfterValue     string
-                       comment                    string
-               }
-               expected := VarAssign{commented, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment}
-               am, acommented, avarname, aspaceAfterVarname, aop, aalign, avalue, aspaceAfterValue, acomment := MatchVarassign(text)
-               if !am {
+               m, actual := MatchVarassign(text)
+               if !m {
                        c.Errorf("Text %q doesn't match variable assignment", text)
                        return
                }
-               actual := VarAssign{acommented, avarname, aspaceAfterVarname, aop, aalign, avalue, aspaceAfterValue, acomment}
-               c.Check(actual, equals, expected)
+
+               expected := mkLineAssignImpl{
+                       commented:         commented,
+                       varname:           varname,
+                       varcanon:          varnameCanon(varname),
+                       varparam:          varnameParam(varname),
+                       spaceAfterVarname: spaceAfterVarname,
+                       op:                NewMkOperator(op),
+                       valueAlign:        align,
+                       value:             value,
+                       valueMk:           nil,
+                       valueMkRest:       "",
+                       fields:            nil,
+                       spaceAfterValue:   spaceAfterValue,
+                       comment:           comment,
+               }
+               c.Check(*actual, deepEquals, expected)
        }
 
        testInvalid := func(text string) {
-               m, _, _, _, _, _, _, _, _ := MatchVarassign(text)
+               m, _ := MatchVarassign(text)
                if m {
                        c.Errorf("Text %q matches variable assignment but shouldn't.", text)
                }
@@ -1151,6 +1193,69 @@ func (s *Suite) Test_MatchVarassign(c *c
        // A single space is typically used for writing documentation, not for commenting out code.
        // Therefore this line doesn't count as commented variable assignment.
        testInvalid("# VAR=value")
+
+       // Ensure that the alignment for the variable value is correct.
+       test("BUILD_DIRS=\tdir1 dir2",
+               false,
+               "BUILD_DIRS",
+               "",
+               "=",
+               "BUILD_DIRS=\t",
+               "dir1 dir2",
+               "",
+               "")
+
+       // Ensure that the alignment for the variable value is correct,
+       // even if the whole line is commented.
+       test("#BUILD_DIRS=\tdir1 dir2",
+               true,
+               "BUILD_DIRS",
+               "",
+               "=",
+               "#BUILD_DIRS=\t",
+               "dir1 dir2",
+               "",
+               "")
+
+       test("MASTER_SITES=\t#none",
+               false,
+               "MASTER_SITES",
+               "",
+               "=",
+               "MASTER_SITES=\t",
+               "",
+               "",
+               "#none")
+
+       test("MASTER_SITES=\t# none",
+               false,
+               "MASTER_SITES",
+               "",
+               "=",
+               "MASTER_SITES=\t",
+               "",
+               "",
+               "# none")
+
+       test("EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
+               false,
+               "EGDIRS",
+               "",
+               "=",
+               "EGDIRS=\t",
+               "${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
+               "",
+               "")
+
+       test("VAR:=\t${VAR:M-*:[\\#]}",
+               false,
+               "VAR",
+               "",
+               ":=",
+               "VAR:=\t",
+               "${VAR:M-*:[#]}",
+               "",
+               "")
 }
 
 func (s *Suite) Test_NewMkOperator(c *check.C) {
@@ -1254,6 +1359,32 @@ func (s *Suite) Test_Indentation_TrackAf
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_Indentation_Varnames__repetition(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpPackage("category/other")
+       t.CreateFileDummyBuildlink3("category/other/buildlink3.mk")
+       t.SetUpPackage("category/package",
+               "DISTNAME=\tpackage-1.0",
+               ".include \"../../category/other/buildlink3.mk\"")
+       t.CreateFileDummyBuildlink3("category/package/buildlink3.mk",
+               ".if ${OPSYS} == NetBSD || ${OPSYS} == FreeBSD",
+               ".  if ${OPSYS} == NetBSD",
+               ".    include \"../../category/other/buildlink3.mk\"",
+               ".  endif",
+               ".endif")
+
+       G.Check(t.File("category/package"))
+
+       // TODO: It feels wrong that OPSYS is mentioned twice here.
+       //  Why only twice and not three times?
+       t.CheckOutputLines(
+               "WARN: ~/category/package/buildlink3.mk:14: " +
+                       "\"../../category/other/buildlink3.mk\" is included conditionally here " +
+                       "(depending on OPSYS, OPSYS) and unconditionally in Makefile:20.")
+}
+
 func (s *Suite) Test_MkLine_DetermineUsedVariables(c *check.C) {
        t := s.Init(c)
 
@@ -1330,6 +1461,362 @@ func (s *Suite) Test_MkLine_UnquoteShell
        test("`", "`")
 }
 
+func (s *Suite) Test_unescapeMkComment(c *check.C) {
+       t := s.Init(c)
+
+       test := func(text string, main, comment string) {
+               aMain, aComment := unescapeMkComment(text)
+               t.Check(
+                       []interface{}{text, aMain, aComment},
+                       deepEquals,
+                       []interface{}{text, main, comment})
+       }
+
+       test("",
+               "",
+               "")
+       test("text",
+               "text",
+               "")
+
+       // The leading space from the comment is preserved to make parsing as exact
+       // as possible.
+       //
+       // The difference between "#defined" and "# defined" is relevant in a few
+       // cases, such as the API documentation of the infrastructure files.
+       test("# comment",
+               "",
+               "# comment")
+       test("#\tcomment",
+               "",
+               "#\tcomment")
+       test("#   comment",
+               "",
+               "#   comment")
+
+       // Other than in the shell, # also starts a comment in the middle of a word.
+       test("COMMENT=\tThe C# compiler",
+               "COMMENT=\tThe C",
+               "# compiler")
+       test("COMMENT=\tThe C\\# compiler",
+               "COMMENT=\tThe C# compiler",
+               "")
+
+       test("${TARGET}: ${SOURCES} # comment",
+               "${TARGET}: ${SOURCES} ",
+               "# comment")
+
+       // A # starts a comment, except if it immediately follows a [.
+       // This is done so that the length modifier :[#] can be written without
+       // escaping the #.
+       test("VAR=\t${OTHER:[#]} # comment",
+               "VAR=\t${OTHER:[#]} ",
+               "# comment")
+
+       // The # in the :[#] modifier may be escaped or not. Both forms are equivalent.
+       test("VAR:=\t${VAR:M-*:[\\#]}",
+               "VAR:=\t${VAR:M-*:[#]}",
+               "")
+
+       // The character [ prevents the following # from starting a comment, even
+       // outside of variable modifiers.
+       test("COMMENT=\t[#] $$\\# $$# comment",
+               "COMMENT=\t[#] $$# $$",
+               "# comment")
+
+       // A backslash always escapes the next character, be it a # for a comment
+       // or something else. This makes it difficult to write a literal \# in a
+       // Makefile, but that's an edge case anyway.
+       test("VAR0=\t#comment",
+               "VAR0=\t",
+               "#comment")
+       test("VAR1=\t\\#no-comment",
+               "VAR1=\t#no-comment",
+               "")
+       test("VAR2=\t\\\\#comment",
+               "VAR2=\t\\\\",
+               "#comment")
+
+       // The backslash is only removed when it escapes a comment.
+       // In particular, it cannot be used to escape a dollar that starts a
+       // variable use.
+       test("VAR0=\t$T",
+               "VAR0=\t$T",
+               "")
+       test("VAR1=\t\\$T",
+               "VAR1=\t\\$T",
+               "")
+       test("VAR2=\t\\\\$T",
+               "VAR2=\t\\\\$T",
+               "")
+
+       // To escape a dollar, write it twice.
+       test("$$shellvar $${shellvar} \\${MKVAR} [] \\x",
+               "$$shellvar $${shellvar} \\${MKVAR} [] \\x",
+               "")
+
+       // Parse errors are recorded in the rest return value.
+       test("${UNCLOSED",
+               "${UNCLOSED",
+               "")
+
+       // In this early phase of parsing, unfinished variable uses are not
+       // interpreted and do not influence the detection of the comment start.
+       test("text before ${UNCLOSED # comment",
+               "text before ${UNCLOSED ",
+               "# comment")
+
+       // The dollar-space refers to a normal Make variable named " ".
+       // The lonely dollar at the very end refers to the variable named "",
+       // which is specially protected in bmake to always contain the empty string.
+       // It is heavily used in .for loops in the form ${:Uvalue}.
+       test("Lonely $ character $",
+               "Lonely $ character $",
+               "")
+
+       // An even number of backslashes does not escape the #.
+       // Therefore it starts a comment here.
+       test("VAR2=\t\\\\#comment",
+               "VAR2=\t\\\\",
+               "#comment")
+}
+
+func (s *Suite) Test_splitMkLine(c *check.C) {
+       t := s.Init(c)
+
+       varuse := func(varname string, modifiers ...string) *MkToken {
+               text := "${" + varname
+               for _, modifier := range modifiers {
+                       text += ":" + modifier
+               }
+               text += "}"
+               return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
+       }
+       varuseText := func(text, varname string, modifiers ...string) *MkToken {
+               return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
+       }
+       text := func(text string) *MkToken {
+               return &MkToken{text, nil}
+       }
+       tokens := func(tokens ...*MkToken) []*MkToken {
+               return tokens
+       }
+       _, _, _, _ = text, varuse, varuseText, tokens
+
+       test := func(text string, main string, tokens []*MkToken, rest string, spaceBeforeComment string, hasComment bool, comment string) {
+               aMain, aTokens, aRest, aSpaceBeforeComment, aHasComment, aComment := splitMkLine(text)
+               t.Check(
+                       []interface{}{text, aTokens, aMain, aRest, aSpaceBeforeComment, aHasComment, aComment},
+                       deepEquals,
+                       []interface{}{text, tokens, main, rest, spaceBeforeComment, hasComment, comment})
+       }
+
+       test("",
+               "",
+               tokens(),
+               "",
+               "",
+               false,
+               "")
+       test("text",
+               "text",
+               tokens(text("text")),
+               "",
+               "",
+               false,
+               "")
+
+       // The leading space from the comment is preserved to make parsing as exact
+       // as possible.
+       //
+       // The difference between "#defined" and "# defined" is relevant in a few
+       // cases, such as the API documentation of the infrastructure files.
+       test("# comment",
+               "",
+               tokens(),
+               "",
+               "",
+               true,
+               " comment")
+       test("#\tcomment",
+               "",
+               tokens(),
+               "",
+               "",
+               true,
+               "\tcomment")
+       test("#   comment",
+               "",
+               tokens(),
+               "",
+               "",
+               true,
+               "   comment")
+
+       // Other than in the shell, # also starts a comment in the middle of a word.
+       test("COMMENT=\tThe C# compiler",
+               "COMMENT=\tThe C",
+               tokens(text("COMMENT=\tThe C")),
+               "",
+               "",
+               true,
+               " compiler")
+       test("COMMENT=\tThe C\\# compiler",
+               "COMMENT=\tThe C# compiler",
+               tokens(text("COMMENT=\tThe C# compiler")),
+               "",
+               "",
+               false,
+               "")
+
+       test("${TARGET}: ${SOURCES} # comment",
+               "${TARGET}: ${SOURCES}",
+               tokens(varuse("TARGET"), text(": "), varuse("SOURCES"), text(" ")),
+               "",
+               " ",
+               true,
+               " comment")
+
+       // A # starts a comment, except if it immediately follows a [.
+       // This is done so that the length modifier :[#] can be written without
+       // escaping the #.
+       test("VAR=\t${OTHER:[#]} # comment",
+               "VAR=\t${OTHER:[#]}",
+               tokens(text("VAR=\t"), varuse("OTHER", "[#]"), text(" ")),
+               "",
+               " ",
+               true,
+               " comment")
+
+       // The # in the :[#] modifier may be escaped or not. Both forms are equivalent.
+       test("VAR:=\t${VAR:M-*:[\\#]}",
+               "VAR:=\t${VAR:M-*:[#]}",
+               tokens(text("VAR:=\t"), varuse("VAR", "M-*", "[#]")),
+               "",
+               "",
+               false,
+               "")
+
+       // A backslash always escapes the next character, be it a # for a comment
+       // or something else. This makes it difficult to write a literal \# in a
+       // Makefile, but that's an edge case anyway.
+       test("VAR0=\t#comment",
+               "VAR0=",
+               tokens(text("VAR0=\t")),
+               "",
+               // Later, when converting this result into a proper variable assignment,
+               // this "space before comment" is reclassified as "space before the value",
+               // in order to align the "#comment" with the other variable values.
+               "\t",
+               true,
+               "comment")
+       test("VAR1=\t\\#no-comment",
+               "VAR1=\t#no-comment",
+               tokens(text("VAR1=\t#no-comment")),
+               "",
+               "",
+               false,
+               "")
+       test("VAR2=\t\\\\#comment",
+               "VAR2=\t\\\\",
+               tokens(text("VAR2=\t\\\\")),
+               "",
+               "",
+               true,
+               "comment")
+
+       // The backslash is only removed when it escapes a comment.
+       // In particular, it cannot be used to escape a dollar that starts a
+       // variable use.
+       test("VAR0=\t$T",
+               "VAR0=\t$T",
+               tokens(text("VAR0=\t"), varuseText("$T", "T")),
+               "",
+               "",
+               false,
+               "")
+       test("VAR1=\t\\$T",
+               "VAR1=\t\\$T",
+               tokens(text("VAR1=\t\\"), varuseText("$T", "T")),
+               "",
+               "",
+               false,
+               "")
+       test("VAR2=\t\\\\$T",
+               "VAR2=\t\\\\$T",
+               tokens(text("VAR2=\t\\\\"), varuseText("$T", "T")),
+               "",
+               "",
+               false,
+               "")
+
+       // To escape a dollar, write it twice.
+       test("$$shellvar $${shellvar} \\${MKVAR} [] \\x",
+               "$$shellvar $${shellvar} \\${MKVAR} [] \\x",
+               tokens(text("$$shellvar $${shellvar} \\"), varuse("MKVAR"), text(" [] \\x")),
+               "",
+               "",
+               false,
+               "")
+
+       // Parse errors are recorded in the rest return value.
+       test("${UNCLOSED",
+               "",
+               tokens(),
+               "${UNCLOSED",
+               "",
+               false,
+               "")
+
+       // When a parse error occurs, the comment is not parsed and the main text
+       // is not trimmed to the right, to keep as much original information as
+       // possible.
+       test("text before ${UNCLOSED # comment",
+               "text before ",
+               tokens(text("text before ")),
+               "${UNCLOSED ", // FIXME: put the space into spaceBeforeComment
+               "",            // FIXME: the space is missing here
+               true,
+               " comment")
+
+       // The dollar-space refers to a normal Make variable named " ".
+       // The lonely dollar at the very end refers to the variable named "",
+       // which is specially protected in bmake to always contain the empty string.
+       // It is heavily used in .for loops in the form ${:Uvalue}.
+       //
+       // TODO: The rest of pkglint assumes that the empty string is not a valid
+       //  variable name, mainly because the empty variable name is not visible
+       //  outside of the bmake debugging mode.
+       test("Lonely $ character $",
+               "Lonely $ character ",
+               tokens(
+                       text("Lonely "),
+                       varuseText("$ " /* instead of "${ }" */, " "),
+                       text("character ")),
+               "$",
+               "",
+               false,
+               "")
+
+       // The character [ prevents the following # from starting a comment, even
+       // outside of variable modifiers.
+       test("COMMENT=\t[#] $$\\# $$# comment",
+               "COMMENT=\t[#] $$# $$",
+               tokens(text("COMMENT=\t[#] $$# $$")),
+               "",
+               "",
+               true,
+               " comment")
+
+       test("VAR2=\t\\\\#comment",
+               "VAR2=\t\\\\",
+               tokens(text("VAR2=\t\\\\")),
+               "",
+               "",
+               true,
+               "comment")
+}
+
 func (s *Suite) Test_matchMkDirective(c *check.C) {
 
        test := func(input, expectedIndent, expectedDirective, expectedArgs, expectedComment string) {
@@ -1340,11 +1827,19 @@ func (s *Suite) Test_matchMkDirective(c 
                        []interface{}{true, expectedIndent, expectedDirective, expectedArgs, expectedComment})
        }
 
+       testFail := func(input string) {
+               m, indent, directive, args, comment := matchMkDirective(input)
+               if m {
+                       c.Errorf("The line %q could be parsed as directive (%q, %q, %q, %q) but shouldn't.",
+                               indent, directive, args, comment)
+               }
+       }
+
        test(".if ${VAR} == value",
                "", "if", "${VAR} == value", "")
 
        test(".\tendif # comment",
-               "\t", "endif", "", "comment")
+               "\t", "endif", "", " comment")
 
        test(".if ${VAR} == \"#\"",
                "", "if", "${VAR} == \"", "\"")
@@ -1354,6 +1849,9 @@ func (s *Suite) Test_matchMkDirective(c 
 
        test(".if ${VAR} == \\",
                "", "if", "${VAR} == \\", "")
+
+       // Unclosed variable
+       testFail(".if ${VAR")
 }
 
 func (s *Suite) Test_MatchMkInclude(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.30 pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.31
--- pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.30 Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker.go      Sun Mar 10 19:01:50 2019
@@ -423,7 +423,7 @@ func (ck MkLineChecker) CheckVaruse(varu
        if G.Opts.WarnQuoting && vuc.quoting != VucQuotUnknown && needsQuoting != unknown {
                // FIXME: Why "Shellword" when there's no indication that this is actually a shell type?
                //  It's for splitting the value into tokens, taking "double" and 'single' quotes into account.
-               ck.CheckVaruseShellword(varname, vartype, vuc, varuse.Mod(), needsQuoting)
+               ck.CheckVaruseShellword(varname, vartype, vuc, varuse.Mod(), needsQuoting == yes)
        }
 
        if G.Pkgsrc.UserDefinedVars.Defined(varname) && !G.Pkgsrc.IsBuildDef(varname) {
@@ -544,17 +544,16 @@ func (ck MkLineChecker) checkVarusePermi
 
        mkline := ck.MkLine
        effPerms := vartype.EffectivePermissions(mkline.Basename)
+       if effPerms == aclpUnknown {
+               return
+       }
+       if effPerms.Contains(aclpUseLoadtime) {
+               return
+       }
 
        // Is the variable used at load time although that is not allowed?
-       directly := false
-       indirectly := false
-       if !effPerms.Contains(aclpUseLoadtime) { // May not be used at load time.
-               if vuc.time == vucTimeParse {
-                       directly = true
-               } else if vuc.vartype != nil && vuc.vartype.Union().Contains(aclpUseLoadtime) {
-                       indirectly = true
-               }
-       }
+       directly := vuc.time == vucTimeParse
+       indirectly := !directly && vuc.vartype != nil && vuc.vartype.Union().Contains(aclpUseLoadtime)
 
        if (directly || indirectly) && !vartype.guessed {
                if tool := G.ToolByVarname(varname); tool != nil {
@@ -566,17 +565,21 @@ func (ck MkLineChecker) checkVarusePermi
                }
        }
 
-       if !effPerms.Contains(aclpUseLoadtime) && !effPerms.Contains(aclpUse) {
+       if !effPerms.Contains(aclpUse) {
                needed := aclpUse
                if directly || indirectly {
                        needed = aclpUseLoadtime
                }
                alternativeFiles := vartype.AllowedFiles(needed)
                if alternativeFiles != "" {
-                       mkline.Warnf("%s may not be used in this file; it would be ok in %s.",
-                               varname, alternativeFiles)
+                       if G.Mk == nil || G.Mk.FirstTimeSlice("don't-use", varname, mkline.Filename) {
+                               mkline.Warnf("%s may not be used in this file; it would be ok in %s.",
+                                       varname, alternativeFiles)
+                       }
                } else {
-                       mkline.Warnf("%s may not be used in any file; it is a write-only variable.", varname)
+                       if G.Mk == nil || G.Mk.FirstTimeSlice("write-only", varname) {
+                               mkline.Warnf("%s may not be used in any file; it is a write-only variable.", varname)
+                       }
                }
 
                ck.explainPermissions(varname, vartype)
@@ -654,7 +657,7 @@ func (ck MkLineChecker) warnVaruseLoadTi
 
 // CheckVaruseShellword checks whether a variable use of the form ${VAR}
 // or ${VAR:modifiers} is allowed in a certain context.
-func (ck MkLineChecker) CheckVaruseShellword(varname string, vartype *Vartype, vuc *VarUseContext, mod string, needsQuoting YesNoUnknown) {
+func (ck MkLineChecker) CheckVaruseShellword(varname string, vartype *Vartype, vuc *VarUseContext, mod string, needsQuoting bool) {
        if trace.Tracing {
                defer trace.Call(varname, vartype, vuc, mod, needsQuoting)()
        }
@@ -672,7 +675,7 @@ func (ck MkLineChecker) CheckVaruseShell
        if mod == ":M*:Q" && !needMstar {
                mkline.Notef("The :M* modifier is not needed here.")
 
-       } else if needsQuoting == yes {
+       } else if needsQuoting {
                modNoQ := strings.TrimSuffix(mod, ":Q")
                modNoM := strings.TrimSuffix(modNoQ, ":M*")
                correctMod := modNoM + ifelseStr(needMstar, ":M*:Q", ":Q")
@@ -744,14 +747,12 @@ func (ck MkLineChecker) CheckVaruseShell
                }
        }
 
-       if hasSuffix(mod, ":Q") && needsQuoting != yes {
+       if hasSuffix(mod, ":Q") && !needsQuoting {
                bad := "${" + varname + mod + "}"
                good := "${" + varname + strings.TrimSuffix(mod, ":Q") + "}"
 
                fix := mkline.Line.Autofix()
-               if needsQuoting == no {
-                       fix.Notef("The :Q operator isn't necessary for ${%s} here.", varname)
-               }
+               fix.Notef("The :Q operator isn't necessary for ${%s} here.", varname)
                fix.Explain(
                        "Many variables in pkgsrc do not need the :Q operator since they",
                        "are not expected to contain whitespace or other special characters.",
@@ -780,17 +781,13 @@ func (ck MkLineChecker) checkVaruseDepre
 }
 
 func (ck MkLineChecker) checkVarassignDecreasingVersions() {
-       if trace.Tracing {
-               defer trace.Call0()()
-       }
-
        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("All values for %s must be positive integers.", mkline.Varname())
+                       mkline.Errorf("Value %q for %s must be a positive integer.", strVersion, mkline.Varname())
                        return
                }
                intVersions[i] = iver
@@ -798,7 +795,8 @@ func (ck MkLineChecker) checkVarassignDe
 
        for i, ver := range intVersions {
                if i > 0 && ver >= intVersions[i-1] {
-                       mkline.Warnf("The values for %s should be in decreasing order.", mkline.Varname())
+                       mkline.Warnf("The values for %s should be in decreasing order (%d before %d).",
+                               mkline.Varname(), ver, intVersions[i-1])
                        G.Explain(
                                "If they aren't, it may be possible that needless versions of",
                                "packages are installed.")
@@ -858,27 +856,37 @@ func (ck MkLineChecker) checkVarassignLe
        }
 }
 
+// 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 the variable is not used and is untyped, it may be a spelling mistake.
        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
+       }
 
-       } else if !varIsUsedSimilar(varname) {
-               if vartypes := G.Pkgsrc.vartypes; vartypes[varname] != nil || vartypes[varcanon] != nil {
-                       // Ok
-               } else if deprecated := G.Pkgsrc.Deprecated; deprecated[varname] != "" || deprecated[varcanon] != "" {
-                       // Ok
-               } else if G.Mk != nil && !G.Mk.FirstTimeSlice("defined but not used: ", varname) {
-                       // Skip
-               } else {
-                       ck.MkLine.Warnf("%s is defined but not used.", varname)
-               }
+       if varIsUsedSimilar(varname) {
+               return
+       }
+
+       if vartypes := G.Pkgsrc.vartypes; vartypes[varname] != nil || vartypes[varcanon] != nil {
+               return
+       }
+
+       if deprecated := G.Pkgsrc.Deprecated; deprecated[varname] != "" || deprecated[varcanon] != "" {
+               return
+       }
+
+       if G.Mk != nil && !G.Mk.FirstTimeSlice("defined but not used: ", varname) {
+               return
        }
+
+       ck.MkLine.Warnf("%s is defined but not used.", varname)
 }
 
 // checkVarassignRightVaruse checks that in a variable assignment,
@@ -1070,7 +1078,7 @@ func (ck MkLineChecker) checkVartype(var
        case value == "":
                break
 
-       case vartype.kindOfList == lkShell:
+       default:
                words, _ := splitIntoMkWords(mkline.Line, value)
                for _, word := range words {
                        ck.CheckVartypeBasic(varname, vartype.basicType, op, word, comment, vartype.guessed)
@@ -1146,26 +1154,34 @@ func (ck MkLineChecker) checkDirectiveCo
                return
        }
 
-       checkCompareVarStr := func(varuse *MkVarUse, op string, value string) {
+       checkCompareVarStr := func(varuse *MkVarUse, op string, str string) {
                varname := varuse.varname
                varmods := varuse.modifiers
                switch len(varmods) {
                case 0:
-                       ck.checkCompareVarStr(varname, op, value)
+                       ck.checkCompareVarStr(varname, op, str)
 
                case 1:
-                       if m, _, _ := varmods[0].MatchMatch(); m && value != "" {
-                               ck.checkVartype(varname, opUseMatch, value, "")
+                       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 November 2018, the modifiers are not modeled.
+                       // 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, value)
+                               trace.Stepf("checkCompareVarStr ${%s%s} %s %s",
+                                       varuse.varname, varuse.Mod(), op, str)
                        }
                }
        }
@@ -1285,10 +1301,12 @@ func (ck MkLineChecker) CheckRelativePat
                return
        }
 
-       abs := resolvedPath
-       if !filepath.IsAbs(abs) {
-               abs = path.Dir(mkline.Filename) + "/" + abs
+       if filepath.IsAbs(resolvedPath) {
+               mkline.Errorf("The path %q must be relative.", resolvedPath)
+               return
        }
+
+       abs := path.Dir(mkline.Filename) + "/" + resolvedPath
        if _, err := os.Stat(abs); err != nil {
                if mustExist && !(G.Mk != nil && G.Mk.indentation.IsCheckedFile(resolvedPath)) {
                        mkline.Errorf("Relative path %q does not exist.", resolvedPath)
@@ -1305,6 +1323,7 @@ func (ck MkLineChecker) CheckRelativePat
                // From a package to another package.
        case hasPrefix(relativePath, "../mk/") && relpath(path.Dir(mkline.Filename), G.Pkgsrc.File(".")) == "..":
                // For category Makefiles.
+               // TODO: Or from a pkgsrc wip package to wip/mk.
        default:
                mkline.Warnf("Invalid relative path %q.", relativePath)
                // TODO: Explain this warning.

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.26 pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.27
--- pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.26    Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go Sun Mar 10 19:01:50 2019
@@ -1,6 +1,9 @@
 package pkglint
 
-import "gopkg.in/check.v1"
+import (
+       "gopkg.in/check.v1"
+       "runtime"
+)
 
 func (s *Suite) Test_MkLineChecker_checkVarassignLeft(c *check.C) {
        t := s.Init(c)
@@ -15,6 +18,48 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: module.mk:123: _VARNAME is defined but not used.")
 }
 
+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",
+               MkRcsID,
+               "",
+               "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.")
+}
+
+// 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",
+               MkRcsID,
+               "_VARNAME=\tvalue")
+
+       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_Check__url2pkg(c *check.C) {
        t := s.Init(c)
 
@@ -286,7 +331,7 @@ func (s *Suite) Test_MkLineChecker_check
        t := s.Init(c)
 
        t.SetUpVartypes()
-       mkline := t.NewMkLine("filename", 1, "DISTNAME=gcc-${GCC_VERSION}")
+       mkline := t.NewMkLine("filename.mk", 1, "DISTNAME=gcc-${GCC_VERSION}")
 
        MkLineChecker{mkline}.checkVartype("DISTNAME", opAssign, "gcc-${GCC_VERSION}", "")
 
@@ -318,11 +363,14 @@ func (s *Suite) Test_MkLineChecker_check
 
        G.Pkg = NewPackage(t.File("graphics/gimp-fix-ca"))
        t.SetUpVartypes()
-       mkline := t.NewMkLine("filename", 10, "MASTER_SITES=http://registry.gimp.org/file/fix-ca.c?action=download&id=9884&file=";)
+       mkline := t.NewMkLine("filename.mk", 10, "MASTER_SITES=http://registry.gimp.org/file/fix-ca.c?action=download&id=9884&file=";)
 
        MkLineChecker{mkline}.checkVarassign()
 
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "WARN: filename.mk:10: The variable MASTER_SITES may not be set " +
+                       "(only given a default value, or appended to) in this file; " +
+                       "it would be ok in Makefile, Makefile.common or options.mk.")
 }
 
 func (s *Suite) Test_MkLineChecker_checkDirectiveCond(c *check.C) {
@@ -331,7 +379,7 @@ func (s *Suite) Test_MkLineChecker_check
        t.SetUpVartypes()
 
        test := func(cond string, output ...string) {
-               MkLineChecker{t.NewMkLine("filename", 1, cond)}.checkDirectiveCond()
+               MkLineChecker{t.NewMkLine("filename.mk", 1, cond)}.checkDirectiveCond()
                if len(output) > 0 {
                        t.CheckOutputLines(output...)
                } else {
@@ -340,50 +388,43 @@ func (s *Suite) Test_MkLineChecker_check
        }
 
        test(".if !empty(PKGSRC_COMPILER:Mmycc)",
-               "WARN: filename:1: The pattern \"mycc\" cannot match any of "+
+               "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:1: A is used but not defined.",
-               "WARN: filename:1: B is used but not defined.")
+               "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:1: \"mailto:someone%example.org@localhost\"; is not a valid URL.",
-               "WARN: filename:1: HOMEPAGE should not be evaluated at load time.",
-               "WARN: filename:1: HOMEPAGE may not be used in any file; it is a write-only variable.")
+               "WARN: filename.mk:1: \"mailto:someone%example.org@localhost\"; is not a valid URL.",
+               "WARN: filename.mk:1: HOMEPAGE should not be evaluated at load time.")
 
        test(".if !empty(PKGSRC_RUN_TEST:M[Y][eE][sS])",
-               "WARN: filename:1: PKGSRC_RUN_TEST should be matched "+
+               "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])",
-               "WARN: filename:1: IS_BUILTIN.Xfixes should not be evaluated at load time.",
-               "WARN: filename:1: IS_BUILTIN.Xfixes may not be used in this file; it would be ok in builtin.mk.")
+       test(".if !empty(IS_BUILTIN.Xfixes:M[yY][eE][sS])")
 
        test(".if !empty(${IS_BUILTIN.Xfixes:M[yY][eE][sS]})",
-               "WARN: filename:1: The empty() function takes a variable name as parameter, "+
-                       "not a variable expression.",
-               "WARN: filename:1: IS_BUILTIN.Xfixes should not be evaluated at load time.",
-               "WARN: filename:1: IS_BUILTIN.Xfixes may not be used in this file; it would be ok in builtin.mk.")
+               "WARN: filename.mk:1: The empty() function takes a variable name as parameter, "+
+                       "not a variable expression.")
 
        test(".if ${PKGSRC_COMPILER} == \"msvc\"",
-               "WARN: filename:1: \"msvc\" is not valid for PKGSRC_COMPILER. "+
+               "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.",
-               "WARN: filename:1: Use ${PKGSRC_COMPILER:Mmsvc} instead of the == operator.")
+               "WARN: filename.mk:1: Use ${PKGSRC_COMPILER:Mmsvc} instead of the == operator.")
 
        test(".if ${PKG_LIBTOOL:Mlibtool}",
-               "NOTE: filename:1: PKG_LIBTOOL should be compared using == instead of matching against \":Mlibtool\".",
-               "WARN: filename:1: PKG_LIBTOOL should not be evaluated at load time.",
-               "WARN: filename:1: PKG_LIBTOOL may not be used in any file; it is a write-only variable.")
+               "NOTE: filename.mk:1: PKG_LIBTOOL should be compared using == instead of matching against \":Mlibtool\".")
 
        test(".if ${MACHINE_PLATFORM:MUnknownOS-*-*} || ${MACHINE_ARCH:Mx86}",
-               "WARN: filename:1: "+
+               "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:1: "+
+               "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 "+
@@ -391,11 +432,13 @@ func (s *Suite) Test_MkLineChecker_check
                        "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:1: MACHINE_ARCH should be compared using == instead of matching against \":Mx86\".")
+               "NOTE: filename.mk:1: MACHINE_ARCH should be compared using == instead of matching against \":Mx86\".")
 
        test(".if ${MASTER_SITES:Mftp://*} == \"ftp://netbsd.org/\"";,
-               "WARN: filename:1: MASTER_SITES should not be evaluated at load time.",
-               "WARN: filename:1: MASTER_SITES may not be used in any file; it is a write-only variable.")
+               // FIXME: Indeed, indeed, the :M modifier ends at the colon.
+               //  Why doesn't pkglint complain loudly about the unknown "//*" modifier?
+               "WARN: filename.mk:1: \"ftp\" is not a valid URL.",
+               "WARN: filename.mk:1: MASTER_SITES should not be evaluated at load time.")
 
        // The only interesting line from the below tracing output is the one
        // containing "checkCompareVarStr".
@@ -405,17 +448,17 @@ func (s *Suite) Test_MkLineChecker_check
                "TRACE: 1 + (*MkParser).mkCondAtom(\"${VAR:Mpattern1:Mpattern2} == comparison\")",
                "TRACE: 1 - (*MkParser).mkCondAtom(\"${VAR:Mpattern1:Mpattern2} == comparison\")",
                "TRACE: 1   checkCompareVarStr ${VAR:Mpattern1:Mpattern2} == comparison",
-               "TRACE: 1 + MkLineChecker.CheckVaruse(filename:1, ${VAR:Mpattern1:Mpattern2}, (no-type time:parse quoting:plain wordpart:false))",
+               "TRACE: 1 + MkLineChecker.CheckVaruse(filename.mk:1, ${VAR:Mpattern1:Mpattern2}, (no-type time:parse quoting:plain wordpart:false))",
                "TRACE: 1 2 + (*Pkgsrc).VariableType(\"VAR\")",
                "TRACE: 1 2 3   No type definition found for \"VAR\".",
                "TRACE: 1 2 - (*Pkgsrc).VariableType(\"VAR\", \"=>\", (*pkglint.Vartype)(nil))",
-               "WARN: filename:1: VAR is used but not defined.",
+               "WARN: filename.mk:1: VAR is used but not defined.",
                "TRACE: 1 2 + MkLineChecker.checkVarusePermissions(\"VAR\", (no-type time:parse quoting:plain wordpart:false))",
                "TRACE: 1 2 3   No type definition found for \"VAR\".",
                "TRACE: 1 2 - MkLineChecker.checkVarusePermissions(\"VAR\", (no-type time:parse quoting:plain wordpart:false))",
                "TRACE: 1 2 + (*MkLineImpl).VariableNeedsQuoting(\"VAR\", (*pkglint.Vartype)(nil), (no-type time:parse quoting:plain wordpart:false))",
                "TRACE: 1 2 - (*MkLineImpl).VariableNeedsQuoting(\"VAR\", (*pkglint.Vartype)(nil), (no-type time:parse quoting:plain wordpart:false), \"=>\", unknown)",
-               "TRACE: 1 - MkLineChecker.CheckVaruse(filename:1, ${VAR:Mpattern1:Mpattern2}, (no-type time:parse quoting:plain wordpart:false))",
+               "TRACE: 1 - MkLineChecker.CheckVaruse(filename.mk:1, ${VAR:Mpattern1:Mpattern2}, (no-type time:parse quoting:plain wordpart:false))",
                "TRACE: - MkLineChecker.checkDirectiveCond(\"${VAR:Mpattern1:Mpattern2} == comparison\")")
        t.EnableSilentTracing()
 }
@@ -471,6 +514,40 @@ func (s *Suite) Test_MkLineChecker_check
        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.SetUpVartypes()
+       mkline := t.NewMkLine("filename.mk", 123, "LICENSE?=\tgnu-gpl-v2")
+
+       MkLineChecker{mkline}.checkVarassignLeftPermissions()
+
+       t.CheckOutputEmpty()
+}
+
+// Setting a default license doesn't make sense in a package Makefile
+// since that Makefile is only used for a single package.
+// It only makes sense to set the license unconditionally there.
+func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions__license_default_Makefile(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mkline := t.NewMkLine("Makefile", 123, "LICENSE?=\tgnu-gpl-v2")
+
+       MkLineChecker{mkline}.checkVarassignLeftPermissions()
+
+       t.CheckOutputLines(
+               "WARN: Makefile:123: " +
+                       "The variable LICENSE may not be given a default value " +
+                       "(only set, or appended to) in this file; " +
+                       "it would be ok in *.")
+}
+
 // 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)
@@ -651,12 +728,13 @@ func (s *Suite) Test_MkLineChecker_check
        t.SetUpVartypes()
        mklines := t.NewMkLines("any.mk",
                MkRcsID,
-               // PKGREVISION may only be set in Makefile, not used at load time; see vardefs.go.
                ".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 evaluated at load time.",
                "WARN: any.mk:2: PKGREVISION may not be used in any file; it is a write-only variable.")
@@ -677,6 +755,127 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: file.mk:2: ONLY_FOR_UNPRIVILEGED should not be evaluated indirectly at load time.")
 }
 
+// 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",
+               MkRcsID,
+               "USE_TOOLS+=\t${PKGREVISION}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: file.mk:2: PKGREVISION should not be evaluated indirectly at load time.",
+               "WARN: file.mk:2: PKGREVISION may 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",
+               MkRcsID,
+               "VAR=\t${VAR} ${AUTO_MKDIRS}")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: buildlink3.mk:2: " +
+                       "AUTO_MKDIRS may not be used in this file; " +
+                       "it would be ok in Makefile, Makefile.* or *.mk.")
+}
+
+func (s *Suite) Test_MkLineChecker_checkVarusePermissions__multiple_times_per_file(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("buildlink3.mk",
+               MkRcsID,
+               "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 may not be used in this file; "+
+                       "it would be ok in Makefile, Makefile.* or *.mk.",
+               "WARN: buildlink3.mk:2: "+
+                       "PKGREVISION may not be used in any file; "+
+                       "it is a write-only variable.")
+}
+
+// In some pkglint tests, the method is called directly without G.Mk being set.
+// In practice this doesn't happen.
+func (s *Suite) Test_MkLineChecker_checkVarusePermissions__without_mklines(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mkline := t.NewMkLine("buildlink3.mk", 123,
+               "VAR=\t${VAR} ${AUTO_MKDIRS} ${AUTO_MKDIRS} ${PKGREVISION} ${PKGREVISION}")
+
+       MkLineChecker{mkline}.Check()
+
+       // Since G.Mk is not set, the duplicates are not suppressed.
+       // Therefore in this case there are more warnings than in realistic situations.
+       t.CheckOutputLines(
+               "WARN: buildlink3.mk:123: VAR is defined but not used.",
+               "WARN: buildlink3.mk:123: VAR is used but not defined.",
+               "WARN: buildlink3.mk:123: "+
+                       "AUTO_MKDIRS may not be used in this file; "+
+                       "it would be ok in Makefile, Makefile.* or *.mk.",
+               "WARN: buildlink3.mk:123: "+
+                       "AUTO_MKDIRS may not be used in this file; "+
+                       "it would be ok in Makefile, Makefile.* or *.mk.",
+               "WARN: buildlink3.mk:123: "+
+                       "PKGREVISION may not be used in any file; "+
+                       "it is a write-only variable.",
+               "WARN: buildlink3.mk:123: "+
+                       "PKGREVISION may not be used in any file; "+
+                       "it is a write-only variable.")
+}
+
+func (s *Suite) Test_MkLineChecker_checkVarassignDecreasingVersions(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "PYTHON_VERSIONS_ACCEPTED=\t36 __future__",
+               "PYTHON_VERSIONS_ACCEPTED=\t36 -13",
+               "PYTHON_VERSIONS_ACCEPTED=\t36 ${PKGVERSION_NOREV}",
+               "PYTHON_VERSIONS_ACCEPTED=\t36 37",
+               "PYTHON_VERSIONS_ACCEPTED=\t37 36 27 25")
+
+       // 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_MkLineChecker_warnVaruseToolLoadTime(c *check.C) {
        t := s.Init(c)
 
@@ -707,6 +906,33 @@ func (s *Suite) Test_MkLineChecker_warnV
                "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",
+               MkRcsID,
+               ".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_Check__warn_varuse_LOCALBASE(c *check.C) {
        t := s.Init(c)
 
@@ -802,6 +1028,32 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: 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",
+               MkRcsID,
+               ".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: \"*\" is not a valid pathname.")
+}
+
 func (s *Suite) Test_MkLineChecker_checkDirectiveCondEmpty(c *check.C) {
        t := s.Init(c)
 
@@ -840,15 +1092,17 @@ func (s *Suite) Test_MkLineChecker_check
        t := s.Init(c)
 
        t.SetUpVartypes()
-       G.Mk = t.NewMkLines("audio/pulseaudio/Makefile",
+       G.Mk = t.NewMkLines("Makefile",
                MkRcsID,
-               ".if ${OPSYS} == \"Darwin\" && ${PKGSRC_COMPILER} == \"clang\"",
+               ".if ${PKGSRC_COMPILER} == \"clang\"",
+               ".elif ${PKGSRC_COMPILER} != \"gcc\"",
                ".endif")
 
        G.Mk.Check()
 
        t.CheckOutputLines(
-               "WARN: audio/pulseaudio/Makefile:2: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.")
+               "WARN: Makefile:2: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
+               "WARN: Makefile:3: Use ${PKGSRC_COMPILER:Ngcc} instead of the != operator.")
 }
 
 func (s *Suite) Test_MkLineChecker_checkVartype__CFLAGS_with_backticks(c *check.C) {
@@ -1298,35 +1552,54 @@ func (s *Suite) Test_MkLineChecker_check
        mklines := t.SetUpFileMkLines("module.mk",
                MkRcsID,
                "EGDIR=                  ${PREFIX}/etc/rc.d",
+               "RPMIGNOREPATH+=         ${PREFIX}/etc/rc.d",
                "_TOOLS_VARNAME.sed=     SED",
                "DIST_SUBDIR=            ${PKGNAME}",
                "WRKSRC=                 ${PKGNAME}",
-               "SITES_distfile.tar.gz=  ${MASTER_SITE_GITHUB:=user/}",
-               // TODO: The first of the below assignments should be flagged as redundant by RedundantScope;
-               //  as of January 2019, that check is only implemented for package Makefiles, not for other files.
-               "PYTHON_VERSIONS_ACCEPTED= -13",
-               "PYTHON_VERSIONS_ACCEPTED= 27 36")
+               "SITES_distfile.tar.gz=  ${MASTER_SITE_GITHUB:=user/}")
 
        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:3: Variable names starting with an underscore (_TOOLS_VARNAME.sed) are reserved for internal pkgsrc use.",
-               "WARN: ~/module.mk:3: _TOOLS_VARNAME.sed is defined but not used.",
-               "WARN: ~/module.mk:4: PKGNAME should not be used in DIST_SUBDIR as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
-               "WARN: ~/module.mk:5: PKGNAME should not be used in WRKSRC as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
-               "WARN: ~/module.mk:6: SITES_distfile.tar.gz is defined but not used.",
-               "WARN: ~/module.mk:6: SITES_* is deprecated. Please use SITES.* instead.",
-               "WARN: ~/module.mk:7: The variable PYTHON_VERSIONS_ACCEPTED may not be set "+
-                       "(only given a default value, or appended to) in this file; "+
-                       "it would be ok in Makefile, Makefile.common or options.mk.",
-               "WARN: ~/module.mk:7: Invalid version number \"-13\".",
-               "ERROR: ~/module.mk:7: All values for PYTHON_VERSIONS_ACCEPTED must be positive integers.",
-               "WARN: ~/module.mk:8: The variable PYTHON_VERSIONS_ACCEPTED may not be set "+
-                       "(only given a default value, or appended to) in this file; "+
-                       "it would be ok in Makefile, Makefile.common or options.mk.",
-               "WARN: ~/module.mk:8: The values for PYTHON_VERSIONS_ACCEPTED should be in decreasing order.")
+               "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.")
+}
+
+func (s *Suite) Test_MkLineChecker_checkVarassignMisc__multiple_inclusion_guards(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.SetUpVartypes()
+       t.CreateFileLines("filename.mk",
+               MkRcsID,
+               ".if !defined(FILENAME_MK)",
+               "FILENAME_MK=\t# defined",
+               ".endif")
+       t.CreateFileLines("Makefile.common",
+               MkRcsID,
+               ".if !defined(MAKEFILE_COMMON)",
+               "MAKEFILE_COMMON=\t# defined",
+               "",
+               ".endif")
+       t.CreateFileLines("other.mk",
+               MkRcsID,
+               "COMMENT=\t# defined")
+
+       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_MkLineChecker_checkText(c *check.C) {
@@ -1409,3 +1682,55 @@ func (s *Suite) Test_MkLineChecker_Check
                // TODO: This warning is unspecific, there is also a pkglint warning "should be ../../category/package".
                "WARN: ~/category/package/module.mk:11: Invalid relative path \"../package/module.mk\".")
 }
+
+func (s *Suite) Test_MkLineChecker_CheckRelativePath__absolute_path(c *check.C) {
+       t := s.Init(c)
+
+       absDir := ifelseStr(runtime.GOOS == "windows", "C:/", "/")
+       // Just a random UUID, to really guarantee that the file does not exist.
+       absPath := absDir + "0f5c2d56-8a7a-4c9d-9caa-859b52bbc8c7"
+
+       t.SetUpPkgsrc()
+       G.Pkgsrc.LoadInfrastructure()
+       mklines := t.SetUpFileMkLines("category/package/module.mk",
+               MkRcsID,
+               "DISTINFO_FILE=\t"+absPath)
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/module.mk:2: The path \"" + absPath + "\" must be relative.")
+}
+
+func (s *Suite) Test_MkLineChecker_CheckRelativePath__include_if_exists(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("filename.mk",
+               MkRcsID,
+               ".include \"included.mk\"",
+               ".sinclude \"included.mk\"")
+
+       mklines.Check()
+
+       // There is no warning for line 3 because of the "s" in "sinclude".
+       t.CheckOutputLines(
+               "ERROR: ~/filename.mk:2: Relative path \"included.mk\" does not exist.")
+}
+
+func (s *Suite) Test_MkLineChecker_CheckRelativePath__wip_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("wip/mk/git-package.mk",
+               MkRcsID)
+       t.SetUpPackage("wip/package",
+               ".include \"../mk/git-package.mk\"")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("wip/package"))
+
+       t.CheckOutputLines(
+               "WARN: ~/wip/package/Makefile:20: "+
+                       "References to the pkgsrc-wip infrastructure should look like \"../../wip/mk\", "+
+                       "not \"../mk\".",
+               "WARN: ~/wip/package/Makefile:20: Invalid relative path \"../mk/git-package.mk\".")
+}

Index: pkgsrc/pkgtools/pkglint/files/mklines.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines.go:1.42 pkgsrc/pkgtools/pkglint/files/mklines.go:1.43
--- pkgsrc/pkgtools/pkglint/files/mklines.go:1.42       Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines.go    Sun Mar 10 19:01:50 2019
@@ -115,7 +115,6 @@ func (mklines *MkLinesImpl) checkAll() {
                "pre-install": true, "do-install": true, "post-install": true,
                "pre-package": true, "do-package": true, "post-package": true,
                "pre-clean": true, "do-clean": true, "post-clean": true}
-       G.Assertf(len(allowedTargets) == 33, "Error in allowedTargets initialization")
 
        mklines.lines.CheckRcsID(0, `#[\t ]+`, "# ")
 
@@ -378,7 +377,13 @@ func (mklines *MkLinesImpl) collectDocum
 
                        parser := NewMkParser(nil, words[1], false)
                        varname := parser.Varname()
-                       if hasSuffix(varname, ".") && parser.lexer.SkipRegexp(G.res.Compile(`^<\w+>`)) {
+                       if len(varname) < 3 {
+                               break
+                       }
+                       if hasSuffix(varname, ".") {
+                               if !parser.lexer.SkipRegexp(G.res.Compile(`^<\w+>`)) {
+                                       break
+                               }
                                varname += "*"
                        }
                        parser.lexer.SkipByte(':')
@@ -389,7 +394,7 @@ func (mklines *MkLinesImpl) collectDocum
                                scope.Use(varcanon, mkline)
                        }
 
-                       if 1 < len(words) && words[1] == "Copyright" {
+                       if words[1] == "Copyright" {
                                relevant = false
                        }
 
@@ -401,37 +406,6 @@ func (mklines *MkLinesImpl) collectDocum
        finish()
 }
 
-func (mklines *MkLinesImpl) CheckRedundantAssignments() {
-       scope := NewRedundantScope()
-
-       isRelevant := func(old, new MkLine) bool {
-               if new.Op() == opAssignEval {
-                       return false
-               }
-               return true
-       }
-
-       scope.OnRedundant = func(old, new MkLine) {
-               if isRelevant(old, new) && old.Value() == new.Value() {
-                       new.Notef("Definition of %s is redundant because of %s.", old.Varname(), new.RefTo(old))
-               }
-       }
-
-       scope.OnOverwrite = func(old, new MkLine) {
-               if isRelevant(old, new) {
-                       old.Warnf("Variable %s is overwritten in %s.", new.Varname(), old.RefTo(new))
-                       G.Explain(
-                               "The variable definition in this line does not have an effect since",
-                               "it is overwritten elsewhere.",
-                               "This typically happens because of a typo (writing = instead of +=)",
-                               "or because the line that overwrites",
-                               "is in another file that is used by several packages.")
-               }
-       }
-
-       mklines.ForEach(scope.Handle)
-}
-
 // CheckForUsedComment checks that this file (a Makefile.common) has the given
 // relativeName in one of the "# used by" comments at the beginning of the file.
 func (mklines *MkLinesImpl) CheckForUsedComment(relativeName string) {
@@ -550,8 +524,8 @@ func (va *VaralignBlock) processVarassig
        if mkline.IsMultiline() {
                // Parsing the continuation marker as variable value is cheating but works well.
                text := strings.TrimSuffix(mkline.raw[0].orignl, "\n")
-               m, _, _, _, _, _, value, _, _ := MatchVarassign(text)
-               continuation = m && value == "\\"
+               m, a := MatchVarassign(text)
+               continuation = m && a.value == "\\"
        }
 
        valueAlign := mkline.ValueAlign()
Index: pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.42 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.43
--- pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.42     Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go  Sun Mar 10 19:01:50 2019
@@ -22,9 +22,9 @@ func (s *Suite) Test_VartypeCheck_AwkCom
        //  The warning should be adjusted to reflect this.
 
        vt.Output(
-               "WARN: filename:1: $0 is ambiguous. "+
+               "WARN: filename.mk:1: $0 is ambiguous. "+
                        "Use ${0} if you mean a Make variable or $$0 if you mean a shell variable.",
-               "WARN: filename:3: $0 is ambiguous. "+
+               "WARN: filename.mk:3: $0 is ambiguous. "+
                        "Use ${0} if you mean a Make variable or $$0 if you mean a shell variable.")
 }
 
@@ -42,8 +42,8 @@ func (s *Suite) Test_VartypeCheck_BasicR
                ".*\\.pl$$")
 
        vt.Output(
-               "WARN: filename:1: Internal pkglint error in MkLine.Tokenize at \"$\".",
-               "WARN: filename:3: Internal pkglint error in MkLine.Tokenize at \"$\".")
+               "WARN: filename.mk:1: Internal pkglint error in MkLine.Tokenize at \"$\".",
+               "WARN: filename.mk:3: Internal pkglint error in MkLine.Tokenize at \"$\".")
 
 }
 
@@ -58,7 +58,7 @@ func (s *Suite) Test_VartypeCheck_Buildl
                "${BUILDLINK_DEPMETHOD.kernel}")
 
        vt.Output(
-               "WARN: filename:2: Invalid dependency method \"unknown\". Valid methods are \"build\" or \"full\".")
+               "WARN: filename.mk:2: Invalid dependency method \"unknown\". Valid methods are \"build\" or \"full\".")
 }
 
 func (s *Suite) Test_VartypeCheck_Category(c *check.C) {
@@ -78,8 +78,8 @@ func (s *Suite) Test_VartypeCheck_Catego
                "wip")
 
        vt.Output(
-               "ERROR: filename:2: Invalid category \"arabic\".",
-               "ERROR: filename:4: Invalid category \"wip\".")
+               "ERROR: filename.mk:2: Invalid category \"arabic\".",
+               "ERROR: filename.mk:4: Invalid category \"wip\".")
 }
 
 func (s *Suite) Test_VartypeCheck_CFlag(c *check.C) {
@@ -103,10 +103,10 @@ func (s *Suite) Test_VartypeCheck_CFlag(
                "`pkg-config`_plus")
 
        vt.Output(
-               "WARN: filename:2: Compiler flag \"/W3\" should start with a hyphen.",
-               "WARN: filename:3: Compiler flag \"target:sparc64\" should start with a hyphen.",
-               "WARN: filename:5: Unknown compiler flag \"-XX:+PrintClassHistogramAfterFullGC\".",
-               "WARN: filename:11: Compiler flag \"`pkg-config`_plus\" should start with a hyphen.")
+               "WARN: filename.mk:2: Compiler flag \"/W3\" should start with a hyphen.",
+               "WARN: filename.mk:3: Compiler flag \"target:sparc64\" should start with a hyphen.",
+               "WARN: filename.mk:5: Unknown compiler flag \"-XX:+PrintClassHistogramAfterFullGC\".",
+               "WARN: filename.mk:11: Compiler flag \"`pkg-config`_plus\" should start with a hyphen.")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -139,19 +139,19 @@ func (s *Suite) Test_VartypeCheck_Commen
                "'SQL injection fuzzer")
 
        vt.Output(
-               "ERROR: filename:2: COMMENT must be set.",
-               "WARN: filename:3: COMMENT should not begin with \"A\".",
-               "WARN: filename:3: COMMENT should not end with a period.",
-               "WARN: filename:4: COMMENT should start with a capital letter.",
-               "WARN: filename:4: COMMENT should not be longer than 70 characters.",
-               "WARN: filename:5: COMMENT should not be enclosed in quotes.",
-               "WARN: filename:6: COMMENT should not be enclosed in quotes.",
-               "WARN: filename:7: COMMENT should not contain \"is a\".",
-               "WARN: filename:8: COMMENT should not contain \"is an\".",
-               "WARN: filename:9: COMMENT should not contain \"is a\".",
-               "WARN: filename:10: COMMENT should not start with the package name.",
-               "WARN: filename:11: COMMENT should not start with the package name.",
-               "WARN: filename:11: COMMENT should not contain \"is a\".")
+               "ERROR: filename.mk:2: COMMENT must be set.",
+               "WARN: filename.mk:3: COMMENT should not begin with \"A\".",
+               "WARN: filename.mk:3: COMMENT should not end with a period.",
+               "WARN: filename.mk:4: COMMENT should start with a capital letter.",
+               "WARN: filename.mk:4: COMMENT should not be longer than 70 characters.",
+               "WARN: filename.mk:5: COMMENT should not be enclosed in quotes.",
+               "WARN: filename.mk:6: COMMENT should not be enclosed in quotes.",
+               "WARN: filename.mk:7: COMMENT should not contain \"is a\".",
+               "WARN: filename.mk:8: COMMENT should not contain \"is an\".",
+               "WARN: filename.mk:9: COMMENT should not contain \"is a\".",
+               "WARN: filename.mk:10: COMMENT should not start with the package name.",
+               "WARN: filename.mk:11: COMMENT should not start with the package name.",
+               "WARN: filename.mk:11: COMMENT should not contain \"is a\".")
 }
 
 func (s *Suite) Test_VartypeCheck_ConfFiles(c *check.C) {
@@ -167,9 +167,9 @@ func (s *Suite) Test_VartypeCheck_ConfFi
                "share/etc/bootrc /etc/bootrc")
 
        vt.Output(
-               "WARN: filename:1: Values for CONF_FILES should always be pairs of paths.",
-               "WARN: filename:3: Values for CONF_FILES should always be pairs of paths.",
-               "WARN: filename:5: The destination file \"/etc/bootrc\" should start with a variable reference.")
+               "WARN: filename.mk:1: Values for CONF_FILES should always be pairs of paths.",
+               "WARN: filename.mk:3: Values for CONF_FILES should always be pairs of paths.",
+               "WARN: filename.mk:5: The destination file \"/etc/bootrc\" should start with a variable reference.")
 }
 
 func (s *Suite) Test_VartypeCheck_Dependency(c *check.C) {
@@ -206,21 +206,21 @@ func (s *Suite) Test_VartypeCheck_Depend
                "package-1.0>=1.0.3")
 
        vt.Output(
-               "WARN: filename:1: Invalid dependency pattern \"Perl\".",
-               "WARN: filename:3: Please use \"perl5-[0-9]*\" instead of \"perl5-*\".",
-               "WARN: filename:5: Only [0-9]* is allowed in the numeric part of a dependency.",
-               "WARN: filename:5: The version pattern \"[5.10-5.22]*\" should not contain a hyphen.",
-               "WARN: filename:6: Invalid dependency pattern \"py-docs\".",
-               "WARN: filename:10: Please use \"5.22{,nb*}\" instead of \"5.22\" as the version pattern.",
-               "WARN: filename:11: Please use \"5.*\" instead of \"5*\" as the version pattern.",
-               "WARN: filename:12: The version pattern \"2.0-[0-9]*\" should not contain a hyphen.",
-               "WARN: filename:21: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.",
-               "WARN: filename:22: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.",
-               "WARN: filename:23: Invalid dependency pattern \"package-1.0|garbage\".",
+               "WARN: filename.mk:1: Invalid dependency pattern \"Perl\".",
+               "WARN: filename.mk:3: Please use \"perl5-[0-9]*\" instead of \"perl5-*\".",
+               "WARN: filename.mk:5: Only [0-9]* is allowed in the numeric part of a dependency.",
+               "WARN: filename.mk:5: The version pattern \"[5.10-5.22]*\" should not contain a hyphen.",
+               "WARN: filename.mk:6: Invalid dependency pattern \"py-docs\".",
+               "WARN: filename.mk:10: Please use \"5.22{,nb*}\" instead of \"5.22\" as the version pattern.",
+               "WARN: filename.mk:11: Please use \"5.*\" instead of \"5*\" as the version pattern.",
+               "WARN: filename.mk:12: The version pattern \"2.0-[0-9]*\" should not contain a hyphen.",
+               "WARN: filename.mk:21: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.",
+               "WARN: filename.mk:22: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.",
+               "WARN: filename.mk:23: Invalid dependency pattern \"package-1.0|garbage\".",
                // TODO: Mention that the path should be removed.
-               "WARN: filename:25: Invalid dependency pattern \"package>=1.0:../../category/package\".",
+               "WARN: filename.mk:25: Invalid dependency pattern \"package>=1.0:../../category/package\".",
                // TODO: Mention that version numbers in a pkgbase must be appended directly, without hyphen.
-               "WARN: filename:26: Invalid dependency pattern \"package-1.0>=1.0.3\".")
+               "WARN: filename.mk:26: Invalid dependency pattern \"package-1.0>=1.0.3\".")
 }
 
 func (s *Suite) Test_VartypeCheck_DependencyWithPath(c *check.C) {
@@ -282,7 +282,7 @@ func (s *Suite) Test_VartypeCheck_DistSu
                ".tar.gz # overrides a definition from a Makefile.common")
 
        vt.Output(
-               "NOTE: filename:1: EXTRACT_SUFX is \".tar.gz\" by default, so this definition may be redundant.")
+               "NOTE: filename.mk:1: EXTRACT_SUFX is \".tar.gz\" by default, so this definition may be redundant.")
 }
 
 func (s *Suite) Test_VartypeCheck_EmulPlatform(c *check.C) {
@@ -295,12 +295,12 @@ func (s *Suite) Test_VartypeCheck_EmulPl
                "${LINUX}")
 
        vt.Output(
-               "WARN: filename:2: \"nextbsd\" is not valid for the operating system part of EMUL_PLATFORM. "+
+               "WARN: filename.mk:2: \"nextbsd\" is not valid for the operating system part of EMUL_PLATFORM. "+
                        "Use one of "+
                        "{ bitrig bsdos cygwin darwin dragonfly freebsd haiku hpux "+
                        "interix irix linux mirbsd netbsd openbsd osf1 solaris sunos "+
                        "} instead.",
-               "WARN: filename:2: \"8087\" is not valid for the hardware architecture part of EMUL_PLATFORM. "+
+               "WARN: filename.mk:2: \"8087\" is not valid for the hardware architecture part of EMUL_PLATFORM. "+
                        "Use one of { "+
                        "aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 "+
                        "cobalt coldfire convex dreamcast "+
@@ -313,7 +313,7 @@ func (s *Suite) Test_VartypeCheck_EmulPl
                        "mlrisc ns32k pc532 pmax powerpc powerpc64 rs6000 "+
                        "s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
                        "} instead.",
-               "WARN: filename:3: \"${LINUX}\" is not a valid emulation platform.")
+               "WARN: filename.mk:3: \"${LINUX}\" is not a valid emulation platform.")
 }
 
 func (s *Suite) Test_VartypeCheck_Enum(c *check.C) {
@@ -329,8 +329,8 @@ func (s *Suite) Test_VartypeCheck_Enum(c
                "[")
 
        vt.Output(
-               "WARN: filename:3: The pattern \"sun-jdk*\" cannot match any of { jdk1 jdk2 jdk4 } for JDK.",
-               "WARN: filename:5: Invalid match pattern \"[\".")
+               "WARN: filename.mk:3: The pattern \"sun-jdk*\" cannot match any of { jdk1 jdk2 jdk4 } for JDK.",
+               "WARN: filename.mk:5: Invalid match pattern \"[\".")
 }
 
 func (s *Suite) Test_VartypeCheck_Enum__use_match(c *check.C) {
@@ -385,13 +385,13 @@ func (s *Suite) Test_VartypeCheck_FetchU
                "${MASTER_SITE_INVALID:=subdir/}")
 
        vt.Output(
-               "WARN: filename:1: Please use ${MASTER_SITE_GITHUB:=example/} "+
+               "WARN: filename.mk:1: Please use ${MASTER_SITE_GITHUB:=example/} "+
                        "instead of \"https://github.com/example/project/\"; "+
                        "and run \""+confMake+" help topic=github\" for further tips.",
-               "WARN: filename:2: Please use ${MASTER_SITE_GNU:=bison} "+
+               "WARN: filename.mk:2: Please use ${MASTER_SITE_GNU:=bison} "+
                        "instead of \"http://ftp.gnu.org/pub/gnu/bison\".";,
-               "ERROR: filename:3: The subdirectory in MASTER_SITE_GNU must end with a slash.",
-               "ERROR: filename:4: The site MASTER_SITE_INVALID does not exist.")
+               "ERROR: filename.mk:3: The subdirectory in MASTER_SITE_GNU must end with a slash.",
+               "ERROR: filename.mk:4: The site MASTER_SITE_INVALID does not exist.")
 
        // PR 46570, keyword gimp-fix-ca
        vt.Values(
@@ -405,7 +405,7 @@ func (s *Suite) Test_VartypeCheck_FetchU
                "http://example.org/download?filename=<distfile>;version=<version>")
 
        vt.Output(
-               "WARN: filename:8: \"http://example.org/download?filename=<distfile>;version=<version>\" is not a valid URL.")
+               "WARN: filename.mk:8: \"http://example.org/download?filename=<distfile>;version=<version>\" is not a valid URL.")
 }
 
 func (s *Suite) Test_VartypeCheck_Filename(c *check.C) {
@@ -417,8 +417,8 @@ func (s *Suite) Test_VartypeCheck_Filena
                "OS/2-manual.txt")
 
        vt.Output(
-               "WARN: filename:1: \"Filename with spaces.docx\" is not a valid filename.",
-               "WARN: filename:2: A filename should not contain a slash.")
+               "WARN: filename.mk:1: \"Filename with spaces.docx\" is not a valid filename.",
+               "WARN: filename.mk:2: A filename should not contain a slash.")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -438,8 +438,8 @@ func (s *Suite) Test_VartypeCheck_FileMa
                "OS/2-manual.txt")
 
        vt.Output(
-               "WARN: filename:1: \"FileMask with spaces.docx\" is not a valid filename mask.",
-               "WARN: filename:2: A filename mask should not contain a slash.")
+               "WARN: filename.mk:1: \"FileMask with spaces.docx\" is not a valid filename mask.",
+               "WARN: filename.mk:2: A filename mask should not contain a slash.")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -463,9 +463,9 @@ func (s *Suite) Test_VartypeCheck_FileMo
                "")
 
        vt.Output(
-               "WARN: filename:1: Invalid file mode \"u+rwx\".",
-               "WARN: filename:4: Invalid file mode \"12345\".",
-               "WARN: filename:6: Invalid file mode \"\".")
+               "WARN: filename.mk:1: Invalid file mode \"u+rwx\".",
+               "WARN: filename.mk:4: Invalid file mode \"12345\".",
+               "WARN: filename.mk:6: Invalid file mode \"\".")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -474,7 +474,7 @@ func (s *Suite) Test_VartypeCheck_FileMo
        // There's no guarantee that a filename only contains [A-Za-z0-9.].
        // Therefore there are no useful checks in this situation.
        vt.Output(
-               "WARN: filename:11: Invalid file mode \"u+rwx\".")
+               "WARN: filename.mk:11: Invalid file mode \"u+rwx\".")
 }
 
 func (s *Suite) Test_VartypeCheck_GccReqd(c *check.C) {
@@ -491,8 +491,8 @@ func (s *Suite) Test_VartypeCheck_GccReq
                "6",
                "7.3")
        vt.Output(
-               "WARN: filename:5: GCC version numbers should only contain the major version (5).",
-               "WARN: filename:7: GCC version numbers should only contain the major version (7).")
+               "WARN: filename.mk:5: GCC version numbers should only contain the major version (5).",
+               "WARN: filename.mk:7: GCC version numbers should only contain the major version (7).")
 }
 
 func (s *Suite) Test_VartypeCheck_Homepage(c *check.C) {
@@ -506,7 +506,7 @@ func (s *Suite) Test_VartypeCheck_Homepa
                "${MASTER_SITES}")
 
        vt.Output(
-               "WARN: filename:3: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
+               "WARN: filename.mk:3: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
 
        G.Pkg = NewPackage(t.File("category/package"))
 
@@ -517,7 +517,7 @@ func (s *Suite) Test_VartypeCheck_Homepa
        // doesn't define MASTER_SITES, that variable cannot be expanded, which means
        // the warning cannot refer to its value.
        vt.Output(
-               "WARN: filename:4: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
+               "WARN: filename.mk:4: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
 
        delete(G.Pkg.vars.firstDef, "MASTER_SITES")
        delete(G.Pkg.vars.lastDef, "MASTER_SITES")
@@ -528,7 +528,7 @@ func (s *Suite) Test_VartypeCheck_Homepa
                "${MASTER_SITES}")
 
        vt.Output(
-               "WARN: filename:5: HOMEPAGE should not be defined in terms of MASTER_SITEs. " +
+               "WARN: filename.mk:5: HOMEPAGE should not be defined in terms of MASTER_SITEs. " +
                        "Use https://cdn.NetBSD.org/pub/pkgsrc/distfiles/ directly.")
 
        delete(G.Pkg.vars.firstDef, "MASTER_SITES")
@@ -542,7 +542,7 @@ func (s *Suite) Test_VartypeCheck_Homepa
        // When MASTER_SITES itself makes use of another variable, pkglint doesn't
        // resolve that variable and just outputs the simple variant of this warning.
        vt.Output(
-               "WARN: filename:6: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
+               "WARN: filename.mk:6: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
 
 }
 
@@ -559,9 +559,9 @@ func (s *Suite) Test_VartypeCheck_Identi
                "")
 
        vt.Output(
-               "WARN: filename:2: Invalid identifier \"identifiers cannot contain spaces\".",
-               "WARN: filename:3: Invalid identifier \"id/cannot/contain/slashes\".",
-               "WARN: filename:5: Invalid identifier \"\".")
+               "WARN: filename.mk:2: Invalid identifier \"identifiers cannot contain spaces\".",
+               "WARN: filename.mk:3: Invalid identifier \"id/cannot/contain/slashes\".",
+               "WARN: filename.mk:5: Invalid identifier \"\".")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -571,7 +571,7 @@ func (s *Suite) Test_VartypeCheck_Identi
                "A*B")
 
        vt.Output(
-               "WARN: filename:12: Invalid identifier pattern \"[A-Z.]\" for MYSQL_CHARSET.")
+               "WARN: filename.mk:12: Invalid identifier pattern \"[A-Z.]\" for MYSQL_CHARSET.")
 }
 
 func (s *Suite) Test_VartypeCheck_Integer(c *check.C) {
@@ -586,8 +586,8 @@ func (s *Suite) Test_VartypeCheck_Intege
                "11111111111111111111111111111111111111111111111")
 
        vt.Output(
-               "WARN: filename:1: Invalid integer \"${OTHER_VAR}\".",
-               "WARN: filename:3: Invalid integer \"-13\".")
+               "WARN: filename.mk:1: Invalid integer \"${OTHER_VAR}\".",
+               "WARN: filename.mk:3: Invalid integer \"-13\".")
 }
 
 func (s *Suite) Test_VartypeCheck_LdFlag(c *check.C) {
@@ -615,10 +615,10 @@ func (s *Suite) Test_VartypeCheck_LdFlag
                "anything")
 
        vt.Output(
-               "WARN: filename:4: Unknown linker flag \"-unknown\".",
-               "WARN: filename:5: Linker flag \"no-hyphen\" should start with a hyphen.",
-               "WARN: filename:6: Please use \"${COMPILER_RPATH_FLAG}\" instead of \"-Wl,--rpath\".",
-               "WARN: filename:12: Linker flag \"`pkg-config`_plus\" should start with a hyphen.")
+               "WARN: filename.mk:4: Unknown linker flag \"-unknown\".",
+               "WARN: filename.mk:5: Linker flag \"no-hyphen\" should start with a hyphen.",
+               "WARN: filename.mk:6: Please use \"${COMPILER_RPATH_FLAG}\" instead of \"-Wl,--rpath\".",
+               "WARN: filename.mk:12: Linker flag \"`pkg-config`_plus\" should start with a hyphen.")
 }
 
 func (s *Suite) Test_VartypeCheck_License(c *check.C) {
@@ -640,9 +640,9 @@ func (s *Suite) Test_VartypeCheck_Licens
                "${UNKNOWN_LICENSE}")
 
        vt.Output(
-               "ERROR: filename:2: Parse error for license condition \"AND mit\".",
-               "WARN: filename:3: License file ~/licenses/artistic does not exist.",
-               "ERROR: filename:4: Parse error for license condition \"${UNKNOWN_LICENSE}\".")
+               "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:4: Parse error for license condition \"${UNKNOWN_LICENSE}\".")
 
        vt.Op(opAssignAppend)
        vt.Values(
@@ -650,8 +650,8 @@ func (s *Suite) Test_VartypeCheck_Licens
                "AND mit")
 
        vt.Output(
-               "ERROR: filename:11: Parse error for appended license condition \"gnu-gpl-v2\".",
-               "WARN: filename:12: License file ~/licenses/mit does not exist.")
+               "ERROR: filename.mk:11: Parse error for appended license condition \"gnu-gpl-v2\".",
+               "WARN: filename.mk:12: License file ~/licenses/mit does not exist.")
 }
 
 func (s *Suite) Test_VartypeCheck_MachineGnuPlatform(c *check.C) {
@@ -668,18 +668,18 @@ func (s *Suite) Test_VartypeCheck_Machin
                "x86_64-pc") // Just for code coverage.
 
        vt.Output(
-               "WARN: filename:2: The pattern \"Cygwin\" cannot match any of "+
+               "WARN: filename.mk:2: The pattern \"Cygwin\" cannot match any of "+
                        "{ aarch64 aarch64_be alpha amd64 arc arm armeb armv4 armv4eb armv6 armv6eb armv7 armv7eb "+
                        "cobalt convex dreamcast hpcmips hpcsh hppa hppa64 i386 i486 ia64 m5407 m68010 m68k m88k "+
                        "mips mips64 mips64el mipseb mipsel mipsn32 mlrisc ns32k pc532 pmax powerpc powerpc64 "+
                        "rs6000 s390 sh shle sparc sparc64 vax x86_64 "+
                        "} for the hardware architecture part of MACHINE_GNU_PLATFORM.",
-               "WARN: filename:2: The pattern \"amd64\" cannot match any of "+
+               "WARN: filename.mk:2: The pattern \"amd64\" cannot match any of "+
                        "{ bitrig bsdos cygwin darwin dragonfly freebsd haiku hpux interix irix linux mirbsd "+
                        "netbsd openbsd osf1 solaris sunos } "+
                        "for the operating system part of MACHINE_GNU_PLATFORM.",
-               "WARN: filename:4: \"*-*-*-*\" is not a valid platform pattern.",
-               "WARN: filename:6: \"x86_64-pc\" is not a valid platform pattern.")
+               "WARN: filename.mk:4: \"*-*-*-*\" is not a valid platform pattern.",
+               "WARN: filename.mk:6: \"x86_64-pc\" is not a valid platform pattern.")
 }
 
 func (s *Suite) Test_VartypeCheck_MachinePlatformPattern(c *check.C) {
@@ -698,12 +698,12 @@ func (s *Suite) Test_VartypeCheck_Machin
                "NetBSD-[0-1]*-*")
 
        vt.Output(
-               "WARN: filename:1: \"linux-i386\" is not a valid platform pattern.",
-               "WARN: filename:2: The pattern \"nextbsd\" cannot match any of "+
+               "WARN: filename.mk:1: \"linux-i386\" is not a valid platform pattern.",
+               "WARN: filename.mk:2: The pattern \"nextbsd\" cannot match any of "+
                        "{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
                        "IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
                        "} for the operating system part of ONLY_FOR_PLATFORM.",
-               "WARN: filename:2: The pattern \"8087\" cannot match any of "+
+               "WARN: filename.mk:2: The pattern \"8087\" cannot match any of "+
                        "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 "+
                        "cobalt coldfire convex dreamcast "+
                        "earm earmeb earmhf earmhfeb earmv4 earmv4eb "+
@@ -714,11 +714,11 @@ func (s *Suite) Test_VartypeCheck_Machin
                        "mlrisc ns32k pc532 pmax powerpc powerpc64 "+
                        "rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
                        "} for the hardware architecture part of ONLY_FOR_PLATFORM.",
-               "WARN: filename:3: The pattern \"netbsd\" cannot match any of "+
+               "WARN: filename.mk:3: The pattern \"netbsd\" cannot match any of "+
                        "{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
                        "IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
                        "} for the operating system part of ONLY_FOR_PLATFORM.",
-               "WARN: filename:3: The pattern \"l*\" cannot match any of "+
+               "WARN: filename.mk:3: The pattern \"l*\" cannot match any of "+
                        "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 "+
                        "cobalt coldfire convex dreamcast "+
                        "earm earmeb earmhf earmhfeb earmv4 earmv4eb "+
@@ -729,8 +729,8 @@ func (s *Suite) Test_VartypeCheck_Machin
                        "mlrisc ns32k pc532 pmax powerpc powerpc64 "+
                        "rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
                        "} for the hardware architecture part of ONLY_FOR_PLATFORM.",
-               "WARN: filename:5: \"FreeBSD*\" is not a valid platform pattern.",
-               "WARN: filename:8: Please use \"[0-1].*\" instead of \"[0-1]*\" as the version pattern.")
+               "WARN: filename.mk:5: \"FreeBSD*\" is not a valid platform pattern.",
+               "WARN: filename.mk:8: Please use \"[0-1].*\" instead of \"[0-1]*\" as the version pattern.")
 }
 
 func (s *Suite) Test_VartypeCheck_MailAddress(c *check.C) {
@@ -744,10 +744,10 @@ func (s *Suite) Test_VartypeCheck_MailAd
                "user1%example.org@localhost,user2%example.org@localhost")
 
        vt.Output(
-               "WARN: filename:1: Please write \"NetBSD.org\" instead of \"netbsd.org\".",
-               "ERROR: filename:2: This mailing list address is obsolete. Use pkgsrc-users%NetBSD.org@localhost instead.",
-               "ERROR: filename:3: This mailing list address is obsolete. Use pkgsrc-users%NetBSD.org@localhost instead.",
-               "WARN: filename:4: \"user1%example.org@localhost,user2%example.org@localhost\" is not a valid mail address.")
+               "WARN: filename.mk:1: Please write \"NetBSD.org\" instead of \"netbsd.org\".",
+               "ERROR: filename.mk:2: This mailing list address is obsolete. Use pkgsrc-users%NetBSD.org@localhost instead.",
+               "ERROR: filename.mk:3: This mailing list address is obsolete. Use pkgsrc-users%NetBSD.org@localhost instead.",
+               "WARN: filename.mk:4: \"user1%example.org@localhost,user2%example.org@localhost\" is not a valid mail address.")
 }
 
 func (s *Suite) Test_VartypeCheck_Message(c *check.C) {
@@ -759,7 +759,7 @@ func (s *Suite) Test_VartypeCheck_Messag
                "Correct paths")
 
        vt.Output(
-               "WARN: filename:1: SUBST_MESSAGE.id should not be quoted.")
+               "WARN: filename.mk:1: SUBST_MESSAGE.id should not be quoted.")
 }
 
 func (s *Suite) Test_VartypeCheck_Option(c *check.C) {
@@ -777,9 +777,9 @@ func (s *Suite) Test_VartypeCheck_Option
                "UPPER")
 
        vt.Output(
-               "WARN: filename:3: Unknown option \"unknown\".",
-               "WARN: filename:4: Use of the underscore character in option names is deprecated.",
-               "ERROR: filename:5: Invalid option name \"UPPER\". "+
+               "WARN: filename.mk:3: Unknown option \"unknown\".",
+               "WARN: filename.mk:4: Use of the underscore character in option names is deprecated.",
+               "ERROR: filename.mk:5: Invalid option name \"UPPER\". "+
                        "Option names must start with a lowercase letter and be all-lowercase.")
 }
 
@@ -792,10 +792,10 @@ func (s *Suite) Test_VartypeCheck_Pathli
                "/directory with spaces")
 
        vt.Output(
-               "ERROR: filename:1: The component \".\" of PATH must be an absolute path.",
-               "ERROR: filename:1: The component \"\" of PATH must be an absolute path.",
-               "WARN: filename:1: \"${PREFIX}/!!!\" is not a valid pathname.",
-               "WARN: filename:2: \"/directory with spaces\" is not a valid pathname.")
+               "ERROR: filename.mk:1: The component \".\" of PATH must be an absolute path.",
+               "ERROR: filename.mk:1: The component \"\" of PATH must be an absolute path.",
+               "WARN: filename.mk:1: \"${PREFIX}/!!!\" is not a valid pathname.",
+               "WARN: filename.mk:2: \"/directory with spaces\" is not a valid pathname.")
 }
 
 func (s *Suite) Test_VartypeCheck_PathMask(c *check.C) {
@@ -808,7 +808,7 @@ func (s *Suite) Test_VartypeCheck_PathMa
                "src/*/*")
 
        vt.Output(
-               "WARN: filename:2: \"src/*&*\" is not a valid pathname mask.")
+               "WARN: filename.mk:2: \"src/*&*\" is not a valid pathname mask.")
 
        vt.Op(opUseMatch)
        vt.Values("any")
@@ -831,7 +831,7 @@ func (s *Suite) Test_VartypeCheck_Pathna
 
        // FIXME: Warn about the absolute pathname in line 4.
        vt.Output(
-               "WARN: filename:1: \"${PREFIX}/*\" is not a valid pathname.")
+               "WARN: filename.mk:1: \"${PREFIX}/*\" is not a valid pathname.")
 }
 
 func (s *Suite) Test_VartypeCheck_Perl5Packlist(c *check.C) {
@@ -843,7 +843,7 @@ func (s *Suite) Test_VartypeCheck_Perl5P
                "anything else")
 
        vt.Output(
-               "WARN: filename:1: PERL5_PACKLIST should not depend on other variables.")
+               "WARN: filename.mk:1: PERL5_PACKLIST should not depend on other variables.")
 }
 
 func (s *Suite) Test_VartypeCheck_Perms(c *check.C) {
@@ -860,8 +860,8 @@ func (s *Suite) Test_VartypeCheck_Perms(
                "${REAL_ROOT_GROUP}")
 
        vt.Output(
-               "ERROR: filename:2: ROOT_USER must not be used in permission definitions. Use REAL_ROOT_USER instead.",
-               "ERROR: filename:5: ROOT_GROUP must not be used in permission definitions. Use REAL_ROOT_GROUP instead.")
+               "ERROR: filename.mk:2: ROOT_USER must not be used in permission definitions. Use REAL_ROOT_USER instead.",
+               "ERROR: filename.mk:5: ROOT_GROUP must not be used in permission definitions. Use REAL_ROOT_GROUP instead.")
 }
 
 func (s *Suite) Test_VartypeCheck_Pkgname(c *check.C) {
@@ -880,7 +880,7 @@ func (s *Suite) Test_VartypeCheck_Pkgnam
                "pkgbase-3.1.4.1.5.9.2.6.5.3.5.8.9.7.9")
 
        vt.Output(
-               "WARN: filename:8: \"pkgbase-z1\" is not a valid package name.")
+               "WARN: filename.mk:8: \"pkgbase-z1\" is not a valid package name.")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -899,8 +899,8 @@ func (s *Suite) Test_VartypeCheck_PkgOpt
                "PKG_OPTS.mc")
 
        vt.Output(
-               "ERROR: filename:1: PKGBASE must not be used in PKG_OPTIONS_VAR.",
-               "ERROR: filename:3: PKG_OPTIONS_VAR must be "+
+               "ERROR: filename.mk:1: PKGBASE must not be used in PKG_OPTIONS_VAR.",
+               "ERROR: filename.mk:3: PKG_OPTIONS_VAR must be "+
                        "of the form \"PKG_OPTIONS.*\", not \"PKG_OPTS.mc\".")
 }
 
@@ -919,10 +919,10 @@ func (s *Suite) Test_VartypeCheck_PkgPat
                "../../invalid/relative")
 
        vt.Output(
-               "ERROR: filename:3: Relative path \"../../invalid\" does not exist.",
-               "WARN: filename:3: \"../../invalid\" is not a valid relative package directory.",
-               "ERROR: filename:4: Relative path \"../../../../invalid/relative\" does not exist.",
-               "WARN: filename:4: \"../../../../invalid/relative\" is not a valid relative package directory.")
+               "ERROR: filename.mk:3: Relative path \"../../invalid\" does not exist.",
+               "WARN: filename.mk:3: \"../../invalid\" is not a valid relative package directory.",
+               "ERROR: filename.mk:4: Relative path \"../../../../invalid/relative\" does not exist.",
+               "WARN: filename.mk:4: \"../../../../invalid/relative\" is not a valid relative package directory.")
 }
 
 func (s *Suite) Test_VartypeCheck_PkgRevision(c *check.C) {
@@ -933,8 +933,8 @@ func (s *Suite) Test_VartypeCheck_PkgRev
                "3a")
 
        vt.Output(
-               "WARN: filename:1: PKGREVISION must be a positive integer number.",
-               "ERROR: filename:1: PKGREVISION only makes sense directly in the package Makefile.")
+               "WARN: filename.mk:1: PKGREVISION must be a positive integer number.",
+               "ERROR: filename.mk:1: PKGREVISION only makes sense directly in the package Makefile.")
 
        vt.File("Makefile")
        vt.Values(
@@ -953,8 +953,8 @@ func (s *Suite) Test_VartypeCheck_Python
                "cairo,X")
 
        vt.Output(
-               "WARN: filename:2: Python dependencies should not contain variables.",
-               "WARN: filename:3: Invalid Python dependency \"cairo,X\".")
+               "WARN: filename.mk:2: Python dependencies should not contain variables.",
+               "WARN: filename.mk:3: Invalid Python dependency \"cairo,X\".")
 }
 
 func (s *Suite) Test_VartypeCheck_PrefixPathname(c *check.C) {
@@ -966,7 +966,7 @@ func (s *Suite) Test_VartypeCheck_Prefix
                "share/locale")
 
        vt.Output(
-               "WARN: filename:1: Please use \"${PKGMANDIR}/man1\" instead of \"man/man1\".")
+               "WARN: filename.mk:1: Please use \"${PKGMANDIR}/man1\" instead of \"man/man1\".")
 }
 
 func (s *Suite) Test_VartypeCheck_RelativePkgPath(c *check.C) {
@@ -985,9 +985,9 @@ func (s *Suite) Test_VartypeCheck_Relati
                "../../invalid/relative")
 
        vt.Output(
-               "ERROR: filename:1: Relative path \"category/other-package\" does not exist.",
-               "ERROR: filename:4: Relative path \"invalid\" does not exist.",
-               "ERROR: filename:5: Relative path \"../../invalid/relative\" does not exist.")
+               "ERROR: filename.mk:1: Relative path \"category/other-package\" does not exist.",
+               "ERROR: filename.mk:4: Relative path \"invalid\" does not exist.",
+               "ERROR: filename.mk:5: Relative path \"../../invalid/relative\" does not exist.")
 }
 
 func (s *Suite) Test_VartypeCheck_Restricted(c *check.C) {
@@ -998,7 +998,7 @@ func (s *Suite) Test_VartypeCheck_Restri
                "May only be distributed free of charge")
 
        vt.Output(
-               "WARN: filename:1: The only valid value for NO_BIN_ON_CDROM is ${RESTRICTED}.")
+               "WARN: filename.mk:1: The only valid value for NO_BIN_ON_CDROM is ${RESTRICTED}.")
 }
 
 func (s *Suite) Test_VartypeCheck_SedCommands(c *check.C) {
@@ -1019,15 +1019,15 @@ func (s *Suite) Test_VartypeCheck_SedCom
                "-e s,$${unclosedShellVar") // Just for code coverage.
 
        vt.Output(
-               "NOTE: filename:1: Please always use \"-e\" in sed commands, even if there is only one substitution.",
-               "NOTE: filename:2: Each sed command should appear in an assignment of its own.",
-               "WARN: filename:3: The # character starts a Makefile comment.",
-               "ERROR: filename:3: Invalid shell words \"\\\"s,\" in sed commands.",
-               "WARN: filename:8: Unknown sed command \"1d\".",
-               "ERROR: filename:9: The -e option to sed requires an argument.",
-               "WARN: filename:10: Unknown sed command \"-i\".",
-               "NOTE: filename:10: Please always use \"-e\" in sed commands, even if there is only one substitution.",
-               "WARN: filename:11: Unclosed shell variable starting at \"$${unclosedShellVar\".")
+               "NOTE: filename.mk:1: Please always use \"-e\" in sed commands, even if there is only one substitution.",
+               "NOTE: filename.mk:2: Each sed command should appear in an assignment of its own.",
+               "WARN: filename.mk:3: The # character starts a Makefile comment.",
+               "ERROR: filename.mk:3: Invalid shell words \"\\\"s,\" in sed commands.",
+               "WARN: filename.mk:8: Unknown sed command \"1d\".",
+               "ERROR: filename.mk:9: The -e option to sed requires an argument.",
+               "WARN: filename.mk:10: Unknown sed command \"-i\".",
+               "NOTE: filename.mk:10: Please always use \"-e\" in sed commands, even if there is only one substitution.",
+               "WARN: filename.mk:11: Unclosed shell variable starting at \"$${unclosedShellVar\".")
 }
 
 func (s *Suite) Test_VartypeCheck_ShellCommand(c *check.C) {
@@ -1057,7 +1057,7 @@ func (s *Suite) Test_VartypeCheck_ShellC
                "echo bin/program;")
 
        vt.Output(
-               "WARN: filename:1: This shell command list should end with a semicolon.")
+               "WARN: filename.mk:1: This shell command list should end with a semicolon.")
 }
 
 func (s *Suite) Test_VartypeCheck_Stage(c *check.C) {
@@ -1070,7 +1070,7 @@ func (s *Suite) Test_VartypeCheck_Stage(
                "pre-test")
 
        vt.Output(
-               "WARN: filename:2: Invalid stage name \"post-modern\". " +
+               "WARN: filename.mk:2: Invalid stage name \"post-modern\". " +
                        "Use one of {pre,do,post}-{extract,patch,configure,build,test,install}.")
 }
 
@@ -1092,10 +1092,10 @@ func (s *Suite) Test_VartypeCheck_Tool(c
                "unknown")
 
        vt.Output(
-               "ERROR: filename:2: Invalid tool dependency \"unknown\". "+
+               "ERROR: filename.mk:2: Invalid tool dependency \"unknown\". "+
                        "Use one of \"bootstrap\", \"build\", \"pkgsrc\", \"run\" or \"test\".",
-               "ERROR: filename:4: Invalid tool dependency \"mal:formed:tool\".",
-               "ERROR: filename:5: Unknown tool \"unknown\".")
+               "ERROR: filename.mk:4: Invalid tool dependency \"mal:formed:tool\".",
+               "ERROR: filename.mk:5: Unknown tool \"unknown\".")
 
        vt.Varname("USE_TOOLS.NetBSD")
        vt.Op(opAssignAppend)
@@ -1104,7 +1104,7 @@ func (s *Suite) Test_VartypeCheck_Tool(c
                "tool2:unknown")
 
        vt.Output(
-               "ERROR: filename:12: Invalid tool dependency \"unknown\". " +
+               "ERROR: filename.mk:12: Invalid tool dependency \"unknown\". " +
                        "Use one of \"bootstrap\", \"build\", \"pkgsrc\", \"run\" or \"test\".")
 
        vt.Varname("TOOLS_NOOP")
@@ -1118,7 +1118,7 @@ func (s *Suite) Test_VartypeCheck_Tool(c
                "gmake:run")
 
        vt.Output(
-               "ERROR: filename:31: Unknown tool \"gmake\".")
+               "ERROR: filename.mk:31: Unknown tool \"gmake\".")
 
        vt.Varname("USE_TOOLS")
        vt.Op(opUseMatch)
@@ -1156,17 +1156,17 @@ func (s *Suite) Test_VartypeCheck_URL(c 
                "string with spaces")
 
        vt.Output(
-               "WARN: filename:4: Please write NetBSD.org instead of www.netbsd.org.",
-               "NOTE: filename:5: For consistency, please add a trailing slash to \"https://www.example.org\".";,
-               "WARN: filename:8: \"\" is not a valid URL.",
-               "WARN: filename:9: \"ftp://example.org/<\" is not a valid URL.",
-               "WARN: filename:10: \"gopher://example.org/<\" is not a valid URL.",
-               "WARN: filename:11: \"http://example.org/<\" is not a valid URL.",
-               "WARN: filename:12: \"https://example.org/<\" is not a valid URL.",
-               "WARN: filename:13: \"https://www.example.org/path with spaces\" is not a valid URL.",
-               "WARN: filename:14: \"httpxs://www.example.org\" is not a valid URL. Only ftp, gopher, http, and https URLs are allowed here.",
-               "WARN: filename:15: \"mailto:someone%example.org@localhost\"; is not a valid URL.",
-               "WARN: filename:16: \"string with spaces\" is not a valid URL.")
+               "WARN: filename.mk:4: Please write NetBSD.org instead of www.netbsd.org.",
+               "NOTE: filename.mk:5: For consistency, please add a trailing slash to \"https://www.example.org\".";,
+               "WARN: filename.mk:8: \"\" is not a valid URL.",
+               "WARN: filename.mk:9: \"ftp://example.org/<\" is not a valid URL.",
+               "WARN: filename.mk:10: \"gopher://example.org/<\" is not a valid URL.",
+               "WARN: filename.mk:11: \"http://example.org/<\" is not a valid URL.",
+               "WARN: filename.mk:12: \"https://example.org/<\" is not a valid URL.",
+               "WARN: filename.mk:13: \"https://www.example.org/path with spaces\" is not a valid URL.",
+               "WARN: filename.mk:14: \"httpxs://www.example.org\" is not a valid URL. Only ftp, gopher, http, and https URLs are allowed here.",
+               "WARN: filename.mk:15: \"mailto:someone%example.org@localhost\"; is not a valid URL.",
+               "WARN: filename.mk:16: \"string with spaces\" is not a valid URL.")
 
        // Yes, even in 2019, some pkgsrc-wip packages really use a gopher HOMEPAGE.
        vt.Values(
@@ -1187,8 +1187,8 @@ func (s *Suite) Test_VartypeCheck_UserGr
                "${OTHER_VAR}")
 
        vt.Output(
-               "WARN: filename:1: Invalid user or group name \"user with spaces\".",
-               "WARN: filename:4: Invalid user or group name \"domain\\\\user\".")
+               "WARN: filename.mk:1: Invalid user or group name \"user with spaces\".",
+               "WARN: filename.mk:4: Invalid user or group name \"domain\\\\user\".")
 }
 
 func (s *Suite) Test_VartypeCheck_VariableName(c *check.C) {
@@ -1202,7 +1202,7 @@ func (s *Suite) Test_VartypeCheck_Variab
                "${INDIRECT}")
 
        vt.Output(
-               "WARN: filename:2: \"VarBase\" is not a valid variable name.")
+               "WARN: filename.mk:2: \"VarBase\" is not a valid variable name.")
 }
 
 func (s *Suite) Test_VartypeCheck_Version(c *check.C) {
@@ -1218,7 +1218,7 @@ func (s *Suite) Test_VartypeCheck_Versio
                "4pre7",
                "${VER}")
        vt.Output(
-               "WARN: filename:4: Invalid version number \"4.1-SNAPSHOT\".")
+               "WARN: filename.mk:4: Invalid version number \"4.1-SNAPSHOT\".")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -1230,10 +1230,10 @@ func (s *Suite) Test_VartypeCheck_Versio
                "1.[2-7].*",
                "[0-9]*")
        vt.Output(
-               "WARN: filename:11: Invalid version number pattern \"a*\".",
-               "WARN: filename:12: Invalid version number pattern \"1.2/456\".",
-               "WARN: filename:13: Please use \"4.*\" instead of \"4*\" as the version pattern.",
-               "WARN: filename:15: Please use \"1.[234].*\" instead of \"1.[234]*\" as the version pattern.")
+               "WARN: filename.mk:11: Invalid version number pattern \"a*\".",
+               "WARN: filename.mk:12: Invalid version number pattern \"1.2/456\".",
+               "WARN: filename.mk:13: Please use \"4.*\" instead of \"4*\" as the version pattern.",
+               "WARN: filename.mk:15: Please use \"1.[234].*\" instead of \"1.[234]*\" as the version pattern.")
 }
 
 func (s *Suite) Test_VartypeCheck_WrapperReorder(c *check.C) {
@@ -1246,8 +1246,8 @@ func (s *Suite) Test_VartypeCheck_Wrappe
                "reorder:l:first",
                "omit:first")
        vt.Output(
-               "WARN: filename:2: Unknown wrapper reorder command \"reorder:l:first\".",
-               "WARN: filename:3: Unknown wrapper reorder command \"omit:first\".")
+               "WARN: filename.mk:2: Unknown wrapper reorder command \"reorder:l:first\".",
+               "WARN: filename.mk:3: Unknown wrapper reorder command \"omit:first\".")
 }
 
 func (s *Suite) Test_VartypeCheck_WrapperTransform(c *check.C) {
@@ -1266,8 +1266,8 @@ func (s *Suite) Test_VartypeCheck_Wrappe
                "unknown",
                "-e 's,-Wall,-Wall -Wextra,'")
        vt.Output(
-               "WARN: filename:7: Unknown wrapper transform command \"rpath:/usr/lib\".",
-               "WARN: filename:8: Unknown wrapper transform command \"unknown\".")
+               "WARN: filename.mk:7: Unknown wrapper transform command \"rpath:/usr/lib\".",
+               "WARN: filename.mk:8: Unknown wrapper transform command \"unknown\".")
 }
 
 func (s *Suite) Test_VartypeCheck_WrksrcSubdirectory(c *check.C) {
@@ -1287,14 +1287,14 @@ func (s *Suite) Test_VartypeCheck_Wrksrc
                "${WRKDIR}/sub",
                "${SRCDIR}/sub")
        vt.Output(
-               "NOTE: filename:1: You can use \".\" instead of \"${WRKSRC}\".",
-               "NOTE: filename:2: You can use \".\" instead of \"${WRKSRC}/\".",
-               "NOTE: filename:3: You can use \".\" instead of \"${WRKSRC}/.\".",
-               "NOTE: filename:4: You can use \"subdir\" instead of \"${WRKSRC}/subdir\".",
-               "NOTE: filename:6: You can use \"directory\" instead of \"${WRKSRC}/directory\".",
-               "WARN: filename:8: \"../other\" is not a valid subdirectory of ${WRKSRC}.",
-               "WARN: filename:9: \"${WRKDIR}/sub\" is not a valid subdirectory of ${WRKSRC}.",
-               "WARN: filename:10: \"${SRCDIR}/sub\" is not a valid subdirectory of ${WRKSRC}.")
+               "NOTE: filename.mk:1: You can use \".\" instead of \"${WRKSRC}\".",
+               "NOTE: filename.mk:2: You can use \".\" instead of \"${WRKSRC}/\".",
+               "NOTE: filename.mk:3: You can use \".\" instead of \"${WRKSRC}/.\".",
+               "NOTE: filename.mk:4: You can use \"subdir\" instead of \"${WRKSRC}/subdir\".",
+               "NOTE: filename.mk:6: You can use \"directory\" instead of \"${WRKSRC}/directory\".",
+               "WARN: filename.mk:8: \"../other\" is not a valid subdirectory of ${WRKSRC}.",
+               "WARN: filename.mk:9: \"${WRKDIR}/sub\" is not a valid subdirectory of ${WRKSRC}.",
+               "WARN: filename.mk:10: \"${SRCDIR}/sub\" is not a valid subdirectory of ${WRKSRC}.")
 }
 
 func (s *Suite) Test_VartypeCheck_Yes(c *check.C) {
@@ -1307,8 +1307,8 @@ func (s *Suite) Test_VartypeCheck_Yes(c 
                "${YESVAR}")
 
        vt.Output(
-               "WARN: filename:2: APACHE_MODULE should be set to YES or yes.",
-               "WARN: filename:3: APACHE_MODULE should be set to YES or yes.")
+               "WARN: filename.mk:2: APACHE_MODULE should be set to YES or yes.",
+               "WARN: filename.mk:3: APACHE_MODULE should be set to YES or yes.")
 
        vt.Varname("PKG_DEVELOPER")
        vt.Op(opUseMatch)
@@ -1318,9 +1318,9 @@ func (s *Suite) Test_VartypeCheck_Yes(c 
                "${YESVAR}")
 
        vt.Output(
-               "WARN: filename:11: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.",
-               "WARN: filename:12: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.",
-               "WARN: filename:13: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.")
+               "WARN: filename.mk:11: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.",
+               "WARN: filename.mk:12: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.",
+               "WARN: filename.mk:13: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.")
 }
 
 func (s *Suite) Test_VartypeCheck_YesNo(c *check.C) {
@@ -1334,8 +1334,8 @@ func (s *Suite) Test_VartypeCheck_YesNo(
                "${YESVAR}")
 
        vt.Output(
-               "WARN: filename:3: GNU_CONFIGURE should be set to YES, yes, NO, or no.",
-               "WARN: filename:4: GNU_CONFIGURE should be set to YES, yes, NO, or no.")
+               "WARN: filename.mk:3: GNU_CONFIGURE should be set to YES, yes, NO, or no.",
+               "WARN: filename.mk:4: GNU_CONFIGURE should be set to YES, yes, NO, or no.")
 }
 
 func (s *Suite) Test_VartypeCheck_YesNoIndirectly(c *check.C) {
@@ -1349,7 +1349,7 @@ func (s *Suite) Test_VartypeCheck_YesNoI
                "${YESVAR}")
 
        vt.Output(
-               "WARN: filename:3: GNU_CONFIGURE should be set to YES, yes, NO, or no.")
+               "WARN: filename.mk:3: GNU_CONFIGURE should be set to YES, yes, NO, or no.")
 }
 
 // VartypeCheckTester helps to test the many different checks in VartypeCheck.
@@ -1375,7 +1375,7 @@ func NewVartypeCheckTester(t *Tester, ch
        return &VartypeCheckTester{
                t,
                checker,
-               "filename",
+               "filename.mk",
                1,
                "",
                opAssign}

Index: pkgsrc/pkgtools/pkglint/files/mklines_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.37 pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.38
--- pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.37  Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines_test.go       Sun Mar 10 19:01:50 2019
@@ -335,12 +335,7 @@ func (s *Suite) Test_MkLines_collectDefi
        // The tools autoreconf and autoheader213 are known at this point because of the USE_TOOLS line.
        // The SUV variable is used implicitly by the SUBST framework, therefore no warning.
        // The OSV.NetBSD variable is used implicitly via the OSV variable, therefore no warning.
-       t.CheckOutputLines(
-               // FIXME: the below warning is wrong; it's ok to have SUBST blocks in all files,
-               //  maybe except buildlink3.mk.
-               "WARN: determine-defined-variables.mk:12: The variable SUBST_VARS.subst may not be set " +
-                       "(only given a default value, or appended to) in this file; " +
-                       "it would be ok in Makefile, Makefile.common or options.mk.")
+       t.CheckOutputEmpty()
 }
 
 func (s *Suite) Test_MkLines_collectDefinedVariables__BUILTIN_FIND_FILES_VAR(c *check.C) {
@@ -371,7 +366,7 @@ func (s *Suite) Test_MkLines_collectDefi
 func (s *Suite) Test_MkLines_collectUsedVariables__simple(c *check.C) {
        t := s.Init(c)
 
-       mklines := t.NewMkLines("filename",
+       mklines := t.NewMkLines("filename.mk",
                "\t${VAR}")
        mkline := mklines.mklines[0]
        G.Mk = mklines
@@ -410,7 +405,7 @@ func (s *Suite) Test_MkLines__private_to
        t := s.Init(c)
 
        t.SetUpVartypes()
-       mklines := t.NewMkLines("filename",
+       mklines := t.NewMkLines("filename.mk",
                MkRcsID,
                "",
                "\tmd5sum filename")
@@ -418,14 +413,14 @@ func (s *Suite) Test_MkLines__private_to
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: filename:3: Unknown shell command \"md5sum\".")
+               "WARN: filename.mk:3: Unknown shell command \"md5sum\".")
 }
 
 func (s *Suite) Test_MkLines__private_tool_defined(c *check.C) {
        t := s.Init(c)
 
        t.SetUpVartypes()
-       mklines := t.NewMkLines("filename",
+       mklines := t.NewMkLines("filename.mk",
                MkRcsID,
                "TOOLS_CREATE+=\tmd5sum",
                "",
@@ -435,7 +430,7 @@ func (s *Suite) Test_MkLines__private_to
 
        // TODO: Is it necessary to add the tool to USE_TOOLS? If not, why not?
        t.CheckOutputLines(
-               "WARN: filename:4: The \"md5sum\" tool is used but not added to USE_TOOLS.")
+               "WARN: filename.mk:4: The \"md5sum\" tool is used but not added to USE_TOOLS.")
 }
 
 func (s *Suite) Test_MkLines_Check__indentation(c *check.C) {
@@ -644,6 +639,10 @@ func (s *Suite) Test_MkLines_collectDocu
                "# VARIABLE",
                "#\tA paragraph of a single line is not enough to be recognized as \"relevant\".",
                "",
+               "# PARAGRAPH",
+               "#\tA paragraph may end in a",
+               "#\tPARA_END_VARNAME.",
+               "",
                "# VARBASE1.<param1>",
                "# VARBASE2.*",
                "# VARBASE3.${id}")
@@ -659,11 +658,12 @@ func (s *Suite) Test_MkLines_collectDocu
        sort.Strings(varnames)
 
        expected := []string{
+               "PARAGRAPH (line 23)",
                "PKG_DEBUG_LEVEL (line 11)",
                "PKG_VERBOSE (line 16)",
-               "VARBASE1.* (line 23)",
-               "VARBASE2.* (line 24)",
-               "VARBASE3.* (line 25)"}
+               "VARBASE1.* (line 27)",
+               "VARBASE2.* (line 28)",
+               "VARBASE3.* (line 29)"}
        c.Check(varnames, deepEquals, expected)
 }
 
@@ -704,237 +704,6 @@ func (s *Suite) Test_MkLines__unknown_op
                "WARN: options.mk:4: Unknown option \"unknown\".")
 }
 
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__override_in_mk(c *check.C) {
-       t := s.Init(c)
-       included := t.NewMkLines("included.mk",
-               "OVERRIDE=\tprevious value",
-               "REDUNDANT=\tredundant")
-       including := t.NewMkLines("including.mk",
-               ".include \"included.mk\"",
-               "OVERRIDE=\toverridden value",
-               "REDUNDANT=\tredundant")
-
-       var allLines []Line
-       allLines = append(allLines, including.lines.Lines[:1]...)
-       allLines = append(allLines, included.lines.Lines...)
-       allLines = append(allLines, including.lines.Lines[1:]...)
-       mklines := NewMkLines(NewLines(included.lines.FileName, allLines))
-
-       // XXX: The warnings from here are not in the same order as the other warnings.
-       // XXX: There may be some warnings for the same file separated by warnings for other files.
-       mklines.CheckRedundantAssignments()
-
-       t.CheckOutputLines(
-               "NOTE: including.mk:3: Definition of REDUNDANT is redundant because of included.mk:2.")
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__override_in_Makefile(c *check.C) {
-       t := s.Init(c)
-       included := t.NewMkLines("module.mk",
-               "VAR=\tvalue ${OTHER}",
-               "VAR?=\tvalue ${OTHER}",
-               "VAR=\tnew value")
-       including := t.NewMkLines("Makefile",
-               ".include \"module.mk\"",
-               "VAR=\tthe package may overwrite variables from other files")
-
-       var allLines []Line
-       allLines = append(allLines, including.lines.Lines[:1]...)
-       allLines = append(allLines, included.lines.Lines...)
-       allLines = append(allLines, including.lines.Lines[1:]...)
-       mklines := NewMkLines(NewLines(including.lines.FileName, allLines))
-
-       // XXX: The warnings from here are not in the same order as the other warnings.
-       // XXX: There may be some warnings for the same file separated by warnings for other files.
-       mklines.CheckRedundantAssignments()
-
-       // No warning for VAR=... in Makefile since it makes sense to have common files
-       // with default values for variables, overriding some of them in each package.
-       t.CheckOutputLines(
-               "NOTE: module.mk:2: Definition of VAR is redundant because of line 1.",
-               "WARN: module.mk:1: Variable VAR is overwritten in line 3.")
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__default_value_definitely_unused(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "VAR=\tvalue ${OTHER}",
-               "VAR?=\tdifferent value")
-
-       mklines.CheckRedundantAssignments()
-
-       // FIXME: A default assignment after an unconditional assignment is redundant.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__default_value_overridden(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "VAR?=\tdefault value",
-               "VAR=\toverridden value")
-
-       mklines.CheckRedundantAssignments()
-
-       t.CheckOutputLines(
-               "WARN: module.mk:1: Variable VAR is overwritten in line 2.")
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__overwrite_same_value(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "VAR=\tvalue ${OTHER}",
-               "VAR=\tvalue ${OTHER}")
-
-       mklines.CheckRedundantAssignments()
-
-       t.CheckOutputLines(
-               "NOTE: module.mk:2: Definition of VAR is redundant because of line 1.")
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__conditional_overwrite(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "VAR=\tdefault",
-               ".if ${OPSYS} == NetBSD",
-               "VAR=\topsys",
-               ".endif")
-
-       mklines.CheckRedundantAssignments()
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__conditional_default(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "VAR=\tdefault",
-               ".if ${OPSYS} == NetBSD",
-               "VAR?=\topsys",
-               ".endif")
-
-       mklines.CheckRedundantAssignments()
-
-       // TODO: WARN: module.mk:3: The value \"opsys\" will never be assigned to VAR because it is defined unconditionally in line 1.
-       t.CheckOutputEmpty()
-}
-
-// These warnings are precise and accurate since the value of VAR is not used between line 2 and 4.
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__overwrite_same_variable_different_value(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "OTHER=\tvalue before",
-               "VAR=\tvalue ${OTHER}",
-               "OTHER=\tvalue after",
-               "VAR=\tvalue ${OTHER}")
-
-       mklines.CheckRedundantAssignments()
-
-       t.CheckOutputLines(
-               "WARN: module.mk:1: Variable OTHER is overwritten in line 3.",
-               "NOTE: module.mk:4: Definition of VAR is redundant because of line 2.")
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__overwrite_different_value_used_between(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "OTHER=\tvalue before",
-               "VAR=\tvalue ${OTHER}",
-
-               // VAR is used here at load time, therefore it must be defined at this point.
-               // At this point, VAR uses the \"before\" value of OTHER.
-               "RESULT1:=\t${VAR}",
-
-               "OTHER=\tvalue after",
-
-               // VAR is used here again at load time, this time using the \"after\" value of OTHER.
-               "RESULT2:=\t${VAR}",
-
-               // Still this definition is redundant.
-               "VAR=\tvalue ${OTHER}")
-
-       mklines.CheckRedundantAssignments()
-
-       t.CheckOutputLines(
-               "WARN: module.mk:1: Variable OTHER is overwritten in line 4.",
-               "NOTE: module.mk:6: Definition of VAR is redundant because of line 2.")
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__procedure_call(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("mk/pthread.buildlink3.mk",
-               "CHECK_BUILTIN.pthread:=\tyes",
-               ".include \"../../mk/pthread.builtin.mk\"",
-               "CHECK_BUILTIN.pthread:=\tno")
-
-       mklines.CheckRedundantAssignments()
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__shell_and_eval(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "VAR:=\tvalue ${OTHER}",
-               "VAR!=\tvalue ${OTHER}")
-
-       mklines.CheckRedundantAssignments()
-
-       // As of November 2018, pkglint doesn't check redundancies that involve the := or != operators.
-       //
-       // What happens here is:
-       //
-       // Line 1 evaluates OTHER at load time.
-       // Line 1 assigns its value to VAR.
-       // Line 2 evaluates OTHER at load time.
-       // Line 2 passes its value through the shell and assigns the result to VAR.
-       //
-       // Since VAR is defined in line 1, not used afterwards and overwritten in line 2, it is redundant.
-       // Well, not quite, because evaluating ${OTHER} might have side-effects from :sh or ::= modifiers,
-       // but these are so rare that they are frowned upon and are not considered by pkglint.
-       //
-       // Expected result:
-       // WARN: module.mk:2: Previous definition of VAR in line 1 is unused.
-
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__shell_and_eval_literal(c *check.C) {
-       t := s.Init(c)
-       mklines := t.NewMkLines("module.mk",
-               "VAR:=\tvalue",
-               "VAR!=\tvalue")
-
-       mklines.CheckRedundantAssignments()
-
-       // Even when := is used with a literal value (which is usually
-       // only done for procedure calls), the shell evaluation can have
-       // so many different side effects that pkglint cannot reliably
-       // help in this situation.
-       //
-       // TODO: Why not? The evaluation in line 1 is trivial to analyze.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_MkLines_CheckRedundantAssignments__included_OPSYS_variable(c *check.C) {
-       t := s.Init(c)
-
-       t.SetUpPackage("category/package",
-               ".include \"../../category/dependency/buildlink3.mk\"",
-               "CONFIGURE_ARGS+=\tone",
-               "CONFIGURE_ARGS=\ttwo",
-               "CONFIGURE_ARGS+=\tthree")
-       t.SetUpPackage("category/dependency")
-       t.CreateFileDummyBuildlink3("category/dependency/buildlink3.mk")
-       t.CreateFileLines("category/dependency/builtin.mk",
-               MkRcsID,
-               "CONFIGURE_ARGS.Darwin+=\tdarwin")
-
-       G.Check(t.File("category/package"))
-
-       t.CheckOutputLines(
-               "WARN: ~/category/package/Makefile:21: Variable CONFIGURE_ARGS is overwritten in line 22.")
-}
-
 func (s *Suite) Test_MkLines_Check__PLIST_VARS(c *check.C) {
        t := s.Init(c)
 
@@ -1101,7 +870,8 @@ func (s *Suite) Test_MkLines_Check__hack
        mklines.Check()
 
        // No warning about including bsd.prefs.mk before using the ?= operator.
-       // FIXME: Why not?
+       // This is because the hacks.mk files are included implicitly by the
+       // pkgsrc infrastructure right after bsd.prefs.mk.
        t.CheckOutputEmpty()
 }
 
@@ -1167,8 +937,8 @@ func (s *Suite) Test_MkLines_Check__extr
        G.Pkg = NewPackage(t.File("category/pkgbase"))
        G.Mk = t.NewMkLines("options.mk",
                MkRcsID,
+               "",
                ".for word in ${PKG_FAIL_REASON}",
-               "PYTHON_VERSIONS_ACCEPTED=\t27 35 30",
                "CONFIGURE_ARGS+=\t--sharedir=${PREFIX}/share/kde",
                "COMMENT=\t# defined",
                ".endfor",
@@ -1181,7 +951,6 @@ func (s *Suite) Test_MkLines_Check__extr
        G.Mk.Check()
 
        t.CheckOutputLines(
-               "WARN: options.mk:3: The values for PYTHON_VERSIONS_ACCEPTED should be in decreasing order.",
                "NOTE: options.mk:5: Please use \"# empty\", \"# none\" or \"# yes\" instead of \"# defined\".",
                "WARN: options.mk:7: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".",
                "WARN: options.mk:11: Building the package should take place entirely inside ${WRKSRC}, not \"${WRKSRC}/..\".",
Index: pkgsrc/pkgtools/pkglint/files/plist.go
diff -u pkgsrc/pkgtools/pkglint/files/plist.go:1.37 pkgsrc/pkgtools/pkglint/files/plist.go:1.38
--- pkgsrc/pkgtools/pkglint/files/plist.go:1.37 Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/plist.go      Sun Mar 10 19:01:50 2019
@@ -344,7 +344,7 @@ func (ck *PlistChecker) checkPathShare(p
        case hasPrefix(text, "share/icons/") && G.Pkg != nil:
                if hasPrefix(text, "share/icons/hicolor/") && G.Pkg.Pkgpath != "graphics/hicolor-icon-theme" {
                        f := "../../graphics/hicolor-icon-theme/buildlink3.mk"
-                       if G.Pkg.included[f] == nil && ck.once.FirstTime("hicolor-icon-theme") {
+                       if !G.Pkg.included.Seen(f) && ck.once.FirstTime("hicolor-icon-theme") {
                                pline.Errorf("Packages that install hicolor icons must include %q in the Makefile.", f)
                        }
                }
@@ -359,7 +359,7 @@ func (ck *PlistChecker) checkPathShare(p
 
                if hasPrefix(text, "share/icons/gnome") && G.Pkg.Pkgpath != "graphics/gnome-icon-theme" {
                        f := "../../graphics/gnome-icon-theme/buildlink3.mk"
-                       if G.Pkg.included[f] == nil {
+                       if !G.Pkg.included.Seen(f) {
                                pline.Errorf("The package Makefile must include %q.", f)
                                G.Explain(
                                        "Packages that install GNOME icons must maintain the icon theme",

Index: pkgsrc/pkgtools/pkglint/files/mkparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser.go:1.24 pkgsrc/pkgtools/pkglint/files/mkparser.go:1.25
--- pkgsrc/pkgtools/pkglint/files/mkparser.go:1.24      Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/mkparser.go   Sun Mar 10 19:01:50 2019
@@ -27,6 +27,7 @@ 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, emitWarnings bool) *MkParser {
        G.Assertf((line != nil) == emitWarnings, "line must be given iff emitWarnings is set")
        return &MkParser{line, textproc.NewLexer(text), emitWarnings}
@@ -120,13 +121,7 @@ func (p *MkParser) VarUse() *MkVarUse {
                }
 
                lexer.Reset(mark)
-       }
-
-       if lexer.SkipByte('@') {
-               return &MkVarUse{"@", nil}
-       }
-       if lexer.SkipByte('<') {
-               return &MkVarUse{"<", nil}
+               return nil
        }
 
        varname := lexer.NextByteSet(textproc.AlnumU)
@@ -152,6 +147,26 @@ func (p *MkParser) VarUse() *MkVarUse {
                return &MkVarUse{sprintf("%c", varname), nil}
        }
 
+       if !lexer.EOF() {
+               symbol := lexer.Rest()[:1]
+               switch symbol {
+               case "$":
+                       // This is an escaped dollar character and not a variable use.
+
+               case "@", "<", " ":
+                       // These variable names are known to exist.
+                       //
+                       // Many others are also possible but not used in practice.
+                       // In particular, when parsing the :C or :S modifier,
+                       // the $ must not be interpreted as a variable name,
+                       // even when it looks like $/ could refer to the "/" variable.
+                       //
+                       // TODO: Find out whether $" is a variable use when it appears in the :M modifier.
+                       lexer.Skip(1)
+                       return &MkVarUse{symbol, nil}
+               }
+       }
+
        lexer.Reset(mark)
        return nil
 }
@@ -466,6 +481,29 @@ func (p *MkParser) mkCondAtom() MkCond {
                                        }
                                }
                                lexer.Reset(mark)
+
+                               lexer.Skip(1)
+                               var rhsText strings.Builder
+                       loop:
+                               for {
+                                       m := lexer.Mark()
+                                       switch {
+                                       case p.VarUse() != nil,
+                                               lexer.NextBytesSet(textproc.Alnum) != "",
+                                               lexer.NextBytesFunc(func(b byte) bool { return b != '"' && b != '\\' }) != "":
+                                               rhsText.WriteString(lexer.Since(m))
+
+                                       case lexer.SkipString("\\\""),
+                                               lexer.SkipString("\\\\"):
+                                               rhsText.WriteByte(lexer.Since(m)[1])
+
+                                       case lexer.SkipByte('"'):
+                                               return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, rhsText.String()}}
+                                       default:
+                                               break loop
+                                       }
+                               }
+                               lexer.Reset(mark)
                        }
                }
 
@@ -524,9 +562,14 @@ func (p *MkParser) mkCondFunc() *mkCond 
 func (p *MkParser) Varname() string {
        lexer := p.lexer
 
+       // TODO: duplicated code in MatchVarassign
        mark := lexer.Mark()
        lexer.SkipByte('.')
-       for p.VarUse() != nil || lexer.NextBytesSet(VarnameBytes) != "" {
+       for lexer.NextBytesSet(VarbaseBytes) != "" || p.VarUse() != nil {
+       }
+       if lexer.SkipByte('.') || hasPrefix(lexer.Since(mark), "SITES_") {
+               for lexer.NextBytesSet(VarparamBytes) != "" || p.VarUse() != nil {
+               }
        }
        return lexer.Since(mark)
 }
@@ -799,6 +842,7 @@ func (w *MkCondWalker) Walk(cond MkCond,
                if callback.VarUse != nil {
                        callback.VarUse(cond.CompareVarStr.Var)
                }
+               w.walkStr(cond.CompareVarStr.Str, callback)
 
        case cond.CompareVarNum != nil:
                if callback.CompareVarNum != nil {
@@ -814,5 +858,17 @@ func (w *MkCondWalker) Walk(cond MkCond,
                        call := cond.Call
                        callback.Call(call.Name, call.Arg)
                }
+               w.walkStr(cond.Call.Arg, callback)
+       }
+}
+
+func (w *MkCondWalker) walkStr(str string, callback *MkCondCallback) {
+       if callback.VarUse != nil {
+               tokens := NewMkParser(nil, str, false).MkTokens()
+               for _, token := range tokens {
+                       if token.Varuse != nil {
+                               callback.VarUse(token.Varuse)
+                       }
+               }
        }
 }

Index: pkgsrc/pkgtools/pkglint/files/mkparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.23 pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.24
--- pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.23 Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/mkparser_test.go      Sun Mar 10 19:01:50 2019
@@ -192,6 +192,11 @@ func (s *Suite) Test_MkParser_VarUse(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?/"))
+
        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]*/"))
 
@@ -331,6 +336,12 @@ func (s *Suite) Test_MkParser_VarUse(c *
 
        t.CheckOutputLines(
                "WARN: Test_MkParser_VarUse.mk:1: Modifier ${PLIST_SUBST_VARS:@var@...@} is missing the final \"@\".")
+
+       // Unfinished variable use
+       testRest("${", nil, "${")
+
+       // Unfinished nested variable use
+       testRest("${${", nil, "${${")
 }
 
 func (s *Suite) Test_MkParser_VarUse__ambiguous(c *check.C) {
@@ -412,6 +423,13 @@ func (s *Suite) Test_MkParser_MkCond(c *
        test("\"${pkg}\" == \"${name}\"",
                &mkCond{CompareVarVar: &MkCondCompareVarVar{varuse("pkg"), "==", varuse("name")}})
 
+       // The right-hand side is not analyzed further to keep the data types simple.
+       test("${ABC} == \"${A}B${C}\"",
+               &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("ABC"), "==", "${A}B${C}"}})
+
+       test("${ABC} == \"${A}\\\"${B}\\\\${C}$${shellvar}${D}\"",
+               &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("ABC"), "==", "${A}\"${B}\\${C}$${shellvar}${D}"}})
+
        test("exists(/etc/hosts)",
                &mkCond{Call: &MkCondCall{"exists", "/etc/hosts"}})
 
@@ -776,9 +794,11 @@ func (s *Suite) Test_MkCondWalker_Walk(c
        mkline := t.NewMkLine("Makefile", 4, ""+
                ".if ${VAR:Mmatch} == ${OTHER} || "+
                "${STR} == Str || "+
+               "${VAR} == \"${PRE}text${POST}\" || "+
                "${NUM} == 3 && "+
                "defined(VAR) && "+
                "!exists(file.mk) && "+
+               "exists(${FILE}) && "+
                "(((${NONEMPTY})))")
        var events []string
 
@@ -830,11 +850,17 @@ func (s *Suite) Test_MkCondWalker_Walk(c
                "        varUse  OTHER",
                " compareVarStr  STR, Str",
                "        varUse  STR",
+               " compareVarStr  VAR, ${PRE}text${POST}",
+               "        varUse  VAR",
+               "        varUse  PRE",
+               "        varUse  POST",
                " compareVarNum  NUM, 3",
                "        varUse  NUM",
                "       defined  VAR",
                "        varUse  VAR",
                "          call  exists, file.mk",
+               "          call  exists, ${FILE}",
+               "        varUse  FILE",
                "           var  NONEMPTY",
                "        varUse  NONEMPTY"})
 }
Index: pkgsrc/pkgtools/pkglint/files/util_test.go
diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.23 pkgsrc/pkgtools/pkglint/files/util_test.go:1.24
--- pkgsrc/pkgtools/pkglint/files/util_test.go:1.23     Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/util_test.go  Sun Mar 10 19:01:50 2019
@@ -73,39 +73,84 @@ func (s *Suite) Test_tabWidth(c *check.C
 }
 
 func (s *Suite) Test_cleanpath(c *check.C) {
-       c.Check(cleanpath("simple/path"), equals, "simple/path")
-       c.Check(cleanpath("/absolute/path"), equals, "/absolute/path")
+       test := func(from, to string) {
+               c.Check(cleanpath(from), equals, to)
+       }
+
+       test("simple/path", "simple/path")
+       test("/absolute/path", "/absolute/path")
 
        // Single dot components are removed, unless it's the only component of the path.
-       c.Check(cleanpath("./././."), equals, ".")
-       c.Check(cleanpath("./././"), equals, ".")
-       c.Check(cleanpath("dir/multi/././/file"), equals, "dir/multi/file")
-       c.Check(cleanpath("dir/"), equals, "dir")
+       test("./././.", ".")
+       test("./././", ".")
+       test("dir/multi/././/file", "dir/multi/file")
+       test("dir/", "dir")
+
+       test("dir/", "dir")
 
        // Components like aa/bb/../.. are removed, but not in the initial part of the path,
        // and only if they are not followed by another "..".
-       c.Check(cleanpath("dir/../dir/../dir/../dir/subdir/../../Makefile"), equals, "dir/../dir/../dir/../Makefile")
-       c.Check(cleanpath("111/222/../../333/444/../../555/666/../../777/888/9"), equals, "111/222/../../777/888/9")
-       c.Check(cleanpath("1/2/3/../../4/5/6/../../7/8/9/../../../../10"), equals, "1/2/3/../../4/7/8/9/../../../../10")
-       c.Check(cleanpath("cat/pkg.v1/../../cat/pkg.v2/Makefile"), equals, "cat/pkg.v1/../../cat/pkg.v2/Makefile")
-       c.Check(cleanpath("aa/../../../../../a/b/c/d"), equals, "aa/../../../../../a/b/c/d")
-       c.Check(cleanpath("aa/bb/../../../../a/b/c/d"), equals, "aa/bb/../../../../a/b/c/d")
-       c.Check(cleanpath("aa/bb/cc/../../../a/b/c/d"), equals, "aa/bb/cc/../../../a/b/c/d")
-       c.Check(cleanpath("aa/bb/cc/dd/../../a/b/c/d"), equals, "aa/bb/a/b/c/d")
-       c.Check(cleanpath("aa/bb/cc/dd/ee/../a/b/c/d"), equals, "aa/bb/cc/dd/ee/../a/b/c/d")
-       c.Check(cleanpath("../../../../../a/b/c/d"), equals, "../../../../../a/b/c/d")
-       c.Check(cleanpath("aa/../../../../a/b/c/d"), equals, "aa/../../../../a/b/c/d")
-       c.Check(cleanpath("aa/bb/../../../a/b/c/d"), equals, "aa/bb/../../../a/b/c/d")
-       c.Check(cleanpath("aa/bb/cc/../../a/b/c/d"), equals, "aa/bb/cc/../../a/b/c/d")
-       c.Check(cleanpath("aa/bb/cc/dd/../a/b/c/d"), equals, "aa/bb/cc/dd/../a/b/c/d")
-       c.Check(cleanpath("aa/../cc/../../a/b/c/d"), equals, "aa/../cc/../../a/b/c/d")
+       test("dir/../dir/../dir/../dir/subdir/../../Makefile", "dir/../dir/../dir/../Makefile")
+       test("111/222/../../333/444/../../555/666/../../777/888/9", "111/222/../../777/888/9")
+       test("1/2/3/../../4/5/6/../../7/8/9/../../../../10", "1/2/3/../../4/7/8/9/../../../../10")
+       test("cat/pkg.v1/../../cat/pkg.v2/Makefile", "cat/pkg.v1/../../cat/pkg.v2/Makefile")
+       test("aa/../../../../../a/b/c/d", "aa/../../../../../a/b/c/d")
+       test("aa/bb/../../../../a/b/c/d", "aa/bb/../../../../a/b/c/d")
+       test("aa/bb/cc/../../../a/b/c/d", "aa/bb/cc/../../../a/b/c/d")
+       test("aa/bb/cc/dd/../../a/b/c/d", "aa/bb/a/b/c/d")
+       test("aa/bb/cc/dd/ee/../a/b/c/d", "aa/bb/cc/dd/ee/../a/b/c/d")
+       test("../../../../../a/b/c/d", "../../../../../a/b/c/d")
+       test("aa/../../../../a/b/c/d", "aa/../../../../a/b/c/d")
+       test("aa/bb/../../../a/b/c/d", "aa/bb/../../../a/b/c/d")
+       test("aa/bb/cc/../../a/b/c/d", "aa/bb/cc/../../a/b/c/d")
+       test("aa/bb/cc/dd/../a/b/c/d", "aa/bb/cc/dd/../a/b/c/d")
+       test("aa/../cc/../../a/b/c/d", "aa/../cc/../../a/b/c/d")
 
        // The initial 2 components of the path are typically category/package, when
        // pkglint is called from the pkgsrc top-level directory.
        // This path serves as the context and therefore is always kept.
-       c.Check(cleanpath("aa/bb/../../cc/dd/../../ee/ff"), equals, "aa/bb/../../ee/ff")
-       c.Check(cleanpath("aa/bb/../../cc/dd/../.."), equals, "aa/bb/../..")
-       c.Check(cleanpath("aa/bb/cc/dd/../.."), equals, "aa/bb")
+       test("aa/bb/../../cc/dd/../../ee/ff", "aa/bb/../../ee/ff")
+       test("aa/bb/../../cc/dd/../..", "aa/bb/../..")
+       test("aa/bb/cc/dd/../..", "aa/bb")
+       test("aa/bb/../../cc/dd/../../ee/ff/buildlink3.mk", "aa/bb/../../ee/ff/buildlink3.mk")
+       test("./aa/bb/../../cc/dd/../../ee/ff/buildlink3.mk", "aa/bb/../../ee/ff/buildlink3.mk")
+
+       test("../.", "..")
+       test("../././././././.", "..")
+       test(".././././././././", "..")
+}
+
+func (s *Suite) Test_relpath(c *check.C) {
+       t := s.Init(c)
+
+       t.Chdir(".")
+       t.Check(G.Pkgsrc.topdir, equals, t.tmpdir)
+
+       test := func(from, to, result string) {
+               c.Check(relpath(from, to), equals, result)
+       }
+
+       test("some/dir", "some/directory", "../../some/directory")
+
+       test("category/package/.", ".", "../..")
+
+       // This case is handled by one of the shortcuts that avoid file system access.
+       test(
+               "./.",
+               "x11/frameworkintegration/../../meta-pkgs/kde/kf5.mk",
+               "meta-pkgs/kde/kf5.mk")
+
+       // This happens when "pkglint -r x11" is run.
+       G.Pkgsrc.topdir = "x11/.."
+
+       test(
+               "./.",
+               "x11/frameworkintegration/../../meta-pkgs/kde/kf5.mk",
+               "meta-pkgs/kde/kf5.mk")
+       test(
+               "x11/..",
+               "x11/frameworkintegration/../../meta-pkgs/kde/kf5.mk",
+               "meta-pkgs/kde/kf5.mk")
 }
 
 // Relpath is called so often that handling the most common calls
@@ -119,9 +164,9 @@ func (s *Suite) Test_relpath__quick(c *c
        test("some/dir", "some/dir/../..", "../..")
        test("some/dir", "some/dir/./././../..", "../..")
        test("some/dir", "some/dir/", ".")
-       test("some/dir", "some/directory", "../directory")
 
-       test("category/package/.", ".", "../..")
+       test("some/dir", ".", "../..")
+       test("some/dir/.", ".", "../..")
 }
 
 // This is not really an internal error but won't happen in practice anyway.
@@ -129,10 +174,14 @@ func (s *Suite) Test_relpath__quick(c *c
 func (s *Suite) Test_relpath__failure_on_Windows(c *check.C) {
        t := s.Init(c)
 
-       if runtime.GOOS == "windows" {
+       if runtime.GOOS == "windows" && hasPrefix(t.tmpdir, "C:/") {
                t.ExpectPanic(
                        func() { relpath("c:/", "d:/") },
-                       "Pkglint internal error: relpath \"c:/\" \"d:/\": Rel: can't make d:/ relative to c:/")
+                       sprintf(
+                               "Pkglint internal error: "+
+                                       "relpath from topdir %q to %q: "+
+                                       "Rel: can't make %s relative to %s",
+                               t.tmpdir, "D:/", "D:/", t.tmpdir))
        }
 }
 
@@ -710,28 +759,3 @@ func (s *Suite) Test_StringInterner(c *c
        t.Check(si.Intern("Hello, world"), equals, "Hello, world")
        t.Check(si.Intern("Hello, world"[0:5]), equals, "Hello")
 }
-
-func (s *Suite) Test_includePath_includes(c *check.C) {
-       t := s.Init(c)
-
-       path := func(locations ...string) includePath {
-               return includePath{locations}
-       }
-
-       var (
-               m   = path("Makefile")
-               mc  = path("Makefile", "Makefile.common")
-               mco = path("Makefile", "Makefile.common", "other.mk")
-               mo  = path("Makefile", "other.mk")
-       )
-
-       t.Check(m.includes(m), equals, false)
-
-       t.Check(m.includes(mc), equals, true)
-       t.Check(m.includes(mco), equals, true)
-       t.Check(mc.includes(mco), equals, true)
-
-       t.Check(mc.includes(m), equals, false)
-       t.Check(mc.includes(mo), equals, false)
-       t.Check(mo.includes(mc), equals, false)
-}

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.46 pkgsrc/pkgtools/pkglint/files/package.go:1.47
--- pkgsrc/pkgtools/pkglint/files/package.go:1.46       Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/package.go    Sun Mar 10 19:01:50 2019
@@ -28,10 +28,21 @@ type Package struct {
        EffectivePkgnameLine MkLine       // The origin of the three Effective* values
        Plist                PlistContent // Files and directories mentioned in the PLIST files
 
-       vars               Scope
-       bl3                map[string]MkLine // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included.
-       included           map[string]MkLine // filename => line
-       seenMakefileCommon bool              // Does the package have any .includes?
+       vars Scope
+       bl3  map[string]MkLine // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included.
+
+       // Remembers the Makefile fragments that have already been included.
+       // The key to the map is the filename relative to the package directory.
+       // Typical keys are "../../category/package/buildlink3.mk".
+       //
+       // TODO: Include files with multiple-inclusion guard only once.
+       //
+       // TODO: Include files without multiple-inclusion guard as often as needed.
+       //
+       // TODO: Set an upper limit, to prevent denial of service.
+       included Once
+
+       seenMakefileCommon bool // Does the package have any .includes?
 
        // Files from .include lines that are nested inside .if.
        // They often depend on OPSYS or on the existence of files in the build environment.
@@ -61,7 +72,7 @@ func NewPackage(dir string) *Package {
                Plist:                 NewPlistContent(),
                vars:                  NewScope(),
                bl3:                   make(map[string]MkLine),
-               included:              make(map[string]MkLine),
+               included:              Once{},
                conditionalIncludes:   make(map[string]MkLine),
                unconditionalIncludes: make(map[string]MkLine),
        }
@@ -152,7 +163,7 @@ func (pkg *Package) checkLinesBuildlink3
        }
 }
 
-func (pkg *Package) loadPackageMakefile() MkLines {
+func (pkg *Package) loadPackageMakefile() (MkLines, MkLines) {
        filename := pkg.File("Makefile")
        if trace.Tracing {
                defer trace.Call1(filename)()
@@ -162,7 +173,7 @@ func (pkg *Package) loadPackageMakefile(
        allLines := NewMkLines(NewLines("", nil))
        if _, result := pkg.readMakefile(filename, mainLines, allLines, ""); !result {
                LoadMk(filename, NotEmpty|LogErrors) // Just for the LogErrors.
-               return nil
+               return nil, nil
        }
 
        // TODO: Is this still necessary? This code is 20 years old and was introduced
@@ -183,7 +194,6 @@ func (pkg *Package) loadPackageMakefile(
        }
 
        allLines.collectUsedVariables()
-       allLines.CheckRedundantAssignments()
 
        pkg.Pkgdir = pkg.vars.LastValue("PKGDIR")
        pkg.DistinfoFile = pkg.vars.LastValue("DISTINFO_FILE")
@@ -212,7 +222,7 @@ func (pkg *Package) loadPackageMakefile(
                trace.Step1("PKGDIR=%s", pkg.Pkgdir)
        }
 
-       return mainLines
+       return mainLines, allLines
 }
 
 // TODO: What is allLines used for, is it still necessary? Would it be better as a field in Package?
@@ -225,71 +235,92 @@ func (pkg *Package) readMakefile(filenam
        if fileMklines == nil {
                return false, false
        }
+
        exists = true
+       result = true
 
        isMainMakefile := len(mainLines.mklines) == 0
 
-       result = true
-       lineAction := func(mkline MkLine) bool {
-               if isMainMakefile {
-                       mainLines.mklines = append(mainLines.mklines, mkline)
-                       mainLines.lines.Lines = append(mainLines.lines.Lines, mkline.Line)
+       handleIncludeLine := func(mkline MkLine) YesNoUnknown {
+               includedFile, incDir, incBase := pkg.findIncludedFile(mkline, filename)
+
+               if includedFile == "" {
+                       return unknown
                }
-               allLines.mklines = append(allLines.mklines, mkline)
-               allLines.lines.Lines = append(allLines.lines.Lines, mkline.Line)
 
-               includedFile, incDir, incBase := pkg.findIncludedFile(mkline, filename)
+               dirname, _ := path.Split(filename)
+               dirname = cleanpath(dirname)
+               fullIncluded := dirname + "/" + includedFile
+               relIncludedFile := relpath(pkg.dir, fullIncluded)
+
+               if !pkg.diveInto(filename, includedFile) {
+                       return unknown
+               }
 
-               if includedFile != "" && pkg.included[includedFile] == nil {
-                       pkg.included[includedFile] = mkline
+               if !pkg.included.FirstTime(relIncludedFile) {
+                       return unknown
+               }
 
-                       // TODO: "../../../.." also matches but shouldn't.
-                       if matches(includedFile, `^\.\./[^./][^/]*/[^/]+`) {
+               if matches(includedFile, `^\.\./[^./][^/]*/[^/]+`) {
+                       if G.Wip && contains(includedFile, "/mk/") {
+                               mkline.Warnf("References to the pkgsrc-wip infrastructure should look like \"../../wip/mk\", not \"../mk\".")
+                       } else {
                                mkline.Warnf("References to other packages should look like \"../../category/package\", not \"../package\".")
-                               mkline.ExplainRelativeDirs()
                        }
+                       mkline.ExplainRelativeDirs()
+               }
 
-                       pkg.collectUsedBy(mkline, incDir, incBase, includedFile)
+               pkg.collectUsedBy(mkline, incDir, incBase, includedFile)
 
-                       skip := contains(filename, "/mk/") || hasSuffix(includedFile, "/bsd.pkg.mk") || IsPrefs(includedFile)
-                       if !skip {
-                               dirname, _ := path.Split(filename)
-                               dirname = cleanpath(dirname)
+               if trace.Tracing {
+                       trace.Step1("Including %q.", fullIncluded)
+               }
+               fullIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", filename, "")
+               innerExists, innerResult := pkg.readMakefile(fullIncluded, mainLines, allLines, fullIncluding)
 
-                               fullIncluded := dirname + "/" + includedFile
-                               if trace.Tracing {
-                                       trace.Step1("Including %q.", fullIncluded)
-                               }
-                               fullIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", filename, "")
-                               innerExists, innerResult := pkg.readMakefile(fullIncluded, mainLines, allLines, fullIncluding)
+               if !innerExists {
+                       if fileMklines.indentation.IsCheckedFile(includedFile) {
+                               return yes // See https://github.com/rillig/pkglint/issues/1
+                       }
 
-                               if !innerExists {
-                                       if fileMklines.indentation.IsCheckedFile(includedFile) {
-                                               return true // See https://github.com/rillig/pkglint/issues/1
-                                       }
+                       // Only look in the directory relative to the
+                       // current file and in the package directory.
+                       // Make(1) has a list of include directories, but pkgsrc
+                       // doesn't make use of that, so pkglint also doesn't
+                       // need this extra complexity.
+                       pkgBasedir := pkg.File(".")
+                       if dirname != pkgBasedir { // Prevent unnecessary syscalls
+                               dirname = pkgBasedir
 
-                                       // Only look in the directory relative to the
-                                       // current file and in the package directory.
-                                       // Make(1) has a list of include directories, but pkgsrc
-                                       // doesn't make use of that, so pkglint also doesn't
-                                       // need this extra complexity.
-                                       pkgBasedir := pkg.File(".")
-                                       if dirname != pkgBasedir { // Prevent unnecessary syscalls
-                                               dirname = pkgBasedir
+                               fullIncludedFallback := dirname + "/" + includedFile
+                               innerExists, innerResult = pkg.readMakefile(fullIncludedFallback, mainLines, allLines, fullIncluding)
+                       }
 
-                                               fullIncludedFallback := dirname + "/" + includedFile
-                                               innerExists, innerResult = pkg.readMakefile(fullIncludedFallback, mainLines, allLines, fullIncluding)
-                                       }
+                       if !innerExists {
+                               mkline.Errorf("Cannot read %q.", includedFile)
+                       }
+               }
 
-                                       if !innerExists {
-                                               mkline.Errorf("Cannot read %q.", includedFile)
-                                       }
-                               }
+               if !innerResult {
+                       result = false
+                       return no
+               }
 
-                               if !innerResult {
-                                       result = false
-                                       return false
-                               }
+               return unknown
+       }
+
+       lineAction := func(mkline MkLine) bool {
+               if isMainMakefile {
+                       mainLines.mklines = append(mainLines.mklines, mkline)
+                       mainLines.lines.Lines = append(mainLines.lines.Lines, mkline.Line)
+               }
+               allLines.mklines = append(allLines.mklines, mkline)
+               allLines.lines.Lines = append(allLines.lines.Lines, mkline.Line)
+
+               if mkline.IsInclude() {
+                       includeResult := handleIncludeLine(mkline)
+                       if includeResult != unknown {
+                               return includeResult == yes
                        }
                }
 
@@ -305,6 +336,7 @@ func (pkg *Package) readMakefile(filenam
                }
                return true
        }
+
        atEnd := func(mkline MkLine) {}
        fileMklines.ForEachEnd(lineAction, atEnd)
 
@@ -315,8 +347,9 @@ func (pkg *Package) readMakefile(filenam
        // For every included buildlink3.mk, include the corresponding builtin.mk
        // automatically since the pkgsrc infrastructure does the same.
        if path.Base(filename) == "buildlink3.mk" {
-               builtin := path.Join(path.Dir(filename), "builtin.mk")
-               if fileExists(builtin) {
+               builtin := cleanpath(path.Dir(filename) + "/builtin.mk")
+               builtinRel := relpath(pkg.dir, builtin)
+               if pkg.included.FirstTime(builtinRel) && fileExists(builtin) {
                        pkg.readMakefile(builtin, mainLines, allLines, "")
                }
        }
@@ -324,6 +357,17 @@ func (pkg *Package) readMakefile(filenam
        return
 }
 
+func (pkg *Package) diveInto(includingFile string, includedFile string) bool {
+       skip := hasSuffix(includedFile, "/bsd.pkg.mk") || IsPrefs(includedFile)
+       if !skip && contains(includingFile, "/mk/") {
+               skip = true
+               if contains(includingFile, "buildlink3.mk") && contains(includedFile, "builtin.mk") {
+                       skip = false
+               }
+       }
+       return !skip
+}
+
 func (pkg *Package) collectUsedBy(mkline MkLine, incDir string, incBase string, includedFile string) {
        switch {
        case
@@ -343,19 +387,17 @@ func (pkg *Package) collectUsedBy(mkline
 
 func (pkg *Package) findIncludedFile(mkline MkLine, includingFilename string) (includedFile, incDir, incBase string) {
 
-       if mkline.IsInclude() {
-               // TODO: resolveVariableRefs uses G.Pkg implicitly. It should be made explicit.
-               // TODO: Try to combine resolveVariableRefs and ResolveVarsInRelativePath.
-               includedFile = resolveVariableRefs(mkline.ResolveVarsInRelativePath(mkline.IncludedFile()))
-               if containsVarRef(includedFile) {
-                       if trace.Tracing && !contains(includingFilename, "/mk/") {
-                               trace.Stepf("%s:%s: Skipping include file %q. This may result in false warnings.",
-                                       mkline.Filename, mkline.Linenos(), includedFile)
-                       }
-                       includedFile = ""
+       // TODO: resolveVariableRefs uses G.Pkg implicitly. It should be made explicit.
+       // TODO: Try to combine resolveVariableRefs and ResolveVarsInRelativePath.
+       includedFile = resolveVariableRefs(mkline.ResolveVarsInRelativePath(mkline.IncludedFile()))
+       if containsVarRef(includedFile) {
+               if trace.Tracing && !contains(includingFilename, "/mk/") {
+                       trace.Stepf("%s:%s: Skipping include file %q. This may result in false warnings.",
+                               mkline.Filename, mkline.Linenos(), includedFile)
                }
-               incDir, incBase = path.Split(includedFile)
+               includedFile = ""
        }
+       incDir, incBase = path.Split(includedFile)
 
        if includedFile != "" {
                if mkline.Basename != "buildlink3.mk" {
@@ -371,7 +413,7 @@ func (pkg *Package) findIncludedFile(mkl
        return
 }
 
-func (pkg *Package) checkfilePackageMakefile(filename string, mklines MkLines) {
+func (pkg *Package) checkfilePackageMakefile(filename string, mklines MkLines, allLines MkLines) {
        if trace.Tracing {
                defer trace.Call1(filename)()
        }
@@ -408,11 +450,18 @@ func (pkg *Package) checkfilePackageMake
        }
 
        if !vars.Defined("LICENSE") && !vars.Defined("META_PACKAGE") && pkg.once.FirstTime("LICENSE") {
-               NewLineWhole(filename).Errorf("Each package must define its LICENSE.")
+               line := NewLineWhole(filename)
+               line.Errorf("Each package must define its LICENSE.")
                // TODO: Explain why the LICENSE is necessary.
+               line.Explain(
+                       "To take a good guess on the license of a package,",
+                       sprintf("run %q.", bmake("guess-license")))
        }
 
-       pkg.checkGnuConfigureUseLanguages()
+       scope := NewRedundantScope()
+       scope.Check(allLines) // Updates the variables in the scope
+       pkg.checkGnuConfigureUseLanguages(scope)
+
        pkg.determineEffectivePkgVars()
        pkg.checkPossibleDowngrade()
 
@@ -434,28 +483,45 @@ func (pkg *Package) checkfilePackageMake
        SaveAutofixChanges(mklines.lines)
 }
 
-func (pkg *Package) checkGnuConfigureUseLanguages() {
-       vars := pkg.vars
+func (pkg *Package) checkGnuConfigureUseLanguages(s *RedundantScope) {
 
-       if gnuLine := vars.FirstDefinition("GNU_CONFIGURE"); gnuLine != nil {
+       gnuConfigure := s.vars["GNU_CONFIGURE"]
+       if gnuConfigure == nil || !gnuConfigure.vari.Constant() {
+               return
+       }
 
-               // FIXME: Instead of using the first definition here, a better approach
-               //  is probably to use all the definitions except those from mk/compiler.mk.
-               //  In real pkgsrc, the last definition is typically from mk/compiler.mk
-               //  and only contains c++.
-               if useLine := vars.FirstDefinition("USE_LANGUAGES"); useLine != nil {
-
-                       if matches(useLine.VarassignComment(), `(?-i)\b(?:c|empty|none)\b`) {
-                               // Don't emit a warning since the comment probably contains a
-                               // statement that C is really not needed.
-
-                       } else if !matches(useLine.Value(), `(?:^|[\t ]+)(?:c|c99|objc)(?:[\t ]+|$)`) {
-                               gnuLine.Warnf(
-                                       "GNU_CONFIGURE almost always needs a C compiler, "+
-                                               "but \"c\" is not added to USE_LANGUAGES in %s.",
-                                       gnuLine.RefTo(useLine))
-                       }
+       useLanguages := s.vars["USE_LANGUAGES"]
+       if useLanguages == nil || !useLanguages.vari.Constant() {
+               return
+       }
+
+       var wrongLines []MkLine
+       for _, mkline := range useLanguages.vari.WriteLocations() {
+
+               if G.Pkgsrc.IsInfra(mkline.Line.Filename) {
+                       continue
+               }
+
+               if matches(mkline.VarassignComment(), `(?-i)\b(?:c|empty|none)\b`) {
+                       // Don't emit a warning since the comment probably contains a
+                       // statement that C is really not needed.
+                       return
+               }
+
+               languages := mkline.Value()
+               if matches(languages, `(?:^|[\t ]+)(?:c|c99|objc)(?:[\t ]+|$)`) {
+                       return
                }
+
+               wrongLines = append(wrongLines, mkline)
+       }
+
+       gnuLine := gnuConfigure.vari.WriteLocations()[0]
+       for _, useLine := range wrongLines {
+               gnuLine.Warnf(
+                       "GNU_CONFIGURE almost always needs a C compiler, "+
+                               "but \"c\" is not added to USE_LANGUAGES in %s.",
+                       gnuLine.RefTo(useLine))
        }
 }
 

Index: pkgsrc/pkgtools/pkglint/files/package_test.go
diff -u pkgsrc/pkgtools/pkglint/files/package_test.go:1.39 pkgsrc/pkgtools/pkglint/files/package_test.go:1.40
--- pkgsrc/pkgtools/pkglint/files/package_test.go:1.39  Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/package_test.go       Sun Mar 10 19:01:50 2019
@@ -28,7 +28,8 @@ func (s *Suite) Test_Package_checkLinesB
 
        t.CreateFileLines("category/dependency/buildlink3.mk")
        G.Pkg = NewPackage(t.File("category/package"))
-       G.Pkg.bl3["../../category/dependency/buildlink3.mk"] = t.NewMkLine("filename", 1, "")
+       G.Pkg.bl3["../../category/dependency/buildlink3.mk"] =
+               t.NewMkLine("../../category/dependency/buildlink3.mk", 1, "")
        mklines := t.NewMkLines("category/package/buildlink3.mk",
                MkRcsID)
 
@@ -212,7 +213,6 @@ func (s *Suite) Test_Package_CheckVarord
        t.CreateFileLines("mk/bsd.pkg.mk", "# dummy")
        t.CreateFileLines("x11/Makefile", MkRcsID)
        t.CreateFileLines("x11/9term/PLIST", PlistRcsID, "bin/9term")
-       t.CreateFileLines("x11/9term/distinfo", RcsID)
        t.CreateFileLines("x11/9term/Makefile",
                MkRcsID,
                "",
@@ -221,6 +221,8 @@ func (s *Suite) Test_Package_CheckVarord
                "",
                "COMMENT=\tTerminal",
                "",
+               "NO_CHECKSUM=\tyes",
+               "",
                ".include \"../../mk/bsd.pkg.mk\"")
 
        t.SetUpVartypes()
@@ -228,6 +230,7 @@ func (s *Suite) Test_Package_CheckVarord
        G.Check(t.File("x11/9term"))
 
        // Since the error is grave enough, the warning about the correct position is suppressed.
+       // TODO: Knowing the correct position helps, though.
        t.CheckOutputLines(
                "ERROR: ~/x11/9term/Makefile: Each package must define its LICENSE.")
 }
@@ -524,13 +527,39 @@ func (s *Suite) Test_Package_loadPackage
 
        // Including a package Makefile directly is an error (in the last line),
        // but that is checked later.
-       // A file including itself does not lead to an endless loop while parsing
-       // but may still produce unexpected warnings, such as redundant definitions.
+       // This test demonstrates that a file including itself does not lead to an
+       // endless loop while parsing. It might trigger an error in the future.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Package__relative_included_filenames_in_same_directory(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "PKGNAME=\tpkgname-1.67",
+               "DISTNAME=\tdistfile_1_67",
+               ".include \"../../category/package/other.mk\"")
+       t.CreateFileLines("category/package/other.mk",
+               MkRcsID,
+               "PKGNAME=\tpkgname-1.67",
+               "DISTNAME=\tdistfile_1_67",
+               ".include \"../../category/package/other.mk\"")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       // TODO: Since other.mk is referenced via "../../category/package",
+       //  it would be nice if this relative path would be reflected in the output
+       //  instead of referring just to "other.mk".
+       //  This needs to be fixed somewhere near relpath.
+       //
+       // The notes are in reverse order because they are produced when checking
+       // other.mk, and there the relative order is correct (line 2 before line 3).
        t.CheckOutputLines(
-               "NOTE: ~/category/package/Makefile:3: "+
-                       "Definition of PKGNAME is redundant because of ../../category/package/Makefile:3.",
                "NOTE: ~/category/package/Makefile:4: "+
-                       "Definition of DISTNAME is redundant because of ../../category/package/Makefile:4.")
+                       "Definition of PKGNAME is redundant because of other.mk:2.",
+               "NOTE: ~/category/package/Makefile:3: "+
+                       "Definition of DISTNAME is redundant because of other.mk:3.")
 }
 
 func (s *Suite) Test_Package_loadPackageMakefile__PECL_VERSION(c *check.C) {
@@ -668,14 +697,16 @@ func (s *Suite) Test_Package__redundant_
        G.checkdirPackage(t.File("math/R-date"))
 
        // The definition in Makefile:6 is redundant because the same definition
-       // occurs later in Makefile.extension:4. Usually the later definition gets
-       // the note. In this case though, it would be wrong to mark the
-       // definition in Makefile.extension as redundant because that definition is
-       // probably used by other packages as well. Therefore in this case the roles
-       // of the two lines are swapped; see RedundantScope.Handle, the ".includes" line.
+       // occurs later in Makefile.extension:4.
+       //
+       // When a file includes another file, it's always the including file that
+       // is marked as redundant since the included file typically provides the
+       // generally useful value for several packages;
+       // see RedundantScope.handleVarassign, keyword includePath.
        t.CheckOutputLines(
                "NOTE: ~/math/R-date/Makefile:6: " +
-                       "Definition of MASTER_SITES is redundant because of ../../math/R/Makefile.extension:4.")
+                       "Definition of MASTER_SITES is redundant " +
+                       "because of ../../math/R/Makefile.extension:4.")
 }
 
 func (s *Suite) Test_Package_checkUpdate(c *check.C) {
@@ -828,6 +859,131 @@ func (s *Suite) Test_Package_checkfilePa
                "NOTE: ~/category/package/Makefile:21: USE_IMAKE makes USE_X11 in line 20 redundant.")
 }
 
+func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__no_C(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "USE_LANGUAGES=\tfortran77",
+               "USE_LANGUAGES+=\tc++14",
+               "USE_LANGUAGES+=\tada",
+               "GNU_CONFIGURE=\tyes")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:23: "+
+                       "GNU_CONFIGURE almost always needs a C compiler, "+
+                       "but \"c\" is not added to USE_LANGUAGES in line 20.",
+               "WARN: ~/category/package/Makefile:23: "+
+                       "GNU_CONFIGURE almost always needs a C compiler, "+
+                       "but \"c\" is not added to USE_LANGUAGES in line 21.",
+               "WARN: ~/category/package/Makefile:23: "+
+                       "GNU_CONFIGURE almost always needs a C compiler, "+
+                       "but \"c\" is not added to USE_LANGUAGES in line 22.")
+}
+
+func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__C_in_the_middle(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "USE_LANGUAGES=\tfortran77",
+               "USE_LANGUAGES+=\tc99",
+               "USE_LANGUAGES+=\tada",
+               "GNU_CONFIGURE=\tyes")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       // Until March 2019 pkglint wrongly warned that USE_LANGUAGES would not
+       // include c or c99, although c99 was added.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__realistic_compiler_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "USE_LANGUAGES=\tfortran77",
+               "USE_LANGUAGES+=\tc++",
+               "USE_LANGUAGES+=\tada",
+               "GNU_CONFIGURE=\tyes",
+               "",
+               ".include \"../../mk/compiler.mk\"")
+       t.CreateFileLines("mk/compiler.mk",
+               MkRcsID,
+               ".include \"bsd.prefs.mk\"",
+               "",
+               "USE_LANGUAGES?=\tc",
+               "USE_LANGUAGES+=\tc",
+               "USE_LANGUAGES+=\tc++")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       // The package defines several languages it needs, but C is not one of them.
+       // When the package is loaded, the included files are read in recursively, even
+       // when they come from the pkgsrc infrastructure.
+       //
+       // Up to March 2019, the USE_LANGUAGES definitions from mk/compiler.mk were
+       // loaded as if they were defined by the package, without taking the conditionals
+       // into account. Thereby "c" was added unconditionally to USE_LANGUAGES.
+       //
+       // Since March 2019 the infrastructure files are ignored when determining the value
+       // of USE_LANGUAGES.
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:23: "+
+                       "GNU_CONFIGURE almost always needs a C compiler, "+
+                       "but \"c\" is not added to USE_LANGUAGES in line 20.",
+               "WARN: ~/category/package/Makefile:23: "+
+                       "GNU_CONFIGURE almost always needs a C compiler, "+
+                       "but \"c\" is not added to USE_LANGUAGES in line 21.",
+               "WARN: ~/category/package/Makefile:23: "+
+                       "GNU_CONFIGURE almost always needs a C compiler, "+
+                       "but \"c\" is not added to USE_LANGUAGES in line 22.")
+}
+
+func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__only_GNU_CONFIGURE(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "GNU_CONFIGURE=\tyes")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__ok(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "GNU_CONFIGURE=\tyes",
+               "USE_LANGUAGES=\tc++ objc")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Package__USE_LANGUAGES_too_late(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               ".include \"../../mk/compiler.mk\"",
+               "USE_LANGUAGES=\tc c99 fortran ada c++14")
+       t.CreateFileLines("mk/compiler.mk",
+               MkRcsID)
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("category/package"))
+
+       // FIXME: There must be a warning "USE_LANGUAGES must be added before including compiler.mk."
+       t.CheckOutputEmpty()
+}
+
 func (s *Suite) Test_Package_readMakefile__skipping(c *check.C) {
        t := s.Init(c)
 
@@ -914,6 +1070,43 @@ func (s *Suite) Test_Package_readMakefil
                "WARN: ~/category/package/Makefile:23: OTHER_VAR is used but not defined.")
 }
 
+// Ensures that the paths in Package.included are indeed relative to the
+// package directory. This hadn't been the case until March 2019.
+func (s *Suite) Test_Package_readMakefile__included(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               ".include \"../../devel/library/buildlink3.mk\"",
+               ".include \"../../lang/language/module.mk\"")
+       t.SetUpPackage("devel/library")
+       t.CreateFileDummyBuildlink3("devel/library/buildlink3.mk")
+       t.CreateFileLines("devel/library/builtin.mk",
+               MkRcsID)
+       t.CreateFileLines("lang/language/module.mk",
+               MkRcsID,
+               ".include \"version.mk\"")
+       t.CreateFileLines("lang/language/version.mk",
+               MkRcsID)
+       pkg := NewPackage(t.File("category/package"))
+
+       pkg.loadPackageMakefile()
+
+       expected := []string{
+               "../../devel/library/buildlink3.mk",
+               "../../devel/library/builtin.mk",
+               "../../lang/language/module.mk",
+               "../../lang/language/version.mk",
+               "suppress-varorder.mk"}
+
+       seen := pkg.included
+       for _, filename := range expected {
+               if !seen.Seen(filename) {
+                       c.Errorf("File %q is not seen.", filename)
+               }
+       }
+       t.Check(seen.seen, check.HasLen, 5)
+}
+
 func (s *Suite) Test_Package_checkLocallyModified(c *check.C) {
        t := s.Init(c)
 
@@ -1040,3 +1233,61 @@ func (s *Suite) Test_Package__redundant_
        // PY_PATCHPLIST is not redundant in these files.
        t.CheckOutputEmpty()
 }
+
+// Pkglint loads some files from the pkgsrc infrastructure and skips others.
+//
+// When a buildlink3.mk file from the infrastructure is included, it should
+// be allowed to include its corresponding builtin.mk file in turn.
+//
+// This is necessary to load the correct variable assignments for the
+// redundancy check, in particular variable assignments that serve as
+// arguments to "procedure calls", such as mk/find-files.mk.
+func (s *Suite) Test_Package_readMakefile__include_infrastructure(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("--dumpmakefile")
+       t.SetUpPackage("category/package",
+               ".include \"../../mk/dlopen.buildlink3.mk\"",
+               ".include \"../../mk/pthread.buildlink3.mk\"")
+       t.CreateFileLines("mk/dlopen.buildlink3.mk",
+               ".include \"dlopen.builtin.mk\"")
+       t.CreateFileLines("mk/dlopen.builtin.mk",
+               ".include \"pthread.builtin.mk\"")
+       t.CreateFileLines("mk/pthread.buildlink3.mk",
+               ".include \"pthread.builtin.mk\"")
+       t.CreateFileLines("mk/pthread.builtin.mk",
+               "# This should be included by pthread.buildlink3.mk")
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "Whole Makefile (with all included files) follows:",
+               "~/category/package/Makefile:1: "+MkRcsID,
+               "~/category/package/Makefile:2: ",
+               "~/category/package/Makefile:3: DISTNAME=\tdistname-1.0",
+               "~/category/package/Makefile:4: #PKGNAME=\tpackage-1.0",
+               "~/category/package/Makefile:5: CATEGORIES=\tcategory",
+               "~/category/package/Makefile:6: MASTER_SITES=\t# none",
+               "~/category/package/Makefile:7: ",
+               "~/category/package/Makefile:8: MAINTAINER=\tpkgsrc-users%NetBSD.org@localhost",
+               "~/category/package/Makefile:9: HOMEPAGE=\t# none",
+               "~/category/package/Makefile:10: COMMENT=\tDummy package",
+               "~/category/package/Makefile:11: LICENSE=\t2-clause-bsd",
+               "~/category/package/Makefile:12: ",
+               "~/category/package/Makefile:13: .include \"suppress-varorder.mk\"",
+               "~/category/package/suppress-varorder.mk:1: "+MkRcsID,
+               "~/category/package/Makefile:14: # empty",
+               "~/category/package/Makefile:15: # empty",
+               "~/category/package/Makefile:16: # empty",
+               "~/category/package/Makefile:17: # empty",
+               "~/category/package/Makefile:18: # empty",
+               "~/category/package/Makefile:19: # empty",
+               "~/category/package/Makefile:20: .include \"../../mk/dlopen.buildlink3.mk\"",
+               "~/category/package/../../mk/dlopen.buildlink3.mk:1: .include \"dlopen.builtin.mk\"",
+               "~/mk/dlopen.builtin.mk:1: .include \"pthread.builtin.mk\"",
+               "~/category/package/Makefile:21: .include \"../../mk/pthread.buildlink3.mk\"",
+               "~/category/package/../../mk/pthread.buildlink3.mk:1: .include \"pthread.builtin.mk\"",
+               "~/mk/pthread.builtin.mk:1: # This should be included by pthread.buildlink3.mk",
+               "~/category/package/Makefile:22: ",
+               "~/category/package/Makefile:23: .include \"../../mk/bsd.pkg.mk\"")
+}

Index: pkgsrc/pkgtools/pkglint/files/pkglint.1
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.1:1.54 pkgsrc/pkgtools/pkglint/files/pkglint.1:1.55
--- pkgsrc/pkgtools/pkglint/files/pkglint.1:1.54        Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.1     Sun Mar 10 19:01:50 2019
@@ -1,4 +1,4 @@
-.\"    $NetBSD: pkglint.1,v 1.54 2019/02/21 23:44:55 rillig Exp $
+.\"    $NetBSD: pkglint.1,v 1.55 2019/03/10 19:01:50 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.40 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.41
--- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.40    Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/shell_test.go Sun Mar 10 19:01:50 2019
@@ -154,7 +154,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        t.SetUpTool("unzip", "UNZIP_CMD", AtRunTime)
 
        test := func(shellCommand string) {
-               G.Mk = t.NewMkLines("filename",
+               G.Mk = t.NewMkLines("filename.mk",
                        "\t"+shellCommand)
                shline := NewShellLine(G.Mk.mklines[0])
 
@@ -170,8 +170,8 @@ func (s *Suite) Test_ShellLine_CheckShel
        test("uname=`uname`; echo $$uname; echo; ${PREFIX}/bin/command")
 
        t.CheckOutputLines(
-               "WARN: filename:1: Unknown shell command \"uname\".",
-               "WARN: filename:1: Please switch to \"set -e\" mode "+
+               "WARN: filename.mk:1: Unknown shell command \"uname\".",
+               "WARN: filename.mk:1: Please switch to \"set -e\" mode "+
                        "before using a semicolon (after \"uname=`uname`\") to separate commands.")
 
        t.SetUpTool("echo", "", AtRunTime)
@@ -180,35 +180,30 @@ func (s *Suite) Test_ShellLine_CheckShel
        test("echo ${PKGNAME:Q}") // VucQuotPlain
 
        t.CheckOutputLines(
-               "WARN: filename:1: PKGNAME may not be used in this file; "+
-                       "it would be ok in Makefile, Makefile.* or *.mk.",
-               "NOTE: filename:1: The :Q operator isn't necessary for ${PKGNAME} here.")
+               "NOTE: filename.mk:1: The :Q operator isn't necessary for ${PKGNAME} here.")
 
        test("echo \"${CFLAGS:Q}\"") // VucQuotDquot
 
        t.CheckOutputLines(
-               "WARN: filename:1: The :Q modifier should not be used inside double quotes.",
-               "WARN: filename:1: CFLAGS may not be used in this file; "+
-                       "it would be ok in Makefile, Makefile.common, options.mk or *.mk.",
-               "WARN: filename:1: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q} "+
+               "WARN: filename.mk:1: The :Q modifier should not be used inside double quotes.",
+               "WARN: filename.mk:1: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q} "+
                        "and make sure the variable appears outside of any quoting characters.")
 
        test("echo '${COMMENT:Q}'") // VucQuotSquot
 
        t.CheckOutputLines(
-               "WARN: filename:1: COMMENT may not be used in any file; it is a write-only variable.",
-               "WARN: filename:1: Please move ${COMMENT:Q} outside of any quoting characters.")
+               "WARN: filename.mk:1: Please move ${COMMENT:Q} outside of any quoting characters.")
 
        test("echo target=$@ exitcode=$$? '$$' \"\\$$\"")
 
        t.CheckOutputLines(
-               "WARN: filename:1: Please use \"${.TARGET}\" instead of \"$@\".",
-               "WARN: filename:1: The $? shell variable is often not available in \"set -e\" mode.")
+               "WARN: filename.mk:1: Please use \"${.TARGET}\" instead of \"$@\".",
+               "WARN: filename.mk:1: The $? shell variable is often not available in \"set -e\" mode.")
 
        test("echo $$@")
 
        t.CheckOutputLines(
-               "WARN: filename:1: The $@ shell variable should only be used in double quotes.")
+               "WARN: filename.mk:1: The $@ shell variable should only be used in double quotes.")
 
        test("echo \"$$\"") // As seen by make(1); the shell sees: echo "$"
 
@@ -233,8 +228,8 @@ func (s *Suite) Test_ShellLine_CheckShel
        test("${RUN} subdir=\"`unzip -c \"$$e\" install.rdf | awk '/re/ { print \"hello\" }'`\"")
 
        t.CheckOutputLines(
-               "WARN: filename:1: Double quotes inside backticks inside double quotes are error prone.",
-               "WARN: filename:1: The exitcode of \"unzip\" at the left of the | operator is ignored.")
+               "WARN: filename.mk:1: Double quotes inside backticks inside double quotes are error prone.",
+               "WARN: filename.mk:1: The exitcode of \"unzip\" at the left of the | operator is ignored.")
 
        // From mail/thunderbird/Makefile, rev. 1.159
        test("" +
@@ -247,9 +242,9 @@ func (s *Suite) Test_ShellLine_CheckShel
                "done")
 
        t.CheckOutputLines(
-               "WARN: filename:1: XPI_FILES is used but not defined.",
-               "WARN: filename:1: Double quotes inside backticks inside double quotes are error prone.",
-               "WARN: filename:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.")
+               "WARN: filename.mk:1: XPI_FILES is used but not defined.",
+               "WARN: filename.mk:1: Double quotes inside backticks inside double quotes are error prone.",
+               "WARN: filename.mk:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.")
 
        // From x11/wxGTK28/Makefile
        test("" +
@@ -262,25 +257,23 @@ func (s *Suite) Test_ShellLine_CheckShel
        // TODO: Why is TOOLS_PATH.msgfmt not recognized?
        //  At least, the warning should be more specific, mentioning USE_TOOLS.
        t.CheckOutputLines(
-               "WARN: filename:1: WRKSRC may not be used in this file; "+
-                       "it would be ok in Makefile, Makefile.* or *.mk.",
-               "WARN: filename:1: Unknown shell command \"[\".",
-               "WARN: filename:1: Unknown shell command \"${TOOLS_PATH.msgfmt}\".")
+               "WARN: filename.mk:1: Unknown shell command \"[\".",
+               "WARN: filename.mk:1: Unknown shell command \"${TOOLS_PATH.msgfmt}\".")
 
        test("@cp from to")
 
        t.CheckOutputLines(
-               "WARN: filename:1: The shell command \"cp\" should not be hidden.")
+               "WARN: filename.mk:1: The shell command \"cp\" should not be hidden.")
 
        test("-cp from to")
 
        t.CheckOutputLines(
-               "WARN: filename:1: Using a leading \"-\" to suppress errors is deprecated.")
+               "WARN: filename.mk:1: Using a leading \"-\" to suppress errors is deprecated.")
 
        test("-${MKDIR} deeply/nested/subdir")
 
        t.CheckOutputLines(
-               "WARN: filename:1: Using a leading \"-\" to suppress errors is deprecated.")
+               "WARN: filename.mk:1: Using a leading \"-\" to suppress errors is deprecated.")
 
        G.Pkg = NewPackage(t.File("category/pkgbase"))
        G.Pkg.Plist.Dirs["share/pkgbase"] = true
@@ -292,15 +285,15 @@ func (s *Suite) Test_ShellLine_CheckShel
        //  the note should not appear then.
 
        t.CheckOutputLines(
-               "NOTE: filename:1: You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= share/pkgbase\" "+
+               "NOTE: filename.mk:1: You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= share/pkgbase\" "+
                        "instead of \"${INSTALL_DATA_DIR}\".",
-               "WARN: filename:1: The INSTALL_*_DIR commands can only handle one directory at a time.")
+               "WARN: filename.mk:1: The INSTALL_*_DIR commands can only handle one directory at a time.")
 
        // A directory that is not found in the PLIST.
        test("${RUN} ${INSTALL_DATA_DIR} ${PREFIX}/share/other")
 
        t.CheckOutputLines(
-               "NOTE: filename:1: You can use \"INSTALLATION_DIRS+= share/other\" instead of \"${INSTALL_DATA_DIR}\".")
+               "NOTE: filename.mk:1: You can use \"INSTALLATION_DIRS+= share/other\" instead of \"${INSTALL_DATA_DIR}\".")
 
        G.Pkg = nil
 
@@ -315,7 +308,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        t := s.Init(c)
 
        test := func(shellCommand string) {
-               G.Mk = t.NewMkLines("filename",
+               G.Mk = t.NewMkLines("filename.mk",
                        "\t"+shellCommand)
 
                G.Mk.ForEach(func(mkline MkLine) {
@@ -327,8 +320,8 @@ func (s *Suite) Test_ShellLine_CheckShel
        test("${STRIP} executable")
 
        t.CheckOutputLines(
-               "WARN: filename:1: Unknown shell command \"${STRIP}\".",
-               "WARN: filename:1: STRIP is used but not defined.")
+               "WARN: filename.mk:1: Unknown shell command \"${STRIP}\".",
+               "WARN: filename.mk:1: STRIP is used but not defined.")
 
        t.SetUpVartypes()
 
@@ -430,7 +423,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        t := s.Init(c)
 
        t.SetUpVartypes()
-       G.Mk = t.NewMkLines("filename",
+       G.Mk = t.NewMkLines("filename.mk",
                "# dummy")
        shline := NewShellLine(G.Mk.mklines[0])
 
@@ -445,13 +438,13 @@ func (s *Suite) Test_ShellLine_CheckShel
        G.Mk.ForEach(func(mkline MkLine) { shline.CheckWord(text, false, RunTime) })
 
        t.CheckOutputLines(
-               "WARN: filename:1: Unknown shell command \"echo\".")
+               "WARN: filename.mk:1: Unknown shell command \"echo\".")
 
        G.Mk.ForEach(func(mkline MkLine) { shline.CheckShellCommandLine(text) })
 
        // No parse errors
        t.CheckOutputLines(
-               "WARN: filename:1: Unknown shell command \"echo\".")
+               "WARN: filename.mk:1: Unknown shell command \"echo\".")
 }
 
 func (s *Suite) Test_ShellLine_CheckShellCommandLine__dollar_without_variable(c *check.C) {
@@ -459,7 +452,7 @@ func (s *Suite) Test_ShellLine_CheckShel
 
        t.SetUpVartypes()
        t.SetUpTool("pax", "", AtRunTime)
-       G.Mk = t.NewMkLines("filename",
+       G.Mk = t.NewMkLines("filename.mk",
                "# dummy")
        shline := NewShellLine(G.Mk.mklines[0])
 
@@ -516,8 +509,7 @@ func (s *Suite) Test_ShellLine_CheckWord
 
        test("${COMMENT:Q}", true)
 
-       t.CheckOutputLines(
-               "WARN: dummy.mk:1: COMMENT may not be used in any file; it is a write-only variable.")
+       t.CheckOutputEmpty()
 
        test("\"${DISTINFO_FILE:Q}\"", true)
 
@@ -541,7 +533,7 @@ func (s *Suite) Test_ShellLine_CheckWord
 func (s *Suite) Test_ShellLine_CheckWord__dollar_without_variable(c *check.C) {
        t := s.Init(c)
 
-       shline := t.NewShellLine("filename", 1, "# dummy")
+       shline := t.NewShellLine("filename.mk", 1, "# dummy")
 
        shline.CheckWord("/.*~$$//g", false, RunTime) // Typical argument to pax(1).
 
@@ -552,7 +544,7 @@ func (s *Suite) Test_ShellLine_CheckWord
        t := s.Init(c)
 
        t.SetUpTool("find", "FIND", AtRunTime)
-       shline := t.NewShellLine("filename", 1, "\tfind . -exec rm -rf {} \\+")
+       shline := t.NewShellLine("filename.mk", 1, "\tfind . -exec rm -rf {} \\+")
 
        shline.CheckShellCommandLine(shline.mkline.ShellCommand())
 
@@ -563,21 +555,21 @@ func (s *Suite) Test_ShellLine_CheckWord
 func (s *Suite) Test_ShellLine_CheckWord__squot_dollar(c *check.C) {
        t := s.Init(c)
 
-       shline := t.NewShellLine("filename", 1, "\t'$")
+       shline := t.NewShellLine("filename.mk", 1, "\t'$")
 
        shline.CheckWord(shline.mkline.ShellCommand(), false, RunTime)
 
        // FIXME: Should be parsed correctly. Make passes the dollar through (probably),
        //  and the shell parser should complain about the unfinished string literal.
        t.CheckOutputLines(
-               "WARN: filename:1: Internal pkglint error in ShTokenizer.ShAtom at \"$\" (quoting=s).",
-               "WARN: filename:1: Internal pkglint error in ShellLine.CheckWord at \"'$\" (quoting=s), rest: $")
+               "WARN: filename.mk:1: Internal pkglint error in ShTokenizer.ShAtom at \"$\" (quoting=s).",
+               "WARN: filename.mk:1: Internal pkglint error in ShellLine.CheckWord at \"'$\" (quoting=s), rest: $")
 }
 
 func (s *Suite) Test_ShellLine_CheckWord__dquot_dollar(c *check.C) {
        t := s.Init(c)
 
-       shline := t.NewShellLine("filename", 1, "\t\"$")
+       shline := t.NewShellLine("filename.mk", 1, "\t\"$")
 
        shline.CheckWord(shline.mkline.ShellCommand(), false, RunTime)
 
@@ -589,12 +581,12 @@ func (s *Suite) Test_ShellLine_CheckWord
 func (s *Suite) Test_ShellLine_CheckWord__dollar_subshell(c *check.C) {
        t := s.Init(c)
 
-       shline := t.NewShellLine("filename", 1, "\t$$(echo output)")
+       shline := t.NewShellLine("filename.mk", 1, "\t$$(echo output)")
 
        shline.CheckWord(shline.mkline.ShellCommand(), false, RunTime)
 
        t.CheckOutputLines(
-               "WARN: filename:1: Invoking subshells via $(...) is not portable enough.")
+               "WARN: filename.mk:1: Invoking subshells via $(...) is not portable enough.")
 }
 
 func (s *Suite) Test_ShellLine_CheckWord__PKGMANDIR(c *check.C) {
@@ -709,9 +701,9 @@ func (s *Suite) Test_ShellLine_CheckShel
 
        echo := t.SetUpTool("echo", "ECHO", AtRunTime)
        echo.MustUseVarForm = true
-       G.Mk = t.NewMkLines("filename",
+       G.Mk = t.NewMkLines("filename.mk",
                "# dummy")
-       mkline := t.NewMkLine("filename", 3, "# dummy")
+       mkline := t.NewMkLine("filename.mk", 3, "# dummy")
 
        MkLineChecker{mkline}.checkText("echo \"hello, world\"")
 
@@ -720,7 +712,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        NewShellLine(mkline).CheckShellCommandLine("echo \"hello, world\"")
 
        t.CheckOutputLines(
-               "WARN: filename:3: Please use \"${ECHO}\" instead of \"echo\".")
+               "WARN: filename.mk:3: Please use \"${ECHO}\" instead of \"echo\".")
 }
 
 func (s *Suite) Test_ShellLine_CheckShellCommandLine__shell_variables(c *check.C) {
@@ -762,21 +754,21 @@ func (s *Suite) Test_ShellLine_CheckShel
 func (s *Suite) Test_ShellLine_checkInstallCommand(c *check.C) {
        t := s.Init(c)
 
-       G.Mk = t.NewMkLines("filename",
+       G.Mk = t.NewMkLines("filename.mk",
                "# dummy")
        G.Mk.target = "do-install"
 
-       shline := t.NewShellLine("filename", 1, "\tdummy")
+       shline := t.NewShellLine("filename.mk", 1, "\tdummy")
 
        shline.checkInstallCommand("sed")
 
        t.CheckOutputLines(
-               "WARN: filename:1: The shell command \"sed\" should not be used in the install phase.")
+               "WARN: filename.mk:1: The shell command \"sed\" should not be used in the install phase.")
 
        shline.checkInstallCommand("cp")
 
        t.CheckOutputLines(
-               "WARN: filename:1: ${CP} should not be used to install files.")
+               "WARN: filename.mk:1: ${CP} should not be used to install files.")
 }
 
 func (s *Suite) Test_splitIntoMkWords(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/substcontext.go
diff -u pkgsrc/pkgtools/pkglint/files/substcontext.go:1.21 pkgsrc/pkgtools/pkglint/files/substcontext.go:1.22
--- pkgsrc/pkgtools/pkglint/files/substcontext.go:1.21  Sat Jan 26 16:31:33 2019
+++ pkgsrc/pkgtools/pkglint/files/substcontext.go       Sun Mar 10 19:01:50 2019
@@ -283,19 +283,27 @@ func (ctx *SubstContext) suggestSubstVar
                        continue
                }
 
+               varop := sprintf("SUBST_VARS.%s%s%s",
+                       ctx.id,
+                       ifelseStr(hasSuffix(ctx.id, "+"), " ", ""),
+                       ifelseStr(ctx.curr.seenVars, "+=", "="))
+
                fix := mkline.Autofix()
-               fix.Notef("The substitution command %q can be replaced with \"SUBST_VARS.%s+= %s\".", token, ctx.id, varname)
+               fix.Notef("The substitution command %q can be replaced with \"%s %s\".",
+                       token, varop, varname)
                fix.Explain(
                        "Replacing @VAR@ with ${VAR} is such a typical pattern that pkgsrc has built-in support for it,",
                        "requiring only the variable name instead of the full sed command.")
                if mkline.VarassignComment() == "" && len(tokens) == 2 && tokens[0] == "-e" {
                        // TODO: Extract the alignment computation somewhere else, so that it is generally available.
                        alignBefore := tabWidth(mkline.ValueAlign())
-                       alignAfter := tabWidth(sprintf("SUBST_VARS.%s+=\t", ctx.id))
+                       alignAfter := tabWidth(varop + "\t")
                        tabs := strings.Repeat("\t", imax((alignAfter-alignBefore)/8, 0))
-                       fix.Replace(mkline.Text, sprintf("SUBST_VARS.%s+=\t%s%s", ctx.id, tabs, varname))
+                       fix.Replace(mkline.Text, varop+"\t"+tabs+varname)
                }
                fix.Anyway()
                fix.Apply()
+
+               ctx.curr.seenVars = true
        }
 }

Index: pkgsrc/pkgtools/pkglint/files/substcontext_test.go
diff -u pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.22 pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.23
--- pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.22     Thu Feb 21 22:49:03 2019
+++ pkgsrc/pkgtools/pkglint/files/substcontext_test.go  Sun Mar 10 19:01:50 2019
@@ -31,7 +31,7 @@ func (s *Suite) Test_SubstContext__incom
 
        t.CheckOutputLines(
                "NOTE: Makefile:13: The substitution command \"s,@PREFIX@,${PREFIX},g\" "+
-                       "can be replaced with \"SUBST_VARS.interp+= PREFIX\".",
+                       "can be replaced with \"SUBST_VARS.interp= PREFIX\".",
                "WARN: Makefile:14: Incomplete SUBST block: SUBST_STAGE.interp missing.")
 }
 
@@ -56,7 +56,7 @@ func (s *Suite) Test_SubstContext__compl
 
        t.CheckOutputLines(
                "NOTE: Makefile:13: The substitution command \"s,@PREFIX@,${PREFIX},g\" " +
-                       "can be replaced with \"SUBST_VARS.p+= PREFIX\".")
+                       "can be replaced with \"SUBST_VARS.p= PREFIX\".")
 }
 
 func (s *Suite) Test_SubstContext__OPSYSVARS(c *check.C) {
@@ -78,7 +78,7 @@ func (s *Suite) Test_SubstContext__OPSYS
 
        t.CheckOutputLines(
                "NOTE: Makefile:14: The substitution command \"s,@PREFIX@,${PREFIX},g\" " +
-                       "can be replaced with \"SUBST_VARS.prefix+= PREFIX\".")
+                       "can be replaced with \"SUBST_VARS.prefix= PREFIX\".")
 }
 
 func (s *Suite) Test_SubstContext__no_class(c *check.C) {
@@ -390,7 +390,7 @@ func (s *Suite) Test_SubstContext_sugges
        t.CheckOutputLines(
                "WARN: subst.mk:6: Please use ${SH:Q} instead of ${SH}.",
                "NOTE: subst.mk:6: The substitution command \"s,@SH@,${SH},g\" "+
-                       "can be replaced with \"SUBST_VARS.test+= SH\".",
+                       "can be replaced with \"SUBST_VARS.test= SH\".",
                "NOTE: subst.mk:7: The substitution command \"s,@SH@,${SH:Q},g\" "+
                        "can be replaced with \"SUBST_VARS.test+= SH\".",
                "WARN: subst.mk:8: Please use ${SH:T:Q} instead of ${SH:T}.",
@@ -416,9 +416,9 @@ func (s *Suite) Test_SubstContext_sugges
 
        t.CheckOutputLines(
                "NOTE: subst.mk:6: The substitution command \"s,@SH@,${SH},g\" "+
-                       "can be replaced with \"SUBST_VARS.test+= SH\".",
+                       "can be replaced with \"SUBST_VARS.test= SH\".",
                "AUTOFIX: subst.mk:6: Replacing \"SUBST_SED.test+=\\t-e s,@SH@,${SH},g\" "+
-                       "with \"SUBST_VARS.test+=\\tSH\".",
+                       "with \"SUBST_VARS.test=\\tSH\".",
                "NOTE: subst.mk:7: The substitution command \"s,@SH@,${SH:Q},g\" "+
                        "can be replaced with \"SUBST_VARS.test+= SH\".",
                "AUTOFIX: subst.mk:7: Replacing \"SUBST_SED.test+=\\t-e s,@SH@,${SH:Q},g\" "+
@@ -441,6 +441,46 @@ func (s *Suite) Test_SubstContext_sugges
                        "with \"SUBST_VARS.test+=\\tSH\".")
 }
 
+// If the SUBST_CLASS identifier ends with a plus, the generated code must
+// use the correct assignment operator and be nicely formatted.
+func (s *Suite) Test_SubstContext_suggestSubstVars__plus(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpTool("sh", "SH", AtRunTime)
+
+       mklines := t.NewMkLines("subst.mk",
+               MkRcsID,
+               "",
+               "SUBST_CLASSES+=\t\tgtk+",
+               "SUBST_STAGE.gtk+ =\tpre-configure",
+               "SUBST_FILES.gtk+ =\tfilename",
+               "SUBST_SED.gtk+ +=\t-e s,@SH@,${SH:Q},g",
+               "SUBST_SED.gtk+ +=\t-e s,@SH@,${SH:Q},g")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "NOTE: subst.mk:6: The substitution command \"s,@SH@,${SH:Q},g\" "+
+                       "can be replaced with \"SUBST_VARS.gtk+ = SH\".",
+               "NOTE: subst.mk:7: The substitution command \"s,@SH@,${SH:Q},g\" "+
+                       "can be replaced with \"SUBST_VARS.gtk+ += SH\".")
+
+       t.SetUpCommandLine("--show-autofix")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "NOTE: subst.mk:6: The substitution command \"s,@SH@,${SH:Q},g\" "+
+                       "can be replaced with \"SUBST_VARS.gtk+ = SH\".",
+               "AUTOFIX: subst.mk:6: Replacing \"SUBST_SED.gtk+ +=\\t-e s,@SH@,${SH:Q},g\" "+
+                       "with \"SUBST_VARS.gtk+ =\\tSH\".",
+               "NOTE: subst.mk:7: The substitution command \"s,@SH@,${SH:Q},g\" "+
+                       "can be replaced with \"SUBST_VARS.gtk+ += SH\".",
+               "AUTOFIX: subst.mk:7: Replacing \"SUBST_SED.gtk+ +=\\t-e s,@SH@,${SH:Q},g\" "+
+                       "with \"SUBST_VARS.gtk+ +=\\tSH\".")
+}
+
 // simulateSubstLines only tests some of the inner workings of SubstContext.
 // It is not realistic for all cases. If in doubt, use MkLines.Check.
 func simulateSubstLines(t *Tester, texts ...string) {

Index: pkgsrc/pkgtools/pkglint/files/util.go
diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.38 pkgsrc/pkgtools/pkglint/files/util.go:1.39
--- pkgsrc/pkgtools/pkglint/files/util.go:1.38  Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/util.go       Sun Mar 10 19:01:50 2019
@@ -349,32 +349,68 @@ func mkopSubst(s string, left bool, from
 
 // relpath returns the relative path from the directory "from"
 // to the filesystem entry "to".
-func relpath(from, to string) string {
+//
+// The relative path is built by going from the "from" directory via the
+// pkgsrc root to the "to" filename. This produces the form
+// "../../category/package" that is found in DEPENDS and .include lines.
+//
+// Both from and to are interpreted relative to the current working directory,
+// unless they are absolute paths.
+//
+// This function should only be used if the relative path from one file to
+// 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 relpath(from, to string) (result string) {
 
-       // From "dir" to "dir/subdir/...".
-       if hasPrefix(to, from) && len(to) > len(from)+1 && to[len(from)] == '/' {
-               return path.Clean(to[len(from)+1:])
+       if trace.Tracing {
+               defer trace.Call(from, to, trace.Result(&result))()
+       }
+
+       cfrom := cleanpath(from)
+       cto := cleanpath(to)
+
+       if cfrom == cto {
+               return "."
        }
 
-       // Take a shortcut for the most common variant in a complete pkgsrc scan,
-       // which is to resolve the relative path from a package to the pkgsrc root.
-       // This avoids unnecessary calls to the filesystem API.
-       if to == "." {
-               fromParts := strings.FieldsFunc(from, func(r rune) bool { return r == '/' })
-               if len(fromParts) == 3 && !hasPrefix(fromParts[0], ".") && !hasPrefix(fromParts[1], ".") && fromParts[2] == "." {
+       // Take a shortcut for the common case from "dir" to "dir/subdir/...".
+       if hasPrefix(cto, cfrom) && len(cto) > len(cfrom)+1 && cto[len(cfrom)] == '/' {
+               return cleanpath(cto[len(cfrom)+1:])
+       }
+
+       // Take a shortcut for the common case from "category/package" to ".".
+       // This is the most common variant in a complete pkgsrc scan.
+       if cto == "." {
+               fromParts := strings.FieldsFunc(cfrom, func(r rune) bool { return r == '/' })
+               if len(fromParts) == 2 && !hasPrefix(fromParts[0], ".") && !hasPrefix(fromParts[1], ".") {
                        return "../.."
                }
        }
 
-       absFrom := abspath(from)
-       absTo := abspath(to)
-       rel, err := filepath.Rel(absFrom, absTo)
-       G.AssertNil(err, "relpath %q %q", from, to)
-       result := filepath.ToSlash(rel)
+       if cfrom == "." && !filepath.IsAbs(cto) {
+               return path.Clean(cto)
+       }
+
+       absFrom := abspath(cfrom)
+       absTopdir := abspath(G.Pkgsrc.topdir)
+       absTo := abspath(cto)
+
+       toTop, err := filepath.Rel(absFrom, absTopdir)
+       G.AssertNil(err, "relpath from %q to topdir %q", absFrom, absTopdir)
+
+       fromTop, err := filepath.Rel(absTopdir, absTo)
+       G.AssertNil(err, "relpath from topdir %q to %q", absTopdir, absTo)
+
+       result = cleanpath(filepath.ToSlash(toTop) + "/" + filepath.ToSlash(fromTop))
+
        if trace.Tracing {
-               trace.Stepf("relpath from %q to %q = %q", from, to, result)
+               trace.Stepf("relpath from %q to %q = %q", cfrom, cto, result)
        }
-       return result
+       return
 }
 
 func abspath(filename string) string {
@@ -401,6 +437,10 @@ func cleanpath(filename string) string {
                }
        }
 
+       for len(parts) > 1 && parts[len(parts)-1] == "." {
+               parts = parts[:len(parts)-1]
+       }
+
        for i := 2; i+3 < len(parts); /* nothing */ {
                if parts[i] != ".." && parts[i+1] != ".." && parts[i+2] == ".." && parts[i+3] == ".." {
                        if i+4 == len(parts) || parts[i+4] != ".." {
@@ -441,6 +481,11 @@ func (o *Once) FirstTimeSlice(whats ...s
        return o.check(crc.Sum64())
 }
 
+func (o *Once) Seen(what string) bool {
+       _, seen := o.seen[crc64.Checksum([]byte(what), crc64.MakeTable(crc64.ECMA))]
+       return seen
+}
+
 func (o *Once) check(key uint64) bool {
        if _, ok := o.seen[key]; ok {
                return false
@@ -454,6 +499,13 @@ func (o *Once) check(key uint64) bool {
 
 // Scope remembers which variables are defined and which are used
 // in a certain scope, such as a package or a file.
+//
+// TODO: Decide whether the scope should consider variable assignments
+//  from the pkgsrc infrastructure. For Package.checkGnuConfigureUseLanguages
+//  it would be better to ignore them completely.
+//
+// TODO: Merge this code with Var, which defines essentially the
+//  same features.
 type Scope struct {
        firstDef map[string]MkLine // TODO: Can this be removed?
        lastDef  map[string]MkLine
@@ -579,8 +631,9 @@ func (s *Scope) FirstDefinition(varname 
        mkline := s.firstDef[varname]
        if mkline != nil && mkline.IsVarassign() {
                lastLine := s.LastDefinition(varname)
-               if lastLine != mkline {
-                       //mkline.Notef("FirstDefinition differs from LastDefinition in %s.", mkline.RefTo(lastLine))
+               if trace.Tracing && lastLine != mkline {
+                       trace.Stepf("%s: FirstDefinition differs from LastDefinition in %s.",
+                               mkline.String(), mkline.RefTo(lastLine))
                }
                return mkline
        }
@@ -722,148 +775,6 @@ func naturalLess(str1, str2 string) bool
        return len1 < len2
 }
 
-// RedundantScope checks for redundant variable definitions and for variables
-// that are accidentally overwritten. It tries to be as correct as possible
-// by not flagging anything that is defined conditionally.
-//
-// There may be some edge cases though like defining PKGNAME, then evaluating
-// it using :=, then defining it again. This pattern is so error-prone that
-// it should not appear in pkgsrc at all, thus pkglint doesn't even expect it.
-// (Well, except for the PKGNAME case, but that's deep in the infrastructure
-// and only affects the "nb13" extension.)
-type RedundantScope struct {
-       vars        map[string]*redundantScopeVarinfo
-       dirLevel    int // The number of enclosing directives (.if, .for).
-       includePath includePath
-       OnRedundant func(old, new MkLine)
-       OnOverwrite func(old, new MkLine)
-}
-type redundantScopeVarinfo struct {
-       mkline      MkLine
-       includePath includePath
-       value       string
-}
-
-func NewRedundantScope() *RedundantScope {
-       return &RedundantScope{vars: make(map[string]*redundantScopeVarinfo)}
-}
-
-func (s *RedundantScope) Handle(mkline MkLine) {
-       if mkline.firstLine == 1 {
-               s.includePath.push(mkline.Location.Filename)
-       } else {
-               s.includePath.popUntil(mkline.Location.Filename)
-       }
-
-       switch {
-       case mkline.IsVarassign():
-               varname := mkline.Varname()
-               if s.dirLevel != 0 {
-                       // Since the variable is defined or assigned conditionally,
-                       // it becomes too complicated for pkglint to check all possible
-                       // code paths. Therefore ignore the variable from now on.
-                       s.vars[varname] = nil
-                       break
-               }
-
-               op := mkline.Op()
-               value := mkline.Value()
-               valueNovar := mkline.WithoutMakeVariables(value)
-               if op == opAssignEval && value == valueNovar {
-                       op = /* effectively */ opAssign
-               }
-
-               existing, found := s.vars[varname]
-               if !found {
-                       if op == opAssignShell || op == opAssignEval {
-                               s.vars[varname] = nil // Won't be checked further.
-                       } else {
-                               if op == opAssignAppend {
-                                       value = " " + value
-                               }
-                               s.vars[varname] = &redundantScopeVarinfo{mkline, s.includePath.copy(), value}
-                       }
-
-               } else if existing != nil {
-                       if op == opAssign && existing.value == value {
-                               op = /* effectively */ opAssignDefault
-                       }
-
-                       switch op {
-                       case opAssign:
-                               if s.includePath.includes(existing.includePath) {
-                                       // This is the usual pattern of including a file and
-                                       // then overwriting some of them. Although technically
-                                       // this overwrites the previous definition, it is not
-                                       // worth a warning since this is used a lot and
-                                       // intentionally.
-                               } else {
-                                       s.OnOverwrite(existing.mkline, mkline)
-                               }
-                               existing.value = value
-                       case opAssignAppend:
-                               existing.value += " " + value
-                       case opAssignDefault:
-                               if existing.includePath.includes(s.includePath) {
-                                       s.OnRedundant(mkline, existing.mkline)
-                               } else if s.includePath.includes(existing.includePath) || s.includePath.equals(existing.includePath) {
-                                       s.OnRedundant(existing.mkline, mkline)
-                               }
-                       case opAssignShell, opAssignEval:
-                               s.vars[varname] = nil // Won't be checked further.
-                       }
-               }
-
-       case mkline.IsDirective():
-               switch mkline.Directive() {
-               case "for", "if", "ifdef", "ifndef":
-                       s.dirLevel++
-               case "endfor", "endif":
-                       s.dirLevel--
-               }
-       }
-}
-
-type includePath struct {
-       files []string
-}
-
-func (p *includePath) push(filename string) {
-       p.files = append(p.files, filename)
-}
-
-func (p *includePath) popUntil(filename string) {
-       for p.files[len(p.files)-1] != filename {
-               p.files = p.files[:len(p.files)-1]
-       }
-}
-
-func (p *includePath) includes(other includePath) bool {
-       for i, filename := range p.files {
-               if i < len(other.files) && other.files[i] == filename {
-                       continue
-               }
-               return false
-       }
-       return len(p.files) < len(other.files)
-}
-
-func (p *includePath) equals(other includePath) bool {
-       if len(p.files) != len(other.files) {
-               return false
-       }
-       for i, filename := range p.files {
-               if other.files[i] != filename {
-                       return false
-               }
-       }
-       return true
-}
-
-func (p *includePath) copy() includePath {
-       return includePath{append([]string(nil), p.files...)}
-}
-
 // IsPrefs returns whether the given file, when included, loads the user
 // preferences.
 func IsPrefs(filename string) bool {
@@ -1004,7 +915,6 @@ func seeGuide(sectionName, sectionID str
 func wrap(max int, lines ...string) []string {
        var wrapped []string
        var sb strings.Builder
-       nonSpace := textproc.Space.Inverse()
 
        for _, line := range lines {
 
@@ -1024,7 +934,7 @@ func wrap(max int, lines ...string) []st
                for !lexer.EOF() {
                        bol := len(lexer.Rest()) == len(line)
                        space := lexer.NextBytesSet(textproc.Space)
-                       word := lexer.NextBytesSet(nonSpace)
+                       word := lexer.NextBytesSet(notSpace)
 
                        if bol && sb.Len() > 0 {
                                space = " "
@@ -1169,3 +1079,26 @@ func (si *StringInterner) Intern(str str
        si.strs[key] = key
        return key
 }
+
+// StringSets stores unique strings in insertion order.
+type StringSet struct {
+       Elements []string
+       seen     map[string]struct{}
+}
+
+func NewStringSet() StringSet {
+       return StringSet{nil, make(map[string]struct{})}
+}
+
+func (s *StringSet) Add(element string) {
+       if _, found := s.seen[element]; !found {
+               s.seen[element] = struct{}{}
+               s.Elements = append(s.Elements, element)
+       }
+}
+
+func (s *StringSet) AddAll(elements []string) {
+       for _, element := range elements {
+               s.Add(element)
+       }
+}

Index: pkgsrc/pkgtools/pkglint/files/var.go
diff -u pkgsrc/pkgtools/pkglint/files/var.go:1.1 pkgsrc/pkgtools/pkglint/files/var.go:1.2
--- pkgsrc/pkgtools/pkglint/files/var.go:1.1    Mon Dec 17 00:15:39 2018
+++ pkgsrc/pkgtools/pkglint/files/var.go        Sun Mar 10 19:01:50 2019
@@ -2,23 +2,275 @@ package pkglint
 
 // Var describes a variable in a Makefile snippet.
 //
-// TODO: Remove this type in June 2019 if it is still a stub.
+// It keeps track of all places where the variable is accessed or modified (see
+// ReadLocations, WriteLocations) and provides information for further static
+// analysis, such as:
+//
+// * Whether the variable value is constant, and if so, what the constant value
+// is (see Constant, ConstantValue).
+//
+// * What its (approximated) value is, either including values from the pkgsrc
+// infrastructure (see ValueInfra) or excluding them (Value).
+//
+// * On which other variables this variable depends (see Conditional,
+// ConditionalVars).
+//
+// TODO: Decide how to handle OPSYS-specific variables, such as LDFLAGS.SunOS.
+//
+// TODO: Decide how to handle parameterized variables, such as SUBST_MESSAGE.doc.
 type Var struct {
        Name string
-       Type *Vartype
+
+       //  0 = neither written nor read
+       //  1 = constant
+       //  2 = constant and read; further writes will make it non-constant
+       //  3 = not constant anymore
+       constantState uint8
+       constantValue string
+
+       value      string
+       valueInfra string
+
+       readLocations  []MkLine
+       writeLocations []MkLine
+
+       conditional     bool
+       conditionalVars StringSet
+
+       refs StringSet
 }
 
-func NewVar(name string) *Var { return &Var{name, nil} }
+func NewVar(name string) *Var {
+       return &Var{name, 0, "", "", "", nil, nil, false, NewStringSet(), NewStringSet()}
+}
 
-// Constant returns whether the variable is only ever assigned a single value,
-// without being dependent on any other variable.
+// Conditional returns whether the variable value depends on other variables.
+func (v *Var) Conditional() bool {
+       return v.conditional
+}
+
+// ConditionalVars returns all variables in conditions on which the value of
+// this variable depends.
 //
-// Multiple assignments (such as VAR=1, VAR+=2, VAR+=3) are considered constant
-// as well, as long as the variable is not used in-between these assignments.
-// That is, no .include or .if may appear there, and none of the ::= modifiers may
-// be involved.
+// The returned slice must not be modified.
+func (v *Var) ConditionalVars() []string {
+       return v.conditionalVars.Elements
+}
+
+// Refs returns all variables on which this variable depends. These are:
+//
+// Variables that are referenced in the value, such as in VAR=${OTHER}.
 //
-// Simple .for loops that append to the variable are ok though.
-func (v *Var) Constant() bool { return false }
+// Variables that are used in conditions that enclose one of the assignments
+// to this variable, such as .if ${OPSYS} == NetBSD.
+//
+// Variables that are used in .for loops in which this variable is assigned
+// a value, such as DIRS in:
+//  .for dir in ${DIRS}
+//  VAR+=${dir}
+//  .endfor
+func (v *Var) Refs() []string {
+       return v.refs.Elements
+}
 
-func (v *Var) ConstantValue() string { return "" }
+// AddRef marks this variable as being dependent on the given variable name.
+// This can be used for the .for loops mentioned in Refs.
+func (v *Var) AddRef(varname string) {
+       v.refs.Add(varname)
+}
+
+// Constant returns whether the variable's value is a constant.
+// It may reference other variables since these references are evaluated
+// lazily, when the variable value is actually needed.
+//
+// Multiple assignments (such as VAR=1, VAR+=2, VAR+=3) are considered to
+// form a single constant as well, as long as the variable is not read before
+// or in-between these assignments. The definition of "read" is very strict
+// here since every mention of the variable counts. This may prevent some
+// essentially constant values from being detected as such, but these special
+// cases may be implemented later.
+//
+// TODO: Simple .for loops that append to the variable are ok as well.
+//  (This needs to be worded more precisely since that part potentially
+//  adds a lot of complexity to the whole data structure.)
+//
+// Variable assignments in the pkgsrc infrastructure are taken into account
+// for determining whether a variable is constant.
+func (v *Var) Constant() bool {
+       return v.constantState == 1 || v.constantState == 2
+}
+
+// ConstantValue returns the constant value of the variable.
+// It is only allowed when Constant() returns true.
+//
+// Variable assignments in the pkgsrc infrastructure are taken into account
+// for determining the constant value.
+func (v *Var) ConstantValue() string {
+       G.Assertf(v.Constant(), "Variable must be constant.")
+       return v.constantValue
+}
+
+// Value returns the (approximated) value of the variable, taking into account
+// all variable assignments that happen outside the pkgsrc infrastructure.
+//
+// For variables that are conditionally assigned (as in .if/.else), the
+// returned value is not reliable. It may be the value from either branch, or
+// even the combined value of both branches.
+//
+// See Constant and ConstantValue for more reliable information.
+func (v *Var) Value() string {
+       return v.value
+}
+
+// ValueInfra returns the (approximated) value of the variable, taking into
+// account all variable assignments from the package, the user and the pkgsrc
+// infrastructure.
+//
+// For variables that are conditionally assigned (as in .if/.else), the
+// returned value is not reliable. It may be the value from either branch, or
+// even the combined value of both branches.
+//
+// See Constant and ConstantValue for more reliable information, but these
+// ignore assignments from the infrastructure.
+func (v *Var) ValueInfra() string {
+       return v.valueInfra
+}
+
+// ReadLocations returns the locations where the variable is read, such as
+// in ${VAR} or defined(VAR) or empty(VAR).
+//
+// Uses inside conditionals are included, no matter whether they are actually
+// reachable in practice.
+//
+// Indirect uses through other variables (such as VAR2=${VAR}, VAR3=${VAR2})
+// are not listed.
+//
+// Variable uses in the pkgsrc infrastructure are taken into account.
+func (v *Var) ReadLocations() []MkLine {
+       return v.readLocations
+}
+
+// WriteLocations returns the locations where the variable is modified.
+//
+// Assignments inside conditionals are included, no matter whether they are actually
+// reachable in practice.
+//
+// Variable assignments in the pkgsrc infrastructure are taken into account.
+func (v *Var) WriteLocations() []MkLine {
+       return v.writeLocations
+}
+
+func (v *Var) Read(mkline MkLine) {
+       v.readLocations = append(v.readLocations, mkline)
+       v.constantState = [...]uint8{3, 2, 2, 3}[v.constantState]
+}
+
+// Write marks the variable as being assigned in the given line.
+// Only standard assignments (VAR=value) are handled.
+// Side-effect assignments (${VAR::=value}) are not handled here since
+// they don't occur in practice.
+func (v *Var) Write(mkline MkLine, conditional bool, conditionVarnames ...string) {
+       G.Assertf(mkline.Varname() == v.Name, "wrong variable name")
+
+       v.writeLocations = append(v.writeLocations, mkline)
+
+       if conditional || len(conditionVarnames) > 0 {
+               v.conditional = true
+       }
+       v.conditionalVars.AddAll(conditionVarnames)
+
+       v.refs.AddAll(mkline.DetermineUsedVariables())
+       v.refs.AddAll(conditionVarnames)
+
+       v.update(mkline, &v.valueInfra)
+       if !G.Pkgsrc.IsInfra(mkline.Line.Filename) {
+               v.update(mkline, &v.value)
+       }
+
+       v.updateConstantValue(mkline)
+}
+
+func (v *Var) update(mkline MkLine, update *string) {
+       firstWrite := len(v.writeLocations) == 1
+       if v.Conditional() && !firstWrite {
+               return
+       }
+
+       value := mkline.Value()
+       switch mkline.Op() {
+       case opAssign, opAssignEval:
+               *update = value
+
+       case opAssignDefault:
+               if firstWrite {
+                       *update = value
+               }
+
+       case opAssignAppend:
+               *update += " " + value
+
+       case opAssignShell:
+               // Ignore these for now.
+               // Later it might be useful to parse the shell commands to
+               // evaluate simple commands like "test && echo yes || echo no".
+       }
+}
+
+func (v *Var) updateConstantValue(mkline MkLine) {
+       if v.constantState == 3 {
+               return
+       }
+
+       // Even if the variable references other variables, this does not
+       // influence whether the variable is considered constant. (Except
+       // for the := operator.)
+       //
+       // Strictly speaking, the referenced variables must be still
+       // be constant at the end of loading the complete package.
+       // (And even after that, because of the ::= modifier. But luckily
+       // almost no one knows that modifier.)
+
+       if v.Conditional() {
+               v.constantState = 3
+               v.constantValue = ""
+               return
+       }
+
+       value := mkline.Value()
+       switch mkline.Op() {
+       case opAssign:
+               v.constantValue = value
+
+       case opAssignEval:
+               if value != mkline.WithoutMakeVariables(value) {
+                       // To leave the variable in the constant state, the current value
+                       // of the referenced variables would need to be resolved.
+                       //
+                       // This in turn requires the proper scope for resolving variable
+                       // references. Furthermore, the referenced variables must be
+                       // constant at this point. Later changes to these variables
+                       // can be ignored though.
+                       //
+                       // Because this sounds complicated to implement, the variable
+                       // is marked as non-constant for now.
+                       v.constantState = 3
+                       v.constantValue = ""
+               } else {
+                       v.constantValue = value
+               }
+
+       case opAssignDefault:
+               if v.constantState == 0 {
+                       v.constantValue = value
+               }
+
+       case opAssignAppend:
+               v.constantValue += " " + value
+
+       case opAssignShell:
+               v.constantState = 3
+               v.constantValue = ""
+       }
+
+       v.constantState = [...]uint8{1, 1, 3, 3}[v.constantState]
+}
Index: pkgsrc/pkgtools/pkglint/files/var_test.go
diff -u pkgsrc/pkgtools/pkglint/files/var_test.go:1.1 pkgsrc/pkgtools/pkglint/files/var_test.go:1.2
--- pkgsrc/pkgtools/pkglint/files/var_test.go:1.1       Mon Dec 17 00:15:39 2018
+++ pkgsrc/pkgtools/pkglint/files/var_test.go   Sun Mar 10 19:01:50 2019
@@ -2,18 +2,336 @@ package pkglint
 
 import "gopkg.in/check.v1"
 
-func (s *Suite) Test_Var_Constant(c *check.C) {
+func (s *Suite) Test_Var_ConstantValue__assign(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME=\toverwritten"), false)
+
+       t.Check(v.ConstantValue(), equals, "overwritten")
+}
+
+// Variables that reference other variable are considered constants.
+// Even if these referenced variables change their value in-between,
+// this does not affect the constant-ness of this variable, since the
+// references are resolved lazily.
+func (s *Suite) Test_Var_ConstantValue__assign_reference(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME=\t${OTHER}"), false)
+
+       t.Check(v.Constant(), equals, true)
+}
+
+func (s *Suite) Test_Var_ConstantValue__assign_eval_reference(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME:=\t${OTHER}"), false)
+
+       // To analyze this case correctly, pkglint would have to know
+       // the current value of ${OTHER} in line 124. For that it would
+       // need the complete scope including all other variables.
+       //
+       // As of March 2019 this is not implemented, therefore pkglint
+       // doesn't treat the variable as constant, to prevent wrong warnings.
+       t.Check(v.Constant(), equals, false)
+}
+
+func (s *Suite) Test_Var_ConstantValue__assign_conditional(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       t.Check(v.ConditionalVars(), check.IsNil)
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME=\tconditional"), true, "OPSYS")
+
+       t.Check(v.Constant(), equals, false)
+}
+
+func (s *Suite) Test_Var_ConstantValue__default(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME?=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME?=\tignored"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+}
+
+func (s *Suite) Test_Var_ConstantValue__eval_then_default(c *check.C) {
+       t := s.Init(c)
+
        v := NewVar("VARNAME")
 
-       // FIXME: Replace this test with an actual use case.
+       v.Write(t.NewMkLine("buildlink3.mk", 123, "VARNAME:=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       v.Write(t.NewMkLine("builtin.mk", 124, "VARNAME?=\tignored"), false)
 
-       c.Check(v.Constant(), equals, false)
+       t.Check(v.ConstantValue(), equals, "value")
 }
 
-func (s *Suite) Test_Var_ConstantValue(c *check.C) {
+func (s *Suite) Test_Var_ConstantValue__append(c *check.C) {
+       t := s.Init(c)
+
        v := NewVar("VARNAME")
 
-       // FIXME: Replace this test with an actual use case.
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME+=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, " value")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME+=\tappended"), false)
+
+       t.Check(v.ConstantValue(), equals, " value appended")
+}
+
+func (s *Suite) Test_Var_ConstantValue__eval(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME:=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME:=\toverwritten"), false)
+
+       t.Check(v.ConstantValue(), equals, "overwritten")
+}
+
+// Variables that are based on running shell commands are never constant.
+func (s *Suite) Test_Var_ConstantValue__shell(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME!=\techo hello"), false)
+
+       t.Check(v.Constant(), equals, false)
+}
+
+func (s *Suite) Test_Var_ConstantValue__referenced_before(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       // Since the value of VARNAME escapes here, the value is not
+       // guaranteed to be the same in all evaluations of ${VARNAME}.
+       // For example, OTHER may be used at load time in an .if
+       // condition.
+       v.Read(t.NewMkLine("readwrite.mk", 123, "OTHER=\t${VARNAME}"))
+
+       t.Check(v.Constant(), equals, false)
+
+       v.Write(t.NewMkLine("readwrite.mk", 124, "VARNAME=\tvalue"), false)
+
+       t.Check(v.Constant(), equals, false)
+}
+
+func (s *Suite) Test_Var_ConstantValue__referenced_in_between(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("readwrite.mk", 123, "VARNAME=\tvalue"), false)
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       // Since the value of VARNAME escapes here, the value is not
+       // guaranteed to be the same in all evaluations of ${VARNAME}.
+       // For example, OTHER may be used at load time in an .if
+       // condition.
+       v.Read(t.NewMkLine("readwrite.mk", 124, "OTHER=\t${VARNAME}"))
+
+       t.Check(v.ConstantValue(), equals, "value")
+
+       v.Write(t.NewMkLine("write.mk", 125, "VARNAME=\toverwritten"), false)
+
+       t.Check(v.Constant(), equals, false)
+}
+
+func (s *Suite) Test_Var_ConditionalVars(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       t.Check(v.Conditional(), equals, false)
+       t.Check(v.ConditionalVars(), check.IsNil)
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME=\tconditional"), true, "OPSYS")
+
+       t.Check(v.Constant(), equals, false)
+       t.Check(v.Conditional(), equals, true)
+       t.Check(v.ConditionalVars(), deepEquals, []string{"OPSYS"})
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME=\tconditional"), true, "OPSYS")
+
+       t.Check(v.Conditional(), equals, true)
+       t.Check(v.ConditionalVars(), deepEquals, []string{"OPSYS"})
+}
+
+func (s *Suite) Test_Var_Value__initial_conditional_write(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME:=\toverwritten conditionally"), true, "OPSYS")
+
+       // Since there is no previous value, the simplest choice is to just
+       // take the first seen value, no matter if that value is conditional
+       // or not.
+       t.Check(v.Conditional(), equals, true)
+       t.Check(v.Constant(), equals, false)
+       t.Check(v.Value(), equals, "overwritten conditionally")
+}
+
+func (s *Suite) Test_Var_Value__conditional_write_after_unconditional(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine("write.mk", 123, "VARNAME=\tvalue"), false)
+
+       t.Check(v.Value(), equals, "value")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME+=\tappended"), false)
+
+       t.Check(v.Value(), equals, "value appended")
+
+       v.Write(t.NewMkLine("write.mk", 124, "VARNAME:=\toverwritten conditionally"), true, "OPSYS")
+
+       // When there is a previous value, it's probably best to keep
+       // that value since this way the following code results in the
+       // most generic value:
+       //  VAR=    generic
+       //  .if ${OPSYS} == NetBSD
+       //  VAR=    specific
+       //  .endif
+       // The value stays the same, still it is marked as conditional and therefore
+       // not constant anymore.
+       t.Check(v.Conditional(), equals, true)
+       t.Check(v.Constant(), equals, false)
+       t.Check(v.Value(), equals, "value appended")
+}
+
+func (s *Suite) Test_Var_Value__infrastructure(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine(t.File("write.mk"), 123, "VARNAME=\tvalue"), false)
+
+       t.Check(v.Value(), equals, "value")
+
+       v.Write(t.NewMkLine(t.File("mk/write.mk"), 123, "VARNAME=\tinfra"), false)
+
+       t.Check(v.Value(), equals, "value")
+
+       v.Write(t.NewMkLine(t.File("wip/mk/write.mk"), 123, "VARNAME=\twip infra"), false)
+
+       t.Check(v.Value(), equals, "value")
+}
+
+func (s *Suite) Test_Var_ValueInfra(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VARNAME")
+
+       v.Write(t.NewMkLine(t.File("write.mk"), 123, "VARNAME=\tvalue"), false)
+
+       t.Check(v.ValueInfra(), equals, "value")
+
+       v.Write(t.NewMkLine(t.File("mk/write.mk"), 123, "VARNAME=\tinfra"), false)
+
+       t.Check(v.ValueInfra(), equals, "infra")
+
+       v.Write(t.NewMkLine(t.File("wip/mk/write.mk"), 123, "VARNAME=\twip infra"), false)
+
+       t.Check(v.ValueInfra(), equals, "wip infra")
+}
+
+func (s *Suite) Test_Var_ReadLocations(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VAR")
+
+       t.Check(v.ReadLocations(), check.IsNil)
+
+       mkline123 := t.NewMkLine("read.mk", 123, "OTHER=\t${VAR}")
+       v.Read(mkline123)
+
+       t.Check(v.ReadLocations(), deepEquals, []MkLine{mkline123})
+
+       mkline124 := t.NewMkLine("read.mk", 124, "OTHER=\t${VAR} ${VAR}")
+       v.Read(mkline124)
+       v.Read(mkline124)
+
+       // For now, count every read of the variable. I'm not yet sure
+       // whether that's the best way or whether to make the lines unique.
+       t.Check(v.ReadLocations(), deepEquals, []MkLine{mkline123, mkline124, mkline124})
+}
+
+func (s *Suite) Test_Var_WriteLocations(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VAR")
+
+       t.Check(v.WriteLocations(), check.IsNil)
+
+       mkline123 := t.NewMkLine("write.mk", 123, "VAR=\tvalue")
+       v.Write(mkline123, false)
+
+       t.Check(v.WriteLocations(), deepEquals, []MkLine{mkline123})
+
+       // Multiple writes from the same line may happen because of a .for loop.
+       mkline125 := t.NewMkLine("write.mk", 125, "VAR+=\t${var}")
+       v.Write(mkline125, false)
+       v.Write(mkline125, false)
+
+       // For now, count every write of the variable. I'm not yet sure
+       // whether that's the best way or whether to make the lines unique.
+       t.Check(v.WriteLocations(), deepEquals, []MkLine{mkline123, mkline125, mkline125})
+}
+
+func (s *Suite) Test_Var_Refs(c *check.C) {
+       t := s.Init(c)
+
+       v := NewVar("VAR")
+
+       t.Check(v.Refs(), check.IsNil)
+
+       // The referenced variables are taken from the mkline.
+       // They don't need to be passed separately.
+       v.Write(t.NewMkLine("write.mk", 123, "VAR=${OTHER} ${${OPSYS} == NetBSD :? ${THEN} : ${ELSE}}"), true, "COND")
+
+       v.AddRef("FOR")
 
-       c.Check(v.ConstantValue(), equals, "")
+       t.Check(v.Refs(), deepEquals, []string{"OTHER", "OPSYS", "THEN", "ELSE", "COND", "FOR"})
 }

Index: pkgsrc/pkgtools/pkglint/files/vardefs.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.55 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.56
--- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.55       Sat Jan 26 16:31:33 2019
+++ pkgsrc/pkgtools/pkglint/files/vardefs.go    Sun Mar 10 19:01:50 2019
@@ -37,7 +37,7 @@ func (src *Pkgsrc) InitVartypes() {
        pkg := func(varname string, kindOfList KindOfList, checker *BasicType) {
                acl(varname, kindOfList, checker, ""+
                        "Makefile: set, use; "+
-                       "buildlink3.mk, builtin.mk:; "+
+                       "buildlink3.mk, builtin.mk: none; "+
                        "Makefile.*, *.mk: default, set, use")
        }
 
@@ -45,7 +45,7 @@ func (src *Pkgsrc) InitVartypes() {
        pkgload := func(varname string, kindOfList KindOfList, checker *BasicType) {
                acl(varname, kindOfList, checker, ""+
                        "Makefile: set, use, use-loadtime; "+
-                       "buildlink3.mk, builtin.mk:; "+
+                       "buildlink3.mk, builtin.mk: none; "+
                        "Makefile.*, *.mk: default, set, use, use-loadtime")
        }
 
@@ -54,21 +54,29 @@ func (src *Pkgsrc) InitVartypes() {
        pkglist := func(varname string, kindOfList KindOfList, checker *BasicType) {
                acl(varname, kindOfList, checker, ""+
                        "Makefile, Makefile.common, options.mk: append, default, set, use; "+
-                       "buildlink3.mk, builtin.mk:; "+
+                       "buildlink3.mk, builtin.mk: none; "+
                        "*.mk: append, default, use")
        }
 
+       // Some package-defined lists may also be appended in buildlink3.mk files,
+       // for example platform-specific CFLAGS and LDFLAGS.
+       pkglistbl3 := func(varname string, kindOfList KindOfList, checker *BasicType) {
+               acl(varname, kindOfList, checker, ""+
+                       "Makefile, Makefile.common, options.mk: append, default, set, use; "+
+                       "buildlink3.mk, builtin.mk, *.mk: append, default, use")
+       }
+
        // sys declares a user-defined or system-defined variable that must not be modified by packages.
        //
        // It also must not be used in buildlink3.mk and builtin.mk files or at load-time,
        // since the system/user preferences may not have been loaded when these files are included.
        sys := func(varname string, kindOfList KindOfList, checker *BasicType) {
-               acl(varname, kindOfList, checker, "buildlink3.mk:; *: use")
+               acl(varname, kindOfList, checker, "buildlink3.mk: none; *: use")
        }
 
        // usr declares a user-defined variable that must not be modified by packages.
        usr := func(varname string, kindOfList KindOfList, checker *BasicType) {
-               acl(varname, kindOfList, checker, "buildlink3.mk:; *: use-loadtime, use")
+               acl(varname, kindOfList, checker, "buildlink3.mk: none; *: use-loadtime, use")
        }
 
        // sysload declares a system-provided variable that may already be used at load time.
@@ -81,7 +89,7 @@ func (src *Pkgsrc) InitVartypes() {
        }
 
        cmdline := func(varname string, kindOfList KindOfList, checker *BasicType) {
-               acl(varname, kindOfList, checker, "buildlink3.mk, builtin.mk:; *: use-loadtime, use")
+               acl(varname, kindOfList, checker, "buildlink3.mk, builtin.mk: none; *: use-loadtime, use")
        }
 
        compilerLanguages := enum(
@@ -100,7 +108,10 @@ func (src *Pkgsrc) InitVartypes() {
                                        }
                                }
                        }
-                       for _, language := range [...]string{"ada", "c", "c99", "c++", "c++11", "fortran", "fortran77", "java", "objc", "obj-c++"} {
+                       alwaysAvailable := [...]string{
+                               "ada", "c", "c99", "c++", "c++11", "c++14",
+                               "fortran", "fortran77", "java", "objc", "obj-c++"}
+                       for _, language := range alwaysAvailable {
                                languages[language] = true
                        }
 
@@ -153,8 +164,8 @@ func (src *Pkgsrc) InitVartypes() {
                return enum(defval)
        }
 
-       // enumFromDirs reads the directories from category, takes all
-       // that have a single number in them and ranks them from earliest
+       // enumFromDirs reads the directories from category, takes all that have
+       // a single number in them (such as php72) and ranks them from earliest
        // to latest.
        //
        // If the directories cannot be found, the allowed values are taken
@@ -232,7 +243,7 @@ func (src *Pkgsrc) InitVartypes() {
        usr("CROSSBASE", lkNone, BtPathname)
        usr("VARBASE", lkNone, BtPathname)
        acl("X11_TYPE", lkNone, enum("modular native"), "*: use-loadtime, use")
-       usr("X11BASE", lkNone, BtPathname)
+       acl("X11BASE", lkNone, BtPathname, "*: use-loadtime, use")
        usr("MOTIFBASE", lkNone, BtPathname)
        usr("PKGINFODIR", lkNone, BtPathname)
        usr("PKGMANDIR", lkNone, BtPathname)
@@ -294,7 +305,7 @@ func (src *Pkgsrc) InitVartypes() {
        usrpkg := func(varname string, kindOfList KindOfList, checker *BasicType) {
                acl(varname, kindOfList, checker, ""+
                        "Makefile: default, set, use, use-loadtime; "+
-                       "buildlink3.mk, builtin.mk:; "+
+                       "buildlink3.mk, builtin.mk: none; "+
                        "Makefile.*, *.mk: default, set, use, use-loadtime; "+
                        "*: use-loadtime, use")
        }
@@ -488,10 +499,10 @@ func (src *Pkgsrc) InitVartypes() {
 
        // some other variables, sorted alphabetically
 
-       acl(".CURDIR", lkNone, BtPathname, "buildlink3.mk:; *: use, use-loadtime")
-       acl(".IMPSRC", lkShell, BtPathname, "buildlink3.mk:; *: use, use-loadtime")
-       acl(".TARGET", lkNone, BtPathname, "buildlink3.mk:; *: use, use-loadtime")
-       acl("@", lkNone, BtPathname, "buildlink3.mk:; *: use, use-loadtime")
+       acl(".CURDIR", lkNone, BtPathname, "buildlink3.mk: none; *: use, use-loadtime")
+       acl(".IMPSRC", lkShell, BtPathname, "buildlink3.mk: none; *: use, use-loadtime")
+       acl(".TARGET", lkNone, BtPathname, "buildlink3.mk: none; *: use, use-loadtime")
+       acl("@", lkNone, BtPathname, "buildlink3.mk: none; *: use, use-loadtime")
        acl("ALL_ENV", lkShell, BtShellWord, "")
        acl("ALTERNATIVES_FILE", lkNone, BtFileName, "")
        acl("ALTERNATIVES_SRC", lkShell, BtPathname, "")
@@ -578,8 +589,8 @@ func (src *Pkgsrc) InitVartypes() {
        acl("CATEGORIES", lkShell, BtCategory, "Makefile: set, append; Makefile.common: set, default, append")
        sysload("CC_VERSION", lkNone, BtMessage)
        sysload("CC", lkNone, BtShellCommand)
-       pkglist("CFLAGS", lkShell, BtCFlag)   // may also be changed by the user
-       pkglist("CFLAGS.*", lkShell, BtCFlag) // may also be changed by the user
+       pkglistbl3("CFLAGS", lkShell, BtCFlag)   // may also be changed by the user
+       pkglistbl3("CFLAGS.*", lkShell, BtCFlag) // may also be changed by the user
        acl("CHECK_BUILTIN", lkNone, BtYesNo, "builtin.mk: default; Makefile: set")
        acl("CHECK_BUILTIN.*", lkNone, BtYesNo, "Makefile, options.mk, buildlink3.mk: set; builtin.mk: default, use-loadtime; *: use-loadtime")
        acl("CHECK_FILES_SKIP", lkShell, BtBasicRegularExpression, "Makefile, Makefile.common: append")
@@ -751,7 +762,7 @@ func (src *Pkgsrc) InitVartypes() {
        pkg("GITHUB_TYPE", lkNone, enum("tag release"))
        pkg("GMAKE_REQD", lkNone, BtVersion)
        acl("GNU_ARCH", lkNone, enum("mips"), "")
-       acl("GNU_ARCH.*", lkNone, BtIdentifier, "buildlink3.mk:; *: set, use")
+       acl("GNU_ARCH.*", lkNone, BtIdentifier, "buildlink3.mk: none; *: set, use")
        acl("GNU_CONFIGURE", lkNone, BtYes, "Makefile, Makefile.common: set")
        acl("GNU_CONFIGURE_INFODIR", lkNone, BtPathname, "Makefile, Makefile.common: set")
        acl("GNU_CONFIGURE_LIBDIR", lkNone, BtPathname, "Makefile, Makefile.common: set")
@@ -807,8 +818,8 @@ func (src *Pkgsrc) InitVartypes() {
        usr("KRB5_DEFAULT", lkNone, enum("heimdal mit-krb5"))
        sys("KRB5_TYPE", lkNone, BtIdentifier)
        sys("LD", lkNone, BtShellCommand)
-       pkglist("LDFLAGS", lkShell, BtLdFlag)
-       pkglist("LDFLAGS.*", lkShell, BtLdFlag)
+       pkglistbl3("LDFLAGS", lkShell, BtLdFlag)      // May also be changed by the user.
+       pkglistbl3("LDFLAGS.*", lkShell, BtLdFlag)    // May also be changed by the user.
        sysload("LIBABISUFFIX", lkNone, BtIdentifier) // Can also be empty.
        sys("LIBGRP", lkNone, BtUserGroupName)
        sys("LIBMODE", lkNone, BtFileMode)
@@ -819,8 +830,8 @@ func (src *Pkgsrc) InitVartypes() {
        sys("LIBTOOL", lkNone, BtShellCommand)
        acl("LIBTOOL_OVERRIDE", lkShell, BtPathmask, "Makefile: set, append")
        pkglist("LIBTOOL_REQD", lkShell, BtVersion)
-       acl("LICENCE", lkNone, BtLicense, "Makefile, Makefile.common, options.mk: set, append")
-       acl("LICENSE", lkNone, BtLicense, "Makefile, Makefile.common, options.mk: set, append")
+       acl("LICENCE", lkNone, BtLicense, "buildlink3.mk, builtin.mk: none; Makefile: set, append; *: default, set, append")
+       acl("LICENSE", lkNone, BtLicense, "buildlink3.mk, builtin.mk: none; Makefile: set, append; *: default, set, append")
        pkg("LICENSE_FILE", lkNone, BtPathname)
        sys("LINK.*", lkNone, BtShellCommand)
        sys("LINKER_RPATH_FLAG", lkNone, BtShellWord)
@@ -937,7 +948,7 @@ func (src *Pkgsrc) InitVartypes() {
        acl("PATCH_ARGS", lkShell, BtShellWord, "")
        acl("PATCH_DIST_ARGS", lkShell, BtShellWord, "Makefile: set, append")
        acl("PATCH_DIST_CAT", lkNone, BtShellCommand, "")
-       acl("PATCH_DIST_STRIP*", lkNone, BtShellWord, "buildlink3.mk, builtin.mk:; Makefile, Makefile.common, *.mk: set")
+       acl("PATCH_DIST_STRIP*", lkNone, BtShellWord, "buildlink3.mk, builtin.mk: none; Makefile, Makefile.common, *.mk: set")
        acl("PATCH_SITES", lkShell, BtFetchURL, "Makefile, Makefile.common, options.mk: set")
        acl("PATCH_STRIP", lkNone, BtShellWord, "")
        sys("PATH", lkNone, BtPathlist)       // From the PATH environment variable.
@@ -986,7 +997,7 @@ func (src *Pkgsrc) InitVartypes() {
        sys("PKGNAME_NOREV", lkNone, BtPkgName)
        sysload("PKGPATH", lkNone, BtPathname)
        acl("PKGREPOSITORY", lkNone, BtUnknown, "")
-       acl("PKGREVISION", lkNone, BtPkgRevision, "Makefile: set")
+       acl("PKGREVISION", lkNone, BtPkgRevision, "Makefile: set; *: none")
        sys("PKGSRCDIR", lkNone, BtPathname)
        acl("PKGSRCTOP", lkNone, BtYes, "Makefile: set")
        sys("PKGSRC_SETENV", lkNone, BtShellCommand)
@@ -1109,6 +1120,7 @@ func (src *Pkgsrc) InitVartypes() {
        pkg("RESTRICTED", lkNone, BtMessage)
        usr("ROOT_USER", lkNone, BtUserGroupName)
        usr("ROOT_GROUP", lkNone, BtUserGroupName)
+       pkglist("RPMIGNOREPATH", lkShell, BtPathmask)
        acl("RUBY_BASE", lkNone, enumFromDirs("lang", `^ruby(\d+)$`, "ruby$1", "ruby22 ruby23 ruby24 ruby25"), ""+
                "special:rubyversion.mk: set; "+
                "*: use-loadtime, use")
@@ -1142,15 +1154,15 @@ func (src *Pkgsrc) InitVartypes() {
        pkglist("SPECIAL_PERMS", lkShell, BtPerms)
        sys("STEP_MSG", lkNone, BtShellCommand)
        sys("STRIP", lkNone, BtShellCommand) // see mk/tools/strip.mk
-       acl("SUBDIR", lkShell, BtFileName, "Makefile: append; *:")
+       acl("SUBDIR", lkShell, BtFileName, "Makefile: append; *: none")
        acl("SUBST_CLASSES", lkShell, BtIdentifier, "Makefile: set, append; *: append")
-       acl("SUBST_CLASSES.*", lkShell, BtIdentifier, "Makefile: set, append; *: append")
+       acl("SUBST_CLASSES.*", lkShell, BtIdentifier, "Makefile: set, append; *: append") // OPSYS-specific
        acl("SUBST_FILES.*", lkShell, BtPathmask, "Makefile, Makefile.*, *.mk: set, append")
        acl("SUBST_FILTER_CMD.*", lkNone, BtShellCommand, "Makefile, Makefile.*, *.mk: set")
        acl("SUBST_MESSAGE.*", lkNone, BtMessage, "Makefile, Makefile.*, *.mk: set")
        acl("SUBST_SED.*", lkNone, BtSedCommands, "Makefile, Makefile.*, *.mk: set, append")
        pkg("SUBST_STAGE.*", lkNone, BtStage)
-       pkglist("SUBST_VARS.*", lkShell, BtVariableName)
+       acl("SUBST_VARS.*", lkShell, BtVariableName, "Makefile, Makefile.*, *.mk: set, append")
        pkglist("SUPERSEDES", lkShell, BtDependency)
        acl("TEST_DEPENDS", lkShell, BtDependencyWithPath, "Makefile, Makefile.common, *.mk: append")
        pkglist("TEST_DIRS", lkShell, BtWrksrcSubdirectory)
@@ -1162,7 +1174,7 @@ func (src *Pkgsrc) InitVartypes() {
        sys("TOOLS_BROKEN", lkShell, BtTool)
        sys("TOOLS_CMD.*", lkNone, BtPathname)
        acl("TOOLS_CREATE", lkShell, BtTool, "Makefile, Makefile.common, options.mk: append")
-       acl("TOOLS_DEPENDS.*", lkShell, BtDependencyWithPath, "buildlink3.mk:; Makefile, Makefile.*: set, default; *: use")
+       acl("TOOLS_DEPENDS.*", lkShell, BtDependencyWithPath, "buildlink3.mk: none; Makefile, Makefile.*: set, default; *: use")
        sys("TOOLS_GNU_MISSING", lkShell, BtTool)
        sys("TOOLS_NOOP", lkShell, BtTool)
        sys("TOOLS_PATH.*", lkNone, BtPathname)
@@ -1181,7 +1193,7 @@ func (src *Pkgsrc) InitVartypes() {
        pkg("USE_CMAKE", lkNone, BtYes)
        usr("USE_DESTDIR", lkNone, BtYes)
        pkglist("USE_FEATURES", lkShell, BtIdentifier)
-       acl("USE_GAMESGROUP", lkNone, BtYesNo, "buildlink3.mk, builtin.mk:; *: set, default, use")
+       acl("USE_GAMESGROUP", lkNone, BtYesNo, "buildlink3.mk, builtin.mk: none; *: set, default, use")
        pkg("USE_GCC_RUNTIME", lkNone, BtYesNo)
        pkg("USE_GNU_CONFIGURE_HOST", lkNone, BtYesNo)
        acl("USE_GNU_ICONV", lkNone, BtYes, "Makefile, Makefile.common, options.mk: set")
@@ -1245,12 +1257,10 @@ func parseACLEntries(varname string, acl
        var result []ACLEntry
        prevperms := "(first)"
        for _, arg := range strings.Split(aclEntries, "; ") {
-               var globs, perms string
-               if fields := strings.SplitN(arg, ": ", 2); len(fields) == 2 {
-                       globs, perms = fields[0], fields[1]
-               } else {
-                       globs = strings.TrimSuffix(arg, ":")
-               }
+               fields := strings.SplitN(arg, ": ", 2)
+               G.Assertf(len(fields) == 2, "Invalid ACL entry %q", arg)
+               globs, perms := fields[0], ifelseStr(fields[1] == "none", "", fields[1])
+
                G.Assertf(perms != prevperms, "Repeated permissions %q for %q.", perms, varname)
                prevperms = perms
 

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.50 pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.51
--- pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.50  Thu Feb 21 23:44:55 2019
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck.go       Sun Mar 10 19:01:50 2019
@@ -878,7 +878,7 @@ func (cv *VartypeCheck) PkgOptionsVar() 
 //
 // Despite its name, it is more similar to RelativePkgDir than to RelativePkgPath.
 func (cv *VartypeCheck) PkgPath() {
-       pkgsrcdir := relpath(path.Dir(cv.MkLine.Filename), G.Pkgsrc.File("."))
+       pkgsrcdir := cv.MkLine.PathToFile(G.Pkgsrc.File("."))
        MkLineChecker{cv.MkLine}.CheckRelativePkgdir(pkgsrcdir + "/" + cv.Value)
 }
 

Index: pkgsrc/pkgtools/pkglint/files/textproc/lexer.go
diff -u pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.4 pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.5
--- pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.4 Thu Feb 21 22:49:04 2019
+++ pkgsrc/pkgtools/pkglint/files/textproc/lexer.go     Sun Mar 10 19:01:50 2019
@@ -226,6 +226,9 @@ func (l *Lexer) Copy() *Lexer { return &
 func (l *Lexer) Commit(other *Lexer) bool { l.rest = other.rest; return true }
 
 // NewByteSet creates a bit mask out of a string like "0-9A-Za-z_".
+// To add an actual hyphen to the bit mask, write it as "---"
+// (a range from hyphen to hyphen).
+//
 // The bit mask can be used with Lexer.NextBytesSet.
 func NewByteSet(chars string) *ByteSet {
        var set ByteSet

Index: pkgsrc/pkgtools/pkglint/files/trace/tracing.go
diff -u pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.6 pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.7
--- pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.6  Thu Feb 21 22:49:04 2019
+++ pkgsrc/pkgtools/pkglint/files/trace/tracing.go      Sun Mar 10 19:01:51 2019
@@ -131,6 +131,8 @@ func (t *Tracer) traceCall(args ...inter
 }
 
 // Result marks an argument as a result and is only logged when the function returns.
+//
+// Usage: defer trace.Call(arg1, arg2, tracing.Result(&result1), tracing.Result(&result2))()
 func (t *Tracer) Result(rv interface{}) Result {
        if reflect.ValueOf(rv).Kind() != reflect.Ptr {
                panic(fmt.Sprintf("Result must be called with a pointer to the result, not %#v.", rv))

Added files:

Index: pkgsrc/pkgtools/pkglint/files/mktokenslexer.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/mktokenslexer.go:1.1
--- /dev/null   Sun Mar 10 19:01:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mktokenslexer.go      Sun Mar 10 19:01:50 2019
@@ -0,0 +1,89 @@
+package pkglint
+
+import (
+       "netbsd.org/pkglint/textproc"
+       "strings"
+)
+
+// MkTokensLexer parses a sequence of variable uses (like ${VAR:Mpattern})
+// interleaved with other text that is uninterpreted by bmake.
+type MkTokensLexer struct {
+       // The lexer for the current text-only token.
+       // If the current token is a variable use, the lexer will always return
+       // EOF internally. That is not visible from the outside though, as EOF is
+       // overridden in this type.
+       *textproc.Lexer
+
+       // The remaining tokens.
+       tokens []*MkToken
+}
+
+func NewMkTokensLexer(tokens []*MkToken) *MkTokensLexer {
+       lexer := &MkTokensLexer{nil, tokens}
+       lexer.next()
+       return lexer
+}
+
+func (m *MkTokensLexer) next() {
+       if len(m.tokens) > 0 && m.tokens[0].Varuse == nil {
+               m.Lexer = textproc.NewLexer(m.tokens[0].Text)
+               m.tokens = m.tokens[1:]
+       } else {
+               m.Lexer = textproc.NewLexer("")
+       }
+}
+
+// EOF returns whether the whole input has been consumed.
+func (m *MkTokensLexer) EOF() bool { return m.Lexer.EOF() && len(m.tokens) == 0 }
+
+// Rest returns the string concatenation of the tokens that have not yet been consumed.
+func (m *MkTokensLexer) Rest() string {
+       var sb strings.Builder
+       sb.WriteString(m.Lexer.Rest())
+       for _, token := range m.tokens {
+               sb.WriteString(token.Text)
+       }
+       return sb.String()
+}
+
+// NextVarUse returns the next varuse token, unless there is some plain text
+// before it. In that case or at EOF, it returns nil.
+func (m *MkTokensLexer) NextVarUse() *MkVarUse {
+       if m.Lexer.EOF() && len(m.tokens) > 0 && m.tokens[0].Varuse != nil {
+               token := m.tokens[0]
+               m.tokens = m.tokens[1:]
+               m.next()
+               return token.Varuse
+       }
+       return nil
+}
+
+// Mark remembers the current position of the lexer.
+// The lexer can later be reset to that position by calling Reset.
+func (m *MkTokensLexer) Mark() MkTokensLexerMark {
+       return MkTokensLexerMark{m.Lexer.Rest(), m.tokens}
+}
+
+// Since returns the text between the given mark and the current position
+// of the lexer.
+func (m *MkTokensLexer) Since(mark MkTokensLexerMark) string {
+       early := (&MkTokensLexer{textproc.NewLexer(mark.rest), mark.tokens}).Rest()
+       late := m.Rest()
+
+       return strings.TrimSuffix(early, late)
+}
+
+// Reset sets the lexer back to the given position.
+// The lexer may be reset to the same mark multiple times,
+// that is, the mark is not destroyed.
+func (m *MkTokensLexer) Reset(mark MkTokensLexerMark) {
+       m.Lexer = textproc.NewLexer(mark.rest)
+       m.tokens = mark.tokens
+}
+
+// MkTokensLexerMark remembers a position of a token lexer,
+// to be restored later.
+type MkTokensLexerMark struct {
+       rest   string
+       tokens []*MkToken
+}
Index: pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go:1.1
--- /dev/null   Sun Mar 10 19:01:51 2019
+++ pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go Sun Mar 10 19:01:50 2019
@@ -0,0 +1,291 @@
+package pkglint
+
+import (
+       "gopkg.in/check.v1"
+       "netbsd.org/pkglint/textproc"
+)
+
+func (s *Suite) Test_MkTokensLexer__empty_slice_returns_EOF(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer(nil)
+
+       t.Check(lexer.EOF(), equals, true)
+}
+
+// A slice of a single token behaves like textproc.Lexer.
+func (s *Suite) Test_MkTokensLexer__single_plain_text_token(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{{"\\# $$ [#] $V", nil}})
+
+       t.Check(lexer.SkipByte('\\'), equals, true)
+       t.Check(lexer.Rest(), equals, "# $$ [#] $V")
+       t.Check(lexer.SkipByte('#'), equals, true)
+       t.Check(lexer.NextHspace(), equals, " ")
+       t.Check(lexer.NextBytesSet(textproc.Space.Inverse()), equals, "$$")
+       t.Check(lexer.Skip(len(lexer.Rest())), equals, true)
+       t.Check(lexer.EOF(), equals, true)
+}
+
+// If the first element of the slice is a variable use, none of the plain
+// text patterns matches.
+//
+// The code that uses the MkTokensLexer needs to distinguish these cases
+// anyway, therefore it doesn't make sense to treat variable uses as plain
+// text.
+func (s *Suite) Test_MkTokensLexer__single_varuse_token(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{{"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")}})
+
+       t.Check(lexer.EOF(), equals, false)
+       t.Check(lexer.PeekByte(), equals, -1)
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR", "Mpattern"))
+}
+
+func (s *Suite) Test_MkTokensLexer__plain_then_varuse(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"plain text", nil},
+               {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")}})
+
+       t.Check(lexer.NextBytesSet(textproc.Digit.Inverse()), equals, "plain text")
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR", "Mpattern"))
+       t.Check(lexer.EOF(), equals, true)
+}
+
+func (s *Suite) Test_MkTokensLexer__varuse_varuse_varuse(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"${dirs:O:u}", NewMkVarUse("dirs", "O", "u")},
+               {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
+               {"${.TARGET}", NewMkVarUse(".TARGET")}})
+
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("dirs", "O", "u"))
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR", "Mpattern"))
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse(".TARGET"))
+       t.Check(lexer.NextVarUse(), check.IsNil)
+}
+
+func (s *Suite) Test_MkTokensLexer__mark_reset_since_in_initial_state(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"${dirs:O:u}", NewMkVarUse("dirs", "O", "u")},
+               {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
+               {"${.TARGET}", NewMkVarUse(".TARGET")}})
+
+       start := lexer.Mark()
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("dirs", "O", "u"))
+       middle := lexer.Mark()
+       t.Check(lexer.Rest(), equals, "${VAR:Mpattern}${.TARGET}")
+       lexer.Reset(start)
+       t.Check(lexer.Rest(), equals, "${dirs:O:u}${VAR:Mpattern}${.TARGET}")
+       lexer.Reset(middle)
+       t.Check(lexer.Rest(), equals, "${VAR:Mpattern}${.TARGET}")
+}
+
+func (s *Suite) Test_MkTokensLexer__mark_reset_since_inside_plain_text(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"plain text", nil},
+               {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
+               {"rest", nil}})
+
+       start := lexer.Mark()
+       t.Check(lexer.NextBytesSet(textproc.Alpha), equals, "plain")
+       middle := lexer.Mark()
+       t.Check(lexer.Rest(), equals, " text${VAR:Mpattern}rest")
+       lexer.Reset(start)
+       t.Check(lexer.Rest(), equals, "plain text${VAR:Mpattern}rest")
+       lexer.Reset(middle)
+       t.Check(lexer.Rest(), equals, " text${VAR:Mpattern}rest")
+}
+
+func (s *Suite) Test_MkTokensLexer__mark_reset_since_after_plain_text(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"plain text", nil},
+               {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
+               {"rest", nil}})
+
+       start := lexer.Mark()
+       t.Check(lexer.SkipString("plain text"), equals, true)
+       end := lexer.Mark()
+       t.Check(lexer.Rest(), equals, "${VAR:Mpattern}rest")
+       lexer.Reset(start)
+       t.Check(lexer.Rest(), equals, "plain text${VAR:Mpattern}rest")
+       lexer.Reset(end)
+       t.Check(lexer.Rest(), equals, "${VAR:Mpattern}rest")
+}
+
+func (s *Suite) Test_MkTokensLexer__mark_reset_since_after_varuse(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
+               {"rest", nil}})
+
+       start := lexer.Mark()
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR", "Mpattern"))
+       end := lexer.Mark()
+       t.Check(lexer.Rest(), equals, "rest")
+       lexer.Reset(start)
+       t.Check(lexer.Rest(), equals, "${VAR:Mpattern}rest")
+       lexer.Reset(end)
+       t.Check(lexer.Rest(), equals, "rest")
+}
+
+func (s *Suite) Test_MkTokensLexer__multiple_marks_in_same_plain_text(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"plain text", nil},
+               {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
+               {"rest", nil}})
+
+       start := lexer.Mark()
+       t.Check(lexer.NextString("plain "), equals, "plain ")
+       middle := lexer.Mark()
+       t.Check(lexer.NextString("text"), equals, "text")
+       end := lexer.Mark()
+       t.Check(lexer.Rest(), equals, "${VAR:Mpattern}rest")
+       lexer.Reset(start)
+       t.Check(lexer.Rest(), equals, "plain text${VAR:Mpattern}rest")
+       lexer.Reset(middle)
+       t.Check(lexer.Rest(), equals, "text${VAR:Mpattern}rest")
+       lexer.Reset(end)
+       t.Check(lexer.Rest(), equals, "${VAR:Mpattern}rest")
+}
+
+func (s *Suite) Test_MkTokensLexer__multiple_marks_in_varuse(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"${VAR1}", NewMkVarUse("VAR1")},
+               {"${VAR2}", NewMkVarUse("VAR2")},
+               {"${VAR3}", NewMkVarUse("VAR3")}})
+
+       start := lexer.Mark()
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR1"))
+       middle := lexer.Mark()
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR2"))
+       further := lexer.Mark()
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR3"))
+       end := lexer.Mark()
+       t.Check(lexer.Rest(), equals, "")
+       lexer.Reset(middle)
+       t.Check(lexer.Rest(), equals, "${VAR2}${VAR3}")
+       lexer.Reset(further)
+       t.Check(lexer.Rest(), equals, "${VAR3}")
+       lexer.Reset(start)
+       t.Check(lexer.Rest(), equals, "${VAR1}${VAR2}${VAR3}")
+       lexer.Reset(end)
+       t.Check(lexer.Rest(), equals, "")
+}
+
+func (s *Suite) Test_MkTokensLexer__EOF_before_plain_text(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{{"rest", nil}})
+
+       t.Check(lexer.EOF(), equals, false)
+}
+
+func (s *Suite) Test_MkTokensLexer__EOF_before_varuse(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{{"${VAR}", NewMkVarUse("VAR")}})
+
+       t.Check(lexer.EOF(), equals, false)
+}
+
+// When the MkTokensLexer is constructed, it gets a copy of the tokens array.
+// In theory it would be possible to change the tokens after starting lexing,
+// but there is no practical case where that would be useful.
+//
+// Since each slice is a separate view on the underlying array, modifying the
+// size of the outside slice does not affect parsing. This is also only a
+// theoretical case.
+//
+// Because all these cases are only theoretical, the MkTokensLexer doesn't
+// bother to make this unnecessary copy and works on the shared slice.
+func (s *Suite) Test_MkTokensLexer__constructor_uses_shared_array(c *check.C) {
+       t := s.Init(c)
+
+       tokens := []*MkToken{{"${VAR}", NewMkVarUse("VAR")}}
+       lexer := NewMkTokensLexer(tokens)
+
+       t.Check(lexer.Rest(), equals, "${VAR}")
+
+       tokens[0].Text = "modified text"
+       tokens[0].Varuse = NewMkVarUse("MODIFIED", "Mpattern")
+       tokens = tokens[0:0]
+
+       t.Check(lexer.Rest(), equals, "modified text")
+}
+
+func (s *Suite) Test_MkTokensLexer__peek_after_varuse(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"${VAR}", NewMkVarUse("VAR")},
+               {"${VAR}", NewMkVarUse("VAR")},
+               {"text", nil}})
+
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR"))
+       t.Check(lexer.PeekByte(), equals, -1)
+
+       t.Check(lexer.NextVarUse(), deepEquals, NewMkVarUse("VAR"))
+       t.Check(lexer.PeekByte(), equals, int('t'))
+}
+
+func (s *Suite) Test_MkTokensLexer__varuse_when_plain_text(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{{"text", nil}})
+
+       t.Check(lexer.NextVarUse(), check.IsNil)
+       t.Check(lexer.NextString("te"), equals, "te")
+       t.Check(lexer.NextVarUse(), check.IsNil)
+       t.Check(lexer.NextString("xt"), equals, "xt")
+       t.Check(lexer.NextVarUse(), check.IsNil)
+}
+
+// The code that creates the tokens for the lexer never puts two
+// plain text MkTokens besides each other. There's no point in doing
+// that since they could have been combined into a single token from
+// the beginning.
+func (s *Suite) Test_MkTokensLexer__adjacent_plain_text(c *check.C) {
+       t := s.Init(c)
+
+       lexer := NewMkTokensLexer([]*MkToken{
+               {"text1", nil},
+               {"text2", nil}})
+
+       // Returns false since the string is distributed over two separate tokens.
+       t.Check(lexer.SkipString("text1text2"), equals, false)
+
+       t.Check(lexer.SkipString("text1"), equals, true)
+
+       // This returns false since the internal lexer is not advanced to the
+       // next text token. To do that, all methods from the internal lexer
+       // would have to be redefined by MkTokensLexer in order to advance the
+       // internal lexer to the next token.
+       //
+       // Since this situation doesn't occur in practice, there's no point in
+       // implementing it.
+       t.Check(lexer.SkipString("text2"), equals, false)
+
+       // Just for covering the "Varuse != nil" branch in MkTokensLexer.NextVarUse.
+       t.Check(lexer.NextVarUse(), check.IsNil)
+
+       // The string is still not found since the next token is only consumed
+       // by the NextVarUse above if it is indeed a VarUse.
+       t.Check(lexer.SkipString("text2"), equals, false)
+}
Index: pkgsrc/pkgtools/pkglint/files/redundantscope.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.1
--- /dev/null   Sun Mar 10 19:01:51 2019
+++ pkgsrc/pkgtools/pkglint/files/redundantscope.go     Sun Mar 10 19:01:50 2019
@@ -0,0 +1,279 @@
+package pkglint
+
+// RedundantScope checks for redundant variable definitions and for variables
+// that are accidentally overwritten. It tries to be as correct as possible
+// by not flagging anything that is defined conditionally.
+//
+// There may be some edge cases though like defining PKGNAME, then evaluating
+// it using :=, then defining it again. This pattern is so error-prone that
+// it should not appear in pkgsrc at all, thus pkglint doesn't even expect it.
+// (Well, except for the PKGNAME case, but that's deep in the infrastructure
+// and only affects the "nb13" extension.)
+//
+// TODO: This scope is not only used for detecting redundancies. It also
+// provides information about whether the variables are constant or depend on
+// other variables. Therefore the name may change soon.
+type RedundantScope struct {
+       vars        map[string]*redundantScopeVarinfo
+       includePath includePath
+}
+type redundantScopeVarinfo struct {
+       vari         *Var
+       includePaths []includePath
+       lastAction   uint8 // 0 = none, 1 = read, 2 = write
+}
+
+func NewRedundantScope() *RedundantScope {
+       return &RedundantScope{vars: make(map[string]*redundantScopeVarinfo)}
+}
+
+func (s *RedundantScope) Check(mklines MkLines) {
+       mklines.ForEach(func(mkline MkLine) {
+               s.Handle(mkline, mklines.indentation)
+       })
+}
+
+func (s *RedundantScope) Handle(mkline MkLine, ind *Indentation) {
+       s.updateIncludePath(mkline)
+
+       switch {
+       case mkline.IsVarassign():
+               s.handleVarassign(mkline, ind)
+       }
+
+       s.handleVarUse(mkline)
+}
+
+func (s *RedundantScope) updateIncludePath(mkline MkLine) {
+       if mkline.firstLine == 1 {
+               s.includePath.push(mkline.Location.Filename)
+       } else {
+               s.includePath.popUntil(mkline.Location.Filename)
+       }
+}
+
+func (s *RedundantScope) handleVarassign(mkline MkLine, ind *Indentation) {
+       varname := mkline.Varname()
+       info := s.get(varname)
+
+       defer func() {
+               info.vari.Write(mkline, ind.Depth("") > 0, ind.Varnames()...)
+               info.lastAction = 2
+               s.access(varname)
+       }()
+
+       // In the very first assignment, no redundancy can occur.
+       prevWrites := info.vari.WriteLocations()
+       if len(prevWrites) == 0 {
+               return
+       }
+
+       // TODO: Just being conditional is only half the truth.
+       //  To be precise, the "conditional path" must differ between
+       //  this variable assignment and the/any? previous one.
+       //  See Test_RedundantScope__overwrite_inside_conditional.
+       //  Anyway, too few warnings are better than wrong warnings.
+       if info.vari.Conditional() || ind.Depth("") > 0 {
+               return
+       }
+
+       // When the variable has been read after the previous write,
+       // it is not redundant.
+       if info.lastAction == 1 {
+               return
+       }
+
+       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,
+               // there _is_ a scope here. So if OTHER doesn't refer to further
+               // variables it's all possible.
+               //
+               // TODO: The above idea seems possible and useful.
+               effOp = opAssign
+       }
+
+       switch effOp {
+
+       case opAssign: // with a different value than before
+               if s.includePath.includedByOrEqualsAll(info.includePaths) {
+
+                       // The situation is:
+                       //
+                       //   including.mk: VAR= initial value
+                       //   included.mk:  VAR= overwriting     <-- you are here
+                       //
+                       // Because the included files is never wrong (by definition),
+                       // the including file gets the warning in this case.
+                       s.onOverwrite(prevWrites[len(prevWrites)-1], mkline)
+               }
+
+       case opAssignDefault: // or opAssign with the same value as before
+               switch {
+
+               case s.includePath.includesOrEqualsAll(info.includePaths):
+
+                       // The situation is:
+                       //
+                       //   included.mk:  VAR=  value
+                       //   including.mk: VAR=  value   <-- you are here
+                       //   including.mk: VAR?= value   <-- or here
+                       //
+                       // After including one or more files, the variable is either
+                       // overwritten or defaulted with the same value as its
+                       // guaranteed current value. All previous accesses to the
+                       // variable were either in this file or in an included file.
+                       s.onRedundant(mkline, prevWrites[len(prevWrites)-1])
+
+               case s.includePath.includedByOrEqualsAll(info.includePaths):
+
+                       // The situation is:
+                       //
+                       //   including.mk: VAR=  value
+                       //   included.mk:  VAR?= value   <-- you are here
+                       //   included.mk:  VAR=  value   <-- or here
+                       //
+                       // A variable has been defined in an including file.
+                       // The current line either has a default assignment or an
+                       // unconditional assignment. This is common and fine.
+                       //
+                       // Except when this line has the same value as the guaranteed
+                       // current value of the variable. Then it is redundant.
+                       if info.vari.Constant() && info.vari.ConstantValue() == mkline.Value() {
+                               s.onRedundant(prevWrites[len(prevWrites)-1], mkline)
+                       }
+               }
+       }
+}
+
+func (s *RedundantScope) handleVarUse(mkline MkLine) {
+       switch {
+       case mkline.IsVarassign(), mkline.IsCommentedVarassign():
+               for _, varname := range mkline.DetermineUsedVariables() {
+                       info := s.get(varname)
+                       info.vari.Read(mkline)
+                       info.lastAction = 1
+                       s.access(varname)
+               }
+
+       case mkline.IsDirective():
+               // TODO: Handle varuse for conditions and loops.
+               break
+
+       case mkline.IsInclude(), mkline.IsSysinclude():
+               // TODO: Handle VarUse for includes, which may reference variables.
+               break
+
+       case mkline.IsDependency():
+               // TODO: Handle VarUse for this case.
+       }
+}
+
+// access returns the info for the given variable, creating it if necessary.
+func (s *RedundantScope) get(varname string) *redundantScopeVarinfo {
+       info := s.vars[varname]
+       if info == nil {
+               v := NewVar(varname)
+               info = &redundantScopeVarinfo{v, nil, 0}
+               s.vars[varname] = info
+       }
+       return info
+}
+
+// access records the current file location, to be used in later inclusion checks.
+func (s *RedundantScope) access(varname string) {
+       info := s.vars[varname]
+       info.includePaths = append(info.includePaths, s.includePath.copy())
+}
+
+func (s *RedundantScope) onRedundant(redundant MkLine, because MkLine) {
+       if redundant.Op() == opAssignDefault {
+               redundant.Notef("Default assignment of %s has no effect because of %s.",
+                       because.Varname(), redundant.RefTo(because))
+       } else {
+               redundant.Notef("Definition of %s is redundant because of %s.",
+                       because.Varname(), redundant.RefTo(because))
+       }
+}
+
+func (s *RedundantScope) onOverwrite(overwritten MkLine, by MkLine) {
+       overwritten.Warnf("Variable %s is overwritten in %s.",
+               overwritten.Varname(), overwritten.RefTo(by))
+       G.Explain(
+               "The variable definition in this line does not have an effect since",
+               "it is overwritten elsewhere.",
+               "This typically happens because of a typo (writing = instead of +=)",
+               "or because the line that overwrites",
+               "is in another file that is used by several packages.")
+}
+
+// includePath remembers the whole sequence of included files,
+// such as Makefile includes ../../a/b/buildlink3.mk includes ../../c/d/buildlink3.mk.
+//
+// This information is used by the RedundantScope to decide whether
+// one of two variable assignments is redundant. Two assignments can
+// only be redundant if one location includes the other.
+type includePath struct {
+       files []string
+}
+
+func (p *includePath) push(filename string) {
+       p.files = append(p.files, filename)
+}
+
+func (p *includePath) popUntil(filename string) {
+       for p.files[len(p.files)-1] != filename {
+               p.files = p.files[:len(p.files)-1]
+       }
+}
+
+func (p *includePath) includes(other includePath) bool {
+       for i, filename := range p.files {
+               if i >= len(other.files) || other.files[i] != filename {
+                       return false
+               }
+       }
+       return len(p.files) < len(other.files)
+}
+
+func (p *includePath) includesOrEqualsAll(others []includePath) bool {
+       for _, other := range others {
+               if !(p.includes(other) || p.equals(other)) {
+                       return false
+               }
+       }
+       return true
+}
+
+func (p *includePath) includedByOrEqualsAll(others []includePath) bool {
+       for _, other := range others {
+               if !(other.includes(*p) || p.equals(other)) {
+                       return false
+               }
+       }
+       return true
+}
+
+func (p *includePath) equals(other includePath) bool {
+       if len(p.files) != len(other.files) {
+               return false
+       }
+       for i, filename := range p.files {
+               if other.files[i] != filename {
+                       return false
+               }
+       }
+       return true
+}
+
+func (p *includePath) copy() includePath {
+       return includePath{append([]string(nil), p.files...)}
+}
Index: pkgsrc/pkgtools/pkglint/files/redundantscope_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.1
--- /dev/null   Sun Mar 10 19:01:51 2019
+++ pkgsrc/pkgtools/pkglint/files/redundantscope_test.go        Sun Mar 10 19:01:50 2019
@@ -0,0 +1,1284 @@
+package pkglint
+
+import "gopkg.in/check.v1"
+
+// In a single file, five variables get a default value and are later overridden
+// with the same value using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_default(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT?=\tvalue",
+               "ASSIGN?=\tvalue",
+               "APPEND?=\tvalue",
+               "EVAL?=\tvalue",
+               "SHELL?=\tvalue",
+               "",
+               "DEFAULT?=\tvalue",
+               "ASSIGN=\tvalue",
+               "APPEND+=\tvalue",
+               "EVAL:=\tvalue",
+               "SHELL!=\tvalue")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "NOTE: file.mk:8: Definition of ASSIGN is redundant because of line 2.",
+               "WARN: file.mk:4: Variable EVAL is overwritten in line 10.")
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get assigned are value and are later overridden
+// with the same value using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_assign(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT=\tvalue",
+               "ASSIGN=\tvalue",
+               "APPEND=\tvalue",
+               "EVAL=\tvalue",
+               "SHELL=\tvalue",
+               "",
+               "DEFAULT?=\tvalue",
+               "ASSIGN=\tvalue",
+               "APPEND+=\tvalue",
+               "EVAL:=\tvalue",
+               "SHELL!=\tvalue")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "NOTE: file.mk:8: Definition of ASSIGN is redundant because of line 2.",
+               "WARN: file.mk:4: Variable EVAL is overwritten in line 10.")
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get appended a value and are later overridden
+// with the same value using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_append(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT+=\tvalue",
+               "ASSIGN+=\tvalue",
+               "APPEND+=\tvalue",
+               "EVAL+=\tvalue",
+               "SHELL+=\tvalue",
+               "",
+               "DEFAULT?=\tvalue",
+               "ASSIGN=\tvalue",
+               "APPEND+=\tvalue",
+               "EVAL:=\tvalue",
+               "SHELL!=\tvalue")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "WARN: file.mk:2: Variable ASSIGN is overwritten in line 8.",
+               "WARN: file.mk:4: Variable EVAL is overwritten in line 10.")
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get assigned a value using the := operator,
+// which in this simple case is equivalent to the = operator. The variables are
+// later overridden with the same value using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_eval(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT:=\tvalue",
+               "ASSIGN:=\tvalue",
+               "APPEND:=\tvalue",
+               "EVAL:=\tvalue",
+               "SHELL:=\tvalue",
+               "",
+               "DEFAULT?=\tvalue",
+               "ASSIGN=\tvalue",
+               "APPEND+=\tvalue",
+               "EVAL:=\tvalue",
+               "SHELL!=\tvalue")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "NOTE: file.mk:8: Definition of ASSIGN is redundant because of line 2.",
+               "WARN: file.mk:4: Variable EVAL is overwritten in line 10.")
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get assigned a value using the != operator,
+// which runs a shell command. As of March 2019 pkglint doesn't try to evaluate
+// the shell commands, therefore the variable values are unknown. The variables
+// are later overridden using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_shell(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT!=\tvalue",
+               "ASSIGN!=\tvalue",
+               "APPEND!=\tvalue",
+               "EVAL!=\tvalue",
+               "SHELL!=\tvalue",
+               "",
+               "DEFAULT?=\tvalue",
+               "ASSIGN=\tvalue",
+               "APPEND+=\tvalue",
+               "EVAL:=\tvalue",
+               "SHELL!=\tvalue")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "WARN: file.mk:2: Variable ASSIGN is overwritten in line 8.",
+               "WARN: file.mk:4: Variable EVAL is overwritten in line 10.")
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get a default value and are later overridden
+// with the same value using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_default_ref(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT?=\t${OTHER}",
+               "ASSIGN?=\t${OTHER}",
+               "APPEND?=\t${OTHER}",
+               "EVAL?=\t${OTHER}",
+               "SHELL?=\t${OTHER}",
+               "",
+               "DEFAULT?=\t${OTHER}",
+               "ASSIGN=\t${OTHER}",
+               "APPEND+=\t${OTHER}",
+               "EVAL:=\t${OTHER}",
+               "SHELL!=\t${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "NOTE: file.mk:8: Definition of ASSIGN is redundant because of line 2.")
+       // TODO: "4: is overwritten later",
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get assigned are value and are later overridden
+// with the same value using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_assign_ref(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT=\t${OTHER}",
+               "ASSIGN=\t${OTHER}",
+               "APPEND=\t${OTHER}",
+               "EVAL=\t${OTHER}",
+               "SHELL=\t${OTHER}",
+               "",
+               "DEFAULT?=\t${OTHER}",
+               "ASSIGN=\t${OTHER}",
+               "APPEND+=\t${OTHER}",
+               "EVAL:=\t${OTHER}",
+               "SHELL!=\t${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "NOTE: file.mk:8: Definition of ASSIGN is redundant because of line 2.")
+       // TODO: "4: is overwritten later",
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get appended a value and are later overridden
+// with the same value using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_append_ref(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT+=\t${OTHER}",
+               "ASSIGN+=\t${OTHER}",
+               "APPEND+=\t${OTHER}",
+               "EVAL+=\t${OTHER}",
+               "SHELL+=\t${OTHER}",
+               "",
+               "DEFAULT?=\t${OTHER}",
+               "ASSIGN=\t${OTHER}",
+               "APPEND+=\t${OTHER}",
+               "EVAL:=\t${OTHER}",
+               "SHELL!=\t${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "WARN: file.mk:2: Variable ASSIGN is overwritten in line 8.")
+       // TODO: "4: is overwritten later",
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get assigned a value using the := operator,
+// which in this simple case is equivalent to the = operator. The variables are
+// later overridden with the same value using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_eval_ref(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT:=\t${OTHER}",
+               "ASSIGN:=\t${OTHER}",
+               "APPEND:=\t${OTHER}",
+               "EVAL:=\t${OTHER}",
+               "SHELL:=\t${OTHER}",
+               "",
+               "DEFAULT?=\t${OTHER}",
+               "ASSIGN=\t${OTHER}",
+               "APPEND+=\t${OTHER}",
+               "EVAL:=\t${OTHER}",
+               "SHELL!=\t${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "NOTE: file.mk:8: Definition of ASSIGN is redundant because of line 2.")
+       // TODO: "4: is overwritten later",
+       // TODO: "5: is overwritten later"
+}
+
+// In a single file, five variables get assigned a value using the != operator,
+// which runs a shell command. As of March 2019 pkglint doesn't try to evaluate
+// the shell commands, therefore the variable values are unknown. The variables
+// are later overridden using the five different assignments operators.
+func (s *Suite) Test_RedundantScope__single_file_shell_ref(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("file.mk",
+               "DEFAULT!=\t${OTHER}",
+               "ASSIGN!=\t${OTHER}",
+               "APPEND!=\t${OTHER}",
+               "EVAL!=\t${OTHER}",
+               "SHELL!=\t${OTHER}",
+               "",
+               "DEFAULT?=\t${OTHER}",
+               "ASSIGN=\t${OTHER}",
+               "APPEND+=\t${OTHER}",
+               "EVAL:=\t${OTHER}",
+               "SHELL!=\t${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: file.mk:7: Default assignment of DEFAULT has no effect because of line 1.",
+               "WARN: file.mk:2: Variable ASSIGN is overwritten in line 8.")
+       // TODO: "4: is overwritten later",
+       // TODO: "5: is overwritten later"
+}
+
+func (s *Suite) Test_RedundantScope__after_including_same_value(c *check.C) {
+       t := s.Init(c)
+
+       // Only test the ?=, = and += operators since the others are ignored,
+       // as of March 2019.
+       include, get := t.SetUpHierarchy()
+       include("including.mk",
+               include("included.mk",
+                       "VAR.def.def?= ${OTHER}",
+                       "VAR.def.asg?= ${OTHER}",
+                       "VAR.def.app?= ${OTHER}",
+                       "VAR.asg.def=  ${OTHER}",
+                       "VAR.asg.asg=  ${OTHER}",
+                       "VAR.asg.app=  ${OTHER}",
+                       "VAR.app.def+= ${OTHER}",
+                       "VAR.app.asg+= ${OTHER}",
+                       "VAR.app.app+= ${OTHER}"),
+               "VAR.def.def?= ${OTHER}",
+               "VAR.def.asg=  ${OTHER}",
+               "VAR.def.app+= ${OTHER}",
+               "VAR.asg.def?= ${OTHER}",
+               "VAR.asg.asg=  ${OTHER}",
+               "VAR.asg.app+= ${OTHER}",
+               "VAR.app.def?= ${OTHER}",
+               "VAR.app.asg=  ${OTHER}",
+               "VAR.app.app+= ${OTHER}")
+       mklines := get("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: including.mk:2: Default assignment of VAR.def.def has no effect because of included.mk:1.",
+               "NOTE: including.mk:3: Definition of VAR.def.asg is redundant because of included.mk:2.",
+               // VAR.def.app defines a default value and then appends to it. This is a common pattern.
+               // Appending the same value feels redundant but probably doesn't happen in practice.
+               // If it does, there should be a note for it.
+               "NOTE: including.mk:5: Default assignment of VAR.asg.def has no effect because of included.mk:4.",
+               "NOTE: including.mk:6: Definition of VAR.asg.asg is redundant because of included.mk:5.",
+               // VAR.asg.app defines a variable and later appends to it. This is a common pattern.
+               // Appending the same value feels redundant but probably doesn't happen in practice.
+               // If it does, there should be a note for it.
+               "NOTE: including.mk:8: Default assignment of VAR.app.def has no effect because of included.mk:7.",
+               // VAR.app.asg first appends and then overwrites. This might be a mistake.
+               // TODO: Find out whether this case happens in actual pkgsrc and if it's accidental.
+               // VAR.app.app first appends and then appends one more. This is a common pattern.
+       )
+}
+
+func (s *Suite) Test_RedundantScope__after_including_different_value(c *check.C) {
+       t := s.Init(c)
+
+       // Only test the ?=, = and += operators since the others are ignored,
+       // as of March 2019.
+       include, get := t.SetUpHierarchy()
+       include("including.mk",
+               include("included.mk",
+                       "VAR.def.def?= ${VALUE}",
+                       "VAR.def.asg?= ${VALUE}",
+                       "VAR.def.app?= ${VALUE}",
+                       "VAR.asg.def=  ${VALUE}",
+                       "VAR.asg.asg=  ${VALUE}",
+                       "VAR.asg.app=  ${VALUE}",
+                       "VAR.app.def+= ${VALUE}",
+                       "VAR.app.asg+= ${VALUE}",
+                       "VAR.app.app+= ${VALUE}"),
+               "VAR.def.def?= ${OTHER}",
+               "VAR.def.asg=  ${OTHER}",
+               "VAR.def.app+= ${OTHER}",
+               "VAR.asg.def?= ${OTHER}",
+               "VAR.asg.asg=  ${OTHER}",
+               "VAR.asg.app+= ${OTHER}",
+               "VAR.app.def?= ${OTHER}",
+               "VAR.app.asg=  ${OTHER}",
+               "VAR.app.app+= ${OTHER}")
+       mklines := get("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: including.mk:2: Default assignment of VAR.def.def has no effect because of included.mk:1.",
+               "NOTE: including.mk:5: Default assignment of VAR.asg.def has no effect because of included.mk:4.",
+               "NOTE: including.mk:8: Default assignment of VAR.app.def has no effect because of included.mk:7.")
+}
+
+func (s *Suite) Test_RedundantScope__before_including_same_value(c *check.C) {
+       t := s.Init(c)
+
+       // Only test the ?=, = and += operators since the others are ignored,
+       // as of March 2019.
+       include, get := t.SetUpHierarchy()
+       include("including.mk",
+               "VAR.def.def?= ${OTHER}",
+               "VAR.def.asg?= ${OTHER}",
+               "VAR.def.app?= ${OTHER}",
+               "VAR.asg.def=  ${OTHER}",
+               "VAR.asg.asg=  ${OTHER}",
+               "VAR.asg.app=  ${OTHER}",
+               "VAR.app.def+= ${OTHER}",
+               "VAR.app.asg+= ${OTHER}",
+               "VAR.app.app+= ${OTHER}",
+               include("included.mk",
+                       "VAR.def.def?= ${OTHER}",
+                       "VAR.def.asg=  ${OTHER}",
+                       "VAR.def.app+= ${OTHER}",
+                       "VAR.asg.def?= ${OTHER}",
+                       "VAR.asg.asg=  ${OTHER}",
+                       "VAR.asg.app+= ${OTHER}",
+                       "VAR.app.def?= ${OTHER}",
+                       "VAR.app.asg=  ${OTHER}",
+                       "VAR.app.app+= ${OTHER}"))
+       mklines := get("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: including.mk:1: Default assignment of VAR.def.def has no effect because of included.mk:1.",
+               "NOTE: including.mk:2: Default assignment of VAR.def.asg has no effect because of included.mk:2.",
+               "NOTE: including.mk:4: Definition of VAR.asg.def is redundant because of included.mk:4.",
+               "NOTE: including.mk:5: Definition of VAR.asg.asg is redundant because of included.mk:5.",
+               "WARN: including.mk:8: Variable VAR.app.asg is overwritten in included.mk:8.")
+}
+
+func (s *Suite) Test_RedundantScope__before_including_different_value(c *check.C) {
+       t := s.Init(c)
+
+       // Only test the ?=, = and += operators since the others are ignored,
+       // as of March 2019.
+       include, get := t.SetUpHierarchy()
+       include("including.mk",
+               "VAR.def.def?= ${VALUE}",
+               "VAR.def.asg?= ${VALUE}",
+               "VAR.def.app?= ${VALUE}",
+               "VAR.asg.def=  ${VALUE}",
+               "VAR.asg.asg=  ${VALUE}",
+               "VAR.asg.app=  ${VALUE}",
+               "VAR.app.def+= ${VALUE}",
+               "VAR.app.asg+= ${VALUE}",
+               "VAR.app.app+= ${VALUE}",
+               include("included.mk",
+                       "VAR.def.def?= ${OTHER}",
+                       "VAR.def.asg=  ${OTHER}",
+                       "VAR.def.app+= ${OTHER}",
+                       "VAR.asg.def?= ${OTHER}",
+                       "VAR.asg.asg=  ${OTHER}",
+                       "VAR.asg.app+= ${OTHER}",
+                       "VAR.app.def?= ${OTHER}",
+                       "VAR.app.asg=  ${OTHER}",
+                       "VAR.app.app+= ${OTHER}"))
+       mklines := get("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "WARN: including.mk:2: Variable VAR.def.asg is overwritten in included.mk:2.",
+               "WARN: including.mk:5: Variable VAR.asg.asg is overwritten in included.mk:5.",
+               "WARN: including.mk:8: Variable VAR.app.asg is overwritten in included.mk:8.")
+}
+
+func (s *Suite) Test_RedundantScope__independent_same_value(c *check.C) {
+       t := s.Init(c)
+
+       // Only test the ?=, = and += operators since the others are ignored,
+       // as of March 2019.
+       include, get := t.SetUpHierarchy()
+       include("including.mk",
+               include("included1.mk",
+                       "VAR.def.def?= ${OTHER}",
+                       "VAR.def.asg?= ${OTHER}",
+                       "VAR.def.app?= ${OTHER}",
+                       "VAR.asg.def=  ${OTHER}",
+                       "VAR.asg.asg=  ${OTHER}",
+                       "VAR.asg.app=  ${OTHER}",
+                       "VAR.app.def+= ${OTHER}",
+                       "VAR.app.asg+= ${OTHER}",
+                       "VAR.app.app+= ${OTHER}"),
+               include("included2.mk",
+                       "VAR.def.def?= ${OTHER}",
+                       "VAR.def.asg=  ${OTHER}",
+                       "VAR.def.app+= ${OTHER}",
+                       "VAR.asg.def?= ${OTHER}",
+                       "VAR.asg.asg=  ${OTHER}",
+                       "VAR.asg.app+= ${OTHER}",
+                       "VAR.app.def?= ${OTHER}",
+                       "VAR.app.asg=  ${OTHER}",
+                       "VAR.app.app+= ${OTHER}"))
+       mklines := get("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       // Since the two included files are independent, there cannot be any
+       // redundancies between them. These redundancies can only be discovered
+       // when one of them includes the other.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__independent_different_value(c *check.C) {
+       t := s.Init(c)
+
+       // Only test the ?=, = and += operators since the others are ignored,
+       // as of March 2019.
+       include, get := t.SetUpHierarchy()
+       include("including.mk",
+               include("included1.mk",
+                       "VAR.def.def?= ${VALUE}",
+                       "VAR.def.asg?= ${VALUE}",
+                       "VAR.def.app?= ${VALUE}",
+                       "VAR.asg.def=  ${VALUE}",
+                       "VAR.asg.asg=  ${VALUE}",
+                       "VAR.asg.app=  ${VALUE}",
+                       "VAR.app.def+= ${VALUE}",
+                       "VAR.app.asg+= ${VALUE}",
+                       "VAR.app.app+= ${VALUE}"),
+               include("included2.mk",
+                       "VAR.def.def?= ${OTHER}",
+                       "VAR.def.asg=  ${OTHER}",
+                       "VAR.def.app+= ${OTHER}",
+                       "VAR.asg.def?= ${OTHER}",
+                       "VAR.asg.asg=  ${OTHER}",
+                       "VAR.asg.app+= ${OTHER}",
+                       "VAR.app.def?= ${OTHER}",
+                       "VAR.app.asg=  ${OTHER}",
+                       "VAR.app.app+= ${OTHER}"))
+       mklines := get("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       // Since the two included files are independent, there cannot be any
+       // redundancies between them. Redundancies can only be discovered
+       // when one of them includes the other.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__file_hierarchy(c *check.C) {
+       t := s.Init(c)
+
+       include, get := t.SetUpHierarchy()
+
+       include("including.mk",
+               include("other.mk",
+                       "VAR= other"),
+               include("module.mk",
+                       "VAR= module",
+                       include("version.mk",
+                               "VAR= version"),
+                       include("env.mk",
+                               "VAR= env")))
+
+       NewRedundantScope().Check(get("including.mk"))
+
+       // No output since the included files are independent.
+       t.CheckOutputEmpty()
+
+       NewRedundantScope().Check(get("other.mk"))
+
+       // No output since the file by itself in neither redundant nor
+       // does it include any other file.
+       t.CheckOutputEmpty()
+
+       NewRedundantScope().Check(get("module.mk"))
+
+       // No warning about env.mk because it is independent from version.mk.
+       // Pkglint only produces warnings when it is very sure that the variable
+       // definition is really redundant in all cases.
+       //
+       // One reason to not warn is that at the point where env.mk is evaluated,
+       // version.mk had last written to the variable. Since version.mk is
+       // independent from env.mk, there is nothing redundant here.
+       // Pkglint doesn't do this, but it could.
+       //
+       // Another reason not to warn is that all locations where the variable has
+       // ever been accessed are saved. And if the current location neither includes
+       // all of the others nor is included by all of the others, there is at least
+       // one access that is in an unrelated file. This is what pkglint does.
+       t.CheckOutputLines(
+               "WARN: module.mk:1: Variable VAR is overwritten in version.mk:1.")
+}
+
+// FIXME: Continue the systematic redundancy tests.
+//
+// A test where the operators = and += define a variable that afterwards
+// is assigned the same value using the ?= operator.
+//
+// Tests where the variables refer to other variables. These variables may
+// be read and written between the relevant assignments.
+//
+// Tests where the variables are defined conditionally using .if, .else, .endif.
+//
+// Tests where the variables are defined in a .for loop that might not be
+// evaluated at all.
+//
+// Tests where files are included conditionally and additionally have conditional
+// sections, arbitrarily nested.
+//
+// Tests that show how to suppress the notes about redundant assignments
+// and overwritten variables. The explanation must be helpful.
+//
+// Tests for dynamic variable assignments. For example BUILD_DIRS.NetBSD may
+// be modified by any assignment of the form BUILD_DIRS.${var} or even ${var}.
+// Without further analysis, pkglint cannot report redundancy warnings for any
+// package that uses such variable assignments.
+
+func (s *Suite) Test_RedundantScope__override_after_including(c *check.C) {
+       t := s.Init(c)
+       t.CreateFileLines("included.mk",
+               "OVERRIDE=\tprevious value",
+               "REDUNDANT=\tredundant")
+       t.CreateFileLines("including.mk",
+               ".include \"included.mk\"",
+               "OVERRIDE=\toverridden value",
+               "REDUNDANT=\tredundant")
+       t.Chdir(".")
+       mklines := t.LoadMkInclude("including.mk")
+
+       // XXX: The warnings from here are not in the same order as the other warnings.
+       // XXX: There may be some warnings for the same file separated by warnings for other files.
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: including.mk:3: Definition of REDUNDANT is redundant because of included.mk:2.")
+}
+
+func (s *Suite) Test_RedundantScope__redundant_assign_after_including(c *check.C) {
+       t := s.Init(c)
+       t.CreateFileLines("included.mk",
+               "REDUNDANT=\tredundant")
+       t.CreateFileLines("including.mk",
+               ".include \"included.mk\"",
+               "REDUNDANT=\tredundant")
+       t.Chdir(".")
+       mklines := t.LoadMkInclude("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: including.mk:2: Definition of REDUNDANT is redundant because of included.mk:1.")
+}
+
+func (s *Suite) Test_RedundantScope__override_in_Makefile_after_including(c *check.C) {
+       t := s.Init(c)
+       t.CreateFileLines("module.mk",
+               "VAR=\tvalue ${OTHER}",
+               "VAR?=\tvalue ${OTHER}",
+               "VAR=\tnew value")
+       t.CreateFileLines("Makefile",
+               ".include \"module.mk\"",
+               "VAR=\tthe package may overwrite variables from other files")
+       t.Chdir(".")
+
+       mklines := t.LoadMkInclude("Makefile")
+
+       // XXX: The warnings from here are not in the same order as the other warnings.
+       // XXX: There may be some warnings for the same file separated by warnings for other files.
+       NewRedundantScope().Check(mklines)
+
+       // No warning for VAR=... in Makefile since it makes sense to have common files
+       // with default values for variables, overriding some of them in each package.
+       t.CheckOutputLines(
+               "NOTE: module.mk:2: Default assignment of VAR has no effect because of line 1.",
+               "WARN: module.mk:2: Variable VAR is overwritten in line 3.")
+}
+
+func (s *Suite) Test_RedundantScope__default_value_definitely_unused(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "VAR=\tvalue ${OTHER}",
+               "VAR?=\tdifferent value")
+
+       NewRedundantScope().Check(mklines)
+
+       // A default assignment after an unconditional assignment is redundant.
+       // Even more so when the variable is not used between the two assignments.
+       t.CheckOutputLines(
+               "NOTE: module.mk:2: Default assignment of VAR has no effect because of line 1.")
+}
+
+func (s *Suite) Test_RedundantScope__default_value_overridden(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "VAR?=\tdefault value",
+               "VAR=\toverridden value")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "WARN: module.mk:1: Variable VAR is overwritten in line 2.")
+}
+
+func (s *Suite) Test_RedundantScope__overwrite_same_value(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "VAR=\tvalue ${OTHER}",
+               "VAR=\tvalue ${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: module.mk:2: Definition of VAR is redundant because of line 1.")
+}
+
+func (s *Suite) Test_RedundantScope__conditional_overwrite(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "VAR=\tdefault",
+               ".if ${OPSYS} == NetBSD",
+               "VAR=\topsys",
+               ".endif")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__overwrite_inside_conditional(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "VAR=\tgeneric",
+               ".if ${OPSYS} == NetBSD",
+               "VAR=\tignored",
+               "VAR=\toverwritten",
+               ".endif")
+
+       NewRedundantScope().Check(mklines)
+
+       // TODO: expected a warning "WARN: module.mk:4: line 3 is ignored"
+       // Since line 3 and line 4 are in the same basic block, line 3 is definitely ignored.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__conditionally_include(c *check.C) {
+       t := s.Init(c)
+       t.CreateFileLines("module.mk",
+               "VAR=\tgeneric",
+               ".if ${OPSYS} == NetBSD",
+               ".  include \"included.mk\"",
+               ".endif")
+       t.CreateFileLines("included.mk",
+               "VAR=\tignored",
+               "VAR=\toverwritten")
+       mklines := t.LoadMkInclude("module.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       // TODO: expected a warning "WARN: module.mk:4: line 3 is ignored"
+       //  Since line 3 and line 4 are in the same basic block, line 3 is definitely ignored.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__conditional_default(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "VAR=\tdefault",
+               ".if ${OPSYS} == NetBSD",
+               "VAR?=\topsys",
+               ".endif")
+
+       NewRedundantScope().Check(mklines)
+
+       // TODO: WARN: module.mk:3: The value \"opsys\" will never be assigned to VAR because it is defined unconditionally in line 1.
+       t.CheckOutputEmpty()
+}
+
+// These warnings are precise and accurate since the value of VAR is not used between line 2 and 4.
+func (s *Suite) Test_RedundantScope__overwrite_same_variable_different_value(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "OTHER=\tvalue before",
+               "VAR=\tvalue ${OTHER}",
+               "OTHER=\tvalue after",
+               "VAR=\tvalue ${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       // Strictly speaking, line 1 is redundant because OTHER is not evaluated
+       // at load time and then immediately overwritten in line 3. If the operator
+       // in line 2 were a := instead of a =, the situation would be clear.
+       // Pkglint doesn't warn about the redundancy in line 1 because it prefers
+       // to omit warnings instead of giving wrong advice.
+       t.CheckOutputLines(
+               "NOTE: module.mk:4: Definition of VAR is redundant because of line 2.")
+}
+
+func (s *Suite) Test_RedundantScope__overwrite_different_value_used_between(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "OTHER=\tvalue before",
+               "VAR=\tvalue ${OTHER}",
+
+               // VAR is used here at load time, therefore it must be defined at this point.
+               // At this point, VAR uses the \"before\" value of OTHER.
+               "RESULT1:=\t${VAR}",
+
+               "OTHER=\tvalue after",
+
+               // VAR is used here again at load time, this time using the \"after\" value of OTHER.
+               "RESULT2:=\t${VAR}",
+
+               // Still this definition is redundant.
+               "VAR=\tvalue ${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       // There is nothing redundant here. Each write is followed by a
+       // corresponding read, except for the last one. That is ok though
+       // because in pkgsrc the last action of a package is to include
+       // bsd.pkg.mk, which reads almost all variables.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__procedure_call_to_noop(c *check.C) {
+       t := s.Init(c)
+
+       include, get := t.SetUpHierarchy()
+       include("mk/pthread.buildlink3.mk",
+               "CHECK_BUILTIN.pthread:= yes",
+               include("pthread.builtin.mk",
+                       "# Nothing happens here."),
+               "CHECK_BUILTIN.pthread:= no")
+
+       NewRedundantScope().Check(get("mk/pthread.buildlink3.mk"))
+
+       t.CheckOutputLines(
+               "WARN: mk/pthread.buildlink3.mk:1: Variable CHECK_BUILTIN.pthread is overwritten in line 3.")
+}
+
+func (s *Suite) Test_RedundantScope__procedure_call_implemented(c *check.C) {
+       t := s.Init(c)
+
+       include, get := t.SetUpHierarchy()
+       include("mk/pthread.buildlink3.mk",
+               "CHECK_BUILTIN.pthread:= yes",
+               include("pthread.builtin.mk",
+                       "CHECK_BUILTIN.pthread?= no",
+                       ".if !empty(CHECK_BUILTIN.pthread:M[Nn][Oo])",
+                       ".endif"),
+               "CHECK_BUILTIN.pthread:= no")
+
+       NewRedundantScope().Check(get("mk/pthread.buildlink3.mk"))
+
+       // This test is a bit unrealistic. It wrongly assumes that all files from
+       // an .include directive are actually included by pkglint.
+       //
+       // See Package.readMakefile/handleIncludeLine/skip.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__procedure_call_implemented_package(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.SetUpPackage("devel/gettext-lib")
+       t.SetUpPackage("x11/Xaos",
+               ".include \"../../devel/gettext-lib/buildlink3.mk\"")
+       t.CreateFileLines("devel/gettext-lib/builtin.mk",
+               MkRcsID,
+               "",
+               ".include \"../../mk/bsd.fast.prefs.mk\"",
+               "",
+               "CHECK_BUILTIN.gettext?=\tno",
+               ".if !empty(CHECK_BUILTIN.gettext:M[nN][oO])",
+               ".endif")
+       t.CreateFileLines("devel/gettext-lib/buildlink3.mk",
+               MkRcsID,
+               "CHECK_BUILTIN.gettext:=\tyes",
+               ".include \"builtin.mk\"",
+               "CHECK_BUILTIN.gettext:=\tno")
+       G.Pkgsrc.LoadInfrastructure()
+
+       // Checking x11/Xaos instead of devel/gettext-lib avoids warnings
+       // about the minimal buildlink3.mk file.
+       G.Check(t.File("x11/Xaos"))
+
+       // There is nothing redundant here.
+       // Up to March 2019, pkglint didn't pass the correct pathnames to Package.included,
+       // which triggered a wrong note here.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__procedure_call_infrastructure(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("x11/alacarte",
+               ".include \"../../mk/pthread.buildlink3.mk\"")
+       t.CreateFileLines("mk/pthread.buildlink3.mk",
+               MkRcsID,
+               "CHECK_BUILTIN.gettext:=\tyes",
+               ".include \"pthread.builtin.mk\"",
+               "CHECK_BUILTIN.gettext:=\tno")
+       t.CreateFileLines("mk/pthread.builtin.mk",
+               MkRcsID,
+               "CHECK_BUILTIN.gettext?=\tno",
+               ".if !empty(CHECK_BUILTIN.gettext:M[nN][oO])",
+               ".endif")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.Check(t.File("x11/alacarte"))
+
+       // There is nothing redundant here.
+       //
+       // 1. pthread.buildlink3.mk sets the variable
+       // 2. pthread.builtin.mk assigns it a default value
+       //    (which is common practice)
+       // 3. pthread.builtin.mk then reads it
+       //    (which marks the next write as non-redundant)
+       // 4. pthread.buildlink3.mk sets the variable again
+       //    (this is considered neither overwriting nor redundant)
+       //
+       // Up to March 2019, pkglint complained:
+       //
+       // WARN: ~/mk/pthread.buildlink3.mk:2:
+       //     Variable CHECK_BUILTIN.gettext is overwritten in line 4.
+       //
+       // The cause for the warning is that when including files from the
+       // infrastructure, pkglint only includes the outermost level of files.
+       // If an infrastructure file includes another infrastructure file,
+       // pkglint skips that, for performance reasons.
+       //
+       // This optimization effectively made the .include for pthread.builtin.mk
+       // a no-op, therefore it was correct to issue a warning here.
+       //
+       // Since this warning is wrong, in March 2019 another special rule has
+       // been added to Package.readMakefile.handleIncludeLine.skip saying that
+       // including a buildlink3.mk file also includes the corresponding
+       // builtin.mk file.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__shell_and_eval(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "VAR:=\tvalue ${OTHER}",
+               "VAR!=\tvalue ${OTHER}")
+
+       NewRedundantScope().Check(mklines)
+
+       // As of November 2018, pkglint doesn't check redundancies that involve the := or != operators.
+       //
+       // What happens here is:
+       //
+       // Line 1 evaluates OTHER at load time.
+       // Line 1 assigns its value to VAR.
+       // Line 2 evaluates OTHER at load time.
+       // Line 2 passes its value through the shell and assigns the result to VAR.
+       //
+       // Since VAR is defined in line 1, not used afterwards and overwritten in line 2, it is redundant.
+       // Well, not quite, because evaluating ${OTHER} might have side-effects from :sh or ::= modifiers,
+       // but these are so rare that they are frowned upon and are not considered by pkglint.
+       //
+       // Expected result:
+       // WARN: module.mk:2: Previous definition of VAR in line 1 is unused.
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__shell_and_eval_literal(c *check.C) {
+       t := s.Init(c)
+       mklines := t.NewMkLines("module.mk",
+               "VAR:=\tvalue",
+               "VAR!=\tvalue")
+
+       NewRedundantScope().Check(mklines)
+
+       // Even when := is used with a literal value (which is usually
+       // only done for procedure calls), the shell evaluation can have
+       // so many different side effects that pkglint cannot reliably
+       // help in this situation.
+       //
+       // TODO: Why not? The evaluation in line 1 is trivial to analyze.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__included_OPSYS_variable(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               ".include \"../../category/dependency/buildlink3.mk\"",
+               "CONFIGURE_ARGS+=\tone",
+               "CONFIGURE_ARGS=\ttwo",
+               "CONFIGURE_ARGS+=\tthree")
+       t.SetUpPackage("category/dependency")
+       t.CreateFileDummyBuildlink3("category/dependency/buildlink3.mk")
+       t.CreateFileLines("category/dependency/builtin.mk",
+               MkRcsID,
+               "CONFIGURE_ARGS.Darwin+=\tdarwin")
+
+       G.Check(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:21: Variable CONFIGURE_ARGS is overwritten in line 22.")
+}
+
+func (s *Suite) Test_RedundantScope__if_then_else(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("if-then-else.mk",
+               MkRcsID,
+               ".if exists(${FILE})",
+               "OS=\tNetBSD",
+               ".else",
+               "OS=\tOTHER",
+               ".endif")
+
+       NewRedundantScope().Check(mklines)
+
+       // These two definitions are of course not redundant since they happen in
+       // different branches of the same .if statement.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__if_then_else_without_variable(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("if-then-else.mk",
+               MkRcsID,
+               ".if exists(/nonexistent)",
+               "IT=\texists",
+               ".else",
+               "IT=\tdoesn't exist",
+               ".endif")
+
+       NewRedundantScope().Check(mklines)
+
+       // These two definitions are of course not redundant since they happen in
+       // different branches of the same .if statement.
+       // Even though the .if condition does not refer to any variables,
+       // this still means that the variable assignments are conditional.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__append_then_default(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("append-then-default.mk",
+               MkRcsID,
+               "VAR+=\tvalue",
+               "VAR?=\tvalue")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: ~/append-then-default.mk:3: Default assignment of VAR has no effect because of line 2.")
+}
+
+func (s *Suite) Test_RedundantScope__assign_then_default_in_same_file(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("assign-then-default.mk",
+               MkRcsID,
+               "VAR=\tvalue",
+               "VAR?=\tvalue")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "NOTE: ~/assign-then-default.mk:3: " +
+                       "Default assignment of VAR has no effect because of line 2.")
+}
+
+func (s *Suite) Test_RedundantScope__eval_then_eval(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("filename.mk",
+               MkRcsID,
+               "VAR:=\tvalue",
+               "VAR:=\tvalue",
+               "VAR:=\tother")
+
+       NewRedundantScope().Check(mklines)
+
+       t.CheckOutputLines(
+               "WARN: ~/filename.mk:2: Variable VAR is overwritten in line 3.",
+               "WARN: ~/filename.mk:3: Variable VAR is overwritten in line 4.")
+}
+
+func (s *Suite) Test_RedundantScope__shell_then_assign(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("filename.mk",
+               MkRcsID,
+               "VAR!=\techo echo",
+               "VAR=\techo echo")
+
+       NewRedundantScope().Check(mklines)
+
+       // Although the two variable assignments look very similar, they do
+       // something entirely different. The first executes the echo command,
+       // and the second just assigns a string. Therefore the actual variable
+       // values are different, and the second assignment is not redundant.
+       // It assigns a different value. Nevertheless, the shell command is
+       // redundant and can be removed since its result is never used.
+       t.CheckOutputLines(
+               "WARN: ~/filename.mk:2: Variable VAR is overwritten in line 3.")
+}
+
+func (s *Suite) Test_RedundantScope__shell_then_read_then_assign(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.SetUpFileMkLines("filename.mk",
+               MkRcsID,
+               "VAR!=\techo echo",
+               "OUTPUT:=${VAR}",
+               "VAR=\techo echo")
+
+       NewRedundantScope().Check(mklines)
+
+       // No warning since the value is used in-between.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__assign_then_default_in_included_file(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("assign-then-default.mk",
+               MkRcsID,
+               "VAR=\tvalue",
+               ".include \"included.mk\"")
+       t.CreateFileLines("included.mk",
+               MkRcsID,
+               "VAR?=\tvalue")
+       mklines := t.LoadMkInclude("assign-then-default.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       // If assign-then-default.mk:2 is deleted, VAR still has the same value.
+       t.CheckOutputLines(
+               "NOTE: ~/assign-then-default.mk:2: Definition of VAR is redundant because of included.mk:2.")
+}
+
+func (s *Suite) Test_RedundantScope__conditionally_included_file(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("including.mk",
+               MkRcsID,
+               "VAR=\tvalue",
+               ".if ${COND}",
+               ".  include \"included.mk\"",
+               ".endif")
+       t.CreateFileLines("included.mk",
+               MkRcsID,
+               "VAR?=\tvalue")
+       mklines := t.LoadMkInclude("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       // The assignment in including.mk:2 is only redundant if included.mk is actually included.
+       // Therefore both included.mk:2 nor including.mk:2 are relevant.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__procedure_parameters(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("mk/pkg-build-options.mk",
+               MkRcsID,
+               "USED:=\t${pkgbase}")
+       t.CreateFileLines("including.mk",
+               MkRcsID,
+               "pkgbase=\tpackage1",
+               ".include \"mk/pkg-build-options.mk\"",
+               "",
+               "pkgbase=\tpackage2",
+               ".include \"mk/pkg-build-options.mk\"",
+               "",
+               "pkgbase=\tpackage3",
+               ".include \"mk/pkg-build-options.mk\"")
+       mklines := t.LoadMkInclude("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       // This variable is not overwritten since it is used in-between
+       // by the included file.
+       t.CheckOutputEmpty()
+}
+
+// Branch coverage for info.vari.Constant(). 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
+// unpredictable for pkglint (as of March 2019), therefore it treats these
+// variables as non-constant.
+func (s *Suite) Test_RedundantScope_handleVarassign__shell_followed_by_default(c *check.C) {
+       t := s.Init(c)
+
+       include, get := t.SetUpHierarchy()
+       include("including.mk",
+               "VAR!= echo 'hello, world'",
+               include("included.mk",
+                       "VAR?= hello world"))
+
+       NewRedundantScope().Check(get("including.mk"))
+
+       // If pkglint should ever learn to interpret simple shell commands, there
+       // should be a warning for including.mk:2 that the shell command generates
+       // the default value.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope__overwrite_definition_from_included_file(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("included.mk",
+               MkRcsID,
+               "WRKSRC=\t${WRKDIR}/${PKGBASE}")
+       t.CreateFileLines("including.mk",
+               MkRcsID,
+               "SUBDIR=\t${WRKSRC}",
+               ".include \"included.mk\"",
+               "WRKSRC=\t${WRKDIR}/overwritten")
+       mklines := t.LoadMkInclude("including.mk")
+
+       NewRedundantScope().Check(mklines)
+
+       // Before pkglint 5.7.2 (2019-03-10), the above setup generated a warning:
+       //
+       // WARN: ~/included.mk:2: Variable WRKSRC is overwritten in including.mk:4.
+       //
+       // This warning is obviously wrong since the included file must never
+       // receive a warning. Of course this default definition may be overridden
+       // by the including file.
+       //
+       // The warning was generated because in including.mk:2 the variable WRKSRC
+       // was used for the first time. Back then, each variable had only a single
+       // include path. That include path marks where the variable is used and
+       // defined.
+       //
+       // The variable definition at included.mk didn't modify this include path.
+       // Therefore pkglint wrongly assumed that this variable was only ever
+       // accessed in including.mk and issued a warning.
+       //
+       // To fix this, the RedundantScope now remembers every access to the
+       // variable, and the redundancy warnings are only issued in cases where
+       // either all variable accesses are in files including the current file,
+       // or all variable accesses are in files included by the current file.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_RedundantScope_handleVarassign__conditional(c *check.C) {
+       t := s.Init(c)
+
+       scope := NewRedundantScope()
+       mklines := t.NewMkLines("filename.mk",
+               MkRcsID,
+               "VAR=\tvalue",
+               ".if 1",
+               "VAR=\tconditional",
+               ".endif")
+
+       mklines.ForEach(func(mkline MkLine) {
+               scope.Handle(mkline, mklines.indentation)
+       })
+
+       t.Check(
+               scope.get("VAR").vari.WriteLocations(),
+               deepEquals,
+               []MkLine{mklines.mklines[1], mklines.mklines[3]})
+}
+
+func (s *Suite) Test_includePath_includes(c *check.C) {
+       t := s.Init(c)
+
+       path := func(locations ...string) includePath {
+               return includePath{locations}
+       }
+
+       var (
+               m   = path("Makefile")
+               mc  = path("Makefile", "Makefile.common")
+               mco = path("Makefile", "Makefile.common", "other.mk")
+               mo  = path("Makefile", "other.mk")
+       )
+
+       t.Check(m.includes(m), equals, false)
+
+       t.Check(m.includes(mc), equals, true)
+       t.Check(m.includes(mco), equals, true)
+       t.Check(mc.includes(mco), equals, true)
+
+       t.Check(mc.includes(m), equals, false)
+       t.Check(mc.includes(mo), equals, false)
+       t.Check(mo.includes(mc), equals, false)
+}
+
+func (s *Suite) Test_includePath_equals(c *check.C) {
+       t := s.Init(c)
+
+       path := func(locations ...string) includePath {
+               return includePath{locations}
+       }
+
+       var (
+               m   = path("Makefile")
+               mc  = path("Makefile", "Makefile.common")
+               mco = path("Makefile", "Makefile.common", "other.mk")
+               mo  = path("Makefile", "other.mk")
+       )
+
+       t.Check(m.equals(m), equals, true)
+
+       t.Check(m.equals(mc), equals, false)
+       t.Check(m.equals(mco), equals, false)
+       t.Check(mc.equals(mco), equals, false)
+
+       t.Check(mc.equals(m), equals, false)
+       t.Check(mc.equals(mo), equals, false)
+       t.Check(mo.equals(mc), equals, false)
+}



Home | Main Index | Thread Index | Old Index