pkgsrc-Changes archive

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

CVS commit: pkgsrc/pkgtools/pkglint



Module Name:    pkgsrc
Committed By:   rillig
Date:           Sat Mar  7 23:35:35 UTC 2020

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile
        pkgsrc/pkgtools/pkglint/files: homepage_test.go logging.go
            mkcondchecker.go mkline_test.go mklines.go mklines_test.go
            mkvarusechecker.go mkvarusechecker_test.go package.go pkglint.go
            pkgsrc.go shell.go shell_test.go tools.go tools_test.go util.go
            util_test.go varalignblock.go varalignblock_test.go vardefs.go
            vartype.go

Log Message:
pkgtools/pkglint: update to 19.4.10

Changes since 19.4.9:

In continuation lines with long values, pkglint no longer suggests to
move the continuation backslash in the middle of the variable value, as
that would be impossible.

Warn when a shell command is assigned to a variable that only takes
pathnames. Shell commands can contain command line options, and these
are not pathnames.

The TOOLS_PLATFORM.tool variables are not defined on every platform.
When these variables are used outside an OPSYS check, a warning lists
the platforms where the tool is undefined or only defined conditionally.


To generate a diff of this commit:
cvs rdiff -u -r1.632 -r1.633 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/homepage_test.go
cvs rdiff -u -r1.41 -r1.42 pkgsrc/pkgtools/pkglint/files/logging.go
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/mkcondchecker.go
cvs rdiff -u -r1.79 -r1.80 pkgsrc/pkgtools/pkglint/files/mkline_test.go
cvs rdiff -u -r1.68 -r1.69 pkgsrc/pkgtools/pkglint/files/mklines.go
cvs rdiff -u -r1.62 -r1.63 pkgsrc/pkgtools/pkglint/files/mklines_test.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go \
    pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go
cvs rdiff -u -r1.81 -r1.82 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.74 -r1.75 pkgsrc/pkgtools/pkglint/files/pkglint.go
cvs rdiff -u -r1.50 -r1.51 pkgsrc/pkgtools/pkglint/files/pkgsrc.go
cvs rdiff -u -r1.58 -r1.59 pkgsrc/pkgtools/pkglint/files/shell.go
cvs rdiff -u -r1.64 -r1.65 pkgsrc/pkgtools/pkglint/files/shell_test.go
cvs rdiff -u -r1.22 -r1.23 pkgsrc/pkgtools/pkglint/files/tools.go
cvs rdiff -u -r1.24 -r1.25 pkgsrc/pkgtools/pkglint/files/tools_test.go
cvs rdiff -u -r1.73 -r1.74 pkgsrc/pkgtools/pkglint/files/util.go
cvs rdiff -u -r1.48 -r1.49 pkgsrc/pkgtools/pkglint/files/util_test.go
cvs rdiff -u -r1.17 -r1.18 pkgsrc/pkgtools/pkglint/files/varalignblock.go
cvs rdiff -u -r1.14 -r1.15 \
    pkgsrc/pkgtools/pkglint/files/varalignblock_test.go
cvs rdiff -u -r1.89 -r1.90 pkgsrc/pkgtools/pkglint/files/vardefs.go
cvs rdiff -u -r1.47 -r1.48 pkgsrc/pkgtools/pkglint/files/vartype.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.632 pkgsrc/pkgtools/pkglint/Makefile:1.633
--- pkgsrc/pkgtools/pkglint/Makefile:1.632      Mon Feb 17 20:22:21 2020
+++ pkgsrc/pkgtools/pkglint/Makefile    Sat Mar  7 23:35:35 2020
@@ -1,6 +1,6 @@
-# $NetBSD: Makefile,v 1.632 2020/02/17 20:22:21 rillig Exp $
+# $NetBSD: Makefile,v 1.633 2020/03/07 23:35:35 rillig Exp $
 
-PKGNAME=       pkglint-19.4.9
+PKGNAME=       pkglint-19.4.10
 CATEGORIES=    pkgtools
 DISTNAME=      tools
 MASTER_SITES=  ${MASTER_SITE_GITHUB:=golang/}

Index: pkgsrc/pkgtools/pkglint/files/homepage_test.go
diff -u pkgsrc/pkgtools/pkglint/files/homepage_test.go:1.3 pkgsrc/pkgtools/pkglint/files/homepage_test.go:1.4
--- pkgsrc/pkgtools/pkglint/files/homepage_test.go:1.3  Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/homepage_test.go      Sat Mar  7 23:35:35 2020
@@ -64,8 +64,8 @@ func (s *Suite) Test_HomepageChecker_che
        vt.Output(
                "WARN: filename.mk:11: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
 
-       delete(pkg.vars.firstDef, "MASTER_SITES")
-       delete(pkg.vars.lastDef, "MASTER_SITES")
+       pkg.vars.v("MASTER_SITES").firstDef = nil
+       pkg.vars.v("MASTER_SITES").lastDef = nil
        pkg.vars.Define("MASTER_SITES", t.NewMkLine(pkg.File("Makefile"), 5,
                "MASTER_SITES=\thttps://cdn.NetBSD.org/pub/pkgsrc/distfiles/";))
 
@@ -76,8 +76,8 @@ func (s *Suite) Test_HomepageChecker_che
                "WARN: filename.mk:21: HOMEPAGE should not be defined in terms of MASTER_SITEs. " +
                        "Use https://cdn.NetBSD.org/pub/pkgsrc/distfiles/ directly.")
 
-       delete(pkg.vars.firstDef, "MASTER_SITES")
-       delete(pkg.vars.lastDef, "MASTER_SITES")
+       pkg.vars.v("MASTER_SITES").firstDef = nil
+       pkg.vars.v("MASTER_SITES").lastDef = nil
        pkg.vars.Define("MASTER_SITES", t.NewMkLine(pkg.File("Makefile"), 5,
                "MASTER_SITES=\t${MASTER_SITE_GITHUB}"))
 
@@ -89,8 +89,8 @@ func (s *Suite) Test_HomepageChecker_che
        vt.Output(
                "WARN: filename.mk:31: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
 
-       delete(pkg.vars.firstDef, "MASTER_SITES")
-       delete(pkg.vars.lastDef, "MASTER_SITES")
+       pkg.vars.v("MASTER_SITES").firstDef = nil
+       pkg.vars.v("MASTER_SITES").lastDef = nil
        pkg.vars.Define("MASTER_SITES", t.NewMkLine(pkg.File("Makefile"), 5,
                "MASTER_SITES=\t# none"))
 
@@ -393,7 +393,7 @@ func (s *Suite) Test_HomepageChecker_che
                "https://no-such-name.example.org/";,
                // The "unknown network error" is for compatibility with Go < 1.13.
                `^WARN: filename\.mk:1: Homepage "https://no-such-name.example.org/"; `+
-                       `cannot be checked: (name not found|unknown network error:.*)$`)
+                       `cannot be checked: (name not found|timeout|unknown network error:.*)$`)
 
        // Syntactically invalid URLs are silently skipped since VartypeCheck.URL
        // already warns about them.

