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 23 23:35:56 UTC 2019

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile PLIST
        pkgsrc/pkgtools/pkglint/files: alternatives.go autofix.go
            autofix_test.go buildlink3.go buildlink3_test.go category.go
            category_test.go check_test.go distinfo.go files.go fuzzer_test.go
            licenses.go licenses_test.go line.go linelexer.go lines.go
            logging.go mkline.go mkline_test.go mklinechecker.go
            mklinechecker_test.go mklineparser.go mklineparser_test.go
            mklines.go mklines_test.go mkparser.go package.go package_test.go
            patches.go pkglint.go pkglint_test.go pkgsrc.go pkgsrc_test.go
            plist.go redundantscope.go redundantscope_test.go shell.go
            shell_test.go shtokenizer.go testnames_test.go tools.go
            tools_test.go toplevel.go util.go util_test.go vardefs.go
            vardefs_test.go vartypecheck.go vartypecheck_test.go
        pkgsrc/pkgtools/pkglint/files/textproc: lexer.go
        pkgsrc/pkgtools/pkglint/files/trace: tracing.go

Log Message:
pkgtools/pkglint: update to 19.3.10

Changes since 19.3.9:

In diagnostics for suggested package updates, the exact line of doc/TODO
is mentioned. If a suggested update has an additional comment, the
brackets around that comment are not output anymore.

The check for defined but not used variables has been improved for the
edge case of defining a variable in the package Makefile and using it
in the buildlink3.mk file of the same package, which just doesn't work.

Makefile fragments in patches/ directories are now completely ignored.
It was a hypothetical case anyway.

Comparing PKGSRC_COMPILER using the == or != operators is now considered
an error instead of a warning. The common cases can be autofixed.


To generate a diff of this commit:
cvs rdiff -u -r1.609 -r1.610 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.16 -r1.17 pkgsrc/pkgtools/pkglint/PLIST
cvs rdiff -u -r1.16 -r1.17 pkgsrc/pkgtools/pkglint/files/alternatives.go
cvs rdiff -u -r1.29 -r1.30 pkgsrc/pkgtools/pkglint/files/autofix.go \
    pkgsrc/pkgtools/pkglint/files/logging.go
cvs rdiff -u -r1.30 -r1.31 pkgsrc/pkgtools/pkglint/files/autofix_test.go
cvs rdiff -u -r1.25 -r1.26 pkgsrc/pkgtools/pkglint/files/buildlink3.go
cvs rdiff -u -r1.35 -r1.36 pkgsrc/pkgtools/pkglint/files/buildlink3_test.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.24 -r1.25 pkgsrc/pkgtools/pkglint/files/category.go \
    pkgsrc/pkgtools/pkglint/files/licenses_test.go
cvs rdiff -u -r1.26 -r1.27 pkgsrc/pkgtools/pkglint/files/category_test.go \
    pkgsrc/pkgtools/pkglint/files/licenses.go
cvs rdiff -u -r1.54 -r1.55 pkgsrc/pkgtools/pkglint/files/check_test.go
cvs rdiff -u -r1.36 -r1.37 pkgsrc/pkgtools/pkglint/files/distinfo.go
cvs rdiff -u -r1.27 -r1.28 pkgsrc/pkgtools/pkglint/files/files.go
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/fuzzer_test.go
cvs rdiff -u -r1.38 -r1.39 pkgsrc/pkgtools/pkglint/files/line.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/linelexer.go
cvs rdiff -u -r1.9 -r1.10 pkgsrc/pkgtools/pkglint/files/lines.go
cvs rdiff -u -r1.63 -r1.64 pkgsrc/pkgtools/pkglint/files/mkline.go
cvs rdiff -u -r1.71 -r1.72 pkgsrc/pkgtools/pkglint/files/mkline_test.go
cvs rdiff -u -r1.53 -r1.54 pkgsrc/pkgtools/pkglint/files/mklinechecker.go
cvs rdiff -u -r1.48 -r1.49 \
    pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go \
    pkgsrc/pkgtools/pkglint/files/shell.go
cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/mklineparser.go \
    pkgsrc/pkgtools/pkglint/files/mklineparser_test.go
cvs rdiff -u -r1.59 -r1.60 pkgsrc/pkgtools/pkglint/files/mklines.go \
    pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
cvs rdiff -u -r1.51 -r1.52 pkgsrc/pkgtools/pkglint/files/mklines_test.go
cvs rdiff -u -r1.68 -r1.69 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.58 -r1.59 pkgsrc/pkgtools/pkglint/files/package_test.go \
    pkgsrc/pkgtools/pkglint/files/util.go
cvs rdiff -u -r1.32 -r1.33 pkgsrc/pkgtools/pkglint/files/patches.go
cvs rdiff -u -r1.64 -r1.65 pkgsrc/pkgtools/pkglint/files/pkglint.go
cvs rdiff -u -r1.49 -r1.50 pkgsrc/pkgtools/pkglint/files/pkglint_test.go
cvs rdiff -u -r1.41 -r1.42 pkgsrc/pkgtools/pkglint/files/pkgsrc.go
cvs rdiff -u -r1.44 -r1.45 pkgsrc/pkgtools/pkglint/files/plist.go
cvs rdiff -u -r1.7 -r1.8 pkgsrc/pkgtools/pkglint/files/redundantscope.go \
    pkgsrc/pkgtools/pkglint/files/redundantscope_test.go
cvs rdiff -u -r1.56 -r1.57 pkgsrc/pkgtools/pkglint/files/shell_test.go
cvs rdiff -u -r1.19 -r1.20 pkgsrc/pkgtools/pkglint/files/shtokenizer.go \
    pkgsrc/pkgtools/pkglint/files/tools.go
cvs rdiff -u -r1.8 -r1.9 pkgsrc/pkgtools/pkglint/files/testnames_test.go
cvs rdiff -u -r1.21 -r1.22 pkgsrc/pkgtools/pkglint/files/tools_test.go
cvs rdiff -u -r1.23 -r1.24 pkgsrc/pkgtools/pkglint/files/toplevel.go \
    pkgsrc/pkgtools/pkglint/files/vardefs_test.go
cvs rdiff -u -r1.77 -r1.78 pkgsrc/pkgtools/pkglint/files/vardefs.go
cvs rdiff -u -r1.66 -r1.67 pkgsrc/pkgtools/pkglint/files/vartypecheck.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/textproc/lexer.go
cvs rdiff -u -r1.10 -r1.11 pkgsrc/pkgtools/pkglint/files/trace/tracing.go

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

Modified files:

Index: pkgsrc/pkgtools/pkglint/Makefile
diff -u pkgsrc/pkgtools/pkglint/Makefile:1.609 pkgsrc/pkgtools/pkglint/Makefile:1.610
--- pkgsrc/pkgtools/pkglint/Makefile:1.609      Tue Nov 19 06:51:38 2019
+++ pkgsrc/pkgtools/pkglint/Makefile    Sat Nov 23 23:35:55 2019
@@ -1,6 +1,6 @@
-# $NetBSD: Makefile,v 1.609 2019/11/19 06:51:38 rillig Exp $
+# $NetBSD: Makefile,v 1.610 2019/11/23 23:35:55 rillig Exp $
 
-PKGNAME=       pkglint-19.3.9
+PKGNAME=       pkglint-19.3.10
 CATEGORIES=    pkgtools
 DISTNAME=      tools
 MASTER_SITES=  ${MASTER_SITE_GITHUB:=golang/}

Index: pkgsrc/pkgtools/pkglint/PLIST
diff -u pkgsrc/pkgtools/pkglint/PLIST:1.16 pkgsrc/pkgtools/pkglint/PLIST:1.17
--- pkgsrc/pkgtools/pkglint/PLIST:1.16  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/PLIST       Sat Nov 23 23:35:55 2019
@@ -1,4 +1,4 @@
-@comment $NetBSD: PLIST,v 1.16 2019/11/17 01:26:25 rillig Exp $
+@comment $NetBSD: PLIST,v 1.17 2019/11/23 23:35:55 rillig Exp $
 bin/pkglint
 gopkg/pkg/${GO_PLATFORM}/netbsd.org/pkglint.a
 gopkg/pkg/${GO_PLATFORM}/netbsd.org/pkglint/getopt.a
@@ -77,6 +77,8 @@ gopkg/src/netbsd.org/pkglint/paragraph.g
 gopkg/src/netbsd.org/pkglint/paragraph_test.go
 gopkg/src/netbsd.org/pkglint/patches.go
 gopkg/src/netbsd.org/pkglint/patches_test.go
+gopkg/src/netbsd.org/pkglint/path.go
+gopkg/src/netbsd.org/pkglint/path_test.go
 gopkg/src/netbsd.org/pkglint/pkglint.0
 gopkg/src/netbsd.org/pkglint/pkglint.1
 gopkg/src/netbsd.org/pkglint/pkglint.go

Index: pkgsrc/pkgtools/pkglint/files/alternatives.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives.go:1.16 pkgsrc/pkgtools/pkglint/files/alternatives.go:1.17
--- pkgsrc/pkgtools/pkglint/files/alternatives.go:1.16  Tue Nov 19 06:51:38 2019
+++ pkgsrc/pkgtools/pkglint/files/alternatives.go       Sat Nov 23 23:35:56 2019
@@ -5,7 +5,7 @@ import (
        "strings"
 )
 
