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:           Wed Jul  1 13:17:41 UTC 2020

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile
        pkgsrc/pkgtools/pkglint/files: autofix.go check_test.go line.go
            mkcondchecker.go mklexer.go mklexer_test.go mkline.go
            mkparser_test.go mkshparser.go mktokenslexer.go
            mktokenslexer_test.go mktypes.go mktypes_test.go mkvarusechecker.go
            mkvarusechecker_test.go package.go package_test.go pkglint_test.go
            plist.go plist_test.go shell.go shell.y shell_test.go
            shtokenizer_test.go vartypecheck.go vartypecheck_test.go

Log Message:
pkgtools/pkglint: update to 20.2.1

Changes since 20.2.0:

Don't warn about a possibly redundant PKGNAME=${DISTNAME} assignment if
PKGNAME is defined somewhere else in the package Makefile.

Warn if NO_CONFIGURE=yes and REPLACE_* are combined.

Suggest to replace ${VAR:@l@-l${l}@} with the simpler ${VAR:S,^,-l,},
as well as ${VAR:@l@${l}suffix@} with the simpler ${VAR:=suffix}.

Allow lua in CATEGORIES.


To generate a diff of this commit:
cvs rdiff -u -r1.660 -r1.661 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.39 -r1.40 pkgsrc/pkgtools/pkglint/files/autofix.go
cvs rdiff -u -r1.74 -r1.75 pkgsrc/pkgtools/pkglint/files/check_test.go
cvs rdiff -u -r1.47 -r1.48 pkgsrc/pkgtools/pkglint/files/line.go
cvs rdiff -u -r1.9 -r1.10 pkgsrc/pkgtools/pkglint/files/mkcondchecker.go
cvs rdiff -u -r1.8 -r1.9 pkgsrc/pkgtools/pkglint/files/mklexer.go
cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/mklexer_test.go \
    pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go
cvs rdiff -u -r1.81 -r1.82 pkgsrc/pkgtools/pkglint/files/mkline.go
cvs rdiff -u -r1.40 -r1.41 pkgsrc/pkgtools/pkglint/files/mkparser_test.go
cvs rdiff -u -r1.19 -r1.20 pkgsrc/pkgtools/pkglint/files/mkshparser.go
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/mktokenslexer.go
cvs rdiff -u -r1.25 -r1.26 pkgsrc/pkgtools/pkglint/files/mktypes.go \
    pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
cvs rdiff -u -r1.23 -r1.24 pkgsrc/pkgtools/pkglint/files/mktypes_test.go
cvs rdiff -u -r1.12 -r1.13 pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go
cvs rdiff -u -r1.13 -r1.14 \
    pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go
cvs rdiff -u -r1.94 -r1.95 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.79 -r1.80 pkgsrc/pkgtools/pkglint/files/package_test.go
cvs rdiff -u -r1.66 -r1.67 pkgsrc/pkgtools/pkglint/files/pkglint_test.go
cvs rdiff -u -r1.59 -r1.60 pkgsrc/pkgtools/pkglint/files/plist.go
cvs rdiff -u -r1.50 -r1.51 pkgsrc/pkgtools/pkglint/files/plist_test.go
cvs rdiff -u -r1.63 -r1.64 pkgsrc/pkgtools/pkglint/files/shell.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/shell.y
cvs rdiff -u -r1.69 -r1.70 pkgsrc/pkgtools/pkglint/files/shell_test.go
cvs rdiff -u -r1.92 -r1.93 pkgsrc/pkgtools/pkglint/files/vartypecheck.go
cvs rdiff -u -r1.84 -r1.85 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go

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

Modified files:

Index: pkgsrc/pkgtools/pkglint/Makefile
diff -u pkgsrc/pkgtools/pkglint/Makefile:1.660 pkgsrc/pkgtools/pkglint/Makefile:1.661
--- pkgsrc/pkgtools/pkglint/Makefile:1.660      Sun Jun 28 10:19:11 2020
+++ pkgsrc/pkgtools/pkglint/Makefile    Wed Jul  1 13:17:41 2020
@@ -1,6 +1,6 @@
-# $NetBSD: Makefile,v 1.660 2020/06/28 10:19:11 rillig Exp $
+# $NetBSD: Makefile,v 1.661 2020/07/01 13:17:41 rillig Exp $
 
-PKGNAME=       pkglint-20.2.0
+PKGNAME=       pkglint-20.2.1
 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.39 pkgsrc/pkgtools/pkglint/files/autofix.go:1.40