Index: pkgsrc/pkgtools/pkglint/files/logging.go
diff -u pkgsrc/pkgtools/pkglint/files/logging.go:1.41 pkgsrc/pkgtools/pkglint/files/logging.go:1.42
--- pkgsrc/pkgtools/pkglint/files/logging.go:1.41       Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/logging.go    Sat Mar  7 23:35:35 2020
@@ -368,7 +368,7 @@ func (l *Logger) ShowSummary(args []stri
                }
 
                l.out.Write(sprintf("%s found.\n",
-                       joinSkipEmptyCambridge("and",
+                       joinCambridge("and",
                                num(l.errors, "error", "errors"),
                                num(l.warnings, "warning", "warnings"),
                                num(l.notes, "note", "notes"))))

Index: pkgsrc/pkgtools/pkglint/files/mkcondchecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mkcondchecker.go:1.4 pkgsrc/pkgtools/pkglint/files/mkcondchecker.go:1.5
--- pkgsrc/pkgtools/pkglint/files/mkcondchecker.go:1.4  Sat Jan 18 21:56:09 2020
+++ pkgsrc/pkgtools/pkglint/files/mkcondchecker.go      Sat Mar  7 23:35:35 2020
@@ -29,6 +29,7 @@ func (ck *MkCondChecker) Check() {
 
        checkVarUse := func(varuse *MkVarUse) {
                var vartype *Vartype // TODO: Insert a better type guess here.
+               // See Test_MkVarUseChecker_checkAssignable__shell_command_in_exists.
                vuc := VarUseContext{vartype, VucLoadTime, VucQuotPlain, false}
                NewMkVarUseChecker(varuse, ck.MkLines, mkline).Check(&vuc)
        }

Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.79 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.80
--- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.79   Sat Jan 18 21:56:09 2020
+++ pkgsrc/pkgtools/pkglint/files/mkline_test.go        Sat Mar  7 23:35:35 2020
@@ -1034,6 +1034,10 @@ func (s *Suite) Test_MkLine_VariableNeed
 }
 
 // Tools, when used in a shell command, must not be quoted.
+// Shell commands may have command line arguments, pathnames must not.
+// The original intention of having both CONFIG_SHELL and CONFIG_SHELL_FLAGS
+// was to separate the command from its arguments.
+// It doesn't hurt though if the command includes some of the arguments as well.
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_shell_command(c *check.C) {
        t := s.Init(c)
 
@@ -1043,11 +1047,14 @@ func (s *Suite) Test_MkLine_VariableNeed
        mklines := t.SetUpFileMkLines("Makefile",
                MkCvsID,
                "",
-               "CONFIG_SHELL=\t${BASH}")
+               "CONFIG_SHELL=\t${BASH}",
+               "DIST_SUBDIR=\t${BASH}")
 
        mklines.Check()
 
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "WARN: ~/Makefile:4: Incompatible types: " +
+                       "BASH (type \"ShellCommand\") cannot be assigned to type \"Pathname\".")
 }
 
 // This test provides code coverage for the "switch vuc.quoting" in the case