-func CheckFileAlternatives(filename string) {
+func CheckFileAlternatives(filename Path) {
        lines := Load(filename, NotEmpty|LogErrors)
        if lines == nil {
                return
@@ -16,7 +16,7 @@ func CheckFileAlternatives(filename stri
                plist = G.Pkg.Plist
        }
 
-       checkPlistWrapper := func(line *Line, wrapper string) {
+       checkPlistWrapper := func(line *Line, wrapper Path) {
                if plist.Files[wrapper] != nil {
                        line.Errorf("Alternative wrapper %q must not appear in the PLIST.", wrapper)
                }
@@ -25,10 +25,10 @@ func CheckFileAlternatives(filename stri
        checkPlistAlternative := func(line *Line, alternative string) {
                relImplementation := strings.Replace(alternative, "@PREFIX@/", "", 1)
                plistName := replaceAll(relImplementation, `@(\w+)@`, "${$1}")
-               if plist.Files[plistName] != nil || G.Pkg.vars.IsDefined("ALTERNATIVES_SRC") {
+               if plist.Files[NewPath(plistName)] != nil || G.Pkg.vars.IsDefined("ALTERNATIVES_SRC") {
                        return
                }
-               if plist.Files[strings.Replace(plistName, "${PKGMANDIR}", "man", 1)] != nil {
+               if plist.Files[NewPath(strings.Replace(plistName, "${PKGMANDIR}", "man", 1))] != nil {
                        return
                }
 
@@ -57,7 +57,7 @@ func CheckFileAlternatives(filename stri
                }
 
                if plist.Files != nil {
-                       checkPlistWrapper(line, wrapper)
+                       checkPlistWrapper(line, NewPath(wrapper))
                        checkPlistAlternative(line, alternative)
                }
 

Index: pkgsrc/pkgtools/pkglint/files/autofix.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix.go:1.29 pkgsrc/pkgtools/pkglint/files/autofix.go:1.30
--- pkgsrc/pkgtools/pkglint/files/autofix.go:1.29       Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix.go    Sat Nov 23 23:35:56 2019
@@ -1,10 +1,8 @@
 package pkglint
 
 import (
-       "io/ioutil"
        "netbsd.org/pkglint/regex"
        "os"
-       "path/filepath"
        "strconv"
        "strings"
 )
@@ -455,12 +453,12 @@ func SaveAutofixChanges(lines *Lines) (a
 
        if G.Testing {
                abs := abspath(lines.Filename)
-               absTmp := abspath(filepath.ToSlash(os.TempDir()))
-               assertf(hasPrefix(abs, absTmp), "%q must be inside %q", abs, absTmp)
+               absTmp := abspath(NewPathSlash(os.TempDir()))
+               assertf(abs.HasPrefixPath(absTmp), "%q must be inside %q", abs, absTmp)
        }
 
-       changes := make(map[string][]string)
-       changed := make(map[string]bool)
+       changes := make(map[Path][]string)
+       changed := make(map[Path]bool)
        for _, line := range lines.Lines {
                chlines := changes[line.Filename]
                if fix := line.autofix; fix != nil {
@@ -488,12 +486,12 @@ func SaveAutofixChanges(lines *Lines) (a
                for _, changedLine := range changedLines {
                        text.WriteString(changedLine)
                }
-               err := ioutil.WriteFile(tmpName, []byte(text.String()), 0666)
+               err := tmpName.WriteString(text.String())
                if err != nil {
                        G.Logger.Errorf(tmpName, "Cannot write: %s", err)
                        continue
                }
-               err = os.Rename(tmpName, filename)
+               err = tmpName.Rename(filename)
                if err != nil {
                        G.Logger.Errorf(tmpName, "Cannot overwrite with autofixed content: %s", err)
                        continue
Index: pkgsrc/pkgtools/pkglint/files/logging.go
diff -u pkgsrc/pkgtools/pkglint/files/logging.go:1.29 pkgsrc/pkgtools/pkglint/files/logging.go:1.30
--- pkgsrc/pkgtools/pkglint/files/logging.go:1.29       Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/logging.go    Sat Nov 23 23:35:56 2019
@@ -5,7 +5,6 @@ import (
        "io"
        "netbsd.org/pkglint/histogram"
        "netbsd.org/pkglint/textproc"
-       "path"
        "strings"
 )
 
@@ -126,12 +125,12 @@ func (l *Logger) Diag(line *Line, level 
        l.Logf(level, filename, linenos, format, msg)
 }
 
-func (l *Logger) FirstTime(filename, linenos, msg string) bool {
+func (l *Logger) FirstTime(filename Path, linenos, msg string) bool {
        if l.Opts.LogVerbose {
                return true
        }
 
-       if !l.logged.FirstTimeSlice(path.Clean(filename), linenos, msg) {
+       if !l.logged.FirstTimeSlice(filename.Clean().String(), linenos, msg) {
                l.suppressDiag = true
                l.suppressExpl = true
                return false
@@ -230,7 +229,7 @@ func (l *Logger) showSource(line *Line) 
 // IsAutofix returns whether one of the --show-autofix or --autofix options is active.
 func (l *Logger) IsAutofix() bool { return l.Opts.Autofix || l.Opts.ShowAutofix }
 
-func (l *Logger) Logf(level *LogLevel, filename, lineno, format, msg string) {
+func (l *Logger) Logf(level *LogLevel, filename Path, lineno, format, msg string) {
        if l.suppressDiag {
                l.suppressDiag = false
                return
@@ -290,7 +289,7 @@ func (l *Logger) Logf(level *LogLevel, f
 // Location.Filename. It may be followed by the usual ":123" for line numbers.
 //
 // For diagnostics, use Logf instead.
-func (l *Logger) Errorf(location string, format string, args ...interface{}) {
+func (l *Logger) Errorf(location Path, format string, args ...interface{}) {
        msg := sprintf(format, args...)
        var diag string
        if l.Opts.GccOutput {

Index: pkgsrc/pkgtools/pkglint/files/autofix_test.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.30 pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.31
--- pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.30  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/autofix_test.go       Sat Nov 23 23:35:56 2019
@@ -1282,7 +1282,7 @@ func (s *Suite) Test_SaveAutofixChanges_
        t.SetUpCommandLine("--autofix")
        lines := t.SetUpFileLines("subdir/file.txt",
                "line 1")
-       _ = os.RemoveAll(t.File("subdir"))
+       _ = os.RemoveAll(t.File("subdir").String())
 
        fix := lines.Lines[0].Autofix()
        fix.Warnf("Should start with an uppercase letter.")
@@ -1308,7 +1308,7 @@ func (s *Suite) Test_SaveAutofixChanges_
                "line 1")
 
        // As long as the file is kept open, it cannot be overwritten or deleted.
-       openFile, err := os.OpenFile(t.File("subdir/file.txt"), 0, 0666)
+       openFile, err := os.OpenFile(t.File("subdir/file.txt").String(), 0, 0666) // TODO: replace with Path.Open
        defer func() { assertNil(openFile.Close(), "") }()
        c.Check(err, check.IsNil)
 
@@ -1336,8 +1336,8 @@ func (s *Suite) Test_SaveAutofixChanges_
        lines := t.SetUpFileLines("file.txt",
                "line 1")
 
-       c.Check(os.RemoveAll(t.File("file.txt")), check.IsNil)
-       c.Check(os.MkdirAll(t.File("file.txt"), 0777), check.IsNil)
+       c.Check(os.RemoveAll(t.File("file.txt").String()), check.IsNil)
+       c.Check(os.MkdirAll(t.File("file.txt").String(), 0777), check.IsNil)
 
        fix := lines.Lines[0].Autofix()
        fix.Warnf("Should start with an uppercase letter.")

Index: pkgsrc/pkgtools/pkglint/files/buildlink3.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.25 pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.26
--- pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.25    Fri Oct 11 23:30:02 2019
+++ pkgsrc/pkgtools/pkglint/files/buildlink3.go Sat Nov 23 23:35:56 2019
@@ -2,7 +2,6 @@ package pkglint
 
 import (
        "netbsd.org/pkglint/pkgver"
-       "path"
        "strings"
 )
 
@@ -21,7 +20,7 @@ func CheckLinesBuildlink3Mk(mklines *MkL
 func (ck *Buildlink3Checker) Check() {
        mklines := ck.mklines
        if trace.Tracing {
-               defer trace.Call1(mklines.lines.Filename)()
+               defer trace.Call(mklines.lines.Filename)()
        }
 
        mklines.Check()
@@ -40,7 +39,7 @@ func (ck *Buildlink3Checker) Check() {
 
        if llex.SkipRegexp(`^BUILDLINK_DEPMETHOD\.([^\t ]+)\?=.*$`) {
                llex.PreviousLine().Warnf("This line belongs inside the .ifdef block.")
-               for llex.SkipString("") {
+               for llex.SkipText("") {
                }
        }
 
@@ -100,7 +99,7 @@ func (ck *Buildlink3Checker) checkUnique
                return
        }
 
-       base, name := trimCommon(pkgbase, path.Base(path.Dir(mkline.Filename)))
+       base, name := trimCommon(pkgbase, mkline.Filename.Dir().Base())
        if base == "" && matches(name, `^(\d*|-cvs|-fossil|-git|-hg|-svn|-devel|-snapshot)$`) {
                return
        }

Index: pkgsrc/pkgtools/pkglint/files/buildlink3_test.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.35 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.36
--- pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.35       Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/buildlink3_test.go    Sat Nov 23 23:35:56 2019
@@ -599,7 +599,7 @@ func (s *Suite) Test_Buildlink3Checker_c
 
        G.InterPackage.Enable()
 
-       test := func(pkgbase, pkgpath string, diagnostics ...string) {
+       test := func(pkgbase string, pkgpath Path, diagnostics ...string) {
                mkline := t.NewMkLine(t.File(pkgpath+"/buildlink3.mk"), 123, "")
 
                (*Buildlink3Checker).checkUniquePkgbase(nil, pkgbase, mkline)
Index: pkgsrc/pkgtools/pkglint/files/mkparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser.go:1.35 pkgsrc/pkgtools/pkglint/files/mkparser.go:1.36
--- pkgsrc/pkgtools/pkglint/files/mkparser.go:1.35      Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mkparser.go   Sat Nov 23 23:35:56 2019
@@ -410,6 +410,8 @@ func (p *MkParser) varUseModifierAt(lexe
 // MkCond parses a condition like ${OPSYS} == "NetBSD".
 //
 // See devel/bmake/files/cond.c.
+//
+// FIXME: Move over to MkTokensParser
 func (p *MkParser) MkCond() *MkCond {
        and := p.mkCondAnd()
        if and == nil {
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.35 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.36
--- pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.35   Tue Nov 19 06:51:38 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go        Sat Nov 23 23:35:56 2019
@@ -292,7 +292,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
                "\t\tdistfile directly from GitHub [rillig 2018-01-01]",
                "\tmk/bsd.pkg.mk: Another infrastructure change [rillig 2018-01-02]")
 
-       t.Main(t.File("category/package"))
+       t.Main("category/package")
 
        // For pkglint's purpose, the infrastructure entries are simply ignored
        // since they do not belong to a single package.
@@ -543,6 +543,7 @@ func (s *Suite) Test_Pkgsrc_parseSuggest
        t := s.Init(c)
 
        lines := t.NewLines("doc/TODO",
+               CvsID,
                "",
                "Suggested package updates",
                "==============",
@@ -556,8 +557,8 @@ func (s *Suite) Test_Pkgsrc_parseSuggest
        todo := G.Pkgsrc.parseSuggestedUpdates(lines)
 
        t.CheckDeepEquals(todo, []SuggestedUpdate{
-               {lines.Lines[5].Location, "CSP", "0.34", ""},
-               {lines.Lines[6].Location, "freeciv-client", "2.5.0", "(urgent)"}})
+               {lines.Lines[6].Location, "CSP", "0.34", ""},
+               {lines.Lines[7].Location, "freeciv-client", "2.5.0", "(urgent)"}})
 }
 
 func (s *Suite) Test_Pkgsrc_parseSuggestedUpdates__wip(c *check.C) {
@@ -577,7 +578,36 @@ func (s *Suite) Test_Pkgsrc_parseSuggest
 
        t.CheckOutputLines(
                "WARN: ~/wip/package/Makefile:3: " +
-                       "This package should be updated to 1.13 ([cool new features]).")
+                       "This package should be updated to 1.13 (cool new features; see ../../wip/TODO:5).")
+}
+
+func (s *Suite) Test_Pkgsrc_parseSuggestedUpdates__parse_errors(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.NewLines("doc/TODO",
+               "", // missing CvsID
+               "Suggested package updates",
+               "==============", // usually this line is a bit longer
+               "",
+               "", // usually there's only a single empty line
+               "\t"+"O wrong bullet",
+               "\t"+"o package-without-version",
+               "\t"+"o CSP-0.34",
+               "\t"+"o freeciv-client-2.5.0 (urgent)", // missing [brackets]
+               "\t"+"o mix-2.5.0 [urgent)",            // bracket + parenthesis
+               "",
+               "\t"+"o ignored-0.0")
+
+       todo := G.Pkgsrc.parseSuggestedUpdates(lines)
+
+       t.CheckDeepEquals(todo, []SuggestedUpdate{
+               {lines.Lines[7].Location, "CSP", "0.34", ""},
+               {lines.Lines[8].Location, "freeciv-client", "2.5.0", "(urgent)"},
+               {lines.Lines[9].Location, "mix", "2.5.0", "[urgent)"}})
+
+       t.CheckOutputLines(
+               "WARN: doc/TODO:6: Invalid line format \"\\tO wrong bullet\".",
+               "WARN: doc/TODO:7: Invalid package name \"package-without-version\".")
 }
 
 func (s *Suite) Test_Pkgsrc_loadTools(c *check.C) {
@@ -1025,12 +1055,12 @@ func (s *Suite) Test_Pkgsrc_VariableType
                "PKGSRC_MAKE_ENV?=\t# none",
                "CPPPATH?=\tcpp",
                "OSNAME.Linux?=\tLinux")
-       pkg := t.SetUpPackage("category/package",
+       t.SetUpPackage("category/package",
                "PKGSRC_MAKE_ENV+=\tCPP=${CPPPATH:Q}",
                "PKGSRC_UNKNOWN_ENV+=\tCPP=${ABCPATH:Q}",
                "OSNAME.SunOS=\t\t${OSNAME.Other}")
 
-       t.Main("-Wall", pkg)
+       t.Main("-Wall", "category/package")
 
        if typ := G.Pkgsrc.VariableType(nil, "PKGSRC_MAKE_ENV"); c.Check(typ, check.NotNil) {
                t.CheckEquals(typ.String(), "ShellWord (list, guessed)")
@@ -1110,7 +1140,7 @@ func (s *Suite) Test_Pkgsrc_checkTopleve
        t.SetUpPackage("category/package2",
                "LICENSE=\tmissing")
 
-       t.Main("-r", "-Cglobal", t.File("."))
+       t.Main("-r", "-Cglobal", ".")
 
        t.CheckOutputLines(
                "WARN: ~/category/package2/Makefile:11: License file ~/licenses/missing does not exist.",
@@ -1162,8 +1192,8 @@ func (s *Suite) Test_Change_Target(c *ch
        moved := Change{loc, Moved, "category/path", "category/other", "author", "2019-01-01"}
        downgraded := Change{loc, Downgraded, "category/path", "1.0", "author", "2019-01-01"}
 
-       t.CheckEquals(renamed.Target(), "category/other")
-       t.CheckEquals(moved.Target(), "category/other")
+       t.CheckEquals(renamed.Target(), NewPath("category/other"))
+       t.CheckEquals(moved.Target(), NewPath("category/other"))
        t.ExpectAssert(func() { downgraded.Target() })
 }
 
Index: pkgsrc/pkgtools/pkglint/files/util_test.go
diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.35 pkgsrc/pkgtools/pkglint/files/util_test.go:1.36
--- pkgsrc/pkgtools/pkglint/files/util_test.go:1.35     Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/util_test.go  Sat Nov 23 23:35:56 2019
@@ -99,12 +99,12 @@ func (s *Suite) Test_isEmptyDir__and_get
 
        if dir := t.File("."); true {
                t.CheckEquals(isEmptyDir(dir), true)
-               t.CheckDeepEquals(getSubdirs(dir), []string(nil))
+               t.CheckDeepEquals(getSubdirs(dir), []Path(nil))
 
                t.CreateFileLines("somedir/file")
 
                t.CheckEquals(isEmptyDir(dir), false)
-               t.CheckDeepEquals(getSubdirs(dir), []string{"somedir"})
+               t.CheckDeepEquals(getSubdirs(dir), []Path{"somedir"})
        }
 
        if absent := t.File("nonexistent"); true {
@@ -122,9 +122,9 @@ func (s *Suite) Test_getSubdirs(c *check
 
        t.CreateFileLines("subdir/file")
        t.CreateFileLines("empty/file")
-       c.Check(os.Remove(t.File("empty/file")), check.IsNil)
+       c.Check(os.Remove(t.File("empty/file").String()), check.IsNil)
 
-       t.CheckDeepEquals(getSubdirs(t.File(".")), []string{"subdir"})
+       t.CheckDeepEquals(getSubdirs(t.File(".")), []Path{"subdir"})
 }
 
 func (s *Suite) Test_isLocallyModified(c *check.C) {
@@ -133,10 +133,10 @@ func (s *Suite) Test_isLocallyModified(c
        unmodified := t.CreateFileLines("unmodified")
        modTime := time.Unix(1136239445, 0).UTC()
 
-       err := os.Chtimes(unmodified, modTime, modTime)
+       err := os.Chtimes(unmodified.String(), modTime, modTime)
        c.Check(err, check.IsNil)
 
-       st, err := os.Lstat(unmodified)
+       st, err := os.Lstat(unmodified.String())
        c.Check(err, check.IsNil)
 
        // Make sure that the file system has second precision and accuracy.
@@ -284,28 +284,6 @@ func (s *Suite) Test_varnameParam(c *che
        t.CheckEquals(varnameParam(".CURDIR"), "")
 }
 
-func (s *Suite) Test_fileExists(c *check.C) {
-       t := s.Init(c)
-
-       t.CreateFileLines("dir/file")
-
-       t.CheckEquals(fileExists(t.File("nonexistent")), false)
-       t.CheckEquals(fileExists(t.File("dir")), false)
-       t.CheckEquals(fileExists(t.File("dir/nonexistent")), false)
-       t.CheckEquals(fileExists(t.File("dir/file")), true)
-}
-
-func (s *Suite) Test_dirExists(c *check.C) {
-       t := s.Init(c)
-
-       t.CreateFileLines("dir/file")
-
-       t.CheckEquals(dirExists(t.File("nonexistent")), false)
-       t.CheckEquals(dirExists(t.File("dir")), true)
-       t.CheckEquals(dirExists(t.File("dir/nonexistent")), false)
-       t.CheckEquals(dirExists(t.File("dir/file")), false)
-}
-
 func (s *Suite) Test_mkopSubst__middle(c *check.C) {
        t := s.Init(c)
 
@@ -360,7 +338,7 @@ func (s *Suite) Test_relpath(c *check.C)
        t.Chdir(".")
        t.CheckEquals(G.Pkgsrc.topdir, t.tmpdir)
 
-       test := func(from, to, result string) {
+       test := func(from, to Path, result Path) {
                t.CheckEquals(relpath(from, to), result)
        }
 
@@ -396,7 +374,7 @@ func (s *Suite) Test_relpath(c *check.C)
 func (s *Suite) Test_relpath__quick(c *check.C) {
        t := s.Init(c)
 
-       test := func(from, to, result string) {
+       test := func(from, to Path, result Path) {
                t.CheckEquals(relpath(from, to), result)
        }
 
@@ -411,7 +389,7 @@ func (s *Suite) Test_relpath__quick(c *c
 func (s *Suite) Test_cleanpath(c *check.C) {
        t := s.Init(c)
 
-       test := func(from, to string) {
+       test := func(from, to Path) {
                t.CheckEquals(cleanpath(from), to)
        }
 
@@ -498,12 +476,12 @@ func (s *Suite) Test_pathContains(c *che
 func (s *Suite) Test_pathContainsDir(c *check.C) {
        t := s.Init(c)
 
-       test := func(haystack, needle string, expected bool) {
+       test := func(haystack, needle Path, expected bool) {
                actual := pathContainsDir(haystack, needle)
                t.CheckEquals(actual, expected)
        }
 
-       testPanic := func(haystack, needle string) {
+       testPanic := func(haystack, needle Path) {
                t.c.Check(
                        func() { _ = pathContainsDir(haystack, needle) },
                        check.PanicMatches,
@@ -850,15 +828,15 @@ func (s *Suite) Test_FileCache(c *check.
        c.Check(cache.Get("Makefile", MustSucceed|LogErrors), check.IsNil) // Wrong LoadOptions.
 
        linesFromCache := cache.Get("Makefile", 0)
-       t.CheckEquals(linesFromCache.Filename, "Makefile")
+       t.CheckEquals(linesFromCache.Filename, NewPath("Makefile"))
        c.Check(linesFromCache.Lines, check.HasLen, 2)
-       t.CheckEquals(linesFromCache.Lines[0].Filename, "Makefile")
+       t.CheckEquals(linesFromCache.Lines[0].Filename, NewPath("Makefile"))
 
        // Cache keys are normalized using path.Clean.
        linesFromCache2 := cache.Get("./Makefile", 0)
-       t.CheckEquals(linesFromCache2.Filename, "./Makefile")
+       t.CheckEquals(linesFromCache2.Filename, NewPath("./Makefile"))
        c.Check(linesFromCache2.Lines, check.HasLen, 2)
-       t.CheckEquals(linesFromCache2.Lines[0].Filename, "./Makefile")
+       t.CheckEquals(linesFromCache2.Lines[0].Filename, NewPath("./Makefile"))
 
        cache.Put("file1.mk", 0, lines)
        cache.Put("file2.mk", 0, lines)

Index: pkgsrc/pkgtools/pkglint/files/category.go
diff -u pkgsrc/pkgtools/pkglint/files/category.go:1.24 pkgsrc/pkgtools/pkglint/files/category.go:1.25
--- pkgsrc/pkgtools/pkglint/files/category.go:1.24      Fri Nov  1 19:56:53 2019
+++ pkgsrc/pkgtools/pkglint/files/category.go   Sat Nov 23 23:35:56 2019
@@ -6,12 +6,12 @@ import (
        "strings"
 )
 
-func CheckdirCategory(dir string) {
+func CheckdirCategory(dir Path) {
        if trace.Tracing {
-               defer trace.Call1(dir)()
+               defer trace.Call(dir)()
        }
 
-       mklines := LoadMk(dir+"/Makefile", NotEmpty|LogErrors)
+       mklines := LoadMk(dir.JoinNoClean("Makefile"), NotEmpty|LogErrors) // TODO: Remove the "./" here already
        if mklines == nil {
                return
        }
@@ -49,7 +49,7 @@ func CheckdirCategory(dir string) {
        mlex.SkipEmptyOrNote()
 
        type subdir struct {
-               name string
+               name Path
                line *MkLine
        }
 
@@ -67,7 +67,7 @@ func CheckdirCategory(dir string) {
                if (mkline.IsVarassignMaybeCommented()) && mkline.Varname() == "SUBDIR" {
                        mlex.Skip()
 
-                       name := mkline.Value()
+                       name := mkline.Value() // TODO: Maybe NewPath here already
                        if mkline.IsCommentedVarassign() && !mkline.HasComment() {
                                mkline.Warnf("%q commented out without giving a reason.", name)
                        }
@@ -78,12 +78,12 @@ func CheckdirCategory(dir string) {
                        seen[name] = mkline
 
                        if len(mSubdirs) > 0 {
-                               if prev := mSubdirs[len(mSubdirs)-1].name; name < prev {
+                               if prev := mSubdirs[len(mSubdirs)-1].name; name < prev.String() {
                                        mkline.Warnf("%q should come before %q.", name, prev)
                                }
                        }
 
-                       mSubdirs = append(mSubdirs, subdir{name, mkline})
+                       mSubdirs = append(mSubdirs, subdir{NewPath(name), mkline})
 
                } else {
                        if !mkline.IsEmpty() {
@@ -96,8 +96,8 @@ func CheckdirCategory(dir string) {
        // To prevent unnecessary warnings about subdirectories that are
        // in one list but not in the other, generate the sets of
        // subdirs of each list.
-       fCheck := make(map[string]bool)
-       mCheck := make(map[string]bool)
+       fCheck := make(map[Path]bool)
+       mCheck := make(map[Path]bool)
        for _, fsub := range fSubdirs {
                fCheck[fsub] = true
        }
@@ -122,7 +122,7 @@ func CheckdirCategory(dir string) {
 
                                fix := line.Autofix()
                                fix.Errorf("%q exists in the file system but not in the Makefile.", fCurrent)
-                               fix.InsertBefore("SUBDIR+=\t" + fCurrent)
+                               fix.InsertBefore("SUBDIR+=\t" + fCurrent.String())
                                fix.Apply()
                        }
                        fRest = fRest[1:]
@@ -155,7 +155,7 @@ func CheckdirCategory(dir string) {
        mklines.SaveAutofixChanges()
 
        if G.Opts.Recursive {
-               var recurseInto []string
+               var recurseInto []Path
                for _, msub := range mSubdirs {
                        if !msub.line.IsCommentedVarassign() {
                                recurseInto = append(recurseInto, joinPath(dir, msub.name))
Index: pkgsrc/pkgtools/pkglint/files/licenses_test.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.24 pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.25
--- pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.24 Tue Oct  1 21:37:59 2019
+++ pkgsrc/pkgtools/pkglint/files/licenses_test.go      Sat Nov 23 23:35:56 2019
@@ -54,7 +54,7 @@ func (s *Suite) Test_LicenseChecker_chec
        t.CreateFileLines("category/package/my-license",
                "An individual license file.")
 
-       t.Main(t.File("category/package"))
+       t.Main("category/package")
 
        // There is no warning about the unusual file name in the package directory.
        // If it were not mentioned in LICENSE_FILE, the file named my-license

Index: pkgsrc/pkgtools/pkglint/files/category_test.go
diff -u pkgsrc/pkgtools/pkglint/files/category_test.go:1.26 pkgsrc/pkgtools/pkglint/files/category_test.go:1.27
--- pkgsrc/pkgtools/pkglint/files/category_test.go:1.26 Fri Nov  1 19:56:53 2019
+++ pkgsrc/pkgtools/pkglint/files/category_test.go      Sat Nov 23 23:35:56 2019
@@ -208,12 +208,12 @@ func (s *Suite) Test_CheckdirCategory__r
        // It is only removed in Pkglint.Main, therefore it stays there even
        // after the call to CheckdirCategory. This is a bit unrealistic,
        // but close enough for this test.
-       t.CheckDeepEquals(G.Todo.entries, []string{"."})
+       t.CheckDeepEquals(G.Todo.entries, []Path{"."})
 
        CheckdirCategory(".")
 
        t.CheckOutputEmpty()
-       t.CheckDeepEquals(G.Todo.entries, []string{"./package", "."})
+       t.CheckDeepEquals(G.Todo.entries, []Path{"./package", "."})
 }
 
 // Ensures that a directory in the file system can be added at the very
Index: pkgsrc/pkgtools/pkglint/files/licenses.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses.go:1.26 pkgsrc/pkgtools/pkglint/files/licenses.go:1.27
--- pkgsrc/pkgtools/pkglint/files/licenses.go:1.26      Sun Jul 14 21:25:47 2019
+++ pkgsrc/pkgtools/pkglint/files/licenses.go   Sat Nov 23 23:35:56 2019
@@ -24,18 +24,18 @@ func (lc *LicenseChecker) Check(value st
 }
 
 func (lc *LicenseChecker) checkName(license string) {
-       licenseFile := ""
+       licenseFile := NewPath("")
        if G.Pkg != nil {
                if mkline := G.Pkg.vars.FirstDefinition("LICENSE_FILE"); mkline != nil {
-                       licenseFile = G.Pkg.File(mkline.ResolveVarsInRelativePath(mkline.Value()))
+                       licenseFile = G.Pkg.File(mkline.ResolveVarsInRelativePath(NewPath(mkline.Value())))
                }
        }
        if licenseFile == "" {
-               licenseFile = G.Pkgsrc.File("licenses/" + license)
+               licenseFile = G.Pkgsrc.File("licenses").JoinNoClean(NewPath(license))
                G.InterPackage.UseLicense(license)
        }
 
-       if !fileExists(licenseFile) {
+       if !licenseFile.IsFile() {
                lc.MkLine.Warnf("License file %s does not exist.", cleanpath(licenseFile))
        }
 

Index: pkgsrc/pkgtools/pkglint/files/check_test.go
diff -u pkgsrc/pkgtools/pkglint/files/check_test.go:1.54 pkgsrc/pkgtools/pkglint/files/check_test.go:1.55
--- pkgsrc/pkgtools/pkglint/files/check_test.go:1.54    Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/check_test.go Sat Nov 23 23:35:56 2019
@@ -7,8 +7,6 @@ import (
        "io/ioutil"
        "netbsd.org/pkglint/regex"
        "os"
-       "path"
-       "path/filepath"
        "regexp"
        "strings"
        "testing"
@@ -128,9 +126,9 @@ type Tester struct {
 
        stdout  bytes.Buffer
        stderr  bytes.Buffer
-       tmpdir  string
+       tmpdir  Path
        prevdir string // The current working directory before the test started
-       relCwd  string // See Tester.Chdir
+       relCwd  Path   // See Tester.Chdir
 
        seenSetUpCommandLine bool
        seenSetupPkgsrc      int
@@ -206,7 +204,7 @@ func (t *Tester) SetUpTool(name, varname
 // The file is then read in, without interpreting line continuations.
 //
 // See SetUpFileMkLines for loading a Makefile fragment.
-func (t *Tester) SetUpFileLines(relativeFileName string, lines ...string) *Lines {
+func (t *Tester) SetUpFileLines(relativeFileName Path, lines ...string) *Lines {
        filename := t.CreateFileLines(relativeFileName, lines...)
        return Load(filename, MustSucceed)
 }
@@ -215,7 +213,7 @@ func (t *Tester) SetUpFileLines(relative
 // The file is then read in, handling line continuations for Makefiles.
 //
 // See SetUpFileLines for loading an ordinary file.
-func (t *Tester) SetUpFileMkLines(relativeFileName string, lines ...string) *MkLines {
+func (t *Tester) SetUpFileMkLines(relativeFileName Path, lines ...string) *MkLines {
        filename := t.CreateFileLines(relativeFileName, lines...)
        return LoadMk(filename, MustSucceed)
 }
@@ -224,15 +222,15 @@ func (t *Tester) SetUpFileMkLines(relati
 // merging all the lines into a single MkLines object.
 //
 // This is useful for testing code related to Package.readMakefile.
-func (t *Tester) LoadMkInclude(relativeFileName string) *MkLines {
+func (t *Tester) LoadMkInclude(relativeFileName Path) *MkLines {
        var lines []*Line
 
        // TODO: Include files with multiple-inclusion guard only once.
        // TODO: Include files without multiple-inclusion guard as often as needed.
        // TODO: Set an upper limit, to prevent denial of service.
 
-       var load func(filename string)
-       load = func(filename string) {
+       var load func(filename Path)
+       load = func(filename Path) {
                for _, mkline := range NewMkLines(Load(filename, MustSucceed)).mklines {
                        lines = append(lines, mkline.Line)
 
@@ -332,11 +330,12 @@ func (t *Tester) SetUpPkgsrc() {
 
 // SetUpCategory makes the given category valid by creating a dummy Makefile.
 // After that, it can be mentioned in the CATEGORIES variable of a package.
-func (t *Tester) SetUpCategory(name string) {
-       assert(!contains(name, "/")) // Category must not contain a slash.
+func (t *Tester) SetUpCategory(name Path) {
+       assert(name.Count() == 1)
 
-       if _, err := os.Stat(t.File(name + "/Makefile")); os.IsNotExist(err) {
-               t.CreateFileLines(name+"/Makefile",
+       makefile := name.JoinNoClean("Makefile")
+       if !t.File(makefile).IsFile() {
+               t.CreateFileLines(makefile,
                        MkCvsID)
        }
 }
@@ -352,11 +351,11 @@ func (t *Tester) SetUpCategory(name stri
 // After calling this method, individual files can be overwritten as necessary.
 // At the end of the setup phase, t.FinishSetUp() must be called to load all
 // the files.
-func (t *Tester) SetUpPackage(pkgpath string, makefileLines ...string) string {
-       assertf(matches(pkgpath, `^[^/]+/[^/]+$`), "pkgpath %q must have the form \"category/package\"", pkgpath)
+func (t *Tester) SetUpPackage(pkgpath Path, makefileLines ...string) Path {
+       assertf(matches(pkgpath.String(), `^[^/]+/[^/]+$`), "pkgpath %q must have the form \"category/package\"", pkgpath)
 
-       distname := path.Base(pkgpath)
-       category := path.Dir(pkgpath)
+       distname := pkgpath.Base()
+       category := pkgpath.Dir()
        if category == "wip" {
                // To avoid boilerplate CATEGORIES definitions for wip packages.
                category = "local"
@@ -365,9 +364,9 @@ func (t *Tester) SetUpPackage(pkgpath st
        t.SetUpPkgsrc()
        t.SetUpCategory(category)
 
-       t.CreateFileLines(pkgpath+"/DESCR",
+       t.CreateFileLines(pkgpath.JoinNoClean("DESCR"),
                "Package description")
-       t.CreateFileLines(pkgpath+"/PLIST",
+       t.CreateFileLines(pkgpath.JoinNoClean("PLIST"),
                PlistCvsID,
                "bin/program")
 
@@ -377,12 +376,12 @@ func (t *Tester) SetUpPackage(pkgpath st
        // correct position. To prevent the tests from having to mention the
        // unrelated warnings about the variable order, that check is suppressed
        // here.
-       t.CreateFileLines(pkgpath+"/suppress-varorder.mk",
+       t.CreateFileLines(pkgpath.JoinNoClean("suppress-varorder.mk"),
                MkCvsID)
 
        // This distinfo file contains dummy hashes since pkglint cannot check the
        // distfiles hashes anyway. It can only check the hashes for the patches.
-       t.CreateFileLines(pkgpath+"/distinfo",
+       t.CreateFileLines(pkgpath.JoinNoClean("distinfo"),
                CvsID,
                "",
                "SHA1 (distfile-1.0.tar.gz) = 12341234",
@@ -395,7 +394,7 @@ func (t *Tester) SetUpPackage(pkgpath st
                "",
                "DISTNAME=\t" + distname + "-1.0",
                "#PKGNAME=\tpackage-1.0",
-               "CATEGORIES=\t" + category,
+               "CATEGORIES=\t" + category.String(),
                "MASTER_SITES=\t# none",
                "",
                "MAINTAINER=\tpkgsrc-users%NetBSD.org@localhost",
@@ -433,7 +432,7 @@ line:
                "",
                ".include \"../../mk/bsd.pkg.mk\"")
 
-       t.CreateFileLines(pkgpath+"/Makefile",
+       t.CreateFileLines(pkgpath.JoinNoClean("Makefile"),
                mlines...)
 
        return t.File(pkgpath)
@@ -443,18 +442,18 @@ line:
 // given lines to it.
 //
 // It returns the full path to the created file.
-func (t *Tester) CreateFileLines(relativeFileName string, lines ...string) (filename string) {
-       var content bytes.Buffer
+func (t *Tester) CreateFileLines(relativeFileName Path, lines ...string) (filename Path) {
+       var content strings.Builder
        for _, line := range lines {
                content.WriteString(line)
                content.WriteString("\n")
        }
 
        filename = t.File(relativeFileName)
-       err := os.MkdirAll(path.Dir(filename), 0777)
+       err := os.MkdirAll(filename.Dir().String(), 0777)
        t.c.Assert(err, check.IsNil)
 
-       err = ioutil.WriteFile(filename, content.Bytes(), 0666)
+       err = filename.WriteString(content.String())
        t.c.Assert(err, check.IsNil)
 
        G.fileCache.Evict(filename)
@@ -464,7 +463,7 @@ func (t *Tester) CreateFileLines(relativ
 
 // CreateFileDummyPatch creates a patch file with the given name in the
 // temporary directory.
-func (t *Tester) CreateFileDummyPatch(relativeFileName string) {
+func (t *Tester) CreateFileDummyPatch(relativeFileName Path) {
        t.CreateFileLines(relativeFileName,
                CvsID,
                "",
@@ -477,9 +476,10 @@ func (t *Tester) CreateFileDummyPatch(re
                "+new")
 }
 
-func (t *Tester) CreateFileDummyBuildlink3(relativeFileName string, customLines ...string) {
-       dir := path.Dir(relativeFileName)
-       lower := path.Base(dir)
+func (t *Tester) CreateFileDummyBuildlink3(relativeFileName Path, customLines ...string) {
+       assert(relativeFileName.Count() == 3)
+       dir := relativeFileName.Dir()
+       lower := dir.Base()
        // see pkgtools/createbuildlink/files/createbuildlink, "package specific variables"
        upper := strings.Replace(strings.ToUpper(lower), "-", "_", -1)
 
@@ -519,26 +519,26 @@ func (t *Tester) CreateFileDummyBuildlin
 // File returns the absolute path to the given file in the
 // temporary directory. It doesn't check whether that file exists.
 // Calls to Tester.Chdir change the base directory for the relative filename.
-func (t *Tester) File(relativeFileName string) string {
+func (t *Tester) File(relativeFileName Path) Path {
        if t.tmpdir == "" {
-               t.tmpdir = filepath.ToSlash(t.c.MkDir())
+               t.tmpdir = NewPathSlash(t.c.MkDir())
        }
        if t.relCwd != "" {
-               return path.Clean(relativeFileName)
+               return relativeFileName.Clean()
        }
-       return path.Clean(joinPath(t.tmpdir, relativeFileName))
+       return t.tmpdir.JoinClean(relativeFileName)
 }
 
 // Copy copies a file inside the temporary directory.
-func (t *Tester) Copy(relativeSrc, relativeDst string) {
+func (t *Tester) Copy(relativeSrc, relativeDst Path) {
        src := t.File(relativeSrc)
        dst := t.File(relativeDst)
 
-       data, err := ioutil.ReadFile(src)
+       data, err := src.ReadString()
        assertNil(err, "Copy.Read")
-       err = os.MkdirAll(path.Dir(dst), 0777)
+       err = os.MkdirAll(dst.Dir().String(), 0777)
        assertNil(err, "Copy.MkdirAll")
-       err = ioutil.WriteFile(dst, data, 0777)
+       err = dst.WriteString(data)
        assertNil(err, "Copy.Write")
 }
 
@@ -554,7 +554,7 @@ func (t *Tester) Copy(relativeSrc, relat
 //
 // As long as this method is not called in a test, the current working
 // directory is indeterminate.
-func (t *Tester) Chdir(relativeDirName string) {
+func (t *Tester) Chdir(relativeDirName Path) {
        if t.relCwd != "" {
                // When multiple calls of Chdir are mixed with calls to CreateFileLines,
                // the resulting Lines and MkLines variables will use relative filenames,
@@ -564,17 +564,17 @@ func (t *Tester) Chdir(relativeDirName s
        }
 
        absDirName := t.File(relativeDirName)
-       assertNil(os.MkdirAll(absDirName, 0700), "MkDirAll")
-       assertNil(os.Chdir(absDirName), "Chdir")
+       assertNil(os.MkdirAll(absDirName.String(), 0700), "MkDirAll")
+       assertNil(os.Chdir(absDirName.String()), "Chdir")
        t.relCwd = relativeDirName
        G.cwd = absDirName
 }
 
 // Remove removes the file or directory from the temporary directory.
 // The file or directory must exist.
-func (t *Tester) Remove(relativeFileName string) {
+func (t *Tester) Remove(relativeFileName Path) {
        filename := t.File(relativeFileName)
-       err := os.Remove(filename)
+       err := os.Remove(filename.String())
        t.c.Assert(err, check.IsNil)
        G.fileCache.Evict(filename)
 }
@@ -604,12 +604,12 @@ func (t *Tester) Remove(relativeFileName
 // subdir/module.mk includes subdir/version.mk, the include line is just:
 //  .include "version.mk"
 func (t *Tester) SetUpHierarchy() (
-       include func(filename string, args ...interface{}) *MkLines,
-       get func(string) *MkLines) {
+       include func(filename Path, args ...interface{}) *MkLines,
+       get func(Path) *MkLines) {
 
-       files := map[string]*MkLines{}
+       files := map[Path]*MkLines{}
 
-       include = func(filename string, args ...interface{}) *MkLines {
+       include = func(filename Path, args ...interface{}) *MkLines {
                var lines []*Line
                lineno := 1
 
@@ -623,7 +623,7 @@ func (t *Tester) SetUpHierarchy() (
                        case string:
                                addLine(arg)
                        case *MkLines:
-                               text := sprintf(".include %q", relpath(path.Dir(filename), arg.lines.Filename))
+                               text := sprintf(".include %q", relpath(filename.Dir(), arg.lines.Filename))
                                addLine(text)
                                lines = append(lines, arg.lines.Lines...)
                        default:
@@ -637,7 +637,7 @@ func (t *Tester) SetUpHierarchy() (
                return mklines
        }
 
-       get = func(filename string) *MkLines {
+       get = func(filename Path) *MkLines {
                assertf(files[filename] != nil, "MkLines with name %q doesn't exist.", filename)
                return files[filename]
        }
@@ -715,10 +715,9 @@ func (t *Tester) Main(args ...string) in
 
        argv := []string{"pkglint"}
        for _, arg := range args {
-               fileArg := t.File(arg)
-               _, err := os.Lstat(fileArg)
-               if err == nil {
-                       argv = append(argv, fileArg)
+               fileArg := t.File(NewPath(arg))
+               if fileArg.Exists() {
+                       argv = append(argv, fileArg.String())
                } else {
                        argv = append(argv, arg)
                }
@@ -845,15 +844,15 @@ func (t *Tester) NewRawLines(args ...int
 
 // NewLine creates an in-memory line with the given text.
 // This line does not correspond to any line in a file.
-func (t *Tester) NewLine(filename string, lineno int, text string) *Line {
+func (t *Tester) NewLine(filename Path, lineno int, text string) *Line {
        textnl := text + "\n"
        rawLine := RawLine{lineno, textnl, textnl}
        return NewLine(filename, lineno, text, &rawLine)
 }
 
 // NewMkLine creates an in-memory line in the Makefile format with the given text.
-func (t *Tester) NewMkLine(filename string, lineno int, text string) *MkLine {
-       basename := path.Base(filename)
+func (t *Tester) NewMkLine(filename Path, lineno int, text string) *MkLine {
+       basename := filename.Base()
        assertf(
                hasSuffix(basename, ".mk") ||
                        basename == "Makefile" ||
@@ -872,14 +871,14 @@ func (t *Tester) NewShellLineChecker(tex
 // NewLines returns a list of simple lines that belong together.
 //
 // To work with line continuations like in Makefiles, use SetUpFileMkLines.
-func (t *Tester) NewLines(filename string, lines ...string) *Lines {
+func (t *Tester) NewLines(filename Path, lines ...string) *Lines {
        return t.NewLinesAt(filename, 1, lines...)
 }
 
 // NewLinesAt returns a list of simple lines that belong together.
 //
 // To work with line continuations like in Makefiles, use SetUpFileMkLines.
-func (t *Tester) NewLinesAt(filename string, firstLine int, texts ...string) *Lines {
+func (t *Tester) NewLinesAt(filename Path, firstLine int, texts ...string) *Lines {
        lines := make([]*Line, len(texts))
        for i, text := range texts {
                lines[i] = t.NewLine(filename, i+firstLine, text)
@@ -893,8 +892,8 @@ func (t *Tester) NewLinesAt(filename str
 //
 // No actual file is created for the lines;
 // see SetUpFileMkLines for loading Makefile fragments with line continuations.
-func (t *Tester) NewMkLines(filename string, lines ...string) *MkLines {
-       basename := path.Base(filename)
+func (t *Tester) NewMkLines(filename Path, lines ...string) *MkLines {
+       basename := filename.Base()
        assertf(
                hasSuffix(basename, ".mk") || basename == "Makefile" || hasPrefix(basename, "Makefile."),
                "filename %q must be realistic, otherwise the variable permissions are wrong", filename)
@@ -924,7 +923,7 @@ func (t *Tester) Output() string {
        }
 
        assertf(t.tmpdir != "", "Tester must be initialized before checking the output.")
-       return strings.Replace(stdout+stderr, t.tmpdir, "~", -1)
+       return strings.Replace(stdout+stderr, t.tmpdir.String(), "~", -1)
 }
 
 // CheckOutputEmpty ensures that the output up to now is empty.
@@ -983,14 +982,14 @@ func (t *Tester) CheckOutputLinesIgnoreS
        t.CheckDeepEquals(actual, expected)
 }
 
-func (t *Tester) compareOutputIgnoreSpace(rawOutput string, expectedLines []string, tmpdir string) ([]string, []string) {
+func (t *Tester) compareOutputIgnoreSpace(rawOutput string, expectedLines []string, tmpdir Path) ([]string, []string) {
        whitespace := regexp.MustCompile(`\s+`)
 
        // Replace all occurrences of tmpdir in the raw output with a tilde,
        // also covering cases where tmpdir is wrapped into multiple lines.
        output := func() string {
                var tmpdirPattern strings.Builder
-               for i, part := range whitespace.Split(tmpdir, -1) {
+               for i, part := range whitespace.Split(tmpdir.String(), -1) {
                        if i > 0 {
                                tmpdirPattern.WriteString("\\s+")
                        }
@@ -1017,7 +1016,7 @@ func (s *Suite) Test_Tester_compareOutpu
        t := s.Init(c)
 
        lines := func(lines ...string) []string { return lines }
-       test := func(rawOutput string, expectedLines []string, tmpdir string, eq bool) {
+       test := func(rawOutput string, expectedLines []string, tmpdir Path, eq bool) {
                actual, expected := t.compareOutputIgnoreSpace(rawOutput, expectedLines, tmpdir)
                t.CheckEquals(actual == nil && expected == nil, eq)
        }
@@ -1137,10 +1136,10 @@ func (t *Tester) DisableTracing() {
 
 // CheckFileLines loads the lines from the temporary file and checks that
 // they equal the given lines.
-func (t *Tester) CheckFileLines(relativeFileName string, lines ...string) {
-       content, err := ioutil.ReadFile(t.File(relativeFileName))
+func (t *Tester) CheckFileLines(relativeFileName Path, lines ...string) {
+       content, err := t.File(relativeFileName).ReadString()
        t.c.Assert(err, check.IsNil)
-       actualLines := strings.Split(string(content), "\n")
+       actualLines := strings.Split(content, "\n")
        actualLines = actualLines[:len(actualLines)-1]
        t.CheckDeepEquals(emptyToNil(actualLines), emptyToNil(lines))
 }
@@ -1149,7 +1148,7 @@ func (t *Tester) CheckFileLines(relative
 // that they equal the given lines. The loaded file may use tabs or spaces
 // for indentation, while the lines in the code use spaces exclusively,
 // in order to make the depth of the indentation clearly visible in the test code.
-func (t *Tester) CheckFileLinesDetab(relativeFileName string, lines ...string) {
+func (t *Tester) CheckFileLinesDetab(relativeFileName Path, lines ...string) {
        actualLines := Load(t.File(relativeFileName), MustSucceed)
 
        var detabbedLines []string
@@ -1167,11 +1166,11 @@ func (t *Tester) CheckFileLinesDetab(rel
 // development.
 func (t *Tester) Use(...interface{}) {}
 
-func (t *Tester) Shquote(format string, rels ...string) string {
+func (t *Tester) Shquote(format string, rels ...Path) string {
        var subs []interface{}
        for _, rel := range rels {
-               quoted := shquote(path.Join(t.tmpdir, rel))
-               subs = append(subs, strings.Replace(quoted, t.tmpdir, "~", -1))
+               quoted := shquote(t.tmpdir.JoinClean(rel).String())
+               subs = append(subs, strings.Replace(quoted, t.tmpdir.String(), "~", -1))
        }
        return sprintf(format, subs...)
 }

Index: pkgsrc/pkgtools/pkglint/files/distinfo.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo.go:1.36 pkgsrc/pkgtools/pkglint/files/distinfo.go:1.37
--- pkgsrc/pkgtools/pkglint/files/distinfo.go:1.36      Tue Oct  1 21:37:59 2019
+++ pkgsrc/pkgtools/pkglint/files/distinfo.go   Sat Nov 23 23:35:56 2019
@@ -8,29 +8,27 @@ import (
        "golang.org/x/crypto/ripemd160"
        "hash"
        "io"
-       "io/ioutil"
-       "os"
        "strings"
 )
 
 func CheckLinesDistinfo(pkg *Package, lines *Lines) {
        if trace.Tracing {
-               defer trace.Call1(lines.Filename)()
+               defer trace.Call(lines.Filename)()
        }
 
        filename := lines.Filename
-       patchdir := "patches"
-       if pkg != nil && dirExists(pkg.File(pkg.Patchdir)) {
+       patchdir := NewPath("patches")
+       if pkg != nil && pkg.File(pkg.Patchdir).IsDir() {
                patchdir = pkg.Patchdir
        }
        if trace.Tracing {
-               trace.Step1("patchdir=%q", patchdir)
+               trace.Stepf("patchdir=%q", patchdir)
        }
 
        distinfoIsCommitted := isCommitted(filename)
        ck := distinfoLinesChecker{
                pkg, lines, patchdir, distinfoIsCommitted,
-               nil, make(map[string]distinfoFileInfo)}
+               nil, make(map[Path]distinfoFileInfo)}
        ck.parse()
        ck.check()
        CheckLinesTrailingEmptyLines(lines)
@@ -42,11 +40,11 @@ func CheckLinesDistinfo(pkg *Package, li
 type distinfoLinesChecker struct {
        pkg                 *Package
        lines               *Lines
-       patchdir            string // Relative to pkg
+       patchdir            Path // Relative to pkg
        distinfoIsCommitted bool
 
-       filenames []string // For keeping the order from top to bottom
-       infos     map[string]distinfoFileInfo
+       filenames []Path // For keeping the order from top to bottom
+       infos     map[Path]distinfoFileInfo
 }
 
 func (ck *distinfoLinesChecker) parse() {
@@ -58,16 +56,16 @@ func (ck *distinfoLinesChecker) parse() 
        }
        llex.SkipEmptyOrNote()
 
-       prevFilename := ""
+       prevFilename := NewPath("")
        var hashes []distinfoHash
 
        isPatch := func() YesNoUnknown {
                switch {
-               case !hasPrefix(prevFilename, "patch-"):
+               case !prevFilename.HasPrefixText("patch-"):
                        return no
                case ck.pkg == nil:
                        return unknown
-               case fileExists(ck.pkg.File(joinPath(ck.patchdir, prevFilename))):
+               case ck.pkg.File(ck.patchdir.JoinNoClean(prevFilename)).IsFile():
                        return yes
                default:
                        return no
@@ -84,7 +82,8 @@ func (ck *distinfoLinesChecker) parse() 
                line := llex.CurrentLine()
                llex.Skip()
 
-               m, alg, filename, hash := match3(line.Text, `^(\w+) \((\w[^)]*)\) = (\S+(?: bytes)?)$`)
+               m, alg, file, hash := match3(line.Text, `^(\w+) \((\w[^)]*)\) = (\S+(?: bytes)?)$`)
+               filename := NewPath(file)
                if !m {
                        line.Errorf("Invalid line: %s", line.Text)
                        continue
@@ -146,7 +145,7 @@ func (ck *distinfoLinesChecker) checkAlg
 
        // At this point, the file is either a missing patch file or a distfile.
 
-       case hasPrefix(filename, "patch-") && algorithms == "SHA1":
+       case filename.HasPrefixText("patch-") && algorithms == "SHA1":
                if ck.pkg.IgnoreMissingPatches {
                        break
                }
@@ -196,7 +195,7 @@ func (ck *distinfoLinesChecker) checkAlg
        distdir := G.Pkgsrc.File("distfiles")
 
        distfile := cleanpath(joinPath(distdir, info.filename()))
-       if !fileExists(distfile) {
+       if !distfile.IsFile() {
 
                // It's a rare situation that the explanation is generated
                // this far from the corresponding diagnostic.
@@ -225,7 +224,7 @@ func (ck *distinfoLinesChecker) checkAlg
        }
 
        computeHash := func(hasher hash.Hash) string {
-               f, err := os.Open(distfile)
+               f, err := distfile.Open()
                assertNil(err, "Opening distfile")
 
                // Don't load the distfile into memory since some of them
@@ -250,7 +249,7 @@ func (ck *distinfoLinesChecker) checkAlg
                case "SHA512":
                        return computeHash(sha512.New())
                default:
-                       fileInfo, err := os.Lstat(distfile)
+                       fileInfo, err := distfile.Lstat()
                        assertNil(err, "Inaccessible distfile info")
                        return sprintf("%d bytes", fileInfo.Size())
                }
@@ -302,7 +301,7 @@ func (ck *distinfoLinesChecker) checkUnr
        if ck.pkg == nil {
                return
        }
-       patchFiles, err := ioutil.ReadDir(ck.pkg.File(ck.patchdir))
+       patchFiles, err := ck.pkg.File(ck.patchdir).ReadDir()
        if err != nil {
                if trace.Tracing {
                        trace.Stepf("Cannot read patchdir %q: %s", ck.patchdir, err)
@@ -311,11 +310,11 @@ func (ck *distinfoLinesChecker) checkUnr
        }
 
        for _, file := range patchFiles {
-               patchName := file.Name()
-               if file.Mode().IsRegular() && ck.infos[patchName].isPatch != yes && hasPrefix(patchName, "patch-") {
+               patchName := NewPath(file.Name())
+               if file.Mode().IsRegular() && ck.infos[patchName].isPatch != yes && patchName.HasPrefixText("patch-") {
                        line := NewLineWhole(ck.lines.Filename)
                        line.Errorf("Patch %q is not recorded. Run %q.",
-                               line.PathToFile(ck.pkg.File(joinPath(ck.patchdir, patchName))),
+                               line.PathToFile(ck.pkg.File(ck.patchdir.JoinNoClean(patchName))),
                                bmake("makepatchsum"))
                }
        }
@@ -336,7 +335,7 @@ func (ck *distinfoLinesChecker) checkGlo
        // Intentionally checking the filename instead of ck.isPatch.
        // Missing the few distfiles that actually start with patch-*
        // is more convenient than having lots of false positive mismatches.
-       if hasPrefix(filename, "patch-") {
+       if filename.HasPrefixText("patch-") {
                return
        }
 
@@ -381,7 +380,7 @@ func (ck *distinfoLinesChecker) checkUnc
        }
 }
 
-func (ck *distinfoLinesChecker) checkPatchSha1(line *Line, patchFileName, distinfoSha1Hex string) {
+func (ck *distinfoLinesChecker) checkPatchSha1(line *Line, patchFileName Path, distinfoSha1Hex string) {
        lines := Load(ck.pkg.File(patchFileName), 0)
        if lines == nil {
                line.Errorf("Patch %s does not exist.", patchFileName)
@@ -409,8 +408,8 @@ type distinfoFileInfo struct {
        hashes  []distinfoHash
 }
 
-func (info *distinfoFileInfo) filename() string { return info.hashes[0].filename }
-func (info *distinfoFileInfo) line() *Line      { return info.hashes[0].line }
+func (info *distinfoFileInfo) filename() Path { return info.hashes[0].filename }
+func (info *distinfoFileInfo) line() *Line    { return info.hashes[0].line }
 
 func (info *distinfoFileInfo) algorithms() string {
        var algs []string
@@ -422,7 +421,7 @@ func (info *distinfoFileInfo) algorithms
 
 type distinfoHash struct {
        line      *Line
-       filename  string
+       filename  Path
        algorithm string
        hash      string
 }

Index: pkgsrc/pkgtools/pkglint/files/files.go
diff -u pkgsrc/pkgtools/pkglint/files/files.go:1.27 pkgsrc/pkgtools/pkglint/files/files.go:1.28
--- pkgsrc/pkgtools/pkglint/files/files.go:1.27 Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/files.go      Sat Nov 23 23:35:56 2019
@@ -1,9 +1,7 @@
 package pkglint
 
 import (
-       "io/ioutil"
        "netbsd.org/pkglint/textproc"
-       "path"
        "strings"
 )
 
@@ -16,7 +14,7 @@ const (
        LogErrors                           //
 )
 
-func LoadMk(filename string, options LoadOptions) *MkLines {
+func LoadMk(filename Path, options LoadOptions) *MkLines {
        lines := Load(filename, options|Makefile)
        if lines == nil {
                return nil
@@ -24,12 +22,12 @@ func LoadMk(filename string, options Loa
        return NewMkLines(lines)
 }
 
-func Load(filename string, options LoadOptions) *Lines {
+func Load(filename Path, options LoadOptions) *Lines {
        if fromCache := G.fileCache.Get(filename, options); fromCache != nil {
                return fromCache
        }
 
-       rawBytes, err := ioutil.ReadFile(filename)
+       rawText, err := filename.ReadString()
        if err != nil {
                switch {
                case options&MustSucceed != 0:
@@ -40,7 +38,6 @@ func Load(filename string, options LoadO
                return nil
        }
 
-       rawText := string(rawBytes)
        if rawText == "" && options&NotEmpty != 0 {
                switch {
                case options&MustSucceed != 0:
@@ -52,17 +49,17 @@ func Load(filename string, options LoadO
        }
 
        if G.Opts.Profiling {
-               G.loaded.Add(path.Clean(filename), 1)
+               G.loaded.Add(filename.Clean().String(), 1)
        }
 
        result := convertToLogicalLines(filename, rawText, options&Makefile != 0)
-       if hasSuffix(filename, ".mk") {
+       if filename.HasSuffixText(".mk") {
                G.fileCache.Put(filename, options, result)
        }
        return result
 }
 
-func convertToLogicalLines(filename string, rawText string, joinBackslashLines bool) *Lines {
+func convertToLogicalLines(filename Path, rawText string, joinBackslashLines bool) *Lines {
        var rawLines []*RawLine
        for lineno, rawLine := range strings.SplitAfter(rawText, "\n") {
                if rawLine != "" {
@@ -92,7 +89,7 @@ func convertToLogicalLines(filename stri
        return NewLines(filename, loglines)
 }
 
-func nextLogicalLine(filename string, rawLines []*RawLine, index int) (*Line, int) {
+func nextLogicalLine(filename Path, rawLines []*RawLine, index int) (*Line, int) {
        { // Handle the common case efficiently
                rawLine := rawLines[index]
                textnl := rawLine.textnl

Index: pkgsrc/pkgtools/pkglint/files/fuzzer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/fuzzer_test.go:1.4 pkgsrc/pkgtools/pkglint/files/fuzzer_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/fuzzer_test.go:1.4    Sun Jun 30 20:56:19 2019
+++ pkgsrc/pkgtools/pkglint/files/fuzzer_test.go        Sat Nov 23 23:35:56 2019
@@ -3,6 +3,7 @@ package pkglint
 import (
        "gopkg.in/check.v1"
        "math/rand"
+       "time"
 )
 
 // Fuzzer generates random strings.
@@ -27,7 +28,7 @@ func NewFuzzer(seed ...int64) *Fuzzer {
        if len(seed) > 0 {
                actualSeed = seed[0]
        } else {
-               actualSeed = rand.Int63()
+               actualSeed = time.Now().UnixNano()
        }
        return &Fuzzer{seed: actualSeed, rnd: rand.New(rand.NewSource(actualSeed))}
 }

Index: pkgsrc/pkgtools/pkglint/files/line.go
diff -u pkgsrc/pkgtools/pkglint/files/line.go:1.38 pkgsrc/pkgtools/pkglint/files/line.go:1.39
--- pkgsrc/pkgtools/pkglint/files/line.go:1.38  Sun Jul 14 21:25:47 2019
+++ pkgsrc/pkgtools/pkglint/files/line.go       Sat Nov 23 23:35:56 2019
@@ -15,7 +15,6 @@ package pkglint
 
 import (
        "netbsd.org/pkglint/regex"
-       "path"
        "strconv"
 )
 
@@ -38,16 +37,16 @@ type RawLine struct {
 func (rline *RawLine) String() string { return sprintf("%d:%s", rline.Lineno, rline.textnl) }
 
 type Location struct {
-       Filename  string // uses / as directory separator on all platforms
-       firstLine int32  // zero means the whole file, -1 means EOF
-       lastLine  int32  // usually the same as firstLine, may differ in Makefiles
+       Filename  Path
+       firstLine int32 // zero means the whole file, -1 means EOF
+       lastLine  int32 // usually the same as firstLine, may differ in Makefiles
 }
 
 func (loc *Location) String() string {
-       return loc.Filename + ":" + loc.Linenos()
+       return loc.Filename.String() + ":" + loc.Linenos()
 }
 
-func NewLocation(filename string, firstLine, lastLine int) Location {
+func NewLocation(filename Path, firstLine, lastLine int) Location {
        return Location{filename, int32(firstLine), int32(lastLine)}
 }
 
@@ -83,23 +82,23 @@ type Line struct {
        // XXX: Filename and Basename could be replaced with a pointer to a Lines object.
 }
 
-func NewLine(filename string, lineno int, text string, rawLine *RawLine) *Line {
+func NewLine(filename Path, lineno int, text string, rawLine *RawLine) *Line {
        assert(rawLine != nil) // Use NewLineMulti for creating a Line with no RawLine attached to it.
        return NewLineMulti(filename, lineno, lineno, text, []*RawLine{rawLine})
 }
 
 // NewLineMulti is for logical Makefile lines that end with backslash.
-func NewLineMulti(filename string, firstLine, lastLine int, text string, rawLines []*RawLine) *Line {
-       return &Line{NewLocation(filename, firstLine, lastLine), path.Base(filename), text, rawLines, nil, Once{}}
+func NewLineMulti(filename Path, firstLine, lastLine int, text string, rawLines []*RawLine) *Line {
+       return &Line{NewLocation(filename, firstLine, lastLine), filename.Base(), text, rawLines, nil, Once{}}
 }
 
 // NewLineEOF creates a dummy line for logging, with the "line number" EOF.
-func NewLineEOF(filename string) *Line {
+func NewLineEOF(filename Path) *Line {
        return NewLineMulti(filename, -1, 0, "", nil)
 }
 
 // NewLineWhole creates a dummy line for logging messages that affect a file as a whole.
-func NewLineWhole(filename string) *Line {
+func NewLineWhole(filename Path) *Line {
        return NewLineMulti(filename, 0, 0, "", nil)
 }
 
@@ -111,7 +110,7 @@ func (line *Line) RefTo(other *Line) str
 
 func (line *Line) RefToLocation(other Location) string {
        if line.Filename != other.Filename {
-               return line.PathToFile(other.Filename) + ":" + other.Linenos()
+               return line.PathToFile(other.Filename).String() + ":" + other.Linenos()
        }
        return "line " + other.Linenos()
 }
@@ -119,8 +118,8 @@ func (line *Line) RefToLocation(other Lo
 // PathToFile returns the relative path from this line to the given file path.
 // This is typically used for arguments in diagnostics, which should always be
 // relative to the line with which the diagnostic is associated.
-func (line *Line) PathToFile(filePath string) string {
-       return relpath(path.Dir(line.Filename), filePath)
+func (line *Line) PathToFile(filePath Path) Path {
+       return relpath(line.Filename.Dir(), filePath)
 }
 
 func (line *Line) IsMultiline() bool {

Index: pkgsrc/pkgtools/pkglint/files/linelexer.go
diff -u pkgsrc/pkgtools/pkglint/files/linelexer.go:1.6 pkgsrc/pkgtools/pkglint/files/linelexer.go:1.7
--- pkgsrc/pkgtools/pkglint/files/linelexer.go:1.6      Sat Nov  2 16:37:48 2019
+++ pkgsrc/pkgtools/pkglint/files/linelexer.go  Sat Nov 23 23:35:56 2019
@@ -4,19 +4,22 @@ import "netbsd.org/pkglint/regex"
 
 // LinesLexer records the state when checking a list of lines from top to bottom.
 type LinesLexer struct {
-       lines *Lines
+       line  *Line
        index int
+       lines *Lines
 }
 
 func NewLinesLexer(lines *Lines) *LinesLexer {
-       return &LinesLexer{lines, 0}
+       llex := LinesLexer{nil, 0, lines}
+       llex.setIndex(0)
+       return &llex
 }
 
 // CurrentLine returns the line that the lexer is currently looking at.
-// If it is at the end of file, the line number of the line is EOF.
+// For the EOF, a virtual line with line number "EOF" is returned.
 func (llex *LinesLexer) CurrentLine() *Line {
-       if llex.index < llex.lines.Len() {
-               return llex.lines.Lines[llex.index]
+       if llex.line != nil {
+               return llex.line
        }
        return NewLineEOF(llex.lines.Filename)
 }
@@ -26,20 +29,20 @@ func (llex *LinesLexer) PreviousLine() *
 }
 
 func (llex *LinesLexer) EOF() bool {
-       return !(llex.index < llex.lines.Len())
+       return llex.line == nil
 }
 
-// Skip skips the current line and returns true.
+// Skip skips the current line.
 func (llex *LinesLexer) Skip() bool {
        if llex.EOF() {
                return false
        }
-       llex.index++
+       llex.next()
        return true
 }
 
 func (llex *LinesLexer) Undo() {
-       llex.index--
+       llex.setIndex(llex.index - 1)
 }
 
 func (llex *LinesLexer) NextRegexp(re regex.Pattern) []string {
@@ -48,8 +51,8 @@ func (llex *LinesLexer) NextRegexp(re re
        }
 
        if !llex.EOF() {
-               if m := match(llex.lines.Lines[llex.index].Text, re); m != nil {
-                       llex.index++
+               if m := match(llex.line.Text, re); m != nil {
+                       llex.next()
                        return m
                }
        }
@@ -65,19 +68,19 @@ func (llex *LinesLexer) SkipPrefix(prefi
                defer trace.Call2(llex.CurrentLine().Text, prefix)()
        }
 
-       if !llex.EOF() && hasPrefix(llex.lines.Lines[llex.index].Text, prefix) {
-               llex.Skip()
+       if !llex.EOF() && hasPrefix(llex.line.Text, prefix) {
+               llex.next()
                return true
        }
        return false
 }
 
-func (llex *LinesLexer) SkipString(text string) bool {
+func (llex *LinesLexer) SkipText(text string) bool {
        if trace.Tracing {
                defer trace.Call2(llex.CurrentLine().Text, text)()
        }
 
-       if !llex.EOF() && llex.lines.Lines[llex.index].Text == text {
+       if !llex.EOF() && llex.line.Text == text {
                llex.Skip()
                return true
        }
@@ -85,7 +88,7 @@ func (llex *LinesLexer) SkipString(text 
 }
 
 func (llex *LinesLexer) SkipEmptyOrNote() bool {
-       if llex.SkipString("") {
+       if llex.SkipText("") {
                return true
        }
 
@@ -106,13 +109,24 @@ func (llex *LinesLexer) SkipEmptyOrNote(
 }
 
 func (llex *LinesLexer) SkipContainsOrWarn(text string) bool {
-       result := llex.SkipString(text)
+       result := llex.SkipText(text)
        if !result {
                llex.CurrentLine().Warnf("This line should contain the following text: %s", text)
        }
        return result
 }
 
+func (llex *LinesLexer) setIndex(index int) {
+       llex.index = index
+       if index < llex.lines.Len() {
+               llex.line = llex.lines.Lines[index]
+       } else {
+               llex.line = nil
+       }
+}
+
+func (llex *LinesLexer) next() { llex.setIndex(llex.index + 1) }
+
 // MkLinesLexer records the state when checking a list of Makefile lines from top to bottom.
 type MkLinesLexer struct {
        mklines *MkLines
@@ -133,7 +147,7 @@ func (mlex *MkLinesLexer) CurrentMkLine(
 
 func (mlex *MkLinesLexer) SkipIf(pred func(mkline *MkLine) bool) bool {
        if !mlex.EOF() && pred(mlex.CurrentMkLine()) {
-               mlex.Skip()
+               mlex.next()
                return true
        }
        return false

Index: pkgsrc/pkgtools/pkglint/files/lines.go
diff -u pkgsrc/pkgtools/pkglint/files/lines.go:1.9 pkgsrc/pkgtools/pkglint/files/lines.go:1.10
--- pkgsrc/pkgtools/pkglint/files/lines.go:1.9  Sat Nov  2 16:37:48 2019
+++ pkgsrc/pkgtools/pkglint/files/lines.go      Sat Nov 23 23:35:56 2019
@@ -2,17 +2,16 @@ package pkglint
 
 import (
        "netbsd.org/pkglint/regex"
-       "path"
 )
 
 type Lines struct {
-       Filename string
-       BaseName string
+       Filename Path
+       BaseName string // TODO: consider converting to Path
        Lines    []*Line
 }
 
-func NewLines(filename string, lines []*Line) *Lines {
-       return &Lines{filename, path.Base(filename), lines}
+func NewLines(filename Path, lines []*Line) *Lines {
+       return &Lines{filename, filename.Base(), lines}
 }
 
 func (ls *Lines) Len() int { return len(ls.Lines) }

Index: pkgsrc/pkgtools/pkglint/files/mkline.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.63 pkgsrc/pkgtools/pkglint/files/mkline.go:1.64
--- pkgsrc/pkgtools/pkglint/files/mkline.go:1.63        Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline.go     Sat Nov 23 23:35:56 2019
@@ -4,7 +4,6 @@ import (
        "fmt"
        "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/textproc"
-       "path"
        "strings"
 )
 
@@ -60,7 +59,7 @@ type mkLineInclude struct {
        mustExist       bool     // for .sinclude, nonexistent files are ignored
        sys             bool     // whether the include uses <file.mk> (very rare) instead of "file.mk"
        indent          string   // the space between the leading "." and the directive
-       includedFile    string   // the text between the <brackets> or "quotes"
+       includedFile    Path     // the text between the <brackets> or "quotes"
        conditionalVars []string // variables on which this inclusion depends (filled in later, as needed)
 }
 
@@ -277,12 +276,12 @@ func (mkline *MkLine) SetHasElseBranch(e
 
 func (mkline *MkLine) MustExist() bool { return mkline.data.(*mkLineInclude).mustExist }
 
-func (mkline *MkLine) IncludedFile() string { return mkline.data.(*mkLineInclude).includedFile }
+func (mkline *MkLine) IncludedFile() Path { return mkline.data.(*mkLineInclude).includedFile }
 
 // IncludedFileFull returns the path to the included file, relative to the
 // current working directory.
-func (mkline *MkLine) IncludedFileFull() string {
-       return cleanpath(path.Join(path.Dir(mkline.Filename), mkline.IncludedFile()))
+func (mkline *MkLine) IncludedFileFull() Path {
+       return cleanpath(mkline.Filename.Dir().JoinClean(mkline.IncludedFile())) // FIXME: JoinNoClean?
 }
 
 func (mkline *MkLine) Targets() string { return mkline.data.(mkLineDependency).targets }
@@ -554,31 +553,31 @@ func (*MkLine) WithoutMakeVariables(valu
        return valueNovar.String()
 }
 
-func (mkline *MkLine) ResolveVarsInRelativePath(relativePath string) string {
-       if !contains(relativePath, "$") {
+func (mkline *MkLine) ResolveVarsInRelativePath(relativePath Path) Path {
+       if !containsVarRef(relativePath.String()) {
                return cleanpath(relativePath)
        }
 
-       var basedir string
+       var basedir Path
        if G.Pkg != nil {
                basedir = G.Pkg.File(".")
        } else {
-               basedir = path.Dir(mkline.Filename)
+               basedir = mkline.Filename.Dir()
        }
 
        tmp := relativePath
-       if contains(tmp, "PKGSRCDIR") {
+       if tmp.ContainsText("PKGSRCDIR") {
                pkgsrcdir := relpath(basedir, G.Pkgsrc.File("."))
 
                if G.Testing {
                        // Relative pkgsrc paths usually only contain two or three levels.
                        // A possible reason for reaching this assertion is a pkglint unit test
                        // that uses t.NewMkLines instead of the correct t.SetUpFileMkLines.
-                       assertf(!contains(pkgsrcdir, "../../../../.."),
+                       assertf(!pkgsrcdir.ContainsPath("../../../../.."),
                                "Relative path %q for %q is too deep below the pkgsrc root %q.",
                                pkgsrcdir, basedir, G.Pkgsrc.File("."))
                }
-               tmp = strings.Replace(tmp, "${PKGSRCDIR}", pkgsrcdir, -1)
+               tmp = tmp.Replace("${PKGSRCDIR}", pkgsrcdir.String())
        }
 
        // Strictly speaking, the .CURDIR should be replaced with the basedir.
@@ -586,7 +585,7 @@ func (mkline *MkLine) ResolveVarsInRelat
        // path, this would produce diagnostics that "this relative path must not
        // be absolute". Since ${.CURDIR} is usually used in package Makefiles and
        // followed by "../.." anyway, the exact directory doesn't matter.
-       tmp = strings.Replace(tmp, "${.CURDIR}", ".", -1)
+       tmp = tmp.Replace("${.CURDIR}", ".")
 
        // TODO: Add test for exists(${.PARSEDIR}/file).
        // TODO: Add test for evaluating ${.PARSEDIR} in an included package.
@@ -595,12 +594,12 @@ func (mkline *MkLine) ResolveVarsInRelat
        //  This is the only practically relevant use case since the category
        //  directories don't contain any *.mk files that could be included.
        // TODO: Add test that suggests ${.PARSEDIR} in .include to be omitted.
-       tmp = strings.Replace(tmp, "${.PARSEDIR}", ".", -1)
+       tmp = tmp.Replace("${.PARSEDIR}", ".")
 
-       replaceLatest := func(varuse, category string, pattern regex.Pattern, replacement string) {
-               if contains(tmp, varuse) {
+       replaceLatest := func(varuse string, category Path, pattern regex.Pattern, replacement string) {
+               if tmp.ContainsText(varuse) {
                        latest := G.Pkgsrc.Latest(category, pattern, replacement)
-                       tmp = strings.Replace(tmp, varuse, latest, -1)
+                       tmp = tmp.Replace(varuse, latest)
                }
        }
 
@@ -617,14 +616,14 @@ func (mkline *MkLine) ResolveVarsInRelat
                // XXX: Even if these variables are defined indirectly,
                // pkglint should be able to resolve them properly.
                // There is already G.Pkg.Value, maybe that can be used here.
-               tmp = strings.Replace(tmp, "${FILESDIR}", G.Pkg.Filesdir, -1)
-               tmp = strings.Replace(tmp, "${PKGDIR}", G.Pkg.Pkgdir, -1)
+               tmp = tmp.Replace("${FILESDIR}", G.Pkg.Filesdir.String())
+               tmp = tmp.Replace("${PKGDIR}", G.Pkg.Pkgdir.String())
        }
 
        tmp = cleanpath(tmp)
 
        if trace.Tracing && relativePath != tmp {
-               trace.Step2("resolveVarsInRelativePath: %q => %q", relativePath, tmp)
+               trace.Stepf("resolveVarsInRelativePath: %q => %q", relativePath, tmp)
        }
        return tmp
 }
@@ -820,7 +819,7 @@ func (mkline *MkLine) ForEachUsed(action
                searchIn(mkline.Sources(), VucLoadTime)
 
        case mkline.IsInclude():
-               searchIn(mkline.IncludedFile(), VucLoadTime)
+               searchIn(mkline.IncludedFile().String(), VucLoadTime)
        }
 }
 
@@ -1053,7 +1052,10 @@ type indentationLevel struct {
        // pkglint will happily accept .include "fname" in both the then and
        // the else branch. This is ok since the primary job of this file list
        // is to prevent wrong pkglint warnings about missing files.
-       checkedFiles []string
+       checkedFiles []Path
+
+       // whether the line is a multiple-inclusion guard
+       guard bool
 }
 
 func (ind *Indentation) IsEmpty() bool {
@@ -1085,9 +1087,9 @@ func (ind *Indentation) Pop() {
        ind.levels = ind.levels[:len(ind.levels)-1]
 }
 
-func (ind *Indentation) Push(mkline *MkLine, indent int, condition string) {
+func (ind *Indentation) Push(mkline *MkLine, indent int, args string, guard bool) {
        assert(mkline.IsDirective())
-       ind.levels = append(ind.levels, indentationLevel{mkline, indent, condition, nil, nil})
+       ind.levels = append(ind.levels, indentationLevel{mkline, indent, args, nil, nil, guard})
 }
 
 // AddVar remembers that the current indentation depends on the given variable,
@@ -1121,12 +1123,12 @@ func (ind *Indentation) DependsOn(varnam
 }
 
 // IsConditional returns whether the current line depends on evaluating
-// any variable in an .if or .elif expression or from a .for loop.
+// any .if or .elif expression, or is inside a .for loop.
 //
 // Variables named *_MK are excluded since they are usually not interesting.
 func (ind *Indentation) IsConditional() bool {
        for _, level := range ind.levels {
-               if len(level.conditionalVars) > 0 {
+               if !level.guard {
                        return true
                }
        }
@@ -1155,14 +1157,14 @@ func (ind *Indentation) Args() string {
        return ind.top().args
 }
 
-func (ind *Indentation) AddCheckedFile(filename string) {
+func (ind *Indentation) AddCheckedFile(filename Path) {
        top := ind.top()
        top.checkedFiles = append(top.checkedFiles, filename)
 }
 
 // HasExists returns whether the given filename has been tested in an
 // exists(filename) condition and thus may or may not exist.
-func (ind *Indentation) HasExists(filename string) bool {
+func (ind *Indentation) HasExists(filename Path) bool {
        for _, level := range ind.levels {
                for _, levelFilename := range level.checkedFiles {
                        if filename == levelFilename {
@@ -1177,13 +1179,16 @@ func (ind *Indentation) TrackBefore(mkli
        if !mkline.IsDirective() {
                return
        }
-       if trace.Tracing {
-               trace.Stepf("Indentation before line %s: %s", mkline.Linenos(), ind)
-       }
 
-       switch mkline.Directive() {
+       directive := mkline.Directive()
+       switch directive {
        case "for", "if", "ifdef", "ifndef":
-               ind.Push(mkline, ind.Depth(mkline.Directive()), mkline.Args())
+               guard := false
+               if directive == "if" {
+                       cond := mkline.Cond()
+                       guard = cond != nil && cond.Not != nil && hasSuffix(cond.Not.Defined, "_MK")
+               }
+               ind.Push(mkline, ind.Depth(directive), mkline.Args(), guard)
        }
 }
 
@@ -1197,11 +1202,8 @@ func (ind *Indentation) TrackAfter(mklin
 
        switch directive {
        case "if":
-               cond := mkline.Cond()
-
                // For multiple-inclusion guards, the indentation stays at the same level.
-               guard := cond != nil && cond.Not != nil && hasSuffix(cond.Not.Defined, "_MK")
-               if !guard {
+               if !ind.top().guard {
                        ind.top().depth += 2
                }
 
@@ -1237,17 +1239,13 @@ func (ind *Indentation) TrackAfter(mklin
                cond.Walk(&MkCondCallback{
                        Call: func(name string, arg string) {
                                if name == "exists" {
-                                       ind.AddCheckedFile(arg)
+                                       ind.AddCheckedFile(NewPath(arg))
                                }
                        }})
        }
-
-       if trace.Tracing {
-               trace.Stepf("Indentation after line %s: %s", mkline.Linenos(), ind)
-       }
 }
 
-func (ind *Indentation) CheckFinish(filename string) {
+func (ind *Indentation) CheckFinish(filename Path) {
        if ind.IsEmpty() {
                return
        }
@@ -1277,7 +1275,7 @@ var (
        VarparamBytes = textproc.NewByteSet("A-Za-z_0-9#*+---./[")
 )
 
-func MatchMkInclude(text string) (m bool, indentation, directive, filename string) {
+func MatchMkInclude(text string) (m bool, indentation, directive string, filename Path) {
        lexer := textproc.NewLexer(text)
        if lexer.SkipByte('.') {
                indentation = lexer.NextHspace()
@@ -1291,7 +1289,7 @@ func MatchMkInclude(text string) (m bool
                                // Note: strictly speaking, the full MkVarUse would have to be parsed
                                // here. But since these usually don't contain double quotes, it has
                                // worked fine up to now.
-                               filename = lexer.NextBytesFunc(func(c byte) bool { return c != '"' })
+                               filename = NewPath(lexer.NextBytesFunc(func(c byte) bool { return c != '"' }))
                                if filename != "" && lexer.SkipByte('"') {
                                        lexer.NextHspace()
                                        if lexer.EOF() {

Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.71 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.72
--- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.71   Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mkline_test.go        Sat Nov 23 23:35:56 2019
@@ -503,7 +503,7 @@ func (s *Suite) Test_MkLine_ResolveVarsI
                MkCvsID)
        mkline := mklines.mklines[0]
 
-       test := func(before string, after string) {
+       test := func(before Path, after Path) {
                t.CheckEquals(mkline.ResolveVarsInRelativePath(before), after)
        }
 
@@ -1129,7 +1129,7 @@ func (s *Suite) Test_MkLine_VariableNeed
                "")
 
        // Just for branch coverage.
-       trace.Tracing = false
+       t.DisableTracing()
        MkLineChecker{mklines, mklines.mklines[2]}.Check()
 
        t.CheckOutputEmpty()
@@ -1265,7 +1265,7 @@ func (s *Suite) Test_Indentation(c *chec
        t.CheckEquals(ind.Depth("if"), 0)
        t.CheckEquals(ind.DependsOn("VARNAME"), false)
 
-       ind.Push(mkline, 2, "")
+       ind.Push(mkline, 2, "", false)
 
        t.CheckEquals(ind.Depth("if"), 2)
        t.CheckEquals(ind.Depth("endfor"), 0)
@@ -1280,7 +1280,7 @@ func (s *Suite) Test_Indentation(c *chec
        t.CheckEquals(ind.DependsOn("LEVEL1.VAR1"), true)
        t.CheckEquals(ind.DependsOn("OTHER_VAR"), false)
 
-       ind.Push(mkline, 2, "")
+       ind.Push(mkline, 2, "", false)
 
        ind.AddVar("LEVEL2.VAR")
 
@@ -1308,7 +1308,7 @@ func (s *Suite) Test_Indentation__realis
                ".if 1",
                ".  if !defined(GUARD_MK)",
                ".  for var in 1 2 3",
-               ".    if !defined(GUARD_MK)",
+               ".    if !defined(GUARD_MK)", // well, not entirely realistic
                ".    if 3",
                ".    endif",
                ".    endif",
@@ -1322,43 +1322,29 @@ func (s *Suite) Test_Indentation__realis
                ".  endfor",
                ".endif")
 
-       t.EnableTracingToLog()
-
        mklines.ForEach(func(mkline *MkLine) {})
 
-       t.CheckOutputLinesMatching(`Indentation`,
-               "TRACE:   Indentation before line 3: []",
-               "TRACE:   Indentation after line 3: [2]",
-               "TRACE:   Indentation before line 4: [2]",
-               "TRACE:   Indentation after line 4: [2 2]",
-               "TRACE:   Indentation before line 5: [2 2]",
-               "TRACE:   Indentation after line 5: [2 2 4]",
-               "TRACE:   Indentation before line 6: [2 2 4]",
-               "TRACE:   Indentation after line 6: [2 2 4 4]",
-               "TRACE:   Indentation before line 7: [2 2 4 4]",
-               "TRACE:   Indentation after line 7: [2 2 4 4 6]",
-               "TRACE:   Indentation before line 8: [2 2 4 4 6]",
-               "TRACE:   Indentation after line 8: [2 2 4 4]",
-               "TRACE:   Indentation before line 9: [2 2 4 4]",
-               "TRACE:   Indentation after line 9: [2 2 4]",
-               "TRACE:   Indentation before line 10: [2 2 4]",
-               "TRACE:   Indentation after line 10: [2 2]",
-               "TRACE:   Indentation before line 11: [2 2]",
-               "TRACE:   Indentation after line 11: [2]",
-               "TRACE:   Indentation before line 12: [2]",
-               "TRACE:   Indentation after line 12: [2]",
-               "TRACE:   Indentation before line 13: [2]",
-               "TRACE:   Indentation after line 13: [2 4]",
-               "TRACE:   Indentation before line 14: [2 4]",
-               "TRACE:   Indentation after line 14: [2]",
-               "TRACE:   Indentation before line 15: [2]",
-               "TRACE:   Indentation after line 15: [2]",
-               "TRACE:   Indentation before line 16: [2]",
-               "TRACE:   Indentation after line 16: [2 4]",
-               "TRACE:   Indentation before line 17: [2 4]",
-               "TRACE:   Indentation after line 17: [2]",
-               "TRACE:   Indentation before line 18: [2]",
-               "TRACE:   Indentation after line 18: []")
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Indentation_String(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("filename.mk",
+               ".if exists(/bin)",
+               "# probably POSIX",
+               ".endif")
+       var str string
+
+       mklines.ForEach(func(mkline *MkLine) {
+               if mkline.IsComment() {
+                       t.CheckEquals(mklines.indentation.IsConditional(), true)
+                       t.Check(mklines.indentation.Varnames(), check.IsNil)
+                       str = mklines.indentation.String()
+               }
+       })
+
+       t.CheckEquals(str, "[2]")
 }
 
 func (s *Suite) Test_Indentation_RememberUsedVariables(c *check.C) {
@@ -1440,7 +1426,7 @@ func (s *Suite) Test_Indentation_TrackAf
 func (s *Suite) Test_MatchMkInclude(c *check.C) {
        t := s.Init(c)
 
-       test := func(input, expectedIndent, expectedDirective, expectedFilename, expectedComment string) {
+       test := func(input, expectedIndent, expectedDirective string, expectedFilename Path, expectedComment string) {
                splitResult := NewMkLineParser().split(nil, input, true)
                m, indent, directive, args := MatchMkInclude(splitResult.main)
                t.CheckDeepEquals(

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.53 pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.54
--- pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.53 Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker.go      Sat Nov 23 23:35:56 2019
@@ -3,9 +3,6 @@ package pkglint
 import (
        "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/textproc"
-       "os"
-       "path"
-       "path/filepath"
        "strconv"
        "strings"
 )
@@ -1089,9 +1086,9 @@ func (ck MkLineChecker) checkVarassignRi
 
        categories := mkline.ValueFields(mkline.Value())
        actual := categories[0]
-       expected := path.Base(path.Dir(path.Dir(mkline.Filename)))
+       expected := mkline.Filename.Dir().Dir().Base()
        if expected == "." {
-               expected = path.Base(path.Dir(path.Dir(G.Pkgsrc.ToRel(mkline.Filename))))
+               expected = G.Pkgsrc.ToRel(mkline.Filename).Dir().Dir().Base()
        }
        if expected == "wip" || actual == expected {
                return
@@ -1194,7 +1191,7 @@ func (ck MkLineChecker) checkVarassignMi
        }
 
        for _, dir := range mkline.ValueFields(mkline.Value()) {
-               if G.Pkg.Plist.Dirs[dir] != nil {
+               if G.Pkg.Plist.Dirs[NewPath(dir)] != nil {
                        mkline.Notef("The directory %q is redundant in %s.", dir, varname)
                        mkline.Explain(
                                "This package defines AUTO_MKDIR, and the directory is contained in the PLIST.",
@@ -1308,12 +1305,12 @@ func (ck MkLineChecker) checkInclude() {
        includedFile := mkline.IncludedFile()
        mustExist := mkline.MustExist()
        if trace.Tracing {
-               trace.Step2("includingFile=%s includedFile=%s", mkline.Filename, includedFile)
+               trace.Stepf("includingFile=%s includedFile=%s", mkline.Filename, includedFile)
        }
        ck.CheckRelativePath(includedFile, mustExist)
 
        switch {
-       case hasSuffix(includedFile, "/Makefile"):
+       case includedFile.HasBase("Makefile"):
                mkline.Errorf("Other Makefiles must not be included directly.")
                mkline.Explain(
                        "To include portions of another Makefile, extract the common parts",
@@ -1329,25 +1326,25 @@ func (ck MkLineChecker) checkInclude() {
                        fix.Apply()
                }
 
-       case hasSuffix(includedFile, "pkgtools/x11-links/buildlink3.mk"):
+       case includedFile.HasSuffixPath("pkgtools/x11-links/buildlink3.mk"):
                fix := mkline.Autofix()
                fix.Errorf("%s must not be included directly. Include \"../../mk/x11.buildlink3.mk\" instead.", includedFile)
                fix.Replace("pkgtools/x11-links/buildlink3.mk", "mk/x11.buildlink3.mk")
                fix.Apply()
 
-       case hasSuffix(includedFile, "graphics/jpeg/buildlink3.mk"):
+       case includedFile.HasSuffixPath("graphics/jpeg/buildlink3.mk"):
                fix := mkline.Autofix()
                fix.Errorf("%s must not be included directly. Include \"../../mk/jpeg.buildlink3.mk\" instead.", includedFile)
                fix.Replace("graphics/jpeg/buildlink3.mk", "mk/jpeg.buildlink3.mk")
                fix.Apply()
 
-       case hasSuffix(includedFile, "/intltool/buildlink3.mk"):
+       case includedFile.HasSuffixPath("intltool/buildlink3.mk"):
                mkline.Warnf("Please write \"USE_TOOLS+= intltool\" instead of this line.")
 
-       case hasSuffix(includedFile, "/builtin.mk"):
+       case includedFile.HasSuffixText("/builtin.mk"): // TODO: maybe HasSuffixPath
                if mkline.Basename != "hacks.mk" && !mkline.HasRationale() {
                        fix := mkline.Autofix()
-                       fix.Errorf("%s must not be included directly. Include \"%s/buildlink3.mk\" instead.", includedFile, path.Dir(includedFile))
+                       fix.Errorf("%s must not be included directly. Include \"%s/buildlink3.mk\" instead.", includedFile, includedFile.Dir())
                        fix.Replace("builtin.mk", "buildlink3.mk")
                        fix.Apply()
                }
@@ -1370,28 +1367,28 @@ func (ck MkLineChecker) checkDirectiveIn
 
 // CheckRelativePath checks a relative path that leads to the directory of another package
 // or to a subdirectory thereof or a file within there.
-func (ck MkLineChecker) CheckRelativePath(relativePath string, mustExist bool) {
+func (ck MkLineChecker) CheckRelativePath(relativePath Path, mustExist bool) {
        if trace.Tracing {
                defer trace.Call(relativePath, mustExist)()
        }
 
        mkline := ck.MkLine
-       if !G.Wip && contains(relativePath, "/wip/") {
+       if !G.Wip && relativePath.ContainsPath("wip") {
                mkline.Errorf("A main pkgsrc package must not depend on a pkgsrc-wip package.")
        }
 
        resolvedPath := mkline.ResolveVarsInRelativePath(relativePath)
-       if containsVarRef(resolvedPath) {
+       if containsVarRef(resolvedPath.String()) {
                return
        }
 
-       if filepath.IsAbs(resolvedPath) {
+       if resolvedPath.IsAbs() {
                mkline.Errorf("The path %q must be relative.", resolvedPath)
                return
        }
 
-       abs := joinPath(path.Dir(mkline.Filename), resolvedPath)
-       if _, err := os.Stat(abs); err != nil {
+       abs := mkline.Filename.Dir().JoinNoClean(resolvedPath)
+       if !abs.Exists() {
                if mustExist && !ck.MkLines.indentation.HasExists(resolvedPath) {
                        mkline.Errorf("Relative path %q does not exist.", resolvedPath)
                }
@@ -1399,21 +1396,21 @@ func (ck MkLineChecker) CheckRelativePat
        }
 
        switch {
-       case !hasPrefix(resolvedPath, "../"):
+       case !resolvedPath.HasPrefixPath(".."):
                break
 
-       case hasPrefix(resolvedPath, "../../mk/"):
+       case resolvedPath.HasPrefixPath("../../mk"):
                // From a package to the infrastructure.
 
-       case matches(resolvedPath, `^\.\./\.\./[^./][^/]*/[^/]`):
+       case matches(resolvedPath.String(), `^\.\./\.\./[^./][^/]*/[^/]`):
                // From a package to another package.
 
-       case hasPrefix(resolvedPath, "../mk/") && relpath(path.Dir(mkline.Filename), G.Pkgsrc.File(".")) == "..":
+       case resolvedPath.HasPrefixPath("../mk") && G.Pkgsrc.ToRel(mkline.Filename).Count() == 2:
                // For category Makefiles.
                // TODO: Or from a pkgsrc wip package to wip/mk.
 
-       case matches(resolvedPath, `^\.\./[^./][^/]*/[^/]`):
-               if G.Wip && contains(resolvedPath, "/mk/") {
+       case matches(resolvedPath.String(), `^\.\./[^./][^/]*/[^/]`):
+               if G.Wip && resolvedPath.ContainsPath("mk") {
                        mkline.Warnf("References to the pkgsrc-wip infrastructure should look like \"../../wip/mk\", not \"../mk\".")
                } else {
                        mkline.Warnf("References to other packages should look like \"../../category/package\", not \"../package\".")
@@ -1433,16 +1430,16 @@ func (ck MkLineChecker) CheckRelativePat
 //
 // When used in .include directives, the relative package directories must be written
 // with the leading ../.. anyway, so the benefit might not be too big at all.
-func (ck MkLineChecker) CheckRelativePkgdir(pkgdir string) {
+func (ck MkLineChecker) CheckRelativePkgdir(pkgdir Path) {
        if trace.Tracing {
-               defer trace.Call1(pkgdir)()
+               defer trace.Call(pkgdir)()
        }
 
        mkline := ck.MkLine
        ck.CheckRelativePath(pkgdir+"/Makefile", true)
        pkgdir = mkline.ResolveVarsInRelativePath(pkgdir)
 
-       if !matches(pkgdir, `^\.\./\.\./([^./][^/]*/[^./][^/]*)$`) && !containsVarRef(pkgdir) {
+       if !matches(pkgdir.String(), `^\.\./\.\./([^./][^/]*/[^./][^/]*)$`) && !containsVarRef(pkgdir.String()) {
                mkline.Warnf("%q is not a valid relative package directory.", pkgdir)
                mkline.Explain(
                        "A relative pathname always starts with \"../../\", followed",
@@ -1722,11 +1719,30 @@ func (ck MkLineChecker) checkCompareVarS
        ck.checkVartype(varname, opUseCompare, value, "")
 
        if varname == "PKGSRC_COMPILER" {
-               ck.MkLine.Warnf("Use ${PKGSRC_COMPILER:%s%s} instead of the %s operator.", condStr(op == "==", "M", "N"), value, op)
-               ck.MkLine.Explain(
-                       "The PKGSRC_COMPILER can be a list of chained compilers, e.g. \"ccache distcc clang\".",
-                       "Therefore, comparing it using == or != leads to wrong results in these cases.")
+               ck.checkCompareVarStrCompiler(op, value)
+       }
+}
+
+func (ck MkLineChecker) checkCompareVarStrCompiler(op string, value string) {
+       if !matches(value, `^\w+$`) {
+               return
        }
+
+       // It would be nice if original text of the whole comparison expression
+       // were available at this point, to avoid guessing how much whitespace
+       // the package author really used.
+
+       matchOp := condStr(op == "==", "M", "N")
+
+       fix := ck.MkLine.Autofix()
+       fix.Errorf("Use ${PKGSRC_COMPILER:%s%s} instead of the %s operator.", matchOp, value, op)
+       fix.Explain(
+               "The PKGSRC_COMPILER can be a list of chained compilers, e.g. \"ccache distcc clang\".",
+               "Therefore, comparing it using == or != leads to wrong results in these cases.")
+       fix.Replace("${PKGSRC_COMPILER} "+op+" "+value, "${PKGSRC_COMPILER:"+matchOp+value+"}")
+       fix.Replace("${PKGSRC_COMPILER} "+op+" \""+value+"\"", "${PKGSRC_COMPILER:"+matchOp+value+"}")
+       fix.Anyway()
+       fix.Apply()
 }
 
 func (ck MkLineChecker) checkDirectiveFor(forVars map[string]bool, indentation *Indentation) {

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.48 pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.49
--- pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.48    Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go Sat Nov 23 23:35:56 2019
@@ -102,6 +102,9 @@ func (s *Suite) Test_MkLineChecker_Check
        t.CheckOutputLines(
                "WARN: x11/xkeyboard-config/Makefile:3: "+
                        "Invalid part \"/xkbcomp\" after variable name \"${XKBBASE}\".",
+               // TODO: Avoid this duplicate diagnostic.
+               "WARN: x11/xkeyboard-config/Makefile:3: "+
+                       "Invalid part \"/xkbcomp\" after variable name \"${XKBBASE}\".",
                "WARN: x11/xkeyboard-config/Makefile:3: XKBBASE is used but not defined.")
 }
 
@@ -2413,7 +2416,7 @@ func (s *Suite) Test_MkLineChecker_Check
 
        t.CreateFileLines("other/package/Makefile")
 
-       test := func(relativePkgdir string, diagnostics ...string) {
+       test := func(relativePkgdir Path, diagnostics ...string) {
                // Must be in the filesystem because of directory references.
                mklines := t.SetUpFileMkLines("category/package/Makefile",
                        "# dummy")
@@ -2614,7 +2617,7 @@ func (s *Suite) Test_MkLineChecker_check
        test(".if ${PKGSRC_COMPILER} == \"msvc\"",
                "WARN: filename.mk:1: \"msvc\" is not valid for PKGSRC_COMPILER. "+
                        "Use one of { ccache ccc clang distcc f2c gcc hp icc ido mipspro mipspro-ucode pcc sunpro xlc } instead.",
-               "WARN: filename.mk:1: Use ${PKGSRC_COMPILER:Mmsvc} instead of the == operator.")
+               "ERROR: filename.mk:1: Use ${PKGSRC_COMPILER:Mmsvc} instead of the == operator.")
 
        test(".if ${PKG_LIBTOOL:Mlibtool}",
                "NOTE: filename.mk:1: PKG_LIBTOOL should be compared using == instead of matching against \":Mlibtool\".",
@@ -2668,10 +2671,11 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: filename.mk:1: VAR is used but not defined.")
 
        test(".if ${MASTER_SITES:Mftp://*} == \"ftp://netbsd.org/\"";,
+               // FIXME: duplicate diagnostic, see MkParser.MkCond.
+               "WARN: filename.mk:1: Invalid variable modifier \"//*\" for \"MASTER_SITES\".",
                "WARN: filename.mk:1: Invalid variable modifier \"//*\" for \"MASTER_SITES\".",
                "WARN: filename.mk:1: \"ftp\" is not a valid URL.",
-               "WARN: filename.mk:1: MASTER_SITES should not be used at load time in any file.",
-               "WARN: filename.mk:1: Invalid variable modifier \"//*\" for \"MASTER_SITES\".")
+               "WARN: filename.mk:1: MASTER_SITES should not be used at load time in any file.")
 }
 
 func (s *Suite) Test_MkLineChecker_checkDirectiveCond__tracing(c *check.C) {
@@ -2703,7 +2707,7 @@ func (s *Suite) Test_MkLineChecker_check
 
        // Don't warn about unknown shell command "cc".
        t.CheckOutputLines(
-               "WARN: security/openssl/Makefile:2: Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.")
+               "ERROR: security/openssl/Makefile:2: Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.")
 }
 
 // The :N modifier filters unwanted values. After this filter, any variable value
@@ -2746,8 +2750,8 @@ func (s *Suite) Test_MkLineChecker_check
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: Makefile:2: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
-               "WARN: Makefile:3: Use ${PKGSRC_COMPILER:Ngcc} instead of the != operator.")
+               "ERROR: Makefile:2: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
+               "ERROR: Makefile:3: Use ${PKGSRC_COMPILER:Ngcc} instead of the != operator.")
 }
 
 func (s *Suite) Test_MkLineChecker_checkDirectiveCondEmpty(c *check.C) {
@@ -3062,6 +3066,62 @@ func (s *Suite) Test_MkLineChecker_check
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_MkLineChecker_checkCompareVarStrCompiler(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpVartypes()
+       t.Chdir(".")
+
+       test := func(cond string, diagnostics ...string) {
+               mklines := t.SetUpFileMkLines("filename.mk",
+                       MkCvsID,
+                       "",
+                       ".if "+cond,
+                       ".endif")
+
+               t.SetUpCommandLine("-Wall")
+               mklines.Check()
+               t.SetUpCommandLine("-Wall", "--autofix")
+               mklines.Check()
+
+               t.CheckOutput(diagnostics)
+       }
+
+       test(
+               "${PKGSRC_COMPILER} == gcc",
+
+               "ERROR: filename.mk:3: "+
+                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.",
+               "AUTOFIX: filename.mk:3: "+
+                       "Replacing \"${PKGSRC_COMPILER} == gcc\" "+
+                       "with \"${PKGSRC_COMPILER:Mgcc}\".")
+
+       // No autofix because of missing whitespace.
+       // TODO: Provide the autofix regardless of the whitespace.
+       test(
+               "${PKGSRC_COMPILER}==gcc",
+
+               "ERROR: filename.mk:3: "+
+                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.")
+
+       // The comparison value can be with or without quotes.
+       test(
+               "${PKGSRC_COMPILER} == \"gcc\"",
+
+               "ERROR: filename.mk:3: "+
+                       "Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.",
+               "AUTOFIX: filename.mk:3: "+
+                       "Replacing \"${PKGSRC_COMPILER} == \\\"gcc\\\"\" "+
+                       "with \"${PKGSRC_COMPILER:Mgcc}\".")
+
+       // No warning because it is not obvious what is meant here.
+       // This case probably doesn't occur in practice.
+       test(
+               "${PKGSRC_COMPILER} == \"distcc gcc\"",
+
+               nil...)
+}
+
 func (s *Suite) Test_MkLineChecker_checkDirectiveFor(c *check.C) {
        t := s.Init(c)
 
Index: pkgsrc/pkgtools/pkglint/files/shell.go
diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.48 pkgsrc/pkgtools/pkglint/files/shell.go:1.49
--- pkgsrc/pkgtools/pkglint/files/shell.go:1.48 Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/shell.go      Sat Nov 23 23:35:56 2019
@@ -250,7 +250,7 @@ func (scc *SimpleCommandChecker) checkAu
                        if m, dirname := match1(arg, `^(?:\$\{DESTDIR\})?\$\{PREFIX(?:|:Q)\}/(.*)`); m {
                                autoMkdirs := false
                                if G.Pkg != nil {
-                                       plistLine := G.Pkg.Plist.Dirs[dirname]
+                                       plistLine := G.Pkg.Plist.Dirs[NewPath(dirname)]
                                        if plistLine != nil && !containsVarRef(plistLine.Text) {
                                                autoMkdirs = true
                                        }
@@ -760,7 +760,7 @@ func (ck *ShellLineChecker) CheckWord(to
 }
 
 func (ck *ShellLineChecker) checkWordQuoting(token string, checkQuoting bool, time ToolTime) {
-       tok := NewShTokenizer(ck.mkline.Line, token, false)
+       tok := NewShTokenizer(ck.mkline.Line, token, true)
 
        atoms := tok.ShAtoms()
        quoting := shqPlain

Index: pkgsrc/pkgtools/pkglint/files/mklineparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.3 pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.4
--- pkgsrc/pkgtools/pkglint/files/mklineparser.go:1.3   Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mklineparser.go       Sat Nov 23 23:35:56 2019
@@ -247,7 +247,7 @@ func (p MkLineParser) parseSysinclude(li
                return nil
        }
 
-       return &MkLine{line, splitResult, &mkLineInclude{directive == "include", true, indent, includedFile, nil}}
+       return &MkLine{line, splitResult, &mkLineInclude{directive == "include", true, indent, NewPath(includedFile), nil}}
 }
 
 func (p MkLineParser) parseDependency(line *Line, splitResult mkLineSplitResult) *MkLine {
Index: pkgsrc/pkgtools/pkglint/files/mklineparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklineparser_test.go:1.3 pkgsrc/pkgtools/pkglint/files/mklineparser_test.go:1.4
--- pkgsrc/pkgtools/pkglint/files/mklineparser_test.go:1.3      Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mklineparser_test.go  Sat Nov 23 23:35:56 2019
@@ -543,7 +543,7 @@ func (s *Suite) Test_MkLineParser_parseI
        t.CheckEquals(mkline.IsInclude(), true)
        t.CheckEquals(mkline.Indent(), "    ")
        t.CheckEquals(mkline.MustExist(), true)
-       t.CheckEquals(mkline.IncludedFile(), "../../mk/bsd.prefs.mk")
+       t.CheckEquals(mkline.IncludedFile(), NewPath("../../mk/bsd.prefs.mk"))
 
        t.CheckEquals(mkline.IsSysinclude(), false)
 }
@@ -557,7 +557,7 @@ func (s *Suite) Test_MkLineParser_parseS
        t.CheckEquals(mkline.IsSysinclude(), true)
        t.CheckEquals(mkline.Indent(), "    ")
        t.CheckEquals(mkline.MustExist(), true)
-       t.CheckEquals(mkline.IncludedFile(), "subdir.mk")
+       t.CheckEquals(mkline.IncludedFile(), NewPath("subdir.mk"))
 
        t.CheckEquals(mkline.IsInclude(), false)
 }

Index: pkgsrc/pkgtools/pkglint/files/mklines.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines.go:1.59 pkgsrc/pkgtools/pkglint/files/mklines.go:1.60
--- pkgsrc/pkgtools/pkglint/files/mklines.go:1.59       Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines.go    Sat Nov 23 23:35:56 2019
@@ -70,7 +70,7 @@ func NewMkLines(lines *Lines) *MkLines {
 
 func (mklines *MkLines) Check() {
        if trace.Tracing {
-               defer trace.Call1(mklines.lines.Filename)()
+               defer trace.Call(mklines.lines.Filename)()
        }
 
        // In the first pass, all additions to BUILD_DEFS and USE_TOOLS
@@ -440,7 +440,7 @@ func (mklines *MkLines) checkVarassignPl
 
 // CheckUsedBy checks that this file (a Makefile.common) has the given
 // relativeName in one of the "# used by" comments at the beginning of the file.
-func (mklines *MkLines) CheckUsedBy(relativeName string) {
+func (mklines *MkLines) CheckUsedBy(relativeName Path) {
        lines := mklines.lines
        if lines.Len() < 3 {
                return
@@ -448,7 +448,7 @@ func (mklines *MkLines) CheckUsedBy(rela
 
        paras := mklines.SplitToParagraphs()
 
-       expected := "# used by " + relativeName
+       expected := "# used by " + relativeName.String()
        found := false
        var usedParas []*Paragraph
 
Index: pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.59 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.60
--- pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.59     Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go  Sat Nov 23 23:35:56 2019
@@ -498,7 +498,7 @@ func (s *Suite) Test_VartypeCheck_Enum__
                "\tlike ^, *, $. In such a case, using the :M or :N modifiers is useful",
                "\tand preferred.",
                "",
-               "WARN: module.mk:5: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
+               "ERROR: module.mk:5: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.",
                "",
                "\tThe PKGSRC_COMPILER can be a list of chained compilers, e.g. \"ccache",
                "\tdistcc clang\". Therefore, comparing it using == or != leads to wrong",
@@ -1792,7 +1792,7 @@ func (s *Suite) Test_VartypeCheck_YesNoI
 type VartypeCheckTester struct {
        tester    *Tester
        basicType *BasicType
-       filename  string
+       filename  Path
        lineno    int
        varname   string
        op        MkOperator
@@ -1820,7 +1820,7 @@ func (vt *VartypeCheckTester) Varname(va
        vt.nextSection()
 }
 
-func (vt *VartypeCheckTester) File(filename string) {
+func (vt *VartypeCheckTester) File(filename Path) {
        vt.filename = filename
        vt.lineno = 1
 }

Index: pkgsrc/pkgtools/pkglint/files/mklines_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.51 pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.52
--- pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.51  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/mklines_test.go       Sat Nov 23 23:35:56 2019
@@ -1069,7 +1069,7 @@ func (s *Suite) Test_MkLines_CheckUsedBy
 
        t.SetUpCommandLine("--show-autofix")
 
-       test := func(pkgpath string, lines []string, diagnostics []string) {
+       test := func(pkgpath Path, lines []string, diagnostics []string) {
                mklines := t.NewMkLines("Makefile.common", lines...)
 
                mklines.CheckUsedBy(pkgpath)
@@ -1155,7 +1155,7 @@ func (s *Suite) Test_MkLines_CheckUsedBy
 func (s *Suite) Test_MkLines_CheckUsedBy(c *check.C) {
        t := s.Init(c)
 
-       test := func(pkgpath string, lines []string, diagnostics []string) {
+       test := func(pkgpath Path, lines []string, diagnostics []string) {
                mklines := t.NewMkLines("Makefile.common", lines...)
 
                mklines.CheckUsedBy(pkgpath)

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.68 pkgsrc/pkgtools/pkglint/files/package.go:1.69
--- pkgsrc/pkgtools/pkglint/files/package.go:1.68       Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/package.go    Sat Nov 23 23:35:56 2019
@@ -3,7 +3,6 @@ package pkglint
 import (
        "netbsd.org/pkglint/pkgver"
        "os"
-       "path"
        "strconv"
        "strings"
 )
@@ -17,12 +16,12 @@ const rePkgname = `^([\w\-.+]+)-(\d[.0-9
 // This is necessary because variables in Makefiles may be used before they are defined,
 // and such dependencies often span multiple files that are included indirectly.
 type Package struct {
-       dir                  string       // The directory of the package, for resolving files
-       Pkgpath              string       // e.g. "category/pkgdir"
-       Pkgdir               string       // PKGDIR from the package Makefile
-       Filesdir             string       // FILESDIR from the package Makefile
-       Patchdir             string       // PATCHDIR from the package Makefile
-       DistinfoFile         string       // DISTINFO_FILE from the package Makefile
+       dir                  Path         // The directory of the package, for resolving files
+       Pkgpath              Path         // e.g. "category/pkgdir"
+       Pkgdir               Path         // PKGDIR from the package Makefile
+       Filesdir             Path         // FILESDIR from the package Makefile
+       Patchdir             Path         // PATCHDIR from the package Makefile
+       DistinfoFile         Path         // DISTINFO_FILE from the package Makefile
        EffectivePkgname     string       // PKGNAME or DISTNAME from the package Makefile, including nb13, can be empty
        EffectivePkgbase     string       // EffectivePkgname without the version
        EffectivePkgversion  string       // The version part of the effective PKGNAME, excluding nb13
@@ -32,7 +31,7 @@ type Package struct {
        vars      Scope
        redundant *RedundantScope
 
-       bl3 map[string]*MkLine // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included.
+       bl3 map[Path]*MkLine // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included.
 
        // Remembers the Makefile fragments that have already been included.
        // The key to the map is the filename relative to the package directory.
@@ -50,21 +49,24 @@ type Package struct {
 
        // Files from .include lines that are nested inside .if.
        // They often depend on OPSYS or on the existence of files in the build environment.
-       conditionalIncludes map[string]*MkLine
+       conditionalIncludes map[Path]*MkLine
        // Files from .include lines that are not nested.
        // These are cross-checked with buildlink3.mk whether they are unconditional there, too.
-       unconditionalIncludes map[string]*MkLine
+       unconditionalIncludes map[Path]*MkLine
 
        IgnoreMissingPatches bool // In distinfo, don't warn about patches that cannot be found.
 
        Once Once
 }
 
-func NewPackage(dir string) *Package {
+func NewPackage(dir Path) *Package {
        pkgpath := G.Pkgsrc.ToRel(dir)
 
        // Package directory must be two subdirectories below the pkgsrc root.
-       assert(strings.Count(pkgpath, "/") == 1)
+       // As of November 2019, it is technically possible to create packages
+       // on different levels, but that is not used at all. Therefore all
+       // relative directories are in the form "../../category/package".
+       assert(pkgpath.Count() == 2)
 
        pkg := Package{
                dir:                   dir,
@@ -75,10 +77,10 @@ func NewPackage(dir string) *Package {
                DistinfoFile:          "${PKGDIR}/distinfo", // TODO: Redundant, see the vars.Fallback below.
                Plist:                 NewPlistContent(),
                vars:                  NewScope(),
-               bl3:                   make(map[string]*MkLine),
+               bl3:                   make(map[Path]*MkLine),
                included:              Once{},
-               conditionalIncludes:   make(map[string]*MkLine),
-               unconditionalIncludes: make(map[string]*MkLine),
+               conditionalIncludes:   make(map[Path]*MkLine),
+               unconditionalIncludes: make(map[Path]*MkLine),
        }
        pkg.vars.DefineAll(G.Pkgsrc.UserDefinedVars)
 
@@ -97,7 +99,7 @@ func NewPackage(dir string) *Package {
        return &pkg
 }
 
-func (pkg *Package) load() ([]string, *MkLines, *MkLines) {
+func (pkg *Package) load() ([]Path, *MkLines, *MkLines) {
        // Load the package Makefile and all included files,
        // to collect all used and defined variables and similar data.
        mklines, allLines := pkg.loadPackageMakefile()
@@ -110,21 +112,30 @@ func (pkg *Package) load() ([]string, *M
                files = append(files, dirglob(pkg.File(pkg.Pkgdir))...)
        }
        files = append(files, dirglob(pkg.File(pkg.Patchdir))...)
-       if pkg.DistinfoFile != pkg.vars.fallback["DISTINFO_FILE"] {
+       if pkg.DistinfoFile != NewPath(pkg.vars.fallback["DISTINFO_FILE"]) {
                files = append(files, pkg.File(pkg.DistinfoFile))
        }
 
+       isRelevantMk := func(filename Path, basename string) bool {
+               if !hasPrefix(basename, "Makefile.") && !filename.HasSuffixText(".mk") {
+                       return false
+               }
+               if filename.Dir().Base() == "patches" {
+                       return false
+               }
+               if pkg.Pkgdir == "." {
+                       return true
+               }
+               return !filename.ContainsPath(pkg.Pkgdir)
+       }
+
        // Determine the used variables and PLIST directories before checking any of the Makefile fragments.
        // TODO: Why is this code necessary? What effect does it have?
        pkg.collectConditionalIncludes(mklines)
        for _, filename := range files {
-               basename := path.Base(filename)
-               if (hasPrefix(basename, "Makefile.") || hasSuffix(filename, ".mk")) &&
-                       !matches(filename, `patch-`) &&
-                       !contains(filename, pkg.Pkgdir+"/") &&
-                       !contains(filename, pkg.Filesdir+"/") {
+               basename := filename.Base()
+               if isRelevantMk(filename, basename) {
                        fragmentMklines := LoadMk(filename, MustSucceed)
-                       fragmentMklines.collectUsedVariables()
                        pkg.collectConditionalIncludes(fragmentMklines)
                }
                if hasPrefix(basename, "PLIST") {
@@ -138,7 +149,7 @@ func (pkg *Package) load() ([]string, *M
 func (pkg *Package) loadPackageMakefile() (*MkLines, *MkLines) {
        filename := pkg.File("Makefile")
        if trace.Tracing {
-               defer trace.Call1(filename)()
+               defer trace.Call(filename)()
        }
 
        mainLines := LoadMk(filename, NotEmpty|LogErrors)
@@ -170,10 +181,10 @@ func (pkg *Package) loadPackageMakefile(
 
        allLines.collectUsedVariables()
 
-       pkg.Pkgdir = pkg.vars.LastValue("PKGDIR")
-       pkg.DistinfoFile = pkg.vars.LastValue("DISTINFO_FILE")
-       pkg.Filesdir = pkg.vars.LastValue("FILESDIR")
-       pkg.Patchdir = pkg.vars.LastValue("PATCHDIR")
+       pkg.Pkgdir = NewPath(pkg.vars.LastValue("PKGDIR"))
+       pkg.DistinfoFile = NewPath(pkg.vars.LastValue("DISTINFO_FILE"))
+       pkg.Filesdir = NewPath(pkg.vars.LastValue("FILESDIR"))
+       pkg.Patchdir = NewPath(pkg.vars.LastValue("PATCHDIR"))
 
        // See lang/php/ext.mk
        if pkg.vars.IsDefinedSimilar("PHPEXT_MK") {
@@ -191,19 +202,19 @@ func (pkg *Package) loadPackageMakefile(
        }
 
        if trace.Tracing {
-               trace.Step1("DISTINFO_FILE=%s", pkg.DistinfoFile)
-               trace.Step1("FILESDIR=%s", pkg.Filesdir)
-               trace.Step1("PATCHDIR=%s", pkg.Patchdir)
-               trace.Step1("PKGDIR=%s", pkg.Pkgdir)
+               trace.Stepf("DISTINFO_FILE=%s", pkg.DistinfoFile)
+               trace.Stepf("FILESDIR=%s", pkg.Filesdir)
+               trace.Stepf("PATCHDIR=%s", pkg.Patchdir)
+               trace.Stepf("PKGDIR=%s", pkg.Pkgdir)
        }
 
        return mainLines, allLines
 }
 
 // TODO: What is allLines used for, is it still necessary? Would it be better as a field in Package?
-func (pkg *Package) parse(mklines *MkLines, allLines *MkLines, includingFileForUsedCheck string) bool {
+func (pkg *Package) parse(mklines *MkLines, allLines *MkLines, includingFileForUsedCheck Path) bool {
        if trace.Tracing {
-               defer trace.Call1(mklines.lines.Filename)()
+               defer trace.Call(mklines.lines.Filename)()
        }
 
        result := mklines.ForEachEnd(
@@ -217,10 +228,10 @@ func (pkg *Package) parse(mklines *MkLin
        // For every included buildlink3.mk, include the corresponding builtin.mk
        // automatically since the pkgsrc infrastructure does the same.
        filename := mklines.lines.Filename
-       if path.Base(filename) == "buildlink3.mk" {
-               builtin := cleanpath(path.Dir(filename) + "/builtin.mk")
+       if filename.Base() == "buildlink3.mk" {
+               builtin := cleanpath(filename.Dir().JoinNoClean("builtin.mk"))
                builtinRel := relpath(pkg.dir, builtin)
-               if pkg.included.FirstTime(builtinRel) && fileExists(builtin) {
+               if pkg.included.FirstTime(builtinRel.String()) && builtin.IsFile() {
                        builtinMkLines := LoadMk(builtin, MustSucceed|LogErrors)
                        pkg.parse(builtinMkLines, allLines, "")
                }
@@ -246,8 +257,8 @@ func (pkg *Package) parseLine(mklines *M
                        return false
                }
 
-               filenameForUsedCheck := ""
-               dir, base := path.Split(includedFile)
+               filenameForUsedCheck := NewPath("")
+               dir, base := includedFile.Split()
                if dir != "" && base == "Makefile.common" && dir != "../../"+pkg.Pkgpath+"/" {
                        filenameForUsedCheck = includingFile
                }
@@ -276,14 +287,14 @@ func (pkg *Package) parseLine(mklines *M
 // the included file is not processed further for whatever reason. But if
 // skip is false, the file could not be read and an appropriate error message
 // has already been logged.
-func (pkg *Package) loadIncluded(mkline *MkLine, includingFile string) (includedMklines *MkLines, skip bool) {
+func (pkg *Package) loadIncluded(mkline *MkLine, includingFile Path) (includedMklines *MkLines, skip bool) {
        includedFile := pkg.resolveIncludedFile(mkline, includingFile)
 
        if includedFile == "" {
                return nil, true
        }
 
-       dirname, _ := path.Split(includingFile)
+       dirname, _ := includingFile.Split()
        dirname = cleanpath(dirname)
        fullIncluded := joinPath(dirname, includedFile)
        relIncludedFile := relpath(pkg.dir, fullIncluded)
@@ -292,14 +303,14 @@ func (pkg *Package) loadIncluded(mkline 
                return nil, true
        }
 
-       if !pkg.included.FirstTime(relIncludedFile) {
+       if !pkg.included.FirstTime(relIncludedFile.String()) {
                return nil, true
        }
 
        pkg.collectSeenInclude(mkline, includedFile)
 
        if trace.Tracing {
-               trace.Step1("Including %q.", fullIncluded)
+               trace.Stepf("Including %q.", fullIncluded)
        }
        includedMklines = LoadMk(fullIncluded, 0)
        if includedMklines != nil {
@@ -330,7 +341,7 @@ func (pkg *Package) loadIncluded(mkline 
        }
 
        mkline.Notef("The path to the included file should be %q.",
-               relpath(path.Dir(mkline.Filename), fullIncludedFallback))
+               mkline.PathToFile(fullIncludedFallback))
        mkline.Explain(
                "The .include directive first searches the file relative to the including file.",
                "And if that doesn't exist, falls back to the current directory, which in the",
@@ -344,13 +355,15 @@ func (pkg *Package) loadIncluded(mkline 
 
 // resolveIncludedFile resolves Makefile variables such as ${PKGPATH} to
 // their actual values.
-func (pkg *Package) resolveIncludedFile(mkline *MkLine, includingFilename string) string {
+func (pkg *Package) resolveIncludedFile(mkline *MkLine, includingFilename Path) Path {
 
        // TODO: resolveVariableRefs uses G.Pkg implicitly. It should be made explicit.
        // TODO: Try to combine resolveVariableRefs and ResolveVarsInRelativePath.
-       includedFile := resolveVariableRefs(nil /* XXX: or maybe some mklines? */, mkline.ResolveVarsInRelativePath(mkline.IncludedFile()))
-       if containsVarRef(includedFile) {
-               if trace.Tracing && !contains(includingFilename, "/mk/") {
+       resolved := mkline.ResolveVarsInRelativePath(mkline.IncludedFile())
+       includedText := resolveVariableRefs(nil /* XXX: or maybe some mklines? */, resolved.String())
+       includedFile := NewPath(includedText)
+       if containsVarRef(includedText) {
+               if trace.Tracing && !includingFilename.ContainsPath("mk") {
                        trace.Stepf("%s:%s: Skipping unresolvable include file %q.",
                                mkline.Filename, mkline.Linenos(), includedFile)
                }
@@ -358,10 +371,10 @@ func (pkg *Package) resolveIncludedFile(
        }
 
        if mkline.Basename != "buildlink3.mk" {
-               if hasSuffix(includedFile, "/buildlink3.mk") {
+               if includedFile.HasSuffixText("/buildlink3.mk") {
                        pkg.bl3[includedFile] = mkline
                        if trace.Tracing {
-                               trace.Step1("Buildlink3 file in package: %q", includedFile)
+                               trace.Step1("Buildlink3 file in package: %q", includedText)
                        }
                }
        }
@@ -373,28 +386,31 @@ func (pkg *Package) resolveIncludedFile(
 //
 // The includingFile is relative to the current working directory,
 // the includedFile is taken directly from the .include directive.
-func (*Package) shouldDiveInto(includingFile string, includedFile string) bool {
+func (*Package) shouldDiveInto(includingFile, includedFile Path) bool {
 
-       if hasSuffix(includedFile, "/bsd.pkg.mk") || IsPrefs(includedFile) {
+       if includedFile.HasSuffixPath("bsd.pkg.mk") || IsPrefs(includedFile) {
                return false
        }
 
-       if contains(includingFile, "/mk/") && !hasPrefix(G.Pkgsrc.ToRel(includingFile), "wip/mk") {
-               return hasSuffix(includingFile, "buildlink3.mk") && hasSuffix(includedFile, "builtin.mk")
+       // FIXME: includingFile may be "../../mk/../devel/readline/buildlink.mk" and thus contain "mk"
+       //  even though the resolved file is not part of the pkgsrc infrastructure.
+       if includingFile.ContainsPath("mk") && !G.Pkgsrc.ToRel(includingFile).HasPrefixPath("wip/mk") {
+               // TODO: try ".buildlink.mk", ".builtin.mk" instead, see wip/clfswm.
+               return includingFile.HasSuffixText("buildlink3.mk") && includedFile.HasSuffixText("builtin.mk")
        }
 
        return true
 }
 
-func (pkg *Package) collectSeenInclude(mkline *MkLine, includedFile string) {
+func (pkg *Package) collectSeenInclude(mkline *MkLine, includedFile Path) {
        if mkline.Basename != "Makefile" {
                return
        }
 
-       incDir, incBase := path.Split(includedFile)
+       incDir, incBase := includedFile.Split()
        switch {
        case
-               hasPrefix(incDir, "../../mk/"),
+               incDir.HasPrefixPath("../../mk"),
                incBase == "buildlink3.mk",
                incBase == "builtin.mk",
                incBase == "options.mk":
@@ -402,7 +418,7 @@ func (pkg *Package) collectSeenInclude(m
        }
 
        if trace.Tracing {
-               trace.Step1("Including %q sets seenInclude.", includedFile)
+               trace.Stepf("Including %q sets seenInclude.", includedFile)
        }
        pkg.seenInclude = true
 }
@@ -412,22 +428,22 @@ func (pkg *Package) collectConditionalIn
                if mkline.IsInclude() {
                        mkline.SetConditionalVars(mklines.indentation.Varnames())
 
-                       key := pkg.Rel(mkline.IncludedFileFull())
+                       includedFile := pkg.Rel(mkline.IncludedFileFull())
                        if mklines.indentation.IsConditional() {
-                               pkg.conditionalIncludes[key] = mkline
+                               pkg.conditionalIncludes[includedFile] = mkline
                        } else {
-                               pkg.unconditionalIncludes[key] = mkline
+                               pkg.unconditionalIncludes[includedFile] = mkline
                        }
                }
        })
 }
 
-func (pkg *Package) loadPlistDirs(plistFilename string) {
+func (pkg *Package) loadPlistDirs(plistFilename Path) {
        lines := Load(plistFilename, MustSucceed)
        ck := PlistChecker{
                pkg,
-               make(map[string]*PlistLine),
-               make(map[string]*PlistLine),
+               make(map[Path]*PlistLine),
+               make(map[Path]*PlistLine),
                "",
                Once{},
                false}
@@ -441,19 +457,19 @@ func (pkg *Package) loadPlistDirs(plistF
        }
 }
 
-func (pkg *Package) check(filenames []string, mklines, allLines *MkLines) {
+func (pkg *Package) check(filenames []Path, mklines, allLines *MkLines) {
        haveDistinfo := false
        havePatches := false
 
        for _, filename := range filenames {
-               if containsVarRef(filename) {
+               if containsVarRef(filename.String()) {
                        if trace.Tracing {
                                trace.Stepf("Skipping file %q because the name contains an unresolved variable.", filename)
                        }
                        continue
                }
 
-               st, err := os.Lstat(filename)
+               st, err := filename.Lstat()
                switch {
                case err != nil:
                        // For a missing custom distinfo file, an error message is already generated
@@ -463,7 +479,7 @@ func (pkg *Package) check(filenames []st
                        // since all those files come from calls to dirglob.
                        break
 
-               case path.Base(filename) == "Makefile" && strings.Count(G.Pkgsrc.ToRel(filename), "/") == 2:
+               case filename.HasBase("Makefile") && G.Pkgsrc.ToRel(filename).Count() == 3:
                        G.checkExecutable(filename, st.Mode())
                        pkg.checkfilePackageMakefile(filename, mklines, allLines)
 
@@ -471,9 +487,9 @@ func (pkg *Package) check(filenames []st
                        pkg.checkDirent(filename, st.Mode())
                }
 
-               if contains(filename, "/patches/patch-") {
+               if filename.ContainsText("/patches/patch-") {
                        havePatches = true
-               } else if hasSuffix(filename, "/distinfo") {
+               } else if filename.HasSuffixPath("distinfo") {
                        haveDistinfo = true
                }
                pkg.checkOwnerMaintainer(filename)
@@ -491,9 +507,9 @@ func (pkg *Package) check(filenames []st
        }
 }
 
-func (pkg *Package) checkfilePackageMakefile(filename string, mklines *MkLines, allLines *MkLines) {
+func (pkg *Package) checkfilePackageMakefile(filename Path, mklines *MkLines, allLines *MkLines) {
        if trace.Tracing {
-               defer trace.Call1(filename)()
+               defer trace.Call(filename)()
        }
 
        vars := pkg.vars
@@ -506,12 +522,12 @@ func (pkg *Package) checkfilePackageMake
 
        if !want {
                distinfoFile := pkg.File(pkg.DistinfoFile)
-               if fileExists(distinfoFile) {
+               if distinfoFile.IsFile() {
                        NewLineWhole(distinfoFile).Warnf("This file should not exist since NO_CHECKSUM or META_PACKAGE is set.")
                }
        } else {
                distinfoFile := pkg.File(pkg.DistinfoFile)
-               if !containsVarRef(distinfoFile) && !fileExists(distinfoFile) {
+               if !containsVarRef(distinfoFile.String()) && !distinfoFile.IsFile() {
                        line := NewLineWhole(distinfoFile)
                        line.Warnf("A package that downloads files should have a distinfo file.")
                        line.Explain(
@@ -554,7 +570,7 @@ func (pkg *Package) checkfilePackageMake
 
        if imake := vars.FirstDefinition("USE_IMAKE"); imake != nil {
                if x11 := vars.FirstDefinition("USE_X11"); x11 != nil {
-                       if !hasSuffix(x11.Filename, "/mk/x11.buildlink3.mk") {
+                       if !x11.Filename.HasSuffixPath("mk/x11.buildlink3.mk") {
                                imake.Notef("USE_IMAKE makes USE_X11 in %s redundant.", imake.RefTo(x11))
                        }
                }
@@ -592,8 +608,8 @@ func (pkg *Package) checkPlist() {
        }
 
        needsPlist, line := pkg.needsPlist()
-       hasPlist := fileExists(pkg.File(pkg.Pkgdir+"/PLIST")) ||
-               fileExists(pkg.File(pkg.Pkgdir+"/PLIST.common"))
+       hasPlist := pkg.File(pkg.Pkgdir.JoinNoClean("PLIST")).IsFile() ||
+               pkg.File(pkg.Pkgdir.JoinNoClean("/PLIST.common")).IsFile()
 
        if needsPlist && !hasPlist {
                line.Warnf("This package should have a PLIST file.")
@@ -966,7 +982,7 @@ func (pkg *Package) checkUseLanguagesCom
        }
 
        handleInclude := func(mkline *MkLine) {
-               _ = seen.FirstTime(pkg.Rel(mkline.IncludedFileFull()))
+               _ = seen.FirstTime(pkg.Rel(mkline.IncludedFileFull()).String())
        }
 
        mklines.ForEach(func(mkline *MkLine) {
@@ -1103,7 +1119,7 @@ func (pkg *Package) checkPossibleDowngra
        change := G.Pkgsrc.LastChange[pkg.Pkgpath]
        if change == nil {
                if trace.Tracing {
-                       trace.Step1("No change log for package %q", pkg.Pkgpath)
+                       trace.Stepf("No change log for package %q", pkg.Pkgpath)
                }
                return
        }
@@ -1147,44 +1163,51 @@ func (pkg *Package) checkUpdate() {
                }
 
                suggver, comment := sugg.Version, sugg.Comment
-               if comment != "" {
-                       comment = " (" + comment + ")"
+
+               commentSuffix := func() string {
+                       if comment != "" {
+                               return " (" + comment + ")"
+                       }
+                       return ""
                }
 
-               pkgnameLine := pkg.EffectivePkgnameLine
+               mkline := pkg.EffectivePkgnameLine
                cmp := pkgver.Compare(pkg.EffectivePkgversion, suggver)
+               ref := mkline.RefToLocation(sugg.Line)
                switch {
 
                case cmp < 0:
-                       pkgnameLine.Warnf("This package should be updated to %s%s.",
-                               sugg.Version, comment)
-                       pkgnameLine.Explain(
-                               "The wishlist for package updates in doc/TODO mentions that a newer",
-                               "version of this package is available.")
+                       if comment != "" {
+                               mkline.Warnf("This package should be updated to %s (%s; see %s).",
+                                       sugg.Version, comment, ref)
+                       } else {
+                               mkline.Warnf("This package should be updated to %s (see %s).",
+                                       sugg.Version, ref)
+                       }
 
                case cmp > 0:
-                       pkgnameLine.Notef("This package is newer than the update request to %s%s.",
-                               suggver, comment)
+                       mkline.Notef("This package is newer than the update request to %s%s from %s.",
+                               suggver, commentSuffix(), ref)
 
                default:
-                       pkgnameLine.Notef("The update request to %s from doc/TODO%s has been done.",
-                               suggver, comment)
+                       mkline.Notef("The update request to %s%s from %s has been done.",
+                               suggver, commentSuffix(), ref)
                }
        }
 }
 
 // checkDirent checks a directory entry based on its filename and its mode
 // (regular file, directory, symlink).
-func (pkg *Package) checkDirent(dirent string, mode os.FileMode) {
+func (pkg *Package) checkDirent(dirent Path, mode os.FileMode) {
        // TODO: merge duplicate code in Pkglint.checkMode
 
-       basename := path.Base(dirent)
+       basename := dirent.Base()
 
        switch {
 
        case mode.IsRegular():
                pkgsrcRel := G.Pkgsrc.ToRel(dirent)
-               depth := strings.Count(pkgsrcRel, "/")
+               depth := pkgsrcRel.Count() - 1 // FIXME
                G.checkReg(dirent, basename, depth)
 
        case hasPrefix(basename, "work"):
@@ -1197,7 +1220,7 @@ func (pkg *Package) checkDirent(dirent s
                switch {
                case basename == "files",
                        basename == "patches",
-                       matches(dirent, `(?:^|/)files/[^/]*$`),
+                       dirent.Dir().Base() == "files",
                        isEmptyDir(dirent):
                        break
 
@@ -1219,7 +1242,7 @@ func (pkg *Package) checkDirent(dirent s
 //
 // Pkglint assumes that the local username is the same as the NetBSD
 // username, which fits most scenarios.
-func (pkg *Package) checkOwnerMaintainer(filename string) {
+func (pkg *Package) checkOwnerMaintainer(filename Path) {
        if trace.Tracing {
                defer trace.Call(filename)()
        }
@@ -1261,7 +1284,7 @@ func (pkg *Package) checkOwnerMaintainer
                "keyword \"maintainer\", for more information.")
 }
 
-func (pkg *Package) checkFreeze(filename string) {
+func (pkg *Package) checkFreeze(filename Path) {
        freezeStart := G.Pkgsrc.LastFreezeStart
        if freezeStart == "" || G.Pkgsrc.LastFreezeEnd != "" {
                return
@@ -1278,8 +1301,8 @@ func (pkg *Package) checkFreeze(filename
                "See https://www.NetBSD.org/developers/pkgsrc/ for the exact rules.")
 }
 
-func (pkg *Package) checkFileMakefileExt(filename string) {
-       base := path.Base(filename)
+func (pkg *Package) checkFileMakefileExt(filename Path) {
+       base := filename.Base()
        if !hasPrefix(base, "Makefile.") || base == "Makefile.common" {
                return
        }
@@ -1313,11 +1336,11 @@ func (pkg *Package) checkLinesBuildlink3
        }
 
        // Collect all the included buildlink3.mk files from the file.
-       includedFiles := make(map[string]*MkLine)
+       includedFiles := make(map[Path]*MkLine)
        for _, mkline := range mklines.mklines {
                if mkline.IsInclude() {
                        includedFile := mkline.IncludedFile()
-                       if hasSuffix(includedFile, "/buildlink3.mk") {
+                       if includedFile.HasSuffixPath("buildlink3.mk") {
                                includedFiles[includedFile] = mkline
                                if pkg.bl3[includedFile] == nil {
                                        mkline.Warnf("%s is included by this file but not by the package.", includedFile)
@@ -1329,7 +1352,7 @@ func (pkg *Package) checkLinesBuildlink3
        if trace.Tracing {
                for packageBl3 := range pkg.bl3 {
                        if includedFiles[packageBl3] == nil {
-                               trace.Step1("%s is included by the package but not by the buildlink3.mk file.", packageBl3)
+                               trace.Stepf("%s is included by the package but not by the buildlink3.mk file.", packageBl3)
                        }
                }
        }
@@ -1405,8 +1428,10 @@ func (pkg *Package) AutofixDistinfo(oldS
 // File returns the (possibly absolute) path to relativeFileName,
 // as resolved from the package's directory.
 // Variables that are known in the package are resolved, e.g. ${PKGDIR}.
-func (pkg *Package) File(relativeFileName string) string {
-       return cleanpath(resolveVariableRefs(nil /* XXX: or maybe some mklines? */, joinPath(pkg.dir, relativeFileName)))
+func (pkg *Package) File(relativeFileName Path) Path {
+       joined := pkg.dir.JoinNoClean(relativeFileName)
+       resolved := resolveVariableRefs(nil /* XXX: or maybe some mklines? */, joined.String())
+       return cleanpath(NewPath(resolved))
 }
 
 // Rel returns the path by which the given filename (as seen from the
@@ -1415,13 +1440,13 @@ func (pkg *Package) File(relativeFileNam
 //
 // Example:
 //  NewPackage("category/package").Rel("other/package") == "../../other/package"
-func (pkg *Package) Rel(filename string) string {
+func (pkg *Package) Rel(filename Path) Path {
        return relpath(pkg.dir, filename)
 }
 
 // Returns whether the given file (relative to the package directory)
 // is included somewhere in the package, either directly or indirectly.
-func (pkg *Package) Includes(filename string) bool {
+func (pkg *Package) Includes(filename Path) bool {
        return pkg.unconditionalIncludes[filename] != nil ||
                pkg.conditionalIncludes[filename] != nil
 }
@@ -1435,12 +1460,12 @@ func (pkg *Package) Includes(filename st
 // 2. Ensure that the entries mentioned in the ALTERNATIVES file
 // also appear in the PLIST files.
 type PlistContent struct {
-       Dirs  map[string]*PlistLine
-       Files map[string]*PlistLine
+       Dirs  map[Path]*PlistLine
+       Files map[Path]*PlistLine
 }
 
 func NewPlistContent() PlistContent {
        return PlistContent{
-               make(map[string]*PlistLine),
-               make(map[string]*PlistLine)}
+               make(map[Path]*PlistLine),
+               make(map[Path]*PlistLine)}
 }

Index: pkgsrc/pkgtools/pkglint/files/package_test.go
diff -u pkgsrc/pkgtools/pkglint/files/package_test.go:1.58 pkgsrc/pkgtools/pkglint/files/package_test.go:1.59
--- pkgsrc/pkgtools/pkglint/files/package_test.go:1.58  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/package_test.go       Sat Nov 23 23:35:56 2019
@@ -311,7 +311,7 @@ func (s *Suite) Test_Package__case_insen
        t.FinishSetUp()
 
        // this test is only interesting on a case-insensitive filesystem
-       if !fileExists(t.File("mk/BSD.PKG.MK")) {
+       if !t.File("mk/BSD.PKG.MK").IsFile() {
                return
        }
 
@@ -332,6 +332,64 @@ func (s *Suite) Test_NewPackage(c *check
        t.ExpectAssert(func() { NewPackage("category") })
 }
 
+func (s *Suite) Test_Package_load__variable_from_Makefile_used_in_builtin_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpPackage("devel/binutils",
+               "BINUTILS_PREFIX=\t${PREFIX}/${MACHINE_GNU_PLATFORM}")
+       t.CreateFileLines("devel/binutils/builtin.mk",
+               MkCvsID,
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "BINUTILS_PREFIX?=\t/usr",
+               "",
+               "BUILTIN_FIND_FILES.BINUTILS_FILES:=\t${BINUTILS_PREFIX}/include/bfd.h")
+       t.FinishSetUp()
+
+       G.Check(t.File("devel/binutils"))
+
+       // The BINUTILS_PREFIX from the Makefile is not used since the
+       // builtin.mk file is only parsed inside of buildlink3.mk, and
+       // that doesn't happen for the package itself, but only for those
+       // packages that depend on this package.
+       t.CheckOutputLines(
+               "WARN: ~/devel/binutils/Makefile:20: " +
+                       "BINUTILS_PREFIX is defined but not used.")
+}
+
+func (s *Suite) Test_Package_load__buildlink3_mk_includes_other_mk(c *check.C) {
+       t := s.Init(c)
+
+       t.SetUpCommandLine("-Wall", "--explain")
+       t.SetUpPackage("multimedia/libav")
+       t.CreateFileDummyBuildlink3("multimedia/libav/buildlink3.mk",
+               ".include \"available.mk\"")
+       t.CreateFileLines("multimedia/libav/available.mk",
+               MkCvsID,
+               "",
+               "LIBAV_AVAILABLE=\tno")
+       t.FinishSetUp()
+
+       G.Check(t.File("multimedia/libav"))
+
+       // From looking at the file available.mk alone, this variable looks
+       // unused indeed, but its intention is to be used by other packages.
+       // The explanation has a large paragraph covering exactly this case,
+       // therefore the warning is ok.
+       t.CheckOutputLines(
+               "WARN: ~/multimedia/libav/available.mk:3: "+
+                       "LIBAV_AVAILABLE is defined but not used.",
+               "",
+               "\tThis might be a simple typo.",
+               "",
+               "\tIf a package provides a file containing several related variables",
+               "\t(such as module.mk, app.mk, extension.mk), that file may define",
+               "\tvariables that look unused since they are only used by other",
+               "\tpackages. These variables should be documented at the head of the",
+               "\tfile; see mk/subst.mk for an example of such a documentation",
+               "\tcomment.",
+               "")
+}
+
 // Demonstrates that Makefile fragments are handled differently,
 // depending on the directory they are in.
 func (s *Suite) Test_Package_load__extra_files(c *check.C) {
@@ -357,7 +415,7 @@ func (s *Suite) Test_Package_load__extra
                "@@ -1,1 +1,1 @@",
                "- old",
                "+ new")
-       t.CreateFileLines("patches/readme.mk",
+       t.CreateFileLines("patches/readme.mk", // Is ignored
                "This is not a BSD-style Makefile.")
        t.Copy("gnu-style.mk", "files/gnu-style.mk")
        t.Copy("gnu-style.mk", "../../category/other/gnu-style.mk")
@@ -374,10 +432,6 @@ func (s *Suite) Test_Package_load__extra
                "ERROR: gnu-style.mk:3: Unknown Makefile line format: \"else\".",
                "ERROR: gnu-style.mk:5: Unknown Makefile line format: \"endif\".",
 
-               // Since the patches directory should contain only patches,
-               // each other file is treated as a file belonging to pkgsrc,
-               // therefore *.mk is interpreted as a Makefile fragment.
-               "ERROR: patches/readme.mk:1: Unknown Makefile line format: \"This is not a BSD-style Makefile.\".",
                "ERROR: distinfo: Patch \"patches/patch-Makefile.mk\" is not recorded. Run \""+confMake+" makepatchsum\".",
 
                // The following diagnostics are duplicated because the files from
@@ -1011,7 +1065,7 @@ func (s *Suite) Test_Package_shouldDiveI
        t := s.Init(c)
        t.Chdir(".")
 
-       test := func(including, included string, expected bool) {
+       test := func(including, included Path, expected bool) {
                actual := (*Package)(nil).shouldDiveInto(including, included)
                t.CheckEquals(actual, expected)
        }
@@ -1105,13 +1159,13 @@ func (s *Suite) Test_Package_loadPlistDi
        pkg := NewPackage(t.File("category/package"))
        pkg.load()
 
-       var dirs []string
+       var dirs []Path
        for dir := range pkg.Plist.Dirs {
                dirs = append(dirs, dir)
        }
-       sort.Strings(dirs)
+       sort.Slice(dirs, func(i, j int) bool { return dirs[i] < dirs[j] })
 
-       t.CheckDeepEquals(dirs, []string{"bin"})
+       t.CheckDeepEquals(dirs, []Path{"bin"})
 }
 
 func (s *Suite) Test_Package_loadPlistDirs(c *check.C) {
@@ -1128,13 +1182,13 @@ func (s *Suite) Test_Package_loadPlistDi
        pkg := NewPackage(t.File("category/package"))
        pkg.load()
 
-       var dirs []string
+       var dirs []Path
        for dir := range pkg.Plist.Dirs {
                dirs = append(dirs, dir)
        }
-       sort.Strings(dirs)
+       sort.Slice(dirs, func(i, j int) bool { return dirs[i] < dirs[j] })
 
-       t.CheckDeepEquals(dirs, []string{"bin", "dir", "dir/subdir"})
+       t.CheckDeepEquals(dirs, []Path{"bin", "dir", "dir/subdir"})
 }
 
 func (s *Suite) Test_Package_check__files_Makefile(c *check.C) {
@@ -2456,6 +2510,8 @@ func (s *Suite) Test_Package_checkPossib
 func (s *Suite) Test_Package_checkUpdate(c *check.C) {
        t := s.Init(c)
 
+       // The package names intentionally differ from the package directories
+       // to ensure that the check uses the package name.
        t.SetUpPackage("category/pkg1",
                "PKGNAME=                package1-1.0")
        t.SetUpPackage("category/pkg2",
@@ -2463,27 +2519,29 @@ func (s *Suite) Test_Package_checkUpdate
        t.SetUpPackage("category/pkg3",
                "PKGNAME=                package3-5.0")
        t.CreateFileLines("doc/TODO",
+               CvsID,
                "Suggested package updates",
+               "=========================",
+               "For possible Perl packages updates, see http://www.NetBSD.org/~wiz/perl.html.";,
                "",
-               "",
-               "\t"+"O wrong bullet",
-               "\t"+"o package-without-version",
                "\t"+"o package1-1.0",
+               "\t"+"o package1-1.0 [with comment]",
+               "\t"+"o package2-2.0",
                "\t"+"o package2-2.0 [nice new features]",
+               "\t"+"o package3-3.0",
                "\t"+"o package3-3.0 [security update]")
        t.Chdir(".")
 
        t.Main("-Wall,no-space", "category/pkg1", "category/pkg2", "category/pkg3")
 
        t.CheckOutputLines(
-               "WARN: category/pkg1/../../doc/TODO:3: Invalid line format \"\".",
-               "WARN: category/pkg1/../../doc/TODO:4: Invalid line format \"\\tO wrong bullet\".",
-               "WARN: category/pkg1/../../doc/TODO:5: Invalid package name \"package-without-version\".",
-               "NOTE: category/pkg1/Makefile:4: The update request to 1.0 from doc/TODO has been done.",
-               "WARN: category/pkg2/Makefile:4: This package should be updated to 2.0 ([nice new features]).",
-               "NOTE: category/pkg3/Makefile:4: This package is newer than the update request to 3.0 ([security update]).",
-               "4 warnings and 2 notes found.",
-               "(Run \"pkglint -e -Wall,no-space category/pkg1 category/pkg2 category/pkg3\" to show explanations.)")
+               "NOTE: category/pkg1/Makefile:4: The update request to 1.0 from ../../doc/TODO:6 has been done.",
+               "NOTE: category/pkg1/Makefile:4: The update request to 1.0 (with comment) from ../../doc/TODO:7 has been done.",
+               "WARN: category/pkg2/Makefile:4: This package should be updated to 2.0 (see ../../doc/TODO:8).",
+               "WARN: category/pkg2/Makefile:4: This package should be updated to 2.0 (nice new features; see ../../doc/TODO:9).",
+               "NOTE: category/pkg3/Makefile:4: This package is newer than the update request to 3.0 from ../../doc/TODO:10.",
+               "NOTE: category/pkg3/Makefile:4: This package is newer than the update request to 3.0 (security update) from ../../doc/TODO:11.",
+               "2 warnings and 4 notes found.")
 }
 
 func (s *Suite) Test_Package_checkDirent__errors(c *check.C) {
@@ -3162,8 +3220,12 @@ func (s *Suite) Test_Package_Includes(c 
        t.CheckEquals(pkg.Includes("conditionally.mk"), true)
        t.CheckEquals(pkg.Includes("other.mk"), false)
 
-       // TODO: Strictly speaking, never.mk should be in conditionalIncludes.
-       //  This is an edge case though. See collectConditionalIncludes and
-       //  Indentation.IsConditional for the current implementation.
-       t.CheckEquals(pkg.conditionalIncludes["never.mk"], (*MkLine)(nil))
+       // The file never.mk is in conditionalIncludes since pkglint only
+       // analyzes on the syntactical level. It doesn't evaluate the
+       // condition from the .if to see whether it is satisfiable.
+       //
+       // See Package.collectConditionalIncludes and Indentation.IsConditional.
+       t.CheckEquals(
+               pkg.conditionalIncludes["never.mk"].Location,
+               NewLocation(t.File("category/package/Makefile"), 22, 22))
 }
Index: pkgsrc/pkgtools/pkglint/files/util.go
diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.58 pkgsrc/pkgtools/pkglint/files/util.go:1.59
--- pkgsrc/pkgtools/pkglint/files/util.go:1.58  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/util.go       Sat Nov 23 23:35:56 2019
@@ -3,12 +3,9 @@ package pkglint
 import (
        "fmt"
        "hash/crc64"
-       "io/ioutil"
        "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/textproc"
-       "os"
        "path"
-       "path/filepath"
        "reflect"
        "regexp"
        "sort"
@@ -30,6 +27,7 @@ func (ynu YesNoUnknown) String() string 
 }
 
 // Short names for commonly used functions.
+
 func contains(s, substr string) bool {
        return strings.Contains(s, substr)
 }
@@ -240,12 +238,12 @@ func assertf(cond bool, format string, a
        }
 }
 
-func isEmptyDir(filename string) bool {
-       if hasSuffix(filename, "/CVS") {
+func isEmptyDir(filename Path) bool {
+       if filename.HasSuffixText("/CVS") {
                return true
        }
 
-       dirents, err := ioutil.ReadDir(filename)
+       dirents, err := filename.ReadDir()
        if err != nil {
                return true // XXX: Why not false?
        }
@@ -255,7 +253,7 @@ func isEmptyDir(filename string) bool {
                if isIgnoredFilename(name) {
                        continue
                }
-               if dirent.IsDir() && isEmptyDir(joinPath(filename, name)) {
+               if dirent.IsDir() && isEmptyDir(filename.JoinNoClean(NewPath(name))) {
                        continue
                }
                return false
@@ -263,17 +261,17 @@ func isEmptyDir(filename string) bool {
        return true
 }
 
-func getSubdirs(filename string) []string {
-       dirents, err := ioutil.ReadDir(filename)
+func getSubdirs(filename Path) []Path {
+       dirents, err := filename.ReadDir()
        if err != nil {
                NewLineWhole(filename).Fatalf("Cannot be read: %s", err)
        }
 
-       var subdirs []string
+       var subdirs []Path
        for _, dirent := range dirents {
                name := dirent.Name()
-               if dirent.IsDir() && !isIgnoredFilename(name) && !isEmptyDir(joinPath(filename, name)) {
-                       subdirs = append(subdirs, name)
+               if dirent.IsDir() && !isIgnoredFilename(name) && !isEmptyDir(filename.JoinNoClean(NewPath(name))) {
+                       subdirs = append(subdirs, NewPath(name))
                }
        }
        return subdirs
@@ -287,24 +285,24 @@ func isIgnoredFilename(filename string) 
        return hasPrefix(filename, ".#")
 }
 
-func dirglob(dirname string) []string {
-       infos, err := ioutil.ReadDir(dirname)
+func dirglob(dirname Path) []Path {
+       infos, err := dirname.ReadDir()
        if err != nil {
                return nil
        }
-       var filenames []string
+       var filenames []Path
        for _, info := range infos {
                if !(isIgnoredFilename(info.Name())) {
-                       filenames = append(filenames, cleanpath(joinPath(dirname, info.Name())))
+                       filenames = append(filenames, cleanpath(dirname.JoinNoClean(NewPath(info.Name()))))
                }
        }
        return filenames
 }
 
 // Checks whether a file is already committed to the CVS repository.
-func isCommitted(filename string) bool {
+func isCommitted(filename Path) bool {
        entries := G.loadCvsEntries(filename)
-       _, found := entries[path.Base(filename)]
+       _, found := entries[filename.Base()]
        return found
 }
 
@@ -313,14 +311,14 @@ func isCommitted(filename string) bool {
 //
 // There is no corresponding test for Git (as used by pkgsrc-wip) since that
 // is more difficult to implement than simply reading a CVS/Entries file.
-func isLocallyModified(filename string) bool {
+func isLocallyModified(filename Path) bool {
        entries := G.loadCvsEntries(filename)
-       entry, found := entries[path.Base(filename)]
+       entry, found := entries[filename.Base()]
        if !found {
                return false
        }
 
-       st, err := os.Stat(filename)
+       st, err := filename.Stat()
        if err != nil {
                return true
        }
@@ -438,16 +436,6 @@ func varnameParam(varname string) string
        return ""
 }
 
-func fileExists(filename string) bool {
-       st, err := os.Stat(filename)
-       return err == nil && st.Mode().IsRegular()
-}
-
-func dirExists(filename string) bool {
-       st, err := os.Stat(filename)
-       return err == nil && st.Mode().IsDir()
-}
-
 func toInt(s string, def int) int {
        if n, err := strconv.Atoi(s); err == nil {
                return n
@@ -469,11 +457,15 @@ func mkopSubst(s string, left bool, from
        })
 }
 
-func joinPath(a, b string, others ...string) string {
+func joinPath(a, b Path, others ...Path) Path {
        if len(others) == 0 {
                return a + "/" + b
        }
-       return a + "/" + b + "/" + strings.Join(others, "/")
+       parts := []string{a.String(), b.String()}
+       for _, part := range others {
+               parts = append(parts, part.String())
+       }
+       return NewPath(strings.Join(parts, "/"))
 }
 
 // relpath returns the relative path from the directory "from"
@@ -493,7 +485,7 @@ func joinPath(a, b string, others ...str
 //
 // TODO: Invent data types for all kinds of relative paths that occur in pkgsrc
 //  and pkglint. Make sure that these paths cannot be accidentally mixed.
-func relpath(from, to string) (result string) {
+func relpath(from, to Path) (result Path) {
 
        if trace.Tracing {
                defer trace.Call(from, to, trace.Result(&result))()
@@ -507,34 +499,31 @@ func relpath(from, to string) (result st
        }
 
        // Take a shortcut for the common case from "dir" to "dir/subdir/...".
-       if hasPrefix(cto, cfrom) && hasPrefix(cto[len(cfrom):], "/") {
+       if cto.HasPrefixPath(cfrom) {
                return cleanpath(cto[len(cfrom)+1:])
        }
 
        // Take a shortcut for the common case from "category/package" to ".".
        // This is the most common variant in a complete pkgsrc scan.
        if cto == "." {
-               fromParts := strings.FieldsFunc(cfrom, func(r rune) bool { return r == '/' })
+               fromParts := cfrom.Parts()
                if len(fromParts) == 2 && !hasPrefix(fromParts[0], ".") && !hasPrefix(fromParts[1], ".") {
                        return "../.."
                }
        }
 
-       if cfrom == "." && !filepath.IsAbs(cto) {
-               return path.Clean(cto)
+       if cfrom == "." && !cto.IsAbs() {
+               return cto.Clean()
        }
 
        absFrom := abspath(cfrom)
        absTopdir := abspath(G.Pkgsrc.topdir)
        absTo := abspath(cto)
 
-       toTop, err := filepath.Rel(absFrom, absTopdir)
-       assertNil(err, "relpath from %q to topdir %q", absFrom, absTopdir)
-
-       fromTop, err := filepath.Rel(absTopdir, absTo)
-       assertNil(err, "relpath from topdir %q to %q", absTopdir, absTo)
+       toTop := absFrom.Rel(absTopdir)
+       fromTop := absTopdir.Rel(absTo)
 
-       result = cleanpath(joinPath(filepath.ToSlash(toTop), filepath.ToSlash(fromTop)))
+       result = cleanpath(toTop.JoinNoClean(fromTop))
 
        if trace.Tracing {
                trace.Stepf("relpath from %q to %q = %q", cfrom, cto, result)
@@ -542,20 +531,20 @@ func relpath(from, to string) (result st
        return
 }
 
-func abspath(filename string) string {
+func abspath(filename Path) Path {
        abs := filename
-       if !filepath.IsAbs(filename) {
+       if !filename.IsAbs() {
                abs = joinPath(G.cwd, abs)
        }
-       return path.Clean(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.
-func cleanpath(filename string) string {
+func cleanpath(filename Path) Path {
        parts := make([]string, 0, 5)
-       lex := textproc.NewLexer(filename)
+       lex := textproc.NewLexer(filename.String())
        for lex.SkipString("./") {
        }
 
@@ -585,7 +574,7 @@ func cleanpath(filename string) string {
        if len(parts) == 0 {
                return "."
        }
-       return strings.Join(parts, "/")
+       return NewPath(strings.Join(parts, "/"))
 }
 
 func pathContains(haystack, needle string) bool {
@@ -602,10 +591,10 @@ func pathContains(haystack, needle strin
        return false
 }
 
-func pathContainsDir(haystack, needle string) bool {
+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[i:], needle) {
+               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
@@ -1010,8 +999,8 @@ func naturalLess(str1, str2 string) bool
 
 // IsPrefs returns whether the given file, when included, loads the user
 // preferences.
-func IsPrefs(filename string) bool {
-       switch path.Base(filename) {
+func IsPrefs(filename Path) bool {
+       switch filename.Base() {
        case // See https://github.com/golang/go/issues/28057
                "bsd.prefs.mk",         // in mk/
                "bsd.fast.prefs.mk",    // in mk/
@@ -1047,7 +1036,7 @@ func NewFileCache(size int) *FileCache {
                0}
 }
 
-func (c *FileCache) Put(filename string, options LoadOptions, lines *Lines) {
+func (c *FileCache) Put(filename Path, options LoadOptions, lines *Lines) {
        key := c.key(filename)
 
        entry := c.mapping[key]
@@ -1101,7 +1090,7 @@ func (c *FileCache) removeOldEntries() {
        }
 }
 
-func (c *FileCache) Get(filename string, options LoadOptions) *Lines {
+func (c *FileCache) Get(filename Path, options LoadOptions) *Lines {
        key := c.key(filename)
        entry, found := c.mapping[key]
        if found && entry.options == options {
@@ -1118,7 +1107,7 @@ func (c *FileCache) Get(filename string,
        return nil
 }
 
-func (c *FileCache) Evict(filename string) {
+func (c *FileCache) Evict(filename Path) {
        key := c.key(filename)
        entry, found := c.mapping[key]
        if !found {
@@ -1136,9 +1125,7 @@ func (c *FileCache) Evict(filename strin
        }
 }
 
-func (c *FileCache) key(filename string) string {
-       return path.Clean(filename)
-}
+func (c *FileCache) key(filename Path) string { return filename.Clean().String() }
 
 func bmakeHelp(topic string) string { return bmake("help topic=" + topic) }
 
@@ -1398,27 +1385,27 @@ func pathMatches(pattern, s string) bool
        return err == nil && matched
 }
 
-type StringQueue struct {
-       entries []string
+type PathQueue struct {
+       entries []Path
 }
 
-func (q *StringQueue) PushFront(entries ...string) {
-       q.entries = append(append([]string(nil), entries...), q.entries...)
+func (q *PathQueue) PushFront(entries ...Path) {
+       q.entries = append(append([]Path(nil), entries...), q.entries...)
 }
 
-func (q *StringQueue) Push(entries ...string) {
+func (q *PathQueue) Push(entries ...Path) {
        q.entries = append(q.entries, entries...)
 }
 
-func (q *StringQueue) IsEmpty() bool {
+func (q *PathQueue) IsEmpty() bool {
        return len(q.entries) == 0
 }
 
-func (q *StringQueue) Front() string {
+func (q *PathQueue) Front() Path {
        return q.entries[0]
 }
 
-func (q *StringQueue) Pop() string {
+func (q *PathQueue) Pop() Path {
        front := q.entries[0]
        q.entries = q.entries[1:]
        return front

Index: pkgsrc/pkgtools/pkglint/files/patches.go
diff -u pkgsrc/pkgtools/pkglint/files/patches.go:1.32 pkgsrc/pkgtools/pkglint/files/patches.go:1.33
--- pkgsrc/pkgtools/pkglint/files/patches.go:1.32       Sat Nov  2 16:37:48 2019
+++ pkgsrc/pkgtools/pkglint/files/patches.go    Sat Nov 23 23:35:56 2019
@@ -79,7 +79,7 @@ func (ck *PatchChecker) Check() {
                }
        }
 
-       if patchedFiles > 1 && !matches(ck.lines.Filename, `\bCVE\b`) {
+       if patchedFiles > 1 && !matches(ck.lines.Filename.String(), `\bCVE\b`) {
                ck.lines.Whole().Warnf("Contains patches for %d files, should be only one.", patchedFiles)
        } else if patchedFiles == 0 {
                ck.lines.Whole().Errorf("Contains no patch.")

Index: pkgsrc/pkgtools/pkglint/files/pkglint.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.64 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.65
--- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.64       Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint.go    Sat Nov 23 23:35:56 2019
@@ -10,7 +10,6 @@ import (
        "os"
        "os/user"
        "path"
-       "path/filepath"
        "runtime"
        "runtime/debug"
        "runtime/pprof"
@@ -26,14 +25,14 @@ type Pkglint struct {
        Pkgsrc Pkgsrc   // Global data, mostly extracted from mk/*.
        Pkg    *Package // The package that is currently checked, or nil.
 
-       Todo           StringQueue // The files or directories that still need to be checked.
-       Wip            bool        // Is the currently checked file or package from pkgsrc-wip?
-       Infrastructure bool        // Is the currently checked file from the pkgsrc infrastructure?
-       Testing        bool        // Is pkglint in self-testing mode (only during development)?
-       Experimental   bool        // For experimental features, only enabled individually in tests
-       Username       string      // For checking against OWNER and MAINTAINER
+       Todo           PathQueue // The files or directories that still need to be checked.
+       Wip            bool      // Is the currently checked file or package from pkgsrc-wip?
+       Infrastructure bool      // Is the currently checked file from the pkgsrc infrastructure?
+       Testing        bool      // Is pkglint in self-testing mode (only during development)?
+       Experimental   bool      // For experimental features, only enabled individually in tests
+       Username       string    // For checking against OWNER and MAINTAINER
 
-       cvsEntriesDir string // Cached to avoid I/O
+       cvsEntriesDir Path // Cached to avoid I/O
        cvsEntries    map[string]CvsEntry
 
        Logger Logger
@@ -43,10 +42,10 @@ type Pkglint struct {
        fileCache *FileCache
        interner  StringInterner
 
-       // cwd is the slash-separated absolute path to the current working
+       // cwd is the absolute path to the current working
        // directory. It is used for speeding up relpath and abspath.
        // There is no other use for it.
-       cwd string
+       cwd Path
 
        InterPackage InterPackage
 }
@@ -58,7 +57,7 @@ func NewPkglint(stdout io.Writer, stderr
        p := Pkglint{
                res:       regex.NewRegistry(),
                fileCache: NewFileCache(200),
-               cwd:       filepath.ToSlash(cwd),
+               cwd:       NewPathSlash(cwd),
                interner:  NewStringInterner()}
        p.Logger.out = NewSeparatorWriter(stdout)
        p.Logger.err = NewSeparatorWriter(stderr)
@@ -208,8 +207,8 @@ func (pkglint *Pkglint) setUpProfiling()
 
 func (pkglint *Pkglint) prepareMainLoop() {
        firstDir := pkglint.Todo.Front()
-       if fileExists(firstDir) {
-               firstDir = path.Dir(firstDir)
+       if firstDir.IsFile() {
+               firstDir = firstDir.Dir()
        }
 
        relTopdir := findPkgsrcTopdir(firstDir)
@@ -222,7 +221,7 @@ func (pkglint *Pkglint) prepareMainLoop(
        }
 
        pkglint.Pkgsrc = NewPkgsrc(joinPath(firstDir, relTopdir))
-       pkglint.Wip = matches(pkglint.Pkgsrc.ToRel(firstDir), `^wip(/|$)`) // Same as in Pkglint.Check.
+       pkglint.Wip = pkglint.Pkgsrc.ToRel(firstDir).HasPrefixPath("wip") // Same as in Pkglint.Check.
        pkglint.Pkgsrc.LoadInfrastructure()
 
        currentUser, err := user.Current()
@@ -283,7 +282,7 @@ func (pkglint *Pkglint) ParseCommandLine
        }
 
        for _, arg := range pkglint.Opts.args {
-               pkglint.Todo.Push(filepath.ToSlash(arg))
+               pkglint.Todo.Push(NewPathSlash(arg))
        }
        if pkglint.Todo.IsEmpty() {
                pkglint.Todo.Push(".")
@@ -299,12 +298,12 @@ func (pkglint *Pkglint) ParseCommandLine
 //
 // It sets up all the global state (infrastructure, wip) for accurately
 // classifying the entry.
-func (pkglint *Pkglint) Check(dirent string) {
+func (pkglint *Pkglint) Check(dirent Path) {
        if trace.Tracing {
-               defer trace.Call1(dirent)()
+               defer trace.Call(dirent)()
        }
 
-       st, err := os.Lstat(dirent)
+       st, err := dirent.Lstat()
        if err != nil {
                NewLineWhole(dirent).Errorf("No such file or directory.")
                return
@@ -313,7 +312,7 @@ func (pkglint *Pkglint) Check(dirent str
        pkglint.checkMode(dirent, st.Mode())
 }
 
-func (pkglint *Pkglint) checkMode(dirent string, mode os.FileMode) {
+func (pkglint *Pkglint) checkMode(dirent Path, mode os.FileMode) {
        // TODO: merge duplicate code in Package.checkDirent
        isDir := mode.IsDir()
        isReg := mode.IsRegular()
@@ -324,14 +323,14 @@ func (pkglint *Pkglint) checkMode(dirent
 
        dir := dirent
        if !isDir {
-               dir = path.Dir(dirent)
+               dir = dirent.Dir()
        }
 
-       basename := path.Base(dirent)
+       basename := dirent.Base()
        pkgsrcRel := pkglint.Pkgsrc.ToRel(dirent)
 
-       pkglint.Wip = matches(pkgsrcRel, `^wip(/|$)`)
-       pkglint.Infrastructure = matches(pkgsrcRel, `^mk(/|$)`)
+       pkglint.Wip = pkgsrcRel.HasPrefixPath("wip")
+       pkglint.Infrastructure = pkgsrcRel.HasPrefixPath("mk")
        pkgsrcdir := findPkgsrcTopdir(dir)
        if pkgsrcdir == "" {
                NewLineWhole(dirent).Errorf("Cannot determine the pkgsrc root directory for %q.", cleanpath(dir))
@@ -339,7 +338,7 @@ func (pkglint *Pkglint) checkMode(dirent
        }
 
        if isReg {
-               depth := strings.Count(pkgsrcRel, "/")
+               depth := pkgsrcRel.Count() - 1 // FIXME
                pkglint.checkExecutable(dirent, mode)
                pkglint.checkReg(dirent, basename, depth)
                return
@@ -363,9 +362,9 @@ func (pkglint *Pkglint) checkMode(dirent
 
 // checkdirPackage checks a complete pkgsrc package, including each
 // of the files individually, and also when seen in combination.
-func (pkglint *Pkglint) checkdirPackage(dir string) {
+func (pkglint *Pkglint) checkdirPackage(dir Path) {
        if trace.Tracing {
-               defer trace.Call1(dir)()
+               defer trace.Call(dir)()
        }
 
        pkglint.Pkg = NewPackage(dir)
@@ -377,9 +376,9 @@ func (pkglint *Pkglint) checkdirPackage(
 }
 
 // Returns the pkgsrc top-level directory, relative to the given directory.
-func findPkgsrcTopdir(dirname string) string {
-       for _, dir := range [...]string{".", "..", "../..", "../../.."} {
-               if fileExists(joinPath(dirname, dir, "mk/bsd.pkg.mk")) {
+func findPkgsrcTopdir(dirname Path) Path {
+       for _, dir := range [...]Path{".", "..", "../..", "../../.."} {
+               if joinPath(dirname, dir, "mk/bsd.pkg.mk").IsFile() {
                        return dir
                }
        }
@@ -427,9 +426,9 @@ func resolveVariableRefs(mklines *MkLine
        }
 }
 
-func CheckFileOther(filename string) {
+func CheckFileOther(filename Path) {
        if trace.Tracing {
-               defer trace.Call1(filename)()
+               defer trace.Call(filename)()
        }
 
        if lines := Load(filename, NotEmpty|LogErrors); lines != nil {
@@ -439,7 +438,7 @@ func CheckFileOther(filename string) {
 
 func CheckLinesDescr(lines *Lines) {
        if trace.Tracing {
-               defer trace.Call1(lines.Filename)()
+               defer trace.Call(lines.Filename)()
        }
 
        for _, line := range lines.Lines {
@@ -473,7 +472,7 @@ func CheckLinesDescr(lines *Lines) {
 
 func CheckLinesMessage(lines *Lines) {
        if trace.Tracing {
-               defer trace.Call1(lines.Filename)()
+               defer trace.Call(lines.Filename)()
        }
 
        // For now, skip all checks when the MESSAGE may be built from multiple
@@ -529,9 +528,9 @@ func CheckLinesMessage(lines *Lines) {
        SaveAutofixChanges(lines)
 }
 
-func CheckFileMk(filename string) {
+func CheckFileMk(filename Path) {
        if trace.Tracing {
-               defer trace.Call1(filename)()
+               defer trace.Call(filename)()
        }
 
        mklines := LoadMk(filename, NotEmpty|LogErrors)
@@ -547,7 +546,7 @@ func CheckFileMk(filename string) {
        mklines.SaveAutofixChanges()
 }
 
-func (pkglint *Pkglint) checkReg(filename, basename string, depth int) {
+func (pkglint *Pkglint) checkReg(filename Path, basename string, depth int) {
 
        if depth == 2 && !pkglint.Wip {
                if contains(basename, "README") || contains(basename, "TODO") {
@@ -606,16 +605,16 @@ func (pkglint *Pkglint) checkReg(filenam
                        CheckLinesPatch(lines)
                }
 
-       case matches(filename, `(?:^|/)patches/manual[^/]*$`):
+       case matches(filename.String(), `(?:^|/)patches/manual[^/]*$`):
                if trace.Tracing {
-                       trace.Step1("Unchecked file %q.", filename)
+                       trace.Stepf("Unchecked file %q.", filename)
                }
 
-       case matches(filename, `(?:^|/)patches/[^/]*$`):
+       case filename.Dir().Base() == "patches":
                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"):
+               !pathContainsDir(filename, "files"): // FIXME: G.Pkgsrc.Rel(filename) instead of filename
                CheckFileMk(filename)
 
        case hasPrefix(basename, "PLIST"):
@@ -627,11 +626,11 @@ func (pkglint *Pkglint) checkReg(filenam
                // This only checks the file but doesn't register the changes globally.
                _ = pkglint.Pkgsrc.loadDocChangesFromFile(filename)
 
-       case matches(filename, `(?:^|/)files/[^/]*$`):
+       case filename.Dir().Base() == "files":
                // Skip files directly in the files/ directory, but not those further down.
 
        case basename == "spec":
-               if !hasPrefix(pkglint.Pkgsrc.ToRel(filename), "regress/") {
+               if !pkglint.Pkgsrc.ToRel(filename).HasPrefixPath("regress") {
                        NewLineWhole(filename).Warnf("Only packages in regress/ may have spec files.")
                }
 
@@ -652,7 +651,7 @@ func (pkglint *Pkglint) matchesLicenseFi
        return basename == path.Base(licenseFile)
 }
 
-func (pkglint *Pkglint) checkExecutable(filename string, mode os.FileMode) {
+func (pkglint *Pkglint) checkExecutable(filename Path, mode os.FileMode) {
        if mode.Perm()&0111 == 0 {
                // Not executable at all.
                return
@@ -677,7 +676,7 @@ func (pkglint *Pkglint) checkExecutable(
        fix.Custom(func(showAutofix, autofix bool) {
                fix.Describef(0, "Clearing executable bits")
                if autofix {
-                       if err := os.Chmod(filename, mode&^0111); err != nil {
+                       if err := filename.Chmod(mode &^ 0111); err != nil {
                                G.Logger.Errorf(cleanpath(filename), "Cannot clear executable bits: %s", err)
                        }
                }
@@ -732,8 +731,8 @@ func (pkglint *Pkglint) tools(mklines *M
        }
 }
 
-func (pkglint *Pkglint) loadCvsEntries(filename string) map[string]CvsEntry {
-       dir := path.Dir(filename)
+func (pkglint *Pkglint) loadCvsEntries(filename Path) map[string]CvsEntry {
+       dir := filename.Dir()
        if dir == pkglint.cvsEntriesDir {
                return pkglint.cvsEntries
        }
@@ -798,8 +797,8 @@ func (ip *InterPackage) Enable() {
 
 func (ip *InterPackage) Enabled() bool { return ip.hashes != nil }
 
-func (ip *InterPackage) Hash(alg, filename string, hashBytes []byte, loc *Location) *Hash {
-       key := alg + ":" + filename
+func (ip *InterPackage) Hash(alg string, filename Path, hashBytes []byte, loc *Location) *Hash {
+       key := alg + ":" + filename.String()
        if otherHash := ip.hashes[key]; otherHash != nil {
                return otherHash
        }

Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.49 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.50
--- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.49  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go       Sat Nov 23 23:35:56 2019
@@ -14,9 +14,9 @@ func (pkglint *Pkglint) isUsable() bool 
 func (s *Suite) Test_Pkglint_Main(c *check.C) {
        t := s.Init(c)
 
-       out, err := os.Create(t.CreateFileLines("out"))
+       out, err := os.Create(t.CreateFileLines("out").String())
        c.Check(err, check.IsNil)
-       outProfiling, err := os.Create(t.CreateFileLines("out.profiling"))
+       outProfiling, err := os.Create(t.CreateFileLines("out.profiling").String())
        c.Check(err, check.IsNil)
 
        t.SetUpPackage("category/package")
@@ -238,7 +238,7 @@ func (s *Suite) Test_Pkglint_Main__compl
                        "Package version \"1.11\" is greater than the latest \"1.10\" "+
                        "from ../../doc/CHANGES-2018:5.",
                "WARN: ~/sysutils/checkperms/Makefile:3: "+
-                       "This package should be updated to 1.13 ([supports more file formats]).",
+                       "This package should be updated to 1.13 (supports more file formats; see ../../doc/TODO:5).",
                "ERROR: ~/sysutils/checkperms/Makefile:4: Invalid category \"tools\".",
                "ERROR: ~/sysutils/checkperms/README: Packages in main pkgsrc must not have a README file.",
                "ERROR: ~/sysutils/checkperms/TODO: Packages in main pkgsrc must not have a TODO file.",
@@ -259,7 +259,7 @@ func (s *Suite) Test_Pkglint_Main__autof
        t.CreateFileLines("filename.mk",
                "")
 
-       exitcode := t.Main("-Wall", "--autofix", t.File("filename.mk"))
+       exitcode := t.Main("-Wall", "--autofix", "filename.mk")
 
        t.CheckOutputLines(
                "AUTOFIX: ~/filename.mk:1: Inserting a line \"" + MkCvsID + "\" before this line.")
@@ -309,7 +309,7 @@ func (s *Suite) Test_Pkglint_Main__profi
 
        // Pkglint always writes the profiling data into the current directory.
        // TODO: Make the location of the profiling log a mandatory parameter.
-       t.CheckEquals(fileExists("pkglint.pprof"), true)
+       t.CheckEquals(NewPath("pkglint.pprof").IsFile(), true)
 
        err := os.Remove("pkglint.pprof")
        c.Check(err, check.IsNil)
@@ -860,7 +860,7 @@ func (s *Suite) Test_Pkglint_checkReg__a
        lines := t.SetUpFileLines("category/package/ALTERNATIVES",
                "bin/tar bin/gnu-tar")
 
-       t.Main(lines.Filename)
+       t.Main(lines.Filename.String())
 
        t.CheckOutputLines(
                "ERROR: ~/category/package/ALTERNATIVES:1: Alternative implementation \"bin/gnu-tar\" must be an absolute path.",
@@ -961,7 +961,7 @@ func (s *Suite) Test_Pkglint_checkReg__r
 
        // Copy category/package/** to wip/package.
        err := filepath.Walk(
-               t.File("category/package"),
+               t.File("category/package").String(),
                func(pathname string, info os.FileInfo, err error) error {
                        if info.Mode().IsRegular() {
                                src := filepath.ToSlash(pathname)
@@ -1047,7 +1047,7 @@ func (s *Suite) Test_Pkglint_checkExecut
        t := s.Init(c)
 
        filename := t.CreateFileLines("file.mk")
-       err := os.Chmod(filename, 0555)
+       err := os.Chmod(filename.String(), 0555)
        assertNil(err, "")
 
        G.checkExecutable(filename, 0555)
@@ -1065,7 +1065,7 @@ func (s *Suite) Test_Pkglint_checkExecut
        // On Windows, this is effectively a no-op test since there is no
        // execute-bit. The only relevant permissions bit is whether a
        // file is readonly or not.
-       st, err := os.Lstat(filename)
+       st, err := filename.Lstat()
        if t.Check(err, check.IsNil) {
                t.CheckEquals(st.Mode()&0111, os.FileMode(0))
        }

Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.41 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.42
--- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.41        Tue Nov 19 06:51:38 2019
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go     Sat Nov 23 23:35:56 2019
@@ -1,7 +1,6 @@
 package pkglint
 
 import (
-       "io/ioutil"
        "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/textproc"
        "os"
@@ -20,7 +19,7 @@ import (
 type Pkgsrc struct {
        // The top directory (PKGSRCDIR), either absolute or relative to
        // the current working directory.
-       topdir string
+       topdir Path
 
        // The set of user-defined variables that are added to BUILD_DEFS
        // within the bsd.pkg.mk file.
@@ -36,7 +35,7 @@ type Pkgsrc struct {
        suggestedUpdates    []SuggestedUpdate
        suggestedWipUpdates []SuggestedUpdate
 
-       LastChange      map[string]*Change
+       LastChange      map[Path]*Change
        LastFreezeStart string // e.g. "2018-01-01", or ""
        LastFreezeEnd   string // e.g. "2018-01-01", or ""
 
@@ -53,7 +52,7 @@ type Pkgsrc struct {
        vartypes   VarTypeRegistry
 }
 
-func NewPkgsrc(dir string) Pkgsrc {
+func NewPkgsrc(dir Path) Pkgsrc {
        return Pkgsrc{
                dir,
                make(map[string]bool),
@@ -63,7 +62,7 @@ func NewPkgsrc(dir string) Pkgsrc {
                make(map[string]string),
                nil,
                nil,
-               make(map[string]*Change),
+               make(map[Path]*Change),
                "",
                "",
                make(map[string][]string),
@@ -149,15 +148,15 @@ func (src *Pkgsrc) loadDocChanges() {
                NewLineWhole(docDir).Fatalf("Cannot be read for loading the package changes.")
        }
 
-       var filenames []string
+       var filenames []Path
        for _, file := range files {
                filename := file.Name()
                if matches(filename, `^CHANGES-20\d\d$`) && filename >= "CHANGES-2011" { // TODO: Why 2011?
-                       filenames = append(filenames, filename)
+                       filenames = append(filenames, NewPath(filename))
                }
        }
 
-       src.LastChange = make(map[string]*Change)
+       src.LastChange = make(map[Path]*Change)
        for _, filename := range filenames {
                changes := src.loadDocChangesFromFile(joinPath(docDir, filename))
                for _, change := range changes {
@@ -171,7 +170,7 @@ func (src *Pkgsrc) loadDocChanges() {
        src.checkRemovedAfterLastFreeze()
 }
 
-func (src *Pkgsrc) loadDocChangesFromFile(filename string) []*Change {
+func (src *Pkgsrc) loadDocChangesFromFile(filename Path) []*Change {
 
        warn := G.Opts.CheckGlobal && !G.Wip
 
@@ -179,7 +178,7 @@ func (src *Pkgsrc) loadDocChangesFromFil
        // This check has been added in 2018.
        // For years earlier than 2018 pkglint doesn't care because it's not a big issue anyway.
        year := ""
-       if _, yyyy := match1(filename, `-(\d\d\d\d)$`); yyyy >= "2018" {
+       if _, yyyy := match1(filename.Base(), `-(\d\d\d\d)$`); yyyy >= "2018" {
                year = yyyy
        }
 
@@ -313,7 +312,7 @@ func (*Pkgsrc) parseDocChange(line *Line
                return &Change{
                        Location: line.Location,
                        Action:   action,
-                       Pkgpath:  intern(pkgpath),
+                       Pkgpath:  NewPath(intern(pkgpath)),
                        target:   intern(condStr(n == 6, f[3], "")),
                        Author:   intern(author),
                        Date:     intern(date),
@@ -332,7 +331,7 @@ func (src *Pkgsrc) checkRemovedAfterLast
        for pkgpath, change := range src.LastChange {
                switch change.Action {
                case Added, Updated, Downgraded:
-                       if !dirExists(src.File(pkgpath)) {
+                       if !src.File(pkgpath).IsDir() {
                                wrong = append(wrong, change)
                        }
                }
@@ -360,32 +359,33 @@ func (src *Pkgsrc) parseSuggestedUpdates
        }
 
        var updates []SuggestedUpdate
-       state := 0
-       for _, line := range lines.Lines {
+
+       llex := NewLinesLexer(lines)
+       for !llex.EOF() && !llex.SkipText("Suggested package updates") {
+               llex.Skip()
+       }
+       for !llex.EOF() && !llex.SkipText("") {
+               llex.Skip()
+       }
+       for llex.SkipText("") {
+       }
+
+       for !llex.EOF() && !llex.SkipText("") {
+               line := llex.CurrentLine()
                text := line.Text
+               llex.Skip()
 
-               // TODO: Replace this state transition scheme with explicit code,
-               //  hoping that the code will be easier to understand.
-               if state == 0 && text == "Suggested package updates" {
-                       state = 1
-               } else if state == 1 && text == "" {
-                       state = 2
-               } else if state == 2 {
-                       state = 3
-               } else if state == 3 && text == "" {
-                       state = 4
-               }
-
-               if state == 3 {
-                       if m, pkgname, comment := match2(text, `^\to[\t ]([^\t ]+)(?:[\t ]*(.+))?$`); m {
-                               if m, pkgbase, pkgversion := match2(pkgname, rePkgname); m {
-                                       updates = append(updates, SuggestedUpdate{line.Location, intern(pkgbase), intern(pkgversion), intern(comment)})
-                               } else {
-                                       line.Warnf("Invalid package name %q.", pkgname)
+               if m, pkgname, comment := match2(text, `^\to[\t ]([^\t ]+)(?:[\t ]*(.+))?$`); m {
+                       if m, pkgbase, pkgversion := match2(pkgname, rePkgname); m {
+                               if hasPrefix(comment, "[") && hasSuffix(comment, "]") {
+                                       comment = comment[1 : len(comment)-1]
                                }
+                               updates = append(updates, SuggestedUpdate{line.Location, intern(pkgbase), intern(pkgversion), intern(comment)})
                        } else {
-                               line.Warnf("Invalid line format %q.", text)
+                               line.Warnf("Invalid package name %q.", pkgname)
                        }
+               } else {
+                       line.Warnf("Invalid line format %q.", text)
                }
        }
        return updates
@@ -405,14 +405,14 @@ func (src *Pkgsrc) loadUserDefinedVars()
 func (src *Pkgsrc) loadTools() {
        tools := src.Tools
 
-       toolFiles := []string{"defaults.mk"}
+       toolFiles := []Path{"defaults.mk"}
        {
                toc := src.File("mk/tools/bsd.tools.mk")
                mklines := LoadMk(toc, MustSucceed|NotEmpty)
                for _, mkline := range mklines.mklines {
                        if mkline.IsInclude() {
                                includedFile := mkline.IncludedFile()
-                               if !contains(includedFile, "/") {
+                               if !includedFile.ContainsText("/") {
                                        toolFiles = append(toolFiles, includedFile)
                                }
                        }
@@ -436,7 +436,7 @@ func (src *Pkgsrc) loadTools() {
                })
        }
 
-       for _, relativeName := range [...]string{"mk/bsd.prefs.mk", "mk/bsd.pkg.mk"} {
+       for _, relativeName := range [...]Path{"mk/bsd.prefs.mk", "mk/bsd.pkg.mk"} {
 
                mklines := src.LoadMk(relativeName, MustSucceed|NotEmpty)
                mklines.ForEach(func(mkline *MkLine) {
@@ -670,7 +670,7 @@ func (src *Pkgsrc) loadUntypedVars() {
                }
        }
 
-       handleMkFile := func(path string) {
+       handleMkFile := func(path Path) {
                mklines := LoadMk(path, MustSucceed)
                mklines.collectVariables()
                mklines.collectUsedVariables()
@@ -686,12 +686,12 @@ func (src *Pkgsrc) loadUntypedVars() {
                assertNil(err, "handleFile %q", pathName)
                baseName := info.Name()
                if info.Mode().IsRegular() && (hasSuffix(baseName, ".mk") || baseName == "mk.conf") {
-                       handleMkFile(filepath.ToSlash(pathName))
+                       handleMkFile(NewPath(filepath.ToSlash(pathName))) // FIXME: This is too deep to handle os-specific paths
                }
                return nil
        }
 
-       err := filepath.Walk(src.File("mk"), handleFile)
+       err := filepath.Walk(src.File("mk").String(), handleFile)
        assertNil(err, "Walk error in pkgsrc infrastructure")
 }
 
@@ -775,7 +775,7 @@ func (src *Pkgsrc) loadDefaultBuildDefs(
 // Example:
 //  Latest("lang", `^php[0-9]+$`, "../../lang/$0")
 //      => "../../lang/php72"
-func (src *Pkgsrc) Latest(category string, re regex.Pattern, repl string) string {
+func (src *Pkgsrc) Latest(category Path, re regex.Pattern, repl string) string {
        versions := src.ListVersions(category, re, repl, true)
 
        if len(versions) > 0 {
@@ -791,7 +791,7 @@ func (src *Pkgsrc) Latest(category strin
 // Example:
 //  ListVersions("lang", `^php[0-9]+$`, "php-$0")
 //      => {"php-53", "php-56", "php-73"}
-func (src *Pkgsrc) ListVersions(category string, re regex.Pattern, repl string, errorIfEmpty bool) []string {
+func (src *Pkgsrc) ListVersions(category Path, re regex.Pattern, repl string, errorIfEmpty bool) []string {
        if G.Testing {
                // Regular expression must be anchored at both ends, to avoid typos.
                assert(hasPrefix(string(re), "^"))
@@ -799,13 +799,11 @@ func (src *Pkgsrc) ListVersions(category
        }
 
        // TODO: Maybe convert cache key to a struct, to save allocations.
-       cacheKey := category + "/" + string(re) + " => " + repl
+       cacheKey := category.String() + "/" + string(re) + " => " + repl
        if latest, found := src.listVersions[cacheKey]; found {
                return latest
        }
 
-       categoryDir := src.File(category)
-
        var names []string
        for _, fileInfo := range src.ReadDir(category) {
                name := fileInfo.Name()
@@ -815,7 +813,7 @@ func (src *Pkgsrc) ListVersions(category
        }
        if len(names) == 0 {
                if errorIfEmpty {
-                       dummyLine.Errorf("Cannot find package versions of %q in %q.", re, categoryDir)
+                       dummyLine.Errorf("Cannot find package versions of %q in %q.", re, src.File(category))
                }
                src.listVersions[cacheKey] = nil
                return nil
@@ -973,7 +971,7 @@ func (src *Pkgsrc) checkToplevelUnusedLi
        for _, licenseFile := range src.ReadDir("licenses") {
                licenseName := licenseFile.Name()
                if !G.InterPackage.IsLicenseUsed(licenseName) {
-                       licensePath := joinPath(licensesDir, licenseName)
+                       licensePath := joinPath(licensesDir, NewPath(licenseName))
                        NewLineWhole(licensePath).Warnf("This license seems to be unused.")
                }
        }
@@ -997,9 +995,9 @@ func (src *Pkgsrc) IsBuildDef(varname st
 // ReadDir lists the files and subdirectories from the given directory
 // (relative to the pkgsrc root), filtering out any ignored files (CVS/*)
 // and empty directories.
-func (src *Pkgsrc) ReadDir(dirName string) []os.FileInfo {
+func (src *Pkgsrc) ReadDir(dirName Path) []os.FileInfo {
        dir := src.File(dirName)
-       files, err := ioutil.ReadDir(dir)
+       files, err := dir.ReadDir()
        if err != nil {
                return nil
        }
@@ -1007,7 +1005,7 @@ func (src *Pkgsrc) ReadDir(dirName strin
        var relevantFiles []os.FileInfo
        for _, dirent := range files {
                name := dirent.Name()
-               if !dirent.IsDir() || !isIgnoredFilename(name) && !isEmptyDir(joinPath(dir, name)) {
+               if !dirent.IsDir() || !isIgnoredFilename(name) && !isEmptyDir(dir.JoinNoClean(NewPath(name))) {
                        relevantFiles = append(relevantFiles, dirent)
                }
        }
@@ -1015,22 +1013,25 @@ func (src *Pkgsrc) ReadDir(dirName strin
        return relevantFiles
 }
 
-func (src *Pkgsrc) LoadMkInfra(filename string, options LoadOptions) *MkLines {
-       if G.Testing {
-               // During testing, the infrastructure files don't have to exist.
-               // They are often emulated by setting their data structures manually.
-               options &^= MustSucceed
+// LoadMkExisting loads a file that must exist.
+//
+// During pkglint testing, these files often don't exist, as they are
+// emulated by setting their data structures manually.
+func (src *Pkgsrc) LoadMkExisting(filename Path) *MkLines {
+       options := NotEmpty
+       if !G.Testing {
+               options |= MustSucceed
        }
        return src.LoadMk(filename, options)
 }
 
 // LoadMk loads the Makefile relative to the pkgsrc top directory.
-func (src *Pkgsrc) LoadMk(filename string, options LoadOptions) *MkLines {
+func (src *Pkgsrc) LoadMk(filename Path, options LoadOptions) *MkLines {
        return LoadMk(src.File(filename), options)
 }
 
 // Load loads the file relative to the pkgsrc top directory.
-func (src *Pkgsrc) Load(filename string, options LoadOptions) *Lines {
+func (src *Pkgsrc) Load(filename Path, options LoadOptions) *Lines {
        return Load(src.File(filename), options)
 }
 
@@ -1038,7 +1039,7 @@ func (src *Pkgsrc) Load(filename string,
 //
 // Example:
 //  NewPkgsrc("/usr/pkgsrc").File("distfiles") => "/usr/pkgsrc/distfiles"
-func (src *Pkgsrc) File(relativeName string) string {
+func (src *Pkgsrc) File(relativeName Path) Path {
        // TODO: Package.File resolves variables, Pkgsrc.File doesn't. They should behave the same.
        return cleanpath(joinPath(src.topdir, relativeName))
 }
@@ -1047,23 +1048,23 @@ func (src *Pkgsrc) File(relativeName str
 //
 // Example:
 //  NewPkgsrc("/usr/pkgsrc").ToRel("/usr/pkgsrc/distfiles") => "distfiles"
-func (src *Pkgsrc) ToRel(filename string) string {
+func (src *Pkgsrc) ToRel(filename Path) Path {
        return relpath(src.topdir, filename)
 }
 
 // IsInfra returns whether the given filename (relative to the pkglint
 // working directory) is part of the pkgsrc infrastructure.
-func (src *Pkgsrc) IsInfra(filename string) bool {
+func (src *Pkgsrc) IsInfra(filename Path) bool {
        rel := src.ToRel(filename)
-       return hasPrefix(rel, "mk/") || hasPrefix(rel, "wip/mk/")
+       return rel.HasPrefixPath("mk") || rel.HasPrefixPath("wip/mk")
 }
 
 // Change describes a modification to a single package, from the doc/CHANGES-* files.
 type Change struct {
        Location Location
        Action   ChangeAction // Added, Updated, Downgraded, Renamed, Moved, Removed
-       Pkgpath  string       // For renamed or moved packages, the previous PKGPATH
-       target   string
+       Pkgpath  Path         // For renamed or moved packages, the previous PKGPATH
+       target   string       // The path or version number, depending on the action
        Author   string
        Date     string
 }
@@ -1075,9 +1076,9 @@ func (ch *Change) Version() string {
 }
 
 // Target returns the target PKGPATH for a Renamed or Moved package.
-func (ch *Change) Target() string {
+func (ch *Change) Target() Path {
        assert(ch.Action == Renamed || ch.Action == Moved)
-       return ch.target
+       return NewPath(ch.target)
 }
 
 // Successor returns the successor for a Removed package.

Index: pkgsrc/pkgtools/pkglint/files/plist.go
diff -u pkgsrc/pkgtools/pkglint/files/plist.go:1.44 pkgsrc/pkgtools/pkglint/files/plist.go:1.45
--- pkgsrc/pkgtools/pkglint/files/plist.go:1.44 Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/plist.go      Sat Nov 23 23:35:56 2019
@@ -9,7 +9,7 @@ import (
 
 func CheckLinesPlist(pkg *Package, lines *Lines) {
        if trace.Tracing {
-               defer trace.Call1(lines.Filename)()
+               defer trace.Call(lines.Filename)()
        }
 
        idOk := lines.CheckCvsID(0, `@comment `, "@comment ")
@@ -31,8 +31,8 @@ func CheckLinesPlist(pkg *Package, lines
 
        ck := PlistChecker{
                pkg,
-               make(map[string]*PlistLine),
-               make(map[string]*PlistLine),
+               make(map[Path]*PlistLine),
+               make(map[Path]*PlistLine),
                "",
                Once{},
                false}
@@ -41,8 +41,8 @@ func CheckLinesPlist(pkg *Package, lines
 
 type PlistChecker struct {
        pkg             *Package
-       allFiles        map[string]*PlistLine
-       allDirs         map[string]*PlistLine
+       allFiles        map[Path]*PlistLine
+       allDirs         map[Path]*PlistLine
        lastFname       string
        once            Once
        nonAsciiAllowed bool
@@ -53,7 +53,7 @@ func (ck *PlistChecker) Load(lines *Line
        ck.collectFilesAndDirs(plines)
 
        if lines.BaseName == "PLIST.common_end" {
-               commonLines := Load(strings.TrimSuffix(lines.Filename, "_end"), NotEmpty)
+               commonLines := Load(lines.Filename.TrimSuffix("_end"), NotEmpty)
                if commonLines != nil {
                        ck.collectFilesAndDirs(ck.NewLines(commonLines))
                }
@@ -107,15 +107,16 @@ func (ck *PlistChecker) collectFilesAndD
                        first := text[0]
                        switch {
                        case plistLineStart.Contains(first):
-                               if prev := ck.allFiles[text]; prev == nil || stringSliceLess(pline.conditions, prev.conditions) {
-                                       ck.allFiles[text] = pline
+                               path := NewPath(text)
+                               if prev := ck.allFiles[path]; prev == nil || stringSliceLess(pline.conditions, prev.conditions) {
+                                       ck.allFiles[path] = pline
                                }
-                               for dir := path.Dir(text); dir != "."; dir = path.Dir(dir) {
+                               for dir := path.Dir(); dir != "."; dir = dir.Dir() {
                                        ck.allDirs[dir] = pline
                                }
                        case first == '@':
                                if m, dirname := match1(text, `^@exec \$\{MKDIR\} %D/(.*)$`); m {
-                                       for dir := dirname; dir != "."; dir = path.Dir(dir) {
+                                       for dir := NewPath(dirname); dir != "."; dir = dir.Dir() {
                                                ck.allDirs[dir] = pline
                                        }
                                }
@@ -259,7 +260,7 @@ func (ck *PlistChecker) checkDuplicate(p
                return
        }
 
-       prev := ck.allFiles[text]
+       prev := ck.allFiles[NewPath(text)]
        if prev == pline || len(prev.conditions) > 0 {
                return
        }
@@ -317,7 +318,7 @@ func (ck *PlistChecker) checkPathLib(pli
 
        if contains(basename, ".a") || contains(basename, ".so") {
                if m, noext := match1(pline.text, `^(.*)(?:\.a|\.so[0-9.]*)$`); m {
-                       if laLine := ck.allFiles[noext+".la"]; laLine != nil {
+                       if laLine := ck.allFiles[NewPath(noext+".la")]; laLine != nil {
                                pline.Warnf("Redundant library found. The libtool library is in %s.", pline.RefTo(laLine.Line))
                        }
                }
@@ -350,7 +351,7 @@ func (ck *PlistChecker) checkPathMan(pli
                pline.Warnf("Unknown section %q for manual page.", section)
        }
 
-       if catOrMan == "cat" && ck.allFiles["man/man"+section+"/"+manpage+"."+section] == nil {
+       if catOrMan == "cat" && ck.allFiles[NewPath("man/man"+section+"/"+manpage+"."+section)] == nil {
                pline.Warnf("Preformatted manual page without unformatted one.")
        }
 

Index: pkgsrc/pkgtools/pkglint/files/redundantscope.go
diff -u pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.7 pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.8
--- pkgsrc/pkgtools/pkglint/files/redundantscope.go:1.7 Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/redundantscope.go     Sat Nov 23 23:35:56 2019
@@ -223,14 +223,14 @@ func (s *RedundantScope) onOverwrite(ove
 // one of two variable assignments is redundant. Two assignments can
 // only be redundant if one location includes the other.
 type includePath struct {
-       files []string
+       files []Path
 }
 
-func (p *includePath) push(filename string) {
+func (p *includePath) push(filename Path) {
        p.files = append(p.files, filename)
 }
 
-func (p *includePath) popUntil(filename string) {
+func (p *includePath) popUntil(filename Path) {
        for p.files[len(p.files)-1] != filename {
                p.files = p.files[:len(p.files)-1]
        }
@@ -276,5 +276,5 @@ func (p *includePath) equals(other inclu
 }
 
 func (p *includePath) copy() includePath {
-       return includePath{append([]string(nil), p.files...)}
+       return includePath{append([]Path(nil), p.files...)}
 }
Index: pkgsrc/pkgtools/pkglint/files/redundantscope_test.go
diff -u pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.7 pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/redundantscope_test.go:1.7    Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/redundantscope_test.go        Sat Nov 23 23:35:56 2019
@@ -1525,7 +1525,7 @@ func (s *Suite) Test_RedundantScope_hand
 func (s *Suite) Test_includePath_includes(c *check.C) {
        t := s.Init(c)
 
-       path := func(locations ...string) includePath {
+       path := func(locations ...Path) includePath {
                return includePath{locations}
        }
 
@@ -1550,7 +1550,7 @@ func (s *Suite) Test_includePath_include
 func (s *Suite) Test_includePath_equals(c *check.C) {
        t := s.Init(c)
 
-       path := func(locations ...string) includePath {
+       path := func(locations ...Path) includePath {
                return includePath{locations}
        }
 

Index: pkgsrc/pkgtools/pkglint/files/shell_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shell_test.go:1.56 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.57
--- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.56    Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/shell_test.go Sat Nov 23 23:35:56 2019
@@ -912,7 +912,10 @@ func (s *Suite) Test_ShellLineChecker_Ch
                "WARN: Makefile:3: $f is ambiguous. Use ${f} if you mean a Make variable or $$f if you mean a shell variable.",
                "WARN: Makefile:3: $f is ambiguous. Use ${f} if you mean a Make variable or $$f if you mean a shell variable.",
                "NOTE: Makefile:3: Please use the SUBST framework instead of ${SED} and ${MV}.",
-               "WARN: Makefile:3: f is used but not defined.")
+               "WARN: Makefile:3: f is used but not defined.",
+               // TODO: Avoid these duplicate diagnostics.
+               "WARN: Makefile:3: $f is ambiguous. Use ${f} if you mean a Make variable or $$f if you mean a shell variable.",
+               "WARN: Makefile:3: $f is ambiguous. Use ${f} if you mean a Make variable or $$f if you mean a shell variable.")
 
        ck.CheckShellCommandLine("install -c manpage.1 ${PREFIX}/man/man1/manpage.1")
 

Index: pkgsrc/pkgtools/pkglint/files/shtokenizer.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.19 pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.20
--- pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.19   Mon Jul  1 22:25:52 2019
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer.go        Sat Nov 23 23:35:56 2019
@@ -8,6 +8,7 @@ type ShTokenizer struct {
 
 func NewShTokenizer(line *Line, text string, emitWarnings bool) *ShTokenizer {
        // TODO: Switching to NewMkParser is nontrivial since emitWarnings must equal (line != nil).
+       // assert((line != nil) == emitWarnings)
        p := MkParser{line, textproc.NewLexer(text), emitWarnings}
        return &ShTokenizer{&p}
 }
Index: pkgsrc/pkgtools/pkglint/files/tools.go
diff -u pkgsrc/pkgtools/pkglint/files/tools.go:1.19 pkgsrc/pkgtools/pkglint/files/tools.go:1.20
--- pkgsrc/pkgtools/pkglint/files/tools.go:1.19 Sun Sep  8 22:47:47 2019
+++ pkgsrc/pkgtools/pkglint/files/tools.go      Sat Nov 23 23:35:56 2019
@@ -345,7 +345,7 @@ func (tr *Tools) implicitTools(toolName 
 
 func (tr *Tools) validity(basename string, useTools bool) Validity {
        switch {
-       case IsPrefs(basename): // IsPrefs is not 100% accurate here but good enough
+       case IsPrefs(NewPath(basename)): // IsPrefs is not 100% accurate here but good enough
                return AfterPrefsMk
        case basename == "Makefile" && !tr.SeenPrefs:
                return AfterPrefsMk

Index: pkgsrc/pkgtools/pkglint/files/testnames_test.go
diff -u pkgsrc/pkgtools/pkglint/files/testnames_test.go:1.8 pkgsrc/pkgtools/pkglint/files/testnames_test.go:1.9
--- pkgsrc/pkgtools/pkglint/files/testnames_test.go:1.8 Tue Nov 19 06:51:38 2019
+++ pkgsrc/pkgtools/pkglint/files/testnames_test.go     Sat Nov 23 23:35:56 2019
@@ -11,6 +11,7 @@ import (
 func (s *Suite) Test__test_names(c *check.C) {
        ck := intqa.NewTestNameChecker(c.Errorf)
        ck.Configure("*", "*", "*", -intqa.EMissingTest)
+       ck.Configure("path.go", "*", "*", +intqa.EMissingTest)
        ck.Configure("*yacc.go", "*", "*", intqa.ENone)
        ck.Check()
 }

Index: pkgsrc/pkgtools/pkglint/files/tools_test.go
diff -u pkgsrc/pkgtools/pkglint/files/tools_test.go:1.21 pkgsrc/pkgtools/pkglint/files/tools_test.go:1.22
--- pkgsrc/pkgtools/pkglint/files/tools_test.go:1.21    Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/tools_test.go Sat Nov 23 23:35:56 2019
@@ -46,7 +46,7 @@ func (s *Suite) Test_Tools__USE_TOOLS_pr
                "\t${SED} < input > output",
                "\t${AWK} < input > output")
 
-       t.Main("-Wall", t.File("module.mk"))
+       t.Main("-Wall", "module.mk")
 
        // Since this test doesn't load the usual tool definitions via
        // G.Pkgsrc.loadTools, AWK is not known at all.

Index: pkgsrc/pkgtools/pkglint/files/toplevel.go
diff -u pkgsrc/pkgtools/pkglint/files/toplevel.go:1.23 pkgsrc/pkgtools/pkglint/files/toplevel.go:1.24
--- pkgsrc/pkgtools/pkglint/files/toplevel.go:1.23      Fri Nov  1 19:56:53 2019
+++ pkgsrc/pkgtools/pkglint/files/toplevel.go   Sat Nov 23 23:35:56 2019
@@ -1,14 +1,14 @@
 package pkglint
 
 type Toplevel struct {
-       dir            string
-       previousSubdir string
-       subdirs        []string
+       dir            Path
+       previousSubdir Path
+       subdirs        []Path
 }
 
-func CheckdirToplevel(dir string) {
+func CheckdirToplevel(dir Path) {
        if trace.Tracing {
-               defer trace.Call1(dir)()
+               defer trace.Call(dir)()
        }
 
        ctx := Toplevel{dir, "", nil}
@@ -36,7 +36,7 @@ func CheckdirToplevel(dir string) {
 }
 
 func (ctx *Toplevel) checkSubdir(mkline *MkLine) {
-       subdir := mkline.Value()
+       subdir := NewPath(mkline.Value())
 
        if mkline.IsCommentedVarassign() {
                if !mkline.HasComment() || mkline.Comment() == "" {
@@ -44,7 +44,7 @@ func (ctx *Toplevel) checkSubdir(mkline 
                }
        }
 
-       if containsVarRef(subdir) || !fileExists(joinPath(ctx.dir, subdir, "Makefile")) {
+       if containsVarRef(subdir.String()) || !ctx.dir.JoinNoClean(subdir).JoinNoClean("Makefile").IsFile() {
                return
        }
 
Index: pkgsrc/pkgtools/pkglint/files/vardefs_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.23 pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.24
--- pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.23  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/vardefs_test.go       Sat Nov 23 23:35:56 2019
@@ -178,7 +178,7 @@ func (s *Suite) Test_VarTypeRegistry_Ini
        G.Testing = false
        t.ExpectFatal(
                t.FinishSetUp,
-               "FATAL: ~/mk/fetch/sites.mk: Cannot be read.")
+               "FATAL: ~/editors/emacs/modules.mk: Cannot be read.")
 }
 
 func (s *Suite) Test_VarTypeRegistry_Init__MASTER_SITES(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/vardefs.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.77 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.78
--- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.77       Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/vardefs.go    Sat Nov 23 23:35:56 2019
@@ -2,7 +2,6 @@ package pkglint
 
 import (
        "netbsd.org/pkglint/regex"
-       "path"
        "strings"
 )
 
@@ -331,7 +330,7 @@ func (reg *VarTypeRegistry) infralist(va
 // compilerLanguages reads the available languages that are typically
 // bundled in a single compiler framework, such as GCC or Clang.
 func (reg *VarTypeRegistry) compilerLanguages(src *Pkgsrc) *BasicType {
-       mklines := src.LoadMkInfra("mk/compiler.mk", NotEmpty|MustSucceed)
+       mklines := src.LoadMkExisting("mk/compiler.mk")
 
        languages := make(map[string]bool)
        if mklines != nil {
@@ -371,8 +370,8 @@ func (reg *VarTypeRegistry) compilerLang
 //
 // If the file cannot be found, the allowed values are taken from
 // defval. This is mostly useful when testing pkglint.
-func (reg *VarTypeRegistry) enumFrom(pkgsrc *Pkgsrc, filename string, defval string, varcanons ...string) *BasicType {
-       mklines := LoadMk(pkgsrc.File(filename), NotEmpty)
+func (reg *VarTypeRegistry) enumFrom(pkgsrc *Pkgsrc, filename Path, defval string, varcanons ...string) *BasicType {
+       mklines := pkgsrc.LoadMkExisting(filename)
        if mklines == nil {
                return enum(defval)
        }
@@ -419,7 +418,7 @@ func (reg *VarTypeRegistry) enumFrom(pkg
 //
 // If the directories cannot be found, the allowed values are taken
 // from defval. This is mostly useful when testing pkglint.
-func (reg *VarTypeRegistry) enumFromDirs(pkgsrc *Pkgsrc, category string, re regex.Pattern, repl string, defval string) *BasicType {
+func (reg *VarTypeRegistry) enumFromDirs(pkgsrc *Pkgsrc, category Path, re regex.Pattern, repl string, defval string) *BasicType {
        versions := pkgsrc.ListVersions(category, re, repl, false)
        if len(versions) == 0 {
                return enum(defval)
@@ -432,10 +431,10 @@ func (reg *VarTypeRegistry) enumFromDirs
 //
 // If no files are found, the allowed values are taken
 // from defval. This should only happen in the pkglint tests.
-func (reg *VarTypeRegistry) enumFromFiles(basedir string, re regex.Pattern, repl string, defval string) *BasicType {
+func (reg *VarTypeRegistry) enumFromFiles(basedir Path, re regex.Pattern, repl string, defval string) *BasicType {
        var relevant []string
        for _, filename := range dirglob(G.Pkgsrc.File(basedir)) {
-               basename := path.Base(filename)
+               basename := filename.Base()
                if matches(basename, re) {
                        relevant = append(relevant, replaceAll(basename, re, repl))
                }
@@ -1249,8 +1248,8 @@ func (reg *VarTypeRegistry) Init(src *Pk
        reg.sys("MANOWN", BtUserGroupName)
        reg.pkglist("MASTER_SITES", BtFetchURL)
 
-       for _, filename := range []string{"mk/fetch/sites.mk", "mk/fetch/fetch.mk"} {
-               sitesMk := src.LoadMkInfra(filename, NotEmpty|MustSucceed)
+       for _, filename := range []Path{"mk/fetch/sites.mk", "mk/fetch/fetch.mk"} {
+               sitesMk := src.LoadMkExisting(filename)
                if sitesMk != nil {
                        sitesMk.ForEach(func(mkline *MkLine) {
                                if mkline.IsVarassign() && hasPrefix(mkline.Varname(), "MASTER_SITE_") {

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.66 pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.67
--- pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.66  Sun Nov 17 01:26:25 2019
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck.go       Sat Nov 23 23:35:56 2019
@@ -225,7 +225,7 @@ func (cv *VartypeCheck) BuildlinkDepmeth
 }
 
 func (cv *VartypeCheck) Category() {
-       if cv.Value != "wip" && fileExists(G.Pkgsrc.File(cv.Value+"/Makefile")) {
+       if cv.Value != "wip" && G.Pkgsrc.File(NewPath(cv.Value).JoinNoClean("Makefile")).IsFile() {
                return
        }
 
@@ -452,7 +452,7 @@ func (cv *VartypeCheck) DependencyWithPa
                }
 
                if !containsVarRef(relpath) {
-                       MkLineChecker{cv.MkLines, cv.MkLine}.CheckRelativePkgdir(relpath)
+                       MkLineChecker{cv.MkLines, cv.MkLine}.CheckRelativePkgdir(NewPath(relpath))
                }
 
                switch pkg {
@@ -1073,22 +1073,23 @@ func (cv *VartypeCheck) PkgOptionsVar() 
 func (cv *VartypeCheck) Pkgpath() {
        cv.Pathname()
 
-       pkgpath := cv.Value
-       if pkgpath != cv.ValueNoVar || cv.Op == opUseMatch {
+       value := cv.Value
+       if value != cv.ValueNoVar || cv.Op == opUseMatch {
                return
        }
 
-       if !G.Wip && hasPrefix(pkgpath, "wip/") {
+       pkgpath := NewPath(value)
+       if !G.Wip && pkgpath.HasPrefixPath("wip") {
                cv.MkLine.Errorf("A main pkgsrc package must not depend on a pkgsrc-wip package.")
        }
 
-       if !fileExists(G.Pkgsrc.File(joinPath(pkgpath, "Makefile"))) {
+       if !G.Pkgsrc.File(pkgpath.JoinNoClean("Makefile")).IsFile() {
                cv.MkLine.Errorf("There is no package in %q.",
-                       relpath(path.Dir(cv.MkLine.Filename), G.Pkgsrc.File(pkgpath)))
+                       cv.MkLine.PathToFile(G.Pkgsrc.File(pkgpath)))
                return
        }
 
-       if !matches(pkgpath, `^([^./][^/]*/[^./][^/]*)$`) {
+       if !matches(value, `^([^./][^/]*/[^./][^/]*)$`) {
                cv.MkLine.Errorf("%q is not a valid path to a package.", pkgpath)
                cv.MkLine.Explain(
                        "A path to a package has the form \"category/pkgbase\".",
@@ -1164,7 +1165,7 @@ func (cv *VartypeCheck) RPkgVer() {
 
 // RelativePkgDir refers to a package directory, e.g. ../../category/pkgbase.
 func (cv *VartypeCheck) RelativePkgDir() {
-       MkLineChecker{cv.MkLines, cv.MkLine}.CheckRelativePkgdir(cv.Value)
+       MkLineChecker{cv.MkLines, cv.MkLine}.CheckRelativePkgdir(NewPath(cv.Value))
 }
 
 // RelativePkgPath refers to a file or directory, e.g. ../../category/pkgbase,
@@ -1172,7 +1173,7 @@ func (cv *VartypeCheck) RelativePkgDir()
 //
 // See RelativePkgDir, which requires a directory, not a file.
 func (cv *VartypeCheck) RelativePkgPath() {
-       MkLineChecker{cv.MkLines, cv.MkLine}.CheckRelativePath(cv.Value, true)
+       MkLineChecker{cv.MkLines, cv.MkLine}.CheckRelativePath(NewPath(cv.Value), true)
 }
 
 func (cv *VartypeCheck) Restricted() {

Index: pkgsrc/pkgtools/pkglint/files/textproc/lexer.go
diff -u pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.6 pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.7
--- pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.6 Tue Oct  1 21:38:00 2019
+++ pkgsrc/pkgtools/pkglint/files/textproc/lexer.go     Sat Nov 23 23:35:56 2019
@@ -75,7 +75,7 @@ func (l *Lexer) NextString(prefix string
        return ""
 }
 
-// SkipString skips over the given string, if the remaining string starts
+// SkipText skips over the given string, if the remaining string starts
 // with it. It returns whether it actually skipped.
 func (l *Lexer) SkipString(prefix string) bool {
        skipped := strings.HasPrefix(l.rest, prefix)

Index: pkgsrc/pkgtools/pkglint/files/trace/tracing.go
diff -u pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.10 pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.11
--- pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.10 Sun Jun 30 20:56:19 2019
+++ pkgsrc/pkgtools/pkglint/files/trace/tracing.go      Sat Nov 23 23:35:56 2019
@@ -92,7 +92,9 @@ func argsStr(args []interface{}) string 
                if rv.Len() > 0 {
                        rv.WriteString(", ")
                }
-               if str, ok := arg.(fmt.Stringer); ok && !isNil(str) {
+               if str, ok := arg.(fmt.GoStringer); ok && !isNil(str) {
+                       rv.WriteString(str.GoString())
+               } else if str, ok := arg.(fmt.Stringer); ok && !isNil(str) {
                        rv.WriteString(str.String())
                } else {
                        _, _ = fmt.Fprintf(&rv, "%#v", arg)



Home | Main Index | Thread Index | Old Index