--- pkgsrc/pkgtools/pkglint/files/autofix.go:1.39       Mon Feb 17 20:22:21 2020
+++ pkgsrc/pkgtools/pkglint/files/autofix.go    Wed Jul  1 13:17:41 2020
@@ -96,6 +96,7 @@ func (fix *Autofix) Explain(explanation 
 
 // Replace replaces "from" with "to", a single time.
 // If the text is not found exactly once, nothing is replaced at all.
+// The diagnostic is given nevertheless, to allow humans to fix it.
 func (fix *Autofix) Replace(from string, to string) {
        fix.ReplaceAfter("", from, to)
 }

Index: pkgsrc/pkgtools/pkglint/files/check_test.go
diff -u pkgsrc/pkgtools/pkglint/files/check_test.go:1.74 pkgsrc/pkgtools/pkglint/files/check_test.go:1.75
--- pkgsrc/pkgtools/pkglint/files/check_test.go:1.74    Sat Jun 20 07:00:44 2020
+++ pkgsrc/pkgtools/pkglint/files/check_test.go Wed Jul  1 13:17:41 2020
@@ -119,8 +119,6 @@ func Test__qa(t *testing.T) {
        ck.Configure("mkshparser.go", "*", "*", -intqa.EMissingTest)     // TODO
        ck.Configure("mkshtypes.go", "*", "*", -intqa.EMissingTest)      // TODO
        ck.Configure("mkshwalker.go", "*", "*", -intqa.EMissingTest)     // TODO
-       ck.Configure("mktokenslexer.go", "*", "*", -intqa.EMissingTest)  // TODO
-       ck.Configure("mktypes.go", "*", "*", -intqa.EMissingTest)        // TODO
        ck.Configure("options.go", "*", "*", -intqa.EMissingTest)        // TODO
        ck.Configure("package.go", "*", "*", -intqa.EMissingTest)        // TODO
        ck.Configure("paragraph.go", "*", "*", -intqa.EMissingTest)      // TODO

Index: pkgsrc/pkgtools/pkglint/files/line.go
diff -u pkgsrc/pkgtools/pkglint/files/line.go:1.47 pkgsrc/pkgtools/pkglint/files/line.go:1.48
--- pkgsrc/pkgtools/pkglint/files/line.go:1.47  Mon Jun  1 20:49:54 2020
+++ pkgsrc/pkgtools/pkglint/files/line.go       Wed Jul  1 13:17:41 2020
@@ -53,8 +53,6 @@ func (loc *Location) File(rel RelPath) C
 
 // Line represents a line of text from a file.
 type Line struct {
-       // TODO: Consider storing pointers to the Filename and Basename instead of strings to save memory.
-       //  But first find out where and why pkglint needs so much memory (200 MB for a full recursive run over pkgsrc + wip).
        Location Location
        Basename RelPath // the last component of the Filename
 
@@ -66,8 +64,6 @@ type Line struct {
        raw  []*RawLine // contains the original text including trailing newline
        fix  *Autofix   // any changes that pkglint would like to apply to the line
        once Once
-
-       // XXX: Filename and Basename could be replaced with a pointer to a Lines object.
 }
 
 func NewLine(filename CurrPath, lineno int, text string, rawLine *RawLine) *Line {

Index: pkgsrc/pkgtools/pkglint/files/mkcondchecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mkcondchecker.go:1.9 pkgsrc/pkgtools/pkglint/files/mkcondchecker.go:1.10
--- pkgsrc/pkgtools/pkglint/files/mkcondchecker.go:1.9  Sun Jun 14 11:35:54 2020
+++ pkgsrc/pkgtools/pkglint/files/mkcondchecker.go      Wed Jul  1 13:17:41 2020
@@ -141,7 +141,7 @@ func (ck *MkCondChecker) checkEmptyType(
                        continue
                }
 
-               switch modifier.Text {
+               switch modifier.String() {
                default:
                        return
                case "O", "u":
@@ -256,7 +256,7 @@ func (ck *MkCondChecker) simplify(varuse
        fix := ck.MkLine.Autofix()
        fix.Notef("%s can be compared using the simpler \"%s\" "+
                "instead of matching against %q.",
-               varname, to, ":"+modifier.Text)
+               varname, to, ":"+modifier.String()) // TODO: Quoted
        fix.Explain(
                "This variable has a single value, not a list of values.",
                "Therefore it feels strange to apply list operators like :M and :N onto it.",

Index: pkgsrc/pkgtools/pkglint/files/mklexer.go
diff -u pkgsrc/pkgtools/pkglint/files/mklexer.go:1.8 pkgsrc/pkgtools/pkglint/files/mklexer.go:1.9
--- pkgsrc/pkgtools/pkglint/files/mklexer.go:1.8        Sun Jun  7 15:49:23 2020
+++ pkgsrc/pkgtools/pkglint/files/mklexer.go    Wed Jul  1 13:17:41 2020
@@ -226,7 +226,7 @@ func (p *MkLexer) VarUseModifiers(varnam
        for lexer.SkipByte(':') || mayOmitColon {
                modifier := p.varUseModifier(varname, closing)
                if modifier != "" {
-                       modifiers = append(modifiers, MkVarUseModifier{modifier})
+                       modifiers = append(modifiers, modifier)
                }
                mayOmitColon = modifier != "" && (modifier[0] == 'S' || modifier[0] == 'C')
        }
@@ -235,7 +235,7 @@ func (p *MkLexer) VarUseModifiers(varnam
 
 // varUseModifier parses a single variable modifier such as :Q or :S,from,to,.
 // The actual parsing starts after the leading colon.
-func (p *MkLexer) varUseModifier(varname string, closing byte) string {
+func (p *MkLexer) varUseModifier(varname string, closing byte) MkVarUseModifier {
        lexer := p.lexer
        mark := lexer.Mark()
 
@@ -260,32 +260,32 @@ func (p *MkLexer) varUseModifier(varname
                        "tu", // To uppercase
                        "tw", // Causes the value to be treated as list of words
                        "u":  // Remove adjacent duplicate words (like uniq(1))
-                       return mod
+                       return MkVarUseModifier(mod)
                }
 
-               if hasPrefix(mod, "ts") {
+               if MkVarUseModifier(mod).HasPrefix("ts") {
                        return p.varUseModifierTs(mod, closing, lexer, varname, mark)
                }
 
        case 'D', 'U':
-               return p.varUseText(closing)
+               return MkVarUseModifier(p.varUseText(closing))
 
        case 'M', 'N':
                return p.varUseModifierMatch(closing)
 
        case 'C', 'S':
                if ok, _, _, _, _ := p.varUseModifierSubst(closing); ok {
-                       return lexer.Since(mark)
+                       return MkVarUseModifier(lexer.Since(mark))
                }
 
        case '@':
                if p.varUseModifierAt(lexer, varname) {
-                       return lexer.Since(mark)
+                       return MkVarUseModifier(lexer.Since(mark))
                }
 
        case '[':
                if lexer.SkipRegexp(regcomp(`^\[(?:[-.\d]+|#)\]`)) {
-                       return lexer.Since(mark)
+                       return MkVarUseModifier(lexer.Since(mark))
                }
 
        case '?':
@@ -293,7 +293,7 @@ func (p *MkLexer) varUseModifier(varname
                p.varUseText(closing)
                if lexer.SkipByte(':') {
                        p.varUseText(closing)
-                       return lexer.Since(mark)
+                       return MkVarUseModifier(lexer.Since(mark))
                }
 
        case ':':
@@ -328,7 +328,7 @@ func (p *MkLexer) varUseModifier(varname
                        "but even these have only local consequences.")
 
                p.varUseText(closing)
-               return lexer.Since(mark)
+               return MkVarUseModifier(lexer.Since(mark))
        }
 
        // ${SOURCES:%.c=%.o}
@@ -342,14 +342,14 @@ func (p *MkLexer) varUseModifier(varname
                                "The :from=to modifier consumes all the text until the end of the variable.",
                                "There cannot be any further modifiers after it.")
                }
-               return modifier
+               return MkVarUseModifier(modifier)
        }
 
        // ${:!uname -a!:[2]}
        lexer.Reset(mark)
        modifier = p.varUseText(closing)
        if hasPrefix(modifier, "!") && hasSuffix(modifier, "!") {
-               return modifier
+               return MkVarUseModifier(modifier)
        }
 
        if modifier != "" {
@@ -365,7 +365,7 @@ func (p *MkLexer) varUseModifier(varname
 // It is only extracted from varUseModifier to make the latter smaller.
 func (p *MkLexer) varUseModifierTs(
        mod string, closing byte, lexer *textproc.Lexer, varname string,
-       mark textproc.LexerMark) string {
+       mark textproc.LexerMark) MkVarUseModifier {
 
        // See devel/bmake/files/var.c:/case 't'
        sep := mod[2:] + p.varUseText(closing)
@@ -383,13 +383,13 @@ func (p *MkLexer) varUseModifierTs(
                        "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 MkVarUseModifier(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 {
+func (p *MkLexer) varUseModifierMatch(closing byte) MkVarUseModifier {
        lexer := p.lexer
        mark := lexer.Mark()
        lexer.Skip(1)
@@ -427,7 +427,7 @@ func (p *MkLexer) varUseModifierMatch(cl
                re := regex.Pattern(condStr(closing == '}', `\\([:}])`, `\\([:)])`))
                arg = replaceAll(arg, re, "$1")
        }
-       return arg
+       return MkVarUseModifier(arg)
 }
 
 // varUseModifierSubst parses a :S,from,to, or a :C,from,to, modifier.

Index: pkgsrc/pkgtools/pkglint/files/mklexer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.5 pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.6
--- pkgsrc/pkgtools/pkglint/files/mklexer_test.go:1.5   Fri Dec 13 01:39:23 2019
+++ pkgsrc/pkgtools/pkglint/files/mklexer_test.go       Wed Jul  1 13:17:41 2020
@@ -605,8 +605,9 @@ func (s *Suite) Test_MkLexer_varUseModif
 
        varUse := p.VarUse()
 
-       t.CheckDeepEquals(varUse.modifiers, []MkVarUseModifier{
-               {"R"}, {"E"}, {"Ox"}, {"tA"}, {"tW"}, {"tw"}})
+       t.CheckDeepEquals(
+               varUse.modifiers,
+               []MkVarUseModifier{"R", "E", "Ox", "tA", "tW", "tw"})
 }
 
 func (s *Suite) Test_MkLexer_varUseModifier__S_parse_error(c *check.C) {
@@ -617,7 +618,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 
        mod := p.varUseModifier("VAR", '}')
 
-       t.CheckEquals(mod, "")
+       t.CheckEquals(mod, MkVarUseModifier(""))
        // XXX: The "S," has just disappeared.
        t.CheckEquals(p.Rest(), "}")
 
@@ -634,7 +635,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 
        modifier := p.varUseModifier("VAR", '}')
 
-       t.CheckEquals(modifier, "tsabc")
+       t.CheckEquals(modifier, MkVarUseModifier("tsabc"))
        t.CheckEquals(p.Rest(), "}")
        t.CheckOutputLines(
                "WARN: filename.mk:123: Invalid separator \"abc\" for :ts modifier of \"VAR\".",
@@ -652,7 +653,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 
        modifier := p.varUseModifier("VAR", '}')
 
-       t.CheckEquals(modifier, "tsabc")
+       t.CheckEquals(modifier, MkVarUseModifier("tsabc"))
        t.CheckEquals(p.Rest(), "}")
 }
 
@@ -664,7 +665,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 
        modifier := p.varUseModifier("VAR", '}')
 
-       t.CheckEquals(modifier, "")
+       t.CheckEquals(modifier, MkVarUseModifier(""))
        t.CheckEquals(p.Rest(), "")
 
        t.CheckOutputLines(
@@ -725,7 +726,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 func (s *Suite) Test_MkLexer_varUseModifier__eq_suffix_replacement(c *check.C) {
        t := s.Init(c)
 
-       test := func(input, modifier, rest string, diagnostics ...string) {
+       test := func(input string, modifier MkVarUseModifier, rest string, diagnostics ...string) {
                line := t.NewLine("filename.mk", 123, "")
                p := NewMkLexer(input, line)
 
@@ -762,7 +763,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 func (s *Suite) Test_MkLexer_varUseModifier__assigment(c *check.C) {
        t := s.Init(c)
 
-       test := func(varname, input, modifier, rest string, diagnostics ...string) {
+       test := func(varname, input string, modifier MkVarUseModifier, rest string, diagnostics ...string) {
                line := t.NewLine("filename.mk", 123, "")
                p := NewMkLexer(input, line)
 
@@ -807,7 +808,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 func (s *Suite) Test_MkLexer_varUseModifierTs(c *check.C) {
        t := s.Init(c)
 
-       test := func(input string, closing byte, mod string, rest string, diagnostics ...string) {
+       test := func(input string, closing byte, mod MkVarUseModifier, rest string, diagnostics ...string) {
                diag := t.NewLine("filename.mk", 123, "")
                lex := NewMkLexer(input, diag)
                mark := lex.lexer.Mark()
@@ -851,7 +852,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 func (s *Suite) Test_MkLexer_varUseModifierMatch(c *check.C) {
        t := s.Init(c)
 
-       testClosing := func(input string, closing byte, modifier, rest string, diagnostics ...string) {
+       testClosing := func(input string, closing byte, modifier MkVarUseModifier, rest string, diagnostics ...string) {
                line := t.NewLine("filename.mk", 123, "")
                p := NewMkLexer(input, line)
 
@@ -862,10 +863,10 @@ func (s *Suite) Test_MkLexer_varUseModif
                t.CheckOutput(diagnostics)
        }
 
-       test := func(input, modifier, rest string, diagnostics ...string) {
+       test := func(input string, modifier MkVarUseModifier, rest string, diagnostics ...string) {
                testClosing(input, '}', modifier, rest, diagnostics...)
        }
-       testParen := func(input, modifier, rest string, diagnostics ...string) {
+       testParen := func(input string, modifier MkVarUseModifier, rest string, diagnostics ...string) {
                testClosing(input, ')', modifier, rest, diagnostics...)
        }
 
@@ -905,7 +906,7 @@ func (s *Suite) Test_MkLexer_varUseModif
 func (s *Suite) Test_MkLexer_varUseModifierMatch__varmod_edge(c *check.C) {
        t := s.Init(c)
 
-       test := func(input, modifier, rest string, diagnostics ...string) {
+       test := func(input string, modifier MkVarUseModifier, rest string, diagnostics ...string) {
                line := t.NewLine("filename.mk", 123, "")
                p := NewMkLexer(input, line)
 
Index: pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go:1.5 pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go:1.6
--- pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go:1.5     Sat Oct 26 09:51:48 2019
+++ pkgsrc/pkgtools/pkglint/files/mktokenslexer_test.go Wed Jul  1 13:17:41 2020
@@ -5,14 +5,6 @@ import (
        "netbsd.org/pkglint/textproc"
 )
 
-func (s *Suite) Test_MkTokensLexer__empty_slice_returns_EOF(c *check.C) {
-       t := s.Init(c)
-
-       lexer := NewMkTokensLexer(nil)
-
-       t.CheckEquals(lexer.EOF(), true)
-}
-
 // A slice of a single token behaves like textproc.Lexer.
 func (s *Suite) Test_MkTokensLexer__single_plain_text_token(c *check.C) {
        t := s.Init(c)
@@ -29,13 +21,133 @@ func (s *Suite) Test_MkTokensLexer__sing
        t.CheckEquals(lexer.EOF(), true)
 }
 
+// When the MkTokensLexer is constructed, it gets a copy of the tokens array.
+// In theory it would be possible to change the tokens after starting lexing,
+// but there is no practical case where that would be useful.
+//
+// Since each slice is a separate view on the underlying array, modifying the
+// size of the outside slice does not affect parsing. This is also only a
+// theoretical case.
+//
+// Because all these cases are only theoretical, the MkTokensLexer doesn't
+// bother to make this unnecessary copy and works on the shared slice.
+func (s *Suite) Test_NewMkTokensLexer__shared_array(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       tokens := b.Tokens(b.VaruseToken("VAR"))
+       lexer := NewMkTokensLexer(tokens)
+
+       t.CheckEquals(lexer.Rest(), "${VAR}")
+
+       tokens[0].Text = "modified text"
+       tokens[0].Varuse = b.VarUse("MODIFIED", "Mpattern")
+
+       t.CheckEquals(lexer.Rest(), "modified text")
+}
+
+func (s *Suite) Test_MkTokensLexer_next(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       tokens := b.Tokens(b.VaruseToken("VAR"))
+       lexer := NewMkTokensLexer(tokens)
+
+       t.CheckEquals(lexer.Lexer.Rest(), "")
+}
+
+func (s *Suite) Test_MkTokensLexer_EOF__plain_text(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       lexer := NewMkTokensLexer(b.Tokens(b.TextToken("rest")))
+
+       t.CheckEquals(lexer.EOF(), false)
+
+       lexer.SkipString("rest")
+
+       t.CheckEquals(lexer.EOF(), true)
+}
+
+func (s *Suite) Test_MkTokensLexer_EOF__varuse(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       lexer := NewMkTokensLexer(b.Tokens(b.VaruseToken("VAR")))
+
+       t.CheckEquals(lexer.EOF(), false)
+
+       lexer.NextVarUse()
+
+       t.CheckEquals(lexer.EOF(), true)
+}
+
+func (s *Suite) Test_MkTokensLexer_Rest(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       lexer := NewMkTokensLexer(b.Tokens(b.VaruseToken("VAR"), b.TextToken("text"), b.VaruseToken("VAR2")))
+
+       t.CheckEquals(lexer.Rest(), "${VAR}text${VAR2}")
+       t.CheckEquals(lexer.NextVarUse().Text, "${VAR}")
+       t.CheckEquals(lexer.Rest(), "text${VAR2}")
+       t.CheckEquals(lexer.SkipString("text"), true)
+       t.CheckEquals(lexer.Rest(), "${VAR2}")
+       t.CheckEquals(lexer.NextVarUse().Text, "${VAR2}")
+       t.CheckEquals(lexer.Rest(), "")
+}
+
+func (s *Suite) Test_MkTokensLexer_Skip(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       lexer := NewMkTokensLexer(b.Tokens(b.VaruseToken("VAR"), b.TextToken("text"), b.VaruseToken("VAR2")))
+
+       t.CheckEquals(lexer.Rest(), "${VAR}text${VAR2}")
+       t.CheckEquals(lexer.NextVarUse().Text, "${VAR}")
+       t.CheckEquals(lexer.Rest(), "text${VAR2}")
+       t.CheckEquals(lexer.Skip(4), true)
+       t.CheckEquals(lexer.Rest(), "${VAR2}")
+       t.CheckEquals(lexer.NextVarUse().Text, "${VAR2}")
+       t.CheckEquals(lexer.Rest(), "")
+}
+
+func (s *Suite) Test_MkTokensLexer_SkipMixed__exact(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       lexer := NewMkTokensLexer(b.Tokens(b.VaruseToken("VAR"), b.TextToken("text"), b.VaruseToken("VAR2")))
+
+       t.CheckEquals(lexer.SkipMixed(17), true)
+       t.CheckEquals(lexer.EOF(), true)
+}
+
+func (s *Suite) Test_MkTokensLexer_SkipMixed__short(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       lexer := NewMkTokensLexer(b.Tokens(b.VaruseToken("VAR"), b.TextToken("text"), b.VaruseToken("VAR2")))
+
+       // After 15 characters, the lexer would be in the middle of a MkVarUse.
+       t.ExpectAssert(func() { lexer.SkipMixed(15) })
+}
+
+func (s *Suite) Test_MkTokensLexer_SkipMixed__long(c *check.C) {
+       t := s.Init(c)
+       b := NewMkTokenBuilder()
+
+       lexer := NewMkTokensLexer(b.Tokens(b.VaruseToken("VAR"), b.TextToken("text"), b.VaruseToken("VAR2")))
+
+       t.ExpectAssert(func() { lexer.SkipMixed(20) })
+}
+
 // If the first element of the slice is a variable use, none of the plain
 // text patterns matches.
 //
 // The code that uses the MkTokensLexer needs to distinguish these cases
 // anyway, therefore it doesn't make sense to treat variable uses as plain
 // text.
-func (s *Suite) Test_MkTokensLexer__single_varuse_token(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_NextVarUse__single_varuse_token(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
@@ -47,7 +159,7 @@ func (s *Suite) Test_MkTokensLexer__sing
        t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
 }
 
-func (s *Suite) Test_MkTokensLexer__plain_then_varuse(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_NextVarUse__plain_then_varuse(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
@@ -61,7 +173,7 @@ func (s *Suite) Test_MkTokensLexer__plai
        t.CheckEquals(lexer.EOF(), true)
 }
 
-func (s *Suite) Test_MkTokensLexer__varuse_varuse_varuse(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_NextVarUse__varuse_varuse_varuse(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
@@ -77,84 +189,99 @@ func (s *Suite) Test_MkTokensLexer__varu
        t.Check(lexer.NextVarUse(), check.IsNil)
 }
 
-func (s *Suite) Test_MkTokensLexer__mark_reset_since_in_initial_state(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_NextVarUse__varuse_when_plain_text(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
-       tokens := b.Tokens(
-               b.VaruseToken("dirs", "O", "u"),
-               b.VaruseToken("VAR", "Mpattern"),
-               b.VaruseToken(".TARGET"))
-       lexer := NewMkTokensLexer(tokens)
+       lexer := NewMkTokensLexer(b.Tokens(b.TextToken("text")))
 
-       start := lexer.Mark()
-       t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
-       middle := lexer.Mark()
-       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}${.TARGET}")
-       lexer.Reset(start)
-       t.CheckEquals(lexer.Rest(), "${dirs:O:u}${VAR:Mpattern}${.TARGET}")
-       lexer.Reset(middle)
-       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}${.TARGET}")
+       t.Check(lexer.NextVarUse(), check.IsNil)
+       t.CheckEquals(lexer.NextString("te"), "te")
+       t.Check(lexer.NextVarUse(), check.IsNil)
+       t.CheckEquals(lexer.NextString("xt"), "xt")
+       t.Check(lexer.NextVarUse(), check.IsNil)
 }
 
-func (s *Suite) Test_MkTokensLexer__mark_reset_since_inside_plain_text(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_NextVarUse__peek_after_varuse(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
-       lexer := NewMkTokensLexer(b.Tokens(
-               b.TextToken("plain text"),
-               b.VaruseToken("VAR", "Mpattern"),
-               b.TextToken("rest")))
+       tokens := b.Tokens(
+               b.VaruseToken("VAR"),
+               b.VaruseToken("VAR"),
+               b.TextToken("text"))
+       lexer := NewMkTokensLexer(tokens)
 
-       start := lexer.Mark()
-       t.CheckEquals(lexer.NextBytesSet(textproc.Alpha), "plain")
-       middle := lexer.Mark()
-       t.CheckEquals(lexer.Rest(), " text${VAR:Mpattern}rest")
-       lexer.Reset(start)
-       t.CheckEquals(lexer.Rest(), "plain text${VAR:Mpattern}rest")
-       lexer.Reset(middle)
-       t.CheckEquals(lexer.Rest(), " text${VAR:Mpattern}rest")
+       t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
+       t.CheckEquals(lexer.PeekByte(), -1)
+
+       t.CheckDeepEquals(lexer.NextVarUse(), tokens[1])
+       t.CheckEquals(lexer.PeekByte(), int('t'))
 }
 
-func (s *Suite) Test_MkTokensLexer__mark_reset_since_after_plain_text(c *check.C) {
+// The code that creates the tokens for the lexer never puts two
+// plain text MkTokens besides each other. There's no point in doing
+// that since they could have been combined into a single token from
+// the beginning.
+func (s *Suite) Test_MkTokensLexer_NextVarUse__adjacent_plain_text(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
        lexer := NewMkTokensLexer(b.Tokens(
-               b.TextToken("plain text"),
-               b.VaruseToken("VAR", "Mpattern"),
-               b.TextToken("rest")))
+               b.TextToken("text1"),
+               b.TextToken("text2")))
 
-       start := lexer.Mark()
-       t.CheckEquals(lexer.SkipString("plain text"), true)
-       end := lexer.Mark()
-       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}rest")
-       lexer.Reset(start)
-       t.CheckEquals(lexer.Rest(), "plain text${VAR:Mpattern}rest")
-       lexer.Reset(end)
-       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}rest")
+       // Returns false since the string is distributed over two separate tokens.
+       t.CheckEquals(lexer.SkipString("text1text2"), false)
+
+       t.CheckEquals(lexer.SkipString("text1"), true)
+
+       // This returns false since the internal lexer is not advanced to the
+       // next text token. To do that, all methods from the internal lexer
+       // would have to be redefined by MkTokensLexer in order to advance the
+       // internal lexer to the next token.
+       //
+       // Since this situation doesn't occur in practice, there's no point in
+       // implementing it.
+       t.CheckEquals(lexer.SkipString("text2"), false)
+
+       // Just for covering the "Varuse != nil" branch in MkTokensLexer.NextVarUse.
+       t.Check(lexer.NextVarUse(), check.IsNil)
+
+       // The string is still not found since the next token is only consumed
+       // by the NextVarUse above if it is indeed a VarUse.
+       t.CheckEquals(lexer.SkipString("text2"), false)
 }
 
-func (s *Suite) Test_MkTokensLexer__mark_reset_since_after_varuse(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_Mark__multiple_marks_in_varuse(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
        tokens := b.Tokens(
-               b.VaruseToken("VAR", "Mpattern"),
-               b.TextToken("rest"))
+               b.VaruseToken("VAR1"),
+               b.VaruseToken("VAR2"),
+               b.VaruseToken("VAR3"))
        lexer := NewMkTokensLexer(tokens)
 
        start := lexer.Mark()
-       t.CheckEquals(lexer.NextVarUse(), tokens[0])
+       t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
+       middle := lexer.Mark()
+       t.CheckDeepEquals(lexer.NextVarUse(), tokens[1])
+       further := lexer.Mark()
+       t.CheckDeepEquals(lexer.NextVarUse(), tokens[2])
        end := lexer.Mark()
-       t.CheckEquals(lexer.Rest(), "rest")
+       t.CheckEquals(lexer.Rest(), "")
+       lexer.Reset(middle)
+       t.CheckEquals(lexer.Rest(), "${VAR2}${VAR3}")
+       lexer.Reset(further)
+       t.CheckEquals(lexer.Rest(), "${VAR3}")
        lexer.Reset(start)
-       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}rest")
+       t.CheckEquals(lexer.Rest(), "${VAR1}${VAR2}${VAR3}")
        lexer.Reset(end)
-       t.CheckEquals(lexer.Rest(), "rest")
+       t.CheckEquals(lexer.Rest(), "")
 }
 
-func (s *Suite) Test_MkTokensLexer__multiple_marks_in_same_plain_text(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_Mark__multiple_marks_in_same_plain_text(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
@@ -177,137 +304,96 @@ func (s *Suite) Test_MkTokensLexer__mult
        t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}rest")
 }
 
-func (s *Suite) Test_MkTokensLexer__multiple_marks_in_varuse(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_Since(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
        tokens := b.Tokens(
-               b.VaruseToken("VAR1"),
-               b.VaruseToken("VAR2"),
-               b.VaruseToken("VAR3"))
+               b.VaruseToken("dirs", "O", "u"),
+               b.VaruseToken("VAR", "Mpattern"),
+               b.VaruseToken(".TARGET"))
        lexer := NewMkTokensLexer(tokens)
 
        start := lexer.Mark()
        t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
        middle := lexer.Mark()
-       t.CheckDeepEquals(lexer.NextVarUse(), tokens[1])
-       further := lexer.Mark()
-       t.CheckDeepEquals(lexer.NextVarUse(), tokens[2])
-       end := lexer.Mark()
-       t.CheckEquals(lexer.Rest(), "")
-       lexer.Reset(middle)
-       t.CheckEquals(lexer.Rest(), "${VAR2}${VAR3}")
-       lexer.Reset(further)
-       t.CheckEquals(lexer.Rest(), "${VAR3}")
-       lexer.Reset(start)
-       t.CheckEquals(lexer.Rest(), "${VAR1}${VAR2}${VAR3}")
-       lexer.Reset(end)
-       t.CheckEquals(lexer.Rest(), "")
+       t.CheckEquals(lexer.Since(start), "${dirs:O:u}")
+       t.CheckEquals(lexer.Since(middle), "")
 }
 
-func (s *Suite) Test_MkTokensLexer__EOF_before_plain_text(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_Reset__initial_state(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
-       lexer := NewMkTokensLexer(b.Tokens(b.TextToken("rest")))
+       tokens := b.Tokens(
+               b.VaruseToken("dirs", "O", "u"),
+               b.VaruseToken("VAR", "Mpattern"),
+               b.VaruseToken(".TARGET"))
+       lexer := NewMkTokensLexer(tokens)
 
-       t.CheckEquals(lexer.EOF(), false)
+       start := lexer.Mark()
+       t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
+       middle := lexer.Mark()
+       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}${.TARGET}")
+       lexer.Reset(start)
+       t.CheckEquals(lexer.Rest(), "${dirs:O:u}${VAR:Mpattern}${.TARGET}")
+       lexer.Reset(middle)
+       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}${.TARGET}")
 }
 
-func (s *Suite) Test_MkTokensLexer__EOF_before_varuse(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_Reset__inside_plain_text(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
-       lexer := NewMkTokensLexer(b.Tokens(b.VaruseToken("VAR")))
+       lexer := NewMkTokensLexer(b.Tokens(
+               b.TextToken("plain text"),
+               b.VaruseToken("VAR", "Mpattern"),
+               b.TextToken("rest")))
 
-       t.CheckEquals(lexer.EOF(), false)
+       start := lexer.Mark()
+       t.CheckEquals(lexer.NextBytesSet(textproc.Alpha), "plain")
+       middle := lexer.Mark()
+       t.CheckEquals(lexer.Rest(), " text${VAR:Mpattern}rest")
+       lexer.Reset(start)
+       t.CheckEquals(lexer.Rest(), "plain text${VAR:Mpattern}rest")
+       lexer.Reset(middle)
+       t.CheckEquals(lexer.Rest(), " text${VAR:Mpattern}rest")
 }
 
-// When the MkTokensLexer is constructed, it gets a copy of the tokens array.
-// In theory it would be possible to change the tokens after starting lexing,
-// but there is no practical case where that would be useful.
-//
-// Since each slice is a separate view on the underlying array, modifying the
-// size of the outside slice does not affect parsing. This is also only a
-// theoretical case.
-//
-// Because all these cases are only theoretical, the MkTokensLexer doesn't
-// bother to make this unnecessary copy and works on the shared slice.
-func (s *Suite) Test_MkTokensLexer__constructor_uses_shared_array(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_Reset__after_plain_text(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
-       tokens := b.Tokens(b.VaruseToken("VAR"))
-       lexer := NewMkTokensLexer(tokens)
-
-       t.CheckEquals(lexer.Rest(), "${VAR}")
-
-       tokens[0].Text = "modified text"
-       tokens[0].Varuse = b.VarUse("MODIFIED", "Mpattern")
+       lexer := NewMkTokensLexer(b.Tokens(
+               b.TextToken("plain text"),
+               b.VaruseToken("VAR", "Mpattern"),
+               b.TextToken("rest")))
 
-       t.CheckEquals(lexer.Rest(), "modified text")
+       start := lexer.Mark()
+       t.CheckEquals(lexer.SkipString("plain text"), true)
+       end := lexer.Mark()
+       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}rest")
+       lexer.Reset(start)
+       t.CheckEquals(lexer.Rest(), "plain text${VAR:Mpattern}rest")
+       lexer.Reset(end)
+       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}rest")
 }
 
-func (s *Suite) Test_MkTokensLexer__peek_after_varuse(c *check.C) {
+func (s *Suite) Test_MkTokensLexer_Reset__after_varuse(c *check.C) {
        t := s.Init(c)
        b := NewMkTokenBuilder()
 
        tokens := b.Tokens(
-               b.VaruseToken("VAR"),
-               b.VaruseToken("VAR"),
-               b.TextToken("text"))
+               b.VaruseToken("VAR", "Mpattern"),
+               b.TextToken("rest"))
        lexer := NewMkTokensLexer(tokens)
 
-       t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
-       t.CheckEquals(lexer.PeekByte(), -1)
-
-       t.CheckDeepEquals(lexer.NextVarUse(), tokens[1])
-       t.CheckEquals(lexer.PeekByte(), int('t'))
-}
-
-func (s *Suite) Test_MkTokensLexer__varuse_when_plain_text(c *check.C) {
-       t := s.Init(c)
-       b := NewMkTokenBuilder()
-
-       lexer := NewMkTokensLexer(b.Tokens(b.TextToken("text")))
-
-       t.Check(lexer.NextVarUse(), check.IsNil)
-       t.CheckEquals(lexer.NextString("te"), "te")
-       t.Check(lexer.NextVarUse(), check.IsNil)
-       t.CheckEquals(lexer.NextString("xt"), "xt")
-       t.Check(lexer.NextVarUse(), check.IsNil)
-}
-
-// The code that creates the tokens for the lexer never puts two
-// plain text MkTokens besides each other. There's no point in doing
-// that since they could have been combined into a single token from
-// the beginning.
-func (s *Suite) Test_MkTokensLexer__adjacent_plain_text(c *check.C) {
-       t := s.Init(c)
-       b := NewMkTokenBuilder()
-
-       lexer := NewMkTokensLexer(b.Tokens(
-               b.TextToken("text1"),
-               b.TextToken("text2")))
-
-       // Returns false since the string is distributed over two separate tokens.
-       t.CheckEquals(lexer.SkipString("text1text2"), false)
-
-       t.CheckEquals(lexer.SkipString("text1"), true)
-
-       // This returns false since the internal lexer is not advanced to the
-       // next text token. To do that, all methods from the internal lexer
-       // would have to be redefined by MkTokensLexer in order to advance the
-       // internal lexer to the next token.
-       //
-       // Since this situation doesn't occur in practice, there's no point in
-       // implementing it.
-       t.CheckEquals(lexer.SkipString("text2"), false)
-
-       // Just for covering the "Varuse != nil" branch in MkTokensLexer.NextVarUse.
-       t.Check(lexer.NextVarUse(), check.IsNil)
-
-       // The string is still not found since the next token is only consumed
-       // by the NextVarUse above if it is indeed a VarUse.
-       t.CheckEquals(lexer.SkipString("text2"), false)
+       start := lexer.Mark()
+       t.CheckEquals(lexer.NextVarUse(), tokens[0])
+       end := lexer.Mark()
+       t.CheckEquals(lexer.Rest(), "rest")
+       lexer.Reset(start)
+       t.CheckEquals(lexer.Rest(), "${VAR:Mpattern}rest")
+       lexer.Reset(end)
+       t.CheckEquals(lexer.Rest(), "rest")
 }

Index: pkgsrc/pkgtools/pkglint/files/mkline.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.81 pkgsrc/pkgtools/pkglint/files/mkline.go:1.82
--- pkgsrc/pkgtools/pkglint/files/mkline.go:1.81        Sun Jun 14 11:35:54 2020
+++ pkgsrc/pkgtools/pkglint/files/mkline.go     Wed Jul  1 13:17:41 2020
@@ -872,7 +872,7 @@ func (mkline *MkLine) ForEachUsedVarUse(
        }
        mkline.ForEachUsedText(varname, time, action)
        for _, mod := range varuse.modifiers {
-               mkline.ForEachUsedText(mod.Text, time, action)
+               mkline.ForEachUsedText(mod.String(), time, action)
        }
 }
 

Index: pkgsrc/pkgtools/pkglint/files/mkparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.40 pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.41
--- pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.40 Sun Jun 14 11:35:54 2020
+++ pkgsrc/pkgtools/pkglint/files/mkparser_test.go      Wed Jul  1 13:17:41 2020
@@ -12,13 +12,13 @@ func (s *Suite) Test_MkParser_MkCond(c *
        cmp := func(left MkCondTerm, op string, right MkCondTerm) *MkCond {
                return &MkCond{Compare: &MkCondCompare{left, op, right}}
        }
-       cvar := func(name string, modifiers ...string) MkCondTerm {
+       cvar := func(name string, modifiers ...MkVarUseModifier) MkCondTerm {
                return MkCondTerm{Var: b.VarUse(name, modifiers...)}
        }
        cstr := func(s string) MkCondTerm { return MkCondTerm{Str: s} }
        cnum := func(s string) MkCondTerm { return MkCondTerm{Num: s} }
 
-       termVar := func(varname string, mods ...string) *MkCond {
+       termVar := func(varname string, mods ...MkVarUseModifier) *MkCond {
                return &MkCond{Term: &MkCondTerm{Var: b.VarUse(varname, mods...)}}
        }
        termNum := func(num string) *MkCond {
@@ -34,7 +34,7 @@ func (s *Suite) Test_MkParser_MkCond(c *
        call := func(name string, arg string) *MkCond {
                return &MkCond{Call: &MkCondCall{name, arg}}
        }
-       empty := func(varname string, mods ...string) *MkCond {
+       empty := func(varname string, mods ...MkVarUseModifier) *MkCond {
                return &MkCond{Empty: b.VarUse(varname, mods...)}
        }
        defined := func(varname string) *MkCond { return &MkCond{Defined: varname} }
@@ -495,7 +495,7 @@ func (s *Suite) Test_MkCondWalker_Walk(c
                strs := make([]string, 1+len(varuse.modifiers))
                strs[0] = varuse.varname
                for i, mod := range varuse.modifiers {
-                       strs[1+i] = mod.Text
+                       strs[1+i] = mod.String()
                }
                return strings.Join(strs, ":")
        }

Index: pkgsrc/pkgtools/pkglint/files/mkshparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshparser.go:1.19 pkgsrc/pkgtools/pkglint/files/mkshparser.go:1.20
--- pkgsrc/pkgtools/pkglint/files/mkshparser.go:1.19    Fri Dec 13 01:39:23 2019
+++ pkgsrc/pkgtools/pkglint/files/mkshparser.go Wed Jul  1 13:17:41 2020
@@ -250,15 +250,15 @@ func (lex *ShellLexer) Lex(lval *shyySym
                lval.Word = p.ShToken()
                lex.atCommandStart = false
 
-               // Inside of a case statement, ${PATTERNS:@p@ (${p}) action ;; @} expands to
+               // Inside of a case statement, ${PATTERNS:@p@ (${p}) continue ;; @} expands to
                // a list of case-items, and after this list a new command starts.
                // This is necessary to return a following "esac" as tkESAC instead of a
                // simple word.
                if lex.sinceCase >= 0 && len(lval.Word.Atoms) == 1 {
                        if varUse := lval.Word.Atoms[0].VarUse(); varUse != nil {
                                if len(varUse.modifiers) > 0 {
-                                       lastModifier := varUse.modifiers[len(varUse.modifiers)-1].Text
-                                       if hasPrefix(lastModifier, "@") {
+                                       lastModifier := varUse.modifiers[len(varUse.modifiers)-1]
+                                       if lastModifier.HasPrefix("@") || lastModifier.HasPrefix("=") {
                                                lex.atCommandStart = true
                                        }
                                }

Index: pkgsrc/pkgtools/pkglint/files/mktokenslexer.go
diff -u pkgsrc/pkgtools/pkglint/files/mktokenslexer.go:1.4 pkgsrc/pkgtools/pkglint/files/mktokenslexer.go:1.5
--- pkgsrc/pkgtools/pkglint/files/mktokenslexer.go:1.4  Sat Jun  6 20:42:56 2020
+++ pkgsrc/pkgtools/pkglint/files/mktokenslexer.go      Wed Jul  1 13:17:41 2020
@@ -60,6 +60,7 @@ func (m *MkTokensLexer) SkipMixed(n int)
                use := m.NextVarUse()
                if use != nil {
                        n -= len(use.Text)
+                       assert(n >= 0)
                } else {
                        skip := imin(len(m.Lexer.Rest()), n)
                        assert(m.Lexer.Skip(skip))

Index: pkgsrc/pkgtools/pkglint/files/mktypes.go
diff -u pkgsrc/pkgtools/pkglint/files/mktypes.go:1.25 pkgsrc/pkgtools/pkglint/files/mktypes.go:1.26
--- pkgsrc/pkgtools/pkglint/files/mktypes.go:1.25       Thu Apr 30 21:15:03 2020
+++ pkgsrc/pkgtools/pkglint/files/mktypes.go    Wed Jul  1 13:17:41 2020
@@ -39,19 +39,31 @@ func NewMkVarUse(varname string, modifie
 
 func (vu *MkVarUse) String() string { return sprintf("${%s%s}", vu.varname, vu.Mod()) }
 
-type MkVarUseModifier struct {
-       Text string // The text of the modifier, without the initial colon.
+// MkVarUseModifier stores the text of the modifier, without the initial colon.
+// Examples: "Q", "S,from,to,g"
+type MkVarUseModifier string
+
+func (m MkVarUseModifier) String() string {
+       return string(m)
+}
+
+func (m MkVarUseModifier) Quoted() string {
+       return strings.Replace(string(m), ":", "\\:", -1)
+}
+
+func (m MkVarUseModifier) HasPrefix(prefix string) bool {
+       return hasPrefix(m.String(), prefix)
 }
 
-func (m MkVarUseModifier) IsQ() bool { return m.Text == "Q" }
+func (m MkVarUseModifier) IsQ() bool { return m == "Q" }
 
 func (m MkVarUseModifier) IsSuffixSubst() bool {
        // XXX: There are other cases
-       return hasPrefix(m.Text, "=")
+       return m.HasPrefix("=")
 }
 
 func (m MkVarUseModifier) MatchSubst() (ok bool, regex bool, from string, to string, options string) {
-       p := NewMkLexer(m.Text, nil)
+       p := NewMkLexer(m.String(), nil)
        return p.varUseModifierSubst('}')
 }
 
@@ -91,7 +103,7 @@ func (m MkVarUseModifier) Subst(str stri
 
        ok, result := m.EvalSubst(str, leftAnchor, from, rightAnchor, to, options)
        if trace.Tracing && ok && result != str {
-               trace.Stepf("Subst: %q %q => %q", str, m.Text, result)
+               trace.Stepf("Subst: %q %q => %q", str, m.String(), result)
        }
        return ok, result
 }
@@ -124,21 +136,22 @@ func (MkVarUseModifier) EvalSubst(s stri
 //  :Npattern   => true,  false, "pattern", true
 //  :X          => false
 func (m MkVarUseModifier) MatchMatch() (ok bool, positive bool, pattern string, exact bool) {
-       if hasPrefix(m.Text, "M") || hasPrefix(m.Text, "N") {
+       if m.HasPrefix("M") || m.HasPrefix("N") {
+               str := m.String()
                // See devel/bmake/files/str.c:^Str_Match
-               exact := !strings.ContainsAny(m.Text[1:], "*?[\\$")
-               return true, m.Text[0] == 'M', m.Text[1:], exact
+               exact := !strings.ContainsAny(str[1:], "*?[\\$")
+               return true, str[0] == 'M', str[1:], exact
        }
        return false, false, "", false
 }
 
-func (m MkVarUseModifier) IsToLower() bool { return m.Text == "tl" }
+func (m MkVarUseModifier) IsToLower() bool { return m == "tl" }
 
 // ChangesList returns true if applying this modifier to a variable
 // may change the expression from a list type to a non-list type
 // or vice versa.
 func (m MkVarUseModifier) ChangesList() bool {
-       text := m.Text
+       text := m.String()
 
        // See MkParser.varUseModifier for the meaning of these modifiers.
        switch text[0] {
@@ -172,7 +185,7 @@ func (vu *MkVarUse) Mod() string {
        var mod strings.Builder
        for _, modifier := range vu.modifiers {
                mod.WriteString(":")
-               mod.WriteString(modifier.Text)
+               mod.WriteString(modifier.String())
        }
        return mod.String()
 }
@@ -184,7 +197,7 @@ func (vu *MkVarUse) IsExpression() bool 
                return false
        }
        mod := vu.modifiers[0]
-       return mod.Text == "L" || hasPrefix(mod.Text, "?")
+       return mod.String() == "L" || mod.HasPrefix("?")
 }
 
 func (vu *MkVarUse) IsQ() bool {
@@ -194,7 +207,7 @@ func (vu *MkVarUse) IsQ() bool {
 
 func (vu *MkVarUse) HasModifier(prefix string) bool {
        for _, mod := range vu.modifiers {
-               if hasPrefix(mod.Text, prefix) {
+               if mod.HasPrefix(prefix) {
                        return true
                }
        }
Index: pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.25 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.26
--- pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.25      Sat Jan  4 19:53:14 2020
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go   Wed Jul  1 13:17:41 2020
@@ -140,10 +140,12 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
 
        operator := func(s string) *ShAtom { return atom(shtOperator, s) }
        comment := func(s string) *ShAtom { return atom(shtComment, s) }
-       mkvar := func(varname string, modifiers ...string) *ShAtom {
+       mkvar := func(varname string, modifiers ...MkVarUseModifier) *ShAtom {
                text := "${" + varname
                for _, modifier := range modifiers {
-                       text += ":" + strings.Replace(strings.Replace(modifier, "\\", "\\\\", -1), ":", "\\:", -1)
+                       escapedBackslash := strings.Replace(modifier.String(), "\\", "\\\\", -1)
+                       escapedColon := strings.Replace(escapedBackslash, ":", "\\:", -1)
+                       text += ":" + escapedColon // TODO: modifier.Quoted
                }
                text += "}"
                varuse := NewMkTokenBuilder().VarUse(varname, modifiers...)

Index: pkgsrc/pkgtools/pkglint/files/mktypes_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.23 pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.24
--- pkgsrc/pkgtools/pkglint/files/mktypes_test.go:1.23  Thu Apr 30 21:15:03 2020
+++ pkgsrc/pkgtools/pkglint/files/mktypes_test.go       Wed Jul  1 13:17:41 2020
@@ -9,19 +9,19 @@ type MkTokenBuilder struct{}
 
 func NewMkTokenBuilder() MkTokenBuilder { return MkTokenBuilder{} }
 
-func (b MkTokenBuilder) VaruseToken(varname string, modifiers ...string) *MkToken {
+func (b MkTokenBuilder) VaruseToken(varname string, modifiers ...MkVarUseModifier) *MkToken {
        var text strings.Builder
        text.WriteString("${")
        text.WriteString(varname)
        for _, modifier := range modifiers {
                text.WriteString(":")
-               text.WriteString(modifier)
+               text.WriteString(modifier.String()) // TODO: Quoted
        }
        text.WriteString("}")
        return &MkToken{Text: text.String(), Varuse: b.VarUse(varname, modifiers...)}
 }
 
-func (b MkTokenBuilder) VaruseTextToken(text, varname string, modifiers ...string) *MkToken {
+func (b MkTokenBuilder) VaruseTextToken(text, varname string, modifiers ...MkVarUseModifier) *MkToken {
        return &MkToken{Text: text, Varuse: b.VarUse(varname, modifiers...)}
 }
 
@@ -31,18 +31,77 @@ func (MkTokenBuilder) TextToken(text str
 
 func (MkTokenBuilder) Tokens(tokens ...*MkToken) []*MkToken { return tokens }
 
-func (MkTokenBuilder) VarUse(varname string, modifiers ...string) *MkVarUse {
-       var mods []MkVarUseModifier
-       for _, modifier := range modifiers {
-               mods = append(mods, MkVarUseModifier{modifier})
+func (MkTokenBuilder) VarUse(varname string, modifiers ...MkVarUseModifier) *MkVarUse {
+       return NewMkVarUse(varname, modifiers...)
+}
+
+func (s *Suite) Test_NewMkVarUse(c *check.C) {
+       t := s.Init(c)
+
+       use := NewMkVarUse("VARNAME", "Q")
+
+       t.CheckEquals(use.String(), "${VARNAME:Q}")
+       t.CheckEquals(use.varname, "VARNAME")
+       t.CheckDeepEquals(use.modifiers, []MkVarUseModifier{"Q"})
+}
+
+func (s *Suite) Test_MkVarUse_String(c *check.C) {
+       t := s.Init(c)
+
+       use := NewMkVarUse("VARNAME", "S,:,colon,", "Q")
+
+       t.CheckEquals(use.String(), "${VARNAME:S,:,colon,:Q}")
+}
+
+func (s *Suite) Test_MkVarUseModifier_String(c *check.C) {
+       t := s.Init(c)
+
+       test := func(mod MkVarUseModifier, str string) {
+               t.CheckEquals(mod.String(), str)
+       }
+
+       test("Q", "Q")
+       test("S/from/to/1g", "S/from/to/1g")
+       test(":", ":")
+}
+
+func (s *Suite) Test_MkVarUseModifier_Quoted(c *check.C) {
+       t := s.Init(c)
+
+       test := func(mod MkVarUseModifier, quoted string) {
+               t.CheckEquals(mod.Quoted(), quoted)
        }
-       return NewMkVarUse(varname, mods...)
+
+       test("Q", "Q")
+       test("S/from/to/1g", "S/from/to/1g")
+       test(":", "\\:")
+}
+
+func (s *Suite) Test_MkVarUseModifier_HasPrefix(c *check.C) {
+       t := s.Init(c)
+
+       t.CheckEquals(MkVarUseModifier("Q").HasPrefix("Q"), true)
+       t.CheckEquals(MkVarUseModifier("S/from/to/1g").HasPrefix("Q"), false)
+}
+
+func (s *Suite) Test_MkVarUseModifier_IsQ(c *check.C) {
+       t := s.Init(c)
+
+       t.CheckEquals(MkVarUseModifier("Q").IsQ(), true)
+       t.CheckEquals(MkVarUseModifier("S/from/to/1g").IsQ(), false)
+}
+
+func (s *Suite) Test_MkVarUseModifier_IsSuffixSubst(c *check.C) {
+       t := s.Init(c)
+
+       t.CheckEquals(MkVarUseModifier("=suffix").IsSuffixSubst(), true)
+       t.CheckEquals(MkVarUseModifier("S,=,eq,").IsSuffixSubst(), false)
 }
 
 func (s *Suite) Test_MkVarUseModifier_MatchSubst(c *check.C) {
        t := s.Init(c)
 
-       mod := MkVarUseModifier{"S/from/to/1g"}
+       mod := MkVarUseModifier("S/from/to/1g")
 
        ok, regex, from, to, options := mod.MatchSubst()
 
@@ -56,7 +115,7 @@ func (s *Suite) Test_MkVarUseModifier_Ma
 func (s *Suite) Test_MkVarUseModifier_MatchSubst__backslash(c *check.C) {
        t := s.Init(c)
 
-       mod := MkVarUseModifier{"S/\\//\\:/"}
+       mod := MkVarUseModifier("S/\\//\\:/")
 
        ok, regex, from, to, options := mod.MatchSubst()
 
@@ -76,7 +135,7 @@ func (s *Suite) Test_MkVarUseModifier_Ma
 func (s *Suite) Test_MkVarUseModifier_MatchSubst__backslash_as_separator(c *check.C) {
        t := s.Init(c)
 
-       mod := MkVarUseModifier{"S\\.post1\\\\1"}
+       mod := MkVarUseModifier("S\\.post1\\\\1")
 
        ok, regex, from, to, options := mod.MatchSubst()
 
@@ -91,7 +150,7 @@ func (s *Suite) Test_MkVarUseModifier_Su
        t := s.Init(c)
 
        test := func(mod, str string, ok bool, result string) {
-               m := MkVarUseModifier{mod}
+               m := MkVarUseModifier(mod)
 
                actualOk, actualResult := m.Subst(str)
 
@@ -154,7 +213,7 @@ func (s *Suite) Test_MkVarUseModifier_Ev
        t := s.Init(c)
 
        test := func(s string, left bool, from string, right bool, to string, flags string, ok bool, result string) {
-               mod := MkVarUseModifier{}
+               mod := MkVarUseModifier("")
 
                actualOk, actual := mod.EvalSubst(s, left, from, right, to, flags)
 
@@ -195,13 +254,12 @@ func (s *Suite) Test_MkVarUseModifier_Ev
 func (s *Suite) Test_MkVarUseModifier_MatchMatch(c *check.C) {
        t := s.Init(c)
 
-       testFail := func(modifier string) {
-               mod := MkVarUseModifier{modifier}
+       testFail := func(modifier MkVarUseModifier) {
+               mod := modifier
                ok, _, _, _ := mod.MatchMatch()
                t.CheckEquals(ok, false)
        }
-       test := func(modifier string, positive bool, pattern string, exact bool) {
-               mod := MkVarUseModifier{modifier}
+       test := func(mod MkVarUseModifier, positive bool, pattern string, exact bool) {
                actualOk, actualPositive, actualPattern, actualExact := mod.MatchMatch()
                t.CheckDeepEquals(
                        []interface{}{actualOk, actualPositive, actualPattern, actualExact},
@@ -217,11 +275,17 @@ func (s *Suite) Test_MkVarUseModifier_Ma
        test("Npattern", false, "pattern", true)
 }
 
+func (s *Suite) Test_MkVarUseModifier_IsToLower(c *check.C) {
+       t := s.Init(c)
+
+       t.CheckEquals(MkVarUseModifier("tl").IsToLower(), true)
+       t.CheckEquals(MkVarUseModifier("tu").IsToLower(), false)
+}
+
 func (s *Suite) Test_MkVarUseModifier_ChangesList(c *check.C) {
        t := s.Init(c)
 
-       test := func(modifier string, changes bool) {
-               mod := MkVarUseModifier{modifier}
+       test := func(mod MkVarUseModifier, changes bool) {
                t.CheckEquals(mod.ChangesList(), changes)
        }
 
@@ -281,3 +345,32 @@ func (s *Suite) Test_MkVarUse_Mod(c *che
        test("${varname:Q}", ":Q")
        test("${PATH:ts::Q}", ":ts::Q")
 }
+
+func (s *Suite) Test_MkVarUse_IsExpression(c *check.C) {
+       t := s.Init(c)
+
+       t.CheckEquals(ToVarUse("${VAR}").IsExpression(), false)
+       t.CheckEquals(ToVarUse("${expr:L}").IsExpression(), true)
+       t.CheckEquals(ToVarUse("${expr:?then:else}").IsExpression(), true)
+}
+
+func (s *Suite) Test_MkVarUse_IsQ(c *check.C) {
+       t := s.Init(c)
+
+       t.CheckEquals(ToVarUse("${VAR}").IsQ(), false)
+       t.CheckEquals(ToVarUse("${VAR:Q}").IsQ(), true)
+       t.CheckEquals(ToVarUse("${VAR:tl}").IsQ(), false)
+       t.CheckEquals(ToVarUse("${VAR:tl:Q}").IsQ(), true)
+       t.CheckEquals(ToVarUse("${VAR:Q:tl}").IsQ(), false)
+}
+
+func (s *Suite) Test_MkVarUse_HasModifier(c *check.C) {
+       t := s.Init(c)
+
+       t.CheckEquals(ToVarUse("${VAR}").HasModifier("Q"), false)
+       t.CheckEquals(ToVarUse("${VAR:Q}").HasModifier("Q"), true)
+       t.CheckEquals(ToVarUse("${VAR:tl}").HasModifier("Q"), false)
+       t.CheckEquals(ToVarUse("${VAR:tl}").HasModifier("t"), true)
+       t.CheckEquals(ToVarUse("${VAR:tl:Q}").HasModifier("Q"), true)
+       t.CheckEquals(ToVarUse("${VAR:Q:tl}").HasModifier("Q"), true)
+}

Index: pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go:1.12 pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go:1.13
--- pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go:1.12       Mon Jun 22 06:35:02 2020
+++ pkgsrc/pkgtools/pkglint/files/mkvarusechecker.go    Wed Jul  1 13:17:41 2020
@@ -1,6 +1,9 @@
 package pkglint
 
-import "strings"
+import (
+       "netbsd.org/pkglint/textproc"
+       "strings"
+)
 
 type MkVarUseChecker struct {
        use     *MkVarUse
@@ -72,9 +75,9 @@ func (ck *MkVarUseChecker) checkModifier
        ck.checkModifiersSuffix()
        ck.checkModifiersRange()
 
-       // TODO: Add checks for a single modifier, among them:
-       // TODO: Suggest to replace ${VAR:@l@-l${l}@} with the simpler ${VAR:S,^,-l,}.
-       // TODO: Suggest to replace ${VAR:@l@${l}suffix@} with the simpler ${VAR:=suffix}.
+       for _, mod := range ck.use.modifiers {
+               ck.checkModifierLoop(mod)
+       }
        // TODO: Investigate why :Q is not checked at this exact place.
 }
 
@@ -133,6 +136,55 @@ func (ck *MkVarUseChecker) checkModifier
        fix.Apply()
 }
 
+func (ck *MkVarUseChecker) checkModifierLoop(mod MkVarUseModifier) {
+       str := mod.String()
+       if !hasSuffix(str, "@") {
+               return
+       }
+       lex := textproc.NewLexer(str[:len(str)-1])
+       if !lex.SkipByte('@') {
+               return
+       }
+       varname := lex.NextBytesSet(textproc.Alnum)
+       if varname == "" || !lex.SkipByte('@') {
+               return
+       }
+       body := lex.Rest()
+       // TODO: Are MkVarUse interpreted the same in the before/after modifiers?
+       if !matches(body, `^[-A-Za-z0-9 $();_{}]+$`) {
+               return
+       }
+       varnameUse := "${" + varname + "}"
+
+       n := 0
+       ck.MkLine.ForEachUsedText(str, VucRunTime, func(varUse *MkVarUse, time VucTime) {
+               if varUse.varname == varname {
+                       n++
+               }
+       })
+       if n != 1 {
+               return
+       }
+
+       if rest := strings.TrimSuffix(body, varnameUse); len(rest) < len(body) {
+               simpler := "S,^," + rest + ","
+               fix := ck.MkLine.Autofix()
+               fix.Notef("The modifier %q can be replaced with the simpler %q.",
+                       str, simpler)
+               fix.Replace(str, simpler)
+               fix.Apply()
+       }
+
+       if rest := strings.TrimPrefix(body, varnameUse); len(rest) < len(body) {
+               simpler := "=" + rest
+               fix := ck.MkLine.Autofix()
+               fix.Notef("The modifier %q can be replaced with the simpler %q.",
+                       str, simpler)
+               fix.Replace(str, simpler)
+               fix.Apply()
+       }
+}
+
 func (ck *MkVarUseChecker) checkVarname(time VucTime) {
        varname := ck.use.varname
        if varname == "@" {

Index: pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go:1.13 pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go:1.14
--- pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go:1.13  Fri Jun 12 19:14:45 2020
+++ pkgsrc/pkgtools/pkglint/files/mkvarusechecker_test.go       Wed Jul  1 13:17:41 2020
@@ -354,6 +354,69 @@ func (s *Suite) Test_MkVarUseChecker_che
        mklines.Check()
 }
 
+func (s *Suite) Test_MkVarUseChecker_checkModifierLoop(c *check.C) {
+       t := s.Init(c)
+
+       autofixTest := func(before, after string, autofix bool) {
+               mklines := t.NewMkLines("filename.mk",
+                       MkCvsID,
+                       "VAR=\t"+before)
+               mklines.ForEach(func(mkline *MkLine) {
+                       mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) {
+                               ck := NewMkVarUseChecker(varUse, nil, mkline)
+                               ck.checkModifiers()
+                       })
+               })
+               if autofix {
+                       t.CheckEquals(mklines.mklines[1].Text, "VAR=\t"+after)
+               }
+       }
+
+       test := func(before, after string, diagnostics ...string) {
+               t.ExpectDiagnosticsAutofix(
+                       func(autofix bool) { autofixTest(before, after, autofix) },
+                       diagnostics...)
+       }
+
+       test("${VAR:@l@-l${l}@}", "${VAR:S,^,-l,}",
+               "NOTE: filename.mk:2: The modifier \"@l@-l${l}@\" "+
+                       "can be replaced with the simpler \"S,^,-l,\".",
+               "AUTOFIX: filename.mk:2: Replacing \"@l@-l${l}@\" with \"S,^,-l,\".")
+
+       // The comma is used in the :S modifier as the separator,
+       // therefore the modifier is left as-is.
+       test("${VAR:@word@-Wl,${word}@}", "${VAR:@word@-Wl,${word}@}",
+               nil...)
+
+       test("${VAR:@l@${l}suffix@}", "${VAR:=suffix}",
+               "NOTE: filename.mk:2: The modifier \"@l@${l}suffix@\" "+
+                       "can be replaced with the simpler \"=suffix\".",
+               "AUTOFIX: filename.mk:2: Replacing \"@l@${l}suffix@\" with \"=suffix\".")
+
+       // Escaping the colon is not yet supported.
+       test("${VAR:@word@${word}: suffix@}", "${VAR:@word@${word}: suffix@}",
+               nil...)
+
+       // The loop variable must be mentioned exactly once.
+       test("${VAR:@var@${var}${var}@}", "${VAR:@var@${var}${var}@}",
+               nil...)
+
+       // Other variables are fine though.
+       test("${VAR:@var@${var}${OTHER}@}", "${VAR:=${OTHER}}",
+               "NOTE: filename.mk:2: The modifier \"@var@${var}${OTHER}@\" "+
+                       "can be replaced with the simpler \"=${OTHER}\".",
+               "AUTOFIX: filename.mk:2: Replacing \"@var@${var}${OTHER}@\" with \"=${OTHER}\".")
+
+       // If the loop variable has modifiers, the :@var@ is probably appropriate.
+       test("${VAR:@var@${var:Q}@}", "${VAR:@var@${var:Q}@}",
+               nil...)
+
+       test("${VAR:@p@${p}) continue;; @}", "${VAR:=) continue;; }",
+               "NOTE: filename.mk:2: The modifier \"@p@${p}) continue;; @\" "+
+                       "can be replaced with the simpler \"=) continue;; \".",
+               "AUTOFIX: filename.mk:2: Replacing \"@p@${p}) continue;; @\" with \"=) continue;; \".")
+}
+
 func (s *Suite) Test_MkVarUseChecker_checkVarname(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.94 pkgsrc/pkgtools/pkglint/files/package.go:1.95
--- pkgsrc/pkgtools/pkglint/files/package.go:1.94       Fri Jun 12 19:14:45 2020
+++ pkgsrc/pkgtools/pkglint/files/package.go    Wed Jul  1 13:17:41 2020
@@ -55,8 +55,6 @@ type Package struct {
        // TODO: Include files with multiple-inclusion guard only once.
        //
        // TODO: Include files without multiple-inclusion guard as often as needed.
-       //
-       // TODO: Set an upper limit, to prevent denial of service.
        included Once
 
        // Does the package have any .includes?
@@ -540,7 +538,7 @@ func (pkg *Package) loadPlistDirs(plistF
        }
        for _, plistLine := range plistLines {
                if plistLine.HasPath() {
-                       rank := NewPlistRank(plistLine.Basename)
+                       rank := NewPlistRank(plistLine.Line.Basename)
                        pkg.PlistLines.Add(plistLine, rank)
                }
                for _, cond := range plistLine.conditions {
@@ -661,15 +659,9 @@ func (pkg *Package) checkfilePackageMake
 
        pkg.checkDistinfoExists()
 
-       // TODO: There are other REPLACE_* variables which are probably also affected by NO_CONFIGURE.
-       vars := pkg.vars
-       if noConfigureLine := vars.FirstDefinition("NO_CONFIGURE"); noConfigureLine != nil {
-               if replacePerlLine := vars.FirstDefinition("REPLACE_PERL"); replacePerlLine != nil {
-                       replacePerlLine.Warnf("REPLACE_PERL is ignored when NO_CONFIGURE is set (in %s).",
-                               replacePerlLine.RelMkLine(noConfigureLine))
-               }
-       }
+       pkg.checkReplaceInterpreter()
 
+       vars := pkg.vars
        if !vars.IsDefined("LICENSE") && !vars.IsDefined("META_PACKAGE") {
                line := NewLineWhole(filename)
                line.Errorf("Each package must define its LICENSE.")
@@ -742,6 +734,34 @@ func (pkg *Package) checkfilePackageMake
        SaveAutofixChanges(mklines.lines)
 }
 
+func (pkg *Package) checkReplaceInterpreter() {
+       vars := pkg.vars
+       noConfigureLine := vars.FirstDefinition("NO_CONFIGURE")
+       if noConfigureLine == nil {
+               return
+       }
+
+       // See mk/configure/replace-interpreter.mk.
+       varnames := [...]string{
+               "REPLACE_AWK",
+               "REPLACE_BASH",
+               "REPLACE_CSH",
+               "REPLACE_KSH",
+               "REPLACE_PERL",
+               "REPLACE_PERL6",
+               "REPLACE_SH",
+               "REPLACE_INTERPRETER"}
+
+       for _, varname := range varnames {
+               mkline := vars.FirstDefinition(varname)
+               if mkline == nil {
+                       continue
+               }
+               mkline.Warnf("%s is ignored when NO_CONFIGURE is set (in %s).",
+                       varname, mkline.RelMkLine(noConfigureLine))
+       }
+}
+
 func (pkg *Package) checkDistinfoExists() {
        vars := pkg.vars
 
@@ -1176,14 +1196,7 @@ func (pkg *Package) determineEffectivePk
                }
        }
 
-       if pkgnameLine != nil && (pkgname == distname || pkgname == "${DISTNAME}") {
-               if !pkgnameLine.HasComment() {
-                       pkgnameLine.Notef("This assignment is probably redundant " +
-                               "since PKGNAME is ${DISTNAME} by default.")
-                       pkgnameLine.Explain(
-                               "To mark this assignment as necessary, add a comment to the end of this line.")
-               }
-       }
+       pkg.checkPkgnameRedundant(pkgnameLine, pkgname, distname)
 
        if pkgname == "" && distnameLine != nil && !containsVarUse(distname) && !matches(distname, rePkgname) {
                distnameLine.Warnf("As DISTNAME is not a valid package name, please define the PKGNAME explicitly.")
@@ -1219,6 +1232,23 @@ func (pkg *Package) determineEffectivePk
        }
 }
 
+func (pkg *Package) checkPkgnameRedundant(pkgnameLine *MkLine, pkgname string, distname string) {
+       if pkgnameLine == nil || pkgnameLine.HasComment() {
+               return
+       }
+       if pkgname != distname && pkgname != "${DISTNAME}" {
+               return
+       }
+       pkgnameInfo := pkg.redundant.vars["PKGNAME"]
+       if len(pkgnameInfo.vari.WriteLocations()) >= 2 {
+               return
+       }
+       pkgnameLine.Notef("This assignment is probably redundant " +
+               "since PKGNAME is ${DISTNAME} by default.")
+       pkgnameLine.Explain(
+               "To mark this assignment as necessary, add a comment to the end of this line.")
+}
+
 // nbPart determines the smallest part of the package version number,
 // typically "nb13" or an empty string.
 //
@@ -1253,7 +1283,7 @@ func (pkg *Package) pkgnameFromDistname(
                                        newDistname = strings.ToLower(newDistname)
                                } else if ok, subst := mod.Subst(newDistname); ok {
                                        if subst == newDistname && !containsVarUse(subst) {
-                                               diag.Notef("The modifier :%s does not have an effect.", mod.Text)
+                                               diag.Notef("The modifier :%s does not have an effect.", mod.String())
                                        }
                                        newDistname = subst
                                } else {

Index: pkgsrc/pkgtools/pkglint/files/package_test.go
diff -u pkgsrc/pkgtools/pkglint/files/package_test.go:1.79 pkgsrc/pkgtools/pkglint/files/package_test.go:1.80
--- pkgsrc/pkgtools/pkglint/files/package_test.go:1.79  Sun Jun  7 15:49:23 2020
+++ pkgsrc/pkgtools/pkglint/files/package_test.go       Wed Jul  1 13:17:41 2020
@@ -2847,6 +2847,46 @@ func (s *Suite) Test_Package_determineEf
                "1 warning found.")
 }
 
+// The infrastructure file mk/haskell.mk sets a default for PKGNAME
+// that differs from the plain DISTNAME. This makes the assignment
+// PKGNAME=${DISTNAME} non-redundant.
+func (s *Suite) Test_Package_determineEffectivePkgVars__Haskell(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "PKGNAME=\t${DISTNAME}",
+               ".include \"../../mk/haskell.mk\"")
+       t.CreateFileLines("mk/haskell.mk",
+               MkCvsID,
+               "PKGNAME?=\ths-${DISTNAME}")
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package"))
+
+       // Up to 2020-06-28, pkglint wrongly produced a note about
+       // PKGNAME being "probably redundant".
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Package_determineEffectivePkgVars__bsd_pkg_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("category/package",
+               "PKGNAME=\t${DISTNAME}")
+       t.CreateFileLines("mk/bsd.pkg.mk",
+               MkCvsID,
+               "PKGNAME?=\t${DISTNAME}")
+       t.FinishSetUp()
+
+       G.Check(t.File("category/package"))
+
+       // Contrary to the one from mk/haskell.mk, the default assignment in
+       // mk/bsd.pkg.mk is not included in the RedundantScope.
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:4: This assignment is probably " +
+                       "redundant since PKGNAME is ${DISTNAME} by default.")
+}
+
 func (s *Suite) Test_Package_nbPart(c *check.C) {
        t := s.Init(c)
 
@@ -2874,7 +2914,10 @@ func (s *Suite) Test_Package_pkgnameFrom
                }
 
                pkg := NewPackage(t.File("category/package"))
-               pkg.loadPackageMakefile()
+               _, allLines := pkg.loadPackageMakefile()
+               pkg.redundant = NewRedundantScope() // See Package.checkfilePackageMakefile.
+               pkg.redundant.IsRelevant = func(mkline *MkLine) bool { return false }
+               pkg.redundant.Check(allLines)
                pkg.determineEffectivePkgVars()
                t.CheckEquals(pkg.EffectivePkgname, expectedPkgname)
                t.CheckOutput(diagnostics)

Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.66 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.67
--- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.66  Sat Jun  6 20:42:56 2020
+++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go       Wed Jul  1 13:17:41 2020
@@ -1475,6 +1475,14 @@ func (s *Suite) Test_InterPackage_Bl3__s
        G.Check("category/package2")
 
        t.CheckOutputLines(
-               "ERROR: category/package2/buildlink3.mk:3: Duplicate package identifier " +
+               "NOTE: category/package1/Makefile:4: The modifier \"@v@${v}@\" "+
+                       "can be replaced with the simpler \"S,^,,\".",
+               "NOTE: category/package1/Makefile:4: The modifier \"@v@${v}@\" "+
+                       "can be replaced with the simpler \"=\".",
+               "NOTE: category/package2/Makefile:4: The modifier \"@v@${v}@\" "+
+                       "can be replaced with the simpler \"S,^,,\".",
+               "NOTE: category/package2/Makefile:4: The modifier \"@v@${v}@\" "+
+                       "can be replaced with the simpler \"=\".",
+               "ERROR: category/package2/buildlink3.mk:3: Duplicate package identifier "+
                        "\"package1\" already appeared in ../../category/package1/buildlink3.mk:3.")
 }

Index: pkgsrc/pkgtools/pkglint/files/plist.go
diff -u pkgsrc/pkgtools/pkglint/files/plist.go:1.59 pkgsrc/pkgtools/pkglint/files/plist.go:1.60
--- pkgsrc/pkgtools/pkglint/files/plist.go:1.59 Mon Jun  1 20:49:54 2020
+++ pkgsrc/pkgtools/pkglint/files/plist.go      Wed Jul  1 13:17:41 2020
@@ -156,7 +156,7 @@ func (ck *PlistChecker) checkLine(pline 
 
        } else if m, cmd, arg := match2(text, `^@([a-z-]+)[\t ]*(.*)`); m {
                pline.CheckDirective(cmd, arg)
-               if cmd == "comment" && pline.Location.lineno > 1 {
+               if cmd == "comment" && pline.Line.Location.lineno > 1 {
                        ck.nonAsciiAllowed = true
                }
 
@@ -407,7 +407,7 @@ func (ck *PlistChecker) checkPathMan(pli
                        "configured by the pkgsrc user.",
                        "Compression and decompression takes place automatically,",
                        "no matter if the .gz extension is mentioned in the PLIST or not.")
-               fix.ReplaceAt(0, len(pline.Text)-len(".gz"), ".gz", "")
+               fix.ReplaceAt(0, len(pline.Line.Text)-len(".gz"), ".gz", "")
                fix.Apply()
        }
 }
@@ -528,12 +528,27 @@ func (ck *PlistChecker) checkOmf(plines 
 }
 
 type PlistLine struct {
-       *Line
+       Line *Line
        // XXX: Why "PLIST.docs" and not simply "docs"?
        conditions []string // e.g. PLIST.docs
        text       string   // Line.Text without any conditions of the form ${PLIST.cond}
 }
 
+func (pline *PlistLine) Autofix() *Autofix { return pline.Line.Autofix() }
+
+func (pline *PlistLine) Errorf(format string, args ...interface{}) {
+       pline.Line.Errorf(format, args...)
+}
+func (pline *PlistLine) Warnf(format string, args ...interface{}) {
+       pline.Line.Warnf(format, args...)
+}
+func (pline *PlistLine) Explain(explanation ...string) {
+       pline.Line.Explain(explanation...)
+}
+func (pline *PlistLine) RelLine(other *Line) string {
+       return pline.Line.RelLine(other)
+}
+
 func (pline *PlistLine) HasPath() bool {
        return pline.text != "" && plistLineStart.Contains(pline.text[0])
 }

Index: pkgsrc/pkgtools/pkglint/files/plist_test.go
diff -u pkgsrc/pkgtools/pkglint/files/plist_test.go:1.50 pkgsrc/pkgtools/pkglint/files/plist_test.go:1.51
--- pkgsrc/pkgtools/pkglint/files/plist_test.go:1.50    Sun Jun  7 15:49:23 2020
+++ pkgsrc/pkgtools/pkglint/files/plist_test.go Wed Jul  1 13:17:41 2020
@@ -153,9 +153,25 @@ func (s *Suite) Test_CheckLinesPlist__so
 func (s *Suite) Test_CheckLinesPlist__sort_common(c *check.C) {
        t := s.Init(c)
 
-       // TODO: Examine what happens if there is a PLIST.common to be sorted.
+       t.SetUpPackage("category/package")
+       t.Chdir("category/package")
+       t.CreateFileLines("PLIST.common",
+               PlistCvsID,
+               "bin/common2",
+               "bin/common1")
+       t.CreateFileLines("PLIST.common_end",
+               PlistCvsID,
+               "bin/common_end2",
+               "bin/common_end1")
+       t.FinishSetUp()
 
-       t.CheckOutputEmpty()
+       G.Check(".")
+
+       t.CheckOutputLines(
+               "WARN: PLIST.common:3: \"bin/common1\" "+
+                       "should be sorted before \"bin/common2\".",
+               "WARN: PLIST.common_end:3: \"bin/common_end1\" "+
+                       "should be sorted before \"bin/common_end2\".")
 }
 
 func (s *Suite) Test_PlistChecker__autofix(c *check.C) {
@@ -442,10 +458,10 @@ func (s *Suite) Test_PlistChecker_Load__
        // the package being checked, for cross-validation.
        t.Check(ck.allFiles["bin/plist"], check.IsNil)
        t.CheckEquals(
-               ck.allFiles["bin/plist_common"].String(),
+               ck.allFiles["bin/plist_common"].Line.String(),
                "PLIST.common:2: bin/plist_common")
        t.CheckEquals(
-               ck.allFiles["bin/plist_common_end"].String(),
+               ck.allFiles["bin/plist_common_end"].Line.String(),
                "PLIST.common_end:2: bin/plist_common_end")
 }
 
@@ -1239,6 +1255,77 @@ func (s *Suite) Test_PlistChecker_checkO
                nil...)
 }
 
+func (s *Suite) Test_PlistLine_Autofix(c *check.C) {
+       t := s.Init(c)
+
+       line := t.NewLine("PLIST", 2, "bin/program")
+       pline := PlistLine{line, nil, line.Text}
+
+       fix := pline.Autofix()
+       fix.Warnf("Warning %s.", "message")
+       fix.Replace("program", "new-name")
+       fix.Apply()
+
+       t.CheckOutputLines(
+               "WARN: PLIST:2: Warning message.")
+}
+
+func (s *Suite) Test_PlistLine_Errorf(c *check.C) {
+       t := s.Init(c)
+
+       line := t.NewLine("PLIST", 2, "bin/program")
+       pline := PlistLine{line, nil, line.Text}
+
+       pline.Errorf("Error %s.", "message")
+
+       t.CheckOutputLines(
+               "ERROR: PLIST:2: Error message.")
+}
+
+func (s *Suite) Test_PlistLine_Warnf(c *check.C) {
+       t := s.Init(c)
+
+       line := t.NewLine("PLIST", 2, "bin/program")
+       pline := PlistLine{line, nil, line.Text}
+
+       pline.Warnf("Warning %s.", "message")
+
+       t.CheckOutputLines(
+               "WARN: PLIST:2: Warning message.")
+}
+
+func (s *Suite) Test_PlistLine_Explain(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("--explain")
+       line := t.NewLine("PLIST", 2, "bin/program")
+       pline := PlistLine{line, nil, line.Text}
+
+       pline.Warnf("Warning %s.", "message")
+       pline.Explain(
+               "Line 1.",
+               "Line 2.")
+
+       t.CheckOutputLines(
+               "WARN: PLIST:2: Warning message.",
+               "",
+               "\tLine 1. Line 2.",
+               "")
+}
+
+func (s *Suite) Test_PlistLine_RelLine(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("--explain")
+       line := t.NewLine("PLIST", 2, "bin/program")
+       pline := PlistLine{line, nil, line.Text}
+
+       pline.Warnf("Warning, see %s.", line.RelLine(line))
+
+       t.CheckOutputLines(
+               "WARN: PLIST:2: Warning, see line 2.")
+}
+
 func (s *Suite) Test_PlistLine_HasPath(c *check.C) {
        t := s.Init(c)
 
@@ -1507,12 +1594,12 @@ func (s *Suite) Test_PlistLines_Add(c *c
 
        for _, line := range plistLines {
                if line.HasPath() {
-                       lines.Add(line, NewPlistRank(line.Basename))
+                       lines.Add(line, NewPlistRank(line.Line.Basename))
                }
        }
        for _, line := range plistCommonLines {
                if line.HasPath() {
-                       lines.Add(line, NewPlistRank(line.Basename))
+                       lines.Add(line, NewPlistRank(line.Line.Basename))
                }
        }
 

Index: pkgsrc/pkgtools/pkglint/files/shell.go
diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.63 pkgsrc/pkgtools/pkglint/files/shell.go:1.64
--- pkgsrc/pkgtools/pkglint/files/shell.go:1.63 Sun Jun  7 15:49:23 2020
+++ pkgsrc/pkgtools/pkglint/files/shell.go      Wed Jul  1 13:17:41 2020
@@ -278,7 +278,7 @@ func (scc *SimpleCommandChecker) checkAu
                autoMkdirs := false
                if scc.mklines.pkg != nil {
                        plistLine := scc.mklines.pkg.Plist.Dirs[prefixRel]
-                       if plistLine != nil && !containsVarUse(plistLine.Text) {
+                       if plistLine != nil && !containsVarUse(plistLine.Line.Text) {
                                autoMkdirs = true
                        }
                }

Index: pkgsrc/pkgtools/pkglint/files/shell.y
diff -u pkgsrc/pkgtools/pkglint/files/shell.y:1.6 pkgsrc/pkgtools/pkglint/files/shell.y:1.7
--- pkgsrc/pkgtools/pkglint/files/shell.y:1.6   Fri Oct 11 23:30:02 2019
+++ pkgsrc/pkgtools/pkglint/files/shell.y       Wed Jul  1 13:17:41 2020
@@ -224,6 +224,7 @@ case_item : case_selector compound_list 
        $$ = &MkShCaseItem{$1, $2, sepNone, nil}
 }
 case_item : tkWORD {
+       // Special case for ${SKIP:@p@ ${p}) continue;; @}
        $$ = &MkShCaseItem{Var: $1}
 }
 

Index: pkgsrc/pkgtools/pkglint/files/shell_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shell_test.go:1.69 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.70
--- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.69    Fri Jun 12 19:14:45 2020
+++ pkgsrc/pkgtools/pkglint/files/shell_test.go Wed Jul  1 13:17:41 2020
@@ -5,6 +5,33 @@ import (
        "strings"
 )
 
+func (s *Suite) Test_SimpleCommandChecker__case_continue_with_loop(c *check.C) {
+       t := s.Init(c)
+
+       code := "case $$fname in ${CHECK_PORTABILITY_SKIP:@p@${p}) continue;; @} esac"
+       line := t.NewLine("filename.mk", 123, "\t"+code)
+
+       program, err := parseShellProgram(line, code)
+       assertNil(err, "parse error")
+       t.CheckEquals(
+               program.AndOrs[0].Pipes[0].Cmds[0].Compound.Case.Cases[0].Var.MkText,
+               "${CHECK_PORTABILITY_SKIP:@p@${p}) continue;; @}")
+}
+
+func (s *Suite) Test_SimpleCommandChecker__case_continue_with_suffix(c *check.C) {
+       t := s.Init(c)
+
+       code := "case $$fname in ${CHECK_PORTABILITY_SKIP:=) continue;; } esac"
+       line := t.NewLine("filename.mk", 123, "\t"+code)
+
+       program, err := parseShellProgram(line, code)
+       assertNil(err, "parse error: parse error at []string{\"esac\"}")
+
+       t.CheckEquals(
+               program.AndOrs[0].Pipes[0].Cmds[0].Compound.Case.Cases[0].Var.MkText,
+               "${CHECK_PORTABILITY_SKIP:=) continue;; }")
+}
+
 // When pkglint is called without -Wextra, the check for unknown shell
 // commands is disabled, as it is still unreliable. As of December 2019
 // there are around 500 warnings in pkgsrc, and several of them are wrong.

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.92 pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.93
--- pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.92  Sun Jun  7 15:49:23 2020
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck.go       Wed Jul  1 13:17:41 2020
@@ -179,7 +179,7 @@ func (cv *VartypeCheck) Category() {
                "gnome", "gnustep",
                "japanese", "java",
                "kde", "korean",
-               "linux", "local",
+               "linux", "local", "lua",
                "perl5", "plan9", "python",
                "R", "ruby",
                "scm",
@@ -580,11 +580,11 @@ func (cv *VartypeCheck) FetchURL() {
                        }
                }
 
-               if len(varUse.modifiers) != 1 || !hasPrefix(varUse.modifiers[0].Text, "=") {
+               if len(varUse.modifiers) != 1 || !varUse.modifiers[0].HasPrefix("=") {
                        continue
                }
 
-               subdir := varUse.modifiers[0].Text[1:]
+               subdir := varUse.modifiers[0].String()[1:]
                if !hasSuffix(subdir, "/") {
                        cv.Errorf("The subdirectory in %s must end with a slash.", name)
                }
@@ -1274,7 +1274,7 @@ func (cv *VartypeCheck) SedCommands() {
                        }
 
                        // The :C modifier is similar enough for parsing.
-                       ok, _, from, _, _ := MkVarUseModifier{"C" + command[1:]}.MatchSubst()
+                       ok, _, from, _, _ := MkVarUseModifier("C" + command[1:]).MatchSubst()
                        if !ok {
                                return
                        }

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.84 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.85
--- pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.84     Fri Jun 12 19:14:45 2020
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go  Wed Jul  1 13:17:41 2020
@@ -241,6 +241,7 @@ func (s *Suite) Test_VartypeCheck_Catego
                "korean",
                "linux",
                "local",
+               "lua",
                "plan9",
                "R",
                "ruby",



Home | Main Index | Thread Index | Old Index