Received: by mail.netbsd.org (Postfix, from userid 605) id C4EFB84DCA; Wed, 3 Oct 2018 22:28:21 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by mail.netbsd.org (Postfix) with ESMTP id C870284D59 for ; Wed, 3 Oct 2018 22:28:20 +0000 (UTC) X-Virus-Scanned: amavisd-new at netbsd.org Received: from mail.netbsd.org ([IPv6:::1]) by localhost (mail.netbsd.org [IPv6:::1]) (amavisd-new, port 10025) with ESMTP id cvr7ZdnXAeyb for ; Wed, 3 Oct 2018 22:27:55 +0000 (UTC) Received: from cvs.NetBSD.org (ivanova.netbsd.org [199.233.217.197]) by mail.netbsd.org (Postfix) with ESMTP id D83AE84D1F for ; Wed, 3 Oct 2018 22:27:54 +0000 (UTC) Received: by cvs.NetBSD.org (Postfix, from userid 500) id D3138FBEE; Wed, 3 Oct 2018 22:27:54 +0000 (UTC) Content-Transfer-Encoding: 7bit Content-Type: multipart/mixed; boundary="_----------=_153860567463630" MIME-Version: 1.0 Date: Wed, 3 Oct 2018 22:27:54 +0000 From: "Roland Illig" Subject: CVS commit: pkgsrc/pkgtools/pkglint To: pkgsrc-changes@NetBSD.org Reply-To: rillig@netbsd.org X-Mailer: log_accum Message-Id: <20181003222754.D3138FBEE@cvs.NetBSD.org> Sender: pkgsrc-changes-owner@NetBSD.org List-Id: pkgsrc-changes.NetBSD.org Precedence: bulk List-Unsubscribe: This is a multi-part message in MIME format. --_----------=_153860567463630 Content-Disposition: inline Content-Transfer-Encoding: 8bit Content-Type: text/plain; charset="US-ASCII" Module Name: pkgsrc Committed By: rillig Date: Wed Oct 3 22:27:54 UTC 2018 Modified Files: pkgsrc/pkgtools/pkglint: Makefile select.mk pkgsrc/pkgtools/pkglint/files: alternatives.go alternatives_test.go autofix.go autofix_test.go buildlink3_test.go category_test.go check_test.go distinfo_test.go expecter.go files.go files_test.go licenses.go licenses_test.go line.go linechecker.go linechecker_test.go logging.go logging_test.go mkline.go mkline_test.go mklinechecker.go mklinechecker_test.go mklines.go mklines_test.go mkparser.go mkparser_test.go mkshparser_test.go mkshtypes.go mkshwalker.go mkshwalker_test.go options.go options_test.go package.go package_test.go parser.go patches_test.go pkglint.go pkglint_test.go pkgsrc.go pkgsrc_test.go plist.go plist_test.go shell.go shell_test.go shtokenizer.go shtokenizer_test.go shtypes.go substcontext_test.go tools.go tools_test.go toplevel_test.go util.go util_test.go vardefs.go vardefs_test.go vartype.go vartype_test.go vartypecheck.go vartypecheck_test.go pkgsrc/pkgtools/pkglint/files/getopt: getopt_test.go pkgsrc/pkgtools/pkglint/files/licenses: licenses.go licenses_test.go pkgsrc/pkgtools/pkglint/files/regex: regex.go pkgsrc/pkgtools/pkglint/files/textproc: prefixreplacer.go Added Files: pkgsrc/pkgtools/pkglint/files: fuzzer_test.go testnames_test.go Log Message: pkgtools/pkglint: Update to 5.6.3 Changes since 5.6.2: * Add check for version patterns 1.5*, which should rather be 1.5.* * Re-enable check for "set -e" and commands that may silently fail because of missing error checking * Lots of internal clean-up and tests To generate a diff of this commit: cvs rdiff -u -r1.548 -r1.549 pkgsrc/pkgtools/pkglint/Makefile cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/select.mk cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/alternatives.go \ pkgsrc/pkgtools/pkglint/files/alternatives_test.go \ pkgsrc/pkgtools/pkglint/files/logging_test.go \ pkgsrc/pkgtools/pkglint/files/mkshwalker.go \ pkgsrc/pkgtools/pkglint/files/options.go \ pkgsrc/pkgtools/pkglint/files/options_test.go \ pkgsrc/pkgtools/pkglint/files/tools.go \ pkgsrc/pkgtools/pkglint/files/tools_test.go cvs rdiff -u -r1.9 -r1.10 pkgsrc/pkgtools/pkglint/files/autofix.go \ pkgsrc/pkgtools/pkglint/files/parser.go \ pkgsrc/pkgtools/pkglint/files/pkgsrc.go \ pkgsrc/pkgtools/pkglint/files/shtypes.go cvs rdiff -u -r1.10 -r1.11 pkgsrc/pkgtools/pkglint/files/autofix_test.go \ pkgsrc/pkgtools/pkglint/files/shtokenizer.go cvs rdiff -u -r1.17 -r1.18 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go \ pkgsrc/pkgtools/pkglint/files/distinfo_test.go \ pkgsrc/pkgtools/pkglint/files/files_test.go cvs rdiff -u -r1.12 -r1.13 pkgsrc/pkgtools/pkglint/files/category_test.go \ pkgsrc/pkgtools/pkglint/files/expecter.go \ pkgsrc/pkgtools/pkglint/files/toplevel_test.go cvs rdiff -u -r1.25 -r1.26 pkgsrc/pkgtools/pkglint/files/check_test.go \ pkgsrc/pkgtools/pkglint/files/line.go \ pkgsrc/pkgtools/pkglint/files/plist_test.go \ pkgsrc/pkgtools/pkglint/files/shell.go cvs rdiff -u -r1.18 -r1.19 pkgsrc/pkgtools/pkglint/files/files.go \ pkgsrc/pkgtools/pkglint/files/mklinechecker.go cvs rdiff -u -r0 -r1.1 pkgsrc/pkgtools/pkglint/files/fuzzer_test.go \ pkgsrc/pkgtools/pkglint/files/testnames_test.go cvs rdiff -u -r1.14 -r1.15 pkgsrc/pkgtools/pkglint/files/licenses.go \ pkgsrc/pkgtools/pkglint/files/logging.go \ pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go \ pkgsrc/pkgtools/pkglint/files/mkparser_test.go \ pkgsrc/pkgtools/pkglint/files/substcontext_test.go cvs rdiff -u -r1.15 -r1.16 pkgsrc/pkgtools/pkglint/files/licenses_test.go cvs rdiff -u -r1.7 -r1.8 pkgsrc/pkgtools/pkglint/files/linechecker.go \ pkgsrc/pkgtools/pkglint/files/linechecker_test.go \ pkgsrc/pkgtools/pkglint/files/mkshtypes.go \ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go \ pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go \ pkgsrc/pkgtools/pkglint/files/vartype_test.go cvs rdiff -u -r1.37 -r1.38 pkgsrc/pkgtools/pkglint/files/mkline.go \ pkgsrc/pkgtools/pkglint/files/pkglint.go cvs rdiff -u -r1.41 -r1.42 pkgsrc/pkgtools/pkglint/files/mkline_test.go cvs rdiff -u -r1.31 -r1.32 pkgsrc/pkgtools/pkglint/files/mklines.go \ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go cvs rdiff -u -r1.27 -r1.28 pkgsrc/pkgtools/pkglint/files/mklines_test.go cvs rdiff -u -r1.16 -r1.17 pkgsrc/pkgtools/pkglint/files/mkparser.go \ pkgsrc/pkgtools/pkglint/files/vartype.go cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/mkshparser_test.go cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go \ pkgsrc/pkgtools/pkglint/files/vardefs_test.go cvs rdiff -u -r1.35 -r1.36 pkgsrc/pkgtools/pkglint/files/package.go cvs rdiff -u -r1.29 -r1.30 pkgsrc/pkgtools/pkglint/files/package_test.go cvs rdiff -u -r1.20 -r1.21 pkgsrc/pkgtools/pkglint/files/patches_test.go cvs rdiff -u -r1.24 -r1.25 pkgsrc/pkgtools/pkglint/files/pkglint_test.go cvs rdiff -u -r1.28 -r1.29 pkgsrc/pkgtools/pkglint/files/plist.go \ pkgsrc/pkgtools/pkglint/files/util.go cvs rdiff -u -r1.30 -r1.31 pkgsrc/pkgtools/pkglint/files/shell_test.go cvs rdiff -u -r1.13 -r1.14 pkgsrc/pkgtools/pkglint/files/util_test.go cvs rdiff -u -r1.45 -r1.46 pkgsrc/pkgtools/pkglint/files/vardefs.go cvs rdiff -u -r1.39 -r1.40 pkgsrc/pkgtools/pkglint/files/vartypecheck.go cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go cvs rdiff -u -r1.2 -r1.3 pkgsrc/pkgtools/pkglint/files/licenses/licenses.go cvs rdiff -u -r1.1 -r1.2 \ pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/regex/regex.go cvs rdiff -u -r1.5 -r1.6 \ pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go Please note that diffs are not public domain; they are subject to the copyright notices on the relevant files. --_----------=_153860567463630 Content-Disposition: inline Content-Length: 337842 Content-Transfer-Encoding: binary Content-Type: text/x-diff; charset=us-ascii Modified files: Index: pkgsrc/pkgtools/pkglint/Makefile diff -u pkgsrc/pkgtools/pkglint/Makefile:1.548 pkgsrc/pkgtools/pkglint/Makefile:1.549 --- pkgsrc/pkgtools/pkglint/Makefile:1.548 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/Makefile Wed Oct 3 22:27:53 2018 @@ -1,6 +1,6 @@ -# $NetBSD: Makefile,v 1.548 2018/09/05 17:56:22 rillig Exp $ +# $NetBSD: Makefile,v 1.549 2018/10/03 22:27:53 rillig Exp $ -PKGNAME= pkglint-5.6.2 +PKGNAME= pkglint-5.6.3 DISTFILES= # none CATEGORIES= pkgtools Index: pkgsrc/pkgtools/pkglint/select.mk diff -u pkgsrc/pkgtools/pkglint/select.mk:1.4 pkgsrc/pkgtools/pkglint/select.mk:1.5 --- pkgsrc/pkgtools/pkglint/select.mk:1.4 Wed Mar 23 16:36:53 2016 +++ pkgsrc/pkgtools/pkglint/select.mk Wed Oct 3 22:27:53 2018 @@ -1,4 +1,4 @@ -# $NetBSD: select.mk,v 1.4 2016/03/23 16:36:53 gdt Exp $ +# $NetBSD: select.mk,v 1.5 2018/10/03 22:27:53 rillig Exp $ # # Selects the proper version of pkglint, depending on whether the # platform supports the Go programming language. @@ -8,7 +8,7 @@ # See lang/go/version.mk # While it's wrong in the above, go14 does not build on NetBSD 5. -.if ${MACHINE_ARCH:Ni386:Nx86_64:Nevbarm} || ${MACHINE_PLATFORM:MSunOS-*-i386} || ${MACHINE_PLATFORM:MNetBSD-[1-5]*-*} +.if ${MACHINE_ARCH:Ni386:Nx86_64:Nevbarm} || ${MACHINE_PLATFORM:MSunOS-*-i386} || ${MACHINE_PLATFORM:MNetBSD-[1-5].*-*} DEPENDS+= pkglint4>=4.82<5:../../pkgtools/pkglint4 .else DEPENDS+= pkglint>=5:../../pkgtools/pkglint Index: pkgsrc/pkgtools/pkglint/files/alternatives.go diff -u pkgsrc/pkgtools/pkglint/files/alternatives.go:1.4 pkgsrc/pkgtools/pkglint/files/alternatives.go:1.5 --- pkgsrc/pkgtools/pkglint/files/alternatives.go:1.4 Thu Aug 16 20:41:42 2018 +++ pkgsrc/pkgtools/pkglint/files/alternatives.go Wed Oct 3 22:27:53 2018 @@ -1,9 +1,6 @@ package main -import ( - "netbsd.org/pkglint/regex" - "strings" -) +import "strings" func CheckfileAlternatives(filename string, plistFiles map[string]bool) { lines := Load(filename, NotEmpty|LogErrors) @@ -19,7 +16,7 @@ func CheckfileAlternatives(filename stri } relImplementation := strings.Replace(implementation, "@PREFIX@/", "", 1) - plistName := regex.Compile(`@(\w+)@`).ReplaceAllString(relImplementation, "${$1}") + plistName := replaceAll(relImplementation, `@(\w+)@`, "${$1}") if !plistFiles[plistName] && !G.Pkg.vars.Defined("ALTERNATIVES_SRC") { if plistName != implementation { line.Errorf("Alternative implementation %q must appear in the PLIST as %q.", implementation, plistName) Index: pkgsrc/pkgtools/pkglint/files/alternatives_test.go diff -u pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.4 pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.5 --- pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.4 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/alternatives_test.go Wed Oct 3 22:27:53 2018 @@ -2,25 +2,30 @@ package main import "gopkg.in/check.v1" -func (s *Suite) Test_Alternatives_PLIST(c *check.C) { +func (s *Suite) Test_CheckfileAlternatives__PLIST(c *check.C) { t := s.Init(c) + t.SetupPackage("category/package") t.Chdir("category/package") - t.SetupFileLines("ALTERNATIVES", + t.CreateFileLines("ALTERNATIVES", "sbin/sendmail @PREFIX@/sbin/sendmail.postfix@POSTFIXVER@", "sbin/sendmail @PREFIX@/sbin/sendmail.exim@EXIMVER@", "bin/echo bin/gnu-echo", "bin/editor bin/vim -e", "invalid") + t.CreateFileLines("PLIST", + PlistRcsID, + "bin/echo", + "bin/vim", + "sbin/sendmail.exim${EXIMVER}") - G.Pkg = NewPackage(".") - G.Pkg.PlistFiles["bin/echo"] = true - G.Pkg.PlistFiles["bin/vim"] = true - G.Pkg.PlistFiles["sbin/sendmail.exim${EXIMVER}"] = true - - CheckfileAlternatives("ALTERNATIVES", G.Pkg.PlistFiles) + G.CheckDirent(".") + // TODO: Remove redundant diagnostics. t.CheckOutputLines( + "NOTE: ALTERNATIVES:1: @PREFIX@/ can be omitted from the file name.", + "NOTE: ALTERNATIVES:2: @PREFIX@/ can be omitted from the file name.", + "ERROR: ALTERNATIVES:5: Invalid ALTERNATIVES line \"invalid\".", "ERROR: ALTERNATIVES:1: Alternative implementation \"@PREFIX@/sbin/sendmail.postfix@POSTFIXVER@\" must appear in the PLIST as \"sbin/sendmail.postfix${POSTFIXVER}\".", "NOTE: ALTERNATIVES:1: @PREFIX@/ can be omitted from the file name.", "NOTE: ALTERNATIVES:2: @PREFIX@/ can be omitted from the file name.", @@ -33,7 +38,7 @@ func (s *Suite) Test_CheckfileAlternativ t := s.Init(c) t.Chdir("category/package") - t.SetupFileLines("ALTERNATIVES") + t.CreateFileLines("ALTERNATIVES") G.Pkg = NewPackage(".") Index: pkgsrc/pkgtools/pkglint/files/logging_test.go diff -u pkgsrc/pkgtools/pkglint/files/logging_test.go:1.4 pkgsrc/pkgtools/pkglint/files/logging_test.go:1.5 --- pkgsrc/pkgtools/pkglint/files/logging_test.go:1.4 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/logging_test.go Wed Oct 3 22:27:53 2018 @@ -15,7 +15,7 @@ import "gopkg.in/check.v1" // To keep the output layout consistent between all these // modes, the source code is written below the diagnostic // also in the default (check-only) mode. -func (s *Suite) Test_show_source_separator(c *check.C) { +func (s *Suite) Test__show_source_separator(c *check.C) { t := s.Init(c) t.SetupCommandLine("--source") @@ -47,7 +47,7 @@ func (s *Suite) Test_show_source_separat ">\tThe third line") } -func (s *Suite) Test_show_source_separator_show_autofix(c *check.C) { +func (s *Suite) Test__show_source_separator_show_autofix(c *check.C) { t := s.Init(c) t.SetupCommandLine("--source", "--show-autofix") @@ -80,7 +80,7 @@ func (s *Suite) Test_show_source_separat "+\tThe bronze medal line") } -func (s *Suite) Test_show_source_separator_autofix(c *check.C) { +func (s *Suite) Test__show_source_separator_autofix(c *check.C) { t := s.Init(c) t.SetupCommandLine("--source", "--autofix") @@ -114,7 +114,7 @@ func (s *Suite) Test_show_source_separat // Demonstrates how to filter log messages. // This is useful in combination with the --autofix option, // to restrict the fixes to exactly one group or topic. -func (s *Suite) Test_Line_log_only(c *check.C) { +func (s *Suite) Test_Line_log__only(c *check.C) { t := s.Init(c) t.SetupCommandLine("--autofix", "--source", "--only", "interesting") @@ -136,7 +136,7 @@ func (s *Suite) Test_Line_log_only(c *ch "+\tThe new2 song") } -func (s *Suite) Test_collect_explanations_with_only(c *check.C) { +func (s *Suite) Test_Pkglint_PrintSummary__explanations_with_only(c *check.C) { t := s.Init(c) t.SetupCommandLine("--only", "interesting") @@ -161,7 +161,7 @@ func (s *Suite) Test_collect_explanation "(Run \"pkglint -e\" to show explanations.)") } -func (s *Suite) Test_explain_with_only(c *check.C) { +func (s *Suite) Test_Explain__only(c *check.C) { t := s.Init(c) t.SetupCommandLine("--only", "interesting", "--explain") @@ -232,10 +232,20 @@ func (s *Suite) Test_Explain__long_lines t := s.Init(c) Explain( - "123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789 ") + "123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789") t.CheckOutputLines( - "Long explanation line: 123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789 ", + "Long explanation line: 123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789", "Break after: 123456789 12345678. abcdefghi. 123456789 123456789 123456789", - "Short space after period: 123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789 ") + "Short space after period: 123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789") +} + +func (s *Suite) Test_Explain__trailing_whitespace(c *check.C) { + t := s.Init(c) + + Explain( + "This is a space: ") + + t.CheckOutputLines( + "Trailing whitespace: \"This is a space: \"") } Index: pkgsrc/pkgtools/pkglint/files/mkshwalker.go diff -u pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.4 pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.5 --- pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.4 Sun Aug 12 16:31:56 2018 +++ pkgsrc/pkgtools/pkglint/files/mkshwalker.go Wed Oct 3 22:27:53 2018 @@ -1,211 +1,323 @@ package main +import ( + "fmt" + "reflect" + "strings" +) + type MkShWalker struct { + Callback struct { + List func(list *MkShList) + AndOr func(andor *MkShAndOr) + Pipeline func(pipeline *MkShPipeline) + Command func(command *MkShCommand) + SimpleCommand func(command *MkShSimpleCommand) + CompoundCommand func(command *MkShCompoundCommand) + Case func(caseClause *MkShCaseClause) + CaseItem func(caseItem *MkShCaseItem) + FunctionDefinition func(funcdef *MkShFunctionDefinition) + If func(ifClause *MkShIfClause) + Loop func(loop *MkShLoopClause) + Words func(words []*ShToken) + Word func(word *ShToken) + Redirects func(redirects []*MkShRedirection) + Redirect func(redirect *MkShRedirection) + For func(forClause *MkShForClause) + Varname func(varname string) + } + Context []MkShWalkerPathElement +} + +type MkShWalkerPathElement struct { + Index int + Element interface{} } func NewMkShWalker() *MkShWalker { return &MkShWalker{} } +// Path returns a representation of the path in the AST that is +// currently visited. +func (w *MkShWalker) Path() string { + var path []string + for _, level := range w.Context { + typeName := reflect.TypeOf(level.Element).Elem().Name() + abbreviated := strings.Replace(typeName, "MkSh", "", 1) + if level.Index == -1 { + path = append(path, abbreviated) + } else { + path = append(path, fmt.Sprintf("%s[%d]", abbreviated, level.Index)) + } + } + return strings.Join(path, ".") +} + // Walk calls the given callback for each node of the parsed shell program, // in visiting order from large to small. -func (w *MkShWalker) Walk(list *MkShList, callback *MkShWalkCallback) { - w.walkList(list, callback) +func (w *MkShWalker) Walk(list *MkShList) { + w.walkList(-1, list) + + G.Assertf(len(w.Context) == 0, "MkShWalker.Walk %v", w.Context) } -func (w *MkShWalker) walkList(list *MkShList, callback *MkShWalkCallback) { - if callback.List != nil { - callback.List(list) +func (w *MkShWalker) walkList(index int, list *MkShList) { + w.push(index, list) + + if callback := w.Callback.List; callback != nil { + callback(list) } - for _, andor := range list.AndOrs { - w.walkAndOr(andor, callback) + for i, andor := range list.AndOrs { + w.walkAndOr(i, andor) } + + w.pop() } -func (w *MkShWalker) walkAndOr(andor *MkShAndOr, callback *MkShWalkCallback) { - if callback.AndOr != nil { - callback.AndOr(andor) +func (w *MkShWalker) walkAndOr(index int, andor *MkShAndOr) { + w.push(index, andor) + + if callback := w.Callback.AndOr; callback != nil { + callback(andor) } - for _, pipeline := range andor.Pipes { - w.walkPipeline(pipeline, callback) + for i, pipeline := range andor.Pipes { + w.walkPipeline(i, pipeline) } + + w.pop() } -func (w *MkShWalker) walkPipeline(pipeline *MkShPipeline, callback *MkShWalkCallback) { - if callback.Pipeline != nil { - callback.Pipeline(pipeline) +func (w *MkShWalker) walkPipeline(index int, pipeline *MkShPipeline) { + w.push(index, pipeline) + + if callback := w.Callback.Pipeline; callback != nil { + callback(pipeline) } - for _, command := range pipeline.Cmds { - w.walkCommand(command, callback) + for i, command := range pipeline.Cmds { + w.walkCommand(i, command) } + + w.pop() } -func (w *MkShWalker) walkCommand(command *MkShCommand, callback *MkShWalkCallback) { - if callback.Command != nil { - callback.Command(command) +func (w *MkShWalker) walkCommand(index int, command *MkShCommand) { + w.push(index, command) + + if callback := w.Callback.Command; callback != nil { + callback(command) } switch { case command.Simple != nil: - w.walkSimpleCommand(command.Simple, callback) + w.walkSimpleCommand(-1, command.Simple) case command.Compound != nil: - w.walkCompoundCommand(command.Compound, callback) - w.walkRedirects(command.Redirects, callback) + w.walkCompoundCommand(-1, command.Compound) + w.walkRedirects(-1, command.Redirects) case command.FuncDef != nil: - w.walkFunctionDefinition(command.FuncDef, callback) - w.walkRedirects(command.Redirects, callback) + w.walkFunctionDefinition(-1, command.FuncDef) + w.walkRedirects(-1, command.Redirects) } + + w.pop() } -func (w *MkShWalker) walkSimpleCommand(command *MkShSimpleCommand, callback *MkShWalkCallback) { - if callback.SimpleCommand != nil { - callback.SimpleCommand(command) +func (w *MkShWalker) walkSimpleCommand(index int, command *MkShSimpleCommand) { + w.push(index, command) + + if callback := w.Callback.SimpleCommand; callback != nil { + callback(command) } - w.walkWords(command.Assignments, callback) + w.walkWords(0, command.Assignments) if command.Name != nil { - w.walkWord(command.Name, callback) + w.walkWord(-1, command.Name) } - w.walkWords(command.Args, callback) - w.walkRedirects(command.Redirections, callback) + w.walkWords(2, command.Args) + w.walkRedirects(-1, command.Redirections) + + w.pop() } -func (w *MkShWalker) walkCompoundCommand(command *MkShCompoundCommand, callback *MkShWalkCallback) { - if callback.CompoundCommand != nil { - callback.CompoundCommand(command) +func (w *MkShWalker) walkCompoundCommand(index int, command *MkShCompoundCommand) { + w.push(index, command) + + if callback := w.Callback.CompoundCommand; callback != nil { + callback(command) } switch { case command.Brace != nil: - w.walkList(command.Brace, callback) + w.walkList(-1, command.Brace) case command.Case != nil: - w.walkCase(command.Case, callback) + w.walkCase(command.Case) case command.For != nil: - w.walkFor(command.For, callback) + w.walkFor(command.For) case command.If != nil: - w.walkIf(command.If, callback) + w.walkIf(command.If) case command.Loop != nil: - w.walkLoop(command.Loop, callback) + w.walkLoop(command.Loop) case command.Subshell != nil: - w.walkList(command.Subshell, callback) + w.walkList(-1, command.Subshell) } + + w.pop() } -func (w *MkShWalker) walkCase(caseClause *MkShCaseClause, callback *MkShWalkCallback) { - if callback.Case != nil { - callback.Case(caseClause) +func (w *MkShWalker) walkCase(caseClause *MkShCaseClause) { + w.push(-1, caseClause) + + if callback := w.Callback.Case; callback != nil { + callback(caseClause) } - w.walkWord(caseClause.Word, callback) - for _, caseItem := range caseClause.Cases { - if callback.CaseItem != nil { - callback.CaseItem(caseItem) + w.walkWord(0, caseClause.Word) + for i, caseItem := range caseClause.Cases { + w.push(i, caseItem) + if callback := w.Callback.CaseItem; callback != nil { + callback(caseItem) } - w.walkWords(caseItem.Patterns, callback) - w.walkList(caseItem.Action, callback) + w.walkWords(0, caseItem.Patterns) + w.walkList(1, caseItem.Action) + w.pop() } + + w.pop() } -func (w *MkShWalker) walkFunctionDefinition(funcdef *MkShFunctionDefinition, callback *MkShWalkCallback) { - if callback.FunctionDefinition != nil { - callback.FunctionDefinition(funcdef) +func (w *MkShWalker) walkFunctionDefinition(index int, funcdef *MkShFunctionDefinition) { + w.push(index, funcdef) + + if callback := w.Callback.FunctionDefinition; callback != nil { + callback(funcdef) } - w.walkCompoundCommand(funcdef.Body, callback) + w.walkCompoundCommand(-1, funcdef.Body) + + w.pop() } -func (w *MkShWalker) walkIf(ifClause *MkShIfClause, callback *MkShWalkCallback) { - if callback.If != nil { - callback.If(ifClause) +func (w *MkShWalker) walkIf(ifClause *MkShIfClause) { + w.push(-1, ifClause) + + if callback := w.Callback.If; callback != nil { + callback(ifClause) } for i, cond := range ifClause.Conds { - w.walkList(cond, callback) - w.walkList(ifClause.Actions[i], callback) + w.walkList(2*i, cond) + w.walkList(2*i+1, ifClause.Actions[i]) } if ifClause.Else != nil { - w.walkList(ifClause.Else, callback) + w.walkList(2*len(ifClause.Conds), ifClause.Else) } + + w.pop() } -func (w *MkShWalker) walkLoop(loop *MkShLoopClause, callback *MkShWalkCallback) { - if callback.Loop != nil { - callback.Loop(loop) +func (w *MkShWalker) walkLoop(loop *MkShLoopClause) { + w.push(-1, loop) + + if callback := w.Callback.Loop; callback != nil { + callback(loop) } - w.walkList(loop.Cond, callback) - w.walkList(loop.Action, callback) + w.walkList(0, loop.Cond) + w.walkList(1, loop.Action) + + w.pop() } -func (w *MkShWalker) walkWords(words []*ShToken, callback *MkShWalkCallback) { - if len(words) != 0 { - if callback.Words != nil { - callback.Words(words) - } +func (w *MkShWalker) walkWords(index int, words []*ShToken) { + if len(words) == 0 { + return + } - for _, word := range words { - w.walkWord(word, callback) - } + w.push(index, words) + + if callback := w.Callback.Words; callback != nil { + callback(words) } + + for i, word := range words { + w.walkWord(i, word) + } + + w.pop() } -func (w *MkShWalker) walkWord(word *ShToken, callback *MkShWalkCallback) { - if callback.Word != nil { - callback.Word(word) +func (w *MkShWalker) walkWord(index int, word *ShToken) { + w.push(index, word) + + if callback := w.Callback.Word; callback != nil { + callback(word) } + + w.pop() } -func (w *MkShWalker) walkRedirects(redirects []*MkShRedirection, callback *MkShWalkCallback) { - if len(redirects) != 0 { - if callback.Redirects != nil { - callback.Redirects(redirects) - } +func (w *MkShWalker) walkRedirects(index int, redirects []*MkShRedirection) { + if len(redirects) == 0 { + return + } - for _, redirect := range redirects { - if callback.Redirect != nil { - callback.Redirect(redirect) - } + w.push(index, redirects) - w.walkWord(redirect.Target, callback) + if callback := w.Callback.Redirects; callback != nil { + callback(redirects) + } + + for i, redirect := range redirects { + if callback := w.Callback.Redirect; callback != nil { + callback(redirect) } + + w.walkWord(i, redirect.Target) + } + + w.pop() +} + +func (w *MkShWalker) walkFor(forClause *MkShForClause) { + w.push(-1, forClause) + + if callback := w.Callback.For; callback != nil { + callback(forClause) + } + if callback := w.Callback.Varname; callback != nil { + callback(forClause.Varname) + } + + w.walkWords(-1, forClause.Values) + w.walkList(-1, forClause.Body) + + w.pop() +} + +// Current provides access to the element that the walker is currently +// processing, especially its index as seen from its parent element. +func (w *MkShWalker) Current() MkShWalkerPathElement { + return w.Context[len(w.Context)-1] +} + +// Parent returns an ancestor element from the currently visited path. +// Parent(0) is the element that is currently visited, +// Parent(1) is its direct parent, and so on. +func (w *MkShWalker) Parent(steps int) interface{} { + index := len(w.Context) - 1 - steps + if index >= 0 { + return w.Context[index].Element } + return nil } -func (w *MkShWalker) walkFor(forClause *MkShForClause, callback *MkShWalkCallback) { - if callback.For != nil { - callback.For(forClause) - } - if callback.Varname != nil { - callback.Varname(forClause.Varname) - } - - w.walkWords(forClause.Values, callback) - w.walkList(forClause.Body, callback) -} - -type MkShWalkCallback struct { - List func(list *MkShList) - AndOr func(andor *MkShAndOr) - Pipeline func(pipeline *MkShPipeline) - Command func(command *MkShCommand) - SimpleCommand func(command *MkShSimpleCommand) - CompoundCommand func(command *MkShCompoundCommand) - Case func(caseClause *MkShCaseClause) - CaseItem func(caseItem *MkShCaseItem) - FunctionDefinition func(funcdef *MkShFunctionDefinition) - If func(ifClause *MkShIfClause) - Loop func(loop *MkShLoopClause) - Words func(words []*ShToken) - Word func(word *ShToken) - Redirects func(redirects []*MkShRedirection) - Redirect func(redirect *MkShRedirection) - For func(forClause *MkShForClause) - Varname func(varname string) +func (w *MkShWalker) push(index int, element interface{}) { + w.Context = append(w.Context, MkShWalkerPathElement{index, element}) } -func NewMkShWalkCallback() *MkShWalkCallback { - return &MkShWalkCallback{} +func (w *MkShWalker) pop() { + w.Context = w.Context[:len(w.Context)-1] } Index: pkgsrc/pkgtools/pkglint/files/options.go diff -u pkgsrc/pkgtools/pkglint/files/options.go:1.4 pkgsrc/pkgtools/pkglint/files/options.go:1.5 --- pkgsrc/pkgtools/pkglint/files/options.go:1.4 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/options.go Wed Oct 3 22:27:53 2018 @@ -16,7 +16,7 @@ func ChecklinesOptionsMk(mklines *MkLine exp.CurrentLine().Warnf("Expected definition of PKG_OPTIONS_VAR.") Explain( "The input variables in an options.mk file should always be", - "mentioned in the same order: PKG_OPTIONS_VAR, ", + "mentioned in the same order: PKG_OPTIONS_VAR,", "PKG_SUPPORTED_OPTIONS, PKG_SUGGESTED_OPTIONS. This way, the", "options.mk files have the same structure and are easy to understand.") return @@ -49,14 +49,9 @@ loop: // The conditionals are typically for OPSYS and MACHINE_ARCH. case mkline.IsInclude(): - includedFile := mkline.IncludeFile() - switch { - case matches(includedFile, `/[^/]+\.buildlink3\.mk$`): - case matches(includedFile, `/[^/]+\.builtin\.mk$`): - case includedFile == "../../mk/bsd.options.mk": + if mkline.IncludeFile() == "../../mk/bsd.options.mk" { exp.Advance() break loop - case IsPrefs(includedFile): } default: Index: pkgsrc/pkgtools/pkglint/files/options_test.go diff -u pkgsrc/pkgtools/pkglint/files/options_test.go:1.4 pkgsrc/pkgtools/pkglint/files/options_test.go:1.5 --- pkgsrc/pkgtools/pkglint/files/options_test.go:1.4 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/options_test.go Wed Oct 3 22:27:53 2018 @@ -15,7 +15,7 @@ func (s *Suite) Test_ChecklinesOptionsMk t.SetupOption("sqlite", "") t.SetupOption("x11", "") - t.SetupFileMkLines("mk/bsd.options.mk", + t.CreateFileLines("mk/bsd.options.mk", MkRcsID) mklines := t.SetupFileMkLines("category/package/options.mk", @@ -70,7 +70,7 @@ func (s *Suite) Test_ChecklinesOptionsMk t.SetupOption("slang", "") t.SetupOption("x11", "") - t.SetupFileMkLines("mk/bsd.options.mk", + t.CreateFileLines("mk/bsd.options.mk", MkRcsID) mklines := t.SetupFileMkLines("category/package/options.mk", @@ -99,7 +99,7 @@ func (s *Suite) Test_ChecklinesOptionsMk t.SetupOption("slang", "") t.SetupOption("x11", "") - t.SetupFileMkLines("mk/bsd.options.mk", + t.CreateFileLines("mk/bsd.options.mk", MkRcsID) mklines := t.SetupFileMkLines("category/package/options.mk", @@ -109,6 +109,10 @@ func (s *Suite) Test_ChecklinesOptionsMk "PKG_SUPPORTED_OPTIONS= # none", "PKG_SUGGESTED_OPTIONS= # none", "", + "# Comment", + ".if ${OPSYS} == NetBSD", + ".endif", + "", ".include \"../../mk/bsd.options.mk\"", "", ".if ${OPSYS} == 'Darwin'", @@ -117,5 +121,5 @@ func (s *Suite) Test_ChecklinesOptionsMk ChecklinesOptionsMk(mklines) t.CheckOutputLines( - "WARN: ~/category/package/options.mk:9: Invalid condition, unrecognized part: \"${OPSYS} == 'Darwin'\".") + "WARN: ~/category/package/options.mk:13: Invalid condition, unrecognized part: \"${OPSYS} == 'Darwin'\".") } Index: pkgsrc/pkgtools/pkglint/files/tools.go diff -u pkgsrc/pkgtools/pkglint/files/tools.go:1.4 pkgsrc/pkgtools/pkglint/files/tools.go:1.5 --- pkgsrc/pkgtools/pkglint/files/tools.go:1.4 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/tools.go Wed Oct 3 22:27:53 2018 @@ -279,11 +279,30 @@ func (tr *Tools) Usable(tool *Tool, time } func (tr *Tools) AddAll(other Tools) { - if trace.Tracing { - defer trace.Call(other.TraceName, "to", tr.TraceName)() + if trace.Tracing && len(other.byName) != 0 { + defer trace.Call(other.TraceName+" to "+tr.TraceName, len(other.byName))() } - for _, otherTool := range other.byName { + // Same as the code below, just a little faster. + if !trace.Tracing { + for _, otherTool := range other.byName { + tool := tr.def(otherTool.Name, otherTool.Varname, nil) + tool.MustUseVarForm = tool.MustUseVarForm || otherTool.MustUseVarForm + if otherTool.Validity > tool.Validity { + tool.SetValidity(otherTool.Validity, tr.TraceName) + } + } + return + } + + var names []string + for name := range other.byName { + names = append(names, name) + } + sort.Strings(names) + + for _, name := range names { + otherTool := other.byName[name] if trace.Tracing { trace.Stepf("Tools.AddAll %+v", *otherTool) } Index: pkgsrc/pkgtools/pkglint/files/tools_test.go diff -u pkgsrc/pkgtools/pkglint/files/tools_test.go:1.4 pkgsrc/pkgtools/pkglint/files/tools_test.go:1.5 --- pkgsrc/pkgtools/pkglint/files/tools_test.go:1.4 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/tools_test.go Wed Oct 3 22:27:53 2018 @@ -7,7 +7,7 @@ func (s *Suite) Test_Tools_ParseToolLine t.SetupToolUsable("tool1", "") t.SetupVartypes() - t.SetupFileLines("Makefile", + t.CreateFileLines("Makefile", MkRcsID, "", "USE_TOOLS.NetBSD+=\ttool1") @@ -18,7 +18,7 @@ func (s *Suite) Test_Tools_ParseToolLine t.CheckOutputEmpty() } -func (s *Suite) Test_Tools_validateToolName__invalid(c *check.C) { +func (s *Suite) Test_Tools_def__invalid_tool_name(c *check.C) { t := s.Init(c) reg := NewTools("") @@ -54,7 +54,7 @@ func (s *Suite) Test_Tools__USE_TOOLS_pr "USE_TOOLS+=\tsed:pkgsrc") t.CreateFileLines("mk/tools/defaults.mk", "_TOOLS_VARNAME.sed=\tSED") - t.SetupFileMkLines("module.mk", + t.CreateFileLines("module.mk", MkRcsID, "", "do-build:", Index: pkgsrc/pkgtools/pkglint/files/autofix.go diff -u pkgsrc/pkgtools/pkglint/files/autofix.go:1.9 pkgsrc/pkgtools/pkglint/files/autofix.go:1.10 --- pkgsrc/pkgtools/pkglint/files/autofix.go:1.9 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/autofix.go Wed Oct 3 22:27:53 2018 @@ -46,8 +46,9 @@ func NewAutofix(line Line) *Autofix { // If printAutofix or autofix is true, the fix should be done in // memory as far as possible (e.g. changes to the text of the line). // -// If autofix is true, the fix should be done permanently -// (e.g. direct changes to the file system). +// If autofix is true, the fix should be done persistently +// (e.g. direct changes to the file system). Except if the fix only +// affects the current line, then SaveAutofixChanges will do that. func (fix *Autofix) Custom(fixer func(printAutofix, autofix bool)) { if fix.skip() { return @@ -104,7 +105,7 @@ func (fix *Autofix) ReplaceRegex(from re return toText } - if replaced := regex.Compile(from).ReplaceAllStringFunc(rawLine.textnl, replace); replaced != rawLine.textnl { + if replaced := replaceAllFunc(rawLine.textnl, from, replace); replaced != rawLine.textnl { if G.opts.PrintAutofix || G.opts.Autofix { rawLine.textnl = replaced } @@ -134,12 +135,12 @@ func (fix *Autofix) Realign(mkline MkLin } for _, rawLine := range fix.lines[1:] { - _, comment, space := regex.Match2(rawLine.textnl, `^(#?)([ \t]*)`) + _, comment, space := match2(rawLine.textnl, `^(#?)([ \t]*)`) width := tabWidth(comment + space) if (oldWidth == 0 || width < oldWidth) && width >= 8 && rawLine.textnl != "\n" { oldWidth = width } - if !regex.Matches(space, `^\t* {0,7}$`) { + if !matches(space, `^\t* {0,7}$`) { normalized = false } } @@ -156,7 +157,7 @@ func (fix *Autofix) Realign(mkline MkLin } for _, rawLine := range fix.lines[1:] { - _, comment, oldSpace := regex.Match2(rawLine.textnl, `^(#?)([ \t]*)`) + _, comment, oldSpace := match2(rawLine.textnl, `^(#?)([ \t]*)`) newWidth := tabWidth(oldSpace) - oldWidth + newWidth newSpace := strings.Repeat("\t", newWidth/8) + strings.Repeat(" ", newWidth%8) replaced := strings.Replace(rawLine.textnl, comment+oldSpace, comment+newSpace, 1) @@ -311,6 +312,7 @@ func SaveAutofixChanges(lines []Line) (a for _, line := range lines { if line.autofix != nil && line.autofix.modified { G.autofixAvailable = true + G.fileCache.Evict(line.Filename) } } return @@ -338,6 +340,7 @@ func SaveAutofixChanges(lines []Line) (a } for fname := range changed { + G.fileCache.Evict(fname) changedLines := changes[fname] tmpname := fname + ".pkglint.tmp" text := "" Index: pkgsrc/pkgtools/pkglint/files/parser.go diff -u pkgsrc/pkgtools/pkglint/files/parser.go:1.9 pkgsrc/pkgtools/pkglint/files/parser.go:1.10 --- pkgsrc/pkgtools/pkglint/files/parser.go:1.9 Mon Jan 1 18:04:15 2018 +++ pkgsrc/pkgtools/pkglint/files/parser.go Wed Oct 3 22:27:53 2018 @@ -12,7 +12,7 @@ type Parser struct { } func NewParser(line Line, s string, emitWarnings bool) *Parser { - return &Parser{line, textproc.NewPrefixReplacer(s), emitWarnings} + return &Parser{line, G.NewPrefixReplacer(s), emitWarnings} } func (p *Parser) EOF() bool { Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.9 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.10 --- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.9 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go Wed Oct 3 22:27:53 2018 @@ -5,6 +5,7 @@ import ( "netbsd.org/pkglint/regex" "netbsd.org/pkglint/trace" "sort" + "strconv" "strings" ) @@ -138,37 +139,64 @@ func (src *Pkgsrc) LoadInfrastructure() // Example: // Latest("lang", `^php[0-9]+$`, "../../lang/$0") => "../../lang/php72" func (src *Pkgsrc) Latest(category string, re regex.Pattern, repl string) string { - key := category + "/" + string(re) + " => " + repl - if latest, found := src.latest[key]; found { + if G.Testing { + G.Assertf( + hasPrefix(string(re), "^") && hasSuffix(string(re), "$"), + "Regular expression %q must be anchored at both ends.", re) + } + + cacheKey := category + "/" + string(re) + " => " + repl + if latest, found := src.latest[cacheKey]; found { return latest } categoryDir := src.File(category) error := func() string { dummyLine.Errorf("Cannot find latest version of %q in %q.", re, categoryDir) - src.latest[key] = "" + src.latest[cacheKey] = "" return "" } - all, err := ioutil.ReadDir(categoryDir) - sort.SliceStable(all, func(i, j int) bool { - return naturalLess(all[i].Name(), all[j].Name()) - }) + fileInfos, err := ioutil.ReadDir(categoryDir) if err != nil { return error() } - latest := "" - for _, fileInfo := range all { - if matches(fileInfo.Name(), re) { - latest = regex.Compile(re).ReplaceAllString(fileInfo.Name(), repl) + var names []string + for _, fileInfo := range fileInfos { + name := fileInfo.Name() + if matches(name, re) { + names = append(names, name) + } + } + + keys := make(map[string]int) + for _, name := range names { + if m, pkgbase, versionStr := match2(name, `^(\D+)(\d+)$`); m { + version, _ := strconv.Atoi(versionStr) + if pkgbase == "postgresql" && version < 60 { + version = 10 * version + } + keys[name] = version } } + + sort.SliceStable(names, func(i, j int) bool { + if keyI, keyJ := keys[names[i]], keys[names[j]]; keyI != 0 && keyJ != 0 { + return keyI < keyJ + } + return naturalLess(names[i], names[j]) + }) + + latest := "" + for _, name := range names { + latest = replaceAll(name, re, repl) + } if latest == "" { return error() } - src.latest[key] = latest + src.latest[cacheKey] = latest return latest } @@ -346,7 +374,7 @@ func (src *Pkgsrc) loadDocChangesFromFil } } } else if text := line.Text; len(text) >= 2 && text[0] == '\t' && 'A' <= text[1] && text[1] <= 'Z' { - line.Warnf("Unknown doc/CHANGES line: %q", text) + line.Warnf("Unknown doc/CHANGES line: %s", text) Explain("See mk/misc/developer.mk for the rules.") } } @@ -549,6 +577,9 @@ func (src *Pkgsrc) initDeprecatedVars() "SVR4_PKGNAME": "Just remove it.", "PKG_INSTALLATION_TYPES": "Just remove it.", + // November 2015, commit abccb56 + "EVAL_PREFIX": "All packages are installed in PREFIX now.", + // January 2016 "SUBST_POSTCMD.*": "Has been removed, as it seemed unused.", @@ -596,7 +627,7 @@ func (src *Pkgsrc) IsBuildDef(varname st func (src *Pkgsrc) loadMasterSites() { mklines := src.LoadMk("mk/fetch/sites.mk", MustSucceed|NotEmpty) - nameToUrl := src.MasterSiteVarToURL + nameToURL := src.MasterSiteVarToURL urlToName := src.MasterSiteURLToVar for _, mkline := range mklines.mklines { if mkline.IsVarassign() { @@ -604,8 +635,8 @@ func (src *Pkgsrc) loadMasterSites() { if hasPrefix(varname, "MASTER_SITE_") && varname != "MASTER_SITE_BACKUP" { for _, url := range splitOnSpace(mkline.Value()) { if matches(url, `^(?:http://|https://|ftp://)`) { - if nameToUrl[varname] == "" { - nameToUrl[varname] = url + if nameToURL[varname] == "" { + nameToURL[varname] = url } urlToName[url] = varname } @@ -615,7 +646,7 @@ func (src *Pkgsrc) loadMasterSites() { } // Explicitly allowed, although not defined in mk/fetch/sites.mk. - nameToUrl["MASTER_SITE_LOCAL"] = "ftp://ftp.NetBSD.org/pub/pkgsrc/distfiles/LOCAL_PORTS/" + nameToURL["MASTER_SITE_LOCAL"] = "ftp://ftp.NetBSD.org/pub/pkgsrc/distfiles/LOCAL_PORTS/" if trace.Tracing { trace.Stepf("Loaded %d MASTER_SITE_* URLs.", len(urlToName)) @@ -629,7 +660,7 @@ func (src *Pkgsrc) loadPkgOptions() { if m, optname, optdescr := match2(line.Text, `^([-0-9a-z_+]+)(?:\s+(.*))?$`); m { src.PkgOptions[optname] = optdescr } else { - line.Fatalf("Unknown line format.") + line.Fatalf("Unknown line format: %s", line.Text) } } } Index: pkgsrc/pkgtools/pkglint/files/shtypes.go diff -u pkgsrc/pkgtools/pkglint/files/shtypes.go:1.9 pkgsrc/pkgtools/pkglint/files/shtypes.go:1.10 --- pkgsrc/pkgtools/pkglint/files/shtypes.go:1.9 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/shtypes.go Wed Oct 3 22:27:53 2018 @@ -40,15 +40,15 @@ type ShAtom struct { Type ShAtomType MkText string Quoting ShQuoting // The quoting state at the end of the token - Data interface{} + data interface{} } func (atom *ShAtom) String() string { - if atom.Type == shtWord && atom.Quoting == shqPlain && atom.Data == nil { + if atom.Type == shtWord && atom.Quoting == shqPlain && atom.data == nil { return fmt.Sprintf("%q", atom.MkText) } if atom.Type == shtVaruse { - varuse := atom.Data.(*MkVarUse) + varuse := atom.VarUse() return fmt.Sprintf("varuse(%q)", varuse.varname+varuse.Mod()) } return fmt.Sprintf("ShAtom(%v, %q, %s)", atom.Type, atom.MkText, atom.Quoting) @@ -57,7 +57,7 @@ func (atom *ShAtom) String() string { // VarUse returns a read access to a Makefile variable, or nil for plain shell tokens. func (atom *ShAtom) VarUse() *MkVarUse { if atom.Type == shtVaruse { - return atom.Data.(*MkVarUse) + return atom.data.(*MkVarUse) } return nil } @@ -67,24 +67,25 @@ func (atom *ShAtom) VarUse() *MkVarUse { type ShQuoting uint8 const ( - shqPlain ShQuoting = iota - shqDquot - shqSquot - shqBackt - shqSubsh - shqDquotBackt - shqBacktDquot - shqBacktSquot - shqSubshSquot - shqDquotBacktDquot - shqDquotBacktSquot + shqPlain ShQuoting = iota // e.g. word + shqDquot // e.g. "word" + shqSquot // e.g. 'word' + shqBackt // e.g. `word` + shqSubsh // e.g. $(word) + shqDquotBackt // e.g. "`word`" + shqBacktDquot // e.g. `"word"` + shqBacktSquot // e.g. `'word'` + shqSubshDquot // e.g. $("word") + shqSubshSquot // e.g. $('word') + shqDquotBacktDquot // e.g. "`"word"`" + shqDquotBacktSquot // e.g. "`'word'`" ) func (q ShQuoting) String() string { return [...]string{ "plain", "d", "s", "b", "S", - "db", "bd", "bs", "Ss", + "db", "bd", "bs", "Sd", "Ss", "dbd", "dbs", }[q] } @@ -103,6 +104,13 @@ func (q ShQuoting) ToVarUseContext() vuc return vucQuotUnknown } +// ShToken is an operator or a keyword or some text intermingled with variables. +// +// Examples: +// ; +// then +// "The number of pkgsrc packages in ${PREFIX} is $$packages." +// // See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_10_02 type ShToken struct { MkText string // The text as it appeared in the Makefile, after replacing `\#` with `#` Index: pkgsrc/pkgtools/pkglint/files/autofix_test.go diff -u pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.10 pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.11 --- pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.10 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/autofix_test.go Wed Oct 3 22:27:53 2018 @@ -7,7 +7,7 @@ import ( "strings" ) -func (s *Suite) Test_Autofix_ReplaceRegex(c *check.C) { +func (s *Suite) Test_Autofix_ReplaceRegex__show_autofix(c *check.C) { t := s.Init(c) t.SetupCommandLine("--show-autofix") @@ -36,7 +36,7 @@ func (s *Suite) Test_Autofix_ReplaceRege "AUTOFIX: ~/Makefile:2: Replacing \"2\" with \"X\".") } -func (s *Suite) Test_Autofix_ReplaceRegex_with_autofix(c *check.C) { +func (s *Suite) Test_Autofix_ReplaceRegex__autofix(c *check.C) { t := s.Init(c) t.SetupCommandLine("--autofix", "--source") @@ -75,7 +75,7 @@ func (s *Suite) Test_Autofix_ReplaceRege "line3") } -func (s *Suite) Test_Autofix_ReplaceRegex_with_show_autofix(c *check.C) { +func (s *Suite) Test_Autofix_ReplaceRegex__show_autofix_and_source(c *check.C) { t := s.Init(c) t.SetupCommandLine("--show-autofix", "--source") @@ -111,11 +111,11 @@ func (s *Suite) Test_Autofix_ReplaceRege "+\tYXXXX") } -func (s *Suite) Test_autofix_MkLines(c *check.C) { +func (s *Suite) Test_SaveAutofixChanges(c *check.C) { t := s.Init(c) t.SetupCommandLine("--autofix") - t.SetupFileLines("category/basename/Makefile", + t.CreateFileLines("category/basename/Makefile", "line1 := value1", "line2 := value2", "line3 := value3") @@ -149,6 +149,27 @@ func (s *Suite) Test_autofix_MkLines(c * "XXXe3 := value3") } +func (s *Suite) Test_SaveAutofixChanges__no_changes_necessary(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("--autofix") + lines := t.SetupFileLines("DESCR", + "Line 1", + "Line 2") + + fix := lines[0].Autofix() + fix.Warnf("Dummy warning.") + fix.Replace("X", "Y") + fix.Apply() + + // Since nothing has been effectively changed, + // nothing needs to be saved. + SaveAutofixChanges(lines) + + // And therefore, no AUTOFIX action must appear in the log. + t.CheckOutputEmpty() +} + func (s *Suite) Test_Autofix__multiple_modifications(c *check.C) { t := s.Init(c) @@ -255,7 +276,7 @@ func (s *Suite) Test_Autofix__multiple_m "AUTOFIX: fname:1: Deleting this line.") } -func (s *Suite) Test_Autofix_show_source_code(c *check.C) { +func (s *Suite) Test_Autofix__show_autofix_and_source(c *check.C) { t := s.Init(c) t.SetupCommandLine("--show-autofix", "--source") @@ -319,7 +340,7 @@ func (s *Suite) Test_Autofix_Delete(c *c // Demonstrates that the --show-autofix option only shows those diagnostics // that would be fixed. -func (s *Suite) Test_Autofix_suppress_unfixable_warnings(c *check.C) { +func (s *Suite) Test_Autofix__suppress_unfixable_warnings(c *check.C) { t := s.Init(c) t.SetupCommandLine("--show-autofix", "--source") @@ -328,14 +349,14 @@ func (s *Suite) Test_Autofix_suppress_un "line2", "line3") - lines[0].Warnf("This warning is not shown since it is not automatically fixed.") + lines[0].Warnf("This warning is not shown since it is not part of a fix.") fix := lines[1].Autofix() fix.Warnf("Something's wrong here.") fix.ReplaceRegex(`.`, "X", -1) fix.Apply() - fix.Warnf("The XXX marks are usually not fixed, use TODO instead.") + fix.Warnf("Since XXX marks are usually not fixed, use TODO instead to draw attention.") fix.Replace("XXX", "TODO") fix.Apply() @@ -351,14 +372,14 @@ func (s *Suite) Test_Autofix_suppress_un "-\tline2", "+\tXXXXX", "", - "WARN: Makefile:2: The XXX marks are usually not fixed, use TODO instead.", + "WARN: Makefile:2: Since XXX marks are usually not fixed, use TODO instead to draw attention.", "AUTOFIX: Makefile:2: Replacing \"XXX\" with \"TODO\".", "-\tline2", "+\tTODOXX") } // If an Autofix doesn't do anything it must not log any diagnostics. -func (s *Suite) Test_Autofix_failed_replace(c *check.C) { +func (s *Suite) Test_Autofix__noop_replace(c *check.C) { t := s.Init(c) line := t.NewLine("Makefile", 14, "Original text") @@ -372,28 +393,9 @@ func (s *Suite) Test_Autofix_failed_repl t.CheckOutputEmpty() } -func (s *Suite) Test_SaveAutofixChanges(c *check.C) { - t := s.Init(c) - - t.SetupCommandLine("--autofix") - lines := t.SetupFileLines("DESCR", - "Line 1", - "Line 2") - - fix := lines[0].Autofix() - fix.Warnf("Dummy warning.") - fix.Replace("X", "Y") - fix.Apply() - - // Since nothing has been effectively changed, - // nothing needs to be saved. - SaveAutofixChanges(lines) - - // And therefore, no AUTOFIX action must appear in the log. - t.CheckOutputEmpty() -} - -func (s *Suite) Test_Autofix_CustomFix(c *check.C) { +// When using Autofix.CustomFix, it is tricky to get all the details right. +// For best results, see the existing examples and the documentation. +func (s *Suite) Test_Autofix_Custom(c *check.C) { t := s.Init(c) lines := t.NewLines("Makefile", Index: pkgsrc/pkgtools/pkglint/files/shtokenizer.go diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.10 pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.11 --- pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.10 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/shtokenizer.go Wed Oct 3 22:27:53 2018 @@ -12,7 +12,8 @@ func NewShTokenizer(line Line, text stri } // ShAtom parses a basic building block of a shell program. -// Examples for such atoms are: variable reference, operator, text, quote, space. +// Examples for such atoms are: variable reference (both make and shell), +// operator, text, quote, space. // // See ShQuote.Feed func (p *ShTokenizer) ShAtom(quoting ShQuoting) *ShAtom { @@ -45,6 +46,8 @@ func (p *ShTokenizer) ShAtom(quoting ShQ atom = p.shAtomBacktDquot() case shqBacktSquot: atom = p.shAtomBacktSquot() + case shqSubshDquot: + atom = p.shAtomSubshDquot() case shqSubshSquot: atom = p.shAtomSubshSquot() case shqDquotBacktDquot: @@ -82,7 +85,7 @@ func (p *ShTokenizer) shAtomPlain() *ShA case repl.AdvanceRegexp(`^#.*`): return &ShAtom{shtComment, repl.Group(0), q, nil} case repl.AdvanceStr("$$("): - return &ShAtom{shtSubshell, repl.Str(), q, nil} + return &ShAtom{shtSubshell, repl.Str(), shqSubsh, nil} } return p.shAtomInternal(q, false, false) @@ -133,31 +136,24 @@ func (p *ShTokenizer) shAtomBackt() *ShA // compatibility with /bin/sh from Solaris 7. func (p *ShTokenizer) shAtomSubsh() *ShAtom { const q = shqSubsh - if op := p.shOperator(q); op != nil { - return op - } repl := p.parser.repl - mark := repl.Mark() - atom := func(typ ShAtomType) *ShAtom { - return &ShAtom{typ, repl.Since(mark), shqSubsh, nil} - } switch { case repl.AdvanceHspace(): - return atom(shtSpace) + return &ShAtom{shtSpace, repl.Str(), q, nil} case repl.AdvanceStr("\""): - //return &ShAtom{shtWord, repl.Str(), shqDquot, nil} + return &ShAtom{shtWord, repl.Str(), shqSubshDquot, nil} case repl.AdvanceStr("'"): return &ShAtom{shtWord, repl.Str(), shqSubshSquot, nil} case repl.AdvanceStr("`"): - //return &ShAtom{shtWord, repl.Str(), shqBackt, nil} - case repl.AdvanceRegexp(`^#.*`): + // FIXME: return &ShAtom{shtWord, repl.Str(), shqBackt, nil} + case repl.AdvanceRegexp(`^#[^)]*`): return &ShAtom{shtComment, repl.Group(0), q, nil} case repl.AdvanceStr(")"): return &ShAtom{shtWord, repl.Str(), shqPlain, nil} case repl.AdvanceRegexp(`^(?:[!#%*+,\-./0-9:=?@A-Z\[\]^_a-z{}~]+|\\[^$]|` + reShDollar + `)+`): return &ShAtom{shtWord, repl.Group(0), q, nil} } - return nil + return p.shOperator(q) } func (p *ShTokenizer) shAtomDquotBackt() *ShAtom { @@ -206,6 +202,17 @@ func (p *ShTokenizer) shAtomBacktSquot() return nil } +func (p *ShTokenizer) shAtomSubshDquot() *ShAtom { + repl := p.parser.repl + switch { + case repl.AdvanceStr("\""): + return &ShAtom{shtWord, repl.Str(), shqSubsh, nil} + case repl.AdvanceRegexp(`^(?:[\t !%&()*+,\-./0-9:;<=>?@A-Z\[\]^_a-z{|}~]+|\\[^$]|` + reShDollar + `)+`): + return &ShAtom{shtWord, repl.Group(0), shqSubshDquot, nil} + } + return nil +} + func (p *ShTokenizer) shAtomSubshSquot() *ShAtom { const q = shqSubshSquot repl := p.parser.repl @@ -317,6 +324,7 @@ func (p *ShTokenizer) ShAtoms() []*ShAto func (p *ShTokenizer) ShToken() *ShToken { var curr *ShAtom q := shqPlain + peek := func() *ShAtom { if curr == nil { curr = p.ShAtom(q) @@ -356,9 +364,7 @@ nextAtom: } repl.Reset(mark) - if len(atoms) == 0 { - return nil - } + G.Assertf(len(atoms) != 0, "ShTokenizer.ShToken") return NewShToken(repl.Since(initialMark), atoms...) } Index: pkgsrc/pkgtools/pkglint/files/buildlink3_test.go diff -u pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.17 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.18 --- pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.17 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/buildlink3_test.go Wed Oct 3 22:27:53 2018 @@ -37,14 +37,14 @@ func (s *Suite) Test_ChecklinesBuildlink // Before version 5.3, pkglint wrongly warned here. // The mk/haskell.mk file takes care of constructing the correct PKGNAME, // but pkglint had not looked at that file. -func (s *Suite) Test_ChecklinesBuildlink3Mk_name_mismatch(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__name_mismatch(c *check.C) { t := s.Init(c) - t.SetupVartypes() - G.Pkg = NewPackage(t.File("x11/hs-X11")) - G.Pkg.EffectivePkgbase = "X11" - G.Pkg.EffectivePkgnameLine = t.NewMkLine("Makefile", 3, "DISTNAME=\tX11-1.0") - mklines := t.NewMkLines("buildlink3.mk", + t.SetupCommandLine("-Wall") + t.SetupPackage("x11/hs-X11", + "DISTNAME=\tX11-1.0") + t.Chdir("x11/hs-X11") + t.CreateFileLines("buildlink3.mk", MkRcsID, "", "BUILDLINK_TREE+=\ths-X11", @@ -59,13 +59,14 @@ func (s *Suite) Test_ChecklinesBuildlink "", "BUILDLINK_TREE+=\t-hs-X11") - ChecklinesBuildlink3Mk(mklines) + G.CheckDirent(".") + // This warning only occurs because pkglint cannot see mk/haskell.mk in this test. t.CheckOutputLines( "ERROR: buildlink3.mk:3: Package name mismatch between \"hs-X11\" in this file and \"X11\" from Makefile:3.") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_name_mismatch_multiple_inclusion(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__name_mismatch_multiple_inclusion(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -88,7 +89,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:9: Definition of BUILDLINK_API_DEPENDS is missing.") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_name_mismatch_abi_api(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__name_mismatch_abi_api(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -115,7 +116,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:10: Only buildlink variables for \"hs-X11\", not \"hs-X12\" may be set in this file.") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_abi_api_versions(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__abi_api_versions(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -140,7 +141,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:9: ABI version \"1.6.0\" should be at least API version \"1.6.1\" (see line 8).") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_no_BUILDLINK_TREE_at_beginning(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__no_BUILDLINK_TREE_at_beginning(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -164,7 +165,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:3: Expected a BUILDLINK_TREE line.") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_no_BUILDLINK_TREE_at_end(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__no_BUILDLINK_TREE_at_end(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -193,7 +194,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:15: This line should contain the following text: BUILDLINK_TREE+=\t-hs-X11") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_multiple_inclusion_wrong(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__multiple_inclusion_wrong(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -212,7 +213,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:6: This line should contain the following text: HS_X11_BUILDLINK3_MK:=") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_missing_endif(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__missing_endif(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -230,7 +231,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:EOF: Expected \".endif\".") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_unknown_dependency_patterns(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__unknown_dependency_patterns(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -257,7 +258,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:10: Unknown dependency pattern \"hs-X11!=1.6.1.2nb2\".") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_PKGBASE_with_variable(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__PKGBASE_with_variable(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -282,7 +283,7 @@ func (s *Suite) Test_ChecklinesBuildlink "WARN: buildlink3.mk:3: Please use \"py\" instead of \"${PYPKGPREFIX}\" (also in other variables in this file).") } -func (s *Suite) Test_ChecklinesBuildlink3Mk_PKGBASE_with_unknown_variable(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__PKGBASE_with_unknown_variable(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -312,7 +313,7 @@ func (s *Suite) Test_ChecklinesBuildlink // This special exception might have been for backwards-compatibility, // but ideally should be handled like everywhere else. // See MkLineChecker.checkInclude. -func (s *Suite) Test_ChecklinesBuildlink3Mk_indentation(c *check.C) { +func (s *Suite) Test_ChecklinesBuildlink3Mk__indentation(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") Index: pkgsrc/pkgtools/pkglint/files/distinfo_test.go diff -u pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.17 pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.18 --- pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.17 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/distinfo_test.go Wed Oct 3 22:27:53 2018 @@ -6,10 +6,10 @@ func (s *Suite) Test_ChecklinesDistinfo( t := s.Init(c) t.Chdir("category/package") - t.SetupFileLines("patches/patch-aa", + t.CreateFileLines("patches/patch-aa", RcsID+" line is ignored for computing the SHA1 hash", "patch contents") - t.SetupFileLines("patches/patch-ab", + t.CreateFileLines("patches/patch-ab", "patch contents") lines := t.SetupFileLines("distinfo", "should be the RCS ID", @@ -34,7 +34,7 @@ func (s *Suite) Test_ChecklinesDistinfo( "WARN: distinfo:9: Patch file \"patch-nonexistent\" does not exist in directory \"patches\".") } -func (s *Suite) Test_ChecklinesDistinfo_global_hash_mismatch(c *check.C) { +func (s *Suite) Test_ChecklinesDistinfo__global_hash_mismatch(c *check.C) { t := s.Init(c) otherLine := t.NewLine("other/distinfo", 7, "dummy") @@ -53,11 +53,11 @@ func (s *Suite) Test_ChecklinesDistinfo_ "ERROR: distinfo:EOF: Expected SHA1, RMD160, SHA512, Size checksums for \"pkgname-1.1.tar.gz\", got SHA512.") } -func (s *Suite) Test_ChecklinesDistinfo_uncommitted_patch(c *check.C) { +func (s *Suite) Test_ChecklinesDistinfo__uncommitted_patch(c *check.C) { t := s.Init(c) t.Chdir("category/package") - t.SetupFileLines("patches/patch-aa", + t.CreateFileLines("patches/patch-aa", RcsID, "", "--- oldfile", @@ -65,7 +65,7 @@ func (s *Suite) Test_ChecklinesDistinfo_ "@@ -1,1 +1,1 @@", "-old", "+new") - t.SetupFileLines("CVS/Entries", + t.CreateFileLines("CVS/Entries", "/distinfo/...") lines := t.SetupFileLines("distinfo", RcsID, @@ -79,13 +79,13 @@ func (s *Suite) Test_ChecklinesDistinfo_ "WARN: distinfo:3: patches/patch-aa is registered in distinfo but not added to CVS.") } -func (s *Suite) Test_ChecklinesDistinfo_unrecorded_patches(c *check.C) { +func (s *Suite) Test_ChecklinesDistinfo__unrecorded_patches(c *check.C) { t := s.Init(c) t.Chdir("category/package") - t.SetupFileLines("patches/CVS/Entries") - t.SetupFileLines("patches/patch-aa") - t.SetupFileLines("patches/patch-src-Makefile") + t.CreateFileLines("patches/CVS/Entries") + t.CreateFileLines("patches/patch-aa") + t.CreateFileLines("patches/patch-src-Makefile") lines := t.SetupFileLines("distinfo", RcsID, "", @@ -102,7 +102,7 @@ func (s *Suite) Test_ChecklinesDistinfo_ "ERROR: distinfo: patch \"patches/patch-src-Makefile\" is not recorded. Run \""+confMake+" makepatchsum\".") } -func (s *Suite) Test_ChecklinesDistinfo_manual_patches(c *check.C) { +func (s *Suite) Test_ChecklinesDistinfo__manual_patches(c *check.C) { t := s.Init(c) t.Chdir("category/package") @@ -142,7 +142,7 @@ func (s *Suite) Test_ChecklinesDistinfo_ MkRcsID, "", "PHPEXT_MK= # defined", - "PHPPKGSRCDIR= lang/php72", + "PHPPKGSRCDIR= ../../lang/php72", "LICENSE?= unknown-license", "COMMENT?= Some PHP package", "GENERATE_PLIST+=# none", @@ -163,7 +163,7 @@ func (s *Suite) Test_ChecklinesDistinfo_ "@@ -1,1 +1,1 @@", "-old", "+new") - t.SetupFileLines("lang/php72/distinfo", + t.CreateFileLines("lang/php72/distinfo", RcsID, "", "SHA1 (patch-php72) = c109b2089f5ddbc5372b2ab28115ff558ee4187d") Index: pkgsrc/pkgtools/pkglint/files/files_test.go diff -u pkgsrc/pkgtools/pkglint/files/files_test.go:1.17 pkgsrc/pkgtools/pkglint/files/files_test.go:1.18 --- pkgsrc/pkgtools/pkglint/files/files_test.go:1.17 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/files_test.go Wed Oct 3 22:27:53 2018 @@ -4,7 +4,7 @@ import ( "gopkg.in/check.v1" ) -func (s *Suite) Test_convertToLogicalLines_no_continuation(c *check.C) { +func (s *Suite) Test_convertToLogicalLines__no_continuation(c *check.C) { rawText := "" + "first line\n" + "second line\n" @@ -16,7 +16,7 @@ func (s *Suite) Test_convertToLogicalLin c.Check(lines[1].String(), equals, "fname_nocont:2: second line") } -func (s *Suite) Test_convertToLogicalLines_continuation(c *check.C) { +func (s *Suite) Test_convertToLogicalLines__continuation(c *check.C) { rawText := "" + "first line \\\n" + "second line\n" + @@ -121,7 +121,7 @@ func (s *Suite) Test_convertToLogicalLin "ERROR: ~/comment.mk:23: Unknown Makefile line format: \"This is no comment\".") } -func (s *Suite) Test_convertToLogicalLines_continuationInLastLine(c *check.C) { +func (s *Suite) Test_convertToLogicalLines__continuation_in_last_line(c *check.C) { t := s.Init(c) rawText := "" + Index: pkgsrc/pkgtools/pkglint/files/category_test.go diff -u pkgsrc/pkgtools/pkglint/files/category_test.go:1.12 pkgsrc/pkgtools/pkglint/files/category_test.go:1.13 --- pkgsrc/pkgtools/pkglint/files/category_test.go:1.12 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/category_test.go Wed Oct 3 22:27:53 2018 @@ -6,7 +6,7 @@ func (s *Suite) Test_CheckdirCategory__t t := s.Init(c) t.SetupVartypes() - t.SetupFileLines("archivers/Makefile", + t.CreateFileLines("archivers/Makefile", "# $", "SUBDIR+=pkg1", "SUBDIR+=\u0020aaaaa", @@ -36,16 +36,16 @@ func (s *Suite) Test_CheckdirCategory__i t := s.Init(c) t.SetupVartypes() - t.SetupFileLines("archivers/Makefile", + t.CreateFileLines("archivers/Makefile", MkRcsID, "COMMENT=\t\\Make $$$$ fast\"", "", "SUBDIR+=\tpackage", "", ".include \"../mk/misc/category.mk\"") - t.SetupFileLines("archivers/package/Makefile", + t.CreateFileLines("archivers/package/Makefile", "# dummy") - t.SetupFileLines("mk/misc/category.mk", + t.CreateFileLines("mk/misc/category.mk", "# dummy") CheckdirCategory(t.File("archivers")) @@ -59,10 +59,10 @@ func (s *Suite) Test_CheckdirCategory__w t.SetupPkgsrc() t.SetupVartypes() - t.SetupFileLines("mk/misc/category.mk") - t.SetupFileLines("wip/package/Makefile") - t.SetupFileLines("wip/fs-only/Makefile") - t.SetupFileLines("wip/Makefile", + t.CreateFileLines("mk/misc/category.mk") + t.CreateFileLines("wip/package/Makefile") + t.CreateFileLines("wip/fs-only/Makefile") + t.CreateFileLines("wip/Makefile", MkRcsID, "COMMENT=\tCategory comment", "", Index: pkgsrc/pkgtools/pkglint/files/expecter.go diff -u pkgsrc/pkgtools/pkglint/files/expecter.go:1.12 pkgsrc/pkgtools/pkglint/files/expecter.go:1.13 --- pkgsrc/pkgtools/pkglint/files/expecter.go:1.12 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/expecter.go Wed Oct 3 22:27:53 2018 @@ -57,7 +57,7 @@ func (exp *Expecter) AdvanceIfMatches(re } if !exp.EOF() { - if m := regex.Match(exp.lines[exp.index].Text, re); m != nil { + if m := G.res.Match(exp.lines[exp.index].Text, re); m != nil { exp.index++ exp.m = m return true Index: pkgsrc/pkgtools/pkglint/files/toplevel_test.go diff -u pkgsrc/pkgtools/pkglint/files/toplevel_test.go:1.12 pkgsrc/pkgtools/pkglint/files/toplevel_test.go:1.13 --- pkgsrc/pkgtools/pkglint/files/toplevel_test.go:1.12 Sun Aug 12 16:31:56 2018 +++ pkgsrc/pkgtools/pkglint/files/toplevel_test.go Wed Oct 3 22:27:53 2018 @@ -5,7 +5,7 @@ import "gopkg.in/check.v1" func (s *Suite) Test_CheckdirToplevel(c *check.C) { t := s.Init(c) - t.SetupFileLines("Makefile", + t.CreateFileLines("Makefile", MkRcsID, "", "SUBDIR+= x11", @@ -15,10 +15,10 @@ func (s *Suite) Test_CheckdirToplevel(c "#SUBDIR+=\tignoreme", "SUBDIR+=\tnonexisting", // This doesn't happen in practice, therefore no warning. "SUBDIR+=\tbbb") - t.SetupFileLines("archivers/Makefile") - t.SetupFileLines("bbb/Makefile") - t.SetupFileLines("ccc/Makefile") - t.SetupFileLines("x11/Makefile") + t.CreateFileLines("archivers/Makefile") + t.CreateFileLines("bbb/Makefile") + t.CreateFileLines("ccc/Makefile") + t.CreateFileLines("x11/Makefile") t.SetupVartypes() CheckdirToplevel(t.File(".")) Index: pkgsrc/pkgtools/pkglint/files/check_test.go diff -u pkgsrc/pkgtools/pkglint/files/check_test.go:1.25 pkgsrc/pkgtools/pkglint/files/check_test.go:1.26 --- pkgsrc/pkgtools/pkglint/files/check_test.go:1.25 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/check_test.go Wed Oct 3 22:27:53 2018 @@ -57,7 +57,8 @@ func (s *Suite) SetUpTest(c *check.C) { t := &Tester{checkC: c} s.Tester = t - G = Pkglint{Testing: true} + G = NewPkglint() + G.Testing = true textproc.Testing = true G.logOut = NewSeparatorWriter(&t.stdout) G.logErr = NewSeparatorWriter(&t.stderr) @@ -89,7 +90,7 @@ func (s *Suite) TearDownTest(c *check.C) G = Pkglint{} // unusable because of missing logOut and logErr textproc.Testing = false if out := t.Output(); out != "" { - fmt.Fprintf(os.Stderr, "Unchecked output in %q; check with: t.CheckOutputLines(%v)", + fmt.Fprintf(os.Stderr, "Unchecked output in %q; check with: t.CheckOutputLines(%#v)", c.TestName(), strings.Split(out, "\n")) } t.tmpdir = "" @@ -238,20 +239,112 @@ func (t *Tester) SetupPkgsrc() { MkRcsID) } -func (t *Tester) CreateFileLines(relativeFilename string, lines ...string) (filename string) { +// SetupCategory makes the given category valid by creating a dummy Makefile. +func (t *Tester) SetupCategory(name string) { + if _, err := os.Stat(name + "/Makefile"); os.IsNotExist(err) { + t.CreateFileLines(name+"/Makefile", + MkRcsID) + } +} + +// SetupPackage sets up all files for a package so that it does not produce +// any warnings. +// +// The given makefileLines start in line 20. Except if they are variable +// definitions for already existing variables, then they replace that line. +// +// Returns the path to the package, ready to be used with Pkglint.CheckDirent. +func (t *Tester) SetupPackage(pkgpath string, makefileLines ...string) string { + category := path.Dir(pkgpath) + + t.SetupPkgsrc() + t.SetupVartypes() + t.SetupCategory(category) + + t.CreateFileLines(pkgpath+"/DESCR", + "Package description") + t.CreateFileLines(pkgpath+"/PLIST", + PlistRcsID, + "bin/program") + t.CreateFileLines(pkgpath+"/distinfo", + RcsID, + "", + "SHA1 (distfile-1.0.tar.gz) = 12341234...", + "RMD160 (distfile-1.0.tar.gz) = 12341234...", + "SHA512 (distfile-1.0.tar.gz) = 12341234...", + "Size (distfile-1.0.tar.gz) = 12341234") + + var mlines []string + mlines = append(mlines, + MkRcsID, + "", + "DISTNAME=\tdistname-1.0", + "CATEGORIES=\t"+category, + "MASTER_SITES=\t# none", + "", + "MAINTAINER=\tpkgsrc-users@NetBSD.org", + "HOMEPAGE=\t# none", + "COMMENT=\tDummy package", + "LICENSE=\t2-clause-bsd", + "") + for len(mlines) < 19 { + mlines = append(mlines, "# empty") + } + +line: + for _, line := range makefileLines { + if m, prefix := match1(line, `^(\w+=)`); m { + for i, existingLine := range mlines { + if hasPrefix(existingLine, prefix) { + mlines[i] = line + continue line + } + } + } + mlines = append(mlines, line) + } + + mlines = append(mlines, + "", + ".include \"../../mk/bsd.pkg.mk\"") + + t.CreateFileLines(pkgpath+"/Makefile", + mlines...) + + return t.File(pkgpath) +} + +func (t *Tester) CreateFileLines(relativeFilename string, lines ...string) (fileName string) { content := "" for _, line := range lines { content += line + "\n" } - filename = t.File(relativeFilename) - err := os.MkdirAll(path.Dir(filename), 0777) + fileName = t.File(relativeFilename) + err := os.MkdirAll(path.Dir(fileName), 0777) t.c().Assert(err, check.IsNil) - err = ioutil.WriteFile(filename, []byte(content), 0666) + err = ioutil.WriteFile(fileName, []byte(content), 0666) t.c().Check(err, check.IsNil) - return filename + G.fileCache.Evict(fileName) + + return fileName +} + +// CreateFileDummyPatch creates a patch file with the given name in the +// temporary directory. +func (t *Tester) CreateFileDummyPatch(relativeFileName string) { + t.CreateFileLines(relativeFileName, + RcsID, + "", + "Documentation", + "", + "--- oldfile", + "+++ newfile", + "@@ -1 +1 @@", + "-old", + "+new") } // File returns the absolute path to the given file in the @@ -295,6 +388,14 @@ func (t *Tester) Chdir(relativeFilename t.relcwd = relativeFilename } +// Remove removes the file from the temporary directory. The file must exist. +func (t *Tester) Remove(relativeFilename string) { + fileName := t.File(relativeFilename) + err := os.Remove(fileName) + t.c().Check(err, check.IsNil) + G.fileCache.Evict(fileName) +} + // ExpectFatal runs the given action and expects that this action calls // Line.Fatalf or uses some other way to panic with a pkglintFatal. // Index: pkgsrc/pkgtools/pkglint/files/line.go diff -u pkgsrc/pkgtools/pkglint/files/line.go:1.25 pkgsrc/pkgtools/pkglint/files/line.go:1.26 --- pkgsrc/pkgtools/pkglint/files/line.go:1.25 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/line.go Wed Oct 3 22:27:53 2018 @@ -39,6 +39,7 @@ type LineImpl struct { Text string raw []*RawLine autofix *Autofix + Once } func NewLine(fname string, lineno int, text string, rawLines []*RawLine) Line { @@ -47,7 +48,7 @@ func NewLine(fname string, lineno int, t // NewLineMulti is for logical Makefile lines that end with backslash. func NewLineMulti(fname string, firstLine, lastLine int, text string, rawLines []*RawLine) Line { - return &LineImpl{fname, path.Base(fname), int32(firstLine), int32(lastLine), text, rawLines, nil} + return &LineImpl{fname, path.Base(fname), int32(firstLine), int32(lastLine), text, rawLines, nil, Once{}} } // NewLineEOF creates a dummy line for logging, with the "line number" EOF. Index: pkgsrc/pkgtools/pkglint/files/plist_test.go diff -u pkgsrc/pkgtools/pkglint/files/plist_test.go:1.25 pkgsrc/pkgtools/pkglint/files/plist_test.go:1.26 --- pkgsrc/pkgtools/pkglint/files/plist_test.go:1.25 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/plist_test.go Wed Oct 3 22:27:53 2018 @@ -62,10 +62,10 @@ func (s *Suite) Test_ChecklinesPlist__em "WARN: PLIST:1: PLIST files shouldn't be empty.") } -func (s *Suite) Test_ChecklinesPlist__commonEnd(c *check.C) { +func (s *Suite) Test_ChecklinesPlist__common_end(c *check.C) { t := s.Init(c) - t.SetupFileLines("PLIST.common", + t.CreateFileLines("PLIST.common", PlistRcsID, "bin/common") lines := t.SetupFileLines("PLIST.common_end", @@ -81,7 +81,6 @@ func (s *Suite) Test_ChecklinesPlist__co t := s.Init(c) G.Pkg = NewPackage(t.File("category/pkgbase")) - G.Pkg.plistSubstCond["PLIST.bincmds"] = true lines := t.NewLines("PLIST", PlistRcsID, "${PLIST.bincmds}bin/subdir/command") @@ -111,7 +110,7 @@ func (s *Suite) Test_ChecklinesPlist__so "WARN: PLIST:6: \"bin/cat\" should be sorted before \"bin/otherprogram\".") } -func (s *Suite) Test_PlistLineSorter_Sort(c *check.C) { +func (s *Suite) Test_plistLineSorter_Sort(c *check.C) { t := s.Init(c) t.SetupCommandLine("--autofix") @@ -167,7 +166,7 @@ func (s *Suite) Test_PlistLineSorter_Sor "@exec echo \"after lib/after.la\"") // The footer starts here } -func (s *Suite) Test_PlistChecker_checkpathMan_gz(c *check.C) { +func (s *Suite) Test_PlistChecker_checkpathMan__gz(c *check.C) { t := s.Init(c) G.Pkg = NewPackage(t.File("category/pkgbase")) @@ -181,7 +180,7 @@ func (s *Suite) Test_PlistChecker_checkp "NOTE: PLIST:2: The .gz extension is unnecessary for manual pages.") } -func (s *Suite) TestPlistChecker_checkpath__PKGMANDIR(c *check.C) { +func (s *Suite) Test_PlistChecker_checkpath__PKGMANDIR(c *check.C) { t := s.Init(c) lines := t.NewLines("PLIST", @@ -194,7 +193,7 @@ func (s *Suite) TestPlistChecker_checkpa "NOTE: PLIST:2: PLIST files should mention \"man/\" instead of \"${PKGMANDIR}\".") } -func (s *Suite) TestPlistChecker_checkpath__python_egg(c *check.C) { +func (s *Suite) Test_PlistChecker_checkpath__python_egg(c *check.C) { t := s.Init(c) lines := t.NewLines("PLIST", Index: pkgsrc/pkgtools/pkglint/files/shell.go diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.25 pkgsrc/pkgtools/pkglint/files/shell.go:1.26 --- pkgsrc/pkgtools/pkglint/files/shell.go:1.25 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/shell.go Wed Oct 3 22:27:53 2018 @@ -79,8 +79,9 @@ outer: case quoting == shqPlain: switch { - case repl.AdvanceRegexp(`^[!#\%&\(\)*+,\-.\/0-9:;<=>?@A-Z\[\]^_a-z{|}~]+`), - repl.AdvanceRegexp(`^\\(?:[ !"#'\(\)*./;?\\^{|}]|\$\$)`): + // FIXME: These regular expressions don't belong here, they are the job of the tokenizer. + case repl.AdvanceRegexp(`^[!#%&()*+,\-./0-9:;<=>?@A-Z\[\]^_a-z{|}~]+`), + repl.AdvanceRegexp(`^\\(?:[ !"#'()*./;?\\^{|}]|\$\$)`): case repl.AdvanceStr("'"): quoting = shqSquot case repl.AdvanceStr("\""): @@ -192,10 +193,10 @@ func (shline *ShellLine) checkVaruseToke switch { case quoting == shqPlain && varuse.IsQ(): // Fine. - case quoting == shqBackt: - // Don't check anything here, to avoid false positives for tool names. + case (quoting == shqSquot || quoting == shqDquot) && matches(varname, `^(?:.*DIR|.*FILE|.*PATH|.*_VAR|PREFIX|.*BASE|PKGNAME)$`): // This is ok if we don't allow these variables to have embedded [\$\\\"\'\`]. + case quoting == shqDquot && varuse.IsQ(): shline.mkline.Warnf("Please don't use the :Q operator in double quotes.") Explain( @@ -249,14 +250,12 @@ func (shline *ShellLine) unescapeBacktic "", "To avoid this uncertainty, escape the double quotes using \\\".") - case repl.AdvanceRegexp("^([^\\\\`]+)"): - unescaped += repl.Group(1) - default: - line.Errorf("Internal pkglint error in ShellLine.unescapeBackticks at %q (rest=%q).", shellword, repl.AdvanceRest()) + G.Assertf(repl.AdvanceRegexp("^([^\\\\`]+)"), "incomplete switch") + unescaped += repl.Group(1) } } - line.Errorf("Unfinished backquotes: rest=%q", repl.Rest()) + line.Errorf("Unfinished backquotes: %s", repl.Rest()) return unescaped, quoting } @@ -304,7 +303,7 @@ func (shline *ShellLine) CheckShellComma line.Notef("You don't need to use \"-\" before %q.", cmd) } - repl := textproc.NewPrefixReplacer(shelltext) + repl := G.NewPrefixReplacer(shelltext) repl.AdvanceRegexp(`^\s+`) if repl.AdvanceRegexp(`^[-@]+`) { shline.checkHiddenAndSuppress(repl.Group(0), repl.Rest()) @@ -338,25 +337,29 @@ func (shline *ShellLine) CheckShellComma spc := &ShellProgramChecker{shline} spc.checkConditionalCd(program) - callback := NewMkShWalkCallback() - callback.SimpleCommand = func(command *MkShSimpleCommand) { + walker := NewMkShWalker() + walker.Callback.SimpleCommand = func(command *MkShSimpleCommand) { scc := NewSimpleCommandChecker(shline, command, time) scc.Check() if scc.strcmd.Name == "set" && scc.strcmd.AnyArgMatches(`^-.*e`) { *pSetE = true } } - callback.List = func(list *MkShList) { - spc.checkSetE(list, pSetE) + walker.Callback.AndOr = func(andor *MkShAndOr) { + if G.opts.WarnExtra && !*pSetE && walker.Current().Index != 0 { + spc.checkSetE(walker.Parent(1).(*MkShList), walker.Current().Index, andor) + } } - callback.Pipeline = func(pipeline *MkShPipeline) { + walker.Callback.Pipeline = func(pipeline *MkShPipeline) { spc.checkPipeExitcode(line, pipeline) } - callback.Word = func(word *ShToken) { + walker.Callback.Word = func(word *ShToken) { + // TODO: Try to replace false with true here; it had been set to false + // TODO: in 2016 for no apparent reason. spc.checkWord(word, false, time) } - NewMkShWalker().Walk(program, callback) + walker.Walk(program) } func (shline *ShellLine) CheckShellCommands(shellcmds string, time ToolTime) { @@ -436,7 +439,7 @@ func (scc *SimpleCommandChecker) Check() } scc.checkCommandStart() - scc.checkAbsolutePathnames() + scc.checkRegexReplace() scc.checkAutoMkdirs() scc.checkInstallMulti() scc.checkPaxPe() @@ -587,7 +590,7 @@ func (scc *SimpleCommandChecker) handleC return true } -func (scc *SimpleCommandChecker) checkAbsolutePathnames() { +func (scc *SimpleCommandChecker) checkRegexReplace() { if trace.Tracing { defer trace.Call()() } @@ -598,7 +601,7 @@ func (scc *SimpleCommandChecker) checkAb if !isSubst { CheckLineAbsolutePathname(scc.shline.mkline.Line, arg) } - if false && isSubst && !matches(arg, `"^[\"\'].*[\"\']$`) { + if G.Testing && isSubst && !matches(arg, `"^[\"\'].*[\"\']$`) { scc.shline.mkline.Warnf("Substitution commands like %q should always be quoted.", arg) Explain( "Usually these substitution commands contain characters like '*' or", @@ -739,20 +742,20 @@ func (spc *ShellProgramChecker) checkCon } } - callback := NewMkShWalkCallback() - callback.If = func(ifClause *MkShIfClause) { + walker := NewMkShWalker() + walker.Callback.If = func(ifClause *MkShIfClause) { for _, cond := range ifClause.Conds { if simple := getSimple(cond); simple != nil { checkConditionalCd(simple) } } } - callback.Loop = func(loop *MkShLoopClause) { + walker.Callback.Loop = func(loop *MkShLoopClause) { if simple := getSimple(loop.Cond); simple != nil { checkConditionalCd(simple) } } - callback.Pipeline = func(pipeline *MkShPipeline) { + walker.Callback.Pipeline = func(pipeline *MkShPipeline) { if pipeline.Negated { spc.shline.mkline.Warnf("The Solaris /bin/sh does not support negation of shell commands.") Explain( @@ -761,17 +764,7 @@ func (spc *ShellProgramChecker) checkCon "https://www.gnu.org/software/autoconf/manual/autoconf.html#Limitations-of-Builtins") } } - NewMkShWalker().Walk(list, callback) -} - -func (spc *ShellProgramChecker) checkWords(words []*ShToken, checkQuoting bool, time ToolTime) { - if trace.Tracing { - defer trace.Call()() - } - - for _, word := range words { - spc.checkWord(word, checkQuoting, time) - } + walker.Walk(list) } func (spc *ShellProgramChecker) checkWord(word *ShToken, checkQuoting bool, time ToolTime) { @@ -787,41 +780,14 @@ func (spc *ShellProgramChecker) checkPip defer trace.Call()() } - oneOf := func(s string, others ...string) bool { - for _, other := range others { - if s == other { - return true - } - } - return false - } - - // canFail tests whether one of the left-hand side commands of a - // shell pipeline can fail. - // - // Examples: - // echo "hello" | sed 's,$, world,,' => cannot fail - // find . -print | xargs cat | wc -l => can fail canFail := func() (bool, string) { for _, cmd := range pipeline.Cmds[:len(pipeline.Cmds)-1] { - simple := cmd.Simple - if simple == nil { + if spc.canFail(cmd) { + if cmd.Simple != nil && cmd.Simple.Name != nil { + return true, cmd.Simple.Name.MkText + } return true, "" } - commandName := simple.Name.MkText - if len(simple.Redirections) != 0 { - return true, commandName - } - tool, _ := G.Tool(commandName, RunTime) - switch { - case tool == nil: - return true, commandName - case oneOf(tool.Name, "echo", "printf"): - case oneOf(tool.Name, "sed", "gsed", "grep", "ggrep") && len(simple.Args) == 1: - break - default: - return true, commandName - } } return false, "" } @@ -844,29 +810,110 @@ func (spc *ShellProgramChecker) checkPip } } -func (spc *ShellProgramChecker) checkSetE(list *MkShList, eflag *bool) { +// canFail returns true if the given shell command can fail. +// Most shell commands can fail for various reasons, such as missing +// files or invalid arguments. +// +// Commands that can fail: +// echo "hello" > file +// sed 's,$, world,,' < input > output +// find . -print +// wc -l * +// +// Commands that cannot fail: +// echo "hello" +// sed 's,$, world,,' +// wc -l +func (spc *ShellProgramChecker) canFail(cmd *MkShCommand) bool { + simple := cmd.Simple + if simple == nil { + return true + } + + if simple.Name == nil { + for _, assignment := range simple.Assignments { + if contains(assignment.MkText, "`") || contains(assignment.MkText, "$(") { + if !contains(assignment.MkText, "|| ${TRUE}") { + return true + } + } + } + return false + } + + for _, redirect := range simple.Redirections { + if redirect.Target != nil && !hasSuffix(redirect.Op, "&") { + return true + } + } + + cmdName := simple.Name.MkText + switch cmdName { + case "${ECHO_MSG}", "${PHASE_MSG}", "${STEP_MSG}", + "${INFO_MSG}", "${WARNING_MSG}", "${ERROR_MSG}", + "${WARNING_CAT}", "${ERROR_CAT}", + "${DO_NADA}": + return false + case "${FAIL_MSG}": + return true + case "set": + } + + tool, _ := G.Tool(cmdName, RunTime) + if tool == nil { + return true + } + + toolName := tool.Name + args := simple.Args + argc := len(args) + switch toolName { + case "echo", "printf", "tr": + return false + case "sed", "gsed": + if argc == 2 && args[0].MkText == "-e" { + return false + } + return argc != 1 + case "grep", "ggrep": + return argc != 1 + } + + return true +} + +func (spc *ShellProgramChecker) checkSetE(list *MkShList, index int, andor *MkShAndOr) { if trace.Tracing { defer trace.Call()() } - // Disabled until the shell parser can recognize "command || exit 1" reliably. - if false && G.opts.WarnExtra && !*eflag && "the current token" == ";" { - *eflag = true - spc.shline.mkline.Warnf("Please switch to \"set -e\" mode before using a semicolon (the one after %q) to separate commands.", "previous token") - Explain( - "Normally, when a shell command fails (returns non-zero), the", - "remaining commands are still executed. For example, the following", - "commands would remove all files from the HOME directory:", - "", - "\tcd \"$HOME\"; cd /nonexistent; rm -rf *", - "", - "To fix this warning, you can:", - "", - "* insert ${RUN} at the beginning of the line", - " (which among other things does \"set -e\")", - "* insert \"set -e\" explicitly at the beginning of the line", - "* use \"&&\" instead of \";\" to separate the commands") + command := list.AndOrs[index-1].Pipes[0].Cmds[0] + if command.Simple == nil || !spc.canFail(command) { + return } + + line := spc.shline.mkline.Line + if !line.FirstTime("switch to set -e") { + return + } + + line.Warnf("Please switch to \"set -e\" mode before using a semicolon (after %q) to separate commands.", + NewStrCommand(command.Simple).String()) + Explain( + "Normally, when a shell command fails (returns non-zero), the", + "remaining commands are still executed. For example, the following", + "commands would remove all files from the HOME directory:", + "", + "\tcd \"$HOME\"; cd /nonexistent; rm -rf *", + "", + "In \"set -e\" mode, the shell stops when a command fails.", + "", + "To fix this warning, you can:", + "", + "* insert ${RUN} at the beginning of the line", + " (which among other things does \"set -e\")", + "* insert \"set -e\" explicitly at the beginning of the line", + "* use \"&&\" instead of \";\" to separate the commands") } // Some shell commands should not be used in the install phase. @@ -920,6 +967,7 @@ func splitIntoShellTokens(line Line, tex } word := "" + rest = text p := NewShTokenizer(line, text, false) emit := func() { if word != "" { Index: pkgsrc/pkgtools/pkglint/files/files.go diff -u pkgsrc/pkgtools/pkglint/files/files.go:1.18 pkgsrc/pkgtools/pkglint/files/files.go:1.19 --- pkgsrc/pkgtools/pkglint/files/files.go:1.18 Thu Aug 16 20:41:42 2018 +++ pkgsrc/pkgtools/pkglint/files/files.go Wed Oct 3 22:27:53 2018 @@ -16,6 +16,11 @@ const ( ) func Load(fileName string, options LoadOptions) []Line { + fromCache := G.fileCache.Get(fileName, options) + if fromCache != nil { + return fromCache + } + rawBytes, err := ioutil.ReadFile(fileName) if err != nil { switch { @@ -42,7 +47,9 @@ func Load(fileName string, options LoadO G.loaded.Add(path.Clean(fileName), 1) } - return convertToLogicalLines(fileName, rawText, options&Makefile != 0) + result := convertToLogicalLines(fileName, rawText, options&Makefile != 0) + G.fileCache.Put(fileName, options, result) + return result } func LoadMk(fileName string, options LoadOptions) *MkLines { Index: pkgsrc/pkgtools/pkglint/files/mklinechecker.go diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.18 pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.19 --- pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.18 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mklinechecker.go Wed Oct 3 22:27:53 2018 @@ -18,7 +18,7 @@ func (ck MkLineChecker) Check() { mkline := ck.MkLine CheckLineTrailingWhitespace(mkline.Line) - CheckLineValidCharacters(mkline.Line, `[\t -~]`) + CheckLineValidCharacters(mkline.Line) switch { case mkline.IsVarassign(): @@ -379,15 +379,11 @@ func (ck MkLineChecker) CheckVaruse(varu ck.checkVarusePermissions(varname, vartype, vuc) if varname == "LOCALBASE" && !G.Infrastructure { - ck.WarnVaruseLocalbase() + ck.MkLine.Warnf("Please use PREFIX instead of LOCALBASE.") } needsQuoting := mkline.VariableNeedsQuoting(varname, vartype, vuc) - if vuc.quoting == vucQuotFor { - ck.checkVaruseFor(varname, vartype, needsQuoting) - } - if G.opts.WarnQuoting && vuc.quoting != vucQuotUnknown && needsQuoting != nqDontKnow { ck.CheckVaruseShellword(varname, vartype, vuc, varuse.Mod(), needsQuoting) } @@ -538,57 +534,6 @@ func (ck MkLineChecker) checkVaruseLoadT } } -func (ck MkLineChecker) WarnVaruseLocalbase() { - ck.MkLine.Warnf("Please use PREFIX instead of LOCALBASE.") - Explain( - // from jlam via private mail. - "Currently, LOCALBASE is typically used in these cases:", - "", - "(1) To locate a file or directory from another package.", - "(2) To refer to own files after installation.", - "", - "Example for (1):", - "", - " STRLIST= ${LOCALBASE}/bin/strlist", - "", - " do-build:", - " cd ${WRKSRC} && ${STRLIST} *.str", - "", - "This should better be:", - "", - " EVAL_PREFIX= STRLIST_PREFIX=strlist", - " STRLIST= ${STRLIST_PREFIX}/bin/strlist", - "", - " do-build:", - " cd ${WRKSRC} && ${STRLIST} *.str", - "", - "Example for (2):", - "", - " CONFIGURE_ENV+= --with-datafiles=${LOCALBASE}/share/pkgbase", - "", - "This should better be:", - "", - " CONFIGURE_ENV+= --with-datafiles=${PREFIX}/share/pkgbase") -} - -func (ck MkLineChecker) checkVaruseFor(varname string, vartype *Vartype, needsQuoting NeedsQuoting) { - if trace.Tracing { - defer trace.Call(varname, vartype, needsQuoting)() - } - - if false && // Too many false positives - vartype != nil && - vartype.kindOfList != lkSpace && - needsQuoting != nqDoesntMatter { - ck.MkLine.Warnf("The variable %s should not be used in .for loops.", varname) - Explain( - "The .for loop splits its argument at sequences of white-space, as", - "opposed to all other places in make(1), which act like the shell.", - "Therefore only variables that are split at whitespace or don't", - "contain any special characters should be used here.") - } -} - // CheckVaruseShellword checks whether a variable use of the form ${VAR} // or ${VAR:Modifier} is allowed in a certain context. func (ck MkLineChecker) CheckVaruseShellword(varname string, vartype *Vartype, vuc *VarUseContext, mod string, needsQuoting NeedsQuoting) { @@ -601,7 +546,7 @@ func (ck MkLineChecker) CheckVaruseShell // configure scripts. // // When doing checks outside a package, the :M* operator is needed for safety. - needMstar := matches(varname, `^(?:.*_)?(?:CFLAGS||CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`) && + needMstar := matches(varname, `^(?:.*_)?(?:CFLAGS|CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`) && (G.Pkg == nil || G.Pkg.vars.Defined("GNU_CONFIGURE")) strippedMod := mod @@ -759,23 +704,12 @@ func (ck MkLineChecker) checkVarassign() ck.checkVarassignSpecific() - if varname == "EVAL_PREFIX" { - if m, evalVarname := match1(value, `^([\w_]+)=`); m { - - // The variables mentioned in EVAL_PREFIX will later be - // defined by find-prefix.mk. Therefore, they are marked - // as known in the current file. - G.Mk.vars.Define(evalVarname, mkline) - } - } - if fix := G.Pkgsrc.Deprecated[varname]; fix != "" { mkline.Warnf("Definition of %s is deprecated. %s", varname, fix) - } else if fix := G.Pkgsrc.Deprecated[varcanon]; fix != "" { + } else if fix = G.Pkgsrc.Deprecated[varcanon]; fix != "" { mkline.Warnf("Definition of %s is deprecated. %s", varname, fix) } - ck.checkVarassignPlistComment(varname, value) ck.checkVarassignVaruse() } @@ -839,10 +773,10 @@ func (ck MkLineChecker) checkVarassignVa mkline := ck.MkLine atoms := NewShTokenizer(mkline.Line, mkline.Value(), false).ShAtoms() for i, atom := range atoms { - if atom.Type == shtVaruse { + if varuse := atom.VarUse(); varuse != nil { isWordPart := isWordPart(atoms, i) vuc := &VarUseContext{vartype, time, atom.Quoting.ToVarUseContext(), isWordPart} - ck.CheckVaruse(atom.Data.(*MkVarUse), vuc) + ck.CheckVaruse(varuse, vuc) } } } @@ -922,34 +856,6 @@ func (ck MkLineChecker) checkVarassignBs "bsd.prefs.mk file, which will take care of everything.") } -func (ck MkLineChecker) checkVarassignPlistComment(varname, value string) { - if false && // This is currently neither correct nor helpful - contains(value, "@comment") && !matches(value, `="@comment "`) { - ck.MkLine.Warnf("Please don't use @comment in %s.", varname) - Explain( - "If you are defining a PLIST condition here, use one of the", - "following patterns instead:", - "", - "1. The direct way, without intermediate variable", - "", - "\tPLIST_SUBST+=\tMY_VAR=\"@comment \"", - "", - "2. The indirect way, with a separate variable", - "", - "\tPLIST_VARS+=\tMY_VAR", - "\t.if ...", - "\tMY_VAR?=\tyes", - "\t.endif") - } - - // Mark the variable as PLIST condition. This is later used in checkfile_PLIST. - if G.Pkg != nil { - if m, plistVarname := match1(value, `(.+)=.*@comment.*`); m { - G.Pkg.plistSubstCond[plistVarname] = true - } - } -} - func (ck MkLineChecker) CheckVartype(varname string, op MkOperator, value, comment string) { if trace.Tracing { defer trace.Call(varname, op, value, comment)() @@ -1040,7 +946,7 @@ func (ck MkLineChecker) checkText(text s rest := text for { - m, r := regex.ReplaceFirst(rest, `(?:^|[^$])\$\{([-A-Z0-9a-z_]+)(\.[\-0-9A-Z_a-z]+)?(?::[^\}]+)?\}`, "") + m, r := G.res.ReplaceFirst(rest, `(?:^|[^$])\$\{([-A-Z0-9a-z_]+)(\.[\-0-9A-Z_a-z]+)?(?::[^\}]+)?\}`, "") if m == nil { break } @@ -1145,7 +1051,7 @@ func (ck MkLineChecker) checkCompareVarS func (ck MkLineChecker) CheckValidCharactersInValue(reValid regex.Pattern) { mkline := ck.MkLine - rest := regex.Compile(reValid).ReplaceAllString(mkline.Value(), "") + rest := replaceAll(mkline.Value(), reValid, "") if rest != "" { uni := "" for _, c := range rest { Index: pkgsrc/pkgtools/pkglint/files/licenses.go diff -u pkgsrc/pkgtools/pkglint/files/licenses.go:1.14 pkgsrc/pkgtools/pkglint/files/licenses.go:1.15 --- pkgsrc/pkgtools/pkglint/files/licenses.go:1.14 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/licenses.go Wed Oct 3 22:27:53 2018 @@ -30,7 +30,7 @@ type LicenseChecker struct { func (lc *LicenseChecker) Check(value string, op MkOperator) { expanded := resolveVariableRefs(value) // For ${PERL5_LICENSE} - licenses := licenses.Parse(ifelseStr(op == opAssignAppend, "append-placeholder ", "") + expanded) + licenses := licenses.Parse(ifelseStr(op == opAssignAppend, "append-placeholder ", "")+expanded, &G.res) if licenses == nil { if op == opAssign { Index: pkgsrc/pkgtools/pkglint/files/logging.go diff -u pkgsrc/pkgtools/pkglint/files/logging.go:1.14 pkgsrc/pkgtools/pkglint/files/logging.go:1.15 --- pkgsrc/pkgtools/pkglint/files/logging.go:1.14 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/logging.go Wed Oct 3 22:27:53 2018 @@ -113,13 +113,16 @@ func Explain(explanation ...string) { for _, s := range explanation { if l := tabWidth(s); l > 68 && contains(s, " ") { lastSpace := strings.LastIndexByte(s[:68], ' ') - G.logErr.Write(fmt.Sprintf("Long explanation line: %s\nBreak after: %s\n", s, s[:lastSpace])) + G.logErr.Printf("Long explanation line: %s\nBreak after: %s\n", s, s[:lastSpace]) } if m, before := match1(s, `(.+)\. [^ ]`); m { if !matches(before, `\d$|e\.g`) { - G.logErr.Write(fmt.Sprintf("Short space after period: %s\n", s)) + G.logErr.Printf("Short space after period: %s\n", s) } } + if hasSuffix(s, " ") || hasSuffix(s, "\t") { + G.logErr.Printf("Trailing whitespace: %q\n", s) + } } } @@ -174,8 +177,10 @@ func (wr *SeparatorWriter) Write(text st io.WriteString(wr.out, "\n") wr.needSeparator = false } - io.WriteString(wr.out, text) - wr.wroteSomething = true + n, err := io.WriteString(wr.out, text) + if err == nil && n > 0 { + wr.wroteSomething = true + } } func (wr *SeparatorWriter) Printf(format string, args ...interface{}) { Index: pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.14 pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.15 --- pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.14 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go Wed Oct 3 22:27:53 2018 @@ -2,6 +2,131 @@ package main import "gopkg.in/check.v1" +func (s *Suite) Test_MkLineChecker_Check__url2pkg(c *check.C) { + t := s.Init(c) + + t.SetupVartypes() + + mkline := t.NewMkLine("fname.mk", 1, "# url2pkg-marker") + + MkLineChecker{mkline}.Check() + + t.CheckOutputLines( + "ERROR: fname.mk:1: This comment indicates unfinished work (url2pkg).") +} + +func (s *Suite) Test_MkLineChecker_Check__buildlink3_include_prefs(c *check.C) { + t := s.Init(c) + + t.SetupVartypes() + + t.CreateFileLines("mk/bsd.prefs.mk") + mklines := t.SetupFileMkLines("category/package/buildlink3.mk", + ".include \"../../mk/bsd.prefs.mk\"") + // If the buildlink3.mk file is not actually created, resolving the + // relative path fails since that depends on the actual file system, + // not on syntactical paths; see os.Stat in CheckRelativePath. + + MkLineChecker{mklines.mklines[0]}.Check() + + t.CheckOutputLines( + "NOTE: ~/category/package/buildlink3.mk:1: For efficiency reasons, please include bsd.fast.prefs.mk instead of bsd.prefs.mk.") +} + +func (s *Suite) Test_MkLineChecker_checkInclude(c *check.C) { + t := s.Init(c) + + t.SetupVartypes() + + t.CreateFileLines("pkgtools/x11-links/buildlink3.mk") + t.CreateFileLines("graphics/jpeg/buildlink3.mk") + t.CreateFileLines("devel/intltool/buildlink3.mk") + t.CreateFileLines("devel/intltool/builtin.mk") + mklines := t.SetupFileMkLines("category/package/fname.mk", + MkRcsID, + "", + ".include \"../../pkgtools/x11-links/buildlink3.mk\"", + ".include \"../../graphics/jpeg/buildlink3.mk\"", + ".include \"../../devel/intltool/buildlink3.mk\"", + ".include \"../../devel/intltool/builtin.mk\"") + + mklines.Check() + + t.CheckOutputLines( + "ERROR: ~/category/package/fname.mk:3: ../../pkgtools/x11-links/buildlink3.mk must not be included directly. "+ + "Include \"../../mk/x11.buildlink3.mk\" instead.", + "ERROR: ~/category/package/fname.mk:4: ../../graphics/jpeg/buildlink3.mk must not be included directly. "+ + "Include \"../../mk/jpeg.buildlink3.mk\" instead.", + "WARN: ~/category/package/fname.mk:5: Please write \"USE_TOOLS+= intltool\" instead of this line.", + "ERROR: ~/category/package/fname.mk:6: ../../devel/intltool/builtin.mk must not be included directly. "+ + "Include \"../../devel/intltool/buildlink3.mk\" instead.") +} + +func (s *Suite) Test_MkLineChecker_checkDirective(c *check.C) { + t := s.Init(c) + + t.SetupVartypes() + + mklines := t.NewMkLines("category/package/fname.mk", + MkRcsID, + "", + ".for", + ".endfor", + "", + ".if", + ".else don't", + ".endif invalid-arg", + "", + ".ifdef FNAME_MK", + ".endif", + ".ifndef FNAME_MK", + ".endif", + "", + ".for var in a b c", + ".endfor", + ".undef var", + "", + ".for VAR in a b c", + ".endfor", + "", + ".for $ in a b c", + ".endfor") + + mklines.Check() + + t.CheckOutputLines( + "ERROR: category/package/fname.mk:3: \".for\" requires arguments.", + "ERROR: category/package/fname.mk:6: \".if\" requires arguments.", + "ERROR: category/package/fname.mk:7: \".else\" does not take arguments. If you meant \"else if\", use \".elif\".", + "ERROR: category/package/fname.mk:8: \".endif\" does not take arguments.", + "WARN: category/package/fname.mk:10: The \".ifdef\" directive is deprecated. Please use \".if defined(FNAME_MK)\" instead.", + "WARN: category/package/fname.mk:12: The \".ifndef\" directive is deprecated. Please use \".if !defined(FNAME_MK)\" instead.", + "NOTE: category/package/fname.mk:17: Using \".undef\" after a \".for\" loop is unnecessary.", + "WARN: category/package/fname.mk:19: .for variable names should not contain uppercase letters.", + "ERROR: category/package/fname.mk:22: Invalid variable name \"$\".") +} + +func (s *Suite) Test_MkLineChecker_checkDependencyRule(c *check.C) { + t := s.Init(c) + + t.SetupVartypes() + + mklines := t.NewMkLines("category/package/fname.mk", + MkRcsID, + "", + ".PHONY: target-1", + "target-2: .PHONY", + ".ORDER: target-1 target-2", + "target-1:", + "target-2:", + "target-3:") + + mklines.Check() + + t.CheckOutputLines( + "WARN: category/package/fname.mk:8: Unusual target \"target-3\".") +} + func (s *Suite) Test_MkLineChecker_CheckVartype__simple_type(c *check.C) { t := s.Init(c) @@ -36,6 +161,30 @@ func (s *Suite) Test_MkLineChecker_Check t.CheckOutputEmpty() } +func (s *Suite) Test_MkLineChecker_CheckVartype__skip(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wno-types") + t.SetupVartypes() + mkline := t.NewMkLine("fname", 1, "DISTNAME=invalid:::distname") + + MkLineChecker{mkline}.Check() + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_MkLineChecker_CheckVartype__append_to_non_list(c *check.C) { + t := s.Init(c) + + t.SetupVartypes() + mkline := t.NewMkLine("fname", 1, "DISTNAME+=suffix") + + MkLineChecker{mkline}.Check() + + t.CheckOutputLines( + "WARN: fname:1: The \"+=\" operator should only be used with lists.") +} + // Pkglint once interpreted all lists as consisting of shell tokens, // splitting this URL at the ampersand. func (s *Suite) Test_MkLineChecker_checkVarassign__URL_with_shell_special_characters(c *check.C) { @@ -138,34 +287,39 @@ func (s *Suite) Test_MkLineChecker_check func (s *Suite) Test_MkLineChecker_checkVarassignPermissions(c *check.C) { t := s.Init(c) - t.SetupCommandLine("-Wall") + t.SetupCommandLine("-Wall,no-space") t.SetupVartypes() - mkline := t.NewMkLine("options.mk", 2, "PKG_DEVELOPER?=\tyes") + mklines := t.NewMkLines("options.mk", + MkRcsID, + "PKG_DEVELOPER?= yes", + "BUILD_DEFS?= VARBASE") - MkLineChecker{mkline}.checkVarassignPermissions() + mklines.Check() t.CheckOutputLines( - "WARN: options.mk:2: The variable PKG_DEVELOPER may not be given a default value by any package.") + "WARN: options.mk:2: The variable PKG_DEVELOPER may not be given a default value by any package.", + "WARN: options.mk:2: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".", + "WARN: options.mk:3: The variable BUILD_DEFS may not be given a default value (only appended to) in this file.") } // Don't check the permissions for infrastructure files since they have their own rules. -func (s *Suite) Test_MkLineChecker_checkVarassignDefPermissions__infrastructure(c *check.C) { +func (s *Suite) Test_MkLineChecker_checkVarassignPermissions__infrastructure(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") t.SetupVartypes() - t.SetupFileMkLines("mk/infra.mk", + t.CreateFileLines("mk/infra.mk", MkRcsID, "", "PKG_DEVELOPER?=\tyes") - t.SetupFileMkLines("mk/bsd.pkg.mk") + t.CreateFileLines("mk/bsd.pkg.mk") G.CheckDirent(t.File("mk/infra.mk")) t.CheckOutputEmpty() } -func (s *Suite) Test_MkLineChecker_CheckVarusePermissions(c *check.C) { +func (s *Suite) Test_MkLineChecker_checkVarusePermissions(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -186,7 +340,7 @@ func (s *Suite) Test_MkLineChecker_Check "NOTE: options.mk:4: This variable value should be aligned to column 17.") } -func (s *Suite) Test_MkLineChecker_CheckVarusePermissions__load_time(c *check.C) { +func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -202,12 +356,13 @@ func (s *Suite) Test_MkLineChecker_Check "NOTE: options.mk:2: This variable value should be aligned to column 17.") } -func (s *Suite) Test_MkLineChecker_WarnVaruseLocalbase(c *check.C) { +func (s *Suite) Test_MkLineChecker__warn_varuse_LOCALBASE(c *check.C) { t := s.Init(c) + t.SetupVartypes() mkline := t.NewMkLine("options.mk", 56, "PKGNAME=${LOCALBASE}") - MkLineChecker{mkline}.WarnVaruseLocalbase() + MkLineChecker{mkline}.Check() t.CheckOutputLines( "WARN: options.mk:56: Please use PREFIX instead of LOCALBASE.") @@ -256,7 +411,7 @@ func (s *Suite) Test_MkLineChecker__Varu t.CheckOutputEmpty() } -func (s *Suite) Test_MkLineChecker_CheckCond__comparison_with_shell_command(c *check.C) { +func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparison_with_shell_command(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -273,7 +428,7 @@ func (s *Suite) Test_MkLineChecker_Check "WARN: security/openssl/Makefile:2: Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.") } -func (s *Suite) Test_MkLine_CheckCond_comparing_PKGSRC_COMPILER_with_eqeq(c *check.C) { +func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparing_PKGSRC_COMPILER_with_eqeq(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -313,7 +468,7 @@ func (s *Suite) Test_MkLineChecker_Check // See PR 46570, Ctrl+F "4. Shell quoting". // Pkglint is correct, since the shell sees this definition for // CPPFLAGS as three words, not one word. -func (s *Suite) Test_MkLineChecker_CheckVartype_CFLAGS(c *check.C) { +func (s *Suite) Test_MkLineChecker_CheckVartype__CFLAGS(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -330,7 +485,7 @@ func (s *Suite) Test_MkLineChecker_Check // Up to 2018-01-28, pkglint applied the autofix also to the continuation // lines, which is incorrect. It replaced the dot in "4.*" with spaces. -func (s *Suite) Test_MkLineChecker_checkDirectiveIndentation_autofix(c *check.C) { +func (s *Suite) Test_MkLineChecker_checkDirectiveIndentation__autofix(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall", "--autofix") @@ -382,6 +537,64 @@ func (s *Suite) Test_MkLineChecker_Check "WARN: ~/options.mk:4: The variable PATH should be quoted as part of a shell word.") } +func (s *Suite) Test_MkLineChecker_CheckVaruseShellword__mstar(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-space") + t.SetupVartypes() + mklines := t.SetupFileMkLines("options.mk", + MkRcsID, + "CONFIGURE_ARGS+= ${CFLAGS:Q}", + "CONFIGURE_ARGS+= ${CFLAGS:M*:Q}", + "CONFIGURE_ARGS+= ${ADA_FLAGS:Q}", + "CONFIGURE_ARGS+= ${ADA_FLAGS:M*:Q}", + "CONFIGURE_ENV+= ${CFLAGS:Q}", + "CONFIGURE_ENV+= ${CFLAGS:M*:Q}", + "CONFIGURE_ENV+= ${ADA_FLAGS:Q}", + "CONFIGURE_ENV+= ${ADA_FLAGS:M*:Q}") + + mklines.Check() + + // FIXME: There should be some notes and warnings; prevented by the PERL5 case in VariableNeedsQuoting. + t.CheckOutputEmpty() +} + +func (s *Suite) Test_MkLineChecker_CheckVaruseShellword__mstar_not_needed(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-space") + pkg := t.SetupPackage("category/package", + "MAKE_FLAGS+=\tCFLAGS=${CFLAGS:M*:Q}", + "MAKE_FLAGS+=\tLFLAGS=${LDFLAGS:M*:Q}") + G.Pkgsrc.LoadInfrastructure() + // FIXME: It is too easy to forget this important call. + + // This package is guaranteed to not use GNU_CONFIGURE. + // Since the :M* hack is only needed for GNU_CONFIGURE, it is not necessary here. + G.CheckDirent(pkg) + + // FIXME: Duplicate diagnostics. + t.CheckOutputLines( + "NOTE: ~/category/package/Makefile:20: The :M* modifier is not needed here.", + "NOTE: ~/category/package/Makefile:20: The :M* modifier is not needed here.", + "NOTE: ~/category/package/Makefile:21: The :M* modifier is not needed here.", + "NOTE: ~/category/package/Makefile:21: The :M* modifier is not needed here.") +} + +func (s *Suite) Test_MkLineChecker_CheckVaruseShellword__q_not_needed(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall") + pkg := t.SetupPackage("category/package", + "MASTER_SITES=\t${HOMEPAGE:Q}") + G.Pkgsrc.LoadInfrastructure() + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:5: The :Q operator should not be used for ${HOMEPAGE} here.") +} + // The ${VARNAME:=suffix} expression should only be used with lists. // It typically appears in MASTER_SITE definitions. func (s *Suite) Test_MkLineChecker_CheckVaruse__eq_nonlist(c *check.C) { @@ -455,7 +668,10 @@ func (s *Suite) Test_MkLineChecker_check "_TOOLS_VARNAME.sed= SED", "DIST_SUBDIR= ${PKGNAME}", "WRKSRC= ${PKGNAME}", - "SITES_distfile.tar.gz= ${MASTER_SITES_GITHUB:=user/}") + "SITES_distfile.tar.gz= ${MASTER_SITES_GITHUB:=user/}", + // TODO: The first of the below assignments should be flagged as redundant by RedundantScope. + "PYTHON_VERSIONS_ACCEPTED= -13", + "PYTHON_VERSIONS_ACCEPTED= 27 36") mklines.Check() @@ -466,7 +682,14 @@ func (s *Suite) Test_MkLineChecker_check "WARN: ~/module.mk:4: PKGNAME should not be used in DIST_SUBDIR, as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.", "WARN: ~/module.mk:5: PKGNAME should not be used in WRKSRC, as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.", "WARN: ~/module.mk:6: SITES_distfile.tar.gz is defined but not used.", - "WARN: ~/module.mk:6: SITES_* is deprecated. Please use SITES.* instead.") + "WARN: ~/module.mk:6: SITES_* is deprecated. Please use SITES.* instead.", + "WARN: ~/module.mk:7: The variable PYTHON_VERSIONS_ACCEPTED may not be set "+ + "(only given a default value, appended to) in this file; it would be ok in Makefile, Makefile.common, options.mk.", + "WARN: ~/module.mk:7: Invalid version number \"-13\".", + "ERROR: ~/module.mk:7: All values for PYTHON_VERSIONS_ACCEPTED must be positive integers.", + "WARN: ~/module.mk:8: The variable PYTHON_VERSIONS_ACCEPTED may not be set "+ + "(only given a default value, appended to) in this file; it would be ok in Makefile, Makefile.common, options.mk.", + "WARN: ~/module.mk:8: The values for PYTHON_VERSIONS_ACCEPTED should be in decreasing order.") } func (s *Suite) Test_MkLineChecker_checkText(c *check.C) { Index: pkgsrc/pkgtools/pkglint/files/mkparser_test.go diff -u pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.14 pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.15 --- pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.14 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mkparser_test.go Wed Oct 3 22:27:53 2018 @@ -104,7 +104,9 @@ func (s *Suite) Test_MkParser_MkTokens(c check("${${${PKG_INFO} -E ${d} || echo:L:sh}:L:C/[^[0-9]]*/ /g:[1..3]:ts.}", varuse("${${PKG_INFO} -E ${d} || echo:L:sh}", "L", "C/[^[0-9]]*/ /g", "[1..3]", "ts.")) - check("${VAR:S/-//S/.//}", varuseText("${VAR:S/-//S/.//}", "VAR", "S/-//", "S/.//")) // For :S and :C, the colon can be left out. + // For :S and :C, the colon can be left out. + check("${VAR:S/-//S/.//}", + varuseText("${VAR:S/-//S/.//}", "VAR", "S/-//", "S/.//")) check("${VAR:ts}", varuse("VAR", "ts")) // The separator character can be left out. check("${VAR:ts\\000012}", varuse("VAR", "ts\\000012")) // The separator character can be a long octal number. @@ -130,12 +132,21 @@ func (s *Suite) Test_MkParser_MkTokens(c checkRest("hello, ${W:L:tl}orld", []*MkToken{ literal("hello, "), varuse("W", "L", "tl"), - literal("orld")}, "") + literal("orld")}, + "") checkRest("ftp://${PKGNAME}/ ${MASTER_SITES:=subdir/}", []*MkToken{ literal("ftp://"), varuse("PKGNAME"), literal("/ "), - varuse("MASTER_SITES", "=subdir/")}, "") + varuse("MASTER_SITES", "=subdir/")}, + "") + + // FIXME: Text must match modifiers. + checkRest("${VAR:S,a,b,c,d,e,f}", + []*MkToken{{ + Text: "${VAR:S,a,b,c,d,e,f}", + Varuse: &MkVarUse{varname: "VAR", modifiers: []string{"S,a,b,"}}}}, + "") } func (s *Suite) Test_MkParser_MkCond(c *check.C) { Index: pkgsrc/pkgtools/pkglint/files/substcontext_test.go diff -u pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.14 pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.15 --- pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.14 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/substcontext_test.go Wed Oct 3 22:27:53 2018 @@ -241,37 +241,18 @@ func (s *Suite) Test_SubstContext__pre_c t := s.Init(c) t.SetupCommandLine("-Wall,no-space") - t.SetupPkgsrc() - G.Pkgsrc.LoadInfrastructure() - t.CreateFileLines("category/Makefile") - t.CreateFileLines("licenses/2-clause-bsd") - - t.Chdir("category/package") - t.CreateFileLines("PLIST", - PlistRcsID, - "bin/program") - t.CreateFileLines("Makefile", - MkRcsID, - "", - "CATEGORIES= category", - "", - "COMMENT= Comment", - "LICENSE= 2-clause-bsd", - "", + pkg := t.SetupPackage("category/package", "SUBST_CLASSES+= os", "SUBST_STAGE.os= pre-configure", "SUBST_FILES.os= guess-os.h", "SUBST_SED.os= -e s,@OPSYS@,Darwin,", "", - "NO_CHECKSUM= yes", - "NO_CONFIGURE= yes", - "", - ".include \"../../mk/bsd.pkg.mk\"") + "NO_CONFIGURE= yes") - G.checkdirPackage(".") + G.CheckDirent(pkg) t.CheckOutputLines( - "WARN: Makefile:9: SUBST_STAGE pre-configure has no effect when NO_CONFIGURE is set (in line 14).") + "WARN: ~/category/package/Makefile:21: SUBST_STAGE pre-configure has no effect when NO_CONFIGURE is set (in line 25).") } func (s *Suite) Test_SubstContext__adjacent(c *check.C) { Index: pkgsrc/pkgtools/pkglint/files/licenses_test.go diff -u pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.15 pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.16 --- pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.15 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/licenses_test.go Wed Oct 3 22:27:53 2018 @@ -4,10 +4,10 @@ import ( "gopkg.in/check.v1" ) -func (s *Suite) Test_checklineLicense(c *check.C) { +func (s *Suite) Test_LicenseChecker_Check(c *check.C) { t := s.Init(c) - t.SetupFileLines("licenses/gnu-gpl-v2", + t.CreateFileLines("licenses/gnu-gpl-v2", "Most software \u2026") mkline := t.NewMkLine("Makefile", 7, "LICENSE=dummy") @@ -48,15 +48,15 @@ func (s *Suite) Test_checkToplevelUnused t := s.Init(c) t.SetupPkgsrc() - t.SetupFileLines("mk/misc/category.mk") - t.SetupFileLines("licenses/2-clause-bsd") - t.SetupFileLines("licenses/gnu-gpl-v3") + t.CreateFileLines("mk/misc/category.mk") + t.CreateFileLines("licenses/2-clause-bsd") + t.CreateFileLines("licenses/gnu-gpl-v3") - t.SetupFileLines("Makefile", + t.CreateFileLines("Makefile", MkRcsID, "SUBDIR+=\tcategory") - t.SetupFileLines("category/Makefile", + t.CreateFileLines("category/Makefile", MkRcsID, "COMMENT=\tExample category", "", @@ -64,14 +64,14 @@ func (s *Suite) Test_checkToplevelUnused "", ".include \"../mk/misc/category.mk\"") - t.SetupFileLines("category/package/Makefile", + t.CreateFileLines("category/package/Makefile", MkRcsID, "CATEGORIES=\tcategory", "", "COMMENT=Example package", "LICENSE=\t2-clause-bsd", "NO_CHECKSUM=\tyes") - t.SetupFileLines("category/package/PLIST", + t.CreateFileLines("category/package/PLIST", PlistRcsID, "bin/program") @@ -88,9 +88,9 @@ func (s *Suite) Test_LicenseChecker_chec t.SetupPkgsrc() t.SetupCommandLine("-Wno-space") - t.SetupFileLines("category/package/DESCR", + t.CreateFileLines("category/package/DESCR", "Package description") - t.SetupFileMkLines("category/package/Makefile", + t.CreateFileLines("category/package/Makefile", MkRcsID, "", "CATEGORIES= chinese", @@ -102,10 +102,10 @@ func (s *Suite) Test_LicenseChecker_chec "NO_CHECKSUM= yes", "", ".include \"../../mk/bsd.pkg.mk\"") - t.SetupFileLines("category/package/PLIST", + t.CreateFileLines("category/package/PLIST", PlistRcsID, "bin/program") - t.SetupFileLines("category/package/my-license", + t.CreateFileLines("category/package/my-license", "An individual license file.") G.Main("pkglint", t.File("category/package")) @@ -113,5 +113,6 @@ func (s *Suite) Test_LicenseChecker_chec // FIXME: It should be allowed to place a license file directly into // the package directory. t.CheckOutputLines( - "WARN: ~/category/package/my-license: Unexpected file found.", "0 errors and 1 warning found.") + "WARN: ~/category/package/my-license: Unexpected file found.", + "0 errors and 1 warning found.") } Index: pkgsrc/pkgtools/pkglint/files/linechecker.go diff -u pkgsrc/pkgtools/pkglint/files/linechecker.go:1.7 pkgsrc/pkgtools/pkglint/files/linechecker.go:1.8 --- pkgsrc/pkgtools/pkglint/files/linechecker.go:1.7 Sat Mar 24 14:32:49 2018 +++ pkgsrc/pkgtools/pkglint/files/linechecker.go Wed Oct 3 22:27:53 2018 @@ -19,8 +19,8 @@ func CheckLineAbsolutePathname(line Line // // Another context where absolute pathnames usually appear is in // assignments like "bindir=/bin". - if m, path := regex.Match1(text, `(?:^|\$[{(]DESTDIR[)}]|[\w_]+\s*=\s*)(/(?:[^"'\s]|"[^"*]"|'[^']*')*)`); m { - if regex.Matches(path, `^/\w`) { + if m, path := match1(text, `(?:^|\s|\$[{(]DESTDIR[)}]|[\w_]+\s*=\s*)(/(?:[^"'\s\\]|"[^"*]"|'[^']*')*)`); m { + if matches(path, `^/\w`) { CheckwordAbsolutePathname(line, path) } } @@ -36,13 +36,14 @@ func CheckLineLength(line Line, maxlengt } } -func CheckLineValidCharacters(line Line, reChar regex.Pattern) { - rest := regex.Compile(reChar).ReplaceAllString(line.Text, "") - if rest != "" { - uni := "" - for _, c := range rest { - uni += fmt.Sprintf(" %U", c) +func CheckLineValidCharacters(line Line) { + uni := "" + for _, r := range line.Text { + if r != '\t' && !(' ' <= r && r <= '~') { + uni += fmt.Sprintf(" %U", r) } + } + if uni != "" { line.Warnf("Line contains invalid characters (%s).", uni[1:]) } } @@ -64,7 +65,7 @@ func CheckLineRcsid(line Line, prefixRe defer trace.Call(prefixRe, suggestedPrefix)() } - if regex.Matches(line.Text, `^`+prefixRe+`\$`+`NetBSD(?::[^\$]+)?\$$`) { + if matches(line.Text, `^`+prefixRe+`\$`+`NetBSD(?::[^\$]+)?\$$`) { return true } @@ -86,13 +87,16 @@ func CheckwordAbsolutePathname(line Line } switch { - case regex.Matches(word, `^/dev/(?:null|tty|zero)$`): - // These are defined by POSIX. + case matches(word, `^/dev/(?:null|tty|zero)$`): + // These are defined by POSIX. + case word == "/bin/sh": - // This is usually correct, although on Solaris, it's pretty feature-crippled. - case regex.Matches(word, `^/s\W`): - // Probably a sed(1) command - case regex.Matches(word, `^/(?:[a-z]|\$[({])`): + // This is usually correct, although on Solaris, it's pretty feature-crippled. + + case matches(word, `/s\W`): + // Probably a sed(1) command, e.g. /find/s,replace,with, + + case matches(word, `^/(?:[a-z]|\$[({])`): // Absolute paths probably start with a lowercase letter. line.Warnf("Found absolute pathname: %s", word) if contains(line.Text, "DESTDIR") { Index: pkgsrc/pkgtools/pkglint/files/linechecker_test.go diff -u pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.7 pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.8 --- pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.7 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/linechecker_test.go Wed Oct 3 22:27:53 2018 @@ -4,22 +4,30 @@ import ( "gopkg.in/check.v1" ) -func (s *Suite) Test_LineChecker_CheckAbsolutePathname(c *check.C) { +func (s *Suite) Test_CheckLineAbsolutePathname(c *check.C) { t := s.Init(c) line := t.NewLine("Makefile", 1, "# dummy") CheckLineAbsolutePathname(line, "bindir=/bin") CheckLineAbsolutePathname(line, "bindir=/../lib") - CheckLineAbsolutePathname(line, "cat /dev/null") // FIXME: Not classified as absolute path. - CheckLineAbsolutePathname(line, "cat /dev//tty") // FIXME: Not classified as absolute patFIXMEh. - CheckLineAbsolutePathname(line, "cat /dev/zero") // FIXME: Not classified as absolute path. - CheckLineAbsolutePathname(line, "cat /dev/stdin") // FIXME: Not classified as absolute path. - CheckLineAbsolutePathname(line, "cat /dev/stdout") // FIXME: Not classified as absolute path. - CheckLineAbsolutePathname(line, "cat /dev/stderr") // FIXME: Not classified as absolute path. + CheckLineAbsolutePathname(line, "cat /dev/null") + CheckLineAbsolutePathname(line, "cat /dev/tty") + CheckLineAbsolutePathname(line, "cat /dev/zero") + CheckLineAbsolutePathname(line, "cat /dev/stdin") + CheckLineAbsolutePathname(line, "cat /dev/stdout") + CheckLineAbsolutePathname(line, "cat /dev/stderr") + CheckLineAbsolutePathname(line, "printf '#! /bin/sh\\nexit 0'") + + // This is not a file name at all, but certainly looks like one. + // Nevertheless, pkglint doesn't fall into the trap. + CheckLineAbsolutePathname(line, "sed -e /usr/s/usr/var/g") t.CheckOutputLines( - "WARN: Makefile:1: Found absolute pathname: /bin") + "WARN: Makefile:1: Found absolute pathname: /bin", + "WARN: Makefile:1: Found absolute pathname: /dev/stdin", + "WARN: Makefile:1: Found absolute pathname: /dev/stdout", + "WARN: Makefile:1: Found absolute pathname: /dev/stderr") } func (s *Suite) Test_CheckLineTrailingWhitespace(c *check.C) { Index: pkgsrc/pkgtools/pkglint/files/mkshtypes.go diff -u pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.7 pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.8 --- pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.7 Mon Jan 1 18:04:15 2018 +++ pkgsrc/pkgtools/pkglint/files/mkshtypes.go Wed Oct 3 22:27:53 2018 @@ -1,14 +1,16 @@ package main import ( - "fmt" "netbsd.org/pkglint/regex" + "strings" ) +// MkShList is a list of shell commands, separated by newlines or semicolons. +// // Example: cd $dir && echo "In $dir"; cd ..; ls -l type MkShList struct { AndOrs []*MkShAndOr - Separators []MkShSeparator + Separators []MkShSeparator // One less entry than in AndOrs. } func NewMkShList() *MkShList { @@ -25,6 +27,9 @@ func (list *MkShList) AddSeparator(separ return list } +// MkShAndOr is a group of commands that are connected with && or || +// conditions. +// // Example: cd $dir && echo "In $dir" || echo "Cannot cd into $dir" type MkShAndOr struct { Pipes []*MkShPipeline @@ -41,6 +46,8 @@ func (andor *MkShAndOr) Add(op string, p return andor } +// MkShPipeline is a group of commands, connected by pipelines. +// // Example: grep word file | sed s,^,---, type MkShPipeline struct { Negated bool @@ -56,6 +63,8 @@ func (pipe *MkShPipeline) Add(cmd *MkShC return pipe } +// MkShCommand is a simple or compound shell command. +// // Example: LC_ALL=C sort */*.c > sorted // Example: dir() { ls -l "$@"; } // Example: { echo "first"; echo "second"; } @@ -66,6 +75,8 @@ type MkShCommand struct { Redirects []*MkShRedirection // For Compound and FuncDef } +// MkShCompoundCommand is a group of commands. +// // Example: { echo "first"; echo "second"; } // Example: for f in *.c; do compile "$f"; done // Example: if [ -f "$file" ]; then echo "It exists"; fi @@ -79,6 +90,8 @@ type MkShCompoundCommand struct { Loop *MkShLoopClause } +// MkShForClause is a "for" loop. +// // Example: for f in *.c; do compile "$f"; done type MkShForClause struct { Varname string @@ -86,12 +99,16 @@ type MkShForClause struct { Body *MkShList } +// MkShCaseClause is a "case" statement, including all its branches. +// // Example: case $filename in *.c) echo "C source" ;; esac type MkShCaseClause struct { Word *ShToken Cases []*MkShCaseItem } +// MkShCaseItem is one branch of a "case" statement. +// // Example: *.c) echo "C source" ;; type MkShCaseItem struct { Patterns []*ShToken @@ -99,6 +116,9 @@ type MkShCaseItem struct { Separator MkShSeparator } +// MkShIfClause is a conditional statement, possibly having +// many branches. +// // Example: if [ -f "$file" ]; then echo "It exists"; fi type MkShIfClause struct { Conds []*MkShList @@ -111,6 +131,8 @@ func (cl *MkShIfClause) Prepend(cond *Mk cl.Actions = append([]*MkShList{action}, cl.Actions...) } +// MkShLoopClause is a "while" or "until" loop. +// // Example: while sleep 1; do printf .; done type MkShLoopClause struct { Cond *MkShList @@ -118,12 +140,17 @@ type MkShLoopClause struct { Until bool } +// MkShFunctionDefinition is the definition of a shell function. +// // Example: dir() { ls -l "$@"; } type MkShFunctionDefinition struct { Name string Body *MkShCompoundCommand } +// MkShSimpleCommand is a shell command that does not involve any +// pipeline or conditionals. +// // Example: LC_ALL=C sort */*.c > sorted type MkShSimpleCommand struct { Assignments []*ShToken @@ -149,9 +176,9 @@ func NewStrCommand(cmd *MkShSimpleComman return strcmd } -// Similar to MkShSimpleCommand, but all components are converted -// to strings to allow for simpler checks, especially for analyzing -// command line options. +// StrCommand is structurally similar to MkShSimpleCommand, but all +// components are converted to strings to allow for simpler checks, +// especially for analyzing command line options. // // Example: LC_ALL=C sort */*.c > sorted type StrCommand struct { @@ -160,6 +187,7 @@ type StrCommand struct { Args []string } +// HasOption checks whether one of the arguments is exactly the given opt. func (c *StrCommand) HasOption(opt string) bool { for _, arg := range c.Args { if arg == opt { @@ -179,9 +207,21 @@ func (c *StrCommand) AnyArgMatches(patte } func (c *StrCommand) String() string { - return fmt.Sprintf("%v %v %v", c.Assignments, c.Name, c.Args) + var strs []string + for _, assignment := range c.Assignments { + strs = append(strs, assignment) + } + if c.Name != "" { + strs = append(strs, c.Name) + } + for _, arg := range c.Args { + strs = append(strs, arg) + } + return strings.Join(strs, " ") } +// MkShRedirection is a single file descriptor redirection. +// // Example: > sorted // Example: 2>&1 type MkShRedirection struct { Index: pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.7 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.8 --- pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.7 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go Wed Oct 3 22:27:53 2018 @@ -61,22 +61,22 @@ func (s *Suite) Test_Pkgsrc_parseSuggest func (s *Suite) Test_Pkgsrc_loadTools(c *check.C) { t := s.Init(c) - t.SetupFileLines("mk/tools/bsd.tools.mk", + t.CreateFileLines("mk/tools/bsd.tools.mk", ".include \"flex.mk\"", ".include \"gettext.mk\"", ".include \"strip.mk\"", ".include \"replace.mk\"") - t.SetupFileLines("mk/tools/defaults.mk", + t.CreateFileLines("mk/tools/defaults.mk", "_TOOLS_VARNAME.chown=CHOWN", "_TOOLS_VARNAME.gawk=AWK", "_TOOLS_VARNAME.mv=MV", "_TOOLS_VARNAME.pwd=PWD") - t.SetupFileLines("mk/tools/flex.mk", + t.CreateFileLines("mk/tools/flex.mk", "# empty") - t.SetupFileLines("mk/tools/gettext.mk", + t.CreateFileLines("mk/tools/gettext.mk", "USE_TOOLS+=msgfmt", "TOOLS_CREATE+=msgfmt") - t.SetupFileLines("mk/tools/strip.mk", + t.CreateFileLines("mk/tools/strip.mk", ".if defined(_INSTALL_UNSTRIPPED) || !defined(TOOLS_PLATFORM.strip)", "TOOLS_NOOP+= strip", ".else", @@ -84,14 +84,14 @@ func (s *Suite) Test_Pkgsrc_loadTools(c "TOOLS_PATH.strip= ${TOOLS_PLATFORM.strip}", ".endif", "STRIP?= strip") - t.SetupFileLines("mk/tools/replace.mk", + t.CreateFileLines("mk/tools/replace.mk", "_TOOLS.bzip2=\tbzip2 bzcat", "#TOOLS_CREATE+=commented out", "_UNRELATED_VAR=\t# empty") - t.SetupFileLines("mk/bsd.prefs.mk", + t.CreateFileLines("mk/bsd.prefs.mk", "USE_TOOLS+=\tpwd", "USE_TOOLS+=\tm4:pkgsrc") - t.SetupFileLines("mk/bsd.pkg.mk", + t.CreateFileLines("mk/bsd.pkg.mk", "USE_TOOLS+=\tmv") G.Pkgsrc.loadTools() @@ -119,17 +119,46 @@ func (s *Suite) Test_Pkgsrc_loadTools(c "TRACE: - (*Tools).Trace(\"Pkgsrc\")") } +// As a side-benefit, loadTools also loads the _BUILD_DEFS. +func (s *Suite) Test_Pkgsrc_loadTools__BUILD_DEFS(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall") + t.SetupToolUsable("echo", "ECHO") + pkg := t.SetupPackage("category/package", + "pre-configure:", + "\t@${ECHO} ${PKG_SYSCONFDIR} ${VARBASE}") + t.CreateFileLines("mk/bsd.pkg.mk", + MkRcsID, + "_BUILD_DEFS+=\tPKG_SYSCONFBASEDIR PKG_SYSCONFDIR") + G.Pkgsrc.LoadInfrastructure() + + G.CheckDirent(pkg) + + c.Check(G.Pkgsrc.IsBuildDef("PKG_SYSCONFDIR"), equals, true) + c.Check(G.Pkgsrc.IsBuildDef("VARBASE"), equals, false) + + // FIXME: There should be a warning for VARBASE, but G.Pkgsrc.UserDefinedVars + // does not contain anything at mklinechecker.go:/UserDefinedVars/. + t.CheckOutputLines() +} + func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile(c *check.C) { t := s.Init(c) - t.SetupFileLines("doc/CHANGES-2018", + t.CreateFileLines("doc/CHANGES-2018", "\tAdded category/package version 1.0 [author1 2015-01-01]", // Wrong year "\tUpdated category/package to 1.5 [author2 2018-01-02]", "\tRenamed category/package to category/pkg [author3 2018-01-03]", "\tMoved category/package to other/package [author4 2018-01-04]", "\tRemoved category/package [author5 2018-01-09]", // Too far in the future "\tRemoved category/package successor category/package2 [author6 2018-01-06]", - "\tDowngraded category/package to 1.2 [author7 2018-01-07]") + "\tDowngraded category/package to 1.2 [author7 2018-01-07]", + "", + "\ttoo few fields", + "\ttoo many many many many many fields", + "\tmissing brackets around author", + "\tAdded another [new package]") changes := G.Pkgsrc.loadDocChangesFromFile(t.File("doc/CHANGES-2018")) @@ -144,22 +173,68 @@ func (s *Suite) Test_Pkgsrc_loadDocChang t.CheckOutputLines( "WARN: ~/doc/CHANGES-2018:1: Year 2015 for category/package does not match the file name ~/doc/CHANGES-2018.", - "WARN: ~/doc/CHANGES-2018:6: Date 2018-01-06 for category/package is earlier than 2018-01-09 for category/package.") + "WARN: ~/doc/CHANGES-2018:6: Date 2018-01-06 for category/package is earlier than 2018-01-09 for category/package.", + "WARN: ~/doc/CHANGES-2018:12: Unknown doc/CHANGES line: \tAdded another [new package]") +} + +func (s *Suite) Test_Pkgsrc_loadDocChanges__not_found(c *check.C) { + t := s.Init(c) + + t.SetupPkgsrc() + t.Remove("doc/CHANGES-2018") + t.Remove("doc/TODO") + t.Remove("doc") + + t.ExpectFatal( + G.Pkgsrc.loadDocChanges, + "FATAL: ~/doc: Cannot be read.") } -func (s *Suite) Test_Pkgsrc_deprecated(c *check.C) { +func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__not_found(c *check.C) { + t := s.Init(c) + + t.ExpectFatal( + func() { G.Pkgsrc.loadDocChangesFromFile(t.File("doc/CHANGES-2018")) }, + "FATAL: ~/doc/CHANGES-2018: Cannot be read.") +} + +func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__wip(c *check.C) { + t := s.Init(c) + + pkg := t.SetupPackage("wip/package", + "DISTNAME=\tpackage-1.11", + "CATEGORIES=\tlocal") + t.CreateFileLines("wip/TODO", + RcsID, + "", + "Suggested package updates", + "", + "\to package-1.13 [cool new features]") + G.Pkgsrc.LoadInfrastructure() + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "WARN: ~/wip/package/Makefile:3: This package should be updated to 1.13 ([cool new features]).") +} + +func (s *Suite) Test_Pkgsrc__deprecated(c *check.C) { t := s.Init(c) G.Pkgsrc.initDeprecatedVars() - mkline := t.NewMkLine("Makefile", 5, "USE_PERL5=\tyes") + mklines := t.NewMkLines("Makefile", + "USE_PERL5=\tyes", + "SUBST_POSTCMD.class=${ECHO}") - MkLineChecker{mkline}.checkVarassign() + MkLineChecker{mklines.mklines[0]}.checkVarassign() + MkLineChecker{mklines.mklines[1]}.checkVarassign() t.CheckOutputLines( - "WARN: Makefile:5: Definition of USE_PERL5 is deprecated. Use USE_TOOLS+=perl or USE_TOOLS+=perl:run instead.") + "WARN: Makefile:1: Definition of USE_PERL5 is deprecated. Use USE_TOOLS+=perl or USE_TOOLS+=perl:run instead.", + "WARN: Makefile:2: Definition of SUBST_POSTCMD.class is deprecated. Has been removed, as it seemed unused.") } -func (s *Suite) Test_Pkgsrc_Latest_no_basedir(c *check.C) { +func (s *Suite) Test_Pkgsrc_Latest__no_basedir(c *check.C) { t := s.Init(c) latest := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0") @@ -169,10 +244,10 @@ func (s *Suite) Test_Pkgsrc_Latest_no_ba "ERROR: Cannot find latest version of \"^python[0-9]+$\" in \"~/lang\".") } -func (s *Suite) Test_Pkgsrc_Latest_no_subdirs(c *check.C) { +func (s *Suite) Test_Pkgsrc_Latest__no_subdirs(c *check.C) { t := s.Init(c) - t.SetupFileLines("lang/Makefile") + t.CreateFileLines("lang/Makefile") latest := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0") @@ -181,46 +256,88 @@ func (s *Suite) Test_Pkgsrc_Latest_no_su "ERROR: Cannot find latest version of \"^python[0-9]+$\" in \"~/lang\".") } -func (s *Suite) Test_Pkgsrc_Latest_single(c *check.C) { +func (s *Suite) Test_Pkgsrc_Latest__single(c *check.C) { t := s.Init(c) - t.SetupFileLines("lang/Makefile") - t.SetupFileLines("lang/python27/Makefile") + t.CreateFileLines("lang/Makefile") + t.CreateFileLines("lang/python27/Makefile") latest := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0") c.Check(latest, equals, "../../lang/python27") + + cached := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0") + + c.Check(cached, equals, "../../lang/python27") } -func (s *Suite) Test_Pkgsrc_Latest_multi(c *check.C) { +func (s *Suite) Test_Pkgsrc_Latest__multi(c *check.C) { t := s.Init(c) - t.SetupFileLines("lang/Makefile") - t.SetupFileLines("lang/python27/Makefile") - t.SetupFileLines("lang/python35/Makefile") + t.CreateFileLines("lang/Makefile") + t.CreateFileLines("lang/python27/Makefile") + t.CreateFileLines("lang/python35/Makefile") latest := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0") c.Check(latest, equals, "../../lang/python35") } -func (s *Suite) Test_Pkgsrc_Latest_numeric(c *check.C) { +func (s *Suite) Test_Pkgsrc_Latest__numeric(c *check.C) { t := s.Init(c) - t.SetupFileLines("databases/postgresql95/Makefile") - t.SetupFileLines("databases/postgresql97/Makefile") - t.SetupFileLines("databases/postgresql100/Makefile") - t.SetupFileLines("databases/postgresql104/Makefile") + t.CreateFileLines("databases/postgresql95/Makefile") + t.CreateFileLines("databases/postgresql97/Makefile") + t.CreateFileLines("databases/postgresql100/Makefile") + t.CreateFileLines("databases/postgresql104/Makefile") latest := G.Pkgsrc.Latest("databases", `^postgresql[0-9]+$`, "$0") c.Check(latest, equals, "postgresql104") } +func (s *Suite) Test_Pkgsrc_Latest__numeric_multiple_numbers(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("emulators/suse_131_32_gtk2/Makefile") + t.CreateFileLines("emulators/suse_131_32_qt5/Makefile") + t.CreateFileLines("emulators/suse_131_gtk2/Makefile") + t.CreateFileLines("emulators/suse_131_qt5/Makefile") + + latest := G.Pkgsrc.Latest("emulators", `^suse_(\d+).*$`, "$1") + + c.Check(latest, equals, "131") +} + +// In 2017, PostgreSQL changed their versioning scheme to SemVer, +// and since the pkgsrc directory contains the major version, +// without any separating dots, the case of version 10 being +// later than 95 needs to be handled specially. +func (s *Suite) Test_Pkgsrc_Latest__postgresql(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("databases/postgresql95/Makefile") + t.CreateFileLines("databases/postgresql97/Makefile") + t.CreateFileLines("databases/postgresql10/Makefile") + t.CreateFileLines("databases/postgresql11/Makefile") + + latest := G.Pkgsrc.Latest("databases", `^postgresql[0-9]+$`, "$0") + + c.Check(latest, equals, "postgresql11") +} + +func (s *Suite) Test_Pkgsrc_Latest__invalid_argument(c *check.C) { + t := s.Init(c) + + t.ExpectFatal( + func() { G.Pkgsrc.Latest("databases", `postgresql[0-9]+`, "$0") }, + "FATAL: Pkglint internal error: Regular expression \"postgresql[0-9]+\" must be anchored at both ends.") +} + func (s *Suite) Test_Pkgsrc_loadPkgOptions(c *check.C) { t := s.Init(c) - t.SetupFileLines("mk/defaults/options.description", + t.CreateFileLines("mk/defaults/options.description", "option-name Description of the option", "<<<<< Merge conflict", "===== Merge conflict", @@ -228,7 +345,7 @@ func (s *Suite) Test_Pkgsrc_loadPkgOptio t.ExpectFatal( G.Pkgsrc.loadPkgOptions, - "FATAL: ~/mk/defaults/options.description:2: Unknown line format.") + "FATAL: ~/mk/defaults/options.description:2: Unknown line format: <<<<< Merge conflict") } func (s *Suite) Test_Pkgsrc_loadTools__no_tools_found(c *check.C) { Index: pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.7 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.8 --- pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.7 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go Wed Oct 3 22:27:53 2018 @@ -1,10 +1,15 @@ package main -import "gopkg.in/check.v1" +import ( + "gopkg.in/check.v1" + "strings" +) func (s *Suite) Test_ShTokenizer_ShAtom(c *check.C) { t := s.Init(c) + // checkRest ensures that the given string is parsed to the expected + // atoms, and returns the remaining text. checkRest := func(s string, expected ...*ShAtom) string { p := NewShTokenizer(dummyLine, s, false) q := shqPlain @@ -14,328 +19,371 @@ func (s *Suite) Test_ShTokenizer_ShAtom( } return p.Rest() } + + // check ensures that the given string is parsed to the expected + // atoms, and that the text is completely consumed by the parser. check := func(str string, expected ...*ShAtom) { rest := checkRest(str, expected...) c.Check(rest, equals, "") t.CheckOutputEmpty() } - token := func(typ ShAtomType, text string, quoting ShQuoting) *ShAtom { - return &ShAtom{typ, text, quoting, nil} + atom := func(typ ShAtomType, text string) *ShAtom { + return &ShAtom{typ, text, shqPlain, nil} } - word := func(s string) *ShAtom { return token(shtWord, s, shqPlain) } - dquot := func(s string) *ShAtom { return token(shtWord, s, shqDquot) } - squot := func(s string) *ShAtom { return token(shtWord, s, shqSquot) } - backt := func(s string) *ShAtom { return token(shtWord, s, shqBackt) } - operator := func(s string) *ShAtom { return token(shtOperator, s, shqPlain) } - varuse := func(varname string, modifiers ...string) *ShAtom { + + operator := func(s string) *ShAtom { return atom(shtOperator, s) } + comment := func(s string) *ShAtom { return atom(shtComment, s) } + mkvar := func(varname string, modifiers ...string) *ShAtom { text := "${" + varname for _, modifier := range modifiers { - text += ":" + modifier + text += ":" + strings.Replace(strings.Replace(modifier, "\\", "\\\\", -1), ":", "\\:", -1) } text += "}" varuse := &MkVarUse{varname: varname, modifiers: modifiers} return &ShAtom{shtVaruse, text, shqPlain, varuse} } - q := func(q ShQuoting, token *ShAtom) *ShAtom { - return &ShAtom{token.Type, token.MkText, q, token.Data} - } - whitespace := func(s string) *ShAtom { return token(shtSpace, s, shqPlain) } - space := token(shtSpace, " ", shqPlain) + text := func(s string) *ShAtom { return atom(shtWord, s) } + whitespace := func(s string) *ShAtom { return atom(shtSpace, s) } + + space := whitespace(" ") semicolon := operator(";") pipe := operator("|") + subshell := atom(shtSubshell, "$$(") + + q := func(q ShQuoting, atom *ShAtom) *ShAtom { + return &ShAtom{atom.Type, atom.MkText, q, atom.data} + } + backt := func(atom *ShAtom) *ShAtom { return q(shqBackt, atom) } + dquot := func(atom *ShAtom) *ShAtom { return q(shqDquot, atom) } + squot := func(atom *ShAtom) *ShAtom { return q(shqSquot, atom) } + subsh := func(atom *ShAtom) *ShAtom { return q(shqSubsh, atom) } + backtDquot := func(atom *ShAtom) *ShAtom { return q(shqBacktDquot, atom) } + backtSquot := func(atom *ShAtom) *ShAtom { return q(shqBacktSquot, atom) } + dquotBackt := func(atom *ShAtom) *ShAtom { return q(shqDquotBackt, atom) } + subshDquot := func(atom *ShAtom) *ShAtom { return q(shqSubshDquot, atom) } + subshSquot := func(atom *ShAtom) *ShAtom { return q(shqSubshSquot, atom) } + dquotBacktDquot := func(atom *ShAtom) *ShAtom { return q(shqDquotBacktDquot, atom) } + dquotBacktSquot := func(atom *ShAtom) *ShAtom { return q(shqDquotBacktSquot, atom) } + + // Ignore unused functions; useful for deleting some of the tests during debugging. + use := func(args ...interface{}) {} + use(checkRest, check) + use(operator, comment, mkvar, text, whitespace) + use(space, semicolon, pipe, subshell) + use(backt, dquot, squot, subsh) + use(backtDquot, backtSquot, dquotBackt, subshDquot, subshSquot) + use(dquotBacktDquot, dquotBacktSquot) check("" /* none */) check("$$var", - word("$$var")) + text("$$var")) check("$$var$$var", - word("$$var$$var")) + text("$$var$$var")) check("$$var;;", - word("$$var"), + text("$$var"), operator(";;")) check("'single-quoted'", - q(shqSquot, word("'")), - q(shqSquot, word("single-quoted")), - q(shqPlain, word("'"))) + squot(text("'")), + squot(text("single-quoted")), + text("'")) rest := checkRest("\"" /* none */) c.Check(rest, equals, "\"") check("$${file%.c}.o", - word("$${file%.c}.o")) + text("$${file%.c}.o")) check("hello", - word("hello")) + text("hello")) check("hello, world", - word("hello,"), + text("hello,"), space, - word("world")) + text("world")) check("\"", - dquot("\"")) + dquot(text("\""))) check("`", - backt("`")) + backt(text("`"))) check("`cat fname`", - backt("`"), - backt("cat"), - token(shtSpace, " ", shqBackt), - backt("fname"), - word("`")) + backt(text("`")), + backt(text("cat")), + backt(space), + backt(text("fname")), + text("`")) check("hello, \"world\"", - word("hello,"), + text("hello,"), space, - dquot("\""), - dquot("world"), - word("\"")) + dquot(text("\"")), + dquot(text("world")), + text("\"")) check("set -e;", - word("set"), + text("set"), space, - word("-e"), + text("-e"), semicolon) check("cd ${WRKSRC}/doc/man/man3; PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\";", - word("cd"), + text("cd"), space, - varuse("WRKSRC"), - word("/doc/man/man3"), + mkvar("WRKSRC"), + text("/doc/man/man3"), semicolon, space, - word("PAGES="), - dquot("\""), - q(shqDquotBackt, word("`")), - q(shqDquotBackt, word("ls")), - q(shqDquotBackt, space), - q(shqDquotBackt, word("-1")), - q(shqDquotBackt, space), - q(shqDquotBackt, operator("|")), - q(shqDquotBackt, space), - q(shqDquotBackt, varuse("SED")), - q(shqDquotBackt, space), - q(shqDquotBackt, word("-e")), - q(shqDquotBackt, space), - q(shqDquotBacktSquot, word("'")), - q(shqDquotBacktSquot, word("s,3qt$$,3,")), - q(shqDquotBackt, word("'")), - q(shqDquot, word("`")), - q(shqPlain, word("\"")), + text("PAGES="), + dquot(text("\"")), + dquotBackt(text("`")), + dquotBackt(text("ls")), + dquotBackt(space), + dquotBackt(text("-1")), + dquotBackt(space), + dquotBackt(operator("|")), + dquotBackt(space), + dquotBackt(mkvar("SED")), + dquotBackt(space), + dquotBackt(text("-e")), + dquotBackt(space), + dquotBacktSquot(text("'")), + dquotBacktSquot(text("s,3qt$$,3,")), + dquotBackt(text("'")), + dquot(text("`")), + text("\""), semicolon) check("ls -1 | ${SED} -e 's,3qt$$,3,'", - word("ls"), space, word("-1"), space, + text("ls"), space, text("-1"), space, pipe, space, - varuse("SED"), space, word("-e"), space, - squot("'"), squot("s,3qt$$,3,"), word("'")) + mkvar("SED"), space, text("-e"), space, + squot(text("'")), squot(text("s,3qt$$,3,")), text("'")) check("(for PAGE in $$PAGES; do ", - &ShAtom{shtOperator, "(", shqPlain, nil}, - word("for"), + operator("("), + text("for"), space, - word("PAGE"), + text("PAGE"), space, - word("in"), + text("in"), space, - word("$$PAGES"), + text("$$PAGES"), semicolon, space, - word("do"), + text("do"), space) check(" ${ECHO} installing ${DESTDIR}${QTPREFIX}/man/man3/$${PAGE}; ", whitespace(" "), - varuse("ECHO"), + mkvar("ECHO"), space, - word("installing"), + text("installing"), space, - varuse("DESTDIR"), - varuse("QTPREFIX"), - word("/man/man3/$${PAGE}"), + mkvar("DESTDIR"), + mkvar("QTPREFIX"), + text("/man/man3/$${PAGE}"), semicolon, space) check(" set - X `head -1 $${PAGE}qt`; ", whitespace(" "), - word("set"), + text("set"), space, - word("-"), + text("-"), space, - word("X"), + text("X"), space, - backt("`"), - backt("head"), - q(shqBackt, space), - backt("-1"), - q(shqBackt, space), - backt("$${PAGE}qt"), - word("`"), + backt(text("`")), + backt(text("head")), + backt(space), + backt(text("-1")), + backt(space), + backt(text("$${PAGE}qt")), + text("`"), semicolon, space) check("`\"one word\"`", - backt("`"), - q(shqBacktDquot, word("\"")), - q(shqBacktDquot, word("one word")), - q(shqBackt, word("\"")), - word("`")) + backt(text("`")), + backtDquot(text("\"")), + backtDquot(text("one word")), + backt(text("\"")), + text("`")) check("$$var \"$$var\" '$$var' `$$var`", - word("$$var"), + text("$$var"), space, - dquot("\""), - dquot("$$var"), - word("\""), - space, - squot("'"), - squot("$$var"), - word("'"), - space, - backt("`"), - backt("$$var"), - word("`")) + dquot(text("\"")), + dquot(text("$$var")), + text("\""), + space, + squot(text("'")), + squot(text("$$var")), + text("'"), + space, + backt(text("`")), + backt(text("$$var")), + text("`")) check("\"`'echo;echo'`\"", - q(shqDquot, word("\"")), - q(shqDquotBackt, word("`")), - q(shqDquotBacktSquot, word("'")), - q(shqDquotBacktSquot, word("echo;echo")), - q(shqDquotBackt, word("'")), - q(shqDquot, word("`")), - q(shqPlain, word("\""))) + dquot(text("\"")), + dquotBackt(text("`")), + dquotBacktSquot(text("'")), + dquotBacktSquot(text("echo;echo")), + dquotBackt(text("'")), + dquot(text("`")), + text("\"")) check("catecho`", - q(shqPlain, word("var=")), - q(shqBackt, word("`")), - q(shqBackt, word("echo")), - q(shqBackt, semicolon), - q(shqBackt, word("echo")), - q(shqBackt, operator("|")), - q(shqBackt, word("echo")), - q(shqBackt, operator("&")), - q(shqBackt, word("echo")), - q(shqBackt, operator("||")), - q(shqBackt, word("echo")), - q(shqBackt, operator("&&")), - q(shqBackt, word("echo")), - q(shqBackt, operator(">")), - q(shqBackt, word("echo")), - q(shqPlain, word("`"))) + text("var="), + backt(text("`")), + backt(text("echo")), + backt(semicolon), + backt(text("echo")), + backt(operator("|")), + backt(text("echo")), + backt(operator("&")), + backt(text("echo")), + backt(operator("||")), + backt(text("echo")), + backt(operator("&&")), + backt(text("echo")), + backt(operator(">")), + backt(text("echo")), + text("`")) check("# comment", - token(shtComment, "# comment", shqPlain)) + comment("# comment")) check("no#comment", - word("no#comment")) + text("no#comment")) check("`# comment`continue", - token(shtWord, "`", shqBackt), - token(shtComment, "# comment", shqBackt), - token(shtWord, "`", shqPlain), - token(shtWord, "continue", shqPlain)) + backt(text("`")), + backt(comment("# comment")), + text("`"), + text("continue")) check("`no#comment`continue", - token(shtWord, "`", shqBackt), - token(shtWord, "no#comment", shqBackt), - token(shtWord, "`", shqPlain), - token(shtWord, "continue", shqPlain)) + backt(text("`")), + backt(text("no#comment")), + text("`"), + text("continue")) check("var=`tr 'A-Z' 'a-z'`", - token(shtWord, "var=", shqPlain), - token(shtWord, "`", shqBackt), - token(shtWord, "tr", shqBackt), - token(shtSpace, " ", shqBackt), - token(shtWord, "'", shqBacktSquot), - token(shtWord, "A-Z", shqBacktSquot), - token(shtWord, "'", shqBackt), - token(shtSpace, " ", shqBackt), - token(shtWord, "'", shqBacktSquot), - token(shtWord, "a-z", shqBacktSquot), - token(shtWord, "'", shqBackt), - token(shtWord, "`", shqPlain)) + text("var="), + backt(text("`")), + backt(text("tr")), + backt(space), + backtSquot(text("'")), + backtSquot(text("A-Z")), + backt(text("'")), + backt(space), + backtSquot(text("'")), + backtSquot(text("a-z")), + backt(text("'")), + text("`")) check("var=\"`echo \"\\`echo foo\\`\"`\"", - token(shtWord, "var=", shqPlain), - token(shtWord, "\"", shqDquot), - token(shtWord, "`", shqDquotBackt), - token(shtWord, "echo", shqDquotBackt), - token(shtSpace, " ", shqDquotBackt), - token(shtWord, "\"", shqDquotBacktDquot), - token(shtWord, "\\`echo foo\\`", shqDquotBacktDquot), // One token, since it doesn't influence parsing. - token(shtWord, "\"", shqDquotBackt), - token(shtWord, "`", shqDquot), - token(shtWord, "\"", shqPlain)) + text("var="), + dquot(text("\"")), + dquotBackt(text("`")), + dquotBackt(text("echo")), + dquotBackt(space), + dquotBacktDquot(text("\"")), + dquotBacktDquot(text("\\`echo foo\\`")), // One atom, since it doesn't influence parsing. + dquotBackt(text("\"")), + dquot(text("`")), + text("\"")) check("if cond1; then action1; elif cond2; then action2; else action3; fi", - word("if"), space, word("cond1"), semicolon, space, - word("then"), space, word("action1"), semicolon, space, - word("elif"), space, word("cond2"), semicolon, space, - word("then"), space, word("action2"), semicolon, space, - word("else"), space, word("action3"), semicolon, space, - word("fi")) - - if false { - check("$$(cat)", - token(shtWord, "$$(", shqSubsh), - token(shtWord, "cat", shqSubsh), - token(shtWord, ")", shqPlain)) - - check("$$(cat 'file')", - token(shtWord, "$$(", shqSubsh), - token(shtWord, "cat", shqSubsh), - token(shtSpace, " ", shqSubsh), - token(shtWord, "'", shqSubshSquot), - token(shtWord, "file", shqSubshSquot), - token(shtWord, "'", shqSubsh), - token(shtWord, ")", shqPlain)) - } + text("if"), space, text("cond1"), semicolon, space, + text("then"), space, text("action1"), semicolon, space, + text("elif"), space, text("cond2"), semicolon, space, + text("then"), space, text("action2"), semicolon, space, + text("else"), space, text("action3"), semicolon, space, + text("fi")) + + check("$$(cat)", + subsh(subshell), + subsh(text("cat")), + text(")")) + + check("$$(cat 'file')", + subsh(subshell), + subsh(text("cat")), + subsh(space), + subshSquot(text("'")), + subshSquot(text("file")), + subsh(text("'")), + text(")")) + + check("$$(# comment) arg", + subsh(subshell), + subsh(comment("# comment")), + text(")"), + space, + text("arg")) + + check("$$(echo \"first\" 'second')", + subsh(subshell), + subsh(text("echo")), + subsh(space), + subshDquot(text("\"")), + subshDquot(text("first")), + subsh(text("\"")), + subsh(space), + subshSquot(text("'")), + subshSquot(text("second")), + subsh(text("'")), + text(")")) } -func (s *Suite) Test_Shtokenizer_ShAtom__quoting(c *check.C) { +func (s *Suite) Test_ShTokenizer_ShAtom__quoting(c *check.C) { checkQuotingChange := func(input, expectedOutput string) { p := NewShTokenizer(dummyLine, input, false) q := shqPlain @@ -372,105 +420,158 @@ func (s *Suite) Test_Shtokenizer_ShAtom_ func (s *Suite) Test_ShTokenizer_ShToken(c *check.C) { t := s.Init(c) - check := func(str string, expected ...*ShToken) { + // testRest ensures that the given string is parsed to the expected + // tokens, and returns the remaining text. + testRest := func(str string, expected ...string) string { + p := NewShTokenizer(dummyLine, str, false) + for _, exp := range expected { + c.Check(p.ShToken().MkText, equals, exp) + } + return p.Rest() + } + test := func(str string, expected ...string) { p := NewShTokenizer(dummyLine, str, false) for _, exp := range expected { - c.Check(p.ShToken(), deepEquals, exp) + c.Check(p.ShToken().MkText, equals, exp) } c.Check(p.Rest(), equals, "") t.CheckOutputEmpty() } + checkNil := func(str string) { + p := NewShTokenizer(dummyLine, str, false) + c.Check(p.ShToken(), check.IsNil) + c.Check(p.Rest(), equals, "") + t.CheckOutputEmpty() + } - check("", - nil) + checkNil("") + checkNil(" ") + rest := testRest("\t\t\t\n\n\n\n\t ", + "\n", + "\n", // TODO: Why three separators? One should be enough. What does the grammar say? + "\n") + c.Check(rest, equals, "\n\t ") // TODO: Why is the newline still here? + + test("echo", + "echo") + + test("`cat file`", + "`cat file`") + + test("PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"", + "PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"") + + test("echo hello, world", + "echo", + "hello,", + "world") + + test("if cond1; then action1; elif cond2; then action2; else action3; fi", + "if", "cond1", ";", "then", + "action1", ";", + "elif", "cond2", ";", "then", + "action2", ";", + "else", "action3", ";", + "fi") + + test("PATH=/nonexistent env PATH=${PATH:Q} true", + "PATH=/nonexistent", + "env", + "PATH=${PATH:Q}", + "true") - check("echo", - NewShToken("echo", - NewShAtom(shtWord, "echo", shqPlain))) - - check("`cat file`", - NewShToken("`cat file`", - NewShAtom(shtWord, "`", shqBackt), - NewShAtom(shtWord, "cat", shqBackt), - NewShAtom(shtSpace, " ", shqBackt), - NewShAtom(shtWord, "file", shqBackt), - NewShAtom(shtWord, "`", shqPlain))) - - check("PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"", - NewShToken("PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"", - NewShAtom(shtWord, "PAGES=", shqPlain), - NewShAtom(shtWord, "\"", shqDquot), - NewShAtom(shtWord, "`", shqDquotBackt), - NewShAtom(shtWord, "ls", shqDquotBackt), - NewShAtom(shtSpace, " ", shqDquotBackt), - NewShAtom(shtWord, "-1", shqDquotBackt), - NewShAtom(shtSpace, " ", shqDquotBackt), - NewShAtom(shtOperator, "|", shqDquotBackt), - NewShAtom(shtSpace, " ", shqDquotBackt), - NewShAtomVaruse("${SED}", shqDquotBackt, "SED"), - NewShAtom(shtSpace, " ", shqDquotBackt), - NewShAtom(shtWord, "-e", shqDquotBackt), - NewShAtom(shtSpace, " ", shqDquotBackt), - NewShAtom(shtWord, "'", shqDquotBacktSquot), - NewShAtom(shtWord, "s,3qt$$,3,", shqDquotBacktSquot), - NewShAtom(shtWord, "'", shqDquotBackt), - NewShAtom(shtWord, "`", shqDquot), - NewShAtom(shtWord, "\"", shqPlain))) - - check("echo hello, world", - NewShToken("echo", - NewShAtom(shtWord, "echo", shqPlain)), - NewShToken("hello,", - NewShAtom(shtWord, "hello,", shqPlain)), - NewShToken("world", - NewShAtom(shtWord, "world", shqPlain))) + test("id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)", + "id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)") - check("if cond1; then action1; elif cond2; then action2; else action3; fi", - NewShToken("if", NewShAtom(shtWord, "if", shqPlain)), - NewShToken("cond1", NewShAtom(shtWord, "cond1", shqPlain)), - NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)), - NewShToken("then", NewShAtom(shtWord, "then", shqPlain)), - NewShToken("action1", NewShAtom(shtWord, "action1", shqPlain)), - NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)), - NewShToken("elif", NewShAtom(shtWord, "elif", shqPlain)), - NewShToken("cond2", NewShAtom(shtWord, "cond2", shqPlain)), - NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)), - NewShToken("then", NewShAtom(shtWord, "then", shqPlain)), - NewShToken("action2", NewShAtom(shtWord, "action2", shqPlain)), - NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)), - NewShToken("else", NewShAtom(shtWord, "else", shqPlain)), - NewShToken("action3", NewShAtom(shtWord, "action3", shqPlain)), - NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)), - NewShToken("fi", NewShAtom(shtWord, "fi", shqPlain))) - - check("PATH=/nonexistent env PATH=${PATH:Q} true", - NewShToken("PATH=/nonexistent", NewShAtom(shtWord, "PATH=/nonexistent", shqPlain)), - NewShToken("env", NewShAtom(shtWord, "env", shqPlain)), - NewShToken("PATH=${PATH:Q}", - NewShAtom(shtWord, "PATH=", shqPlain), - NewShAtomVaruse("${PATH:Q}", shqPlain, "PATH", "Q")), - NewShToken("true", NewShAtom(shtWord, "true", shqPlain))) - - if false { // Don't know how to tokenize this correctly. - check("id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)", - NewShToken("id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)", - NewShAtom(shtWord, "id=", shqPlain), - NewShAtom(shtWord, "$$(", shqPlain), - NewShAtomVaruse("${AWK}", shqPlain, "AWK"))) + test("id=`${AWK} '{print}' < ${WRKSRC}/idfile`", + "id=`${AWK} '{print}' < ${WRKSRC}/idfile`") +} + +func (s *Suite) Test_ShTokenizer__examples_from_fuzzing(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("fuzzing.mk", + MkRcsID, + "", + "pre-configure:", + + // Covers shAtomBacktDquot: return nil. + // These are nested backticks with double quotes, + // which should be avoided since POSIX marks them as unspecified. + "\t"+"`\"`", + + // Covers shAtomBacktSquot: return nil + "\t"+"`'$`", + + // Covers shAtomDquotBacktSquot: return nil + "\t"+"\"`'`y", + + // Covers shAtomDquotBackt: return nil + // FIXME: Pkglint must parse unescpaed dollar in the same way, everywhere. + "\t"+"\"`$|", + + // Covers shAtomDquotBacktDquot: return nil + // FIXME: Pkglint must support unlimited nesting. + "\t"+"\"`\"`", + + // Covers shAtomSubshDquot: return nil + "\t"+"$$(\"'", + + // Covers shAtomSubsh: case repl.AdvanceStr("`") + "\t"+"$$(`", + + // Covers shAtomSubshSquot: return nil + "\t"+"$$('$)", + + // Covers shAtomDquotBackt: case repl.AdvanceRegexp("^#[^`]*") + "\t"+"\"`# comment") + + mklines.Check() + + // Just good that these redundant error messages don't occur every day. + t.CheckOutputLines( + "WARN: fuzzing.mk:4: Pkglint parse error in ShTokenizer.ShAtom at \"`\" (quoting=bd).", + "WARN: fuzzing.mk:4: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}", + + "WARN: fuzzing.mk:5: Pkglint parse error in ShTokenizer.ShAtom at \"$`\" (quoting=bs).", + "WARN: fuzzing.mk:5: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}", + "WARN: fuzzing.mk:5: Pkglint parse error in MkLine.Tokenize at \"$`\".", + + "WARN: fuzzing.mk:6: Pkglint parse error in ShTokenizer.ShAtom at \"`y\" (quoting=dbs).", + "WARN: fuzzing.mk:6: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}", + + "WARN: fuzzing.mk:7: Pkglint parse error in ShTokenizer.ShAtom at \"$|\" (quoting=db).", + "WARN: fuzzing.mk:7: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}", + "WARN: fuzzing.mk:7: Pkglint parse error in MkLine.Tokenize at \"$|\".", + + "WARN: fuzzing.mk:8: Pkglint parse error in ShTokenizer.ShAtom at \"`\" (quoting=dbd).", + "WARN: fuzzing.mk:8: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}", + + "WARN: fuzzing.mk:9: Pkglint parse error in ShTokenizer.ShAtom at \"'\" (quoting=Sd).", + "WARN: fuzzing.mk:9: Invoking subshells via $(...) is not portable enough.", + + "WARN: fuzzing.mk:10: Pkglint parse error in ShTokenizer.ShAtom at \"`\" (quoting=S).", + "WARN: fuzzing.mk:10: Invoking subshells via $(...) is not portable enough.", + + "WARN: fuzzing.mk:11: Pkglint parse error in ShTokenizer.ShAtom at \"$)\" (quoting=Ss).", + "WARN: fuzzing.mk:11: Invoking subshells via $(...) is not portable enough.", + "WARN: fuzzing.mk:11: Pkglint parse error in MkLine.Tokenize at \"$)\".", + + "WARN: fuzzing.mk:12: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}") +} + +func (s *Suite) Test_ShTokenizer__fuzzing(c *check.C) { + t := s.Init(c) + + fuzzer := NewFuzzer() + fuzzer.Char("\"'`$();|_#", 10) + fuzzer.Range('a', 'z', 5) + + defer fuzzer.CheckOk() + for i := 0; i < 1000; i++ { + tokenizer := NewShTokenizer(dummyLine, fuzzer.Generate(50), false) + tokenizer.ShAtoms() + t.Output() // Discard the output, only react on fatal errors. } - check("id=`${AWK} '{print}' < ${WRKSRC}/idfile`", - NewShToken("id=`${AWK} '{print}' < ${WRKSRC}/idfile`", - NewShAtom(shtWord, "id=", shqPlain), - NewShAtom(shtWord, "`", shqBackt), - NewShAtomVaruse("${AWK}", shqBackt, "AWK"), - NewShAtom(shtSpace, " ", shqBackt), - NewShAtom(shtWord, "'", shqBacktSquot), - NewShAtom(shtWord, "{print}", shqBacktSquot), - NewShAtom(shtWord, "'", shqBackt), - NewShAtom(shtSpace, " ", shqBackt), - NewShAtom(shtOperator, "<", shqBackt), - NewShAtom(shtSpace, " ", shqBackt), - NewShAtomVaruse("${WRKSRC}", shqBackt, "WRKSRC"), - NewShAtom(shtWord, "/idfile", shqBackt), - NewShAtom(shtWord, "`", shqPlain))) + fuzzer.Ok() } Index: pkgsrc/pkgtools/pkglint/files/vartype_test.go diff -u pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.7 pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.8 --- pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.7 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/vartype_test.go Wed Oct 3 22:27:53 2018 @@ -23,7 +23,7 @@ func (s *Suite) Test_Vartype_EffectivePe } } -func (s *Suite) Test_VarChecker_HasEnum(c *check.C) { +func (s *Suite) Test_BasicType_HasEnum(c *check.C) { vc := enum("catinstall middle maninstall") c.Check(vc.HasEnum("catinstall"), equals, true) @@ -31,7 +31,7 @@ func (s *Suite) Test_VarChecker_HasEnum( c.Check(vc.HasEnum("maninstall"), equals, true) } -func (s *Suite) Test_AclPermissions_Contains(c *check.C) { +func (s *Suite) Test_ACLPermissions_Contains(c *check.C) { perms := aclpAllRuntime c.Check(perms.Contains(aclpAllRuntime), equals, true) @@ -39,7 +39,7 @@ func (s *Suite) Test_AclPermissions_Cont c.Check(perms.Contains(aclpUseLoadtime), equals, false) } -func (s *Suite) Test_AclPermissions_String(c *check.C) { +func (s *Suite) Test_ACLPermissions_String(c *check.C) { c.Check(ACLPermissions(0).String(), equals, "none") c.Check(aclpAll.String(), equals, "set, set-default, append, use-loadtime, use") c.Check(aclpUnknown.String(), equals, "unknown") Index: pkgsrc/pkgtools/pkglint/files/mkline.go diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.37 pkgsrc/pkgtools/pkglint/files/mkline.go:1.38 --- pkgsrc/pkgtools/pkglint/files/mkline.go:1.37 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mkline.go Wed Oct 3 22:27:53 2018 @@ -4,7 +4,6 @@ package main import ( "fmt" - "netbsd.org/pkglint/regex" "netbsd.org/pkglint/trace" "path" "strings" @@ -327,12 +326,12 @@ func (mkline *MkLineImpl) ValueTokens() func (mkline *MkLineImpl) WithoutMakeVariables(value string) string { valueNovar := value for { - var m []string // TODO: properly parse nested variables - m, valueNovar = regex.ReplaceFirst(valueNovar, `\$\{[^{}]*\}`, "") - if m == nil { + replaced := replaceFirst(valueNovar, `\$\{[^{}]*\}`, "") + if replaced == valueNovar { return valueNovar } + valueNovar = replaced } } @@ -357,7 +356,7 @@ func (mkline *MkLineImpl) ResolveVarsInR tmp = strings.Replace(tmp, "${PHPPKGSRCDIR}", G.Pkgsrc.Latest("lang", `^php[0-9]+$`, "../../lang/$0"), -1) } if contains(tmp, "${SUSE_DIR_PREFIX}") { - suseDirPrefix := G.Pkgsrc.Latest("emulators", `^(suse[0-9]+)_base`, "$1") + suseDirPrefix := G.Pkgsrc.Latest("emulators", `^(suse[0-9]+)_base$`, "$1") tmp = strings.Replace(tmp, "${SUSE_DIR_PREFIX}", suseDirPrefix, -1) } if contains(tmp, "${PYPKGSRCDIR}") { @@ -372,14 +371,14 @@ func (mkline *MkLineImpl) ResolveVarsInR } if adjustDepth { - if m, pkgpath := match1(tmp, `^\.\./\.\./([^.].*)$`); m { - tmp = pkgsrcdir + "/" + pkgpath + if hasPrefix(tmp, "../../") && !hasPrefix(tmp[6:], ".") { + tmp = pkgsrcdir + "/" + tmp[6:] } } tmp = cleanpath(tmp) - if trace.Tracing { + if trace.Tracing && relativePath != tmp { trace.Step2("resolveVarsInRelativePath: %q => %q", relativePath, tmp) } return tmp @@ -584,7 +583,7 @@ func (mkline *MkLineImpl) VariableNeedsQ // TODO: merge with determineUsedVariables func (mkline *MkLineImpl) ExtractUsedVariables(text string) []string { - re := regex.Compile(`^(?:[^\$]+|\$[\$*<>?@]|\$\{([.0-9A-Z_a-z]+)(?::(?:[^\${}]|\$[^{])+)?\})`) + re := G.res.Compile(`^(?:[^\$]+|\$[\$*<>?@]|\$\{([.0-9A-Z_a-z]+)(?::(?:[^\${}]|\$[^{])+)?\})`) rest := text var result []string for { @@ -635,7 +634,7 @@ func (mkline *MkLineImpl) DetermineUsedV } rest = rest[min:] - m := regex.Compile(`(?:\$\{|\$\(|defined\(|empty\()([*+\-.0-9A-Z_a-z]+)[:})]`).FindStringSubmatchIndex(rest) + m := G.res.Compile(`(?:\$\{|\$\(|defined\(|empty\()([*+\-.0-9A-Z_a-z]+)[:})]`).FindStringSubmatchIndex(rest) if m == nil { return } @@ -918,9 +917,17 @@ func (ind *Indentation) TrackAfter(mklin switch directive { case "if": // For multiple-inclusion guards, the indentation stays at the same level. - if m, varname := match1(args, `^!defined\(([\w]+_MK)\)$`); m { - ind.AddVar(varname) - } else { + guard := false + if hasPrefix(args, "!defined") && hasSuffix(args, "_MK)") { + if hasPrefix(args, "!defined(") && hasSuffix(args, ")") { + varname := args[9 : len(args)-1] + if varname != "" && isalnum(varname) { + ind.AddVar(varname) + guard = true + } + } + } + if !guard { ind.top().depth += 2 } @@ -1055,5 +1062,27 @@ func MatchVarassign(text string) (m, com } func MatchMkInclude(text string) (m bool, indentation, directive, filename string) { - return match3(text, `^\.(\s*)(s?include)\s+\"([^\"]+)\"\s*(?:#.*)?$`) + repl := G.NewPrefixReplacer(text) + if repl.AdvanceStr(".") { + if repl.AdvanceHspace() { + indentation = repl.Str() + } + if repl.AdvanceStr("include") || repl.AdvanceStr("sinclude") { + directive = repl.Str() + repl.AdvanceHspace() + if repl.AdvanceByte('"') { + if repl.AdvanceBytesFunc(func(c byte) bool { return c != '"' }) { + filename = repl.Str() + if repl.AdvanceByte('"') { + repl.AdvanceHspace() + if repl.EOF() || repl.PeekByte() == '#' { + m = true + return + } + } + } + } + } + } + return false, "", "", "" } Index: pkgsrc/pkgtools/pkglint/files/pkglint.go diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.37 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.38 --- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.37 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/pkglint.go Wed Oct 3 22:27:53 2018 @@ -5,6 +5,7 @@ import ( "netbsd.org/pkglint/getopt" "netbsd.org/pkglint/histogram" "netbsd.org/pkglint/regex" + "netbsd.org/pkglint/textproc" "netbsd.org/pkglint/trace" "os" "os/user" @@ -49,8 +50,16 @@ type Pkglint struct { logOut *SeparatorWriter logErr *SeparatorWriter - loghisto *histogram.Histogram - loaded *histogram.Histogram + loghisto *histogram.Histogram + loaded *histogram.Histogram + res regex.Registry + fileCache *FileCache +} + +func NewPkglint() Pkglint { + return Pkglint{ + res: regex.NewRegistry(), + fileCache: NewFileCache(100)} } type CmdOpts struct { @@ -106,13 +115,15 @@ type Hash struct { // G is the abbreviation for "global state"; // it is the only global variable in this Go package -var G Pkglint +var G = NewPkglint() + +var exit = os.Exit // Indirect access, to allow main() to be tested. func main() { G.logOut = NewSeparatorWriter(os.Stdout) G.logErr = NewSeparatorWriter(os.Stderr) trace.Out = os.Stdout - os.Exit(G.Main(os.Args...)) + exit(G.Main(os.Args...)) } // Main runs the main program with the given arguments. @@ -139,21 +150,22 @@ func (pkglint *Pkglint) Main(argv ...str if pkglint.opts.Profiling { f, err := os.Create("pkglint.pprof") if err != nil { - dummyLine.Fatalf("Cannot create profiling file: %s", err) + G.Panicf("Cannot create profiling file: %s", err) } defer f.Close() pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() - regex.Profiling = true + pkglint.res.Profiling() pkglint.loghisto = histogram.New() pkglint.loaded = histogram.New() defer func() { pkglint.logOut.Write("") pkglint.loghisto.PrintStats("loghisto", pkglint.logOut.out, -1) - regex.PrintStats(pkglint.logOut.out) + G.res.PrintStats(pkglint.logOut.out) pkglint.loaded.PrintStats("loaded", pkglint.logOut.out, 10) + pkglint.logOut.WriteLine(fmt.Sprintf("fileCache: %d hits, %d misses", pkglint.fileCache.hits, pkglint.fileCache.misses)) }() } @@ -170,7 +182,7 @@ func (pkglint *Pkglint) Main(argv ...str } relTopdir := findPkgsrcTopdir(firstArg) if relTopdir == "" { - dummyLine.Fatalf("%q is not inside a pkgsrc tree.", firstArg) + G.Panicf("%q is not inside a pkgsrc tree.", firstArg) } pkglint.Pkgsrc = NewPkgsrc(firstArg + "/" + relTopdir) @@ -179,7 +191,7 @@ func (pkglint *Pkglint) Main(argv ...str currentUser, err := user.Current() if err == nil { // On Windows, this is `Computername\Username`. - pkglint.CurrentUsername = regex.Compile(`^.*\\`).ReplaceAllString(currentUser.Username, "") + pkglint.CurrentUsername = replaceAll(currentUser.Username, `^.*\\`, "") } for len(pkglint.Todo) != 0 { @@ -332,6 +344,27 @@ func (pkglint *Pkglint) CheckDirent(fnam } } +func (pkglint *Pkglint) Panicf(format string, args ...interface{}) { + prefix := ifelseStr(G.opts.GccOutput, llFatal.GccName, llFatal.TraditionalName) + pkglint.logErr.Write(prefix + ": " + fmt.Sprintf(format, args...) + "\n") + panic(pkglintFatal{}) +} + +// Assertf checks that the condition is true. Otherwise it terminates the +// process with a fatal error message, prefixed with "Pkglint internal error". +// +// This method must only be used for programming errors. +// For runtime errors, use Panicf. +func (pkglint *Pkglint) Assertf(cond bool, format string, args ...interface{}) { + if !cond { + pkglint.Panicf("Pkglint internal error: "+format, args...) + } +} + +func (pkglint *Pkglint) NewPrefixReplacer(s string) *textproc.PrefixReplacer { + return textproc.NewPrefixReplacer(s, &pkglint.res) +} + // Returns the pkgsrc top-level directory, relative to the given file or directory. func findPkgsrcTopdir(fname string) string { for _, dir := range [...]string{".", "..", "../..", "../../.."} { @@ -342,9 +375,9 @@ func findPkgsrcTopdir(fname string) stri return "" } -func resolveVariableRefs(text string) string { - if trace.Tracing { - defer trace.Call1(text)() +func resolveVariableRefs(text string) (resolved string) { + if !contains(text, "${") { + return text } visited := make(map[string]bool) // To prevent endless loops @@ -354,14 +387,8 @@ func resolveVariableRefs(text string) st if !visited[varname] { visited[varname] = true if G.Pkg != nil { - switch varname { - case "KRB5_TYPE": - return "heimdal" - case "PGSQL_VERSION": - return "95" - } - if mkline := G.Pkg.vars.FirstDefinition(varname); mkline != nil { - return mkline.Value() + if value, ok := G.Pkg.vars.Value(varname); ok { + return value } } if G.Mk != nil { @@ -375,8 +402,11 @@ func resolveVariableRefs(text string) st str := text for { - replaced := regex.Compile(`\$\{([\w.]+)\}`).ReplaceAllStringFunc(str, replacer) + replaced := replaceAllFunc(str, `\$\{([\w.]+)\}`, replacer) if replaced == str { + if trace.Tracing && str != text { + trace.Stepf("resolveVariableRefs %q => %q", text, replaced) + } return replaced } str = replaced @@ -401,7 +431,7 @@ func ChecklinesDescr(lines []Line) { for _, line := range lines { CheckLineLength(line, 80) CheckLineTrailingWhitespace(line) - CheckLineValidCharacters(line, `[\t -~]`) + CheckLineValidCharacters(line) if contains(line.Text, "${") { line.Notef("Variables are not expanded in the DESCR file.") } @@ -453,7 +483,7 @@ func ChecklinesMessage(lines []Line) { for _, line := range lines { CheckLineLength(line, 80) CheckLineTrailingWhitespace(line) - CheckLineValidCharacters(line, `[\t -~]`) + CheckLineValidCharacters(line) } if lastLine := lines[len(lines)-1]; lastLine.Text != hline { fix := lastLine.Autofix() @@ -516,10 +546,16 @@ func (pkglint *Pkglint) Checkfile(fname return } - pkglint.checkExecutable(st, fname) + pkglint.checkExecutable(st) + pkglint.checkMode(fname, st.Mode()) +} +// checkMode checks a directory entry based on its file name and its mode +// (regular file, directory, symlink). +func (pkglint *Pkglint) checkMode(fname string, mode os.FileMode) { + basename := path.Base(fname) switch { - case st.Mode().IsDir(): + case mode.IsDir(): switch { case basename == "files" || basename == "patches" || isIgnoredFilename(basename): // Ok @@ -529,12 +565,12 @@ func (pkglint *Pkglint) Checkfile(fname NewLineWhole(fname).Warnf("Unknown directory name.") } - case st.Mode()&os.ModeSymlink != 0: + case mode&os.ModeSymlink != 0: if !hasPrefix(basename, "work") { NewLineWhole(fname).Warnf("Unknown symlink name.") } - case !st.Mode().IsRegular(): + case !mode.IsRegular(): NewLineWhole(fname).Errorf("Only files and directories are allowed in pkgsrc.") case basename == "ALTERNATIVES": @@ -609,9 +645,6 @@ func (pkglint *Pkglint) Checkfile(fname } } - case basename == "TODO" || basename == "README": - // Ok - case hasPrefix(basename, "CHANGES-"): // This only checks the file, but doesn't register the changes globally. _ = pkglint.Pkgsrc.loadDocChangesFromFile(fname) @@ -620,7 +653,9 @@ func (pkglint *Pkglint) Checkfile(fname // Skip case basename == "spec": - // Ok in regression tests + if !hasPrefix(G.Pkgsrc.ToRel(fname), "regress/") { + NewLineWhole(fname).Warnf("Only packages in regress/ may have spec files.") + } default: NewLineWhole(fname).Warnf("Unexpected file found.") @@ -630,7 +665,8 @@ func (pkglint *Pkglint) Checkfile(fname } } -func (pkglint *Pkglint) checkExecutable(st os.FileInfo, fname string) { +func (pkglint *Pkglint) checkExecutable(st os.FileInfo) { + fname := st.Name() if st.Mode().IsRegular() && st.Mode().Perm()&0111 != 0 && !isCommitted(fname) { line := NewLine(fname, 0, "", nil) fix := line.Autofix() @@ -643,7 +679,7 @@ func (pkglint *Pkglint) checkExecutable( fix.Custom(func(printAutofix, autofix bool) { fix.Describef(0, "Clearing executable bits") if autofix { - if err := os.Chmod(line.Filename, st.Mode()&^0111); err != nil { + if err := os.Chmod(fname, st.Mode()&^0111); err != nil { line.Errorf("Cannot clear executable bits: %s", err) } } @@ -714,6 +750,11 @@ func (pkglint *Pkglint) Tool(command str return } +// ToolByVarname looks up the tool by its variable name, e.g. "SED". +// +// The returned tool may come either from the current Makefile or the +// current package. It is not guaranteed to be usable; that must be +// checked by the calling code. func (pkglint *Pkglint) ToolByVarname(varname string, time ToolTime) *Tool { var tool *Tool Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.41 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.42 --- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.41 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mkline_test.go Wed Oct 3 22:27:53 2018 @@ -2,7 +2,7 @@ package main import "gopkg.in/check.v1" -func (s *Suite) Test_VaralignBlock_Check_autofix(c *check.C) { +func (s *Suite) Test_VaralignBlock_Check__autofix(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wspace", "--show-autofix") @@ -68,7 +68,7 @@ func (s *Suite) Test_VaralignBlock_Check "NOTE: file.mk:3: This variable value should be aligned to column 9.") } -func (s *Suite) Test_VaralignBlock_Check_longest_line_no_space(c *check.C) { +func (s *Suite) Test_VaralignBlock_Check__longest_line_no_space(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wspace") @@ -91,7 +91,7 @@ func (s *Suite) Test_VaralignBlock_Check "NOTE: file.mk:4: This variable value should be aligned to column 33.") } -func (s *Suite) Test_VaralignBlock_Check_only_spaces(c *check.C) { +func (s *Suite) Test_VaralignBlock_Check__only_spaces(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wspace") @@ -219,7 +219,7 @@ func (s *Suite) Test_NewMkLine__autofix_ } // Guessing the variable type works for both plain and parameterized variable names. -func (s *Suite) Test_MkLine_VariableType_varparam(c *check.C) { +func (s *Suite) Test_Pkgsrc_VariableType__varparam(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -242,13 +242,13 @@ func (s *Suite) Test_VarUseContext_Strin vartype := G.Pkgsrc.VariableType("PKGNAME") vuc := &VarUseContext{vartype, vucTimeUnknown, vucQuotBackt, false} - c.Check(vuc.String(), equals, "(PkgName time:unknown quoting:backt wordpart:false)") + c.Check(vuc.String(), equals, "(Pkgname time:unknown quoting:backt wordpart:false)") } // In variable assignments, a plain '#' introduces a line comment, unless // it is escaped by a backslash. In shell commands, on the other hand, it // is interpreted literally. -func (s *Suite) Test_NewMkLine_numbersign(c *check.C) { +func (s *Suite) Test_NewMkLine__number_sign(c *check.C) { t := s.Init(c) mklineVarassignEscaped := t.NewMkLine("fname", 1, "SED_CMD=\t's,\\#,hash,g'") @@ -273,7 +273,7 @@ func (s *Suite) Test_NewMkLine_numbersig "WARN: fname:1: The # character starts a comment.") } -func (s *Suite) Test_NewMkLine_leading_space(c *check.C) { +func (s *Suite) Test_NewMkLine__leading_space(c *check.C) { t := s.Init(c) _ = t.NewMkLine("rubyversion.mk", 427, " _RUBYVER=\t2.15") @@ -311,7 +311,7 @@ func (s *Suite) Test_MkLines_Check__extr "NOTE: options.mk:11: You can use \"../build\" instead of \"${WRKSRC}/../build\".") } -func (s *Suite) Test_MkLine_variableNeedsQuoting__unknown_rhs(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__unknown_rhs(c *check.C) { t := s.Init(c) mkline := t.NewMkLine("fname", 1, "PKGNAME := ${UNKNOWN}") @@ -323,7 +323,7 @@ func (s *Suite) Test_MkLine_variableNeed c.Check(nq, equals, nqDontKnow) } -func (s *Suite) Test_MkLine_variableNeedsQuoting__append_URL_to_list_of_URLs(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__append_URL_to_list_of_URLs(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -341,7 +341,7 @@ func (s *Suite) Test_MkLine_variableNeed t.CheckOutputEmpty() // Up to pkglint 5.3.6, it warned about a missing :Q here, which was wrong. } -func (s *Suite) Test_MkLine_variableNeedsQuoting__append_list_to_list(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__append_list_to_list(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -355,7 +355,7 @@ func (s *Suite) Test_MkLine_variableNeed t.CheckOutputEmpty() } -func (s *Suite) Test_MkLine_variableNeedsQuoting__eval_shell(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__eval_shell(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -369,7 +369,7 @@ func (s *Suite) Test_MkLine_variableNeed "NOTE: builtin.mk:3: The :Q operator isn't necessary for ${BUILTIN_PKG.Xfixes} here.") } -func (s *Suite) Test_MkLine_variableNeedsQuoting__command_in_single_quotes(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_single_quotes(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -382,7 +382,7 @@ func (s *Suite) Test_MkLine_variableNeed "WARN: Makefile:3: Please use ${INSTALL:Q} instead of ${INSTALL} and make sure the variable appears outside of any quoting characters.") } -func (s *Suite) Test_MkLine_variableNeedsQuoting__command_in_command(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_command(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -401,7 +401,7 @@ func (s *Suite) Test_MkLine_variableNeed "WARN: Makefile:2: The exitcode of \"${FIND}\" at the left of the | operator is ignored.") } -func (s *Suite) Test_MkLine_variableNeedsQuoting__word_as_part_of_word(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__word_as_part_of_word(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -421,7 +421,7 @@ func (s *Suite) Test_MkLine_variableNeed // therefore no warning is issued in both these cases. // // Based on graphics/circos/Makefile. -func (s *Suite) Test_MkLine_variableNeedsQuoting__command_as_command_argument(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_as_command_argument(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -441,7 +441,7 @@ func (s *Suite) Test_MkLine_variableNeed } // Based on mail/mailfront/Makefile. -func (s *Suite) Test_MkLine_variableNeedsQuoting__URL_as_part_of_word_in_list(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__URL_as_part_of_word_in_list(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -460,7 +460,7 @@ func (s *Suite) Test_MkLine_variableNeed // modifier. // // Based on www/firefox31/xpi.mk. -func (s *Suite) Test_MkLine_variableNeedsQuoting__command_in_subshell(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_subshell(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -483,7 +483,7 @@ func (s *Suite) Test_MkLine_variableNeed // LDFLAGS (and even more so CPPFLAGS and CFLAGS) may contain special // shell characters like quotes or backslashes. Therefore, quoting them // correctly is more tricky than with other variables. -func (s *Suite) Test_MkLine_variableNeedsQuoting__LDFLAGS_in_single_quotes(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__LDFLAGS_in_single_quotes(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -506,7 +506,7 @@ func (s *Suite) Test_MkLine_variableNeed // requires the variable to be declared as "lkSpace". // In this case it doesn't matter though since each option is an identifier, // and these do not pose any quoting or escaping problems. -func (s *Suite) Test_MkLine_variableNeedsQuoting__package_options(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__package_options(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -542,7 +542,7 @@ func (s *Suite) Test_MkLines_Check__MAST "WARN: devel/catch/Makefile:5: HOMEPAGE should not be defined in terms of MASTER_SITEs.") } -func (s *Suite) Test_MkLine_variableNeedsQuoting__tool_in_quotes_in_subshell_in_shellwords(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_quotes_in_subshell_in_shellwords(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -559,7 +559,7 @@ func (s *Suite) Test_MkLine_variableNeed t.CheckOutputEmpty() } -func (s *Suite) Test_MkLine_variableNeedsQuoting__LDADD_in_BUILDLINK_TRANSFORM(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__LDADD_in_BUILDLINK_TRANSFORM(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -574,7 +574,7 @@ func (s *Suite) Test_MkLine_variableNeed "WARN: x11/qt5-qtbase/Makefile.common:1: Please use ${BUILDLINK_LDADD.dl:Q} instead of ${BUILDLINK_LDADD.dl:M*}.") } -func (s *Suite) Test_MkLine_variableNeedsQuoting__command_in_message(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_message(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -588,7 +588,7 @@ func (s *Suite) Test_MkLine_variableNeed t.CheckOutputEmpty() } -func (s *Suite) Test_MkLine_variableNeedsQuoting__guessed_list_variable_in_quotes(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__guessed_list_variable_in_quotes(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -604,7 +604,7 @@ func (s *Suite) Test_MkLine_variableNeed "WARN: audio/jack-rack/Makefile:3: The variable LADSPA_PLUGIN_PATH should be quoted as part of a shell word.") } -func (s *Suite) Test_MkLine_variableNeedsQuoting__list_in_list(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__list_in_list(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -618,7 +618,7 @@ func (s *Suite) Test_MkLine_variableNeed t.CheckOutputEmpty() // Don't warn about missing :Q modifiers. } -func (s *Suite) Test_MkLine_variableNeedsQuoting__PKGNAME_and_URL_list_in_URL_list(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__PKGNAME_and_URL_list_in_URL_list(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -633,7 +633,7 @@ func (s *Suite) Test_MkLine_variableNeed t.CheckOutputEmpty() // Don't warn about missing :Q modifiers. } -func (s *Suite) Test_MkLine_variableNeedsQuoting__tool_in_CONFIGURE_ENV(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_CONFIGURE_ENV(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -654,7 +654,7 @@ func (s *Suite) Test_MkLine_variableNeed "NOTE: Makefile:3: The :Q operator isn't necessary for ${TOOLS_TAR} here.") } -func (s *Suite) Test_MkLine_variableNeedsQuoting__backticks(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__backticks(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -682,7 +682,7 @@ func (s *Suite) Test_MkLine_variableNeed // the :Q modifier can be safely removed since pkgsrc will never support // having special characters in these directory names. // For guessed variable types be cautious and don't autofix them. -func (s *Suite) Test_MkLine_variableNeedsQuoting__only_remove_known(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__only_remove_known(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall", "--autofix") @@ -709,7 +709,7 @@ func (s *Suite) Test_MkLine_variableNeed // TODO: COMPILER_RPATH_FLAG and LINKER_RPATH_FLAG have different types // defined in vardefs.go; examine why. -func (s *Suite) Test_MkLine_variableNeedsQuoting__shellword_part(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__shellword_part(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall,no-space") @@ -729,7 +729,7 @@ func (s *Suite) Test_MkLine_variableNeed } // Tools, when used in a shell command, must not be quoted. -func (s *Suite) Test_MkLine_variableNeedsQuoting__tool_in_shell_command(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_shell_command(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall,no-space") @@ -747,7 +747,7 @@ func (s *Suite) Test_MkLine_variableNeed } // These examples from real pkgsrc end up in the final nqDontKnow case. -func (s *Suite) Test_MkLine_variableNeedsQuoting__uncovered_cases(c *check.C) { +func (s *Suite) Test_MkLine_VariableNeedsQuoting__uncovered_cases(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall,no-space") @@ -770,7 +770,7 @@ func (s *Suite) Test_MkLine_variableNeed "WARN: ~/Makefile:4: LINKER_RPATH_FLAG should not be evaluated at load time.") } -func (s *Suite) Test_MkLine_Pkgmandir(c *check.C) { +func (s *Suite) Test_ShellLine_CheckWord__PKGMANDIR(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -820,7 +820,7 @@ func (s *Suite) Test_MkLines_Check__shel "WARN: x11/lablgtk1/Makefile:2: Please use ${CC:Q} instead of ${CC}.") } -func (s *Suite) Test_MkLine_shell_varuse_in_backt_dquot(c *check.C) { +func (s *Suite) Test_MkLine__shell_varuse_in_backt_dquot(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -838,7 +838,7 @@ func (s *Suite) Test_MkLine_shell_varuse } // See PR 46570, Ctrl+F "3. In lang/perl5". -func (s *Suite) Test_MkLine_VariableType(c *check.C) { +func (s *Suite) Test_Pkgsrc_VariableType(c *check.C) { t := s.Init(c) t.SetupVartypes() Index: pkgsrc/pkgtools/pkglint/files/mklines.go diff -u pkgsrc/pkgtools/pkglint/files/mklines.go:1.31 pkgsrc/pkgtools/pkglint/files/mklines.go:1.32 --- pkgsrc/pkgtools/pkglint/files/mklines.go:1.31 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mklines.go Wed Oct 3 22:27:53 2018 @@ -112,8 +112,8 @@ func (mklines *MkLines) Check() { switch mkline.Varcanon() { case "PLIST_VARS": - value := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "") - for _, id := range value { + ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "") + for _, id := range ids { if !mklines.plistVarSkip && mklines.plistVarSet[id] == nil { mkline.Warnf("%q is added to PLIST_VARS, but PLIST.%s is not defined in this file.", id, id) } @@ -220,8 +220,8 @@ func (mklines *MkLines) DetermineDefined } case "PLIST_VARS": - value := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "") - for _, id := range value { + ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "") + for _, id := range ids { if trace.Tracing { trace.Step1("PLIST.%s is added to PLIST_VARS.", id) } @@ -255,8 +255,8 @@ func (mklines *MkLines) collectPlistVars if mkline.IsVarassign() { switch mkline.Varcanon() { case "PLIST_VARS": - value := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "") - for _, id := range value { + ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "") + for _, id := range ids { if containsVarRef(id) { mklines.plistVarSkip = true } else { @@ -310,9 +310,13 @@ func (mklines *MkLines) determineDocumen for _, mkline := range mklines.mklines { text := mkline.Text - words := splitOnSpace(text) + switch { + case hasPrefix(text, "#"): + words := splitOnSpace(text) + if len(words) <= 1 { + break + } - if 1 < len(words) && words[0] == "#" { commentLines++ parser := NewMkParser(mkline.Line, words[1], false) @@ -330,9 +334,8 @@ func (mklines *MkLines) determineDocumen if 1 < len(words) && words[1] == "Copyright" { relevant = false } - } - if text == "" { + case mkline.IsEmpty(): finish() } } Index: pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.31 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.32 --- pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.31 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go Wed Oct 3 22:27:53 2018 @@ -48,9 +48,9 @@ func (s *Suite) Test_VartypeCheck_Catego t := s.Init(c) vt := NewVartypeCheckTester(t, (*VartypeCheck).Category) - t.SetupFileLines("filesyscategory/Makefile", + t.CreateFileLines("filesyscategory/Makefile", "# empty") - t.SetupFileLines("wip/Makefile", + t.CreateFileLines("wip/Makefile", "# empty") vt.Varname("CATEGORIES") @@ -669,7 +669,7 @@ func (s *Suite) Test_VartypeCheck_Perms( } func (s *Suite) Test_VartypeCheck_Pkgname(c *check.C) { - vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).PkgName) + vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).Pkgname) vt.Varname("PKGNAME") vt.Values( @@ -754,7 +754,8 @@ func (s *Suite) Test_VartypeCheck_Machin "NetBSD-1.6.2-i386", "FreeBSD*", "FreeBSD-*", - "${LINUX}") + "${LINUX}", + "NetBSD-[0-1]*-*") vt.Output( "WARN: fname:1: \"linux-i386\" is not a valid platform pattern.", @@ -780,7 +781,8 @@ func (s *Suite) Test_VartypeCheck_Machin "i386 i586 i686 ia64 m68000 m68k m88k mips mips64 mips64eb mips64el mipseb mipsel mipsn32 "+ "mlrisc ns32k pc532 pmax powerpc powerpc64 rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+ "} for the hardware architecture part of ONLY_FOR_PLATFORM.", - "WARN: fname:5: \"FreeBSD*\" is not a valid platform pattern.") + "WARN: fname:5: \"FreeBSD*\" is not a valid platform pattern.", + "WARN: fname:8: Please use \"[0-1].*\" instead of \"[0-1]*\" as the version pattern.") } func (s *Suite) Test_VartypeCheck_PythonDependency(c *check.C) { @@ -1015,10 +1017,16 @@ func (s *Suite) Test_VartypeCheck_Versio vt.Values( "a*", "1.2/456", + "4*", + "?.??", + "1.[234]*", + "1.[2-7].*", "[0-9]*") vt.Output( "WARN: fname:11: Invalid version number pattern \"a*\".", - "WARN: fname:12: Invalid version number pattern \"1.2/456\".") + "WARN: fname:12: Invalid version number pattern \"1.2/456\".", + "WARN: fname:13: Please use \"4.*\" instead of \"4*\" as the version pattern.", + "WARN: fname:15: Please use \"1.[234].*\" instead of \"1.[234]*\" as the version pattern.") } func (s *Suite) Test_VartypeCheck_WrapperReorder(c *check.C) { Index: pkgsrc/pkgtools/pkglint/files/mklines_test.go diff -u pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.27 pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.28 --- pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.27 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mklines_test.go Wed Oct 3 22:27:53 2018 @@ -64,7 +64,7 @@ func (s *Suite) Test_MkLineChecker_check "ERROR: ~/Makefile:2: Other Makefiles must not be included directly.") } -func (s *Suite) Test_MkLines_quoting_LDFLAGS_for_GNU_configure(c *check.C) { +func (s *Suite) Test_MkLines__quoting_LDFLAGS_for_GNU_configure(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -386,7 +386,7 @@ func (s *Suite) Test_MkLines_DetermineUs c.Check(mklines.vars.FirstUse("outer.*"), equals, mkline) } -func (s *Suite) Test_MkLines_PrivateTool_Undefined(c *check.C) { +func (s *Suite) Test_MkLines__private_tool_undefined(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -402,7 +402,7 @@ func (s *Suite) Test_MkLines_PrivateTool "WARN: fname:3: Unknown shell command \"md5sum\".") } -func (s *Suite) Test_MkLines_PrivateTool_Defined(c *check.C) { +func (s *Suite) Test_MkLines__private_tool_defined(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -420,7 +420,7 @@ func (s *Suite) Test_MkLines_PrivateTool "WARN: fname:4: The \"md5sum\" tool is used but not added to USE_TOOLS.") } -func (s *Suite) Test_MkLines_Check_indentation(c *check.C) { +func (s *Suite) Test_MkLines_Check__indentation(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -519,7 +519,7 @@ func (s *Suite) Test_MkLines_Check__unba // Demonstrates how to define your own make(1) targets for creating // files in the current directory. The pkgsrc-wip category Makefile // does this, while all other categories don't need any custom code. -func (s *Suite) Test_MkLines_wip_category_Makefile(c *check.C) { +func (s *Suite) Test_MkLines__wip_category_Makefile(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall", "--explain") Index: pkgsrc/pkgtools/pkglint/files/mkparser.go diff -u pkgsrc/pkgtools/pkglint/files/mkparser.go:1.16 pkgsrc/pkgtools/pkglint/files/mkparser.go:1.17 --- pkgsrc/pkgtools/pkglint/files/mkparser.go:1.16 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mkparser.go Wed Oct 3 22:27:53 2018 @@ -6,6 +6,8 @@ import ( "strings" ) +// MkParser wraps a Parser and provides methods for parsing +// things related to Makefiles. type MkParser struct { *Parser } @@ -193,6 +195,7 @@ loop: } repl.Reset(modifierMark) + // FIXME: Why AdvanceRegexp? This accepts :S,a,b,c,d,e,f but shouldn't. for p.VarUse() != nil || repl.AdvanceRegexp(regex.Pattern(`^([^:$`+closing+`]|\$\$)+`)) { } if suffixSubst := repl.Since(modifierMark); contains(suffixSubst, "=") { Index: pkgsrc/pkgtools/pkglint/files/vartype.go diff -u pkgsrc/pkgtools/pkglint/files/vartype.go:1.16 pkgsrc/pkgtools/pkglint/files/vartype.go:1.17 --- pkgsrc/pkgtools/pkglint/files/vartype.go:1.16 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/vartype.go Wed Oct 3 22:27:53 2018 @@ -232,7 +232,7 @@ var ( BtPathname = &BasicType{"Pathname", (*VartypeCheck).Pathname} BtPerl5Packlist = &BasicType{"Perl5Packlist", (*VartypeCheck).Perl5Packlist} BtPerms = &BasicType{"Perms", (*VartypeCheck).Perms} - BtPkgName = &BasicType{"PkgName", (*VartypeCheck).PkgName} + BtPkgName = &BasicType{"Pkgname", (*VartypeCheck).Pkgname} BtPkgPath = &BasicType{"PkgPath", (*VartypeCheck).PkgPath} BtPkgOptionsVar = &BasicType{"PkgOptionsVar", (*VartypeCheck).PkgOptionsVar} BtPkgRevision = &BasicType{"PkgRevision", (*VartypeCheck).PkgRevision} Index: pkgsrc/pkgtools/pkglint/files/mkshparser_test.go diff -u pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.6 pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.7 --- pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.6 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/mkshparser_test.go Wed Oct 3 22:27:53 2018 @@ -27,7 +27,15 @@ type ShSuite struct { var _ = check.Suite(&ShSuite{}) -func (s *ShSuite) Test_ShellParser_program(c *check.C) { +func (s *ShSuite) SetUpTest(c *check.C) { + G = NewPkglint() +} + +func (s *ShSuite) TearDownTest(c *check.C) { + G = Pkglint{} // Make it unusable +} + +func (s *ShSuite) Test_ShellParser__program(c *check.C) { b := s.init(c) s.test("", @@ -103,7 +111,7 @@ func (s *ShSuite) Test_ShellParser_progr b.List().AddCommand(b.SimpleCommand("action2")).AddSemicolon()))) } -func (s *ShSuite) Test_ShellParser_list(c *check.C) { +func (s *ShSuite) Test_ShellParser__list(c *check.C) { b := s.init(c) s.test("echo1 && echo2", @@ -125,7 +133,7 @@ func (s *ShSuite) Test_ShellParser_list( AddBackground()) } -func (s *ShSuite) Test_ShellParser_and_or(c *check.C) { +func (s *ShSuite) Test_ShellParser__and_or(c *check.C) { b := s.init(c) s.test("echo1 | echo2", @@ -154,7 +162,7 @@ func (s *ShSuite) Test_ShellParser_and_o b.SimpleCommand("echo4"))))) } -func (s *ShSuite) Test_ShellParser_pipeline(c *check.C) { +func (s *ShSuite) Test_ShellParser__pipeline(c *check.C) { b := s.init(c) s.test("command1 | command2", @@ -168,7 +176,7 @@ func (s *ShSuite) Test_ShellParser_pipel b.SimpleCommand("command2"))))) } -func (s *ShSuite) Test_ShellParser_pipe_sequence(c *check.C) { +func (s *ShSuite) Test_ShellParser__pipe_sequence(c *check.C) { b := s.init(c) s.test("command1 | if true ; then : ; fi", @@ -179,7 +187,7 @@ func (s *ShSuite) Test_ShellParser_pipe_ b.List().AddCommand(b.SimpleCommand(":")).AddSemicolon()))))) } -func (s *ShSuite) Test_ShellParser_command(c *check.C) { +func (s *ShSuite) Test_ShellParser__command(c *check.C) { b := s.init(c) s.test("simple_command", @@ -205,7 +213,7 @@ func (s *ShSuite) Test_ShellParser_comma b.Redirection(2, ">&", "1")))) } -func (s *ShSuite) Test_ShellParser_compound_command(c *check.C) { +func (s *ShSuite) Test_ShellParser__compound_command(c *check.C) { b := s.init(c) s.test("{ brace ; }", @@ -228,7 +236,7 @@ func (s *ShSuite) Test_ShellParser_compo } -func (s *ShSuite) Test_ShellParser_subshell(c *check.C) { +func (s *ShSuite) Test_ShellParser__subshell(c *check.C) { b := s.init(c) sub3 := b.Subshell(b.List().AddCommand(b.SimpleCommand("sub3"))) @@ -237,7 +245,7 @@ func (s *ShSuite) Test_ShellParser_subsh s.test("( ( ( sub3 ) ; sub2 ) ; sub1 )", b.List().AddCommand(sub1)) } -func (s *ShSuite) Test_ShellParser_compound_list(c *check.C) { +func (s *ShSuite) Test_ShellParser__compound_list(c *check.C) { b := s.init(c) s.test("( \n echo )", @@ -245,13 +253,13 @@ func (s *ShSuite) Test_ShellParser_compo b.List().AddCommand(b.SimpleCommand("echo"))))) } -func (s *ShSuite) Test_ShellParser_term(c *check.C) { +func (s *ShSuite) Test_ShellParser__term(c *check.C) { b := s.init(c) _ = b } -func (s *ShSuite) Test_ShellParser_for_clause(c *check.C) { +func (s *ShSuite) Test_ShellParser__for_clause(c *check.C) { b := s.init(c) s.test("for var do echo $var ; done", @@ -296,7 +304,7 @@ func (s *ShSuite) Test_ShellParser_for_c b.List().AddCommand(b.SimpleCommand("echo", "$$i$$j")).AddSemicolon()))))) } -func (s *ShSuite) Test_ShellParser_case_clause(c *check.C) { +func (s *ShSuite) Test_ShellParser__case_clause(c *check.C) { b := s.init(c) s.test("case $var in esac", @@ -335,7 +343,7 @@ func (s *ShSuite) Test_ShellParser_case_ } -func (s *ShSuite) Test_ShellParser_if_clause(c *check.C) { +func (s *ShSuite) Test_ShellParser__if_clause(c *check.C) { b := s.init(c) s.test( @@ -354,7 +362,7 @@ func (s *ShSuite) Test_ShellParser_if_cl b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon()))))) } -func (s *ShSuite) Test_ShellParser_while_clause(c *check.C) { +func (s *ShSuite) Test_ShellParser__while_clause(c *check.C) { b := s.init(c) s.test("while condition ; do action ; done", @@ -363,7 +371,7 @@ func (s *ShSuite) Test_ShellParser_while b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon()))) } -func (s *ShSuite) Test_ShellParser_until_clause(c *check.C) { +func (s *ShSuite) Test_ShellParser__until_clause(c *check.C) { b := s.init(c) s.test("until condition ; do action ; done", @@ -372,13 +380,13 @@ func (s *ShSuite) Test_ShellParser_until b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon()))) } -func (s *ShSuite) Test_ShellParser_function_definition(c *check.C) { +func (s *ShSuite) Test_ShellParser__function_definition(c *check.C) { b := s.init(c) _ = b } -func (s *ShSuite) Test_ShellParser_brace_group(c *check.C) { +func (s *ShSuite) Test_ShellParser__brace_group(c *check.C) { b := s.init(c) // No semicolon necessary after the closing brace. @@ -389,7 +397,7 @@ func (s *ShSuite) Test_ShellParser_brace b.List().AddCommand(b.SimpleCommand("echo", "yes")).AddSemicolon()))))) } -func (s *ShSuite) Test_ShellParser_simple_command(c *check.C) { +func (s *ShSuite) Test_ShellParser__simple_command(c *check.C) { b := s.init(c) s.test( @@ -427,7 +435,7 @@ func (s *ShSuite) Test_ShellParser_simpl b.List().AddCommand(b.SimpleCommand("{OpenGrok", "args"))) } -func (s *ShSuite) Test_ShellParser_io_redirect(c *check.C) { +func (s *ShSuite) Test_ShellParser__io_redirect(c *check.C) { b := s.init(c) s.test("echo >> ${PLIST_SRC}", @@ -472,7 +480,7 @@ func (s *ShSuite) Test_ShellParser_io_re {-1, ">", b.Token("/dev/stderr")}}}})) } -func (s *ShSuite) Test_ShellParser_io_here(c *check.C) { +func (s *ShSuite) Test_ShellParser__io_here(c *check.C) { b := s.init(c) _ = b Index: pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go diff -u pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.3 pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.4 --- pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.3 Sun Aug 12 16:31:56 2018 +++ pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go Wed Oct 3 22:27:53 2018 @@ -26,12 +26,16 @@ func (s *Suite) Test_MkShWalker_Walk(c * commands = append(commands, fmt.Sprintf("%16s %s", kind, detail)) } - callback := NewMkShWalkCallback() + walker := NewMkShWalker() + callback := &walker.Callback callback.List = func(list *MkShList) { add("List", "with %d andOrs", len(list.AndOrs)) } callback.AndOr = func(andor *MkShAndOr) { add("AndOr", "with %d pipelines", len(andor.Pipes)) } callback.Pipeline = func(pipeline *MkShPipeline) { add("Pipeline", "with %d commands", len(pipeline.Cmds)) } callback.Command = func(command *MkShCommand) { add("Command", "") } - callback.SimpleCommand = func(command *MkShSimpleCommand) { add("SimpleCommand", "%s", NewStrCommand(command).String()) } + callback.SimpleCommand = func(command *MkShSimpleCommand) { + add("SimpleCommand", "%s", NewStrCommand(command).String()) + add("Path", "%s", walker.Path()) + } callback.CompoundCommand = func(command *MkShCompoundCommand) { add("CompoundCommand", "") } callback.Case = func(caseClause *MkShCaseClause) { add("Case", "with %d items", len(caseClause.Cases)) } callback.CaseItem = func(caseItem *MkShCaseItem) { add("CaseItem", "with %d patterns", len(caseItem.Patterns)) } @@ -45,7 +49,7 @@ func (s *Suite) Test_MkShWalker_Walk(c * callback.For = func(forClause *MkShForClause) { add("For", "variable %s", forClause.Varname) } callback.Varname = func(varname string) { add("Varname", "%s", varname) } - NewMkShWalker().Walk(list, callback) + walker.Walk(list) c.Check(commands, deepEquals, []string{ " List with 5 andOrs", @@ -58,13 +62,15 @@ func (s *Suite) Test_MkShWalker_Walk(c * " AndOr with 1 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] condition []", + " SimpleCommand condition", + " Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.IfClause.List[0].AndOr[0].Pipeline[0].Command[0].SimpleCommand", " Word condition", " List with 1 andOrs", " AndOr with 1 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] action []", + " SimpleCommand action", + " Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.IfClause.List[1].AndOr[0].Pipeline[0].Command[0].SimpleCommand", " Word action", " List with 1 andOrs", " AndOr with 1 pipelines", @@ -80,19 +86,22 @@ func (s *Suite) Test_MkShWalker_Walk(c * " AndOr with 1 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] case-item-action []", + " SimpleCommand case-item-action", + " Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.IfClause.List[2].AndOr[0].Pipeline[0].Command[0].CompoundCommand.CaseClause.CaseItem[0].List[1].AndOr[0].Pipeline[0].Command[0].SimpleCommand", " Word case-item-action", " AndOr with 1 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] set [-e]", + " SimpleCommand set -e", + " Path List.AndOr[1].Pipeline[0].Command[0].SimpleCommand", " Word set", " Words with 1 words", " Word -e", " AndOr with 1 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] cd [${WRKSRC}/locale]", + " SimpleCommand cd ${WRKSRC}/locale", + " Path List.AndOr[2].Pipeline[0].Command[0].SimpleCommand", " Word cd", " Words with 1 words", " Word ${WRKSRC}/locale", @@ -108,7 +117,8 @@ func (s *Suite) Test_MkShWalker_Walk(c * " AndOr with 2 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] [ [\"$${lang}\" = \"wxstd.po\" ]]", + " SimpleCommand [ \"$${lang}\" = \"wxstd.po\" ]", + " Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.ForClause.List.AndOr[0].Pipeline[0].Command[0].SimpleCommand", " Word [", " Words with 4 words", " Word \"$${lang}\"", @@ -117,12 +127,14 @@ func (s *Suite) Test_MkShWalker_Walk(c * " Word ]", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] continue []", + " SimpleCommand continue", + " Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.ForClause.List.AndOr[0].Pipeline[1].Command[0].SimpleCommand", " Word continue", " AndOr with 1 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] ${TOOLS_PATH.msgfmt} [-c -o \"$${lang%.po}.mo\" \"$${lang}\"]", + " SimpleCommand ${TOOLS_PATH.msgfmt} -c -o \"$${lang%.po}.mo\" \"$${lang}\"", + " Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.ForClause.List.AndOr[1].Pipeline[0].Command[0].SimpleCommand", " Word ${TOOLS_PATH.msgfmt}", " Words with 4 words", " Word -c", @@ -138,7 +150,8 @@ func (s *Suite) Test_MkShWalker_Walk(c * " AndOr with 1 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] : []", + " SimpleCommand :", + " Path List.AndOr[4].Pipeline[0].Command[0].CompoundCommand.LoopClause.List[0].AndOr[0].Pipeline[0].Command[0].SimpleCommand", " Word :", " List with 1 andOrs", " AndOr with 1 pipelines", @@ -150,10 +163,15 @@ func (s *Suite) Test_MkShWalker_Walk(c * " AndOr with 1 pipelines", " Pipeline with 1 commands", " Command ", - " SimpleCommand [] : []", + " SimpleCommand :", + " Path List.AndOr[4].Pipeline[0].Command[0].CompoundCommand.LoopClause.List[1].AndOr[0].Pipeline[0].Command[0].FunctionDefinition.CompoundCommand.List.AndOr[0].Pipeline[0].Command[0].SimpleCommand", " Word :", " Redirects with 1 redirects", " Redirect >&", " Word 2"}) + + // After parsing, there is not a single level of indentation, + // therefore even Parent(0) returns nil. + c.Check(walker.Parent(0), equals, nil) } } Index: pkgsrc/pkgtools/pkglint/files/vardefs_test.go diff -u pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.3 pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.4 --- pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.3 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/vardefs_test.go Wed Oct 3 22:27:53 2018 @@ -2,21 +2,21 @@ package main import "gopkg.in/check.v1" -func (s *Suite) Test_InitVartypes__enumFrom(c *check.C) { +func (s *Suite) Test_Pkgsrc_InitVartypes__enumFrom(c *check.C) { t := s.Init(c) - t.SetupFileMkLines("editors/emacs/modules.mk", + t.CreateFileLines("editors/emacs/modules.mk", MkRcsID, "", "_EMACS_VERSIONS_ALL= emacs31", "_EMACS_VERSIONS_ALL+= emacs29") - t.SetupFileMkLines("mk/java-vm.mk", + t.CreateFileLines("mk/java-vm.mk", MkRcsID, "", "_PKG_JVMS.8= openjdk8 oracle-jdk8", "_PKG_JVMS.7= ${_PKG_JVMS.8} openjdk7 sun-jdk7", "_PKG_JVMS.6= ${_PKG_JVMS.7} sun-jdk6 jdk16") - t.SetupFileMkLines("mk/compiler.mk", + t.CreateFileLines("mk/compiler.mk", MkRcsID, "", "_COMPILERS= gcc ido mipspro-ucode \\", @@ -41,3 +41,23 @@ func (s *Suite) Test_InitVartypes__enumF checkEnumValues("USE_LANGUAGES", "ShellList of enum: ada c c++ c++03 c++0x c++11 c++14 c99 fortran fortran77 gnu++03 gnu++0x gnu++11 gnu++14 java obj-c++ objc ") checkEnumValues("PKGSRC_COMPILER", "ShellList of enum: ccache distcc f2c g95 gcc ido mipspro-ucode sunpro ") } + +func (s *Suite) Test_parseACLEntries(c *check.C) { + t := s.Init(c) + + t.ExpectFatal( + func() { parseACLEntries("VARNAME", "buildlink3.mk: *; *: *") }, + "FATAL: Invalid ACL permission \"*\" for \"VARNAME\".") + + t.ExpectFatal( + func() { parseACLEntries("VARNAME", "buildlink3.mk: use; *: use") }, + "FATAL: Repeated permissions \"use\" for \"VARNAME\".") + + t.ExpectFatal( + func() { parseACLEntries("VARNAME", "*.txt: use") }, + "FATAL: Invalid ACL glob \"*.txt\" for \"VARNAME\".") + + t.ExpectFatal( + func() { parseACLEntries("VARNAME", "*.mk: use; buildlink3.mk: append") }, + "FATAL: Ineffective ACL glob \"buildlink3.mk\" for \"VARNAME\".") +} Index: pkgsrc/pkgtools/pkglint/files/package.go diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.35 pkgsrc/pkgtools/pkglint/files/package.go:1.36 --- pkgsrc/pkgtools/pkglint/files/package.go:1.35 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/package.go Wed Oct 3 22:27:53 2018 @@ -31,7 +31,6 @@ type Package struct { vars Scope bl3 map[string]Line // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included. - plistSubstCond map[string]bool // varname => true; all variables that are used as conditions (@comment or nothing) in PLISTs. included map[string]Line // fname => line seenMakefileCommon bool // Does the package have any .includes? conditionalIncludes map[string]MkLine @@ -52,24 +51,33 @@ func NewPackage(dir string) *Package { Pkgdir: ".", Filesdir: "files", Patchdir: "patches", - DistinfoFile: "distinfo", + DistinfoFile: "${PKGDIR}/distinfo", PlistDirs: make(map[string]bool), PlistFiles: make(map[string]bool), vars: NewScope(), bl3: make(map[string]Line), - plistSubstCond: make(map[string]bool), included: make(map[string]Line), conditionalIncludes: make(map[string]MkLine), unconditionalIncludes: make(map[string]MkLine), } pkg.vars.DefineAll(G.Pkgsrc.UserDefinedVars) + + pkg.vars.Fallback("PKGDIR", ".") + pkg.vars.Fallback("DISTINFO_FILE", "${PKGDIR}/distinfo") + pkg.vars.Fallback("FILESDIR", "files") + pkg.vars.Fallback("PATCHDIR", "patches") + pkg.vars.Fallback("KRB5_TYPE", "heimdal") + pkg.vars.Fallback("PGSQL_VERSION", "95") + pkg.vars.Fallback(".CURDIR", ".") // FIXME: In reality, this is an absolute pathname. + return pkg } // 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(pkg.dir + "/" + relativeFilename) + return cleanpath(resolveVariableRefs(pkg.dir + "/" + relativeFilename)) } func (pkg *Package) checkPossibleDowngrade() { @@ -93,7 +101,7 @@ func (pkg *Package) checkPossibleDowngra } if change.Action == "Updated" { - changeVersion := regex.Compile(`nb\d+$`).ReplaceAllString(change.Version, "") + changeVersion := replaceAll(change.Version, `nb\d+$`, "") if pkgver.Compare(pkgversion, changeVersion) < 0 { mkline.Warnf("The package is being downgraded from %s (see %s) to %s.", change.Version, change.Line.ReferenceFrom(mkline.Line), pkgversion) Explain( @@ -158,7 +166,7 @@ func (pkglint *Pkglint) checkdirPackage( files = append(files, dirglob(pkg.File(pkg.Filesdir))...) } files = append(files, dirglob(pkg.File(pkg.Patchdir))...) - if pkg.DistinfoFile != "distinfo" && pkg.DistinfoFile != "./distinfo" { + if pkg.DistinfoFile != pkg.vars.fallback["DISTINFO_FILE"] { files = append(files, pkg.File(pkg.DistinfoFile)) } haveDistinfo := false @@ -181,11 +189,14 @@ func (pkglint *Pkglint) checkdirPackage( for _, fname := range files { if containsVarRef(fname) { + if trace.Tracing { + trace.Stepf("Skipping file %q because the name contains an unresolved variable.", fname) + } continue } if fname == pkg.File("Makefile") { if st, err := os.Lstat(fname); err == nil { - pkglint.checkExecutable(st, fname) + pkglint.checkExecutable(st) } if G.opts.CheckMakefile { pkg.checkfilePackageMakefile(fname, lines) @@ -201,7 +212,7 @@ func (pkglint *Pkglint) checkdirPackage( pkg.checkLocallyModified(fname) } - if G.opts.CheckDistinfo && G.opts.CheckPatches { + if pkg.Pkgdir == "." && G.opts.CheckDistinfo && G.opts.CheckPatches { if havePatches && !haveDistinfo { NewLineWhole(pkg.File(pkg.DistinfoFile)).Warnf("File not found. Please run \"%s makepatchsum\".", confMake) } @@ -214,10 +225,6 @@ func (pkglint *Pkglint) checkdirPackage( } } } - - if !isEmptyDir(pkg.File("scripts")) { - NewLineWhole(pkg.File("scripts")).Warnf("This directory and its contents are deprecated! Please call the script(s) explicitly from the corresponding target(s) in the pkg's Makefile.") - } } func (pkg *Package) loadPackageMakefile() *MkLines { @@ -227,7 +234,8 @@ func (pkg *Package) loadPackageMakefile( } mainLines, allLines := NewMkLines(nil), NewMkLines(nil) - if !pkg.readMakefile(fname, mainLines, allLines, "") { + if _, result := pkg.readMakefile(fname, mainLines, allLines, ""); !result { + LoadMk(fname, NotEmpty|LogErrors) // Just for the LogErrors. return nil } @@ -241,10 +249,10 @@ func (pkg *Package) loadPackageMakefile( allLines.DetermineUsedVariables() allLines.CheckRedundantVariables() - pkg.Pkgdir = pkg.expandVariableWithDefault("PKGDIR", ".") - pkg.DistinfoFile = pkg.expandVariableWithDefault("DISTINFO_FILE", "distinfo") - pkg.Filesdir = pkg.expandVariableWithDefault("FILESDIR", "files") - pkg.Patchdir = pkg.expandVariableWithDefault("PATCHDIR", "patches") + pkg.Pkgdir, _ = pkg.vars.Value("PKGDIR") + pkg.DistinfoFile, _ = pkg.vars.Value("DISTINFO_FILE") + pkg.Filesdir, _ = pkg.vars.Value("FILESDIR") + pkg.Patchdir, _ = pkg.vars.Value("PATCHDIR") // See lang/php/ext.mk if varIsDefined("PHPEXT_MK") { @@ -271,19 +279,20 @@ func (pkg *Package) loadPackageMakefile( return mainLines } -func (pkg *Package) readMakefile(fname string, mainLines *MkLines, allLines *MkLines, includingFnameForUsedCheck string) bool { +func (pkg *Package) readMakefile(fname string, mainLines *MkLines, allLines *MkLines, includingFnameForUsedCheck string) (exists bool, result bool) { if trace.Tracing { defer trace.Call1(fname)() } - fileMklines := LoadMk(fname, NotEmpty|LogErrors) + fileMklines := LoadMk(fname, NotEmpty) if fileMklines == nil { - return false + return false, false } + exists = true isMainMakefile := len(mainLines.mklines) == 0 - result := true + result = true lineAction := func(mkline MkLine) bool { if isMainMakefile { mainLines.mklines = append(mainLines.mklines, mkline) @@ -308,7 +317,7 @@ func (pkg *Package) readMakefile(fname s if includeFile != "" { if path.Base(fname) != "buildlink3.mk" { if m, bl3File := match1(includeFile, `^\.\./\.\./(.*)/buildlink3\.mk$`); m { - G.Pkg.bl3[bl3File] = mkline.Line + pkg.bl3[bl3File] = mkline.Line if trace.Tracing { trace.Step1("Buildlink3 file in package: %q", bl3File) } @@ -316,8 +325,8 @@ func (pkg *Package) readMakefile(fname s } } - if includeFile != "" && G.Pkg.included[includeFile] == nil { - G.Pkg.included[includeFile] = mkline.Line + if includeFile != "" && pkg.included[includeFile] == nil { + pkg.included[includeFile] = mkline.Line if matches(includeFile, `^\.\./[^./][^/]*/[^/]+`) { mkline.Warnf("References to other packages should look like \"../../category/package\", not \"../package\".") @@ -328,7 +337,7 @@ func (pkg *Package) readMakefile(fname s if trace.Tracing { trace.Step1("Including %q sets seenMakefileCommon.", includeFile) } - G.Pkg.seenMakefileCommon = true + pkg.seenMakefileCommon = true } skip := contains(fname, "/mk/") || hasSuffix(includeFile, "/bsd.pkg.mk") || IsPrefs(includeFile) @@ -336,30 +345,37 @@ func (pkg *Package) readMakefile(fname s dirname, _ := path.Split(fname) dirname = cleanpath(dirname) - // Only look in the directory relative to the - // current file and in the current working directory. - // Pkglint doesn't have an include dir list, like make(1) does. - if !fileExists(dirname + "/" + includeFile) { + fullIncluded := dirname + "/" + includeFile + if trace.Tracing { + trace.Step1("Including %q.", fullIncluded) + } + fullIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", fname, "") + innerExists, innerResult := pkg.readMakefile(fullIncluded, mainLines, allLines, fullIncluding) + if !innerExists { if fileMklines.indentation.IsCheckedFile(includeFile) { return true // See https://github.com/rillig/pkglint/issues/1 + } - } else if dirname != pkg.File(".") { // Prevent unnecessary syscalls - dirname = pkg.File(".") - if !fileExists(dirname + "/" + includeFile) { - mkline.Errorf("Cannot read %q.", dirname+"/"+includeFile) - result = false - return false - } + // Only look in the directory relative to the + // current file and in the package directory. + // Make(1) has a list of include directories, but pkgsrc + // doesn't make use of that, so pkglint also doesn't + // need this extra complexity. + pkgBasedir := pkg.File(".") + if dirname != pkgBasedir { // Prevent unnecessary syscalls + dirname = pkgBasedir + + fullIncludedFallback := dirname + "/" + includeFile + innerExists, innerResult = pkg.readMakefile(fullIncludedFallback, mainLines, allLines, fullIncluding) } - } - if trace.Tracing { - trace.Step1("Including %q.", dirname+"/"+includeFile) + if !innerExists { + mkline.Errorf("Cannot read %q.", includeFile) + } } - absIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", fname, "") - absIncluded := dirname + "/" + includeFile - if !pkg.readMakefile(absIncluded, mainLines, allLines, absIncluding) { + + if !innerResult { result = false return false } @@ -369,11 +385,11 @@ func (pkg *Package) readMakefile(fname s if mkline.IsVarassign() { varname, op, value := mkline.Varname(), mkline.Op(), mkline.Value() - if op != opAssignDefault || !G.Pkg.vars.Defined(varname) { + if op != opAssignDefault || !pkg.vars.Defined(varname) { if trace.Tracing { trace.Stepf("varassign(%q, %q, %q)", varname, op, value) } - G.Pkg.vars.Define(varname, mkline) + pkg.vars.Define(varname, mkline) } } return true @@ -385,7 +401,7 @@ func (pkg *Package) readMakefile(fname s fileMklines.checkForUsedComment(G.Pkgsrc.ToRel(includingFnameForUsedCheck)) } - return result + return } func (pkg *Package) checkfilePackageMakefile(fname string, mklines *MkLines) { @@ -413,28 +429,14 @@ func (pkg *Package) checkfilePackageMake } if perlLine, noconfLine := vars.FirstDefinition("REPLACE_PERL"), vars.FirstDefinition("NO_CONFIGURE"); perlLine != nil && noconfLine != nil { - perlLine.Warnf("REPLACE_PERL is ignored when NO_CONFIGURE is set (in %s)", noconfLine.ReferenceFrom(perlLine.Line)) + perlLine.Warnf("REPLACE_PERL is ignored when NO_CONFIGURE is set (in %s).", noconfLine.ReferenceFrom(perlLine.Line)) } if !vars.Defined("LICENSE") && !vars.Defined("META_PACKAGE") && pkg.once.FirstTime("LICENSE") { NewLineWhole(fname).Errorf("Each package must define its LICENSE.") } - if gnuLine, useLine := vars.FirstDefinition("GNU_CONFIGURE"), vars.FirstDefinition("USE_LANGUAGES"); gnuLine != nil && useLine != nil { - if matches(useLine.VarassignComment(), `(?-i)\b(?:c|empty|none)\b`) { - // Don't emit a warning, since the comment - // probably contains a statement that C is - // really not needed. - - } else if !G.Infrastructure && useLine.Filename == "../../mk/compiler.mk" { - // Ignore this one - - } else if !matches(useLine.Value(), `(?:^|\s+)(?:c|c99|objc)(?:\s+|$)`) { - gnuLine.Warnf("GNU_CONFIGURE almost always needs a C compiler, but \"c\" is not added to USE_LANGUAGES in %s.", - useLine.ReferenceFrom(gnuLine.Line)) - } - } - + pkg.checkGnuConfigureUseLanguages() pkg.determineEffectivePkgVars() pkg.checkPossibleDowngrade() @@ -454,6 +456,22 @@ func (pkg *Package) checkfilePackageMake SaveAutofixChanges(mklines.lines) } +func (pkg *Package) checkGnuConfigureUseLanguages() { + vars := pkg.vars + + if gnuLine, useLine := vars.FirstDefinition("GNU_CONFIGURE"), vars.FirstDefinition("USE_LANGUAGES"); gnuLine != nil && useLine != nil { + if matches(useLine.VarassignComment(), `(?-i)\b(?:c|empty|none)\b`) { + // Don't emit a warning, since the comment + // probably contains a statement that C is + // really not needed. + + } else if !matches(useLine.Value(), `(?:^|\s+)(?:c|c99|objc)(?:\s+|$)`) { + gnuLine.Warnf("GNU_CONFIGURE almost always needs a C compiler, but \"c\" is not added to USE_LANGUAGES in %s.", + useLine.ReferenceFrom(gnuLine.Line)) + } + } +} + func (pkg *Package) getNbpart() string { pkgrevision, _ := pkg.vars.Value("PKGREVISION") if rev, err := strconv.Atoi(pkgrevision); err == nil { @@ -514,19 +532,17 @@ func (pkg *Package) determineEffectivePk func (pkg *Package) pkgnameFromDistname(pkgname, distname string) string { tokens := NewMkParser(dummyLine, pkgname, false).MkTokens() - subst := func(str, smod string) (result string) { - if trace.Tracing { - defer trace.Call(str, smod, trace.Result(&result))() - } + // Example: + // subst("distname-1.0", "S,name,file,g") => "distfile-1.0" + subst := func(str, smod string) string { qsep := regexp.QuoteMeta(smod[1:2]) - if m, left, from, right, to, flags := regex.Match5(smod, regex.Pattern(`^S`+qsep+`(\^?)([^:]*?)(\$?)`+qsep+`([^:]*)`+qsep+`([1g]*)$`)); m { - result := mkopSubst(str, left != "", from, right != "", to, flags) - if trace.Tracing { - trace.Stepf("subst %q %q => %q", str, smod, result) - } - return result + m, left, from, right, to, flags := match5(smod, regex.Pattern(`^S`+qsep+`(\^?)([^:]*?)(\$?)`+qsep+`([^:]*)`+qsep+`([1g]*)$`)) + G.Assertf(m, "pkgnameFromDistname %q", smod) + result := mkopSubst(str, left != "", from, right != "", to, flags) + if trace.Tracing && result != str { + trace.Stepf("pkgnameFromDistname.subst: %q %q => %q", str, smod, result) } - return str + return result } result := "" @@ -551,23 +567,6 @@ func (pkg *Package) pkgnameFromDistname( return result } -func (pkg *Package) expandVariableWithDefault(varname, defaultValue string) string { - mkline := G.Pkg.vars.FirstDefinition(varname) - if mkline == nil { - return defaultValue - } - - value := mkline.Value() - value = mkline.ResolveVarsInRelativePath(value, true) - if containsVarRef(value) { - value = resolveVariableRefs(value) - } - if trace.Tracing { - trace.Step2("Expanded %q to %q", varname, value) - } - return value -} - func (pkg *Package) checkUpdate() { if pkg.EffectivePkgbase != "" { for _, sugg := range G.Pkgsrc.GetSuggestedPackageUpdates() { @@ -848,10 +847,7 @@ func (pkg *Package) checkLocallyModified owner, _ := pkg.vars.Value("OWNER") maintainer, _ := pkg.vars.Value("MAINTAINER") - if containsVarRef(owner) { - owner = "" - } - if containsVarRef(maintainer) || maintainer == "pkgsrc-users@NetBSD.org" { + if maintainer == "pkgsrc-users@NetBSD.org" { maintainer = "" } if owner == "" && maintainer == "" { Index: pkgsrc/pkgtools/pkglint/files/package_test.go diff -u pkgsrc/pkgtools/pkglint/files/package_test.go:1.29 pkgsrc/pkgtools/pkglint/files/package_test.go:1.30 --- pkgsrc/pkgtools/pkglint/files/package_test.go:1.29 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/package_test.go Wed Oct 3 22:27:53 2018 @@ -2,6 +2,40 @@ package main import "gopkg.in/check.v1" +func (s *Suite) Test_Package_checklinesBuildlink3Inclusion__file_but_not_package(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("category/dependency/buildlink3.mk") + G.Pkg = NewPackage(t.File("category/package")) + mklines := t.NewMkLines("category/package/buildlink3.mk", + MkRcsID, + "", + ".include \"../../category/dependency/buildlink3.mk\"") + + G.Pkg.checklinesBuildlink3Inclusion(mklines) + + t.CheckOutputLines( + "WARN: category/package/buildlink3.mk:3: category/dependency/buildlink3.mk is included by this file but not by the package.") +} + +func (s *Suite) Test_Package_checklinesBuildlink3Inclusion__package_but_not_file(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("category/dependency/buildlink3.mk") + G.Pkg = NewPackage(t.File("category/package")) + G.Pkg.bl3["../../category/dependency/buildlink3.mk"] = t.NewLine("fname", 1, "") + mklines := t.NewMkLines("category/package/buildlink3.mk", + MkRcsID) + + t.EnableTracingToLog() + G.Pkg.checklinesBuildlink3Inclusion(mklines) + + t.CheckOutputLines( + "TRACE: + (*Package).checklinesBuildlink3Inclusion()", + "TRACE: 1 ../../category/dependency/buildlink3.mk/buildlink3.mk is included by the package but not by the buildlink3.mk file.", + "TRACE: - (*Package).checklinesBuildlink3Inclusion()") +} + func (s *Suite) Test_Package_pkgnameFromDistname(c *check.C) { t := s.Init(c) @@ -18,6 +52,9 @@ func (s *Suite) Test_Package_pkgnameFrom c.Check(pkg.pkgnameFromDistname("${DISTNAME:C/beta/.0./}", "fspanel-0.8beta1"), equals, "${DISTNAME:C/beta/.0./}") c.Check(pkg.pkgnameFromDistname("${DISTNAME:S/-0$/.0/1}", "aspell-af-0.50-0"), equals, "aspell-af-0.50.0") + // FIXME: Should produce a parse error since the :S modifier is malformed; see Test_MkParser_MkTokens. + c.Check(pkg.pkgnameFromDistname("${DISTNAME:S,a,b,c,d}", "aspell-af-0.50-0"), equals, "bspell-af-0.50-0") + t.CheckOutputEmpty() } @@ -87,7 +124,7 @@ func (s *Suite) Test_Package_CheckVarord "DISTNAME=\tdistname-1.0", "CATEGORIES=\tsysutils", "", - "MAINTAINER=\tpkgsrc-users@pkgsrc.org", + "MAINTAINER=\tpkgsrc-users@NetBSD.org", "# comment", "COMMENT=\tComment", "LICENSE=\tgnu-gpl-v2")) @@ -109,7 +146,7 @@ func (s *Suite) Test_Package_CheckVarord "CATEGORIES=\tsysutils", "", ".if ${DISTNAME:Mdistname-*}", - "MAINTAINER=\tpkgsrc-users@pkgsrc.org", + "MAINTAINER=\tpkgsrc-users@NetBSD.org", ".endif", "LICENSE=\tgnu-gpl-v2")) @@ -118,7 +155,7 @@ func (s *Suite) Test_Package_CheckVarord t.CheckOutputEmpty() } -func (s *Suite) Test_Package_CheckVarorder_GitHub(c *check.C) { +func (s *Suite) Test_Package_CheckVarorder__GitHub(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Worder") @@ -139,7 +176,7 @@ func (s *Suite) Test_Package_CheckVarord t.CheckOutputEmpty() } -func (s *Suite) Test_Package_varorder_license(c *check.C) { +func (s *Suite) Test_Package_CheckVarorder__license(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Worder") @@ -276,6 +313,35 @@ func (s *Suite) Test_Package_determineEf c.Check(pkg.EffectivePkgversion, equals, "1.0") } +func (s *Suite) Test_Package_determineEffectivePkgVars__same(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-order") + pkg := t.SetupPackage("category/package", + "DISTNAME=\tdistname-1.0", + "PKGNAME=\tdistname-1.0") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "NOTE: ~/category/package/Makefile:20: " + + "PKGNAME is ${DISTNAME} by default. You probably don't need to define PKGNAME.") +} + +func (s *Suite) Test_Package_determineEffectivePkgVars__invalid_DISTNAME(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-order") + pkg := t.SetupPackage("category/package", + "DISTNAME=\tpkgname-version") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:3: " + + "As DISTNAME is not a valid package name, please define the PKGNAME explicitly.") +} + func (s *Suite) Test_Package_checkPossibleDowngrade(c *check.C) { t := s.Init(c) @@ -300,11 +366,48 @@ func (s *Suite) Test_Package_checkPossib t.CheckOutputEmpty() } -func (s *Suite) Test_checkdirPackage(c *check.C) { +func (s *Suite) Test_Package_loadPackageMakefile__dump(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("--dumpmakefile") + t.SetupVartypes() + t.SetupPkgsrc() + t.CreateFileLines("category/Makefile") + t.CreateFileLines("category/package/PLIST", + PlistRcsID, + "bin/program") + t.CreateFileLines("category/package/distinfo", + RcsID, + "", + "SHA1 (distfile-1.0.tar.gz) = 12341234...", + "RMD160 (distfile-1.0.tar.gz) = 12341234...", + "SHA512 (distfile-1.0.tar.gz) = 12341234...", + "Size (distfile-1.0.tar.gz) = 12341234...") + t.CreateFileLines("category/package/Makefile", + MkRcsID, + "", + "CATEGORIES=category", + "", + "COMMENT=\tComment", + "LICENSE=\t2-clause-bsd") + + G.checkdirPackage(t.File("category/package")) + + t.CheckOutputLines( + "Whole Makefile (with all included files) follows:", + "~/category/package/Makefile:1: # $NetBSD: package_test.go,v 1.30 2018/10/03 22:27:53 rillig Exp $", + "~/category/package/Makefile:2: ", + "~/category/package/Makefile:3: CATEGORIES=category", + "~/category/package/Makefile:4: ", + "~/category/package/Makefile:5: COMMENT=\tComment", + "~/category/package/Makefile:6: LICENSE=\t2-clause-bsd") +} + +func (s *Suite) Test_Pkglint_checkdirPackage(c *check.C) { t := s.Init(c) t.Chdir("category/package") - t.SetupFileLines("Makefile", + t.CreateFileLines("Makefile", MkRcsID) G.checkdirPackage(".") @@ -316,6 +419,56 @@ func (s *Suite) Test_checkdirPackage(c * "WARN: Makefile: No COMMENT given.") } +func (s *Suite) Test_Pkglint_checkdirPackage__PKGDIR(c *check.C) { + t := s.Init(c) + + t.SetupVartypes() + t.SetupPkgsrc() + t.CreateFileLines("category/Makefile") + t.CreateFileLines("other/package/Makefile", + MkRcsID) + t.CreateFileLines("other/package/PLIST", + PlistRcsID, + "bin/program") + t.CreateFileLines("other/package/distinfo", + RcsID, + "", + "SHA1 (patch-aa) = da39a3ee5e6b4b0d3255bfef95601890afd80709") + t.CreateFileLines("category/package/patches/patch-aa", + RcsID) + t.Chdir("category/package") + t.CreateFileLines("Makefile", + MkRcsID, + "", + "CATEGORIES=category", + "", + "COMMENT=\tComment", + "LICENSE=\t2-clause-bsd", + "PKGDIR=\t../../other/package") + + // DISTINFO_FILE is resolved relative to PKGDIR, the other places + // are resolved relative to the package base directory. + G.checkdirPackage(".") + + t.CheckOutputLines( + "ERROR: patches/patch-aa:1: Patch files must not be empty.") +} + +func (s *Suite) Test_Pkglint_checkdirPackage__patch_without_distinfo(c *check.C) { + t := s.Init(c) + + pkg := t.SetupPackage("category/package") + t.CreateFileDummyPatch("category/package/patches/patch-aa") + t.Remove("category/package/distinfo") + + G.CheckDirent(pkg) + + // FIXME: One of the below warnings is redundant. + t.CheckOutputLines( + "WARN: ~/category/package/distinfo: File not found. Please run \""+confMake+" makesum\" or define NO_CHECKSUM=yes in the package Makefile.", + "WARN: ~/category/package/distinfo: File not found. Please run \""+confMake+" makepatchsum\".") +} + func (s *Suite) Test_Pkglint_checkdirPackage__meta_package_without_license(c *check.C) { t := s.Init(c) @@ -414,7 +567,7 @@ func (s *Suite) Test_Package__varuse_at_ func (s *Suite) Test_Package_loadPackageMakefile(c *check.C) { t := s.Init(c) - t.SetupFileLines("category/package/Makefile", + t.CreateFileLines("category/package/Makefile", MkRcsID, "", "PKGNAME=pkgname-1.67", @@ -433,7 +586,34 @@ func (s *Suite) Test_Package_loadPackage "NOTE: ~/category/package/Makefile:4: Definition of DISTNAME is redundant because of Makefile:4.") } -func (s *Suite) Test_Package_conditionalAndUnconditionalInclude(c *check.C) { +func (s *Suite) Test_Package_loadPackageMakefile__PECL_VERSION(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("lang/php/ext.mk", + MkRcsID, + "", + "PHPEXT_MK= # defined", + "PHPPKGSRCDIR= ../../lang/php72", + "LICENSE?= unknown-license", + "COMMENT?= Some PHP package", + "GENERATE_PLIST+=# none", + "", + ".if !defined(PECL_VERSION)", + "DISTINFO_FILE= ${.CURDIR}/${PHPPKGSRCDIR}/distinfo", + ".endif", + ".if defined(USE_PHP_EXT_PATCHES)", + "PATCHDIR= ${.CURDIR}/${PHPPKGSRCDIR}/patches", + ".endif") + pkg := t.SetupPackage("category/package", + "PECL_VERSION=\t1.1.2", + ".include \"../../lang/php/ext.mk\"") + + G.CheckDirent(pkg) + + t.CheckOutputLines() +} + +func (s *Suite) Test_Package_CheckInclude__conditional_and_unconditional_include(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -479,7 +659,7 @@ func (s *Suite) Test_Package_conditional } // See https://github.com/rillig/pkglint/issues/1 -func (s *Suite) Test_Package_includeWithoutExists(c *check.C) { +func (s *Suite) Test_Package__include_without_exists(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -494,11 +674,11 @@ func (s *Suite) Test_Package_includeWith G.checkdirPackage(t.File("category/package")) t.CheckOutputLines( - "ERROR: ~/category/package/options.mk: Cannot be read.") + "ERROR: ~/category/package/Makefile:3: Cannot read \"options.mk\".") } // See https://github.com/rillig/pkglint/issues/1 -func (s *Suite) Test_Package_includeAfterExists(c *check.C) { +func (s *Suite) Test_Package__include_after_exists(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -524,7 +704,7 @@ func (s *Suite) Test_Package_includeAfte } // See https://github.com/rillig/pkglint/issues/1 -func (s *Suite) Test_Package_includeOtherAfterExists(c *check.C) { +func (s *Suite) Test_Package_readMakefile__include_other_after_exists(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -541,7 +721,7 @@ func (s *Suite) Test_Package_includeOthe G.checkdirPackage(t.File("category/package")) t.CheckOutputLines( - "ERROR: ~/category/package/another.mk: Cannot be read.") + "ERROR: ~/category/package/Makefile:4: Cannot read \"another.mk\".") } // See https://mail-index.netbsd.org/tech-pkg/2018/07/22/msg020092.html @@ -650,3 +830,232 @@ func (s *Suite) Test_NewPackage(c *check check.PanicMatches, `Package directory "category" must be two subdirectories below the pkgsrc root ".*".`) } + +// Before 2018-09-09, the .CURDIR variable did not have a fallback value. +// When resolving the relative path x11/gst-x11/${.CURDIR}/../../multimedia/gst-base/distinfo, +// "gst-x11/${.CURDIR}" was interpreted as "category/package", and the whole +// path was resolved to "x11/multimedia/gst-base/distinfo, which of course +// could not be found. +func (s *Suite) Test__distinfo_from_other_package(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-space") + t.SetupPkgsrc() + t.Chdir(".") + t.CreateFileLines("x11/gst-x11/Makefile", + MkRcsID, + ".include \"../../multimedia/gst-base/Makefile.common\"", + ".include \"../../mk/bsd.pkg.mk\"") + t.CreateFileLines("multimedia/gst-base/Makefile.common", + MkRcsID, + ".include \"plugins.mk\"") + t.CreateFileLines("multimedia/gst-base/plugins.mk", + MkRcsID, + "DISTINFO_FILE=\t${.CURDIR}/../../multimedia/gst-base/distinfo") + t.CreateFileLines("multimedia/gst-base/distinfo", + RcsID, + "", + "SHA1 (patch-aa) = 1234") + + G.CheckDirent("x11/gst-x11") + + t.CheckOutputLines( + "WARN: x11/gst-x11/Makefile: Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.", + "ERROR: x11/gst-x11/Makefile: Each package must define its LICENSE.", + "WARN: x11/gst-x11/Makefile: No COMMENT given.", + "WARN: x11/gst-x11/../../multimedia/gst-base/distinfo:3: Patch file \"patch-aa\" does not exist in directory \"../../x11/gst-x11/patches\".") +} + +func (s *Suite) Test_Package_checkfilePackageMakefile__GNU_CONFIGURE(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-space") + pkg := t.SetupPackage("category/package", + "GNU_CONFIGURE=\tyes", + "USE_LANGUAGES=\t#") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:20: GNU_CONFIGURE almost always needs a C compiler, but \"c\" is not added to USE_LANGUAGES in line 21.") +} + +func (s *Suite) Test_Package_checkfilePackageMakefile__GNU_CONFIGURE_ok(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-space") + pkg := t.SetupPackage("category/package", + "GNU_CONFIGURE=\tyes", + "USE_LANGUAGES=\t# none, really") + + G.CheckDirent(pkg) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkfilePackageMakefile__REPLACE_PERL(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-space") + pkg := t.SetupPackage("category/package", + "REPLACE_PERL=\t*.pl", + "NO_CONFIGURE=\tyes") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:20: REPLACE_PERL is ignored when NO_CONFIGURE is set (in line 21).") +} + +func (s *Suite) Test_Package_checkfilePackageMakefile__META_PACKAGE_with_distinfo(c *check.C) { + t := s.Init(c) + + pkg := t.SetupPackage("category/package", + "META_PACKAGE=\tyes") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "WARN: ~/category/package/distinfo: " + + "This file should not exist if NO_CHECKSUM or META_PACKAGE is set.") +} + +func (s *Suite) Test_Package_checkfilePackageMakefile__USE_IMAKE_and_USE_X11(c *check.C) { + t := s.Init(c) + + pkg := t.SetupPackage("category/package", + "USE_X11=\tyes", + "USE_IMAKE=\tyes") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "NOTE: ~/category/package/Makefile:21: USE_IMAKE makes USE_X11 in line 20 superfluous.") +} + +func (s *Suite) Test_Package_readMakefile__skipping(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-space") + pkg := t.SetupPackage("category/package", + ".include \"${MYSQL_PKGSRCDIR:S/-client$/-server/}/buildlink3.mk\"") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "NOTE: ~/category/package/Makefile:20: " + + "Skipping include file \"${MYSQL_PKGSRCDIR:S/-client$/-server/}/buildlink3.mk\". " + + "This may result in false warnings.") +} + +func (s *Suite) Test_Package_readMakefile__not_found(c *check.C) { + t := s.Init(c) + + pkg := t.SetupPackage("category/package", + ".include \"../../devel/zlib/buildlink3.mk\"") + t.CreateFileLines("devel/zlib/buildlink3.mk", + ".include \"../../enoent/enoent/buildlink3.mk\"") + + G.checkdirPackage(pkg) + + t.CheckOutputLines( + "ERROR: ~/devel/zlib/buildlink3.mk:1: Cannot read \"../../enoent/enoent/buildlink3.mk\".") +} + +func (s *Suite) Test_Package_readMakefile__relative(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("category/package/extra.mk", + MkRcsID) + pkg := t.SetupPackage("category/package", + ".include \"../package/extra.mk\"") + + G.CheckDirent(pkg) + + // FIXME: One of the below warnings is redundant. + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:20: References to other packages should look like \"../../category/package\", not \"../package\".", + "WARN: ~/category/package/Makefile:20: Invalid relative path \"../package/extra.mk\".") +} + +func (s *Suite) Test_Package_checkLocallyModified(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-order") + G.CurrentUsername = "example-user" + t.CreateFileLines("category/package/CVS/Entries", + "/Makefile//modified//") + + // Since MAINTAINER= pkgsrc-users@NetBSD.org, everyone may commit changes. + + pkg := t.SetupPackage("category/package") + + G.CheckDirent(pkg) + + t.CheckOutputEmpty() + + // A package with a MAINTAINER may be edited with care. + + t.CreateFileLines("category/package/Makefile", + MkRcsID, + "", + "DISTNAME=\tdistname-1.0", + "CATEGORIES=\tcategory", + "MASTER_SITES=\t# none", + "", + "MAINTAINER=\tmaintainer@example.org", // Different from default value + "HOMEPAGE=\t# none", + "COMMENT=\tDummy package", + "LICENSE=\t2-clause-bsd", + "", + ".include \"../../mk/bsd.pkg.mk\"") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "NOTE: ~/category/package/Makefile: " + + "Please only commit changes that maintainer@example.org would approve.") + + // A package with an OWNER may NOT be edited by others. + + pkg = t.SetupPackage("category/package", + "OWNER=\towner@example.org") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "WARN: ~/category/package/Makefile: " + + "Don't commit changes to this file without asking the OWNER, owner@example.org.") + + // ... unless you are the owner, of course. + + G.CurrentUsername = "owner" + + G.CheckDirent(pkg) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Pkglint_checkdirPackage__filename_with_variable(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-order") + pkg := t.SetupPackage("category/package", + ".include \"../../mk/bsd.prefs.mk\"", + "", + "RUBY_VERSIONS_ACCEPTED=\t22 23 24 25", // As of 2018. + ".for rv in ${RUBY_VERSIONS_ACCEPTED}", + "RUBY_VER?=\t\t${rv}", + ".endfor", + "", + "RUBY_PKGDIR=\t../../lang/ruby-${RUBY_VER}-base", + "DISTINFO_FILE=\t${RUBY_PKGDIR}/distinfo") + + // Pkglint cannot currently resolve the location of DISTINFO_FILE completely + // because the variable \"rv\" comes from a .for loop. + // + // TODO: resolve variables in simple .for loops like the above. + G.CheckDirent(pkg) + + t.CheckOutputEmpty() +} Index: pkgsrc/pkgtools/pkglint/files/patches_test.go diff -u pkgsrc/pkgtools/pkglint/files/patches_test.go:1.20 pkgsrc/pkgtools/pkglint/files/patches_test.go:1.21 --- pkgsrc/pkgtools/pkglint/files/patches_test.go:1.20 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/patches_test.go Wed Oct 3 22:27:53 2018 @@ -42,7 +42,7 @@ func (s *Suite) Test_ChecklinesPatch__wi "-old line", "+new line", " context after") - t.SetupFileLines("distinfo", + t.CreateFileLines("distinfo", RcsID, "", // The hash is taken from a breakpoint at the beginning of AutofixDistinfo, oldSha1 @@ -691,8 +691,8 @@ func (s *Suite) Test_PatchChecker_checkt ChecklinesPatch(lines) t.CheckOutputLines( - "WARN: ~/patch-aa:7: Found RCS tag \"$Id: patches_test.go,v 1.20 2018/09/05 17:56:22 rillig Exp $\". Please remove it.", - "WARN: ~/patch-aa:8: Found RCS tag \"$Id: patches_test.go,v 1.20 2018/09/05 17:56:22 rillig Exp $\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".", + "WARN: ~/patch-aa:7: Found RCS tag \"$Id: patches_test.go,v 1.21 2018/10/03 22:27:53 rillig Exp $\". Please remove it.", + "WARN: ~/patch-aa:8: Found RCS tag \"$Id: patches_test.go,v 1.21 2018/10/03 22:27:53 rillig Exp $\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".", "WARN: ~/patch-aa:11: Found RCS tag \"$Author: rillig $\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".") } Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.24 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.25 --- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.24 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go Wed Oct 3 22:27:53 2018 @@ -3,13 +3,14 @@ package main import ( "io/ioutil" "strings" + "time" "gopkg.in/check.v1" "netbsd.org/pkglint/trace" "os" ) -func (s *Suite) Test_Pkglint_Main_help(c *check.C) { +func (s *Suite) Test_Pkglint_Main__help(c *check.C) { t := s.Init(c) exitcode := G.Main("pkglint", "-h") @@ -18,7 +19,7 @@ func (s *Suite) Test_Pkglint_Main_help(c c.Check(t.Output(), check.Matches, `^\Qusage: pkglint [options] dir...\E\n(?s).+`) } -func (s *Suite) Test_Pkglint_Main_version(c *check.C) { +func (s *Suite) Test_Pkglint_Main__version(c *check.C) { t := s.Init(c) exitcode := G.Main("pkglint", "--version") @@ -28,7 +29,7 @@ func (s *Suite) Test_Pkglint_Main_versio confVersion) } -func (s *Suite) Test_Pkglint_Main_no_args(c *check.C) { +func (s *Suite) Test_Pkglint_Main__no_args(c *check.C) { t := s.Init(c) exitcode := G.Main("pkglint") @@ -115,6 +116,18 @@ func (s *Suite) Test_Pkglint_Main__unkno " (Prefix a flag with \"no-\" to disable it.)") } +func (s *Suite) Test_Pkglint_Main__panic(c *check.C) { + t := s.Init(c) + + pkg := t.SetupPackage("category/package") + + G.logOut = nil // Force an error that cannot happen in practice. + + c.Check( + func() { G.Main("pkglint", pkg) }, + check.PanicMatches, `(?s).*\bnil pointer\b.*`) +} + // Demonstrates which infrastructure files are necessary to actually run // pkglint in a realistic scenario. // For most tests, this setup is too much work, therefore they @@ -129,7 +142,7 @@ func (s *Suite) Test_Pkglint_Main__compl // FIXME: pkglint should warn that the latest version in this file // (1.10) doesn't match the current version in the package (1.11). - t.SetupFileLines("doc/CHANGES-2018", + t.CreateFileLines("doc/CHANGES-2018", RcsID, "", "Changes to the packages collection and infrastructure in 2018:", @@ -137,7 +150,7 @@ func (s *Suite) Test_Pkglint_Main__compl "\tUpdated sysutils/checkperms to 1.10 [rillig 2018-01-05]") // See Pkgsrc.loadSuggestedUpdates. - t.SetupFileLines("doc/TODO", + t.CreateFileLines("doc/TODO", RcsID, "", "Suggested package updates", @@ -145,19 +158,19 @@ func (s *Suite) Test_Pkglint_Main__compl "\to checkperms-1.13 [supports more file formats]") // The LICENSE in the package Makefile is searched here. - t.SetupFileLines("licenses/bsd-2", + t.CreateFileLines("licenses/bsd-2", "# dummy") // The MASTER_SITES in the package Makefile are searched here. // See Pkgsrc.loadMasterSites. - t.SetupFileMkLines("mk/fetch/sites.mk", + t.CreateFileLines("mk/fetch/sites.mk", MkRcsID, "", "MASTER_SITE_GITHUB+=\thttps://github.com/") // The existence of this file makes the category "sysutils" valid. // The category "tools" on the other hand is not valid. - t.SetupFileMkLines("sysutils/Makefile", + t.CreateFileLines("sysutils/Makefile", MkRcsID) // The package Makefile is quite simple, containing just the @@ -165,21 +178,21 @@ func (s *Suite) Test_Pkglint_Main__compl // values is partly defined in the pkgsrc infrastructure files // (as defined in the previous lines), and partly in the pkglint // code directly. Many details can be found in vartypecheck.go. - t.SetupFileMkLines("sysutils/checkperms/Makefile", + t.CreateFileLines("sysutils/checkperms/Makefile", MkRcsID, "", "DISTNAME=\tcheckperms-1.11", "CATEGORIES=\tsysutils tools", "MASTER_SITES=\t${MASTER_SITE_GITHUB:=rillig/}", "", - "MAINTAINER=\tpkgsrc-users@pkgsrc.org", + "MAINTAINER=\tpkgsrc-users@NetBSD.org", "HOMEPAGE=\thttps://github.com/rillig/checkperms/", "COMMENT=\tCheck file permissions", "LICENSE=\tbsd-2", "", ".include \"../../mk/bsd.pkg.mk\"") - t.SetupFileLines("sysutils/checkperms/MESSAGE", + t.CreateFileLines("sysutils/checkperms/MESSAGE", "===========================================================================", RcsID, "", @@ -187,18 +200,18 @@ func (s *Suite) Test_Pkglint_Main__compl "", "===========================================================================") - t.SetupFileLines("sysutils/checkperms/PLIST", + t.CreateFileLines("sysutils/checkperms/PLIST", PlistRcsID, "bin/checkperms", "man/man1/checkperms.1") - t.SetupFileLines("sysutils/checkperms/README", + t.CreateFileLines("sysutils/checkperms/README", "When updating this package, test the pkgsrc bootstrap.") - t.SetupFileLines("sysutils/checkperms/TODO", + t.CreateFileLines("sysutils/checkperms/TODO", "Make the package work on MS-DOS") - t.SetupFileLines("sysutils/checkperms/patches/patch-checkperms.c", + t.CreateFileLines("sysutils/checkperms/patches/patch-checkperms.c", RcsID, "", "A simple patch demonstrating that pkglint checks for missing", @@ -211,7 +224,7 @@ func (s *Suite) Test_Pkglint_Main__compl "+// Header 1", "+// Header 2", "+// Header 3") - t.SetupFileLines("sysutils/checkperms/distinfo", + t.CreateFileLines("sysutils/checkperms/distinfo", RcsID, "", "SHA1 (checkperms-1.12.tar.gz) = 34c084b4d06bcd7a8bba922ff57677e651eeced5", @@ -242,7 +255,7 @@ func (s *Suite) Test_Pkglint_Main__compl // pkgsrcdir=... // env PKGLINT_TESTCMDLINE="$pkgsrcdir -r" ./pkglint.test -test.coverprofile pkglint.cov // go tool cover -html=pkglint.cov -o coverage.html -func (s *Suite) Test_Pkglint_coverage(c *check.C) { +func (s *Suite) Test_Pkglint__coverage(c *check.C) { cmdline := os.Getenv("PKGLINT_TESTCMDLINE") if cmdline != "" { G.logOut, G.logErr, trace.Out = NewSeparatorWriter(os.Stdout), NewSeparatorWriter(os.Stderr), os.Stdout @@ -253,7 +266,7 @@ func (s *Suite) Test_Pkglint_coverage(c func (s *Suite) Test_Pkglint_CheckDirent__outside(c *check.C) { t := s.Init(c) - t.SetupFileLines("empty") + t.CreateFileLines("empty") G.CheckDirent(t.File(".")) @@ -261,13 +274,55 @@ func (s *Suite) Test_Pkglint_CheckDirent "ERROR: ~: Cannot determine the pkgsrc root directory for \"~\".") } +func (s *Suite) Test_Pkglint_CheckDirent__empty_directory(c *check.C) { + t := s.Init(c) + + t.SetupPkgsrc() + t.CreateFileLines("category/package/CVS/Entries") + + G.CheckDirent(t.File("category/package")) + + // Empty directories are silently skipped. + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Pkglint_CheckDirent__files_directory(c *check.C) { + t := s.Init(c) + + t.SetupPkgsrc() + t.CreateFileLines("category/package/files/README.md") + + G.CheckDirent(t.File("category/package/files")) + + // This diagnostic is not really correct, but it's an edge case anyway. + t.CheckOutputLines( + "ERROR: ~/category/package/files: Cannot check directories outside a pkgsrc tree.") +} + +func (s *Suite) Test_Pkglint_CheckDirent__manual_patch(c *check.C) { + t := s.Init(c) + + t.SetupPkgsrc() + t.CreateFileLines("category/package/patches/manual-configure") + t.CreateFileLines("category/package/Makefile", + MkRcsID) + + G.CheckDirent(t.File("category/package")) + + t.CheckOutputLines( + "WARN: ~/category/package/Makefile: Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.", + "WARN: ~/category/package/distinfo: File not found. Please run \""+confMake+" makesum\" or define NO_CHECKSUM=yes in the package Makefile.", + "ERROR: ~/category/package/Makefile: Each package must define its LICENSE.", + "WARN: ~/category/package/Makefile: No COMMENT given.") +} + func (s *Suite) Test_Pkglint_CheckDirent(c *check.C) { t := s.Init(c) - t.SetupFileLines("mk/bsd.pkg.mk") - t.SetupFileLines("category/package/Makefile") - t.SetupFileLines("category/Makefile") - t.SetupFileLines("Makefile") + t.CreateFileLines("mk/bsd.pkg.mk") + t.CreateFileLines("category/package/Makefile") + t.CreateFileLines("category/Makefile") + t.CreateFileLines("Makefile") G.CheckDirent(t.File(".")) @@ -434,10 +489,11 @@ func (s *Suite) Test_Pkglint__profiling( t := s.Init(c) t.SetupPkgsrc() - G.Main("pkglint", "--profiling", t.File(".")) + t.Chdir(".") + + G.Main("pkglint", "--profiling") // Pkglint always writes the profiling data into the current directory. - // Luckily, this directory is usually writable. c.Check(fileExists("pkglint.pprof"), equals, true) err := os.Remove("pkglint.pprof") @@ -447,7 +503,20 @@ func (s *Suite) Test_Pkglint__profiling( // or not interesting enough, since that info includes the exact timing // that the top time-consuming regular expressions took. firstOutput := strings.Split(t.Output(), "\n")[0] - c.Check(firstOutput, equals, "ERROR: ~/Makefile: Cannot be read.") + c.Check(firstOutput, equals, "ERROR: Makefile: Cannot be read.") +} + +func (s *Suite) Test_Pkglint__profiling_error(c *check.C) { + t := s.Init(c) + + t.SetupPkgsrc() + t.Chdir(".") + t.CreateFileLines("pkglint.pprof/file") + + exitcode := G.Main("pkglint", "--profiling") + + c.Check(exitcode, equals, 1) + c.Check(t.Output(), check.Matches, `^FATAL: Cannot create profiling file: open pkglint\.pprof: .*\n$`) } func (s *Suite) Test_Pkglint_Checkfile__in_current_working_directory(c *check.C) { @@ -594,35 +663,17 @@ func (s *Suite) Test_Pkglint_ToolByVarna c.Check(G.ToolByVarname("TOOL", RunTime), equals, global) } -func (s *Suite) Test_Pkglint_Checkfile__CheckExtra(c *check.C) { +func (s *Suite) Test_CheckfileExtra(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Call", "-Wall,no-space") - t.SetupPkgsrc() - G.Pkgsrc.LoadInfrastructure() - t.CreateFileLines("licenses/gnu-gpl-2.0") - t.CreateFileLines("category/Makefile") - t.CreateFileLines("category/package/Makefile", - MkRcsID, - "", - "DISTNAME= pkgname-1.0", - "CATEGORIES= category", - "", - "COMMENT= Comment", - "LICENSE= gnu-gpl-2.0", - "", - "NO_CHECKSUM= yes", - "", - ".include \"../../mk/bsd.pkg.mk\"") - t.CreateFileLines("category/package/PLIST", - PlistRcsID, - "bin/program") + pkg := t.SetupPackage("category/package") t.CreateFileLines("category/package/INSTALL", "#! /bin/sh") t.CreateFileLines("category/package/DEINSTALL", "#! /bin/sh") - G.CheckDirent(t.File("category/package")) + G.CheckDirent(pkg) t.CheckOutputEmpty() } @@ -631,31 +682,13 @@ func (s *Suite) Test_Pkglint_Checkfile__ t := s.Init(c) t.SetupCommandLine("-Call", "-Wall,no-space", "--import") - t.SetupPkgsrc() - G.Pkgsrc.LoadInfrastructure() - t.CreateFileLines("licenses/gnu-gpl-2.0") - t.CreateFileLines("category/Makefile") - t.CreateFileLines("category/package/Makefile", - MkRcsID, - "", - "DISTNAME= pkgname-1.0", - "CATEGORIES= category", - "", - "COMMENT= Comment", - "LICENSE= gnu-gpl-2.0", - "", - "NO_CHECKSUM= yes", - "", - ".include \"../../mk/bsd.pkg.mk\"") - t.CreateFileLines("category/package/PLIST", - PlistRcsID, - "bin/program") + pkg := t.SetupPackage("category/package") t.CreateFileLines("category/package/work/log") t.CreateFileLines("category/package/Makefile~") t.CreateFileLines("category/package/Makefile.orig") t.CreateFileLines("category/package/Makefile.rej") - G.CheckDirent(t.File("category/package")) + G.CheckDirent(pkg) t.CheckOutputLines( "ERROR: ~/category/package/Makefile.orig: Must be cleaned up before committing the package.", @@ -775,3 +808,145 @@ func (s *Suite) Test_Pkglint_Checkfile__ "ERROR: wip/package/TODO: Must be cleaned up before committing the package.", "4 errors and 0 warnings found.") } + +func (s *Suite) Test_Pkglint_Checkfile__unknown_file_in_patches(c *check.C) { + t := s.Init(c) + + t.CreateFileDummyPatch("category/Makefile/patches/index") + + G.Checkfile(t.File("category/Makefile/patches/index")) + + t.CheckOutputLines( + "WARN: ~/category/Makefile/patches/index: " + + "Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.") +} + +func (s *Suite) Test_Pkglint_Checkfile__file_in_files(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("category/package/files/index") + + G.Checkfile(t.File("category/package/files/index")) + + // These files are ignored since they could contain anything. + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Pkglint_Checkfile__spec(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("category/package/spec") + t.CreateFileLines("regress/package/spec") + + G.Checkfile(t.File("category/package/spec")) + G.Checkfile(t.File("regress/package/spec")) + + t.CheckOutputLines( + "WARN: ~/category/package/spec: Only packages in regress/ may have spec files.") +} + +func (s *Suite) Test_Pkglint_checkMode__skipped(c *check.C) { + t := s.Init(c) + + G.checkMode("work", os.ModeSymlink) + G.checkMode("work.i386", os.ModeSymlink) + G.checkMode("work.hostname", os.ModeSymlink) + G.checkMode("other", os.ModeSymlink) + + G.checkMode("device", os.ModeDevice) + + t.CheckOutputLines( + "WARN: other: Unknown symlink name.", + "ERROR: device: Only files and directories are allowed in pkgsrc.") +} + +func (s *Suite) Test_Pkglint_checkdirPackage__ALTERNATIVES(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall,no-space") + pkg := t.SetupPackage("category/package") + t.CreateFileLines("category/package/ALTERNATIVES", + "bin/wrapper bin/wrapper-impl") + + G.CheckDirent(pkg) + + t.CheckOutputLines( + "ERROR: ~/category/package/ALTERNATIVES:1: " + + "Alternative implementation \"bin/wrapper-impl\" must appear in the PLIST.") +} + +func (s *Suite) Test_CheckfileMk__enoent(c *check.C) { + t := s.Init(c) + + CheckfileMk(t.File("fname.mk")) + + t.CheckOutputLines( + "ERROR: ~/fname.mk: Cannot be read.") +} + +func (s *Suite) Test_Pkglint_checkExecutable(c *check.C) { + t := s.Init(c) + + G.checkExecutable(ExecutableFileInfo{t.File("fname.mk")}) + + t.CheckOutputLines( + "WARN: ~/fname.mk: Should not be executable.") + + t.SetupCommandLine("--autofix") + + G.checkExecutable(ExecutableFileInfo{t.File("fname.mk")}) + + // FIXME: The error message "Cannot clear executable bits" is swallowed. + t.CheckOutputLines( + "AUTOFIX: ~/fname.mk: Clearing executable bits") +} + +func (s *Suite) Test_main(c *check.C) { + t := s.Init(c) + + out, err := os.Create(t.CreateFileLines("out")) + c.Check(err, check.IsNil) + + pkg := t.SetupPackage("category/package") + + func() { + args := os.Args + stdout := os.Stdout + stderr := os.Stderr + prevExit := exit + defer func() { + os.Stderr = stderr + os.Stdout = stdout + os.Args = args + exit = prevExit + }() + os.Args = []string{"pkglint", pkg} + os.Stdout = out + os.Stderr = out + exit = func(code int) { + c.Check(code, equals, 0) + } + + main() + }() + + err = out.Close() + c.Check(err, check.IsNil) + + t.CheckOutputEmpty() + t.CheckFileLines("out", + "Looks fine.") +} + +// ExecutableFileInfo mocks a FileInfo because on Windows, +// regular files don't have the executable bit. +type ExecutableFileInfo struct { + name string +} + +func (i ExecutableFileInfo) Name() string { return i.name } +func (i ExecutableFileInfo) Size() int64 { return 13 } +func (i ExecutableFileInfo) Mode() os.FileMode { return 0777 } +func (i ExecutableFileInfo) ModTime() time.Time { return time.Unix(0, 0) } +func (i ExecutableFileInfo) IsDir() bool { return false } +func (i ExecutableFileInfo) Sys() interface{} { return nil } Index: pkgsrc/pkgtools/pkglint/files/plist.go diff -u pkgsrc/pkgtools/pkglint/files/plist.go:1.28 pkgsrc/pkgtools/pkglint/files/plist.go:1.29 --- pkgsrc/pkgtools/pkglint/files/plist.go:1.28 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/plist.go Wed Oct 3 22:27:53 2018 @@ -1,7 +1,6 @@ package main import ( - "netbsd.org/pkglint/regex" "netbsd.org/pkglint/trace" "path" "sort" @@ -300,7 +299,7 @@ func (ck *PlistChecker) checkpathLib(pli } func (ck *PlistChecker) checkpathMan(pline *PlistLine) { - m, catOrMan, section, manpage, ext, gz := regex.Match5(pline.text, `^man/(cat|man)(\w+)/(.*?)\.(\w+)(\.gz)?$`) + m, catOrMan, section, manpage, ext, gz := match5(pline.text, `^man/(cat|man)(\w+)/(.*?)\.(\w+)(\.gz)?$`) if !m { // maybe: line.Warnf("Invalid filename %q for manual page.", text) return Index: pkgsrc/pkgtools/pkglint/files/util.go diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.28 pkgsrc/pkgtools/pkglint/files/util.go:1.29 --- pkgsrc/pkgtools/pkglint/files/util.go:1.28 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/util.go Wed Oct 3 22:27:53 2018 @@ -13,6 +13,7 @@ import ( "strconv" "strings" "time" + "unicode" ) type YesNoUnknown uint8 @@ -38,19 +39,32 @@ func hasSuffix(s, suffix string) bool { return strings.HasSuffix(s, suffix) } func matches(s string, re regex.Pattern) bool { - return regex.Matches(s, re) + return G.res.Matches(s, re) } func match1(s string, re regex.Pattern) (matched bool, m1 string) { - return regex.Match1(s, re) + return G.res.Match1(s, re) } func match2(s string, re regex.Pattern) (matched bool, m1, m2 string) { - return regex.Match2(s, re) + return G.res.Match2(s, re) } func match3(s string, re regex.Pattern) (matched bool, m1, m2, m3 string) { - return regex.Match3(s, re) + return G.res.Match3(s, re) } func match4(s string, re regex.Pattern) (matched bool, m1, m2, m3, m4 string) { - return regex.Match4(s, re) + return G.res.Match4(s, re) +} +func match5(s string, re regex.Pattern) (matched bool, m1, m2, m3, m4, m5 string) { + return G.res.Match5(s, re) +} +func replaceFirst(s string, re regex.Pattern, repl string) string { + m, replaced := G.res.ReplaceFirst(s, re, repl) + return ifelseStr(m != nil, replaced, s) +} +func replaceAll(s string, re regex.Pattern, repl string) string { + return G.res.Compile(re).ReplaceAllString(s, repl) +} +func replaceAllFunc(s string, re regex.Pattern, repl func(string) string) string { + return G.res.Compile(re).ReplaceAllStringFunc(s, repl) } func ifelseStr(cond bool, a, b string) string { @@ -77,7 +91,7 @@ func imax(a, b int) int { } func mustMatch(s string, re regex.Pattern) []string { - if m := regex.Match(s, re); m != nil { + if m := G.res.Match(s, re); m != nil { return m } panic(fmt.Sprintf("mustMatch %q %q", s, re)) @@ -138,12 +152,9 @@ func isCommitted(fname string) bool { } func isLocallyModified(fname string) bool { - lines := loadCvsEntries(fname) - if len(lines) == 0 { - return false - } - baseName := path.Base(fname) + + lines := loadCvsEntries(fname) for _, line := range lines { fields := strings.Split(line.Text, "/") if 3 < len(fields) && fields[1] == baseName { @@ -156,7 +167,7 @@ func isLocallyModified(fname string) boo cvsModTime := fields[3] fsModTime := st.ModTime().Format(time.ANSIC) if trace.Tracing { - trace.Stepf("cvs.time=%q fs.time=%q", cvsModTime, st.ModTime()) + trace.Stepf("cvs.time=%q fs.time=%q", cvsModTime, fsModTime) } return cvsModTime != fsModTime @@ -220,21 +231,21 @@ func shorten(s string, maxChars int) str func varnameBase(varname string) string { dot := strings.IndexByte(varname, '.') - if dot != -1 { + if dot > 0 { return varname[:dot] } return varname } func varnameCanon(varname string) string { dot := strings.IndexByte(varname, '.') - if dot != -1 { + if dot > 0 { return varname[:dot] + ".*" } return varname } func varnameParam(varname string) string { dot := strings.IndexByte(varname, '.') - if dot != -1 { + if dot > 0 { return varname[dot+1:] } return "" @@ -267,7 +278,28 @@ func varIsUsed(varname string) bool { } func splitOnSpace(s string) []string { - return regex.Compile(`\S+`).FindAllString(s, -1) + i := 0 + n := len(s) + + for i < n && unicode.IsSpace(rune(s[i])) { + i++ + } + + var parts []string + for i < n { + start := i + for i < n && !unicode.IsSpace(rune(s[i])) { + i++ + } + if start != i { + parts = append(parts, s[start:i]) + } + for i < n && unicode.IsSpace(rune(s[i])) { + i++ + } + } + + return parts } func fileExists(fname string) bool { @@ -317,7 +349,7 @@ func mkopSubst(s string, left bool, from re := regex.Pattern(ifelseStr(left, "^", "") + regexp.QuoteMeta(from) + ifelseStr(right, "$", "")) done := false gflag := contains(flags, "g") - return regex.Compile(re).ReplaceAllStringFunc(s, func(match string) string { + return replaceAllFunc(s, re, func(match string) string { if gflag || !done { done = !gflag return to @@ -328,13 +360,10 @@ func mkopSubst(s string, left bool, from // relpath returns the relative path from `from` to `to`. func relpath(from, to string) string { - absFrom, err1 := filepath.Abs(from) - absTo, err2 := filepath.Abs(to) - rel, err3 := filepath.Rel(absFrom, absTo) - if err1 != nil || err2 != nil || err3 != nil { - trace.Stepf("relpath.panic", from, to, err1, err2, err3) - panic(fmt.Sprintf("relpath %q, %q", from, to)) - } + absFrom := abspath(from) + absTo := abspath(to) + rel, err := filepath.Rel(absFrom, absTo) + G.Assertf(err == nil, "relpath %q %q.", from, to) result := filepath.ToSlash(rel) if trace.Tracing { trace.Stepf("relpath from %q to %q = %q", from, to, result) @@ -344,9 +373,7 @@ func relpath(from, to string) string { func abspath(fname string) string { abs, err := filepath.Abs(fname) - if err != nil { - NewLineWhole(fname).Fatalf("Cannot determine absolute path.") - } + G.Assertf(err == nil, "abspath %q.", fname) return filepath.ToSlash(abs) } @@ -367,7 +394,24 @@ func cleanpath(fname string) string { for contains(tmp, "//") { tmp = strings.Replace(tmp, "//", "/", -1) } - tmp = reReplaceRepeatedly(tmp, `/[^.][^/]*/[^.][^/]*/\.\./\.\./`, "/") + + // Repeatedly replace `/[^.][^/]*/[^.][^/]*/\.\./\.\./` with "/" +again: + slash0 := -1 + slash1 := -1 + slash2 := -1 + for i, ch := range []byte(tmp) { + if ch == '/' { + slash0 = slash1 + slash1 = slash2 + slash2 = i + if slash0 != -1 && tmp[slash0+1:slash1] != ".." && tmp[slash1+1:slash2] != ".." && hasPrefix(tmp[i:], "/../../") { + tmp = tmp[:slash0] + tmp[i+6:] + goto again + } + } + } + tmp = strings.TrimSuffix(tmp, "/") return tmp } @@ -376,14 +420,6 @@ func containsVarRef(s string) bool { return contains(s, "${") } -func reReplaceRepeatedly(from string, re regex.Pattern, to string) string { - replaced := regex.Compile(re).ReplaceAllString(from, to) - if replaced != from { - return reReplaceRepeatedly(replaced, re, to) - } - return replaced -} - func hasAlnumPrefix(s string) bool { if s == "" { return false @@ -412,12 +448,13 @@ func (o *Once) FirstTime(what string) bo // Scope remembers which variables are defined and which are used // in a certain scope, such as a package or a file. type Scope struct { - defined map[string]MkLine - used map[string]MkLine + defined map[string]MkLine + fallback map[string]string + used map[string]MkLine } func NewScope() Scope { - return Scope{make(map[string]MkLine), make(map[string]MkLine)} + return Scope{make(map[string]MkLine), make(map[string]string), make(map[string]MkLine)} } // Define marks the variable and its canonicalized form as defined. @@ -437,6 +474,10 @@ func (s *Scope) Define(varname string, m } } +func (s *Scope) Fallback(varname string, value string) { + s.fallback[varname] = value +} + // Use marks the variable and its canonicalized form as used. func (s *Scope) Use(varname string, line MkLine) { if s.used[varname] == nil { @@ -514,12 +555,21 @@ func (s *Scope) Value(varname string) (v if mkline != nil { return mkline.Value(), true } + if fallback, ok := s.fallback[varname]; ok { + return fallback, true + } return "", false } func (s *Scope) DefineAll(other Scope) { - for varname, mkline := range other.defined { - s.Define(varname, mkline) + var varnames []string + for varname := range other.defined { + varnames = append(varnames, varname) + } + sort.Strings(varnames) + + for _, varname := range varnames { + s.Define(varname, other.defined[varname]) } } @@ -690,3 +740,102 @@ func IsPrefs(fileName string) bool { } return false } + +func isalnum(s string) bool { + for _, ch := range []byte(s) { + if !('0' <= ch && ch <= '9' || 'A' <= ch && ch <= 'Z' || ch == '_' || 'a' <= ch && ch <= 'z') { + return false + } + } + return true +} + +// FileCache reduces the IO load for commonly loaded files by about 50%, +// especially for buildlink3.mk and *.buildlink3.mk files. +type FileCache struct { + table []*fileCacheEntry + mapping map[string]*fileCacheEntry // Pointers into FileCache.table + hits int + misses int +} + +type fileCacheEntry struct { + count int + fileName string + options LoadOptions + lines []Line +} + +func NewFileCache(size int) *FileCache { + return &FileCache{ + make([]*fileCacheEntry, 0, size), + make(map[string]*fileCacheEntry), + 0, + 0} +} + +func (c *FileCache) Put(fileName string, options LoadOptions, lines []Line) { + key := c.key(fileName) + + entry := c.mapping[key] + if entry == nil { + if len(c.table) == cap(c.table) { + sort.Slice(c.table, func(i, j int) bool { return c.table[j].count < c.table[i].count }) + minCount := c.table[len(c.table)-1].count + newLen := len(c.table) + for newLen > 0 && c.table[newLen-1].count == minCount { + delete(c.mapping, c.key(c.table[newLen-1].fileName)) + newLen-- + } + c.table = c.table[0:newLen] + + // To avoid files from getting stuck in the cache. + for _, e := range c.table { + e.count /= 2 + } + } + + entry = &fileCacheEntry{0, fileName, options, lines} + c.table = append(c.table, entry) + c.mapping[key] = entry + } + + entry.count = 0 + entry.fileName = fileName + entry.options = options + entry.lines = lines +} + +func (c *FileCache) Get(fileName string, options LoadOptions) []Line { + key := c.key(fileName) + entry, found := c.mapping[key] + if found && entry.options == options { + c.hits++ + entry.count++ + + lines := make([]Line, len(entry.lines)) + for i, line := range entry.lines { + lines[i] = NewLineMulti(fileName, int(line.firstLine), int(line.lastLine), line.Text, line.raw) + } + return lines + } + c.misses++ + return nil +} + +func (c *FileCache) Evict(fileName string) { + key := c.key(fileName) + entry, found := c.mapping[key] + if found { + delete(c.mapping, key) + + sort.Slice(c.table, func(i, j int) bool { + return c.table[j] == entry && c.table[i] != entry + }) + c.table = c.table[0 : len(c.table)-1] + } +} + +func (c *FileCache) key(fileName string) string { + return path.Clean(fileName) +} Index: pkgsrc/pkgtools/pkglint/files/shell_test.go diff -u pkgsrc/pkgtools/pkglint/files/shell_test.go:1.30 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.31 --- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.30 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/shell_test.go Wed Oct 3 22:27:53 2018 @@ -2,7 +2,6 @@ package main import ( "gopkg.in/check.v1" - "netbsd.org/pkglint/textproc" ) func (s *Suite) Test_splitIntoShellTokens__line_continuation(c *check.C) { @@ -27,7 +26,7 @@ func (s *Suite) Test_splitIntoShellToken func (s *Suite) Test_splitIntoShellTokens__dollar_subshell(c *check.C) { words, rest := splitIntoShellTokens(dummyLine, "id=$$(${AWK} '{print}' < ${WRKSRC}/idfile) && echo \"$$id\"") - c.Check(words, deepEquals, []string{"id=", "$$(", "${AWK}", "'{print}'", "<", "${WRKSRC}/idfile", ")", "&&", "echo", "\"$$id\""}) + c.Check(words, deepEquals, []string{"id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)", "&&", "echo", "\"$$id\""}) c.Check(rest, equals, "") } @@ -138,6 +137,10 @@ func (s *Suite) Test_ShellLine_CheckShel t.SetupCommandLine("-Wall") t.SetupVartypes() + t.SetupToolUsable("awk", "AWK") + t.SetupToolUsable("cp", "CP") + t.SetupToolUsable("mkdir", "MKDIR") // This is actually "mkdir -p". + t.SetupToolUsable("unzip", "UNZIP_CMD") checkShellCommandLine := func(shellCommand string) { G.Mk = t.NewMkLines("fname", @@ -157,6 +160,7 @@ func (s *Suite) Test_ShellLine_CheckShel t.CheckOutputLines( "WARN: fname:1: Unknown shell command \"uname\".", + "WARN: fname:1: Please switch to \"set -e\" mode before using a semicolon (after \"uname=`uname`\") to separate commands.", "WARN: fname:1: Unknown shell command \"echo\".", "WARN: fname:1: Unknown shell command \"echo\".") @@ -216,9 +220,7 @@ func (s *Suite) Test_ShellLine_CheckShel checkShellCommandLine("${RUN} subdir=\"`unzip -c \"$$e\" install.rdf | awk '/re/ { print \"hello\" }'`\"") t.CheckOutputLines( - "WARN: fname:1: The exitcode of \"unzip\" at the left of the | operator is ignored.", - "WARN: fname:1: Unknown shell command \"unzip\".", - "WARN: fname:1: Unknown shell command \"awk\".") + "WARN: fname:1: The exitcode of \"unzip\" at the left of the | operator is ignored.") // From mail/thunderbird/Makefile, rev. 1.159 checkShellCommandLine("" + @@ -232,12 +234,7 @@ func (s *Suite) Test_ShellLine_CheckShel t.CheckOutputLines( "WARN: fname:1: XPI_FILES is used but not defined.", - "WARN: fname:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.", - "WARN: fname:1: UNZIP_CMD is used but not defined.", - "WARN: fname:1: Unknown shell command \"awk\".", - "WARN: fname:1: Unknown shell command \"${MKDIR}\".", - "WARN: fname:1: MKDIR is used but not defined.", - "WARN: fname:1: UNZIP_CMD is used but not defined.") + "WARN: fname:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.") // From x11/wxGTK28/Makefile checkShellCommandLine("" + @@ -255,8 +252,18 @@ func (s *Suite) Test_ShellLine_CheckShel checkShellCommandLine("@cp from to") t.CheckOutputLines( - "WARN: fname:1: The shell command \"cp\" should not be hidden.", - "WARN: fname:1: Unknown shell command \"cp\".") + "WARN: fname:1: The shell command \"cp\" should not be hidden.") + + checkShellCommandLine("-cp from to") + + t.CheckOutputLines( + "WARN: fname:1: Using a leading \"-\" to suppress errors is deprecated.") + + checkShellCommandLine("-${MKDIR} deeply/nested/subdir") + + t.CheckOutputLines( + "NOTE: fname:1: You don't need to use \"-\" before \"${MKDIR} deeply/nested/subdir\".", + "WARN: fname:1: Using a leading \"-\" to suppress errors is deprecated.") G.Pkg = NewPackage(t.File("category/pkgbase")) G.Pkg.PlistDirs["share/pkgbase"] = true @@ -283,7 +290,7 @@ func (s *Suite) Test_ShellLine_CheckShel t.CheckOutputEmpty() // No warning about missing error checking. } -func (s *Suite) Test_ShellLine_CheckShellCommandLine_strip(c *check.C) { +func (s *Suite) Test_ShellLine_CheckShellCommandLine__strip(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -344,8 +351,7 @@ func (s *Suite) Test_ShellLine_CheckShel "AUTOFIX: Makefile:1: Replacing \"${PKGNAME:Q}\" with \"${PKGNAME}\".") } -// Simple commands like echo(1) or printf(1) are assumed to never fail. -func (s *Suite) Test_ShellLine_CheckShellCommandLine__exitcode(c *check.C) { +func (s *Suite) Test_ShellProgramChecker_checkPipeExitcode(c *check.C) { t := s.Init(c) t.SetupCommandLine("-Wall") @@ -363,7 +369,10 @@ func (s *Suite) Test_ShellLine_CheckShel "\t cat | echo | right-side", "\t echo | cat | right-side", "\t sed s,s,s, filename | right-side", - "\t sed s,s,s < input | right-side") + "\t sed s,s,s < input | right-side", + "\t ./unknown | right-side", + "\t var=value | right-side", + "\t if :; then :; fi | right-side") for _, mkline := range G.Mk.mklines { shline := NewShellLine(mkline) @@ -375,7 +384,9 @@ func (s *Suite) Test_ShellLine_CheckShel "WARN: Makefile:5: The exitcode of \"cat\" at the left of the | operator is ignored.", "WARN: Makefile:6: The exitcode of \"cat\" at the left of the | operator is ignored.", "WARN: Makefile:7: The exitcode of \"sed\" at the left of the | operator is ignored.", - "WARN: Makefile:8: The exitcode of \"sed\" at the left of the | operator is ignored.") + "WARN: Makefile:8: The exitcode of \"sed\" at the left of the | operator is ignored.", + "WARN: Makefile:9: The exitcode of \"./unknown\" at the left of the | operator is ignored.", + "WARN: Makefile:11: The exitcode of the command at the left of the | operator is ignored.") } func (s *Suite) Test_ShellLine_CheckShellCommandLine__autofix(c *check.C) { @@ -423,7 +434,7 @@ func (s *Suite) Test_ShellLine_CheckShel "WARN: fname:1: Unknown shell command \"echo\".") } -func (s *Suite) Test_ShellLine_CheckShelltext__dollar_without_variable(c *check.C) { +func (s *Suite) Test_ShellLine_CheckShellCommandLine__dollar_without_variable(c *check.C) { t := s.Init(c) t.SetupVartypes() @@ -511,6 +522,44 @@ func (s *Suite) Test_ShellLine_CheckWord t.CheckOutputEmpty() } +func (s *Suite) Test_ShellLine_CheckWord__backslash_plus(c *check.C) { + t := s.Init(c) + + shline := t.NewShellLine("fname", 1, "\tfind . -exec rm -rf {} \\+") + + shline.CheckShellCommandLine(shline.mkline.ShellCommand()) + + // FIXME: A backslash before any other character than "\` keeps its original meaning. + t.CheckOutputLines( + "WARN: fname:1: Pkglint parse error in ShellLine.CheckWord at \"\\\\+\" (quoting=plain), rest: \\+") +} + +func (s *Suite) Test_ShellLine_CheckWord__squot_dollar(c *check.C) { + t := s.Init(c) + + shline := t.NewShellLine("fname", 1, "\t'$") + + shline.CheckWord(shline.mkline.ShellCommand(), false, RunTime) + + // FIXME: Should be parsed correctly. Make passes the dollar through (probably), + // and the shell parser should complain about the unfinished string literal. + t.CheckOutputLines( + "WARN: fname:1: Pkglint parse error in ShellLine.CheckWord at \"'$\" (quoting=s), rest: $") +} + +func (s *Suite) Test_ShellLine_CheckWord__dquot_dollar(c *check.C) { + t := s.Init(c) + + shline := t.NewShellLine("fname", 1, "\t\"$") + + shline.CheckWord(shline.mkline.ShellCommand(), false, RunTime) + + // FIXME: Should be parsed correctly. Make passes the dollar through (probably), + // and the shell parser should complain about the unfinished string literal. + t.CheckOutputLines( + "WARN: fname:1: Pkglint parse error in ShellLine.CheckWord at \"\\\"$\" (quoting=d), rest: $") +} + func (s *Suite) Test_ShellLine_CheckWord__dollar_subshell(c *check.C) { t := s.Init(c) @@ -522,6 +571,67 @@ func (s *Suite) Test_ShellLine_CheckWord "WARN: fname:1: Invoking subshells via $(...) is not portable enough.") } +func (s *Suite) Test_ShellLine_unescapeBackticks__unfinished(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall") + mklines := t.NewMkLines("fname.mk", + MkRcsID, + "", + "pre-configure:", + "\t`${VAR}", // Error in first shell word + "\techo `${VAR}") // Error after first shell word + + // Breakpoint in ShellLine.CheckShellCommand + // Breakpoint in ShellLine.CheckToken + // Breakpoint in ShellLine.unescapeBackticks + mklines.Check() + + // FIXME: Mention the unfinished backquote. + t.CheckOutputLines( + "WARN: fname.mk:4: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}", + "WARN: fname.mk:5: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"echo\"}") +} + +func (s *Suite) Test_ShellLine_unescapeBackticks__unfinished_direct(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall") + + // This call is unrealistic. It doesn't happen in practice, and this + // direct, forcing test is only to reach the code coverage. + NewShellLine(dummyMkLine).unescapeBackticks( + "dummy", + G.NewPrefixReplacer(""), + shqBackt) + + t.CheckOutputLines( + "ERROR: Unfinished backquotes: ") +} + +func (s *Suite) Test_ShellLine_variableNeedsQuoting(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall") + t.SetupVartypes() + t.SetupToolUsable("cp", "") + mklines := t.NewMkLines("fname.mk", + MkRcsID, + "", + // It's a bit silly to use shell variables in CONFIGURE_ARGS, + // but currently that's the only way to run ShellLine.variableNeedsQuoting. + "CONFIGURE_ARGS+=\t; cp $$dir $$\\# $$target", + "pre-configure:", + "\tcp $$dir $$\\# $$target") + + mklines.Check() + + // Quoting check is currently disabled for real shell commands. + // See ShellLine.CheckShellCommand, spc.checkWord. + t.CheckOutputLines( + "WARN: fname.mk:3: Unquoted shell variable \"target\".") +} + func (s *Suite) Test_ShellLine_CheckShellCommandLine__echo(c *check.C) { t := s.Init(c) @@ -689,7 +799,7 @@ func (s *Suite) Test_ShellLine_unescapeB shline := t.NewShellLine("dummy.mk", 13, "# dummy") // foobar="`echo \"foo bar\" "\ " "three"`" text := "foobar=\"`echo \\\"foo bar\\\" \"\\ \" \"three\"`\"" - repl := textproc.NewPrefixReplacer(text) + repl := G.NewPrefixReplacer(text) repl.AdvanceStr("foobar=\"`") backtCommand, newQuoting := shline.unescapeBackticks(text, repl, shqDquotBackt) @@ -761,6 +871,64 @@ func (s *Suite) Test_ShellLine_CheckShel "WARN: Makefile:3: Found absolute pathname: /etc/passwd") } +func (s *Suite) Test_ShellLine_CheckShellCommand__subshell(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("Makefile", + MkRcsID, + "", + "pre-configure:", + "\t@(echo ok)", + "\techo $$(uname -r); echo $$(expr 4 '*' $$(echo 1024))", + "\t@(echo nb$$(uname -r) $$(${EXPR} 4 \\* $$(echo 1024)))") + + mklines.Check() + + // FIXME: Fix the parse errors (nested subshells). + // FIXME: Fix the duplicate diagnostic in line 6. + // FIXME: "(" is not a shell command, it's an operator. + t.CheckOutputLines( + "WARN: Makefile:4: The shell command \"(\" should not be hidden.", + "WARN: Makefile:5: Pkglint parse error in ShTokenizer.ShAtom at \"$$(echo 1024))\" (quoting=S).", + "WARN: Makefile:5: Invoking subshells via $(...) is not portable enough.", + "WARN: Makefile:6: Pkglint parse error in ShTokenizer.ShAtom at \"$$(echo 1024)))\" (quoting=S).", + "WARN: Makefile:6: The shell command \"(\" should not be hidden.", + "WARN: Makefile:6: Pkglint parse error in ShTokenizer.ShAtom at \"$$(echo 1024)))\" (quoting=S).", + "WARN: Makefile:6: Invoking subshells via $(...) is not portable enough.") +} + +func (s *Suite) Test_ShellLine_CheckShellCommand__case_patterns_from_variable(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("Makefile", + MkRcsID, + "", + "pre-configure:", + "\tcase $$file in ${CHECK_PERMS_SKIP:@pattern@${pattern}) ;;@} *) continue; esac") + + mklines.Check() + + // FIXME: Support the above variable expansion. + t.CheckOutputLines( + "WARN: Makefile:4: Pkglint ShellLine.CheckShellCommand: " + + "parse error at []string{\"*\", \")\", \"continue\", \";\", \"esac\"}") +} + +func (s *Suite) Test_ShellLine_checkHiddenAndSuppress(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("Makefile", + MkRcsID, + "", + "show-all-targets: .PHONY", + "\t@echo 'hello'", + "\t@ls -l") + + mklines.Check() + + t.CheckOutputEmpty() +} + func (s *Suite) Test_SimpleCommandChecker_handleForbiddenCommand(c *check.C) { t := s.Init(c) @@ -779,6 +947,66 @@ func (s *Suite) Test_SimpleCommandChecke "ERROR: Makefile:3: \"truss\" must not be used in Makefiles.") } +func (s *Suite) Test_SimpleCommandChecker_handleCommandVariable(c *check.C) { + t := s.Init(c) + + t.SetupToolUsable("perl", "PERL5") + t.SetupTool("perl6", "PERL6") + mklines := t.NewMkLines("Makefile", + MkRcsID, + "", + "PERL5_VARS_CMD=\t${PERL5:Q}", + "PERL5_VARS_CMD=\t${PERL6:Q}") + + mklines.Check() + + // FIXME: Warn about using _PERL5_VARS because it starts with an underscore. + // FIXME: Omit the redundant PERL5_VARS_CMD warning in line 4. + // FIXME: In PERL5:Q and PERL6:Q, the :Q is wrong. + t.CheckOutputLines( + "WARN: Makefile:3: PERL5_VARS_CMD is defined but not used.", + "WARN: Makefile:4: The \"perl6\" tool is used but not added to USE_TOOLS.", + "WARN: Makefile:4: PERL5_VARS_CMD is defined but not used.") +} + +// The package Makefile and other .mk files in a package directory +// may use any shell commands defined by any included files. +// But only if the package is checked as a whole. +// +// On the contrary, when pkglint checks a single .mk file, these +// commands are not guaranteed to be defined, not even when the +// .mk file includes the file defining the command. +// FIXME: This paragraph sounds wrong. All commands from included files should be valid. +// +// The variable must not be called *_CMD, or another code path is taken. +func (s *Suite) Test_SimpleCommandChecker_handleCommandVariable__from_package(c *check.C) { + t := s.Init(c) + + pkg := t.SetupPackage("category/package", + "post-install:", + "\t${PYTHON_BIN}", + "", + ".include \"extra.mk\"") + t.CreateFileLines("category/package/extra.mk", + MkRcsID, + "PYTHON_BIN= my_cmd") + + G.CheckDirent(pkg) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_SimpleCommandChecker_handleComment(c *check.C) { + t := s.Init(c) + + mkline := t.NewMkLine("file.mk", 3, "\t# comment; continuation") + + MkLineChecker{mkline}.Check() + + t.CheckOutputLines( + "WARN: file.mk:3: A shell comment should not contain semicolons.") +} + func (s *Suite) Test_SimpleCommandChecker_checkPaxPe(c *check.C) { t := s.Init(c) @@ -813,16 +1041,122 @@ func (s *Suite) Test_SimpleCommandChecke "WARN: Makefile:4: Please use ${ECHO_N} instead of \"echo -n\".") } -func (s *Suite) Test_SimpleCommandChecker_checkConditionalCd(c *check.C) { +func (s *Suite) Test_ShellProgramChecker_checkConditionalCd(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("Makefile", MkRcsID, "pre-configure:", - "\t${RUN} while cd ..; do printf .; done") + "\t${RUN} while cd ..; do printf .; done", + // TODO: "\t${RUN} if ls | tr -d $$; then :; fi", + "\t${RUN} if ls | tr -d shell$$; then :; fi") + + mklines.Check() + + // FIXME: Fix the parse error. + t.CheckOutputLines( + "ERROR: Makefile:3: The Solaris /bin/sh cannot handle \"cd\" inside conditionals.", + "WARN: Pkglint parse error in ShTokenizer.ShAtom at \"$$\" (quoting=plain).") +} + +func (s *Suite) Test_SimpleCommandChecker_checkRegexReplace(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("Makefile", + MkRcsID, + "pre-configure:", + "\t${PAX} -s s,.*,, src dst", + "\tpax -s s,.*,, src dst", + "\t${SED} -e s,.*,, src dst", + "\tsed -e s,.*,, src dst", + "\tpax -s s,\\.orig,, src dst", + "\tsed -e s,a,b,g src dst") + + mklines.Check() + + // FIXME: warn for "pax -s". + // FIXME: warn for "sed -e". + // TODO: don't warn for "pax .orig". + // TODO: don't warn for "s,a,b,g". + t.CheckOutputLines( + "WARN: Makefile:3: Substitution commands like \"s,.*,,\" should always be quoted.", + "WARN: Makefile:5: Substitution commands like \"s,.*,,\" should always be quoted.") + +} + +func (s *Suite) Test_ShellProgramChecker_checkSetE__simple_commands(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall") + t.SetupToolUsable("echo", "") + t.SetupToolUsable("rm", "") + t.SetupToolUsable("touch", "") + mklines := t.NewMkLines("Makefile", + MkRcsID, + "pre-configure:", + "\techo 1; echo 2; echo 3", + "\techo 1; touch file; rm file", + "\techo 1; var=value; echo 3") + + mklines.Check() + + t.CheckOutputLines( + "WARN: Makefile:4: Please switch to \"set -e\" mode before using a semicolon (after \"touch file\") to separate commands.") +} + +func (s *Suite) Test_ShellProgramChecker_checkSetE__compound_commands(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall") + t.SetupToolUsable("echo", "") + t.SetupToolUsable("touch", "") + mklines := t.NewMkLines("Makefile", + MkRcsID, + "pre-configure:", + "\ttouch file; for f in file; do echo \"$$f\"; done", + "\tfor f in file; do echo \"$$f\"; done; touch file") + + mklines.Check() + + t.CheckOutputLines( + "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon (after \"touch file\") to separate commands.") +} + +func (s *Suite) Test_ShellProgramChecker_canFail(c *check.C) { + t := s.Init(c) + + t.SetupCommandLine("-Wall") + t.SetupVartypes() + t.SetupToolUsable("echo", "") + t.SetupToolUsable("grep", "GREP") + t.SetupToolUsable("sed", "") + t.SetupToolUsable("touch", "") + t.SetupToolUsable("tr", "tr") + t.SetupToolUsable("true", "TRUE") + mklines := t.NewMkLines("Makefile", + MkRcsID, + "pre-configure:", + "\tsocklen=`${GREP} 'expr' ${WRKSRC}/config.h`; echo 'done.'", + "\tsocklen=`${GREP} 'expr' ${WRKSRC}/config.h || ${TRUE}`; echo 'done.'", + "\t${ECHO_MSG} \"Message\"; echo 'done.'", + "\t${FAIL_MSG} \"Failure\"; echo 'done.'", + "\tset -x; echo 'done.'", + "\techo 'input' | sed -e s,in,out,; echo 'done.'", + "\tsed -e s,in,out,; echo 'done.'", + "\tsed s,in,out,; echo 'done.'", + "\tgrep input; echo 'done.'", + "\ttouch file; echo 'done.'", + "\techo 'starting'; echo 'done.'", + "\techo 'logging' > log; echo 'done.'", + "\techo 'to stderr' 1>&2; echo 'done.'", + "\techo 'hello' | tr -d 'aeiou'") mklines.Check() t.CheckOutputLines( - "ERROR: Makefile:3: The Solaris /bin/sh cannot handle \"cd\" inside conditionals.") + "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon (after \"socklen=`${GREP} 'expr' ${WRKSRC}/config.h`\") to separate commands.", + "WARN: Makefile:6: Please switch to \"set -e\" mode before using a semicolon (after \"${FAIL_MSG} \\\"Failure\\\"\") to separate commands.", + "WARN: Makefile:7: Please switch to \"set -e\" mode before using a semicolon (after \"set -x\") to separate commands.", + "WARN: Makefile:12: Please switch to \"set -e\" mode before using a semicolon (after \"touch file\") to separate commands.", + "WARN: Makefile:14: Please switch to \"set -e\" mode before using a semicolon (after \"echo 'logging'\") to separate commands.") } Index: pkgsrc/pkgtools/pkglint/files/util_test.go diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.13 pkgsrc/pkgtools/pkglint/files/util_test.go:1.14 --- pkgsrc/pkgtools/pkglint/files/util_test.go:1.13 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/util_test.go Wed Oct 3 22:27:53 2018 @@ -2,8 +2,6 @@ package main import ( "gopkg.in/check.v1" - "netbsd.org/pkglint/regex" - "netbsd.org/pkglint/textproc" "os" "runtime" "testing" @@ -16,36 +14,36 @@ func (s *Suite) Test_YesNoUnknown_String c.Check(unknown.String(), equals, "unknown") } -func (s *Suite) Test_MkopSubst__middle(c *check.C) { +func (s *Suite) Test_mkopSubst__middle(c *check.C) { c.Check(mkopSubst("pkgname", false, "kgna", false, "ri", ""), equals, "prime") c.Check(mkopSubst("pkgname", false, "pkgname", false, "replacement", ""), equals, "replacement") c.Check(mkopSubst("aaaaaaa", false, "a", false, "b", ""), equals, "baaaaaa") } -func (s *Suite) Test_MkopSubst__left(c *check.C) { +func (s *Suite) Test_mkopSubst__left(c *check.C) { c.Check(mkopSubst("pkgname", true, "kgna", false, "ri", ""), equals, "pkgname") c.Check(mkopSubst("pkgname", true, "pkgname", false, "replacement", ""), equals, "replacement") } -func (s *Suite) Test_MkopSubst__right(c *check.C) { +func (s *Suite) Test_mkopSubst__right(c *check.C) { c.Check(mkopSubst("pkgname", false, "kgna", true, "ri", ""), equals, "pkgname") c.Check(mkopSubst("pkgname", false, "pkgname", true, "replacement", ""), equals, "replacement") } -func (s *Suite) Test_MkopSubst__leftRight(c *check.C) { +func (s *Suite) Test_mkopSubst__left_and_right(c *check.C) { c.Check(mkopSubst("pkgname", true, "kgna", true, "ri", ""), equals, "pkgname") c.Check(mkopSubst("pkgname", false, "pkgname", false, "replacement", ""), equals, "replacement") } -func (s *Suite) Test_MkopSubst__gflag(c *check.C) { +func (s *Suite) Test_mkopSubst__gflag(c *check.C) { c.Check(mkopSubst("aaaaa", false, "a", false, "b", "g"), equals, "bbbbb") c.Check(mkopSubst("aaaaa", true, "a", false, "b", "g"), equals, "baaaa") c.Check(mkopSubst("aaaaa", false, "a", true, "b", "g"), equals, "aaaab") c.Check(mkopSubst("aaaaa", true, "a", true, "b", "g"), equals, "aaaaa") } -func (s *Suite) Test_replaceFirst(c *check.C) { - m, rest := regex.ReplaceFirst("a+b+c+d", `(\w)(.)(\w)`, "X") +func (s *Suite) Test__regex_ReplaceFirst(c *check.C) { + m, rest := G.res.ReplaceFirst("a+b+c+d", `(\w)(.)(\w)`, "X") c.Assert(m, check.NotNil) c.Check(m, check.DeepEquals, []string{"a+b", "a", "+", "b"}) @@ -65,7 +63,7 @@ func (s *Suite) Test_shorten(c *check.C) c.Check(shorten("aaa", 5), equals, "aaa") } -func (s *Suite) Test_tabLength(c *check.C) { +func (s *Suite) Test_tabWidth(c *check.C) { c.Check(tabWidth("12345"), equals, 5) c.Check(tabWidth("\t"), equals, 8) c.Check(tabWidth("123\t"), equals, 8) @@ -81,13 +79,18 @@ func (s *Suite) Test_cleanpath(c *check. c.Check(cleanpath("dir/../dir/../dir/../dir/subdir/../../Makefile"), equals, "dir/../dir/../dir/../Makefile") c.Check(cleanpath("dir/multi/././/file"), equals, "dir/multi/file") c.Check(cleanpath("111/222/../../333/444/../../555/666/../../777/888/9"), equals, "111/222/../../777/888/9") + c.Check(cleanpath("1/2/3/../../4/5/6/../../7/8/9/../../../../10"), equals, "1/10") c.Check(cleanpath("cat/pkg.v1/../../cat/pkg.v2/Makefile"), equals, "cat/pkg.v1/../../cat/pkg.v2/Makefile") c.Check(cleanpath("dir/"), equals, "dir") } func (s *Suite) Test_relpath(c *check.C) { + t := s.Init(c) + if runtime.GOOS == "windows" { - c.Check(func() { relpath("c:/", "d:/") }, check.Panics, "relpath \"c:/\", \"d:/\"") + t.ExpectFatal( + func() { relpath("c:/", "d:/") }, + "FATAL: Pkglint internal error: relpath \"c:/\" \"d:/\".") } } @@ -97,21 +100,21 @@ func (s *Suite) Test_abspath(c *check.C) if runtime.GOOS == "windows" { t.ExpectFatal( func() { abspath("file\u0000name") }, - "FATAL: file\x00name: Cannot determine absolute path.") + "FATAL: Pkglint internal error: abspath \"file\\x00name\".") } } -func (s *Suite) Test_isEmptyDir_and_getSubdirs(c *check.C) { +func (s *Suite) Test_isEmptyDir__and_getSubdirs(c *check.C) { t := s.Init(c) - t.SetupFileLines("CVS/Entries", + t.CreateFileLines("CVS/Entries", "dummy") if dir := t.File("."); true { c.Check(isEmptyDir(dir), equals, true) c.Check(getSubdirs(dir), check.DeepEquals, []string(nil)) - t.SetupFileLines("somedir/file") + t.CreateFileLines("somedir/file") c.Check(isEmptyDir(dir), equals, false) c.Check(getSubdirs(dir), check.DeepEquals, []string{"somedir"}) @@ -130,7 +133,7 @@ func (s *Suite) Test_isEmptyDir_and_getS func (s *Suite) Test_isEmptyDir__empty_subdir(c *check.C) { t := s.Init(c) - t.SetupFileLines("CVS/Entries", + t.CreateFileLines("CVS/Entries", "dummy") t.CreateFileLines("subdir/CVS/Entries", "dummy") @@ -138,8 +141,8 @@ func (s *Suite) Test_isEmptyDir__empty_s c.Check(isEmptyDir(t.File(".")), equals, true) } -func (s *Suite) Test_PrefixReplacer_Since(c *check.C) { - repl := textproc.NewPrefixReplacer("hello, world") +func (s *Suite) Test__PrefixReplacer_Since(c *check.C) { + repl := G.NewPrefixReplacer("hello, world") mark := repl.Mark() repl.AdvanceRegexp(`^\w+`) c.Check(repl.Since(mark), equals, "hello") @@ -168,7 +171,7 @@ func Benchmark_match3_bsd_pkg_mk(b *test } } -func Benchmark_match3_samedir(b *testing.B) { +func Benchmark_match3_same_dir(b *testing.B) { for i := 0; i < b.N; i++ { match3(".include \"options.mk\"", reMkIncludeBenchmark) } @@ -192,7 +195,7 @@ func Benchmark_match3_bsd_pkg_mk_positiv } } -func Benchmark_match3_samedir_positive(b *testing.B) { +func Benchmark_match3_same_dir_positive(b *testing.B) { for i := 0; i < b.N; i++ { match3(".include \"options.mk\"", reMkIncludeBenchmarkPositive) } @@ -305,4 +308,95 @@ func (s *Suite) Test_naturalLess(c *chec c.Check(naturalLess("000", "0000"), equals, true) c.Check(naturalLess("0000", "000"), equals, false) c.Check(naturalLess("000", "000"), equals, false) + c.Check(naturalLess("00011", "000111"), equals, true) + c.Check(naturalLess("00011", "00012"), equals, true) +} + +func (s *Suite) Test_varnameBase(c *check.C) { + c.Check(varnameBase("VAR"), equals, "VAR") + c.Check(varnameBase("VAR.param"), equals, "VAR") + c.Check(varnameBase(".CURDIR"), equals, ".CURDIR") +} + +func (s *Suite) Test_varnameParam(c *check.C) { + c.Check(varnameParam("VAR"), equals, "") + c.Check(varnameParam("VAR.param"), equals, "param") + c.Check(varnameParam(".CURDIR"), equals, "") +} + +func (s *Suite) Test_varnameCanon(c *check.C) { + c.Check(varnameCanon("VAR"), equals, "VAR") + c.Check(varnameCanon("VAR.param"), equals, "VAR.*") + c.Check(varnameCanon(".CURDIR"), equals, ".CURDIR") +} + +func (s *Suite) Test_isalnum(c *check.C) { + c.Check(isalnum(""), equals, true) + c.Check(isalnum("/"), equals, false) + c.Check(isalnum("0"), equals, true) + c.Check(isalnum("9"), equals, true) + c.Check(isalnum(":"), equals, false) + c.Check(isalnum("@"), equals, false) + c.Check(isalnum("A"), equals, true) + c.Check(isalnum("Z"), equals, true) + c.Check(isalnum("["), equals, false) + c.Check(isalnum("_"), equals, true) + c.Check(isalnum("`"), equals, false) + c.Check(isalnum("a"), equals, true) + c.Check(isalnum("z"), equals, true) + c.Check(isalnum("{"), equals, false) + c.Check(isalnum("Hello_world005"), equals, true) + c.Check(isalnum("Hello,world005"), equals, false) +} + +func (s *Suite) Test_FileCache(c *check.C) { + t := s.Init(c) + + cache := NewFileCache(3) + + lines := t.NewLines("Makefile", + MkRcsID, + "# line 2") + + c.Check(cache.Get("Makefile", 0), check.IsNil) + c.Check(cache.hits, equals, 0) + c.Check(cache.misses, equals, 1) + + cache.Put("Makefile", 0, lines) + c.Check(cache.Get("Makefile", MustSucceed|LogErrors), check.IsNil) // Wrong LoadOptions. + + linesFromCache := cache.Get("Makefile", 0) + c.Check(linesFromCache, check.HasLen, 2) + c.Check(linesFromCache[1].Filename, equals, "Makefile") + + // Cache keys are normalized using path.Clean. + linesFromCache2 := cache.Get("./Makefile", 0) + c.Check(linesFromCache2, check.HasLen, 2) + c.Check(linesFromCache2[1].Filename, equals, "./Makefile") + + cache.Put("file1.mk", 0, lines) + cache.Put("file2.mk", 0, lines) + + // Now the cache is full. All three entries can be retrieved. + c.Check(cache.Get("Makefile", 0), check.NotNil) + c.Check(cache.Get("file1.mk", 0), check.NotNil) + c.Check(cache.Get("file2.mk", 0), check.NotNil) + + // Adding another entry removes all entries with minimum count, + // which currently are file1.mk and file2.mk. + // Makefile is still in the cache because it was accessed once. + cache.Put("file3.mk", 0, lines) + + c.Check(cache.Get("Makefile", 0), check.NotNil) + c.Check(cache.Get("file1.mk", 0), check.IsNil) + c.Check(cache.Get("file2.mk", 0), check.IsNil) + c.Check(cache.Get("file3.mk", 0), check.NotNil) + + cache.Evict("Makefile") + + c.Check(cache.Get("Makefile", 0), check.IsNil) + c.Check(cache.table, check.HasLen, 1) + c.Check(cache.mapping, check.HasLen, 1) + c.Check(cache.hits, equals, 7) + c.Check(cache.misses, equals, 5) } Index: pkgsrc/pkgtools/pkglint/files/vardefs.go diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.45 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.46 --- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.45 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/vardefs.go Wed Oct 3 22:27:53 2018 @@ -1,7 +1,6 @@ package main import ( - "fmt" "netbsd.org/pkglint/trace" "path" "strings" @@ -26,9 +25,6 @@ func (src *Pkgsrc) InitVartypes() { vartype := &Vartype{kindOfList, checker, parseACLEntries(varname, aclEntries), false} - if src.vartypes == nil { - src.vartypes = make(map[string]*Vartype) - } if varparam == "" || varparam == "*" { src.vartypes[varbase] = vartype } @@ -247,203 +243,209 @@ func (src *Pkgsrc) InitVartypes() { usr("BIN_INSTALL_FLAGS", lkShell, BtShellWord) usr("LOCALPATCHES", lkNone, BtPathname) - if false { - // The remaining variables from mk/defaults/mk.conf may be overridden by packages. - // Therefore they need a separate definition of "user-settable". - usr := func(varname string, kindOfList KindOfList, checker *BasicType) { - acl(varname, kindOfList, checker, ""+ - "Makefile: set, use; "+ - "buildlink3.mk, builtin.mk:; "+ - "Makefile.*, *.mk: default, set, use; "+ - "*: use-loadtime, use") - } - usr("ACROREAD_FONTPATH", lkNone, BtPathlist) - usr("AMANDA_USER", lkNone, BtUserGroupName) - usr("AMANDA_TMP", lkNone, BtPathname) - usr("AMANDA_VAR", lkNone, BtPathname) - usr("APACHE_USER", lkNone, BtUserGroupName) - usr("APACHE_GROUP", lkNone, BtUserGroupName) - usr("APACHE_SUEXEC_CONFIGURE_ARGS", lkShell, BtShellWord) - usr("APACHE_SUEXEC_DOCROOT", lkShell, BtPathname) - usr("ARLA_CACHE", lkNone, BtPathname) - usr("BIND_DIR", lkNone, BtPathname) - usr("BIND_GROUP", lkNone, BtUserGroupName) - usr("BIND_USER", lkNone, BtUserGroupName) - usr("CACTI_GROUP", lkNone, BtUserGroupName) - usr("CACTI_USER", lkNone, BtUserGroupName) - usr("CANNA_GROUP", lkNone, BtUserGroupName) - usr("CANNA_USER", lkNone, BtUserGroupName) - usr("CDRECORD_CONF", lkNone, BtPathname) - usr("CLAMAV_GROUP", lkNone, BtUserGroupName) - usr("CLAMAV_USER", lkNone, BtUserGroupName) - usr("CLAMAV_DBDIR", lkNone, BtPathname) - usr("CONSERVER_DEFAULTHOST", lkNone, BtIdentifier) - usr("CONSERVER_DEFAULTPORT", lkNone, BtInteger) - usr("CUPS_GROUP", lkNone, BtUserGroupName) - usr("CUPS_USER", lkNone, BtUserGroupName) - usr("CUPS_SYSTEM_GROUPS", lkShell, BtUserGroupName) - usr("CYRUS_IDLE", lkNone, enum("poll idled no")) - usr("CYRUS_GROUP", lkNone, BtUserGroupName) - usr("CYRUS_USER", lkNone, BtUserGroupName) - usr("DBUS_GROUP", lkNone, BtUserGroupName) - usr("DBUS_USER", lkNone, BtUserGroupName) - usr("DEFANG_GROUP", lkNone, BtUserGroupName) - usr("DEFANG_USER", lkNone, BtUserGroupName) - usr("DEFANG_SPOOLDIR", lkNone, BtPathname) - usr("DEFAULT_IRC_SERVER", lkNone, BtIdentifier) - usr("DEFAULT_SERIAL_DEVICE", lkNone, BtPathname) - usr("DIALER_GROUP", lkNone, BtUserGroupName) - usr("DT_LAYOUT", lkNone, enum("US FI FR GER DV")) - usr("ELK_GUI", lkShell, enum("none xaw motif")) - usr("EMACS_TYPE", lkNone, enum("emacs25 emacs25nox emacs21 emacs21nox emacs20 xemacs214 xemacs215")) - usr("EXIM_GROUP", lkNone, BtUserGroupName) - usr("EXIM_USER", lkNone, BtUserGroupName) - usr("FLUXBOX_USE_XINERAMA", lkNone, enum("YES NO")) - usr("FLUXBOX_USE_KDE", lkNone, enum("YES NO")) - usr("FLUXBOX_USE_GNOME", lkNone, enum("YES NO")) - usr("FLUXBOX_USE_XFT", lkNone, enum("YES NO")) - usr("FOX_USE_XUNICODE", lkNone, enum("YES NO")) - usr("FREEWNN_USER", lkNone, BtUserGroupName) - usr("FREEWNN_GROUP", lkNone, BtUserGroupName) - usr("GAMES_USER", lkNone, BtUserGroupName) - usr("GAMES_GROUP", lkNone, BtUserGroupName) - usr("GAMEMODE", lkNone, BtFileMode) - usr("GAMEDIRMODE", lkNone, BtFileMode) - usr("GAMEDATAMODE", lkNone, BtFileMode) - usr("GAMEGRP", lkNone, BtUserGroupName) - usr("GAMEOWN", lkNone, BtUserGroupName) - usr("GRUB_NETWORK_CARDS", lkNone, BtIdentifier) - usr("GRUB_PRESET_COMMAND", lkNone, enum("bootp dhcp rarp")) - usr("GRUB_SCAN_ARGS", lkShell, BtShellWord) - usr("HASKELL_COMPILER", lkNone, enum("ghc")) - usr("HOWL_GROUP", lkNone, BtUserGroupName) - usr("HOWL_USER", lkNone, BtUserGroupName) - usr("ICECAST_CHROOTDIR", lkNone, BtPathname) - usr("ICECAST_CHUNKLEN", lkNone, BtInteger) - usr("ICECAST_SOURCE_BUFFSIZE", lkNone, BtInteger) - usr("IMAP_UW_CCLIENT_MBOX_FMT", lkNone, enum("mbox mbx mh mmdf mtx mx news phile tenex unix")) - usr("IMAP_UW_MAILSPOOLHOME", lkNone, BtFilename) - usr("IMDICTDIR", lkNone, BtPathname) - usr("INN_DATA_DIR", lkNone, BtPathname) - usr("INN_USER", lkNone, BtUserGroupName) - usr("INN_GROUP", lkNone, BtUserGroupName) - usr("IRCD_HYBRID_NICLEN", lkNone, BtInteger) - usr("IRCD_HYBRID_TOPICLEN", lkNone, BtInteger) - usr("IRCD_HYBRID_SYSLOG_EVENTS", lkNone, BtUnknown) - usr("IRCD_HYBRID_SYSLOG_FACILITY", lkNone, BtIdentifier) - usr("IRCD_HYBRID_MAXCONN", lkNone, BtInteger) - usr("IRCD_HYBRID_IRC_USER", lkNone, BtUserGroupName) - usr("IRCD_HYBRID_IRC_GROUP", lkNone, BtUserGroupName) - usr("IRRD_USE_PGP", lkNone, enum("5 2")) - usr("JABBERD_USER", lkNone, BtUserGroupName) - usr("JABBERD_GROUP", lkNone, BtUserGroupName) - usr("JABBERD_LOGDIR", lkNone, BtPathname) - usr("JABBERD_SPOOLDIR", lkNone, BtPathname) - usr("JABBERD_PIDDIR", lkNone, BtPathname) - usr("JAKARTA_HOME", lkNone, BtPathname) - usr("KERBEROS", lkNone, BtYes) - usr("KERMIT_SUID_UUCP", lkNone, BtYes) - usr("KJS_USE_PCRE", lkNone, BtYes) - usr("KNEWS_DOMAIN_FILE", lkNone, BtPathname) - usr("KNEWS_DOMAIN_NAME", lkNone, BtIdentifier) - usr("LIBDVDCSS_HOMEPAGE", lkNone, BtHomepage) - usr("LIBDVDCSS_MASTER_SITES", lkShell, BtFetchURL) - usr("LATEX2HTML_ICONPATH", lkNone, BtURL) - usr("LEAFNODE_DATA_DIR", lkNone, BtPathname) - usr("LEAFNODE_USER", lkNone, BtUserGroupName) - usr("LEAFNODE_GROUP", lkNone, BtUserGroupName) - usr("LINUX_LOCALES", lkShell, BtIdentifier) - usr("MAILAGENT_DOMAIN", lkNone, BtIdentifier) - usr("MAILAGENT_EMAIL", lkNone, BtMailAddress) - usr("MAILAGENT_FQDN", lkNone, BtIdentifier) - usr("MAILAGENT_ORGANIZATION", lkNone, BtUnknown) - usr("MAJORDOMO_HOMEDIR", lkNone, BtPathname) - usr("MAKEINFO_ARGS", lkShell, BtShellWord) - usr("MECAB_CHARSET", lkNone, BtIdentifier) - usr("MEDIATOMB_GROUP", lkNone, BtUserGroupName) - usr("MEDIATOMB_USER", lkNone, BtUserGroupName) - usr("MLDONKEY_GROUP", lkNone, BtUserGroupName) - usr("MLDONKEY_HOME", lkNone, BtPathname) - usr("MLDONKEY_USER", lkNone, BtUserGroupName) - usr("MONOTONE_GROUP", lkNone, BtUserGroupName) - usr("MONOTONE_USER", lkNone, BtUserGroupName) - usr("MOTIF_TYPE", lkNone, enum("motif openmotif lesstif dt")) - usr("MOTIF_TYPE_DEFAULT", lkNone, enum("motif openmotif lesstif dt")) - usr("MTOOLS_ENABLE_FLOPPYD", lkNone, BtYesNo) - usr("MYSQL_USER", lkNone, BtUserGroupName) - usr("MYSQL_GROUP", lkNone, BtUserGroupName) - usr("MYSQL_DATADIR", lkNone, BtPathname) - usr("MYSQL_CHARSET", lkNone, BtIdentifier) - usr("MYSQL_EXTRA_CHARSET", lkShell, BtIdentifier) - usr("NAGIOS_GROUP", lkNone, BtUserGroupName) - usr("NAGIOS_USER", lkNone, BtUserGroupName) - usr("NAGIOSCMD_GROUP", lkNone, BtUserGroupName) - usr("NAGIOSDIR", lkNone, BtPathname) - usr("NBPAX_PROGRAM_PREFIX", lkNone, BtIdentifier) - usr("NMH_EDITOR", lkNone, BtIdentifier) - usr("NMH_MTA", lkNone, enum("smtp sendmail")) - usr("NMH_PAGER", lkNone, BtIdentifier) - usr("NS_PREFERRED", lkNone, enum("communicator navigator mozilla")) - usr("OPENSSH_CHROOT", lkNone, BtPathname) - usr("OPENSSH_USER", lkNone, BtUserGroupName) - usr("OPENSSH_GROUP", lkNone, BtUserGroupName) - usr("P4USER", lkNone, BtUserGroupName) - usr("P4GROUP", lkNone, BtUserGroupName) - usr("P4ROOT", lkNone, BtPathname) - usr("P4PORT", lkNone, BtInteger) - usr("PALMOS_DEFAULT_SDK", lkNone, enum("1 2 3.1 3.5")) - usr("PAPERSIZE", lkNone, enum("A4 Letter")) - usr("PGGROUP", lkNone, BtUserGroupName) - usr("PGUSER", lkNone, BtUserGroupName) - usr("PGHOME", lkNone, BtPathname) - usr("PILRC_USE_GTK", lkNone, BtYesNo) - usr("PKG_JVM_DEFAULT", lkNone, jvms) - usr("POPTOP_USE_MPPE", lkNone, BtYes) - usr("PROCMAIL_MAILSPOOLHOME", lkNone, BtFilename) - usr("PROCMAIL_TRUSTED_IDS", lkShell, BtIdentifier) - usr("PVM_SSH", lkNone, BtPathname) - usr("QMAILDIR", lkNone, BtPathname) - usr("QMAIL_QFILTER_TMPDIR", lkNone, BtPathname) - usr("QMAIL_QUEUE_DIR", lkNone, BtPathname) - usr("QMAIL_QUEUE_EXTRA", lkNone, BtMailAddress) - usr("QPOPPER_FAC", lkNone, BtIdentifier) - usr("QPOPPER_USER", lkNone, BtUserGroupName) - usr("QPOPPER_SPOOL_DIR", lkNone, BtPathname) - usr("RASMOL_DEPTH", lkNone, enum("8 16 32")) - usr("RELAY_CTRL_DIR", lkNone, BtPathname) - usr("RPM_DB_PREFIX", lkNone, BtPathname) - usr("RSSH_SCP_PATH", lkNone, BtPathname) - usr("RSSH_SFTP_SERVER_PATH", lkNone, BtPathname) - usr("RSSH_CVS_PATH", lkNone, BtPathname) - usr("RSSH_RDIST_PATH", lkNone, BtPathname) - usr("RSSH_RSYNC_PATH", lkNone, BtPathname) - usr("SAWFISH_THEMES", lkShell, BtFilename) - usr("SCREWS_GROUP", lkNone, BtUserGroupName) - usr("SCREWS_USER", lkNone, BtUserGroupName) - usr("SDIST_PAWD", lkNone, enum("pawd pwd")) - usr("SERIAL_DEVICES", lkShell, BtPathname) - usr("SILC_CLIENT_WITH_PERL", lkNone, BtYesNo) - usr("SSH_SUID", lkNone, BtYesNo) - usr("SSYNC_PAWD", lkNone, enum("pawd pwd")) - usr("SUSE_PREFER", lkNone, enum("13.1 12.1 10.0")) - usr("TEXMFSITE", lkNone, BtPathname) - usr("THTTPD_LOG_FACILITY", lkNone, BtIdentifier) - usr("UNPRIVILEGED", lkNone, BtYesNo) - usr("USE_CROSS_COMPILE", lkNone, BtYesNo) - usr("USERPPP_GROUP", lkNone, BtUserGroupName) - usr("UUCP_GROUP", lkNone, BtUserGroupName) - usr("UUCP_USER", lkNone, BtUserGroupName) - usr("VIM_EXTRA_OPTS", lkShell, BtShellWord) - usr("WCALC_HTMLDIR", lkNone, BtPathname) - usr("WCALC_HTMLPATH", lkNone, BtPathname) // URL path - usr("WCALC_CGIDIR", lkNone, BtPrefixPathname) - usr("WCALC_CGIPATH", lkNone, BtPathname) // URL path - usr("WDM_MANAGERS", lkShell, BtIdentifier) - usr("X10_PORT", lkNone, BtPathname) - usr("XAW_TYPE", lkNone, enum("standard 3d xpm neXtaw")) - usr("XLOCK_DEFAULT_MODE", lkNone, BtIdentifier) - usr("ZSH_STATIC", lkNone, BtYes) + // The remaining variables from mk/defaults/mk.conf may be overridden by packages. + // Therefore they need a separate definition of "user-settable". + // + // It is debatable whether packages should be allowed to override these + // variables at all since then there are two competing sources for the + // default values. Current practice is to have exactly this ambiguity, + // combined with some package Makefiles including bsd.prefs.mk and others + // omitting this necessary inclusion. + // + // TODO: parse all the below information directly from mk/defaults/mk.conf. + usrpkg := func(varname string, kindOfList KindOfList, checker *BasicType) { + acl(varname, kindOfList, checker, ""+ + "Makefile: default, set, use, use-loadtime; "+ + "buildlink3.mk, builtin.mk:; "+ + "Makefile.*, *.mk: default, set, use, use-loadtime; "+ + "*: use-loadtime, use") } + usrpkg("ACROREAD_FONTPATH", lkNone, BtPathlist) + usrpkg("AMANDA_USER", lkNone, BtUserGroupName) + usrpkg("AMANDA_TMP", lkNone, BtPathname) + usrpkg("AMANDA_VAR", lkNone, BtPathname) + usrpkg("APACHE_USER", lkNone, BtUserGroupName) + usrpkg("APACHE_GROUP", lkNone, BtUserGroupName) + usrpkg("APACHE_SUEXEC_CONFIGURE_ARGS", lkShell, BtShellWord) + usrpkg("APACHE_SUEXEC_DOCROOT", lkShell, BtPathname) + usrpkg("ARLA_CACHE", lkNone, BtPathname) + usrpkg("BIND_DIR", lkNone, BtPathname) + usrpkg("BIND_GROUP", lkNone, BtUserGroupName) + usrpkg("BIND_USER", lkNone, BtUserGroupName) + usrpkg("CACTI_GROUP", lkNone, BtUserGroupName) + usrpkg("CACTI_USER", lkNone, BtUserGroupName) + usrpkg("CANNA_GROUP", lkNone, BtUserGroupName) + usrpkg("CANNA_USER", lkNone, BtUserGroupName) + usrpkg("CDRECORD_CONF", lkNone, BtPathname) + usrpkg("CLAMAV_GROUP", lkNone, BtUserGroupName) + usrpkg("CLAMAV_USER", lkNone, BtUserGroupName) + usrpkg("CLAMAV_DBDIR", lkNone, BtPathname) + usrpkg("CONSERVER_DEFAULTHOST", lkNone, BtIdentifier) + usrpkg("CONSERVER_DEFAULTPORT", lkNone, BtInteger) + usrpkg("CUPS_GROUP", lkNone, BtUserGroupName) + usrpkg("CUPS_USER", lkNone, BtUserGroupName) + usrpkg("CUPS_SYSTEM_GROUPS", lkShell, BtUserGroupName) + usrpkg("CYRUS_IDLE", lkNone, enum("poll idled no")) + usrpkg("CYRUS_GROUP", lkNone, BtUserGroupName) + usrpkg("CYRUS_USER", lkNone, BtUserGroupName) + usrpkg("DBUS_GROUP", lkNone, BtUserGroupName) + usrpkg("DBUS_USER", lkNone, BtUserGroupName) + usrpkg("DEFANG_GROUP", lkNone, BtUserGroupName) + usrpkg("DEFANG_USER", lkNone, BtUserGroupName) + usrpkg("DEFANG_SPOOLDIR", lkNone, BtPathname) + usrpkg("DEFAULT_IRC_SERVER", lkNone, BtIdentifier) + usrpkg("DEFAULT_SERIAL_DEVICE", lkNone, BtPathname) + usrpkg("DIALER_GROUP", lkNone, BtUserGroupName) + usrpkg("DT_LAYOUT", lkNone, enum("US FI FR GER DV")) + usrpkg("ELK_GUI", lkShell, enum("none xaw motif")) + usrpkg("EMACS_TYPE", lkNone, emacsVersions) + usrpkg("EXIM_GROUP", lkNone, BtUserGroupName) + usrpkg("EXIM_USER", lkNone, BtUserGroupName) + usrpkg("FLUXBOX_USE_XINERAMA", lkNone, enum("YES NO")) + usrpkg("FLUXBOX_USE_KDE", lkNone, enum("YES NO")) + usrpkg("FLUXBOX_USE_GNOME", lkNone, enum("YES NO")) + usrpkg("FLUXBOX_USE_XFT", lkNone, enum("YES NO")) + usrpkg("FOX_USE_XUNICODE", lkNone, enum("YES NO")) + usrpkg("FREEWNN_USER", lkNone, BtUserGroupName) + usrpkg("FREEWNN_GROUP", lkNone, BtUserGroupName) + usrpkg("GAMES_USER", lkNone, BtUserGroupName) + usrpkg("GAMES_GROUP", lkNone, BtUserGroupName) + usrpkg("GAMEMODE", lkNone, BtFileMode) + usrpkg("GAMEDIRMODE", lkNone, BtFileMode) + usrpkg("GAMEDATAMODE", lkNone, BtFileMode) + usrpkg("GAMEGRP", lkNone, BtUserGroupName) + usrpkg("GAMEOWN", lkNone, BtUserGroupName) + usrpkg("GRUB_NETWORK_CARDS", lkNone, BtIdentifier) + usrpkg("GRUB_PRESET_COMMAND", lkNone, enum("bootp dhcp rarp")) + usrpkg("GRUB_SCAN_ARGS", lkShell, BtShellWord) + usrpkg("HASKELL_COMPILER", lkNone, enum("ghc")) + usrpkg("HOWL_GROUP", lkNone, BtUserGroupName) + usrpkg("HOWL_USER", lkNone, BtUserGroupName) + usrpkg("ICECAST_CHROOTDIR", lkNone, BtPathname) + usrpkg("ICECAST_CHUNKLEN", lkNone, BtInteger) + usrpkg("ICECAST_SOURCE_BUFFSIZE", lkNone, BtInteger) + usrpkg("IMAP_UW_CCLIENT_MBOX_FMT", lkNone, enum("mbox mbx mh mmdf mtx mx news phile tenex unix")) + usrpkg("IMAP_UW_MAILSPOOLHOME", lkNone, BtFilename) + usrpkg("IMDICTDIR", lkNone, BtPathname) + usrpkg("INN_DATA_DIR", lkNone, BtPathname) + usrpkg("INN_USER", lkNone, BtUserGroupName) + usrpkg("INN_GROUP", lkNone, BtUserGroupName) + usrpkg("IRCD_HYBRID_NICLEN", lkNone, BtInteger) + usrpkg("IRCD_HYBRID_TOPICLEN", lkNone, BtInteger) + usrpkg("IRCD_HYBRID_SYSLOG_EVENTS", lkNone, BtUnknown) + usrpkg("IRCD_HYBRID_SYSLOG_FACILITY", lkNone, BtIdentifier) + usrpkg("IRCD_HYBRID_MAXCONN", lkNone, BtInteger) + usrpkg("IRCD_HYBRID_IRC_USER", lkNone, BtUserGroupName) + usrpkg("IRCD_HYBRID_IRC_GROUP", lkNone, BtUserGroupName) + usrpkg("IRRD_USE_PGP", lkNone, enum("5 2")) + usrpkg("JABBERD_USER", lkNone, BtUserGroupName) + usrpkg("JABBERD_GROUP", lkNone, BtUserGroupName) + usrpkg("JABBERD_LOGDIR", lkNone, BtPathname) + usrpkg("JABBERD_SPOOLDIR", lkNone, BtPathname) + usrpkg("JABBERD_PIDDIR", lkNone, BtPathname) + usrpkg("JAKARTA_HOME", lkNone, BtPathname) + usrpkg("KERBEROS", lkNone, BtYes) + usrpkg("KERMIT_SUID_UUCP", lkNone, BtYes) + usrpkg("KJS_USE_PCRE", lkNone, BtYes) + usrpkg("KNEWS_DOMAIN_FILE", lkNone, BtPathname) + usrpkg("KNEWS_DOMAIN_NAME", lkNone, BtIdentifier) + usrpkg("LIBDVDCSS_HOMEPAGE", lkNone, BtHomepage) + usrpkg("LIBDVDCSS_MASTER_SITES", lkShell, BtFetchURL) + usrpkg("LATEX2HTML_ICONPATH", lkNone, BtURL) + usrpkg("LEAFNODE_DATA_DIR", lkNone, BtPathname) + usrpkg("LEAFNODE_USER", lkNone, BtUserGroupName) + usrpkg("LEAFNODE_GROUP", lkNone, BtUserGroupName) + usrpkg("LINUX_LOCALES", lkShell, BtIdentifier) + usrpkg("MAILAGENT_DOMAIN", lkNone, BtIdentifier) + usrpkg("MAILAGENT_EMAIL", lkNone, BtMailAddress) + usrpkg("MAILAGENT_FQDN", lkNone, BtIdentifier) + usrpkg("MAILAGENT_ORGANIZATION", lkNone, BtUnknown) + usrpkg("MAJORDOMO_HOMEDIR", lkNone, BtPathname) + usrpkg("MAKEINFO_ARGS", lkShell, BtShellWord) + usrpkg("MECAB_CHARSET", lkNone, BtIdentifier) + usrpkg("MEDIATOMB_GROUP", lkNone, BtUserGroupName) + usrpkg("MEDIATOMB_USER", lkNone, BtUserGroupName) + usrpkg("MLDONKEY_GROUP", lkNone, BtUserGroupName) + usrpkg("MLDONKEY_HOME", lkNone, BtPathname) + usrpkg("MLDONKEY_USER", lkNone, BtUserGroupName) + usrpkg("MONOTONE_GROUP", lkNone, BtUserGroupName) + usrpkg("MONOTONE_USER", lkNone, BtUserGroupName) + usrpkg("MOTIF_TYPE", lkNone, enum("motif openmotif lesstif dt")) + usrpkg("MOTIF_TYPE_DEFAULT", lkNone, enum("motif openmotif lesstif dt")) + usrpkg("MTOOLS_ENABLE_FLOPPYD", lkNone, BtYesNo) + usrpkg("MYSQL_USER", lkNone, BtUserGroupName) + usrpkg("MYSQL_GROUP", lkNone, BtUserGroupName) + usrpkg("MYSQL_DATADIR", lkNone, BtPathname) + usrpkg("MYSQL_CHARSET", lkNone, BtIdentifier) + usrpkg("MYSQL_EXTRA_CHARSET", lkShell, BtIdentifier) + usrpkg("NAGIOS_GROUP", lkNone, BtUserGroupName) + usrpkg("NAGIOS_USER", lkNone, BtUserGroupName) + usrpkg("NAGIOSCMD_GROUP", lkNone, BtUserGroupName) + usrpkg("NAGIOSDIR", lkNone, BtPathname) + usrpkg("NBPAX_PROGRAM_PREFIX", lkNone, BtUnknown) + usrpkg("NMH_EDITOR", lkNone, BtIdentifier) + usrpkg("NMH_MTA", lkNone, enum("smtp sendmail")) + usrpkg("NMH_PAGER", lkNone, BtIdentifier) + usrpkg("NS_PREFERRED", lkNone, enum("communicator navigator mozilla")) + usrpkg("OPENSSH_CHROOT", lkNone, BtPathname) + usrpkg("OPENSSH_USER", lkNone, BtUserGroupName) + usrpkg("OPENSSH_GROUP", lkNone, BtUserGroupName) + usrpkg("P4USER", lkNone, BtUserGroupName) + usrpkg("P4GROUP", lkNone, BtUserGroupName) + usrpkg("P4ROOT", lkNone, BtPathname) + usrpkg("P4PORT", lkNone, BtInteger) + usrpkg("PALMOS_DEFAULT_SDK", lkNone, enum("1 2 3.1 3.5")) + usrpkg("PAPERSIZE", lkNone, enum("A4 Letter")) + usrpkg("PGGROUP", lkNone, BtUserGroupName) + usrpkg("PGUSER", lkNone, BtUserGroupName) + usrpkg("PGHOME", lkNone, BtPathname) + usrpkg("PILRC_USE_GTK", lkNone, BtYesNo) + usrpkg("PKG_JVM_DEFAULT", lkNone, jvms) + usrpkg("POPTOP_USE_MPPE", lkNone, BtYes) + usrpkg("PROCMAIL_MAILSPOOLHOME", lkNone, BtFilename) + usrpkg("PROCMAIL_TRUSTED_IDS", lkShell, BtUnknown) // Comma-separated list of string or integer literals. + usrpkg("PVM_SSH", lkNone, BtPathname) + usrpkg("QMAILDIR", lkNone, BtPathname) + usrpkg("QMAIL_QFILTER_TMPDIR", lkNone, BtPathname) + usrpkg("QMAIL_QUEUE_DIR", lkNone, BtPathname) + usrpkg("QMAIL_QUEUE_EXTRA", lkNone, BtMailAddress) + usrpkg("QPOPPER_FAC", lkNone, BtIdentifier) + usrpkg("QPOPPER_USER", lkNone, BtUserGroupName) + usrpkg("QPOPPER_SPOOL_DIR", lkNone, BtPathname) + usrpkg("RASMOL_DEPTH", lkNone, enum("8 16 32")) + usrpkg("RELAY_CTRL_DIR", lkNone, BtPathname) + usrpkg("RPM_DB_PREFIX", lkNone, BtPathname) + usrpkg("RSSH_SCP_PATH", lkNone, BtPathname) + usrpkg("RSSH_SFTP_SERVER_PATH", lkNone, BtPathname) + usrpkg("RSSH_CVS_PATH", lkNone, BtPathname) + usrpkg("RSSH_RDIST_PATH", lkNone, BtPathname) + usrpkg("RSSH_RSYNC_PATH", lkNone, BtPathname) + usrpkg("SAWFISH_THEMES", lkShell, BtFilename) + usrpkg("SCREWS_GROUP", lkNone, BtUserGroupName) + usrpkg("SCREWS_USER", lkNone, BtUserGroupName) + usrpkg("SDIST_PAWD", lkNone, enum("pawd pwd")) + usrpkg("SERIAL_DEVICES", lkShell, BtPathname) + usrpkg("SILC_CLIENT_WITH_PERL", lkNone, BtYesNo) + usrpkg("SSH_SUID", lkNone, BtYesNo) + usrpkg("SSYNC_PAWD", lkNone, enum("pawd pwd")) + usrpkg("SUSE_PREFER", lkNone, enum("13.1 12.1 10.0")) + usrpkg("TEXMFSITE", lkNone, BtPathname) + usrpkg("THTTPD_LOG_FACILITY", lkNone, BtIdentifier) + usrpkg("UNPRIVILEGED", lkNone, BtYesNo) + usrpkg("USE_CROSS_COMPILE", lkNone, BtYesNo) + usrpkg("USERPPP_GROUP", lkNone, BtUserGroupName) + usrpkg("UUCP_GROUP", lkNone, BtUserGroupName) + usrpkg("UUCP_USER", lkNone, BtUserGroupName) + usrpkg("VIM_EXTRA_OPTS", lkShell, BtShellWord) + usrpkg("WCALC_HTMLDIR", lkNone, BtPathname) + usrpkg("WCALC_HTMLPATH", lkNone, BtPathname) // URL path + usrpkg("WCALC_CGIDIR", lkNone, BtPrefixPathname) + usrpkg("WCALC_CGIPATH", lkNone, BtPathname) // URL path + usrpkg("WDM_MANAGERS", lkShell, BtIdentifier) + usrpkg("X10_PORT", lkNone, BtPathname) + usrpkg("XAW_TYPE", lkNone, enum("standard 3d xpm neXtaw")) + usrpkg("XLOCK_DEFAULT_MODE", lkNone, BtIdentifier) + usrpkg("ZSH_STATIC", lkNone, BtYes) // some other variables, sorted alphabetically @@ -654,7 +656,6 @@ func (src *Pkgsrc) InitVartypes() { usr("EMUL_TYPE.*", lkNone, enum("native builtin suse suse-9.1 suse-9.x suse-10.0 suse-10.x")) sys("ERROR_CAT", lkNone, BtShellCommand) sys("ERROR_MSG", lkNone, BtShellCommand) - acl("EVAL_PREFIX", lkSpace, BtShellWord, "Makefile, Makefile.common: append") // XXX: Combining ShellWord with lkSpace looks weird. sys("EXPORT_SYMBOLS_LDFLAGS", lkShell, BtLdFlag) sys("EXTRACT_CMD", lkNone, BtShellCommand) pkg("EXTRACT_DIR", lkNone, BtPathname) @@ -956,7 +957,7 @@ func (src *Pkgsrc) InitVartypes() { acl("PKG_SYSCONFVAR", lkNone, BtIdentifier, "") acl("PKG_UID", lkNone, BtInteger, "Makefile: set") acl("PKG_USERS", lkShell, BtShellWord, "Makefile: set, append") - pkg("PKG_USERS_VARS", lkShell, BtVariableName) + pkglist("PKG_USERS_VARS", lkShell, BtVariableName) acl("PKG_USE_KERBEROS", lkNone, BtYes, "Makefile, Makefile.common: set") pkg("PLIST.*", lkNone, BtYes) pkglist("PLIST_VARS", lkShell, BtIdentifier) @@ -978,8 +979,8 @@ func (src *Pkgsrc) InitVartypes() { acl("PYPKGPREFIX", lkNone, enum("py27 py34 py35 py36"), "pyversion.mk: set; *: use-loadtime, use") pkg("PYTHON_FOR_BUILD_ONLY", lkNone, BtYes) pkglist("REPLACE_PYTHON", lkShell, BtPathmask) - pkg("PYTHON_VERSIONS_ACCEPTED", lkShell, BtVersion) - pkg("PYTHON_VERSIONS_INCOMPATIBLE", lkShell, BtVersion) + pkglist("PYTHON_VERSIONS_ACCEPTED", lkShell, BtVersion) + pkglist("PYTHON_VERSIONS_INCOMPATIBLE", lkShell, BtVersion) usr("PYTHON_VERSION_DEFAULT", lkNone, BtVersion) usr("PYTHON_VERSION_REQD", lkNone, BtVersion) pkglist("PYTHON_VERSIONED_DEPENDENCIES", lkShell, BtPythonDependency) @@ -1139,9 +1140,10 @@ func parseACLEntries(varname string, acl globs = strings.TrimSuffix(arg, ":") } if perms == prevperms { - fmt.Printf("Repeated permissions for %s: %s\n", varname, perms) + G.Panicf("Repeated permissions %q for %q.", perms, varname) } prevperms = perms + var permissions ACLPermissions for _, perm := range strings.Split(perms, ", ") { switch perm { @@ -1158,9 +1160,10 @@ func parseACLEntries(varname string, acl case "": break default: - print(fmt.Sprintf("Invalid ACL permission %q for varname %q.\n", perm, varname)) + G.Panicf("Invalid ACL permission %q for %q.", perm, varname) } } + for _, glob := range strings.Split(globs, ", ") { switch glob { case "*", @@ -1169,11 +1172,11 @@ func parseACLEntries(varname string, acl "bsd.options.mk", "pkgconfig-builtin.mk", "pyversion.mk": break default: - print(fmt.Sprintf("Invalid ACL glob %q for varname %q.\n", glob, varname)) + G.Panicf("Invalid ACL glob %q for %q.", glob, varname) } for _, prev := range result { if matched, err := path.Match(prev.glob, glob); err != nil || matched { - print(fmt.Sprintf("Ineffective ACL glob %q for varname %q.\n", glob, varname)) + G.Panicf("Ineffective ACL glob %q for %q.", glob, varname) } } result = append(result, ACLEntry{glob, permissions}) Index: pkgsrc/pkgtools/pkglint/files/vartypecheck.go diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.39 pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.40 --- pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.39 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/vartypecheck.go Wed Oct 3 22:27:53 2018 @@ -1,7 +1,6 @@ package main import ( - "netbsd.org/pkglint/regex" "netbsd.org/pkglint/trace" "path" "sort" @@ -277,7 +276,8 @@ func (cv *VartypeCheck) Dependency() { "foo-* matches foo-1.2, but also foo-client-1.2 and foo-server-1.2.") } - if nocclasses := regex.Compile(`\[[\d-]+\]`).ReplaceAllString(wildcard, ""); contains(nocclasses, "-") { + withoutCharClasses := replaceAll(wildcard, `\[[\d-]+\]`, "") + if contains(withoutCharClasses, "-") { line.Warnf("The version pattern %q should not contain a hyphen.", wildcard) Explain( "Pkgsrc interprets package names with version numbers like this:", @@ -730,7 +730,7 @@ func (cv *VartypeCheck) Perms() { } } -func (cv *VartypeCheck) PkgName() { +func (cv *VartypeCheck) Pkgname() { if cv.Op != opUseMatch && cv.Value == cv.ValueNoVar && !matches(cv.Value, rePkgname) { cv.Line.Warnf("%q is not a valid package name. A valid package name has the form packagename-version, where version consists only of digits, letters and dots.", cv.Value) } @@ -792,25 +792,34 @@ func (cv *VartypeCheck) MachinePlatformP pattern += "-*" } - if m, opsysPattern, _, archPattern := match3(pattern, reTriple); m { + if m, opsysPattern, versionPattern, archPattern := match3(pattern, reTriple); m { opsysCv := &VartypeCheck{ cv.MkLine, cv.Line, "the operating system part of " + cv.Varname, - opUseMatch, // Always allow patterns, since this is a PlatformPattern. + opUseMatch, // Always allow patterns, since this is a platform pattern. opsysPattern, opsysPattern, cv.MkComment, cv.Guessed} enumMachineOpsys.checker(opsysCv) - // no check for os_version + versionCv := &VartypeCheck{ + cv.MkLine, + cv.Line, + "the version part of " + cv.Varname, + opUseMatch, // Always allow patterns, since this is a platform pattern. + versionPattern, + versionPattern, + cv.MkComment, + cv.Guessed} + versionCv.Version() archCv := &VartypeCheck{ cv.MkLine, cv.Line, "the hardware architecture part of " + cv.Varname, - opUseMatch, // Always allow patterns, since this is a PlatformPattern. + opUseMatch, // Always allow patterns, since this is a platform pattern. archPattern, archPattern, cv.MkComment, @@ -976,6 +985,7 @@ func (cv *VartypeCheck) Tool() { } } +// Unknown doesn't check for anything. func (cv *VartypeCheck) Unknown() { // Do nothing. } @@ -1035,12 +1045,30 @@ func (cv *VartypeCheck) VariableName() { } func (cv *VartypeCheck) Version() { + line := cv.Line + value := cv.Value + if cv.Op == opUseMatch { - if !matches(cv.Value, `^[\d?\[][\w\-.*?\[\]]+$`) { - cv.Line.Warnf("Invalid version number pattern %q.", cv.Value) + if value != "*" && !matches(value, `^[\d?\[][\w\-.*?\[\]]+$`) { + line.Warnf("Invalid version number pattern %q.", value) + return + } + + const digit = `(?:\d|\[[\d-]+\])` + const alnum = `(?:\w|\[[\d-]+\])` + if m, ver, suffix := match2(value, `^(`+digit+alnum+`*(?:\.`+alnum+`+)*)(\.\*|\*|)$`); m { + if suffix == "*" && ver != "[0-9]" { + line.Warnf("Please use %q instead of %q as the version pattern.", ver+".*", ver+"*") + Explain( + "For example, the version \"1*\" also matches \"10.0.0\", which is", + "probably not intended.") + } } - } else if cv.Value == cv.ValueNoVar && !matches(cv.Value, `^\d[\w.]*$`) { - cv.Line.Warnf("Invalid version number %q.", cv.Value) + return + } + + if value == cv.ValueNoVar && !matches(value, `^\d[\w.]*$`) { + line.Warnf("Invalid version number %q.", value) } } Index: pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go diff -u pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.5 pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.6 --- pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.5 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go Wed Oct 3 22:27:54 2018 @@ -137,3 +137,43 @@ func (s *Suite) Test_Options_Parse_strin c.Check(err.Error(), check.Equals, "progname: option requires an argument: --include") } + +func (s *Suite) Test_Options_Parse__long_flags(c *check.C) { + var aflag, bflag, cflag, dflag, eflag, fflag, gflag, hflag, iflag, jflag bool + + opts := NewOptions() + opts.AddFlagVar('a', "aflag", &aflag, false, "") + opts.AddFlagVar('b', "bflag", &bflag, false, "") + opts.AddFlagVar('c', "cflag", &cflag, false, "") + opts.AddFlagVar('d', "dflag", &dflag, false, "") + opts.AddFlagVar('e', "eflag", &eflag, true, "") + opts.AddFlagVar('f', "fflag", &fflag, true, "") + opts.AddFlagVar('g', "gflag", &gflag, true, "") + opts.AddFlagVar('h', "hflag", &hflag, true, "") + opts.AddFlagVar('i', "iflag", &iflag, false, "") + opts.AddFlagVar('j', "jflag", &jflag, false, "") + + args, err := opts.Parse([]string{"progname", + "--aflag=true", + "--bflag=on", + "--cflag=enabled", + "--dflag=1", + "--eflag=false", + "--fflag=off", + "--gflag=disabled", + "--hflag=0", + "--iflag", + "--jflag=unknown"}) + + c.Check(args, check.HasLen, 0) + c.Check(aflag, check.Equals, true) + c.Check(bflag, check.Equals, true) + c.Check(cflag, check.Equals, true) + c.Check(dflag, check.Equals, true) + c.Check(eflag, check.Equals, false) + c.Check(fflag, check.Equals, false) + c.Check(gflag, check.Equals, false) + c.Check(hflag, check.Equals, false) + c.Check(iflag, check.Equals, true) + c.Check(err, check.ErrorMatches, `^progname: invalid argument for option --jflag$`) +} Index: pkgsrc/pkgtools/pkglint/files/licenses/licenses.go diff -u pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.2 pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.3 --- pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.2 Thu Mar 16 20:03:22 2017 +++ pkgsrc/pkgtools/pkglint/files/licenses/licenses.go Wed Oct 3 22:27:54 2018 @@ -1,6 +1,9 @@ package licenses -import "netbsd.org/pkglint/textproc" +import ( + "netbsd.org/pkglint/regex" + "netbsd.org/pkglint/textproc" +) // Condition describes a complex license condition. // It has either `Name` or `Paren` or `Children` set. @@ -14,8 +17,8 @@ type Condition struct { Children []*Condition `json:",omitempty"` } -func Parse(licenses string) *Condition { - lexer := &licenseLexer{repl: textproc.NewPrefixReplacer(licenses)} +func Parse(licenses string, res *regex.Registry) *Condition { + lexer := &licenseLexer{repl: textproc.NewPrefixReplacer(licenses, res)} result := liyyNewParser().Parse(lexer) if result == 0 { return lexer.result Index: pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go diff -u pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.1 pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.2 --- pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.1 Tue Jan 17 22:37:28 2017 +++ pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go Wed Oct 3 22:27:54 2018 @@ -3,6 +3,7 @@ package licenses import ( "encoding/json" "gopkg.in/check.v1" + "netbsd.org/pkglint/regex" "strings" "testing" ) @@ -10,11 +11,15 @@ import ( type Suite struct{} func (s *Suite) Test_Parse(c *check.C) { + res := regex.NewRegistry() checkParse := func(cond string, expected string) { - c.Check(toJSON(Parse(cond)), check.Equals, expected) + c.Check(toJSON(Parse(cond, &res)), check.Equals, expected) + } + checkParseDeep := func(cond string, expected *Condition) { + c.Check(Parse(cond, &res), check.DeepEquals, expected) } - c.Check(Parse("gnu-gpl-v2"), check.DeepEquals, NewSingleton(NewName("gnu-gpl-v2"))) + checkParseDeep("gnu-gpl-v2", NewSingleton(NewName("gnu-gpl-v2"))) checkParse("gnu-gpl-v2", "{Children:[{Name:gnu-gpl-v2}]}") checkParse("a AND b", "{And:true,Children:[{Name:a},{Name:b}]}") @@ -24,21 +29,18 @@ func (s *Suite) Test_Parse(c *check.C) { checkParse("(a OR b) AND c", "{And:true,Children:[{Paren:{Or:true,Children:[{Name:a},{Name:b}]}},{Name:c}]}") checkParse("a AND b AND c AND d", "{And:true,Children:[{Name:a},{Name:b},{Name:c},{Name:d}]}") - c.Check( - Parse("a AND b AND c AND d"), - check.DeepEquals, + checkParseDeep( + "a AND b AND c AND d", NewAnd(NewName("a"), NewName("b"), NewName("c"), NewName("d"))) checkParse("a OR b OR c OR d", "{Or:true,Children:[{Name:a},{Name:b},{Name:c},{Name:d}]}") - c.Check( - Parse("a OR b OR c OR d"), - check.DeepEquals, + checkParseDeep( + "a OR b OR c OR d", NewOr(NewName("a"), NewName("b"), NewName("c"), NewName("d"))) checkParse("(a OR b) AND (c AND d)", "{And:true,Children:[{Paren:{Or:true,Children:[{Name:a},{Name:b}]}},{Paren:{And:true,Children:[{Name:c},{Name:d}]}}]}") - c.Check( - (Parse("(a OR b) AND (c AND d)")), - check.DeepEquals, + checkParseDeep( + "(a OR b) AND (c AND d)", NewAnd( NewParen(NewOr(NewName("a"), NewName("b"))), NewParen(NewAnd(NewName("c"), NewName("d"))))) @@ -46,9 +48,9 @@ func (s *Suite) Test_Parse(c *check.C) { checkParse("a AND b OR c AND d", "{And:true,Or:true,Children:[{Name:a},{Name:b},{Name:c},{Name:d}]}") checkParse("((a AND (b AND c)))", "{Children:[{Paren:{Children:[{Paren:{And:true,Children:[{Name:a},{Paren:{And:true,Children:[{Name:b},{Name:c}]}}]}}]}}]}") - c.Check(Parse("a AND b OR c AND d").String(), check.Equals, "a MIXED b MIXED c MIXED d") + c.Check(Parse("a AND b OR c AND d", &res).String(), check.Equals, "a MIXED b MIXED c MIXED d") - c.Check(Parse("AND artistic"), check.IsNil) + c.Check(Parse("AND artistic", &res), check.IsNil) } func (s *Suite) Test_Condition_String(c *check.C) { Index: pkgsrc/pkgtools/pkglint/files/regex/regex.go diff -u pkgsrc/pkgtools/pkglint/files/regex/regex.go:1.3 pkgsrc/pkgtools/pkglint/files/regex/regex.go:1.4 --- pkgsrc/pkgtools/pkglint/files/regex/regex.go:1.3 Thu Aug 16 20:41:42 2018 +++ pkgsrc/pkgtools/pkglint/files/regex/regex.go Wed Oct 3 22:27:54 2018 @@ -14,97 +14,107 @@ import ( type Pattern string -var ( - Profiling bool -) - -var ( - res = make(map[Pattern]*regexp.Regexp) - rematch = histogram.New() - renomatch = histogram.New() - retime = histogram.New() -) +type Registry struct { + res map[Pattern]*regexp.Regexp + rematch *histogram.Histogram + renomatch *histogram.Histogram + retime *histogram.Histogram + profiling bool +} + +func NewRegistry() Registry { + return Registry{make(map[Pattern]*regexp.Regexp), nil, nil, nil, false} +} + +func (r *Registry) Profiling() { + if !r.profiling { + r.rematch = histogram.New() + r.renomatch = histogram.New() + r.retime = histogram.New() + r.profiling = true + } +} -func Compile(re Pattern) *regexp.Regexp { - cre := res[re] +func (r *Registry) Compile(re Pattern) *regexp.Regexp { + cre := r.res[re] if cre == nil { cre = regexp.MustCompile(string(re)) - res[re] = cre + r.res[re] = cre } return cre } -func Match(s string, re Pattern) []string { - if !Profiling { - return Compile(re).FindStringSubmatch(s) +func (r *Registry) Match(s string, re Pattern) []string { + if !r.profiling { + return r.Compile(re).FindStringSubmatch(s) } before := time.Now() immediatelyBefore := time.Now() - m := Compile(re).FindStringSubmatch(s) + m := r.Compile(re).FindStringSubmatch(s) after := time.Now() delay := immediatelyBefore.UnixNano() - before.UnixNano() timeTaken := after.UnixNano() - immediatelyBefore.UnixNano() - delay - retime.Add(string(re), int(timeTaken)) + r.retime.Add(string(re), int(timeTaken)) if m != nil { - rematch.Add(string(re), 1) + r.rematch.Add(string(re), 1) } else { - renomatch.Add(string(re), 1) + r.renomatch.Add(string(re), 1) } return m } -func Matches(s string, re Pattern) bool { - matches := Compile(re).MatchString(s) - if Profiling { +func (r *Registry) Matches(s string, re Pattern) bool { + matches := r.Compile(re).MatchString(s) + if r.profiling { if matches { - rematch.Add(string(re), 1) + r.rematch.Add(string(re), 1) } else { - renomatch.Add(string(re), 1) + r.renomatch.Add(string(re), 1) } } return matches } -func Match1(s string, re Pattern) (matched bool, m1 string) { - if m := matchn(s, re, 1); m != nil { +func (r *Registry) Match1(s string, re Pattern) (matched bool, m1 string) { + if m := r.matchn(s, re, 1); m != nil { return true, m[1] } return } -func Match2(s string, re Pattern) (matched bool, m1, m2 string) { - if m := matchn(s, re, 2); m != nil { +func (r *Registry) Match2(s string, re Pattern) (matched bool, m1, m2 string) { + if m := r.matchn(s, re, 2); m != nil { return true, m[1], m[2] } return } -func Match3(s string, re Pattern) (matched bool, m1, m2, m3 string) { - if m := matchn(s, re, 3); m != nil { +func (r *Registry) Match3(s string, re Pattern) (matched bool, m1, m2, m3 string) { + if m := r.matchn(s, re, 3); m != nil { return true, m[1], m[2], m[3] } return } -func Match4(s string, re Pattern) (matched bool, m1, m2, m3, m4 string) { - if m := matchn(s, re, 4); m != nil { +func (r *Registry) Match4(s string, re Pattern) (matched bool, m1, m2, m3, m4 string) { + if m := r.matchn(s, re, 4); m != nil { return true, m[1], m[2], m[3], m[4] } return } -func Match5(s string, re Pattern) (matched bool, m1, m2, m3, m4, m5 string) { - if m := matchn(s, re, 5); m != nil { +func (r *Registry) Match5(s string, re Pattern) (matched bool, m1, m2, m3, m4, m5 string) { + if m := r.matchn(s, re, 5); m != nil { return true, m[1], m[2], m[3], m[4], m[5] } return } -func ReplaceFirst(s string, re Pattern, replacement string) ([]string, string) { - if m := Compile(re).FindStringSubmatchIndex(s); m != nil { +func (r *Registry) ReplaceFirst(s string, re Pattern, replacement string) ([]string, string) { + if m := r.Compile(re).FindStringSubmatchIndex(s); m != nil { replaced := s[:m[0]] + replacement + s[m[1]:] mm := make([]string, len(m)/2) for i := 0; i < len(m); i += 2 { @@ -115,16 +125,16 @@ func ReplaceFirst(s string, re Pattern, return nil, s } -func PrintStats(out io.Writer) { - if Profiling { - rematch.PrintStats("rematch", out, 10) - renomatch.PrintStats("renomatch", out, 10) - retime.PrintStats("retime", out, 10) +func (r *Registry) PrintStats(out io.Writer) { + if r.profiling { + r.rematch.PrintStats("rematch", out, 10) + r.renomatch.PrintStats("renomatch", out, 10) + r.retime.PrintStats("retime", out, 10) } } -func matchn(s string, re Pattern, n int) []string { - if m := Match(s, re); m != nil { +func (r *Registry) matchn(s string, re Pattern, n int) []string { + if m := r.Match(s, re); m != nil { if len(m) != 1+n { panic(fmt.Sprintf("expected match%d, got match%d for %q", len(m)-1, n, re)) } Index: pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go diff -u pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go:1.5 pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go:1.6 --- pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go:1.5 Wed Sep 5 17:56:22 2018 +++ pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go Wed Oct 3 22:27:54 2018 @@ -17,10 +17,11 @@ type PrefixReplacer struct { rest string s string m []string + res *regex.Registry } -func NewPrefixReplacer(s string) *PrefixReplacer { - return &PrefixReplacer{s, "", nil} +func NewPrefixReplacer(s string, res *regex.Registry) *PrefixReplacer { + return &PrefixReplacer{s, "", nil, res} } func (pr *PrefixReplacer) EOF() bool { @@ -98,10 +99,10 @@ func (pr *PrefixReplacer) AdvanceRegexp( if !strings.HasPrefix(string(re), "^") { panic(fmt.Sprintf("PrefixReplacer.AdvanceRegexp: regular expression %q must have prefix %q.", re, "^")) } - if Testing && regex.Matches("", re) { + if Testing && pr.res.Matches("", re) { panic(fmt.Sprintf("PrefixReplacer.AdvanceRegexp: the empty string must not match the regular expression %q.", re)) } - if m := regex.Match(pr.rest, re); m != nil { + if m := pr.res.Match(pr.rest, re); m != nil { if trace.Tracing { trace.Stepf("PrefixReplacer.AdvanceRegexp(%q, %q, %q)", pr.rest, re, m[0]) } @@ -153,5 +154,5 @@ func (pr *PrefixReplacer) HasPrefix(str } func (pr *PrefixReplacer) HasPrefixRegexp(re regex.Pattern) bool { - return regex.Matches(pr.rest, re) + return pr.res.Matches(pr.rest, re) } Added files: Index: pkgsrc/pkgtools/pkglint/files/fuzzer_test.go diff -u /dev/null pkgsrc/pkgtools/pkglint/files/fuzzer_test.go:1.1 --- /dev/null Wed Oct 3 22:27:54 2018 +++ pkgsrc/pkgtools/pkglint/files/fuzzer_test.go Wed Oct 3 22:27:53 2018 @@ -0,0 +1,90 @@ +package main + +import ( + "gopkg.in/check.v1" + "math/rand" +) + +type Fuzzer struct { + seed int64 + rnd *rand.Rand + stock []struct { + r rune + weight int + } + total int + last string + ok bool +} + +func NewFuzzer(seed ...int64) *Fuzzer { + var actualSeed int64 + if len(seed) != 0 { + actualSeed = seed[0] + } else { + actualSeed = rand.Int63() + } + return &Fuzzer{seed: actualSeed, rnd: rand.New(rand.NewSource(actualSeed))} +} + +// Char randomly generates a character from the given set. +// Each character has the given weight. +func (f *Fuzzer) Char(set string, weight int) { + for _, r := range set { + f.addChar(r, weight) + } +} + +// Range randomly generates a character from the given range. +// Each character has the given weight. +func (f *Fuzzer) Range(minIncl, maxIncl rune, weight int) { + for r := minIncl; r <= maxIncl; r++ { + f.addChar(r, weight) + } +} + +func (f *Fuzzer) addChar(r rune, weight int) { + f.stock = append(f.stock, struct { + r rune + weight int + }{r, weight}) + f.total += weight +} + +func (f *Fuzzer) Generate(length int) string { + s := "" + for i := 0; i < length; i++ { + s += string(f.randomChar()) + } + f.last = s + return s +} + +func (f *Fuzzer) randomChar() rune { + i := int(f.rnd.Int31n(int32(f.total))) + for _, entry := range f.stock { + i -= entry.weight + if i < 0 { + return entry.r + } + } + panic("Out of stock") +} + +func (f *Fuzzer) CheckOk() { + if !f.ok { + dummyLine.Errorf("Fuzzing failed with seed %d, last generated value: %s", f.seed, f.last) + } +} + +func (f *Fuzzer) Ok() { f.ok = true } + +func (s *Suite) Test_Fuzzer__out_of_stock(c *check.C) { + fuzzer := NewFuzzer(0) + fuzzer.total = 1 // Trick the fuzzer to achieve full code coverage. + + c.Check( + func() { fuzzer.Generate(1) }, + check.Panics, + "Out of stock") +} Index: pkgsrc/pkgtools/pkglint/files/testnames_test.go diff -u /dev/null pkgsrc/pkgtools/pkglint/files/testnames_test.go:1.1 --- /dev/null Wed Oct 3 22:27:54 2018 +++ pkgsrc/pkgtools/pkglint/files/testnames_test.go Wed Oct 3 22:27:53 2018 @@ -0,0 +1,130 @@ +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "gopkg.in/check.v1" + "os" + "sort" + "strings" +) + +// Ensures that all test names follow a common naming scheme: +// +// Test_${Type}_${Method}__${description_using_underscores} +func (s *Suite) Test__test_names(c *check.C) { + + // addTestee adds a single type or function declaration + // to the testees. + addTestee := func(testees *[]string, decl ast.Decl) { + switch decl := decl.(type) { + + case *ast.GenDecl: + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + *testees = append(*testees, spec.Name.Name) + } + } + + case *ast.FuncDecl: + typePrefix := "" + if decl.Recv != nil { + typeExpr := decl.Recv.List[0].Type.(ast.Expr) + var typeName string + if star, ok := typeExpr.(*ast.StarExpr); ok { + typeName = star.X.(*ast.Ident).Name + } else { + typeName = typeExpr.(*ast.Ident).Name + } + typePrefix = strings.TrimSuffix(typeName, "Impl") + "." + } + *testees = append(*testees, typePrefix+decl.Name.Name) + } + } + + // loadAllTestees returns all type, function and method names + // from the current package, in the form FunctionName or + // TypeName.MethodName (omitting the * from the type name). + loadAllTestees := func() []string { + fset := token.NewFileSet() + pkgs, err := parser.ParseDir(fset, ".", func(fi os.FileInfo) bool { return true }, 0) + if err != nil { + panic(err) + } + + var typesAndFunctions []string + for _, file := range pkgs["main"].Files { + for _, decl := range file.Decls { + addTestee(&typesAndFunctions, decl) + } + } + + sort.Strings(typesAndFunctions) + return typesAndFunctions + } + + generateAllowedPrefixes := func(typesAndFunctions []string) map[string]bool { + prefixes := make(map[string]bool) + for _, funcName := range typesAndFunctions { + prefix := strings.Replace(funcName, ".", "_", 1) + prefixes[prefix] = true + } + + // Allow some special test name prefixes. + prefixes["Varalign"] = true + prefixes["ShellParser"] = true + return prefixes + } + + checkTestName := func(fullTestMethod string, testee string, descr string, prefixes map[string]bool) { + if !prefixes[testee] { + c.Errorf("%s: Testee %q not found.\n", fullTestMethod, testee) + } + if matches(descr, `\p{Ll}\p{Lu}`) { + switch descr { + case "comparing_YesNo_variable_to_string", + "GitHub", + "enumFrom", + "dquotBacktDquot", + "and_getSubdirs": + // These exceptions are ok. + + default: + c.Errorf("%s: Test description must not use CamelCase.\n", fullTestMethod) + } + } + } + + checkAll := func(typesAndFunctions []string, prefixes map[string]bool) { + for _, funcName := range typesAndFunctions { + typeAndMethod := strings.SplitN(funcName, ".", 2) + if len(typeAndMethod) == 2 { + method := typeAndMethod[1] + switch { + case !hasPrefix(method, "Test"): + // Ignore + + case hasPrefix(method, "Test__"): + // OK + + case hasPrefix(method, "Test_"): + refAndDescr := strings.SplitN(method[5:], "__", 2) + descr := "" + if len(refAndDescr) > 1 { + descr = refAndDescr[1] + } + checkTestName(funcName, refAndDescr[0], descr, prefixes) + + default: + c.Errorf("%s: Missing underscore.\n", funcName) + } + } + } + } + + testees := loadAllTestees() + prefixes := generateAllowedPrefixes(testees) + checkAll(testees, prefixes) +} --_----------=_153860567463630--