Index: pkgsrc/pkgtools/pkglint/files/mklines.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines.go:1.68 pkgsrc/pkgtools/pkglint/files/mklines.go:1.69
--- pkgsrc/pkgtools/pkglint/files/mklines.go:1.68       Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/mklines.go    Sat Mar  7 23:35:35 2020
@@ -194,9 +194,9 @@ func (mklines *MkLines) collectDocumente
                // The commentLines include the the line containing the variable name,
                // leaving 2 of these 3 lines for the actual documentation.
                if commentLines >= 3 && relevant {
-                       forEachStringMkLine(scope.used, func(varname string, mkline *MkLine) {
-                               mklines.allVars.Define(varname, mkline)
-                               mklines.allVars.Use(varname, mkline, VucRunTime)
+                       scope.forEach(func(varname string, data *scopeVar) {
+                               mklines.allVars.Define(varname, data.used)
+                               mklines.allVars.Use(varname, data.used, VucRunTime)
                        })
                }
 

Index: pkgsrc/pkgtools/pkglint/files/mklines_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.62 pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.63
--- pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.62  Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/mklines_test.go       Sat Mar  7 23:35:35 2020
@@ -494,7 +494,8 @@ func (s *Suite) Test_MkLines_collectUsed
 
        mklines.collectUsedVariables()
 
-       t.CheckDeepEquals(mklines.allVars.used, map[string]*MkLine{"VAR": mkline})
+       t.Check(mklines.allVars.vs, check.HasLen, 1)
+       t.CheckEquals(mklines.allVars.v("VAR").used, mkline)
        t.CheckEquals(mklines.allVars.FirstUse("VAR"), mkline)
 }
 
@@ -513,7 +514,7 @@ func (s *Suite) Test_MkLines_collectUsed
 
        mklines.collectUsedVariables()
 
-       t.CheckEquals(len(mklines.allVars.used), 5)
+       t.CheckEquals(len(mklines.allVars.vs), 5)
        t.CheckEquals(mklines.allVars.FirstUse("lparam"), assignMkline)
        t.CheckEquals(mklines.allVars.FirstUse("rparam"), assignMkline)
        t.CheckEquals(mklines.allVars.FirstUse("inner"), shellMkline)
@@ -570,9 +571,11 @@ func (s *Suite) Test_MkLines_collectDocu
        mklines.collectDocumentedVariables()
 
        var varnames []string
-       for varname, mkline := range mklines.allVars.used {
-               varnames = append(varnames, sprintf("%s (line %s)", varname, mkline.Linenos()))
-       }
+       mklines.allVars.forEach(func(varname string, data *scopeVar) {
+               if data.used != nil {
+                       varnames = append(varnames, sprintf("%s (line %s)", varname, data.used.Linenos()))
+               }
+       })
        sort.Strings(varnames)
 
        expected := []string{
@@ -694,7 +697,7 @@ func (s *Suite) Test_MkLines_collectVari
        mklines.Check()
 
        t.CheckDeepEquals(
-               keys(mklines.allVars.firstDef),
+               keys(mklines.allVars.vs),
                []string{
                        "BUILTIN_FIND_FILES_VAR",
                        "BUILTIN_FIND_HEADERS_VAR",

Index: pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go:1.6 pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go:1.7
--- pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go:1.6        Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go    Sat Mar  7 23:35:35 2020
@@ -27,8 +27,10 @@ func (ck *MkVarUseChecker) Check(vuc *Va
 
        ck.checkVarname(vuc.time)
        ck.checkModifiers()
+       ck.checkAssignable(vuc)
        ck.checkQuoting(vuc)
 
+       ck.checkToolsPlatform()
        ck.checkBuildDefs()
        ck.checkDeprecated()
 
@@ -445,6 +447,35 @@ func (ck *MkVarUseChecker) warnToolLoadT
                "except in the package Makefile itself.")
 }
 
+func (ck *MkVarUseChecker) checkAssignable(vuc *VarUseContext) {
+       leftType := vuc.vartype
+       if leftType == nil || leftType.basicType != BtPathname {
+               return
+       }
+       rightType := G.Pkgsrc.VariableType(ck.MkLines, ck.use.varname)
+       if rightType == nil || rightType.basicType != BtShellCommand {
+               return
+       }
+
+       mkline := ck.MkLine
+       if mkline.Varcanon() == "PKG_SHELL.*" {
+               switch ck.use.varname {
+               case "SH", "BASH", "TOOLS_PLATFORM.sh":
+                       return
+               }
+       }
+
+       mkline.Warnf(
+               "Incompatible types: %s (type %q) cannot be assigned to type %q.",
+               ck.use.varname, rightType.basicType.name, leftType.basicType.name)
+       mkline.Explain(
+               "Shell commands often start with a pathname.",
+               "They could also start with a list of environment variable",
+               "definitions, since that is accepted by the shell.",
+               "They can also contain addition command line arguments",
+               "that are not filenames at all.")
+}
+
 // checkVarUseWords checks whether a variable use of the form ${VAR}
 // or ${VAR:modifiers} is allowed in a certain context.
 func (ck *MkVarUseChecker) checkQuoting(vuc *VarUseContext) {
@@ -638,6 +669,40 @@ func (ck *MkVarUseChecker) warnRedundant
        fix.Apply()
 }
 
+func (ck *MkVarUseChecker) checkToolsPlatform() {
+       if ck.MkLine.IsDirective() {
+               return
+       }
+
+       varname := ck.use.varname
+       if varnameCanon(varname) != "TOOLS_PLATFORM.*" {
+               return
+       }
+
+       indentation := ck.MkLines.indentation
+       switch {
+       case indentation.DependsOn("OPSYS"),
+               indentation.DependsOn("MACHINE_PLATFORM"),
+               indentation.DependsOn(varname):
+               // TODO: Only return if the conditional is on the correct OPSYS.
+               return
+       }
+
+       toolName := varnameParam(varname)
+       tool := G.Pkgsrc.Tools.ByName(toolName)
+       if tool == nil {
+               return
+       }
+
+       if len(tool.undefinedOn) > 0 {
+               ck.MkLine.Warnf("%s is undefined on %s.",
+                       varname, joinCambridge("and", tool.undefinedOn...))
+       } else if len(tool.conditionalOn) > 0 {
+               ck.MkLine.Warnf("%s may be undefined on %s.",
+                       varname, joinCambridge("and", tool.conditionalOn...))
+       }
+}
+
 func (ck *MkVarUseChecker) checkBuildDefs() {
        varname := ck.use.varname
 
Index: pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go:1.6 pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go:1.7
--- pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go:1.6   Sat Jan 18 21:56:09 2020
+++ pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go       Sat Mar  7 23:35:35 2020
@@ -957,6 +957,77 @@ func (s *Suite) Test_MkVarUseChecker_war
                "WARN: ~/category/package/Makefile:7: The tool ${MK_TOOL} cannot be used at load time.")
 }
 
+func (s *Suite) Test_MkVarUseChecker_checkAssignable(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       mklines := t.NewMkLines("filename.mk",
+               "BUILTIN_FIND_FILES_VAR:=\tBIN_FILE",
+               "BUILTIN_FIND_FILES.BIN_FILE=\t${TOOLS_PLATFORM.file} /bin/file /usr/bin/file")
+
+       mklines.ForEach(func(mkline *MkLine) {
+               ck := NewMkAssignChecker(mkline, mklines)
+               ck.checkVarassignRight()
+       })
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:2: Incompatible types: " +
+                       "TOOLS_PLATFORM.file (type \"ShellCommand\") " +
+                       "cannot be assigned to type \"Pathname\".")
+}
+
+// NetBSD's chsh program only allows a simple pathname for the shell, without
+// any command line arguments. This makes sense since the shell is started
+// using execve, not system (which would require shell-like argument parsing).
+//
+// Under the assumption that TOOLS_PLATFORM.sh does not contain any command
+// line arguments, it's ok in that special case. This covers most of the
+// real-life situations where this type mismatch (Pathname := ShellCommand)
+// occurs.
+func (s *Suite) Test_MkVarUseChecker_checkAssignable__shell_command_to_pathname(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.SetUpTool("sh", "SH", AtRunTime)
+       t.SetUpTool("bash", "BASH", AtRunTime)
+       mklines := t.NewMkLines("filename.mk",
+               "PKG_SHELL.user=\t${TOOLS_PLATFORM.sh}",
+               "PKG_SHELL.user=\t${SH}",
+               "PKG_SHELL.user=\t${BASH}")
+
+       mklines.ForEach(func(mkline *MkLine) {
+               ck := NewMkAssignChecker(mkline, mklines)
+               ck.checkVarassignRight()
+       })
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:1: Please use ${TOOLS_PLATFORM.sh:Q} " +
+                       "instead of ${TOOLS_PLATFORM.sh}.")
+}
+
+func (s *Suite) Test_MkVarUseChecker_checkAssignable__shell_command_in_exists(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpTool("sh", "SH", AfterPrefsMk)
+       t.SetUpTool("bash", "BASH", AfterPrefsMk)
+       t.SetUpPkgsrc()
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               ".include \"mk/bsd.prefs.mk\"",
+               ".if exists(${TOOLS_PLATFORM.sh})",
+               ".elif exists(${SH})",
+               ".elif exists(${BASH})",
+               ".endif")
+
+       mklines.Check()
+
+       // TODO: Call MkVarUseChecker.checkAssignable with a VarUseContext of type
+       //  BtPathname here.
+       t.CheckOutputEmpty()
+}
+
 func (s *Suite) Test_MkVarUseChecker_checkQuoting(c *check.C) {
        t := s.Init(c)
 
@@ -1153,6 +1224,65 @@ func (s *Suite) Test_MkVarUseChecker_fix
                "AUTOFIX: ~/filename.mk:5: Replacing \"${CFLAGS:N*:Q}\" with \"${CFLAGS:N*:M*:Q}\".")
 }
 
