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 Nov 30 20:35:12 UTC 2019

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile
        pkgsrc/pkgtools/pkglint/files: autofix.go autofix_test.go category.go
            distinfo.go mklexer.go mklexer_test.go mkline.go mklineparser.go
            mkparser.go mktokenslexer.go package.go path.go path_test.go
            pkglint.0 pkglint.1 pkglint.go pkglint_test.go pkgsrc.go
            pkgsrc_test.go shtokenizer_test.go toplevel.go util.go util_test.go
        pkgsrc/pkgtools/pkglint/files/licenses: licenses.go licenses_test.go
        pkgsrc/pkgtools/pkglint/files/textproc: lexer.go

Log Message:
pkgtools/pkglint: update to 19.3.12

Changes since 19.3.11:

The command line option -Wstyle has been removed since it didn't have
any effect.

License names may contain underscores. This fixes 3 parse errors and 2
wrong notes about seemingly unused licenses.

The parser for Makefile variables has been improved for some edge cases.
The :M and :N modifiers behave surprisingly when they contain unbalanced
parentheses or braces. Pkglint now parses them in the same way as bmake,
but doesn't warn since these cases are not actually used in pkgsrc.


To generate a diff of this commit:
cvs rdiff -u -r1.611 -r1.612 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.31 -r1.32 pkgsrc/pkgtools/pkglint/files/autofix.go
cvs rdiff -u -r1.32 -r1.33 pkgsrc/pkgtools/pkglint/files/autofix_test.go
cvs rdiff -u -r1.25 -r1.26 pkgsrc/pkgtools/pkglint/files/category.go
cvs rdiff -u -r1.37 -r1.38 pkgsrc/pkgtools/pkglint/files/distinfo.go \
    pkgsrc/pkgtools/pkglint/files/mkparser.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go \
    pkgsrc/pkgtools/pkglint/files/util_test.go
cvs rdiff -u -r1.1 -r1.2 pkgsrc/pkgtools/pkglint/files/mklexer.go \
    pkgsrc/pkgtools/pkglint/files/mklexer_test.go
cvs rdiff -u -r1.65 -r1.66 pkgsrc/pkgtools/pkglint/files/mkline.go
cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/mklineparser.go
cvs rdiff -u -r1.2 -r1.3 pkgsrc/pkgtools/pkglint/files/mktokenslexer.go \
    pkgsrc/pkgtools/pkglint/files/path.go \
    pkgsrc/pkgtools/pkglint/files/path_test.go
cvs rdiff -u -r1.70 -r1.71 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.40 -r1.41 pkgsrc/pkgtools/pkglint/files/pkglint.0
cvs rdiff -u -r1.57 -r1.58 pkgsrc/pkgtools/pkglint/files/pkglint.1
cvs rdiff -u -r1.66 -r1.67 pkgsrc/pkgtools/pkglint/files/pkglint.go
cvs rdiff -u -r1.51 -r1.52 pkgsrc/pkgtools/pkglint/files/pkglint_test.go
cvs rdiff -u -r1.43 -r1.44 pkgsrc/pkgtools/pkglint/files/pkgsrc.go
cvs rdiff -u -r1.20 -r1.21 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
cvs rdiff -u -r1.24 -r1.25 pkgsrc/pkgtools/pkglint/files/toplevel.go
cvs rdiff -u -r1.60 -r1.61 pkgsrc/pkgtools/pkglint/files/util.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/licenses/licenses.go
cvs rdiff -u -r1.8 -r1.9 \
    pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go
cvs rdiff -u -r1.7 -r1.8 pkgsrc/pkgtools/pkglint/files/textproc/lexer.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.611 pkgsrc/pkgtools/pkglint/Makefile:1.612
--- pkgsrc/pkgtools/pkglint/Makefile:1.611      Wed Nov 27 22:10:06 2019
+++ pkgsrc/pkgtools/pkglint/Makefile    Sat Nov 30 20:35:11 2019
@@ -1,6 +1,6 @@
-# $NetBSD: Makefile,v 1.611 2019/11/27 22:10:06 rillig Exp $
+# $NetBSD: Makefile,v 1.612 2019/11/30 20:35:11 rillig Exp $
 
-PKGNAME=       pkglint-19.3.11
+PKGNAME=       pkglint-19.3.12
 CATEGORIES=    pkgtools
 DISTNAME=      tools
 MASTER_SITES=  ${MASTER_SITE_GITHUB:=golang/}

Index: pkgsrc/pkgtools/pkglint/files/autofix.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix.go:1.31 pkgsrc/pkgtools/pkglint/files/autofix.go:1.32
--- pkgsrc/pkgtools/pkglint/files/autofix.go:1.31       Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix.go    Sat Nov 30 20:35:11 2019
@@ -442,8 +442,8 @@ func SaveAutofixChanges(lines *Lines) (a
        }
 
        if G.Testing {
-               abs := abspath(lines.Filename)
-               absTmp := abspath(NewPathSlash(os.TempDir()))
+               abs := G.Abs(lines.Filename)
+               absTmp := G.Abs(NewPathSlash(os.TempDir()))
                assertf(abs.HasPrefixPath(absTmp), "%q must be inside %q", abs, absTmp)
        }
 

Index: pkgsrc/pkgtools/pkglint/files/autofix_test.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.32 pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.33
--- pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.32  Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix_test.go       Sat Nov 30 20:35:11 2019
@@ -578,8 +578,7 @@ func (s *Suite) Test_Autofix_ReplaceAt(c
                "AUTOFIX: filename.mk:1: Replacing \"=\" with \"+=\".")
 
        // If the text at the precisely given position does not match,
-       // the note is still printed because of the fix.Anyway(), but
-       // nothing is replaced automatically.
+       // the note is still printed, but nothing is replaced automatically.
        test(
                lines(
                        "VAR=value1 \\",
@@ -997,9 +996,9 @@ func (s *Suite) Test_Autofix_Apply__auto
                "AUTOFIX: filename:5: Replacing \"text\" with \"replacement\".")
 }
 