+func (s *Suite) Test_MkVarUseChecker_checkToolsPlatform(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPkgsrc()
+       t.SetUpTool("available", "", AfterPrefsMk)
+       t.SetUpTool("cond1", "", AfterPrefsMk)
+       t.SetUpTool("cond2", "", AfterPrefsMk)
+       t.SetUpTool("undefined", "", AfterPrefsMk)
+       t.CreateFileLines("mk/tools/tools.NetBSD.mk",
+               "TOOLS_PLATFORM.available?=\t/bin/available",
+               "TOOLS_PLATFORM.cond1?=\t/usr/cond1",
+               "TOOLS_PLATFORM.cond2?=\t/usr/cond2",
+               "TOOLS_PLATFORM.undefined?=\t/usr/undefined")
+       t.CreateFileLines("mk/tools/tools.SunOS.mk",
+               "TOOLS_PLATFORM.available?=\t/bin/available",
+               "",
+               ".if exists(/usr/gnu/bin/cond1)",
+               "TOOLS_PLATFORM.cond1?=\t/usr/gnu/bin/cond1",
+               ".endif",
+               "",
+               ".if exists(/usr/gnu/bin/cond2)",
+               "TOOLS_PLATFORM.cond2?=\t/usr/gnu/bin/cond2",
+               ".else",
+               "TOOLS_PLATFORM.cond2?=\t/usr/sfw/bin/cond2",
+               ".endif",
+               "",
+               "# No definition for undefined.")
+       t.Chdir(".")
+       t.FinishSetUp()
+       mklines := t.NewMkLines("filename.mk",
+               MkCvsID,
+               "",
+               ".include \"mk/bsd.prefs.mk\"",
+               "",
+               ".if ${OPSYS} == SunOS",
+               "post-build:",
+               "\t${TOOLS_PLATFORM.available}",
+               "\t${TOOLS_PLATFORM.cond1}",
+               "\t${TOOLS_PLATFORM.cond2}",
+               "\t${TOOLS_PLATFORM.undefined}",
+               ".endif",
+               "",
+               "do-build:",
+               "\t${TOOLS_PLATFORM.available}",
+               "\t${TOOLS_PLATFORM.cond1}",
+               "\t${TOOLS_PLATFORM.cond2}",
+               "\t${TOOLS_PLATFORM.undefined}",
+               "",
+               ".if defined(TOOLS_PLATFORM.undefined)",
+               "\t${TOOLS_PLATFORM.undefined}",
+               ".endif")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: filename.mk:15: TOOLS_PLATFORM.cond1 may be undefined on SunOS.",
+               "WARN: filename.mk:17: TOOLS_PLATFORM.undefined is undefined on SunOS.")
+}
+
 func (s *Suite) Test_MkVarUseChecker_checkBuildDefs(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.81 pkgsrc/pkgtools/pkglint/files/package.go:1.82
--- pkgsrc/pkgtools/pkglint/files/package.go:1.81       Mon Feb 17 20:22:21 2020
+++ pkgsrc/pkgtools/pkglint/files/package.go    Sat Mar  7 23:35:35 2020
@@ -135,7 +135,7 @@ func (pkg *Package) load() ([]CurrPath, 
                files = append(files, pkg.File(pkg.Pkgdir).ReadPaths()...)
        }
        files = append(files, pkg.File(pkg.Patchdir).ReadPaths()...)
-       if pkg.DistinfoFile != NewPackagePathString(pkg.vars.fallback["DISTINFO_FILE"]) {
+       if pkg.DistinfoFile != NewPackagePathString(pkg.vars.v("DISTINFO_FILE").fallback) {
                files = append(files, pkg.File(pkg.DistinfoFile))
        }
 

Index: pkgsrc/pkgtools/pkglint/files/pkglint.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.74 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.75
--- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.74       Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/pkglint.go    Sat Mar  7 23:35:35 2020
@@ -384,13 +384,13 @@ func resolveVariableRefs(text string, mk
 
                        if mklines != nil {
                                // TODO: At load time, use mklines.loadVars instead.
-                               if value, ok := mklines.allVars.LastValueFound(varname); ok {
+                               if value, found, indeterminate := mklines.allVars.LastValueFound(varname); found && !indeterminate {
                                        return value
                                }
                        }
 
                        if pkg != nil {
-                               if value, ok := pkg.vars.LastValueFound(varname); ok {
+                               if value, found, indeterminate := pkg.vars.LastValueFound(varname); found && !indeterminate {
                                        return value
                                }
                        }

Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.50 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.51
--- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.50        Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go     Sat Mar  7 23:35:35 2020
@@ -465,11 +465,84 @@ func (src *Pkgsrc) loadTools() {
                })
        }
 
+       src.loadToolsPlatform()
+
        if trace.Tracing {
                tools.Trace()
        }
 }
 
+func (src *Pkgsrc) loadToolsPlatform() {
+       var systems []string
+       scopes := make(map[string]*RedundantScope)
+       for _, mkFile := range src.File("mk/tools").ReadPaths() {
+               m, opsys := match1(mkFile.Base(), `^tools\.(.+)\.mk$`)
+               if !m {
+                       continue
+               }
+               systems = append(systems, opsys)
+
+               mklines := LoadMk(mkFile, nil, MustSucceed)
+               scope := NewRedundantScope()
+               // Suppress any warnings, just compute the variable state.
+               scope.IsRelevant = func(*MkLine) bool { return false }
+               scope.Check(mklines)
+               scopes[opsys] = scope
+       }
+
+       // 0 = undefined, 1 = conditional, 2 = definitely assigned
+       type status int
+       statusByNameAndOpsys := make(map[string]map[string]status)
+
+       for opsys, scope := range scopes {
+               for varname, varinfo := range scope.vars {
+                       if varnameCanon(varname) == "TOOLS_PLATFORM.*" {
+                               var s status
+                               if varinfo.vari.IsConditional() {
+                                       if len(varinfo.vari.WriteLocations()) == 1 {
+                                               s = 1
+                                       } else {
+                                               // TODO: Don't just count the number of assignments,
+                                               //  check whether they definitely assign the variable.
+                                               //  See substScope.
+                                               s = 2
+                                       }
+                               } else if varinfo.vari.IsConstant() {
+                                       s = 2
+                               } else {
+                                       continue
+                               }
+
+                               name := varnameParam(varname)
+                               if statusByNameAndOpsys[name] == nil {
+                                       statusByNameAndOpsys[name] = make(map[string]status)
+                               }
+                               statusByNameAndOpsys[name][opsys] = s
+                       }
+               }
+       }
+
+       for name, tool := range src.Tools.byName {
+               undefined := make(map[string]bool)
+               conditional := make(map[string]bool)
+               for _, opsys := range systems {
+                       undefined[opsys] = true
+                       conditional[opsys] = true
+               }
+               for opsys, status := range statusByNameAndOpsys[name] {
+                       switch status {
+                       case 1:
+                               delete(undefined, opsys)
+                       case 2:
+                               delete(undefined, opsys)
+                               delete(conditional, opsys)
+                       }
+               }
+               tool.undefinedOn = keysSorted(undefined)
+               tool.conditionalOn = keysSorted(conditional)
+       }
+}
+
 func (src *Pkgsrc) addBuildDefs(varnames ...string) {
        for _, varname := range varnames {
                src.buildDefs[varname] = true
@@ -680,11 +753,16 @@ func (src *Pkgsrc) loadUntypedVars() {
                mklines := LoadMk(path, nil, MustSucceed)
                mklines.collectVariables()
                mklines.collectUsedVariables()
-               def := func(varname string, mkline *MkLine) {
-                       define(varnameCanon(varname), mkline)
-               }
-               forEachStringMkLine(mklines.allVars.firstDef, def)
-               forEachStringMkLine(mklines.allVars.used, def)
+               mklines.allVars.forEach(func(varname string, data *scopeVar) {
+                       if data.firstDef != nil {
+                               define(varnameCanon(varname), data.firstDef)
+                       }
+               })
+               mklines.allVars.forEach(func(varname string, data *scopeVar) {
+                       if data.used != nil {
+                               define(varnameCanon(varname), data.used)
+                       }
+               })
        }
 
        handleFile := func(pathName string, info os.FileInfo, err error) error {

Index: pkgsrc/pkgtools/pkglint/files/shell.go
diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.58 pkgsrc/pkgtools/pkglint/files/shell.go:1.59
--- pkgsrc/pkgtools/pkglint/files/shell.go:1.58 Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/shell.go      Sat Mar  7 23:35:35 2020
@@ -168,7 +168,8 @@ func (scc *SimpleCommandChecker) handleC
 
        varname := varuse.varname
 
-       if vartype := G.Pkgsrc.VariableType(scc.mklines, varname); vartype != nil && vartype.basicType.name == "ShellCommand" {
+       vartype := G.Pkgsrc.VariableType(scc.mklines, varname)
+       if vartype != nil && (vartype.basicType == BtShellCommand || vartype.basicType == BtPathname) {
                scc.checkInstallCommand(shellword)
                return true
        }

Index: pkgsrc/pkgtools/pkglint/files/shell_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shell_test.go:1.64 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.65
--- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.64    Sat Jan  4 19:53:14 2020
+++ pkgsrc/pkgtools/pkglint/files/shell_test.go Sat Mar  7 23:35:35 2020
@@ -45,6 +45,20 @@ func (s *Suite) Test_SimpleCommandChecke
                "WARN: Makefile:8: UNKNOWN_TOOL is used but not defined.")
 }
 
+// Despite its name, the TOOLS_PATH.* name the whole shell command,
+// not just the path of its executable.
+func (s *Suite) Test_SimpleCommandChecker_checkCommandStart__TOOLS_PATH(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "CONFIG_SHELL=\t${TOOLS_PATH.bash}")
+       t.Chdir("category/package")
+       t.FinishSetUp()
+       G.checkdirPackage(".")
+
+       t.CheckOutputEmpty()
+}
+
 func (s *Suite) Test_SimpleCommandChecker_checkInstallCommand(c *check.C) {
        t := s.Init(c)
 
@@ -841,6 +855,7 @@ func (s *Suite) Test_ShellLineChecker_Ch
        t := s.Init(c)
 
        t.SetUpVartypes()
+       t.SetUpTool("[", "TEST", AtRunTime)
        t.SetUpTool("awk", "AWK", AtRunTime)
        t.SetUpTool("cp", "CP", AtRunTime)
        t.SetUpTool("echo", "", AtRunTime)
@@ -927,16 +942,13 @@ func (s *Suite) Test_ShellLineChecker_Ch
                "WARN: filename.mk:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.")
 
        // From x11/wxGTK28/Makefile
-       // TODO: Why is TOOLS_PATH.msgfmt not recognized?
-       //  At least, the warning should be more specific, mentioning USE_TOOLS.
        test(""+
                "set -e; cd ${WRKSRC}/locale; "+
                "for lang in *.po; do "+
                "  [ \"$${lang}\" = \"wxstd.po\" ] && continue; "+
                "  ${TOOLS_PATH.msgfmt} -c -o \"$${lang%.po}.mo\" \"$${lang}\"; "+
                "done",
-               "WARN: filename.mk:1: Unknown shell command \"[\".",
-               "WARN: filename.mk:1: Unknown shell command \"${TOOLS_PATH.msgfmt}\".")
+               nil...)
 
        test("@cp from to",
                "WARN: filename.mk:1: The shell command \"cp\" should not be hidden.")

Index: pkgsrc/pkgtools/pkglint/files/tools.go
diff -u pkgsrc/pkgtools/pkglint/files/tools.go:1.22 pkgsrc/pkgtools/pkglint/files/tools.go:1.23
--- pkgsrc/pkgtools/pkglint/files/tools.go:1.22 Sat Jan  4 19:53:14 2020
+++ pkgsrc/pkgtools/pkglint/files/tools.go      Sat Mar  7 23:35:35 2020
@@ -33,6 +33,15 @@ type Tool struct {
        MustUseVarForm bool
        Validity       Validity
        Aliases        []string
+
+       // The operating systems on which the tool is defined conditionally,
+       // usually by enclosing the tool definition in an ".if exists".
+       // See mk/tools/tools.*.mk.
+       conditionalOn []string
+
+       // The operating systems on which the tool is not defined at all.
+       // See mk/tools/tools.*.mk.
+       undefinedOn []string
 }
 
 func (tool *Tool) String() string {
@@ -152,7 +161,7 @@ func (tr *Tools) Define(name, varname st
 func (tr *Tools) def(name, varname string, mustUseVarForm bool, validity Validity, aliases []string) *Tool {
        assert(tr.IsValidToolName(name))
 
-       fresh := Tool{name, varname, mustUseVarForm, validity, aliases}
+       fresh := Tool{name, varname, mustUseVarForm, validity, aliases, nil, nil}
 
        tool := tr.byName[name]
        if tool == nil {

Index: pkgsrc/pkgtools/pkglint/files/tools_test.go
diff -u pkgsrc/pkgtools/pkglint/files/tools_test.go:1.24 pkgsrc/pkgtools/pkglint/files/tools_test.go:1.25
--- pkgsrc/pkgtools/pkglint/files/tools_test.go:1.24    Sun Dec  8 00:06:38 2019
+++ pkgsrc/pkgtools/pkglint/files/tools_test.go Sat Mar  7 23:35:35 2020
@@ -5,15 +5,15 @@ import "gopkg.in/check.v1"
 func (s *Suite) Test_Tool_UsableAtLoadTime(c *check.C) {
        t := s.Init(c)
 
-       nowhere := Tool{"nowhere", "", false, Nowhere, nil}
+       nowhere := Tool{"nowhere", "", false, Nowhere, nil, nil, nil}
        t.CheckEquals(nowhere.UsableAtLoadTime(false), false)
        t.CheckEquals(nowhere.UsableAtLoadTime(true), false)
 
-       load := Tool{"load", "", false, AfterPrefsMk, nil}
+       load := Tool{"load", "", false, AfterPrefsMk, nil, nil, nil}
        t.CheckEquals(load.UsableAtLoadTime(false), false)
        t.CheckEquals(load.UsableAtLoadTime(true), true)
 
-       run := Tool{"run", "", false, AtRunTime, nil}
+       run := Tool{"run", "", false, AtRunTime, nil, nil, nil}
        t.CheckEquals(run.UsableAtLoadTime(false), false)
        t.CheckEquals(run.UsableAtLoadTime(true), false)
 }
@@ -52,13 +52,13 @@ func (s *Suite) Test_Tool_UsableAtLoadTi
 func (s *Suite) Test_Tool_UsableAtRunTime(c *check.C) {
        t := s.Init(c)
 
-       nowhere := Tool{"nowhere", "", false, Nowhere, nil}
+       nowhere := Tool{"nowhere", "", false, Nowhere, nil, nil, nil}
        t.CheckEquals(nowhere.UsableAtRunTime(), false)
 
-       load := Tool{"load", "", false, AfterPrefsMk, nil}
+       load := Tool{"load", "", false, AfterPrefsMk, nil, nil, nil}
        t.CheckEquals(load.UsableAtRunTime(), true)
 
-       run := Tool{"run", "", false, AtRunTime, nil}
+       run := Tool{"run", "", false, AtRunTime, nil, nil, nil}
        t.CheckEquals(run.UsableAtRunTime(), true)
 }
 

Index: pkgsrc/pkgtools/pkglint/files/util.go
diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.73 pkgsrc/pkgtools/pkglint/files/util.go:1.74
--- pkgsrc/pkgtools/pkglint/files/util.go:1.73  Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/util.go       Sat Mar  7 23:35:35 2020
@@ -210,12 +210,16 @@ func condInt(cond bool, trueValue, false
 }
 
 func keysJoined(m map[string]bool) string {
+       return strings.Join(keysSorted(m), " ")
+}
+
+func keysSorted(m map[string]bool) []string {
        var keys []string
        for key := range m {
                keys = append(keys, key)
        }
        sort.Strings(keys)
-       return strings.Join(keys, " ")
+       return keys
 }
 
 func copyStringMkLine(m map[string]*MkLine) map[string]*MkLine {
@@ -617,22 +621,30 @@ func (o *Once) check(key uint64) bool {
 //
 // See also RedundantScope.
 type Scope struct {
-       firstDef       map[string]*MkLine // TODO: Can this be removed?
-       lastDef        map[string]*MkLine
-       value          map[string]string
-       used           map[string]*MkLine
-       usedAtLoadTime map[string]bool
-       fallback       map[string]string
+       vs map[string]*scopeVar
+}
+
+type scopeVar struct {
+       firstDef       *MkLine
+       lastDef        *MkLine
+       value          string
+       used           *MkLine
+       fallback       string
+       usedAtLoadTime bool
+       indeterminate  bool
 }
 
 func NewScope() Scope {
-       return Scope{
-               make(map[string]*MkLine),
-               make(map[string]*MkLine),
-               make(map[string]string),
-               make(map[string]*MkLine),
-               make(map[string]bool),
-               make(map[string]string)}
+       return Scope{make(map[string]*scopeVar)}
+}
+
+func (s *Scope) v(varname string) *scopeVar {
+       if v, found := s.vs[varname]; found {
+               return v
+       }
+       var sv scopeVar
+       s.vs[varname] = &sv
+       return &sv
 }
 
 // Define marks the variable and its canonicalized form as defined.
@@ -645,8 +657,9 @@ func (s *Scope) Define(varname string, m
 }
 
 func (s *Scope) def(name string, mkline *MkLine) {
-       if s.firstDef[name] == nil {
-               s.firstDef[name] = mkline
+       v := s.v(name)
+       if v.firstDef == nil {
+               v.firstDef = mkline
                if trace.Tracing {
                        trace.Step2("Defining %q for the first time in %s", name, mkline.String())
                }
@@ -654,7 +667,7 @@ func (s *Scope) def(name string, mkline 
                trace.Step2("Defining %q in %s", name, mkline.String())
        }
 
-       s.lastDef[name] = mkline
+       v.lastDef = mkline
 
        // In most cases the defining lines are indeed variable assignments.
        // Exceptions are comments from documentation sections, which still mark
@@ -669,35 +682,37 @@ func (s *Scope) def(name string, mkline 
                value := mkline.Value()
                if trace.Tracing {
                        trace.Stepf("Scope.Define.append %s: %s = %q + %q",
-                               mkline.String(), name, s.value[name], value)
+                               mkline.String(), name, v.value, value)
                }
-               s.value[name] += " " + value
+               v.value += " " + value
        case opAssignDefault:
-               if _, set := s.value[name]; !set {
-                       s.value[name] = mkline.Value()
+               if v.value == "" && !v.indeterminate {
+                       v.value = mkline.Value()
                }
        case opAssignShell:
-               delete(s.value, name)
+               v.value = ""
+               v.indeterminate = true
        default:
-               s.value[name] = mkline.Value()
+               v.value = mkline.Value()
        }
 }
 
 func (s *Scope) Fallback(varname string, value string) {
-       s.fallback[varname] = value
+       s.v(varname).fallback = value
 }
 
 // Use marks the variable and its canonicalized form as used.
 func (s *Scope) Use(varname string, line *MkLine, time VucTime) {
        use := func(name string) {
-               if s.used[name] == nil {
-                       s.used[name] = line
+               v := s.v(name)
+               if v.used == nil {
+                       v.used = line
                        if trace.Tracing {
                                trace.Step2("Using %q in %s", name, line.String())
                        }
                }
                if time == VucLoadTime {
-                       s.usedAtLoadTime[name] = true
+                       v.usedAtLoadTime = true
                }
        }
 
@@ -710,7 +725,7 @@ func (s *Scope) Use(varname string, line
 //  - mentioned in a commented variable assignment,
 //  - mentioned in a documentation comment.
 func (s *Scope) Mentioned(varname string) *MkLine {
-       return s.firstDef[varname]
+       return s.v(varname).firstDef
 }
 
 // IsDefined tests whether the variable is defined.
@@ -719,7 +734,7 @@ func (s *Scope) Mentioned(varname string
 // Even if IsDefined returns true, FirstDefinition doesn't necessarily return true
 // since the latter ignores the default definitions from vardefs.go, keyword dummyVardefMkline.
 func (s *Scope) IsDefined(varname string) bool {
-       mkline := s.firstDef[varname]
+       mkline := s.v(varname).firstDef
        return mkline != nil && mkline.IsVarassign()
 }
 
@@ -745,21 +760,21 @@ func (s *Scope) IsDefinedSimilar(varname
 // IsUsed tests whether the variable is used.
 // It does NOT test the canonicalized variable name.
 func (s *Scope) IsUsed(varname string) bool {
-       return s.used[varname] != nil
+       return s.v(varname).used != nil
 }
 
 // IsUsedSimilar tests whether the variable or its canonicalized form is used.
 func (s *Scope) IsUsedSimilar(varname string) bool {
-       if s.used[varname] != nil {
+       if s.v(varname).used != nil {
                return true
        }
-       return s.used[varnameCanon(varname)] != nil
+       return s.v(varnameCanon(varname)).used != nil
 }
 
 // IsUsedAtLoadTime returns true if the variable is used at load time
 // somewhere.
 func (s *Scope) IsUsedAtLoadTime(varname string) bool {
-       return s.usedAtLoadTime[varname]
+       return s.v(varname).usedAtLoadTime
 }
 
 // FirstDefinition returns the line in which the variable has been defined first.
@@ -771,7 +786,7 @@ func (s *Scope) IsUsedAtLoadTime(varname
 // round: the including file sets a value first, and the included file then
 // assigns a default value using ?=.
 func (s *Scope) FirstDefinition(varname string) *MkLine {
-       mkline := s.firstDef[varname]
+       mkline := s.v(varname).firstDef
        if mkline != nil && mkline.IsVarassign() {
                lastLine := s.LastDefinition(varname)
                if trace.Tracing && lastLine != mkline {
@@ -792,7 +807,7 @@ func (s *Scope) FirstDefinition(varname 
 // round: the including file sets a value first, and the included file then
 // assigns a default value using ?=.
 func (s *Scope) LastDefinition(varname string) *MkLine {
-       mkline := s.lastDef[varname]
+       mkline := s.v(varname).lastDef
        if mkline != nil && mkline.IsVarassign() {
                return mkline
        }
@@ -804,10 +819,10 @@ func (s *Scope) LastDefinition(varname s
 // mk/defaults/mk.conf for documentation.
 func (s *Scope) Commented(varname string) *MkLine {
        var mklines []*MkLine
-       if first := s.firstDef[varname]; first != nil {
+       if first := s.v(varname).firstDef; first != nil {
                mklines = append(mklines, first)
        }
-       if last := s.lastDef[varname]; last != nil {
+       if last := s.v(varname).lastDef; last != nil {
                mklines = append(mklines, last)
        }
 
@@ -827,7 +842,7 @@ func (s *Scope) Commented(varname string
 }
 
 func (s *Scope) FirstUse(varname string) *MkLine {
-       return s.used[varname]
+       return s.v(varname).used
 }
 
 // LastValue returns the value from the last variable definition.
@@ -837,28 +852,50 @@ func (s *Scope) FirstUse(varname string)
 // was not found, or that the variable value cannot be determined
 // reliably. To distinguish these cases, call LastValueFound instead.
 func (s *Scope) LastValue(varname string) string {
-       value, _ := s.LastValueFound(varname)
+       value, _, _ := s.LastValueFound(varname)
        return value
 }
 
-func (s *Scope) LastValueFound(varname string) (value string, found bool) {
-       value, found = s.value[varname]
-       if !found {
-               value, found = s.fallback[varname]
+func (s *Scope) LastValueFound(varname string) (value string, found bool, indeterminate bool) {
+       v := s.vs[varname]
+       if v == nil {
+               return
        }
-       return
+
+       value = v.value
+       found = v.firstDef != nil && v.firstDef.IsVarassign()
+       indeterminate = v.indeterminate
+       if found {
+               return
+       }
+
+       return v.fallback, v.fallback != "", v.indeterminate
 }
 
 func (s *Scope) DefineAll(other Scope) {
        var varnames []string
-       for varname := range other.firstDef {
+       for varname := range other.vs {
                varnames = append(varnames, varname)
        }
        sort.Strings(varnames)
 
        for _, varname := range varnames {
-               s.Define(varname, other.firstDef[varname])
-               s.Define(varname, other.lastDef[varname])
+               v := other.vs[varname]
+               if v.firstDef != nil {
+                       s.Define(varname, v.firstDef)
+                       s.Define(varname, v.lastDef)
+               }
+       }
+}
+
+func (s *Scope) forEach(action func(varname string, data *scopeVar)) {
+       var keys []string
+       for key := range s.vs {
+               keys = append(keys, key)
+       }
+       sort.Strings(keys)
+       for _, key := range keys {
+               action(key, s.vs[key])
        }
 }
 
@@ -1183,32 +1220,32 @@ func joinSkipEmpty(sep string, elements 
        return strings.Join(nonempty, sep)
 }
 
-func joinSkipEmptyCambridge(conn string, elements ...string) string {
-       var nonempty []string
+// joinCambridge returns "first, second conn third".
+// It is used when each element is a single word.
+// Empty elements are ignored completely.
+func joinCambridge(conn string, elements ...string) string {
+       parts := make([]string, 0, 2+2*len(elements))
        for _, element := range elements {
                if element != "" {
-                       nonempty = append(nonempty, element)
+                       parts = append(parts, ", ", element)
                }
        }
 
-       var sb strings.Builder
-       for i, element := range nonempty {
-               if i > 0 {
-                       if i == len(nonempty)-1 {
-                               sb.WriteRune(' ')
-                               sb.WriteString(conn)
-                               sb.WriteRune(' ')
-                       } else {
-                               sb.WriteString(", ")
-                       }
-               }
-               sb.WriteString(element)
+       if len(parts) == 0 {
+               return ""
+       }
+       if len(parts) < 4 {
+               return parts[1]
        }
 
-       return sb.String()
+       parts = append(parts[1:len(parts)-2], " ", conn, " ", parts[len(parts)-1])
+       return strings.Join(parts, "")
 }
 
-func joinSkipEmptyOxford(conn string, elements ...string) string {
+// joinOxford returns "first, second, conn third".
+// It is used when each element may consist of multiple words.
+// Empty elements are ignored completely.
+func joinOxford(conn string, elements ...string) string {
        var nonempty []string
        for _, element := range elements {
                if element != "" {

Index: pkgsrc/pkgtools/pkglint/files/util_test.go
diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.48 pkgsrc/pkgtools/pkglint/files/util_test.go:1.49
--- pkgsrc/pkgtools/pkglint/files/util_test.go:1.48     Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/util_test.go  Sat Mar  7 23:35:35 2020
@@ -543,9 +543,10 @@ func (s *Suite) Test_Scope__commented_va
        t.CheckEquals(scope.Mentioned("VAR"), mkline)
        t.CheckEquals(scope.Commented("VAR"), mkline)
 
-       value, found := scope.LastValueFound("VAR")
+       value, found, indeterminate := scope.LastValueFound("VAR")
        t.CheckEquals(value, "")
        t.CheckEquals(found, false)
+       t.CheckEquals(indeterminate, false)
 }
 
 func (s *Suite) Test_Scope_Define(c *check.C) {
@@ -553,39 +554,40 @@ func (s *Suite) Test_Scope_Define(c *che
 
        scope := NewScope()
 
-       test := func(line string, ok bool, value string) {
+       test := func(line string, found, indeterminate bool, value string) {
                mkline := t.NewMkLine("file.mk", 123, line)
                scope.Define("BUILD_DIRS", mkline)
 
-               actualValue, actualFound := scope.LastValueFound("BUILD_DIRS")
+               actualValue, actualFound, actualIndeterminate := scope.LastValueFound("BUILD_DIRS")
 
-               t.CheckEquals(actualValue, value)
-               t.CheckEquals(actualFound, ok)
-               t.CheckEquals(scope.value["BUILD_DIRS"], value)
+               t.CheckDeepEquals(
+                       []interface{}{actualFound, actualIndeterminate, actualValue},
+                       []interface{}{found, indeterminate, value})
+               t.CheckEquals(scope.vs["BUILD_DIRS"].value, value)
        }
 
        test("BUILD_DIRS?=\tdefault",
-               true, "default")
+               true, false, "default")
 
        test(
                "BUILD_DIRS=\tone two three",
-               true, "one two three")
+               true, false, "one two three")
 
        test(
                "BUILD_DIRS+=\tfour",
-               true, "one two three four")
+               true, false, "one two three four")
 
        // Later default assignments do not have an effect.
        test("BUILD_DIRS?=\tdefault",
-               true, "one two three four")
+               true, false, "one two three four")
 
        test("BUILD_DIRS!=\techo dynamic",
-               false, "")
+               true, true, "")
 
-       // FIXME: This is not correct. The shell assignment sets the variable,
-       //  after which all further default assignments are ignored.
+       // The shell assignment above sets the variable to an indeterminate
+       // value, after which all further default assignments are ignored.
        test("BUILD_DIRS?=\tdefault after shell assignment",
-               true, "default after shell assignment")
+               true, true, "")
 }
 
 func (s *Suite) Test_Scope_Mentioned(c *check.C) {
@@ -727,9 +729,7 @@ func (s *Suite) Test_Scope_DefineAll(c *
        dst := NewScope()
        dst.DefineAll(src)
 
-       c.Check(dst.firstDef, check.HasLen, 0)
-       c.Check(dst.lastDef, check.HasLen, 0)
-       c.Check(dst.used, check.HasLen, 0)
+       c.Check(dst.vs, check.HasLen, 0)
 
        src.Define("VAR", t.NewMkLine("file.mk", 1, "VAR=value"))
        dst.DefineAll(src)
@@ -1020,23 +1020,23 @@ func (s *Suite) Test_joinSkipEmpty(c *ch
                "one, two, three")
 }
 
-func (s *Suite) Test_joinSkipEmptyCambridge(c *check.C) {
+func (s *Suite) Test_joinCambridge(c *check.C) {
        t := s.Init(c)
 
        t.CheckDeepEquals(
-               joinSkipEmptyCambridge("and", "", "one", "", "", "two", "", "three"),
+               joinCambridge("and", "", "one", "", "", "two", "", "three"),
                "one, two and three")
 
        t.CheckDeepEquals(
-               joinSkipEmptyCambridge("and", "", "one", "", ""),
+               joinCambridge("and", "", "one", "", ""),
                "one")
 }
 
-func (s *Suite) Test_joinSkipEmptyOxford(c *check.C) {
+func (s *Suite) Test_joinOxford(c *check.C) {
        t := s.Init(c)
 
        t.CheckDeepEquals(
-               joinSkipEmptyOxford("and", "", "one", "", "", "two", "", "three"),
+               joinOxford("and", "", "one", "", "", "two", "", "three"),
                "one, two, and three")
 }
 

Index: pkgsrc/pkgtools/pkglint/files/varalignblock.go
diff -u pkgsrc/pkgtools/pkglint/files/varalignblock.go:1.17 pkgsrc/pkgtools/pkglint/files/varalignblock.go:1.18
--- pkgsrc/pkgtools/pkglint/files/varalignblock.go:1.17 Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/varalignblock.go      Sat Mar  7 23:35:35 2020
@@ -562,6 +562,7 @@ func (info *varalignLine) alignValueSing
        }
        fix.ReplaceAt(info.rawIndex, info.spaceBeforeValueIndex(), oldSpace, newSpace)
        fix.Apply()
+       info.spaceBeforeValue = newSpace
 }
 
 func (info *varalignLine) alignValueInitial(newWidth int) {
@@ -855,10 +856,7 @@ func (p *varalignParts) isCanonicalFollo
 }
 
 func (p *varalignParts) isTooLongFor(valueColumn int) bool {
-       // FIXME: As of 2020-01-27, this method is called from
-       //  Test_VaralignBlock__right_margin with valueColumn == 0,
-       //  which doesn't make sense. It should at least be 8.
-       column := tabWidthAppend(valueColumn, p.value)
+       column := tabWidthAppend(imax(valueColumn, 8), p.value)
        if p.isContinuation() {
                column += 2
        }

Index: pkgsrc/pkgtools/pkglint/files/varalignblock_test.go
diff -u pkgsrc/pkgtools/pkglint/files/varalignblock_test.go:1.14 pkgsrc/pkgtools/pkglint/files/varalignblock_test.go:1.15
--- pkgsrc/pkgtools/pkglint/files/varalignblock_test.go:1.14    Mon Feb 17 20:22:21 2020
+++ pkgsrc/pkgtools/pkglint/files/varalignblock_test.go Sat Mar  7 23:35:35 2020
@@ -2073,7 +2073,7 @@ func (s *Suite) Test_VaralignBlock__righ
                "\t............................................................70 \t\t\\",
                "\t........................................................66")
        vt.Diagnostics(
-               "NOTE: Makefile:2: The continuation backslash should be in column 73, not 81.",
+               "NOTE: Makefile:2: The continuation backslash should be preceded by a single space.",
                "NOTE: Makefile:3: The continuation backslash should be in column 73, not 81.")
        vt.Autofixes(
                "AUTOFIX: Makefile:2: Replacing \"\\t\" with \" \".",
@@ -3585,11 +3585,7 @@ func (s *Suite) Test_varalignLine_alignV
                        t.CheckEqualsf(
                                mkline.RawText(0), after,
                                "Line.raw.text, autofix=%v", autofix)
-
-                       // As of 2019-12-11, the info fields are not updated
-                       // accordingly, but they should.
-                       // FIXME: update info accordingly
-                       t.CheckEqualsf(info.String(), before,
+                       t.CheckEqualsf(info.String(), after,
                                "info.String, autofix=%v", autofix)
                }
 

Index: pkgsrc/pkgtools/pkglint/files/vardefs.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.89 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.90
--- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.89       Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/vardefs.go    Sat Mar  7 23:35:35 2020
@@ -421,7 +421,7 @@ func (reg *VarTypeRegistry) enumFrom(
                G.Logger.TechFatalf(
                        mklines.lines.Filename,
                        "Must contain at least 1 variable definition for %s.",
-                       joinSkipEmptyCambridge("or", varcanons...))
+                       joinCambridge("or", varcanons...))
        }
 
        if trace.Tracing {
@@ -1067,7 +1067,8 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.pkg("CONFIGURE_SCRIPT", BtPathname)
        reg.pkglist("CONFIG_GUESS_OVERRIDE", BtPathPattern)
        reg.pkglist("CONFIG_STATUS_OVERRIDE", BtPathPattern)
-       reg.pkg("CONFIG_SHELL", BtPathname)
+       reg.pkg("CONFIG_SHELL", BtShellCommand)
+       reg.cmdline("CONFIG_SHELL_FLAGS", BtShellWord, List)
        reg.pkglist("CONFIG_SUB_OVERRIDE", BtPathPattern)
        reg.pkglist("CONFLICTS", BtDependencyPattern)
        reg.pkgappend("CONF_FILES", BtConfFiles)

Index: pkgsrc/pkgtools/pkglint/files/vartype.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype.go:1.47 pkgsrc/pkgtools/pkglint/files/vartype.go:1.48
--- pkgsrc/pkgtools/pkglint/files/vartype.go:1.47       Sat Feb 15 13:48:40 2020
+++ pkgsrc/pkgtools/pkglint/files/vartype.go    Sat Mar  7 23:35:35 2020
@@ -182,7 +182,7 @@ func (perms ACLPermissions) String() str
 }
 
 func (perms ACLPermissions) HumanString() string {
-       return joinSkipEmptyOxford("or",
+       return joinOxford("or",
                condStr(perms.Contains(aclpSet), "set", ""),
                condStr(perms.Contains(aclpSetDefault), "given a default value", ""),
                condStr(perms.Contains(aclpAppend), "appended to", ""),
@@ -265,12 +265,12 @@ func (vt *Vartype) AlternativeFiles(perm
                neg = merge(neg)
        }
 
-       positive := joinSkipEmptyCambridge("or", pos...)
+       positive := joinCambridge("or", pos...)
        if positive == "" {
                return ""
        }
 
-       negative := joinSkipEmptyCambridge("or", neg...)
+       negative := joinCambridge("or", neg...)
        if negative == "" {
                return positive
        }



Home | Main Index | Thread Index | Old Index