-// In --autofix mode or --show-autofix mode, the fix.Anyway doesn't
-// have any effect, therefore the errors from such autofixes are
-// not counted, and the exitcode stays at 0.
+// In --autofix mode or --show-autofix mode, those errors that have
+// been automatically fixed are not counted, and the others are filtered
+// out, therefore the exitcode stays at 0.
 func (s *Suite) Test_Autofix_Apply__anyway_error(c *check.C) {
        t := s.Init(c)
 
@@ -1036,8 +1035,8 @@ func (s *Suite) Test_Autofix_Apply__sour
        // Nothing is replaced since, as of June 2019, pkglint doesn't
        // know which of the three "word" should be replaced.
        //
-       // The note is not logged since fix.Anyway only applies when neither
-       // --show-autofix nor --autofix is given in the command line.
+       // The note is not logged since --show-autofix nor --autofix is
+       // given in the command line.
        t.CheckOutputEmpty()
        t.CheckFileLines("filename",
                "word word word")

Index: pkgsrc/pkgtools/pkglint/files/category.go
diff -u pkgsrc/pkgtools/pkglint/files/category.go:1.25 pkgsrc/pkgtools/pkglint/files/category.go:1.26
--- pkgsrc/pkgtools/pkglint/files/category.go:1.25      Sat Nov 23 23:35:56 2019
+++ pkgsrc/pkgtools/pkglint/files/category.go   Sat Nov 30 20:35:11 2019
@@ -158,7 +158,7 @@ func CheckdirCategory(dir Path) {
                var recurseInto []Path
                for _, msub := range mSubdirs {
                        if !msub.line.IsCommentedVarassign() {
-                               recurseInto = append(recurseInto, joinPath(dir, msub.name))
+                               recurseInto = append(recurseInto, dir.JoinNoClean(msub.name))
                        }
                }
                G.Todo.PushFront(recurseInto...)

Index: pkgsrc/pkgtools/pkglint/files/distinfo.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo.go:1.37 pkgsrc/pkgtools/pkglint/files/distinfo.go:1.38
--- pkgsrc/pkgtools/pkglint/files/distinfo.go:1.37      Sat Nov 23 23:35:56 2019
+++ pkgsrc/pkgtools/pkglint/files/distinfo.go   Sat Nov 30 20:35:11 2019
@@ -194,7 +194,7 @@ func (ck *distinfoLinesChecker) checkAlg
 
        distdir := G.Pkgsrc.File("distfiles")
 
-       distfile := cleanpath(joinPath(distdir, info.filename()))
+       distfile := cleanpath(distdir.JoinNoClean(info.filename()))
        if !distfile.IsFile() {
 
                // It's a rare situation that the explanation is generated
@@ -370,7 +370,7 @@ func (ck *distinfoLinesChecker) checkUnc
        hash := info.hash
        line := info.line
 
-       patchFileName := joinPath(ck.patchdir, patchName)
+       patchFileName := ck.patchdir.JoinNoClean(patchName)
        resolvedPatchFileName := ck.pkg.File(patchFileName)
        if ck.distinfoIsCommitted && !isCommitted(resolvedPatchFileName) {
                line.Warnf("%s is registered in distinfo but not added to CVS.", line.PathToFile(resolvedPatchFileName))
Index: pkgsrc/pkgtools/pkglint/files/mkparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser.go:1.37 pkgsrc/pkgtools/pkglint/files/mkparser.go:1.38
--- pkgsrc/pkgtools/pkglint/files/mkparser.go:1.37      Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/mkparser.go   Sat Nov 30 20:35:11 2019
@@ -200,7 +200,7 @@ func (p *MkParser) mkCondTerm() *MkCondT
        lexer.Reset(mark)
 
        lexer.Skip(1)
-       var rhsText strings.Builder
+       rhsText := NewLazyStringBuilder(lexer.Rest())
 loop:
        for {
                m := lexer.Mark()
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.37 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.38
--- pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.37   Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go        Sat Nov 30 20:35:11 2019
@@ -1245,7 +1245,7 @@ func (s *Suite) Test_Pkgsrc_Relpath(c *c
                // See Tester.Chdir; a direct Chdir works here since this test
                // neither loads lines nor processes them.
                assertNil(os.Chdir(path.String()), "Chdir %s", path)
-               G.cwd = abspath(path)
+               G.cwd = G.Abs(path)
        }
 
        t.CreateFileLines("testdir/subdir/dummy")
Index: pkgsrc/pkgtools/pkglint/files/util_test.go
diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.37 pkgsrc/pkgtools/pkglint/files/util_test.go:1.38
--- pkgsrc/pkgtools/pkglint/files/util_test.go:1.37     Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/util_test.go  Sat Nov 30 20:35:11 2019
@@ -387,80 +387,6 @@ func (s *Suite) Test_cleanpath(c *check.
        test(".././././././././", "..")
 }
 
-func (s *Suite) Test_pathContains(c *check.C) {
-       t := s.Init(c)
-
-       test := func(haystack, needle string, expected bool) {
-               actual := pathContains(haystack, needle)
-               t.CheckEquals(actual, expected)
-       }
-
-       testPanic := func(haystack, needle string) {
-               t.c.Check(
-                       func() { _ = pathContains(haystack, needle) },
-                       check.PanicMatches,
-                       `runtime error: index out of range.*`)
-       }
-
-       testPanic("", "")
-       testPanic("a", "")
-       testPanic("a/b/c", "")
-
-       test("a", "a", true)
-       test("a", "b", false)
-       test("a", "A", false)
-       test("a/b/c", "a", true)
-       test("a/b/c", "b", true)
-       test("a/b/c", "c", true)
-       test("a/b/c", "a/b", true)
-       test("a/b/c", "b/c", true)
-       test("a/b/c", "a/b/c", true)
-       test("aa/bb/cc", "a/b", false)
-       test("aa/bb/cc", "a/bb", false)
-       test("aa/bb/cc", "aa/b", false)
-       test("aa/bb/cc", "aa/bb", true)
-       test("aa/bb/cc", "a", false)
-       test("aa/bb/cc", "b", false)
-       test("aa/bb/cc", "c", false)
-}
-
-func (s *Suite) Test_pathContainsDir(c *check.C) {
-       t := s.Init(c)
-
-       test := func(haystack, needle Path, expected bool) {
-               actual := pathContainsDir(haystack, needle)
-               t.CheckEquals(actual, expected)
-       }
-
-       testPanic := func(haystack, needle Path) {
-               t.c.Check(
-                       func() { _ = pathContainsDir(haystack, needle) },
-                       check.PanicMatches,
-                       `^runtime error: index out of range.*`)
-       }
-
-       testPanic("", "")
-       testPanic("a", "")
-       testPanic("a/b/c", "")
-
-       test("a", "a", false)
-       test("a", "b", false)
-       test("a", "A", false)
-       test("a/b/c", "a", true)
-       test("a/b/c", "b", true)
-       test("a/b/c", "c", false)
-       test("a/b/c", "a/b", true)
-       test("a/b/c", "b/c", false)
-       test("a/b/c", "a/b/c", false)
-       test("aa/bb/cc", "a/b", false)
-       test("aa/bb/cc", "a/bb", false)
-       test("aa/bb/cc", "aa/b", false)
-       test("aa/bb/cc", "aa/bb", true)
-       test("aa/bb/cc", "a", false)
-       test("aa/bb/cc", "b", false)
-       test("aa/bb/cc", "c", false)
-}
-
 const reMkIncludeBenchmark = `^\.([\t ]*)(s?include)[\t ]+\"([^\"]+)\"[\t ]*(?:#.*)?$`
 const reMkIncludeBenchmarkPositive = `^\.([\t ]*)(s?include)[\t ]+\"(.+)\"[\t ]*(?:#.*)?$`
 
@@ -1111,3 +1037,91 @@ func (s *Suite) Test_shquote(c *check.C)
        test("simple", "simple")
        test("~", "'~'")
 }
+
+func (s *Suite) Test_LazyStringBuilder_WriteByte__exact_match(c *check.C) {
+       t := s.Init(c)
+
+       sb := NewLazyStringBuilder("word")
+
+       sb.WriteByte('w')
+       sb.WriteByte('o')
+       sb.WriteByte('r')
+       sb.WriteByte('d')
+
+       t.CheckEquals(sb.String(), "word")
+       c.Check(sb.buf, check.IsNil)
+}
+
+func (s *Suite) Test_LazyStringBuilder_WriteByte__longer_than_expected(c *check.C) {
+       t := s.Init(c)
+
+       sb := NewLazyStringBuilder("word")
+       sb.WriteByte('w')
+       sb.WriteByte('o')
+       sb.WriteByte('r')
+       sb.WriteByte('d')
+       sb.WriteByte('s')
+
+       t.CheckEquals(sb.String(), "words")
+       t.CheckDeepEquals(sb.buf, []byte{'w', 'o', 'r', 'd', 's'})
+}
+
+func (s *Suite) Test_LazyStringBuilder_WriteByte__shorter_than_expected(c *check.C) {
+       t := s.Init(c)
+
+       sb := NewLazyStringBuilder("word")
+       sb.WriteByte('w')
+       sb.WriteByte('o')
+
+       t.CheckEquals(sb.String(), "wo")
+       c.Check(sb.buf, check.IsNil)
+
+       sb.WriteByte('r')
+       sb.WriteByte('d')
+
+       t.CheckEquals(sb.String(), "word")
+       c.Check(sb.buf, check.IsNil)
+}
+
+func (s *Suite) Test_LazyStringBuilder_WriteByte__other_than_expected(c *check.C) {
+       t := s.Init(c)
+
+       sb := NewLazyStringBuilder("word")
+       sb.WriteByte('w')
+       sb.WriteByte('o')
+       sb.WriteByte('l')
+       sb.WriteByte('f')
+
+       t.CheckEquals(sb.String(), "wolf")
+       t.CheckDeepEquals(sb.buf, []byte{'w', 'o', 'l', 'f'})
+}
+
+func (s *Suite) Test_LazyStringBuilder_Reset(c *check.C) {
+       t := s.Init(c)
+
+       sb := NewLazyStringBuilder("word")
+       sb.WriteByte('w')
+
+       sb.Reset("other")
+
+       t.CheckEquals(sb.String(), "")
+
+       sb.WriteString("word")
+
+       t.CheckEquals(sb.String(), "word")
+       t.CheckEquals(sb.usingBuf, true)
+       t.CheckDeepEquals(sb.buf, []byte("word"))
+
+       sb.Reset("")
+
+       t.CheckEquals(sb.String(), "")
+       t.CheckEquals(sb.usingBuf, false)
+       t.CheckDeepEquals(sb.buf, []byte("word"))
+
+       sb.WriteByte('x')
+
+       // Ensure that the buffer is reset properly.
+       t.CheckEquals(sb.String(), "x")
+       t.CheckEquals(sb.usingBuf, true)
+       t.CheckDeepEquals(sb.buf, []byte("x"))
+}

Index: pkgsrc/pkgtools/pkglint/files/mklexer.go
diff -u pkgsrc/pkgtools/pkglint/files/mklexer.go:1.1 pkgsrc/pkgtools/pkglint/files/mklexer.go:1.2
--- pkgsrc/pkgtools/pkglint/files/mklexer.go:1.1        Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/mklexer.go    Sat Nov 30 20:35:11 2019
@@ -8,6 +8,15 @@ import (
 
 // MkLexer splits a text into a sequence of variable uses
 // and plain text.
+//
+// The actual parsing algorithm in devel/bmake/files/var.c differs from
+// pkglint's parser in many ways and produces different results in
+// almost all edge cases. See devel/bmake/files/var.c:/'\\\\'/.
+//
+// The pkglint parser had been built from scratch using naive assumptions
+// about how bmake parses these expressions. These assumptions do not hold
+// a strict test, but luckily the pkgsrc package developers don't explore
+// these edge cases anyway.
 type MkLexer struct {
        lexer *textproc.Lexer
        line  *Line
@@ -153,10 +162,12 @@ func (p *MkLexer) Varname() string {
 // This is used for the :L and :? modifiers since they accept arbitrary
 // text as the "variable name" and effectively interpret it as the variable
 // value instead.
+//
+// See devel/bmake/files/var.c:/^VarGetPattern/
 func (p *MkLexer) varUseText(closing byte) string {
        lexer := p.lexer
        start := lexer.Mark()
-       re := regcomp(regex.Pattern(condStr(closing == '}', `^([^$:}]|\$\$)+`, `^([^$:)]|\$\$)+`)))
+       re := regcomp(regex.Pattern(condStr(closing == '}', `^([^$:\\}]|\$\$|\\.)+`, `^([^$:\\)]|\$\$|\\.)+`)))
        for p.VarUse() != nil || lexer.SkipRegexp(re) {
        }
        return lexer.Since(start)
@@ -213,34 +224,14 @@ func (p *MkLexer) varUseModifier(varname
                }
 
                if hasPrefix(mod, "ts") {
-                       // See devel/bmake/files/var.c:/case 't'
-                       sep := mod[2:] + p.varUseText(closing)
-                       switch {
-                       case sep == "":
-                               lexer.SkipString(":")
-                       case len(sep) == 1:
-                               break
-                       case matches(sep, `^\\\d+`):
-                               break
-                       default:
-                               if p.line != nil {
-                                       p.line.Warnf("Invalid separator %q for :ts modifier of %q.", sep, varname)
-                                       p.line.Explain(
-                                               "The separator for the :ts modifier must be either a single character",
-                                               "or an escape sequence like \\t or \\n or an octal or decimal escape",
-                                               "sequence; see the bmake man page for further details.")
-                               }
-                       }
-                       return lexer.Since(mark)
+                       return p.varUseModifierSeparator(mod, closing, lexer, varname, mark)
                }
 
-       case '=', 'D', 'M', 'N', 'U':
-               lexer.Skip(1)
-               re := regcomp(regex.Pattern(condStr(closing == '}', `^([^$:\\}]|\$\$|\\.)+`, `^([^$:\\)]|\$\$|\\.)+`)))
-               for p.VarUse() != nil || lexer.SkipRegexp(re) {
-               }
-               arg := lexer.Since(mark)
-               return strings.Replace(arg, "\\:", ":", -1)
+       case 'D', 'U':
+               return p.varUseText(closing)
+
+       case 'M', 'N':
+               return p.varUseModifierMatch(closing)
 
        case 'C', 'S':
                if ok, _, _, _, _ := p.varUseModifierSubst(closing); ok {
@@ -268,10 +259,7 @@ func (p *MkLexer) varUseModifier(varname
 
        lexer.Reset(mark)
 
-       re := regcomp(regex.Pattern(condStr(closing == '}', `^([^:$}]|\$\$)+`, `^([^:$)]|\$\$)+`)))
-       for p.VarUse() != nil || lexer.SkipRegexp(re) {
-       }
-       modifier := lexer.Since(mark)
+       modifier := p.varUseText(closing)
 
        // ${SOURCES:%.c=%.o} or ${:!uname -a!:[2]}
        if contains(modifier, "=") || (hasPrefix(modifier, "!") && hasSuffix(modifier, "!")) {
@@ -285,6 +273,79 @@ func (p *MkLexer) varUseModifier(varname
        return ""
 }
 
+// varUseModifierSeparator parses the :ts modifier.
+//
+// The API of this method is tricky.
+// It is only extracted from varUseModifier to make the latter smaller.
+func (p *MkLexer) varUseModifierSeparator(
+       mod string, closing byte, lexer *textproc.Lexer, varname string,
+       mark textproc.LexerMark) string {
+
+       // See devel/bmake/files/var.c:/case 't'
+       sep := mod[2:] + p.varUseText(closing)
+       switch {
+       case sep == "":
+               lexer.SkipString(":")
+       case len(sep) == 1:
+               break
+       case matches(sep, `^\\\d+`):
+               break
+       default:
+               if p.line != nil {
+                       p.line.Warnf("Invalid separator %q for :ts modifier of %q.", sep, varname)
+                       p.line.Explain(
+                               "The separator for the :ts modifier must be either a single character",
+                               "or an escape sequence like \\t or \\n or an octal or decimal escape",
+                               "sequence; see the bmake man page for further details.")
+               }
+       }
+       return lexer.Since(mark)
+}
+
+// varUseModifierMatch parses an :M or :N pattern.
+//
+// See devel/bmake/files/var.c:/^ApplyModifiers/, case 'M'.
+func (p *MkLexer) varUseModifierMatch(closing byte) string {
+       lexer := p.lexer
+       mark := lexer.Mark()
+       lexer.Skip(1)
+       opening := byte(condInt(closing == '}', '{', '('))
+       _ = opening
+
+       nest := 1
+       seenBackslash := false
+       for !lexer.EOF() {
+               ch := lexer.PeekByte()
+               if ch == ':' && nest == 1 {
+                       break
+               }
+
+               if ch == '\\' {
+                       seenBackslash = true
+                       lexer.Skip(1)
+                       _ = lexer.SkipByte(':') || lexer.SkipByte(opening) || lexer.SkipByte(closing)
+                       continue
+               }
+
+               if ch == '(' || ch == '{' {
+                       nest++
+               } else if ch == ')' || ch == '}' {
+                       nest--
+                       if nest == 0 {
+                               break
+                       }
+               }
+               lexer.Skip(1)
+       }
+
+       arg := lexer.Since(mark)
+       if seenBackslash {
+               re := regex.Pattern(condStr(closing == '}', `\\([:}])`, `\\([:)])`))
+               arg = replaceAll(arg, re, "$1")
+       }
+       return arg
+}
+
 // varUseModifierSubst parses a :S,from,to, or a :C,from,to, modifier.
 func (p *MkLexer) varUseModifierSubst(closing byte) (ok bool, regex bool, from string, to string, options string) {
        lexer := p.lexer
Index: pkgsrc/pkgtools/pkglint/files/mklexer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.1 pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.2
--- pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.1   Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/mklexer_test.go       Sat Nov 30 20:35:11 2019
@@ -482,6 +482,44 @@ func (s *Suite) Test_MkLexer_Varname(c *
        testRest("VARNAME/rest", "VARNAME", "/rest")
 }
 
+func (s *Suite) Test_MkLexer_varUseText(c *check.C) {
+       t := s.Init(c)
+
+       test := func(text string, expected string, diagnostics ...string) {
+               line := t.NewLine("Makefile", 20, "\t"+text)
+               p := NewMkLexer(text, line)
+
+               actual := p.varUseText('}')
+
+               t.CheckDeepEquals(actual, expected)
+               t.CheckEquals(p.Rest(), text[len(expected):])
+               t.CheckOutput(diagnostics)
+       }
+
+       test("", "")
+       test("asdf", "asdf")
+
+       test("a$$a b", "a$$a b")
+       test("a$$a b", "a$$a b")
+
+       test("a$a b", "a$a b",
+               "WARN: Makefile:20: $a is ambiguous. Use ${a} if you mean "+
+                       "a Make variable or $$a if you mean a shell variable.")
+
+       test("a${INNER} b", "a${INNER} b")
+
+       test("a${${${${${$(NESTED)}}}}}", "a${${${${${$(NESTED)}}}}}",
+               "WARN: Makefile:20: Please use curly braces {} "+
+                       "instead of round parentheses () for NESTED.")
+
+       test("a)b", "a)b") // Since the closing character is '}', not ')'.
+
+       test("a:b", "a")
+       test("a\\ba", "a\\ba")
+       test("a\\:a", "a\\:a")
+       test("a\\\\:a", "a\\\\")
+}
+
 func (s *Suite) Test_MkLexer_VarUseModifiers(c *check.C) {
        t := s.Init(c)
 
@@ -518,6 +556,11 @@ func (s *Suite) Test_MkLexer_VarUseModif
        test("${BUILD_DIRS:[3]:L}", varUse("BUILD_DIRS", "[3]", "L"))
 
        test("${PATH:ts::Q}", varUse("PATH", "ts:", "Q"))
+
+       // The :Q at the end is part of the right-hand side of the = modifier.
+       // It does not quote anything.
+       // See devel/bmake/files/var.c:/^VarGetPattern/.
+       test("${VAR:old=new:Q}", varUse("VAR", "old=new", "Q")) // FIXME
 }
 
 func (s *Suite) Test_MkLexer_varUseModifier__invalid_ts_modifier_with_warning(c *check.C) {
@@ -617,6 +660,131 @@ func (s *Suite) Test_MkLexer_varUseModif
                "WARN: filename.mk:123: Invalid variable modifier \"?yes${INNER}\" for \"${VAR}\".")
 }
 
+func (s *Suite) Test_MkLexer_varUseModifier__eq_suffix_replacement(c *check.C) {
+       t := s.Init(c)
+
+       test := func(input, modifier, rest string) {
+               line := t.NewLine("filename.mk", 123, "")
+               p := NewMkLexer(input, line)
+
+               actual := p.varUseModifier("VARNAME", '}')
+
+               t.CheckDeepEquals(actual, modifier)
+               t.CheckEquals(p.Rest(), rest)
+       }
+
+       test("%.c=%.o", "%.c=%.o", "")
+       test("%\\:c=%.o", "%\\:c=%.o", "") // FIXME: remove the escaping.
+       test("%\\:c=%.o", "%\\:c=%.o", "") // FIXME: remove the escaping.
+
+       // The backslashes are only removed before parentheses,
+       // braces and colons; see devel/bmake/files/var.c:/^VarGetPattern/
+       test(".\\a\\b\\c=.abc", ".\\a\\b\\c=.abc", "")
+
+       // See devel/bmake/files/var.c:/^#define IS_A_MATCH/.
+       // FIXME: The :rest must be part of the replacement.
+       test("%.c=%.o:rest", "%.c=%.o", ":rest")
+       test("\\}\\\\\\$=", "\\}\\\\\\$=", "")
+       // FIXME: test("\\}\\\\\\$=", "}\\$=", "")
+       test("=\\}\\\\\\$\\&", "=\\}\\\\\\$\\&", "")
+       // FIXME: test("=\\}\\\\\\$\\&", "=}\\$&", "")
+}
+
+func (s *Suite) Test_MkLexer_varUseModifierMatch(c *check.C) {
+       t := s.Init(c)
+
+       testClosing := func(input string, closing byte, modifier, rest string, diagnostics ...string) {
+               line := t.NewLine("filename.mk", 123, "")
+               p := NewMkLexer(input, line)
+
+               actual := p.varUseModifier("VARNAME", closing)
+
+               t.CheckDeepEquals(actual, modifier)
+               t.CheckEquals(p.Rest(), rest)
+               t.CheckOutput(diagnostics)
+       }
+
+       test := func(input, modifier, rest string, diagnostics ...string) {
+               testClosing(input, '}', modifier, rest, diagnostics...)
+       }
+       testParen := func(input, modifier, rest string, diagnostics ...string) {
+               testClosing(input, ')', modifier, rest, diagnostics...)
+       }
+
+       // Backslashes are removed only for : and the closing character.
+       test("M\\(\\{\\}\\)\\::rest", "M\\(\\{}\\):", ":rest")
+
+       // But not before other backslashes.
+       // Therefore, the first backslash does not escape the second.
+       // The second backslash doesn't have an effect either,
+       // since the parenthesis is just an ordinary character here.
+       test("M\\\\(:nested):rest", "M\\\\(:nested)", ":rest")
+
+       // If the variable uses parentheses instead of braces,
+       // the opening parenthesis is escaped by the second backslash
+       // and thus doesn't increase the nesting level.
+       // Nevertheless it is not unescaped. This is probably a bug in bmake.
+       testParen("M\\\\(:rest", "M\\\\(", ":rest")
+       testParen("M(:nested):rest", "M(:nested)", ":rest")
+
+       test("Mpattern", "Mpattern", "")
+       test("Mpattern}closed", "Mpattern", "}closed")
+       test("Mpattern:rest", "Mpattern", ":rest")
+
+       test("M{{{}}}}", "M{{{}}}", "}")
+
+       // See devel/bmake/files/var.c:/== '\('/.
+       test("M(}}", "M(}", "}")
+}
+
+// See src/usr.bin/make/unit-tests/varmod-edge.mk 1.4.
+//
+// The difference between this test and the bmake unit test is that in
+// this test the pattern is visible, while in the bmake test it is hidden
+// and can only be made visible by adding a fprintf to Str_Match or by
+// carefully analyzing the result of Str_Match, which removes another level
+// of backslashes.
+func (s *Suite) Test_MkLexer_varUseModifierMatch__varmod_edge(c *check.C) {
+       t := s.Init(c)
+
+       test := func(input, modifier, rest string, diagnostics ...string) {
+               line := t.NewLine("filename.mk", 123, "")
+               p := NewMkLexer(input, line)
+
+               actual := p.varUseModifier("VARNAME", '}')
+
+               t.CheckDeepEquals(actual, modifier)
+               t.CheckEquals(p.Rest(), rest)
+               t.CheckOutput(diagnostics)
+       }
+
+       // M-paren
+       test("M(*)}", "M(*)", "}")
+
+       // M-mixed
+       test("M(*}}", "M(*}", "}")
+
+       // M-nest-mix
+       test("M${:U*)}}", "M${:U*)", "}}")
+
+       // M-nest-brk
+       test("M${:U[[[[[]}}", "M${:U[[[[[]}", "}")
+
+       // M-pat-err
+       // TODO: Warn about the malformed pattern, since bmake doesn't.
+       //  See devel/bmake/files/str.c:/^Str_Match/.
+       test("M${:U[[}}", "M${:U[[}", "}")
+
+       // M-bsbs
+       test("M\\\\(}}", "M\\\\(}", "}")
+
+       // M-bs1-par
+       test("M\\(:M*}}", "M\\(:M*}", "}")
+
+       // M-bs2-par
+       test("M\\\\(:M*}}", "M\\\\(:M*}", "}")
+}
+
 func (s *Suite) Test_MkLexer_varUseModifierSubst(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/mkline.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.65 pkgsrc/pkgtools/pkglint/files/mkline.go:1.66
--- pkgsrc/pkgtools/pkglint/files/mkline.go:1.65        Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline.go     Sat Nov 30 20:35:11 2019
@@ -414,15 +414,16 @@ var notSpace = textproc.Space.Inverse()
 // See UnquoteShell.
 func (mkline *MkLine) ValueFields(value string) []string {
        var fields []string
-       var field strings.Builder
 
        lexer := NewMkTokensLexer(mkline.Tokenize(value, false))
        lexer.SkipHspace()
 
+       field := NewLazyStringBuilder(lexer.Rest())
+
        emit := func() {
                if field.Len() > 0 {
                        fields = append(fields, field.String())
-                       field.Reset()
+                       field.Reset(lexer.Rest())
                }
        }
 
@@ -542,7 +543,7 @@ func (mkline *MkLine) Fields() []string 
 }
 
 func (*MkLine) WithoutMakeVariables(value string) string {
-       var valueNovar strings.Builder
+       valueNovar := NewLazyStringBuilder(value)
        tokens, _ := NewMkLexer(value, nil).MkTokens()
        for _, token := range tokens {
                if token.Varuse == nil {
@@ -828,7 +829,7 @@ func (mkline *MkLine) ForEachUsed(action
 //
 // See ValueFields.
 func (mkline *MkLine) UnquoteShell(str string, warn bool) string {
-       var sb strings.Builder
+       sb := NewLazyStringBuilder(str)
        lexer := NewMkTokensLexer(mkline.Tokenize(str, false))
 
        plain := func() {

Index: pkgsrc/pkgtools/pkglint/files/mklineparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.5 pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.6
--- pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.5   Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/mklineparser.go       Sat Nov 30 20:35:11 2019
@@ -304,7 +304,7 @@ func (MkLineParser) split(line *Line, te
        lexer := parser.lexer
 
        parseOther := func() string {
-               var sb strings.Builder
+               sb := NewLazyStringBuilder(lexer.Rest())
 
                for !lexer.EOF() {
                        if lexer.SkipString("$$") {

Index: pkgsrc/pkgtools/pkglint/files/mktokenslexer.go
diff -u pkgsrc/pkgtools/pkglint/files/mktokenslexer.go:1.2 pkgsrc/pkgtools/pkglint/files/mktokenslexer.go:1.3
--- pkgsrc/pkgtools/pkglint/files/mktokenslexer.go:1.2  Tue Oct  1 21:37:59 2019
+++ pkgsrc/pkgtools/pkglint/files/mktokenslexer.go      Sat Nov 30 20:35:11 2019
@@ -38,7 +38,7 @@ func (m *MkTokensLexer) EOF() bool { ret
 
 // Rest returns the string concatenation of the tokens that have not yet been consumed.
 func (m *MkTokensLexer) Rest() string {
-       var sb strings.Builder
+       sb := NewLazyStringBuilder(m.Lexer.Rest())
        sb.WriteString(m.Lexer.Rest())
        for _, token := range m.tokens {
                sb.WriteString(token.Text)
Index: pkgsrc/pkgtools/pkglint/files/path.go
diff -u pkgsrc/pkgtools/pkglint/files/path.go:1.2 pkgsrc/pkgtools/pkglint/files/path.go:1.3
--- pkgsrc/pkgtools/pkglint/files/path.go:1.2   Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/path.go       Sat Nov 30 20:35:11 2019
@@ -29,6 +29,10 @@ func (p Path) String() string { return s
 
 func (p Path) GoString() string { return sprintf("%q", string(p)) }
 
+// IsEmpty returns true if the path is completely empty,
+// which is usually a sign of an uninitialized variable.
+func (p Path) IsEmpty() bool { return p == "" }
+
 func (p Path) Dir() Path { return Path(path.Dir(string(p))) }
 
 func (p Path) Base() string { return path.Base(string(p)) }
@@ -96,7 +100,6 @@ func (p Path) HasPrefixPath(prefix Path)
        return true
 }
 
-// TODO: Check each call whether ContainsPath is more appropriate; add tests
 func (p Path) ContainsText(contained string) bool {
        return contains(string(p), contained)
 }
@@ -126,7 +129,7 @@ func (p Path) HasSuffixText(suffix strin
        return hasSuffix(string(p), suffix)
 }
 
-// HasSuffixPath returns whether the path ends with the given prefix.
+// HasSuffixPath returns whether the path ends with the given suffix.
 // The basic unit of comparison is a path component, not a character.
 func (p Path) HasSuffixPath(suffix Path) bool {
        return hasSuffix(string(p), string(suffix)) &&
Index: pkgsrc/pkgtools/pkglint/files/path_test.go
diff -u pkgsrc/pkgtools/pkglint/files/path_test.go:1.2 pkgsrc/pkgtools/pkglint/files/path_test.go:1.3
--- pkgsrc/pkgtools/pkglint/files/path_test.go:1.2      Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/path_test.go  Sat Nov 30 20:35:11 2019
@@ -48,6 +48,18 @@ func (s *Suite) Test_Path_GoString(c *ch
        test("c\\d", "\"c\\\\d\"")
 }
 
+func (s *Suite) Test_Path_IsEmpty(c *check.C) {
+       t := s.Init(c)
+
+       test := func(p Path, isEmpty bool) {
+               t.CheckEquals(p.IsEmpty(), isEmpty)
+       }
+
+       test("", true)
+       test(".", false)
+       test("/", false)
+}
+
 func (s *Suite) Test_Path_Dir(c *check.C) {
        t := s.Init(c)
 
@@ -247,7 +259,7 @@ func (s *Suite) Test_Path_ContainsPath(c
 
        test("", "", true)          // It doesn't make sense to search for empty paths.
        test(".", "", false)        // It doesn't make sense to search for empty paths.
-       test("filename", ".", true) // Every path contains "." implicitly at the beginning
+       test("filename", ".", true) // Every relative path contains "." implicitly at the beginning
        test("a.b", ".", true)
        test("..", ".", true)
        test("filename", "", false)
@@ -263,6 +275,23 @@ func (s *Suite) Test_Path_ContainsPath(c
        test("a/bb/c", "b/c", false)
        test("mk/fetch/fetch.mk", "mk", true)
        test("category/package/../../wip/mk/../..", "mk", true)
+
+       test("a", "a", true)
+       test("a", "b", false)
+       test("a", "A", false)
+       test("a/b/c", "a", true)
+       test("a/b/c", "b", true)
+       test("a/b/c", "c", true)
+       test("a/b/c", "a/b", true)
+       test("a/b/c", "b/c", true)
+       test("a/b/c", "a/b/c", true)
+       test("aa/bb/cc", "a/b", false)
+       test("aa/bb/cc", "a/bb", false)
+       test("aa/bb/cc", "aa/b", false)
+       test("aa/bb/cc", "aa/bb", true)
+       test("aa/bb/cc", "a", false)
+       test("aa/bb/cc", "b", false)
+       test("aa/bb/cc", "c", false)
 }
 
 func (s *Suite) Test_Path_ContainsPathCanonical(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.70 pkgsrc/pkgtools/pkglint/files/package.go:1.71
--- pkgsrc/pkgtools/pkglint/files/package.go:1.70       Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/package.go    Sat Nov 30 20:35:11 2019
@@ -294,9 +294,9 @@ func (pkg *Package) loadIncluded(mkline 
                return nil, true
        }
 
-       dirname, _ := includingFile.Split()
+       dirname, _ := includingFile.Split() // TODO: .Dir?
        dirname = cleanpath(dirname)
-       fullIncluded := joinPath(dirname, includedFile)
+       fullIncluded := dirname.JoinNoClean(includedFile)
        relIncludedFile := G.Pkgsrc.Relpath(pkg.dir, fullIncluded)
 
        if !pkg.shouldDiveInto(includingFile, includedFile) {
@@ -334,7 +334,7 @@ func (pkg *Package) loadIncluded(mkline 
 
        dirname = pkgBasedir
 
-       fullIncludedFallback := joinPath(dirname, includedFile)
+       fullIncludedFallback := dirname.JoinNoClean(includedFile)
        includedMklines = LoadMk(fullIncludedFallback, 0)
        if includedMklines == nil {
                return nil, false
@@ -1080,7 +1080,7 @@ func (pkg *Package) pkgnameFromDistname(
 
        // TODO: Make this resolving of variable references available to all other variables as well.
 
-       var result strings.Builder
+       result := NewLazyStringBuilder(pkgname)
        for _, token := range tokens {
                if token.Varuse != nil {
                        if token.Varuse.varname != "DISTNAME" {

Index: pkgsrc/pkgtools/pkglint/files/pkglint.0
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.0:1.40 pkgsrc/pkgtools/pkglint/files/pkglint.0:1.41
--- pkgsrc/pkgtools/pkglint/files/pkglint.0:1.40        Sun Jun 30 21:03:16 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.0     Sat Nov 30 20:35:11 2019
@@ -81,9 +81,6 @@ DDEESSCCRRIIPPTTIIOONN
 
      [[nnoo‐‐]]ssppaaccee          Emit notes for inconsistent use of whitespace.
 
-     [[nnoo‐‐]]ssttyyllee          Warn for stylistic issues that don’t affect the build
-                         process.
-
    OOtthheerr aarrgguummeennttss
            _d_i_r _._._.             The pkgsrc directories to be checked.  If omit‐
                                ted, the current directory is checked.
@@ -114,4 +111,4 @@ BBUUGGSS
      If you don’t understand the messages, feel free to ask the author or on
      the ⟨pkgsrc‐users%pkgsrc.org@localhost⟩ mailing list.
 
-NetBSD 8.0                       June 17, 2019                      NetBSD 8.0
+NetBSD 8.0                     November 30, 2019                    NetBSD 8.0

Index: pkgsrc/pkgtools/pkglint/files/pkglint.1
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.1:1.57 pkgsrc/pkgtools/pkglint/files/pkglint.1:1.58
--- pkgsrc/pkgtools/pkglint/files/pkglint.1:1.57        Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.1     Sat Nov 30 20:35:11 2019
@@ -1,4 +1,4 @@
-.\"    $NetBSD: pkglint.1,v 1.57 2019/11/27 22:10:07 rillig Exp $
+.\"    $NetBSD: pkglint.1,v 1.58 2019/11/30 20:35:11 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>.
@@ -8,7 +8,7 @@
 .\" Thomas Klausner <wiz%NetBSD.org@localhost>, 2012.
 .\" Roland Illig <rillig%NetBSD.org@localhost>, 2015-2019.
 .\"
-.Dd November 27, 2019
+.Dd November 30, 2019
 .Dt PKGLINT 1
 .Os
 .Sh NAME
@@ -107,8 +107,6 @@ Warn for possibly invalid quoting of mak
 and shell variables themselves.
 .It Cm [no-]space
 Emit notes for inconsistent use of whitespace.
-.It Cm [no-]style
-Warn for stylistic issues that don't affect the build process.
 .El
 .\" =======================================================================
 .Ss Other arguments

Index: pkgsrc/pkgtools/pkglint/files/pkglint.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.66 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.67
--- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.66       Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.go    Sat Nov 30 20:35:11 2019
@@ -81,8 +81,7 @@ type CmdOpts struct {
        WarnExtra,
        WarnPerm,
        WarnQuoting,
-       WarnSpace,
-       WarnStyle bool
+       WarnSpace bool
 
        Profiling,
        ShowHelp,
@@ -221,7 +220,7 @@ func (pkglint *Pkglint) prepareMainLoop(
                NewLineWhole(firstDir).Fatalf("Must be inside a pkgsrc tree.")
        }
 
-       pkglint.Pkgsrc = NewPkgsrc(joinPath(firstDir, relTopdir))
+       pkglint.Pkgsrc = NewPkgsrc(firstDir.JoinNoClean(relTopdir))
        pkglint.Wip = pkglint.Pkgsrc.IsWip(firstDir) // See Pkglint.checkMode.
        pkglint.Pkgsrc.LoadInfrastructure()
 
@@ -259,7 +258,6 @@ func (pkglint *Pkglint) ParseCommandLine
        warn.AddFlagVar("perm", &gopts.WarnPerm, false, "warn about unforeseen variable definition and use")
        warn.AddFlagVar("quoting", &gopts.WarnQuoting, false, "warn about quoting issues")
        warn.AddFlagVar("space", &gopts.WarnSpace, false, "warn about inconsistent use of whitespace")
-       warn.AddFlagVar("style", &gopts.WarnStyle, false, "warn about stylistic issues")
 
        remainingArgs, err := opts.Parse(args)
        if err != nil {
@@ -377,7 +375,7 @@ func (pkglint *Pkglint) checkdirPackage(
 // Returns the pkgsrc top-level directory, relative to the given directory.
 func findPkgsrcTopdir(dirname Path) Path {
        for _, dir := range [...]Path{".", "..", "../..", "../../.."} {
-               if joinPath(dirname, dir, "mk/bsd.pkg.mk").IsFile() {
+               if dirname.JoinNoClean(dir).JoinNoClean("mk/bsd.pkg.mk").IsFile() {
                        return dir
                }
        }
@@ -618,7 +616,7 @@ func (pkglint *Pkglint) checkReg(filenam
                NewLineWhole(filename).Warnf("Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.")
 
        case (hasPrefix(basename, "Makefile") || hasSuffix(basename, ".mk")) &&
-               !pathContainsDir(filename, "files"): // FIXME: G.Pkgsrc.Rel(filename) instead of filename
+               !filename.Dir().ContainsPath("files"): // FIXME: G.Pkgsrc.Rel(filename) instead of filename
                CheckFileMk(filename)
 
        case hasPrefix(basename, "PLIST"):
@@ -786,6 +784,13 @@ func (pkglint *Pkglint) loadCvsEntries(f
        return entries
 }
 
+func (pkglint *Pkglint) Abs(filename Path) Path {
+       if !filename.IsAbs() {
+               return pkglint.cwd.JoinNoClean(filename).Clean()
+       }
+       return filename.Clean()
+}
+
 type InterPackage struct {
        hashes       map[string]*Hash    // Maps "alg:filename" => hash (inter-package check).
        usedLicenses map[string]struct{} // Maps "license name" => true (inter-package check).

Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.51 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.52
--- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.51  Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go       Sat Nov 30 20:35:11 2019
@@ -78,7 +78,6 @@ func (s *Suite) Test_Pkglint_Main__help(
                "    perm      warn about unforeseen variable definition and use (disabled)",
                "    quoting   warn about quoting issues (disabled)",
                "    space     warn about inconsistent use of whitespace (disabled)",
-               "    style     warn about stylistic issues (disabled)",
                "",
                "  (Prefix a flag with \"no-\" to disable it.)")
 }

Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.43 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.44
--- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.43        Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go     Sat Nov 30 20:35:11 2019
@@ -158,7 +158,7 @@ func (src *Pkgsrc) loadDocChanges() {
 
        src.LastChange = make(map[Path]*Change)
        for _, filename := range filenames {
-               changes := src.loadDocChangesFromFile(joinPath(docDir, filename))
+               changes := src.loadDocChangesFromFile(docDir.JoinNoClean(filename))
                for _, change := range changes {
                        src.LastChange[change.Pkgpath] = change
                        if change.Action == Renamed || change.Action == Moved {
@@ -971,7 +971,7 @@ func (src *Pkgsrc) checkToplevelUnusedLi
        for _, licenseFile := range src.ReadDir("licenses") {
                licenseName := licenseFile.Name()
                if !G.InterPackage.IsLicenseUsed(licenseName) {
-                       licensePath := joinPath(licensesDir, NewPath(licenseName))
+                       licensePath := licensesDir.JoinNoClean(NewPath(licenseName))
                        NewLineWhole(licensePath).Warnf("This license seems to be unused.")
                }
        }
@@ -1081,9 +1081,9 @@ func (src *Pkgsrc) Relpath(from, to Path
                return cto.Clean()
        }
 
-       absFrom := abspath(cfrom)
-       absTopdir := abspath(src.topdir)
-       absTo := abspath(cto)
+       absFrom := G.Abs(cfrom)
+       absTopdir := G.Abs(src.topdir)
+       absTo := G.Abs(cto)
 
        up := absFrom.Rel(absTopdir)
        down := absTopdir.Rel(absTo)

Index: pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.20 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.21
--- pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.20      Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go   Sat Nov 30 20:35:11 2019
@@ -511,6 +511,18 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                subshBackt(text("nested-subshell")),
                subsh(operator("`")),
                operator(")"))
+
+       // Subshell with unbalanced parentheses, taken from src/build.sh,
+       // around line 160. Many shells (and pkglint) fail this test,
+       // therefore just don't write code like this.
+       test("var=$$(case x in x) still-subshell;; esac);",
+               text("var="), subsh(subshell),
+               subsh(text("case")), subsh(space), subsh(text("x")), subsh(space),
+               subsh(text("in")), subsh(space), subsh(text("x")),
+               // XXX: The parenthesis is for the case pattern, not the end of the subshell.
+               operator(")"), space,
+               text("still-subshell"), operator(";;"), space,
+               text("esac"), operator(")"), operator(";"))
 }
 
 func (s *Suite) Test_ShTokenizer_ShAtom__quoting(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/toplevel.go
diff -u pkgsrc/pkgtools/pkglint/files/toplevel.go:1.24 pkgsrc/pkgtools/pkglint/files/toplevel.go:1.25
--- pkgsrc/pkgtools/pkglint/files/toplevel.go:1.24      Sat Nov 23 23:35:56 2019
+++ pkgsrc/pkgtools/pkglint/files/toplevel.go   Sat Nov 30 20:35:11 2019
@@ -62,6 +62,6 @@ func (ctx *Toplevel) checkSubdir(mkline 
        ctx.previousSubdir = subdir
 
        if !mkline.IsCommentedVarassign() {
-               ctx.subdirs = append(ctx.subdirs, joinPath(ctx.dir, subdir))
+               ctx.subdirs = append(ctx.subdirs, ctx.dir.JoinNoClean(subdir))
        }
 }

Index: pkgsrc/pkgtools/pkglint/files/util.go
diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.60 pkgsrc/pkgtools/pkglint/files/util.go:1.61
--- pkgsrc/pkgtools/pkglint/files/util.go:1.60  Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/util.go       Sat Nov 30 20:35:11 2019
@@ -457,29 +457,9 @@ func mkopSubst(s string, left bool, from
        })
 }
 
-// FIXME: Replace with Path.JoinNoClean
-func joinPath(a, b Path, others ...Path) Path {
-       if len(others) == 0 {
-               return a + "/" + b
-       }
-       parts := []string{a.String(), b.String()}
-       for _, part := range others {
-               parts = append(parts, part.String())
-       }
-       return NewPath(strings.Join(parts, "/"))
-}
-
-func abspath(filename Path) Path {
-       abs := filename
-       if !filename.IsAbs() {
-               abs = joinPath(G.cwd, abs)
-       }
-       return abs.Clean()
-}
-
 // Differs from path.Clean in that only "../../" is replaced, not "../".
 // Also, the initial directory is always kept.
-// This is to provide the package path as context in recursive invocations of pkglint.
+// This is to provide the package path as context in deeply nested .include chains.
 func cleanpath(filename Path) Path {
        parts := make([]string, 0, 5)
        lex := textproc.NewLexer(filename.String())
@@ -515,34 +495,6 @@ func cleanpath(filename Path) Path {
        return NewPath(strings.Join(parts, "/"))
 }
 
-func pathContains(haystack, needle string) bool {
-       n0 := needle[0]
-       for i := 0; i < 1+len(haystack)-len(needle); i++ {
-               if haystack[i] == n0 && hasPrefix(haystack[i:], needle) {
-                       if i == 0 || haystack[i-1] == '/' {
-                               if i+len(needle) == len(haystack) || haystack[i+len(needle)] == '/' {
-                                       return true
-                               }
-                       }
-               }
-       }
-       return false
-}
-
-func pathContainsDir(haystack, needle Path) bool {
-       n0 := needle[0]
-       for i := 0; i < 1+len(haystack)-len(needle); i++ {
-               if haystack[i] == n0 && hasPrefix(haystack.String()[i:], needle.String()) {
-                       if i == 0 || haystack[i-1] == '/' {
-                               if i+len(needle) < len(haystack) && haystack[i+len(needle)] == '/' {
-                                       return true
-                               }
-                       }
-               }
-       }
-       return false
-}
-
 func containsVarRef(s string) bool {
        return contains(s, "${") || contains(s, "$(")
 }
@@ -1128,23 +1080,13 @@ func wrap(max int, lines ...string) []st
 // at the risk of interpreting malicious data from the files checked by pkglint.
 // This escaping is not reversible, and it doesn't need to.
 func escapePrintable(s string) string {
-       i := 0
-       for i < len(s) && textproc.XPrint.Contains(s[i]) {
-               i++
-       }
-       if i == len(s) {
-               return s
-       }
-
-       var escaped strings.Builder
-       escaped.WriteString(s[:i])
-       rest := s[i:]
-       for j, r := range rest {
+       escaped := NewLazyStringBuilder(s)
+       for i, r := range s {
                switch {
-               case rune(byte(r)) == r && textproc.XPrint.Contains(byte(rest[j])):
+               case rune(byte(r)) == r && textproc.XPrint.Contains(s[i]):
                        escaped.WriteByte(byte(r))
-               case r == 0xFFFD && !hasPrefix(rest[j:], "\uFFFD"):
-                       _, _ = fmt.Fprintf(&escaped, "<0x%02X>", rest[j])
+               case r == 0xFFFD && !hasPrefix(s[i:], "\uFFFD"):
+                       _, _ = fmt.Fprintf(&escaped, "<0x%02X>", s[i])
                default:
                        _, _ = fmt.Fprintf(&escaped, "<%U>", r)
                }
@@ -1348,3 +1290,73 @@ func (q *PathQueue) Pop() Path {
        q.entries = q.entries[1:]
        return front
 }
+
+// LazyStringBuilder builds a string that is most probably equal to an
+// already existing string. In that case, it avoids any memory allocations.
+type LazyStringBuilder struct {
+       Expected string
+       len      int
+       usingBuf bool
+       buf      []byte
+}
+
+func (b *LazyStringBuilder) Write(p []byte) (n int, err error) {
+       for _, c := range p {
+               b.WriteByte(c)
+       }
+       return len(p), nil
+}
+
+func NewLazyStringBuilder(expected string) LazyStringBuilder {
+       return LazyStringBuilder{Expected: expected}
+}
+
+func (b *LazyStringBuilder) Len() int {
+       return b.len
+}
+
+func (b *LazyStringBuilder) WriteString(s string) {
+       if !b.usingBuf && b.len+len(s) <= len(b.Expected) && hasPrefix(b.Expected[b.len:], s) {
+               b.len += len(s)
+               return
+       }
+       for _, c := range []byte(s) {
+               b.WriteByte(c)
+       }
+}
+
+func (b *LazyStringBuilder) WriteByte(c byte) {
+       if !b.usingBuf && b.len < len(b.Expected) && b.Expected[b.len] == c {
+               b.len++
+               return
+       }
+       b.writeToBuf(c)
+}
+
+func (b *LazyStringBuilder) writeToBuf(c byte) {
+       if !b.usingBuf {
+               if cap(b.buf) >= b.len {
+                       b.buf = b.buf[:b.len]
+                       assert(copy(b.buf, b.Expected) == b.len)
+               } else {
+                       b.buf = []byte(b.Expected)[:b.len]
+               }
+               b.usingBuf = true
+       }
+
+       b.buf = append(b.buf, c)
+       b.len++
+}
+
+func (b *LazyStringBuilder) Reset(expected string) {
+       b.Expected = expected
+       b.usingBuf = false
+       b.len = 0
+}
+
+func (b *LazyStringBuilder) String() string {
+       if b.usingBuf {
+               return string(b.buf[:b.len])
+       }
+       return b.Expected[:b.len]
+}

Index: pkgsrc/pkgtools/pkglint/files/licenses/licenses.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.6 pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.7
--- pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.6      Sun Dec  2 01:57:48 2018
+++ pkgsrc/pkgtools/pkglint/files/licenses/licenses.go  Sat Nov 30 20:35:11 2019
@@ -67,7 +67,7 @@ type licenseLexer struct {
        error  string
 }
 
-var licenseNameChars = textproc.NewByteSet("A-Za-z0-9---.")
+var licenseNameChars = textproc.NewByteSet("A-Za-z0-9---._")
 
 func (lexer *licenseLexer) Lex(llval *liyySymType) int {
        lex := lexer.lexer

Index: pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.8 pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.9
--- pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.8 Wed Nov 27 22:10:07 2019
+++ pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go     Sat Nov 30 20:35:11 2019
@@ -4,6 +4,7 @@ import (
        "encoding/json"
        "gopkg.in/check.v1"
        "netbsd.org/pkglint/intqa"
+       "netbsd.org/pkglint/textproc"
        "strings"
        "testing"
 )
@@ -22,6 +23,7 @@ func (s *Suite) Test_Parse(c *check.C) {
        testDeep("gnu-gpl-v2", NewName("gnu-gpl-v2"))
 
        test("gnu-gpl-v2", "{Name:gnu-gpl-v2}")
+       test("citrix_ica-license", "{Name:citrix_ica-license}")
 
        test("a AND b", "{And:true,Children:[{Name:a},{Name:b}]}")
        test("a OR b", "{Or:true,Children:[{Name:a},{Name:b}]}")
@@ -108,6 +110,39 @@ func (s *Suite) Test_Condition_Walk(c *c
        c.Check(out, check.DeepEquals, []string{"a", "b", "OR", "()", "c", "d", "AND", "()", "AND"})
 }
 
+func (s *Suite) Test_licenseLexer_Lex(c *check.C) {
+
+       test := func(text string, tokenType int) {
+               lexer := &licenseLexer{lexer: textproc.NewLexer(text)}
+               var token liyySymType
+               lex := lexer.Lex(&token)
+               c.Check(lex, check.Equals, tokenType)
+       }
+       testName := func(text string, name string) {
+               lexer := &licenseLexer{lexer: textproc.NewLexer(text)}
+               var token liyySymType
+               lex := lexer.Lex(&token)
+               c.Check(lex, check.Equals, ltNAME)
+               c.Check(token.Node.Name, check.Equals, name)
+       }
+
+       test("", 0)
+       test("(", ltOPEN)
+       test(")", ltCLOSE)
+       test("AND", ltAND)
+       test("OR", ltOR)
+       test("?", -1)
+       test("license-name", ltNAME)
+       test("license_name", ltNAME)
+
+       test("AND rest", ltAND)
+       test("AND-rest", ltNAME)
+
+       testName("license-name", "license-name")
+       testName("license_name", "license_name")
+       testName("AND-rest", "AND-rest")
+}
+
 func NewName(name string) *Condition {
        return &Condition{Name: name}
 }

Index: pkgsrc/pkgtools/pkglint/files/textproc/lexer.go
diff -u pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.7 pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.8
--- pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.7 Sat Nov 23 23:35:56 2019
+++ pkgsrc/pkgtools/pkglint/files/textproc/lexer.go     Sat Nov 30 20:35:11 2019
@@ -59,7 +59,8 @@ func (l *Lexer) TestByteSet(set *ByteSet
        return 0 < len(rest) && set.Contains(rest[0])
 }
 
-// Skip skips the next n bytes.
+// Skip skips the next n bytes, or panics if the rest is shorter than n bytes.
+// Returns false only if n == 0.
 func (l *Lexer) Skip(n int) bool {
        l.rest = l.rest[n:]
        return n > 0



Home | Main Index | Thread Index | Old Index