diff options
author | rillig <rillig@pkgsrc.org> | 2019-06-30 20:56:18 +0000 |
---|---|---|
committer | rillig <rillig@pkgsrc.org> | 2019-06-30 20:56:18 +0000 |
commit | 705b6a55cc98cda8d732ca886796e48b180d4258 (patch) | |
tree | d6c03aee392418755fbd7a5a77d29baff57855d6 /pkgtools | |
parent | 5ae793651d2eac119a606ed67f0a6b98429e38bd (diff) | |
download | pkgsrc-705b6a55cc98cda8d732ca886796e48b180d4258.tar.gz |
pkgtools/pkglint: update to 5.7.14
Changes since 5.7.13:
- Removed the -Cextra command line option since it didn't produce useful
warnings.
- Removed unwarranted warnings about _WRAP_EXTRA_ARGS.CC being used in
packages.
- Cleaned up the canonical order of variables in package Makefiles.
- Added a few commands to those that cannot fail, to reduce the number of
"at the left of the | operator" in shell programs.
- Fixed warnings about "-ggdb" being an unknown shell command.
- Reduced number of warnings about lists being used where a single value
is expected.
- Replaced unreliable check for invalid CFLAGS and LDFLAGS with a more
practical check.
- Renamed "RCS tag" to "CVS tag" to make the diagnostics more modern.
- Added warning when PKGNAME or PKGVERSION is used in MASTER_SITES.
- Reworded warning for missing or superfluous PLIST files.
- Lots of other detail changes, refactorings and automatic tests.
Diffstat (limited to 'pkgtools')
82 files changed, 6134 insertions, 2785 deletions
diff --git a/pkgtools/pkglint/Makefile b/pkgtools/pkglint/Makefile index 9ec9dc61fa0..808bc09cfd2 100644 --- a/pkgtools/pkglint/Makefile +++ b/pkgtools/pkglint/Makefile @@ -1,6 +1,6 @@ -# $NetBSD: Makefile,v 1.585 2019/06/10 19:51:57 rillig Exp $ +# $NetBSD: Makefile,v 1.586 2019/06/30 20:56:18 rillig Exp $ -PKGNAME= pkglint-5.7.13 +PKGNAME= pkglint-5.7.14 CATEGORIES= pkgtools DISTNAME= tools MASTER_SITES= ${MASTER_SITE_GITHUB:=golang/} diff --git a/pkgtools/pkglint/files/alternatives.go b/pkgtools/pkglint/files/alternatives.go index 7818dc2a02d..4fb88b2247e 100644 --- a/pkgtools/pkglint/files/alternatives.go +++ b/pkgtools/pkglint/files/alternatives.go @@ -16,13 +16,13 @@ func CheckFileAlternatives(filename string) { plist = G.Pkg.Plist } - checkPlistWrapper := func(line Line, wrapper string) { + checkPlistWrapper := func(line *Line, wrapper string) { if plist.Files[wrapper] { line.Errorf("Alternative wrapper %q must not appear in the PLIST.", wrapper) } } - checkPlistAlternative := func(line Line, alternative string) { + checkPlistAlternative := func(line *Line, alternative string) { relImplementation := strings.Replace(alternative, "@PREFIX@/", "", 1) plistName := replaceAll(relImplementation, `@(\w+)@`, "${$1}") if plist.Files[plistName] || G.Pkg.vars.Defined("ALTERNATIVES_SRC") { diff --git a/pkgtools/pkglint/files/alternatives_test.go b/pkgtools/pkglint/files/alternatives_test.go index 596f99fb6ab..62ddfb5ef03 100644 --- a/pkgtools/pkglint/files/alternatives_test.go +++ b/pkgtools/pkglint/files/alternatives_test.go @@ -17,7 +17,7 @@ func (s *Suite) Test_CheckFileAlternatives__PLIST(c *check.C) { "highscores @VARBASE@/game/scores", "sbin/init /sbin/init") t.CreateFileLines("PLIST", - PlistRcsID, + PlistCvsID, "bin/echo", "bin/vim", "sbin/sendmail.exim${EXIMVER}") diff --git a/pkgtools/pkglint/files/autofix.go b/pkgtools/pkglint/files/autofix.go index 9996e9c3fb6..11ea21fb9a6 100644 --- a/pkgtools/pkglint/files/autofix.go +++ b/pkgtools/pkglint/files/autofix.go @@ -13,7 +13,7 @@ import ( // The modifications are kept in memory only, // until they are written to disk by SaveAutofixChanges. type Autofix struct { - line Line + line *Line linesBefore []string // Newly inserted lines, including \n linesAfter []string // Newly inserted lines, including \n // Whether an actual fix has been applied (or, without --show-autofix, @@ -51,7 +51,10 @@ const SilentAutofixFormat = "SilentAutofixFormat" // Since these are not really diagnostics, duplicates are not suppressed. const AutofixFormat = "AutofixFormat" -func NewAutofix(line Line) *Autofix { +func NewAutofix(line *Line) *Autofix { + // FIXME: replacing the returned value with + // &Autofix{line: line, autofixShortTerm: autofixShortTerm{anyway: true}} + // makes some tests output source code without diagnostic. return &Autofix{line: line} } @@ -75,9 +78,7 @@ func (fix *Autofix) Explain(explanation ...string) { // Since a silent fix doesn't have a diagnostic, its explanation would // not provide any clue as to what diagnostic it belongs. That would // be confusing, therefore this case is not allowed. - assertf( - fix.diagFormat != SilentAutofixFormat, - "Autofix: Silent fixes cannot have an explanation.") + assert(fix.diagFormat != SilentAutofixFormat) fix.explanation = explanation } @@ -95,8 +96,19 @@ func (fix *Autofix) ReplaceAfter(prefix, from string, to string) { return } + prefixFrom := prefix + from + prefixTo := prefix + to + + n := 0 for _, rawLine := range fix.line.raw { - replaced := strings.Replace(rawLine.textnl, prefix+from, prefix+to, 1) + n += strings.Count(rawLine.textnl, prefixFrom) + } + if n != 1 { + return + } + + for _, rawLine := range fix.line.raw { + replaced := strings.Replace(rawLine.textnl, prefixFrom, prefixTo, 1) if replaced != rawLine.textnl { if G.Logger.IsAutofix() { rawLine.textnl = replaced @@ -108,7 +120,7 @@ func (fix *Autofix) ReplaceAfter(prefix, from string, to string) { // TODO: Do this properly by parsing the whole line again, // and ideally everything that depends on the parsed line. // This probably requires a generic notification mechanism. - fix.line.Text = strings.Replace(fix.line.Text, prefix+from, prefix+to, 1) + fix.line.Text = strings.Replace(fix.line.Text, prefixFrom, prefixTo, 1) } fix.Describef(rawLine.Lineno, "Replacing %q with %q.", from, to) return @@ -274,11 +286,10 @@ func (fix *Autofix) Anyway() { func (fix *Autofix) Apply() { line := fix.line + // Each autofix must have a log level and a diagnostic. // To fix this assertion, call one of Autofix.Errorf, Autofix.Warnf // or Autofix.Notef before calling Apply. - assertf( - fix.level != nil, - "Each autofix must have a log level and a diagnostic.") + assert(fix.level != nil) reset := func() { if len(fix.actions) > 0 { @@ -339,14 +350,14 @@ func (fix *Autofix) Apply() { reset() } -func (fix *Autofix) Realign(mkline MkLine, newWidth int) { +func (fix *Autofix) Realign(mkline *MkLine, newWidth int) { // XXX: Check whether this method can be implemented as Custom fix. // This complicated code should not be in the Autofix type. fix.assertRealLine() - assertf(mkline.IsMultiline(), "Line must be a multiline.") - assertf(mkline.IsVarassign() || mkline.IsCommentedVarassign(), "Line must be a variable assignment.") + assert(mkline.IsMultiline()) + assert(mkline.IsVarassign() || mkline.IsCommentedVarassign()) if fix.skip() { return @@ -409,8 +420,8 @@ func (fix *Autofix) setDiag(level *LogLevel, format string, args []interface{}) "Autofix: format %q must end with a period.", format) } - assertf(fix.level == nil, "Autofix can only have a single diagnostic.") - assertf(fix.diagFormat == "", "Autofix can only have a single diagnostic.") + assert(fix.level == nil) // Autofix can only have a single diagnostic. + assert(fix.diagFormat == "") // Autofix can only have a single diagnostic. fix.level = level fix.diagFormat = format @@ -418,22 +429,21 @@ func (fix *Autofix) setDiag(level *LogLevel, format string, args []interface{}) } func (fix *Autofix) skip() bool { - assertf( - fix.diagFormat != "", - "Autofix: The diagnostic must be given before the action.") + assert(fix.diagFormat != "") // The diagnostic must be given before the action. + // This check is necessary for the --only command line option. return !G.Logger.shallBeLogged(fix.diagFormat) } func (fix *Autofix) assertRealLine() { - assertf(fix.line.firstLine >= 1, "Cannot autofix this line since it is not a real line.") + assert(fix.line.firstLine >= 1) // Cannot autofix this line since it is not a real line. } // SaveAutofixChanges writes the given lines back into their files, // applying the autofix changes. // The lines may come from different files. // Only files that actually have changed lines are saved. -func SaveAutofixChanges(lines Lines) (autofixed bool) { +func SaveAutofixChanges(lines *Lines) (autofixed bool) { if trace.Tracing { defer trace.Call0()() } diff --git a/pkgtools/pkglint/files/autofix_test.go b/pkgtools/pkglint/files/autofix_test.go index 2f54fb1ca5e..993ce219e77 100644 --- a/pkgtools/pkglint/files/autofix_test.go +++ b/pkgtools/pkglint/files/autofix_test.go @@ -14,9 +14,7 @@ func (s *Suite) Test_Autofix_Warnf__duplicate(c *check.C) { fix := line.Autofix() fix.Warnf("Warning 1.") - t.ExpectPanic( - func() { fix.Warnf("Warning 2.") }, - "Pkglint internal error: Autofix can only have a single diagnostic.") + t.ExpectAssert(func() { fix.Warnf("Warning 2.") }) } func (s *Suite) Test_Autofix__default_leaves_line_unchanged(c *check.C) { @@ -58,7 +56,7 @@ func (s *Suite) Test_Autofix__show_autofix_modifies_line(c *check.C) { fix := line.Autofix() fix.Warnf("Row should be replaced with line.") - fix.ReplaceAfter("", "row", "line") + fix.ReplaceAfter("", "# row", "# line") fix.ReplaceRegex(`row \d+`, "the above line", -1) fix.InsertBefore("above") fix.InsertAfter("below") @@ -70,7 +68,7 @@ func (s *Suite) Test_Autofix__show_autofix_modifies_line(c *check.C) { "below\n") t.CheckOutputLines( "WARN: ~/Makefile:1--2: Row should be replaced with line.", - "AUTOFIX: ~/Makefile:1: Replacing \"row\" with \"line\".", + "AUTOFIX: ~/Makefile:1: Replacing \"# row\" with \"# line\".", "AUTOFIX: ~/Makefile:2: Replacing \"row 1\" with \"the above line\".", "AUTOFIX: ~/Makefile:1: Inserting a line \"above\" before this line.", "AUTOFIX: ~/Makefile:2: Inserting a line \"below\" after this line.", @@ -83,7 +81,7 @@ func (s *Suite) Test_Autofix__show_autofix_modifies_line(c *check.C) { c.Check(fix.modified, equals, true) } -func (s *Suite) Test_Autofix_ReplaceAfter__autofix(c *check.C) { +func (s *Suite) Test_Autofix_ReplaceAfter__autofix_in_continuation_line(c *check.C) { t := s.Init(c) t.SetUpCommandLine("--autofix", "--source") @@ -93,18 +91,53 @@ func (s *Suite) Test_Autofix_ReplaceAfter__autofix(c *check.C) { "continuation 2") fix := mklines.lines.Lines[0].Autofix() - fix.Warnf("N should be replaced with V.") - fix.ReplaceAfter("", "n", "v") + fix.Warnf("Line should be replaced with Row.") + fix.ReplaceAfter("", "line", "row") fix.Apply() t.CheckOutputLines( - "AUTOFIX: ~/Makefile:1: Replacing \"n\" with \"v\".", + "AUTOFIX: ~/Makefile:1: Replacing \"line\" with \"row\".", "-\t# line 1 \\", - "+\t# live 1 \\", + "+\t# row 1 \\", "\tcontinuation 1 \\", "\tcontinuation 2") } +func (s *Suite) Test_Autofix_ReplaceAfter__autofix_several_times_in_continuation_line(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("--autofix", "--source") + mklines := t.SetUpFileMkLines("Makefile", + "# line 1 \\", + "continuation 1 \\", + "continuation 2") + + fix := mklines.lines.Lines[0].Autofix() + fix.Warnf("N should be replaced with V.") + fix.ReplaceAfter("", "n", "v") + fix.Apply() + + // Nothing is logged or fixed because the "n" appears more than once, + // and as of June 2019, pkglint doesn't know which occurrence is the + // correct one. + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Autofix_ReplaceAfter__autofix_one_time(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("--autofix", "--source") + mklines := t.SetUpFileMkLines("Makefile", + MkCvsID, + "VAR=\t$$(var) $(var)") + + mklines.Check() + + // Nothing is replaced since, as of June 2019, pkglint doesn't + // know which of the two "$(var)" should be replaced. + t.CheckOutputEmpty() +} + func (s *Suite) Test_Autofix_ReplaceRegex__show_autofix(c *check.C) { t := s.Init(c) @@ -157,19 +190,19 @@ func (s *Suite) Test_Autofix_ReplaceRegex__autofix(c *check.C) { // After calling fix.Apply above, the autofix is ready to be used again. fix.Warnf("Use Y instead of X.") - fix.Replace("X", "Y") + fix.Replace("XXX", "YYY") fix.Apply() t.CheckOutputLines( - "AUTOFIX: ~/Makefile:2: Replacing \"X\" with \"Y\".", + "AUTOFIX: ~/Makefile:2: Replacing \"XXX\" with \"YYY\".", "-\tline2", - "+\tYXXe2") + "+\tYYYe2") SaveAutofixChanges(lines) t.CheckFileLines("Makefile", "line1", - "YXXe2", + "YYYe2", "line3") } @@ -188,7 +221,7 @@ func (s *Suite) Test_Autofix_ReplaceRegex__show_autofix_and_source(c *check.C) { fix.Apply() fix.Warnf("Use Y instead of X.") - fix.Replace("X", "Y") + fix.Replace("XXXXX", "YYYYY") fix.Apply() SaveAutofixChanges(lines) @@ -204,9 +237,9 @@ func (s *Suite) Test_Autofix_ReplaceRegex__show_autofix_and_source(c *check.C) { "+\tXXXXX", "", "WARN: ~/Makefile:2: Use Y instead of X.", - "AUTOFIX: ~/Makefile:2: Replacing \"X\" with \"Y\".", + "AUTOFIX: ~/Makefile:2: Replacing \"XXXXX\" with \"YYYYY\".", "-\tline2", - "+\tYXXXX") + "+\tYYYYY") } // When an autofix replaces text, it does not touch those @@ -309,7 +342,7 @@ func (s *Suite) Test_Autofix__multiple_fixes(c *check.C) { { fix := line.Autofix() fix.Warnf(SilentAutofixFormat) - fix.Replace("i", "u") + fix.Replace("ig", "ug") fix.Apply() } @@ -317,7 +350,7 @@ func (s *Suite) Test_Autofix__multiple_fixes(c *check.C) { c.Check(line.raw, check.DeepEquals, t.NewRawLines(1, "original\n", "lruginao\n")) c.Check(line.raw[0].textnl, equals, "lruginao\n") t.CheckOutputLines( - "AUTOFIX: filename:1: Replacing \"i\" with \"u\".") + "AUTOFIX: filename:1: Replacing \"ig\" with \"ug\".") { fix := line.Autofix() @@ -429,9 +462,7 @@ func (s *Suite) Test_Autofix_Explain__SilentAutofixFormat(c *check.C) { fix := line.Autofix() fix.Warnf(SilentAutofixFormat) - t.ExpectPanic( - func() { fix.Explain("Explanation for inserting a line before.") }, - "Pkglint internal error: Autofix: Silent fixes cannot have an explanation.") + t.ExpectAssert(func() { fix.Explain("Explanation for inserting a line before.") }) } // To combine a silent diagnostic with an explanation, two separate autofixes @@ -468,7 +499,7 @@ func (s *Suite) Test_Autofix__show_autofix_and_source_continuation_line(c *check t.SetUpCommandLine("--show-autofix", "--source") mklines := t.SetUpFileMkLines("Makefile", - MkRcsID, + MkCvsID, "# before \\", "The old song \\", "after") @@ -534,7 +565,7 @@ func (s *Suite) Test_Autofix_Delete__continuation_line(c *check.C) { t.SetUpCommandLine("--show-autofix", "--source") mklines := t.SetUpFileMkLines("Makefile", - MkRcsID, + MkCvsID, "# line 1 \\", "continued") line := mklines.lines.Lines[1] @@ -713,7 +744,7 @@ func (s *Suite) Test_Autofix_Custom__in_memory(c *check.C) { "line2", "line3") - doFix := func(line Line) { + doFix := func(line *Line) { fix := line.Autofix() fix.Warnf("Please write in ALL-UPPERCASE.") fix.Custom(func(showAutofix, autofix bool) { @@ -816,20 +847,18 @@ func (s *Suite) Test_Autofix_Apply__panic(c *check.C) { line := t.NewLine("filename", 123, "text") - t.ExpectPanic( + t.ExpectAssert( func() { fix := line.Autofix() fix.Apply() - }, - "Pkglint internal error: Each autofix must have a log level and a diagnostic.") + }) - t.ExpectPanic( + t.ExpectAssert( func() { fix := line.Autofix() fix.Replace("from", "to") fix.Apply() - }, - "Pkglint internal error: Autofix: The diagnostic must be given before the action.") + }) t.ExpectPanic( func() { @@ -936,6 +965,29 @@ func (s *Suite) Test_Autofix_Apply__autofix_and_show_autofix_options(c *check.C) "AUTOFIX: filename:5: Replacing \"text\" with \"replacement\".") } +// In --autofix mode or --show-autofix mode, the fix.Anyway doesn't +// have any effect, therefore the errors from such autofixes are +// not counted, and the exitcode stays at 0. +func (s *Suite) Test_Autofix_Apply__anyway_error(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("--autofix") + mklines := t.SetUpFileMkLines("filename.mk", + MkCvsID, + "VAR=\tvalue") + + fix := mklines.mklines[1].Autofix() + fix.Errorf("From must be To.") + fix.Replace("from", "to") + fix.Anyway() + fix.Apply() + + mklines.SaveAutofixChanges() + + t.Check(G.Logger.errors, equals, 0) + t.CheckOutputEmpty() +} + // Ensures that without explanations, the separator between the individual // diagnostics are generated. func (s *Suite) Test_Autofix_Apply__source_without_explain(c *check.C) { @@ -1017,16 +1069,14 @@ func (s *Suite) Test_Autofix_Realign__wrong_line_type(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, ".if \\", "${PKGSRC_RUN_TESTS}") mkline := mklines.mklines[1] fix := mkline.Autofix() - t.ExpectPanic( - func() { fix.Realign(mkline, 16) }, - "Pkglint internal error: Line must be a variable assignment.") + t.ExpectAssert(func() { fix.Realign(mkline, 16) }) } func (s *Suite) Test_Autofix_Realign__short_continuation_line(c *check.C) { @@ -1034,7 +1084,7 @@ func (s *Suite) Test_Autofix_Realign__short_continuation_line(c *check.C) { t.SetUpCommandLine("--autofix") mklines := t.SetUpFileMkLines("file.mk", - MkRcsID, + MkCvsID, "BUILD_DIRS= \\", "\tdir \\", "") @@ -1050,7 +1100,7 @@ func (s *Suite) Test_Autofix_Realign__short_continuation_line(c *check.C) { t.CheckOutputEmpty() t.CheckFileLines("file.mk", - MkRcsID, + MkCvsID, "BUILD_DIRS= \\", "\tdir \\", "") @@ -1061,7 +1111,7 @@ func (s *Suite) Test_Autofix_Realign__multiline_indented_with_spaces(c *check.C) t.SetUpCommandLine("--autofix") mklines := t.SetUpFileMkLines("file.mk", - MkRcsID, + MkCvsID, "BUILD_DIRS= \\", "\t dir1 \\", "\t\tdir2 \\", @@ -1079,7 +1129,7 @@ func (s *Suite) Test_Autofix_Realign__multiline_indented_with_spaces(c *check.C) t.CheckOutputLines( "AUTOFIX: ~/file.mk:3: Replacing indentation \"\\t \" with \"\\t\\t\".") t.CheckFileLines("file.mk", - MkRcsID, + MkCvsID, "BUILD_DIRS= \\", "\t\tdir1 \\", "\t\tdir2 \\", @@ -1109,14 +1159,10 @@ func (s *Suite) Test_Autofix_setDiag__bad_call_sequence(c *check.C) { fix := line.Autofix() fix.Notef("Note.") - t.ExpectPanic( - func() { fix.Notef("Note 2.") }, - "Pkglint internal error: Autofix can only have a single diagnostic.") + t.ExpectAssert(func() { fix.Notef("Note 2.") }) fix.level = nil // To cover the second assertion. - t.ExpectPanic( - func() { fix.Notef("Note 2.") }, - "Pkglint internal error: Autofix can only have a single diagnostic.") + t.ExpectAssert(func() { fix.Notef("Note 2.") }) } func (s *Suite) Test_Autofix_assertRealLine(c *check.C) { @@ -1126,9 +1172,7 @@ func (s *Suite) Test_Autofix_assertRealLine(c *check.C) { fix := line.Autofix() fix.Warnf("Warning.") - t.ExpectPanic( - func() { fix.Replace("from", "to") }, - "Pkglint internal error: Cannot autofix this line since it is not a real line.") + t.ExpectAssert(func() { fix.Replace("from", "to") }) } func (s *Suite) Test_SaveAutofixChanges__file_removed(c *check.C) { @@ -1164,7 +1208,7 @@ func (s *Suite) Test_SaveAutofixChanges__file_busy_Windows(c *check.C) { // As long as the file is kept open, it cannot be overwritten or deleted. openFile, err := os.OpenFile(t.File("subdir/file.txt"), 0, 0666) - defer openFile.Close() + defer func() { assertNil(openFile.Close(), "") }() c.Check(err, check.IsNil) fix := lines.Lines[0].Autofix() @@ -1220,7 +1264,7 @@ func (s *Suite) Test_Autofix__lonely_source(c *check.C) { "DISTNAME=\txorgproto-1.0") t.CreateFileDummyBuildlink3("x11/xorgproto/buildlink3.mk") t.CreateFileLines("x11/xorgproto/builtin.mk", - MkRcsID, + MkCvsID, "", "BUILTIN_PKG:=\txorgproto", "", diff --git a/pkgtools/pkglint/files/buildlink3.go b/pkgtools/pkglint/files/buildlink3.go index c396a124d07..f87acee5232 100644 --- a/pkgtools/pkglint/files/buildlink3.go +++ b/pkgtools/pkglint/files/buildlink3.go @@ -7,28 +7,28 @@ import ( ) type Buildlink3Checker struct { - mklines MkLines + mklines *MkLines pkgbase string - pkgbaseLine MkLine - abiLine, apiLine MkLine + pkgbaseLine *MkLine + abiLine, apiLine *MkLine abi, api *DependencyPattern } -func CheckLinesBuildlink3Mk(mklines MkLines) { +func CheckLinesBuildlink3Mk(mklines *MkLines) { (&Buildlink3Checker{mklines: mklines}).Check() } func (ck *Buildlink3Checker) Check() { mklines := ck.mklines if trace.Tracing { - defer trace.Call1(mklines.lines.FileName)() + defer trace.Call1(mklines.lines.Filename)() } mklines.Check() llex := NewMkLinesLexer(mklines) - for llex.SkipIf(MkLine.IsComment) { + for llex.SkipIf((*MkLine).IsComment) { line := llex.PreviousLine() // See pkgtools/createbuildlink/files/createbuildlink if hasPrefix(line.Text, "# XXX This file was created automatically") { @@ -94,7 +94,7 @@ func (ck *Buildlink3Checker) checkFirstParagraph(mlex *MkLinesLexer) bool { return true } -func (ck *Buildlink3Checker) checkUniquePkgbase(pkgbase string, mkline MkLine) { +func (ck *Buildlink3Checker) checkUniquePkgbase(pkgbase string, mkline *MkLine) { prev := G.InterPackage.Bl3(pkgbase, &mkline.Location) if prev == nil { return @@ -182,7 +182,7 @@ func (ck *Buildlink3Checker) checkMainPart(mlex *MkLinesLexer) bool { return true } -func (ck *Buildlink3Checker) checkVarassign(mlex *MkLinesLexer, mkline MkLine, pkgbase string) { +func (ck *Buildlink3Checker) checkVarassign(mlex *MkLinesLexer, mkline *MkLine, pkgbase string) { varname, value := mkline.Varname(), mkline.Value() doCheck := false @@ -229,7 +229,7 @@ func (ck *Buildlink3Checker) checkVarassign(mlex *MkLinesLexer, mkline MkLine, p } } -func (ck *Buildlink3Checker) checkVaruseInPkgbase(pkgbase string, pkgbaseLine MkLine) { +func (ck *Buildlink3Checker) checkVaruseInPkgbase(pkgbase string, pkgbaseLine *MkLine) { tokens, _ := pkgbaseLine.ValueTokens() for _, token := range tokens { if token.Varuse == nil { diff --git a/pkgtools/pkglint/files/buildlink3_test.go b/pkgtools/pkglint/files/buildlink3_test.go index 5d1a50daf90..17d7d86cce8 100644 --- a/pkgtools/pkglint/files/buildlink3_test.go +++ b/pkgtools/pkglint/files/buildlink3_test.go @@ -9,12 +9,10 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__package(c *check.C) { t := s.Init(c) t.CreateFileLines("category/dependency1/buildlink3.mk", - MkRcsID) + MkCvsID) t.CreateFileLines("category/dependency2/buildlink3.mk", - MkRcsID) + MkCvsID) t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0", - "", ".include \"../../category/dependency1/buildlink3.mk\"") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk", @@ -36,7 +34,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__unfinished_url2pkg(c *check.C) { t.CreateFileLines("x11/Xbae/Makefile") t.CreateFileLines("mk/motif.buildlink3.mk") mklines := t.SetUpFileMkLines("category/package/buildlink3.mk", - MkRcsID, + MkCvsID, "# XXX This file was created automatically using createbuildlink-@PKGVERSION@", "", "BUILDLINK_TREE+=\tXbae", @@ -73,7 +71,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch_Haskell_incomplete(c "DISTNAME=\tX11-1.0") t.Chdir("x11/hs-X11") t.CreateFileLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", @@ -105,7 +103,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch_Haskell_complete(c *c t := s.Init(c) t.CreateFileLines("mk/haskell.mk", - MkRcsID, + MkCvsID, "PKGNAME?=\ths-${DISTNAME}") t.SetUpPackage("x11/hs-X11", "DISTNAME=\tX11-1.0", @@ -113,7 +111,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch_Haskell_complete(c *c ".include \"../../mk/haskell.mk\"") t.Chdir("x11/hs-X11") t.CreateFileLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", @@ -140,7 +138,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch__Perl(c *check.C) { "DISTNAME=\tGtk2-1.0", "PKGNAME=\t${DISTNAME:C:Gtk2:p5-gtk2:}") t.CreateFileLines("x11/p5-gtk2/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\tp5-gtk2", "", @@ -171,7 +169,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch_multiple_inclusion(c t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\tpkgbase1", "", @@ -195,7 +193,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch_abi_api(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", @@ -222,7 +220,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__abi_api_versions(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", @@ -252,7 +250,7 @@ func (s *Suite) Test_Buildlink3Checker_checkVarassign__abi_api_versions_brace(c t.SetUpVartypes() t.CreateFileLines("multimedia/totem/Makefile") mklines := t.SetUpFileMkLines("multimedia/totem/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ttotem", "", @@ -279,7 +277,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__missing_BUILDLINK_TREE_at_beginning t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", ".if !defined(HS_X11_BUILDLINK3_MK)", "HS_X11_BUILDLINK3_MK:=", @@ -298,7 +296,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__missing_BUILDLINK_TREE_at_end(c *ch t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", @@ -324,7 +322,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__DEPMETHOD_placement(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_DEPMETHOD.hs-X11?=\tfull", "", @@ -351,7 +349,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__multiple_inclusion_wrong(c *check.C t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", @@ -372,7 +370,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__missing_endif(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\tpkgbase1", "", @@ -391,7 +389,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__invalid_dependency_patterns(c *chec t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", @@ -418,7 +416,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__PKGBASE_with_variable(c *check.C) { t.SetUpVartypes() mklinesPhp := t.NewMkLines("x11/php-wxwidgets/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\t${PHP_PKG_PREFIX}-wxWidgets", "", @@ -432,7 +430,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__PKGBASE_with_variable(c *check.C) { "", "BUILDLINK_TREE+=\t-${PHP_PKG_PREFIX}-wxWidgets") mklinesPy := t.NewMkLines("x11/py-wxwidgets/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\t${PYPKGPREFIX}-wxWidgets", "", @@ -446,7 +444,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__PKGBASE_with_variable(c *check.C) { "", "BUILDLINK_TREE+=\t-${PYPKGPREFIX}-wxWidgets") mklinesRuby1 := t.NewMkLines("x11/ruby1-wxwidgets/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\t${RUBY_BASE}-wxWidgets", "", @@ -460,7 +458,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__PKGBASE_with_variable(c *check.C) { "", "BUILDLINK_TREE+=\t-${RUBY_BASE}-wxWidgets") mklinesRuby2 := t.NewMkLines("x11/ruby2-wxwidgets/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\t${RUBY_PKGPREFIX}-wxWidgets", "", @@ -491,7 +489,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__PKGBASE_with_unknown_variable(c *ch t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\t${LICENSE}-wxWidgets", "", @@ -576,8 +574,7 @@ func (s *Suite) Test_Buildlink3Checker_checkUniquePkgbase(c *check.C) { func (s *Suite) Test_Buildlink3Checker_checkMainPart__if_else_endif(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk", ".if ${X11_TYPE} == modular", ".else", @@ -592,8 +589,7 @@ func (s *Suite) Test_Buildlink3Checker_checkMainPart__if_else_endif(c *check.C) func (s *Suite) Test_Buildlink3Checker_checkVarassign__dependencies_with_path(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk", "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=1.0:../../category/package", "BUILDLINK_API_DEPENDS.package+=\tpackage>=1.5:../../category/package") @@ -613,11 +609,10 @@ func (s *Suite) Test_Buildlink3Checker_checkVarassign__dependencies_with_path(c func (s *Suite) Test_Buildlink3Checker_checkVarassign__abi_without_api(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") // t.CreateFileDummyBuildlink3() cannot be used here since it always adds an API line. t.CreateFileLines("category/package/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\tpackage", "", @@ -643,8 +638,7 @@ func (s *Suite) Test_Buildlink3Checker_checkVarassign__abi_without_api(c *check. func (s *Suite) Test_Buildlink3Checker_checkVarassign__abi_and_api_with_variables(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk", "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=${ABI_VERSION}", "BUILDLINK_API_DEPENDS.package+=\tpackage>=${API_VERSION}", @@ -662,8 +656,7 @@ func (s *Suite) Test_Buildlink3Checker_checkVarassign__abi_and_api_with_variable func (s *Suite) Test_Buildlink3Checker_checkVarassign__api_with_variable(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk", "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=1.0", "BUILDLINK_API_DEPENDS.package+=\tpackage>=${API_VERSION}", @@ -680,8 +673,7 @@ func (s *Suite) Test_Buildlink3Checker_checkVarassign__api_with_variable(c *chec func (s *Suite) Test_Buildlink3Checker_checkVarassign__abi_and_api_with_pattern(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk", "BUILDLINK_ABI_DEPENDS.package+=\tpackage-1.*", "BUILDLINK_API_DEPENDS.package+=\tpackage-2.*") @@ -697,8 +689,7 @@ func (s *Suite) Test_Buildlink3Checker_checkVarassign__abi_and_api_with_pattern( func (s *Suite) Test_Buildlink3Checker_checkVarassign__api_with_pattern(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk", "BUILDLINK_ABI_DEPENDS.package+=\tpackage>=1", "BUILDLINK_API_DEPENDS.package+=\tpackage-1.*") @@ -714,8 +705,7 @@ func (s *Suite) Test_Buildlink3Checker_checkVarassign__api_with_pattern(c *check func (s *Suite) Test_Buildlink3Checker_checkVarassign__other_variables(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk", "BUILDLINK_TREE+=\tmistake", // Wrong, but doesn't happen in practice. "", @@ -762,6 +752,8 @@ func (s *Suite) Test_Buildlink3Checker_checkSecondParagraph__missing_mkbase(c *c // There is no warning from buildlink3.mk about mismatched package names // since that is only a follow-up error of being unable to parse the pkgbase. t.CheckOutputLines( + "WARN: ~/category/package/Makefile:3: As DISTNAME is not a valid package name, "+ + "please define the PKGNAME explicitly.", "WARN: ~/category/package/Makefile:4: \"\" is not a valid package name.") } @@ -772,7 +764,7 @@ func (s *Suite) Test_Buildlink3Checker_checkMainPart__nested_if(c *check.C) { t.SetUpVartypes() mklines := t.SetUpFileMkLines("category/package/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", @@ -799,7 +791,7 @@ func (s *Suite) Test_Buildlink3Checker_checkMainPart__comment_at_end_of_file(c * t.SetUpVartypes() mklines := t.SetUpFileMkLines("category/package/buildlink3.mk", - MkRcsID, + MkCvsID, "", "BUILDLINK_TREE+=\ths-X11", "", diff --git a/pkgtools/pkglint/files/category.go b/pkgtools/pkglint/files/category.go index 46a0c2a0a61..4519df9c604 100644 --- a/pkgtools/pkglint/files/category.go +++ b/pkgtools/pkglint/files/category.go @@ -23,7 +23,7 @@ func CheckdirCategory(dir string) { } mlex.SkipEmptyOrNote() - if mlex.SkipIf(func(mkline MkLine) bool { return mkline.IsVarassign() && mkline.Varname() == "COMMENT" }) { + if mlex.SkipIf(func(mkline *MkLine) bool { return mkline.IsVarassign() && mkline.Varname() == "COMMENT" }) { mkline := mlex.PreviousMkLine() lex := textproc.NewLexer(mkline.Value()) @@ -50,7 +50,7 @@ func CheckdirCategory(dir string) { type subdir struct { name string - line MkLine + line *MkLine } // And now to the most complicated part of the category Makefiles, @@ -60,7 +60,7 @@ func CheckdirCategory(dir string) { fSubdirs := getSubdirs(dir) var mSubdirs []subdir - seen := make(map[string]MkLine) + seen := make(map[string]*MkLine) for !mlex.EOF() { mkline := mlex.CurrentMkLine() @@ -113,7 +113,7 @@ func CheckdirCategory(dir string) { if len(fRest) > 0 && (len(mRest) == 0 || fRest[0] < mRest[0].name) { fCurrent := fRest[0] if !mCheck[fCurrent] { - var line Line + var line *Line if len(mRest) > 0 { line = mRest[0].line.Line } else { diff --git a/pkgtools/pkglint/files/category_test.go b/pkgtools/pkglint/files/category_test.go index c92769fffd4..ee49f398c7b 100644 --- a/pkgtools/pkglint/files/category_test.go +++ b/pkgtools/pkglint/files/category_test.go @@ -41,7 +41,7 @@ func (s *Suite) Test_CheckdirCategory__invalid_comment(c *check.C) { t.SetUpVartypes() t.CreateFileLines("archivers/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\t\\Make $$$$ fast\"", "", @@ -72,7 +72,7 @@ func (s *Suite) Test_CheckdirCategory__wip(c *check.C) { t.CreateFileLines("mk/misc/category.mk") t.CreateFileLines("wip/package/Makefile") t.CreateFileLines("wip/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory comment", "", @@ -102,7 +102,7 @@ func (s *Suite) Test_CheckdirCategory__subdirs(c *check.C) { t.CreateFileLines("category/commented-mk-and-fs/Makefile") t.CreateFileLines("category/commented-without-reason/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory comment", "", @@ -137,7 +137,7 @@ func (s *Suite) Test_CheckdirCategory__only_in_Makefile(c *check.C) { t.CreateFileLines("mk/misc/category.mk") t.CreateFileLines("category/both/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory comment", "", @@ -166,7 +166,7 @@ func (s *Suite) Test_CheckdirCategory__only_in_file_system(c *check.C) { t.CreateFileLines("category/both/Makefile") t.CreateFileLines("category/only-in-fs/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory comment", "", @@ -193,7 +193,7 @@ func (s *Suite) Test_CheckdirCategory__recursive(c *check.C) { t.CreateFileLines("category/commented/Makefile") t.CreateFileLines("category/package/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory comment", "", @@ -204,13 +204,22 @@ func (s *Suite) Test_CheckdirCategory__recursive(c *check.C) { t.Chdir("category") t.FinishSetUp() + // The default argument "." is added when parsing the command line. + // It is only removed in Pkglint.Main, therefore it stays there even + // after the call to CheckdirCategory. This is a bit unrealistic, + // but close enough for this test. + t.Check( + G.Todo, + deepEquals, + []string{"."}) + CheckdirCategory(".") t.CheckOutputEmpty() t.Check( G.Todo, deepEquals, - []string{"./package"}) + []string{"./package", "."}) } // Ensures that a directory in the file system can be added at the very @@ -230,7 +239,7 @@ func (s *Suite) Test_CheckdirCategory__subdirs_file_system_at_the_bottom(c *chec t.CreateFileLines("category/mk-and-fs/Makefile") t.CreateFileLines("category/zzz-fs-only/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory comment", "", @@ -254,7 +263,7 @@ func (s *Suite) Test_CheckdirCategory__indentation(c *check.C) { t.CreateFileLines("category/package1/Makefile") t.CreateFileLines("category/package2/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory comment", "", @@ -277,7 +286,7 @@ func (s *Suite) Test_CheckdirCategory__comment_at_the_top(c *check.C) { t.CreateFileLines("mk/misc/category.mk") t.CreateFileLines("category/package/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "# This category collects all programs that don't fit anywhere else.", "", @@ -312,7 +321,7 @@ func (s *Suite) Test_CheckdirCategory__unexpected_EOF_while_reading_SUBDIR(c *ch t.CreateFileLines("mk/misc/category.mk") t.CreateFileLines("category/package/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory comment", "", diff --git a/pkgtools/pkglint/files/check_test.go b/pkgtools/pkglint/files/check_test.go index 32e247f227f..493084c1182 100644 --- a/pkgtools/pkglint/files/check_test.go +++ b/pkgtools/pkglint/files/check_test.go @@ -19,9 +19,9 @@ import ( var equals = check.Equals var deepEquals = check.DeepEquals -const RcsID = "$" + "NetBSD$" -const MkRcsID = "# $" + "NetBSD$" -const PlistRcsID = "@comment $" + "NetBSD$" +const CvsID = "$" + "NetBSD$" +const MkCvsID = "# $" + "NetBSD$" +const PlistCvsID = "@comment $" + "NetBSD$" type Suite struct { Tester *Tester @@ -72,29 +72,28 @@ func (s *Suite) SetUpTest(c *check.C) { t.c = c t.SetUpCommandLine("-Wall") // To catch duplicate warnings - t.c = nil // To improve code coverage and ensure that trace.Result works // in all cases. The latter cannot be ensured at compile time. t.EnableSilentTracing() prevdir, err := os.Getwd() - if err != nil { - c.Fatalf("Cannot get current working directory: %s", err) - } + assertNil(err, "Cannot get current working directory: %s", err) t.prevdir = prevdir + + // No longer usable; see https://github.com/go-check/check/issues/22 + t.c = nil } func (s *Suite) TearDownTest(c *check.C) { t := s.Tester t.c = nil // No longer usable; see https://github.com/go-check/check/issues/22 - if err := os.Chdir(t.prevdir); err != nil { - t.Errorf("Cannot chdir back to previous dir: %s", err) - } + err := os.Chdir(t.prevdir) + assertNil(err, "Cannot chdir back to previous dir: %s", err) if t.seenSetupPkgsrc > 0 && !t.seenFinish && !t.seenMain { - t.Errorf("After t.SetupPkgsrc(), t.FinishSetUp() or t.Main() must be called.") + t.Errorf("After t.SetupPkgsrc(), either t.FinishSetUp() or t.Main() must be called.") } if out := t.Output(); out != "" { @@ -202,7 +201,7 @@ func (t *Tester) SetUpTool(name, varname string, validity Validity) *Tool { // The file is then read in, without interpreting line continuations. // // See SetUpFileMkLines for loading a Makefile fragment. -func (t *Tester) SetUpFileLines(relativeFileName string, lines ...string) Lines { +func (t *Tester) SetUpFileLines(relativeFileName string, lines ...string) *Lines { filename := t.CreateFileLines(relativeFileName, lines...) return Load(filename, MustSucceed) } @@ -211,7 +210,7 @@ func (t *Tester) SetUpFileLines(relativeFileName string, lines ...string) Lines // The file is then read in, handling line continuations for Makefiles. // // See SetUpFileLines for loading an ordinary file. -func (t *Tester) SetUpFileMkLines(relativeFileName string, lines ...string) MkLines { +func (t *Tester) SetUpFileMkLines(relativeFileName string, lines ...string) *MkLines { filename := t.CreateFileLines(relativeFileName, lines...) return LoadMk(filename, MustSucceed) } @@ -220,8 +219,8 @@ func (t *Tester) SetUpFileMkLines(relativeFileName string, lines ...string) MkLi // merging all the lines into a single MkLines object. // // This is useful for testing code related to Package.readMakefile. -func (t *Tester) LoadMkInclude(relativeFileName string) MkLines { - var lines []Line +func (t *Tester) LoadMkInclude(relativeFileName string) *MkLines { + var lines []*Line // TODO: Include files with multiple-inclusion guard only once. // TODO: Include files without multiple-inclusion guard as often as needed. @@ -251,23 +250,20 @@ func (t *Tester) LoadMkInclude(relativeFileName string) MkLines { // Individual files may be overwritten by calling other SetUp* methods. // // This setup is especially interesting for testing Pkglint.Main. -// -// If the test works on a lower level than Pkglint.Main, -// LoadInfrastructure must be called to actually load the infrastructure files. func (t *Tester) SetUpPkgsrc() { // This file is needed to locate the pkgsrc root directory. // See findPkgsrcTopdir. t.CreateFileLines("mk/bsd.pkg.mk", - MkRcsID) + MkCvsID) // See Pkgsrc.loadDocChanges. t.CreateFileLines("doc/CHANGES-2018", - RcsID) + CvsID) // See Pkgsrc.loadSuggestedUpdates. t.CreateFileLines("doc/TODO", - RcsID) + CvsID) // Some example licenses so that the tests for whole packages // don't need to define them on their own. @@ -283,7 +279,7 @@ func (t *Tester) SetUpPkgsrc() { // // See Pkgsrc.loadMasterSites. t.CreateFileLines("mk/fetch/sites.mk", - MkRcsID) + MkCvsID) // The options for the PKG_OPTIONS framework are defined here. // @@ -295,7 +291,7 @@ func (t *Tester) SetUpPkgsrc() { // The user-defined variables are read in to check for missing // BUILD_DEFS declarations in the package Makefile. t.CreateFileLines("mk/defaults/mk.conf", - MkRcsID) + MkCvsID) // The tool definitions are defined in various files in mk/tools/. // The relevant files are listed in bsd.tools.mk. @@ -303,14 +299,14 @@ func (t *Tester) SetUpPkgsrc() { t.CreateFileLines("mk/tools/bsd.tools.mk", ".include \"defaults.mk\"") t.CreateFileLines("mk/tools/defaults.mk", - MkRcsID) + MkCvsID) // Those tools that are added to USE_TOOLS in bsd.prefs.mk may be // used at load time by packages. t.CreateFileLines("mk/bsd.prefs.mk", - MkRcsID) + MkCvsID) t.CreateFileLines("mk/bsd.fast.prefs.mk", - MkRcsID) + MkCvsID) // Category Makefiles require this file for the common definitions. t.CreateFileLines("mk/misc/category.mk") @@ -321,11 +317,11 @@ func (t *Tester) SetUpPkgsrc() { // SetUpCategory makes the given category valid by creating a dummy Makefile. // After that, it can be mentioned in the CATEGORIES variable of a package. func (t *Tester) SetUpCategory(name string) { - assertf(!contains(name, "/"), "Category must not contain a slash.") + assert(!contains(name, "/")) // Category must not contain a slash. if _, err := os.Stat(t.File(name + "/Makefile")); os.IsNotExist(err) { t.CreateFileLines(name+"/Makefile", - MkRcsID) + MkCvsID) } } @@ -341,7 +337,9 @@ func (t *Tester) SetUpCategory(name string) { // At the end of the setup phase, t.FinishSetUp() must be called to load all // the files. func (t *Tester) SetUpPackage(pkgpath string, makefileLines ...string) string { + assertf(matches(pkgpath, `^[^/]+/[^/]+$`), "pkgpath %q must have the form \"category/package\"", pkgpath) + distname := path.Base(pkgpath) category := path.Dir(pkgpath) if category == "wip" { // To avoid boilerplate CATEGORIES definitions for wip packages. @@ -354,7 +352,7 @@ func (t *Tester) SetUpPackage(pkgpath string, makefileLines ...string) string { t.CreateFileLines(pkgpath+"/DESCR", "Package description") t.CreateFileLines(pkgpath+"/PLIST", - PlistRcsID, + PlistCvsID, "bin/program") // Because the package Makefile includes this file, the check for the @@ -364,12 +362,12 @@ func (t *Tester) SetUpPackage(pkgpath string, makefileLines ...string) string { // unrelated warnings about the variable order, that check is suppressed // here. t.CreateFileLines(pkgpath+"/suppress-varorder.mk", - MkRcsID) + MkCvsID) // This distinfo file contains dummy hashes since pkglint cannot check the // distfiles hashes anyway. It can only check the hashes for the patches. t.CreateFileLines(pkgpath+"/distinfo", - RcsID, + CvsID, "", "SHA1 (distfile-1.0.tar.gz) = 12341234", "RMD160 (distfile-1.0.tar.gz) = 12341234", @@ -377,9 +375,9 @@ func (t *Tester) SetUpPackage(pkgpath string, makefileLines ...string) string { "Size (distfile-1.0.tar.gz) = 12341234") mlines := []string{ - MkRcsID, + MkCvsID, "", - "DISTNAME=\tdistname-1.0", + "DISTNAME=\t" + distname + "-1.0", "#PKGNAME=\tpackage-1.0", "CATEGORIES=\t" + category, "MASTER_SITES=\t# none", @@ -396,6 +394,8 @@ func (t *Tester) SetUpPackage(pkgpath string, makefileLines ...string) string { line: for _, line := range makefileLines { + assert(!hasSuffix(line, "\\")) // Continuation lines are not yet supported. + if m, prefix := match1(line, `^#?(\w+=)`); m { for i, existingLine := range mlines[:19] { if hasPrefix(strings.TrimPrefix(existingLine, "#"), prefix) { @@ -444,7 +444,7 @@ func (t *Tester) CreateFileLines(relativeFileName string, lines ...string) (file // temporary directory. func (t *Tester) CreateFileDummyPatch(relativeFileName string) { t.CreateFileLines(relativeFileName, - RcsID, + CvsID, "", "Documentation", "", @@ -458,7 +458,8 @@ func (t *Tester) CreateFileDummyPatch(relativeFileName string) { func (t *Tester) CreateFileDummyBuildlink3(relativeFileName string, customLines ...string) { dir := path.Dir(relativeFileName) lower := path.Base(dir) - upper := strings.ToUpper(lower) + // see pkgtools/createbuildlink/files/createbuildlink, "package specific variables" + upper := strings.Replace(strings.ToUpper(lower), "-", "_", -1) width := tabWidth(sprintf("BUILDLINK_API_DEPENDS.%s+=\t", lower)) @@ -472,7 +473,7 @@ func (t *Tester) CreateFileDummyBuildlink3(relativeFileName string, customLines var lines []string lines = append(lines, - MkRcsID, + MkCvsID, "", sprintf("BUILDLINK_TREE+=\t%s", lower), "", @@ -508,9 +509,14 @@ func (t *Tester) File(relativeFileName string) string { // Copy copies a file inside the temporary directory. func (t *Tester) Copy(relativeSrc, relativeDst string) { - data, err := ioutil.ReadFile(t.File(relativeSrc)) + src := t.File(relativeSrc) + dst := t.File(relativeDst) + + data, err := ioutil.ReadFile(src) assertNil(err, "Copy.Read") - err = ioutil.WriteFile(t.File(relativeDst), data, 0777) + err = os.MkdirAll(path.Dir(dst), 0777) + assertNil(err, "Copy.MkdirAll") + err = ioutil.WriteFile(dst, data, 0777) assertNil(err, "Copy.Write") } @@ -536,15 +542,14 @@ func (t *Tester) Chdir(relativeDirName string) { } absDirName := t.File(relativeDirName) - _ = os.MkdirAll(absDirName, 0700) - if err := os.Chdir(absDirName); err != nil { - t.c.Fatalf("Cannot chdir: %s", err) - } + assertNil(os.MkdirAll(absDirName, 0700), "MkDirAll") + assertNil(os.Chdir(absDirName), "Chdir") t.relCwd = relativeDirName G.cwd = absDirName } -// Remove removes the file from the temporary directory. The file must exist. +// Remove removes the file or directory from the temporary directory. +// The file or directory must exist. func (t *Tester) Remove(relativeFileName string) { filename := t.File(relativeFileName) err := os.Remove(filename) @@ -577,13 +582,13 @@ func (t *Tester) Remove(relativeFileName string) { // subdir/module.mk includes subdir/version.mk, the include line is just: // .include "version.mk" func (t *Tester) SetUpHierarchy() ( - include func(filename string, args ...interface{}) MkLines, - get func(string) MkLines) { + include func(filename string, args ...interface{}) *MkLines, + get func(string) *MkLines) { - files := map[string]MkLines{} + files := map[string]*MkLines{} - include = func(filename string, args ...interface{}) MkLines { - var lines []Line + include = func(filename string, args ...interface{}) *MkLines { + var lines []*Line lineno := 1 addLine := func(text string) { @@ -595,8 +600,8 @@ func (t *Tester) SetUpHierarchy() ( switch arg := arg.(type) { case string: addLine(arg) - case MkLines: - text := sprintf(".include %q", relpath(path.Dir(filename), arg.lines.FileName)) + case *MkLines: + text := sprintf(".include %q", relpath(path.Dir(filename), arg.lines.Filename)) addLine(text) lines = append(lines, arg.lines.Lines...) default: @@ -610,7 +615,7 @@ func (t *Tester) SetUpHierarchy() ( return mklines } - get = func(filename string) MkLines { + get = func(filename string) *MkLines { assertf(files[filename] != nil, "MkLines with name %q doesn't exist.", filename) return files[filename] } @@ -636,7 +641,7 @@ func (s *Suite) Test_Tester_SetUpHierarchy(c *check.C) { mklines := get("including.mk") - mklines.ForEach(func(mkline MkLine) { mkline.Notef("Text is: %s", mkline.Text) }) + mklines.ForEach(func(mkline *MkLine) { mkline.Notef("Text is: %s", mkline.Text) }) t.CheckOutputLines( "NOTE: including.mk:1: Text is: .include \"other.mk\"", @@ -690,7 +695,7 @@ func (t *Tester) Main(args ...string) int { } } - return G.Main(argv...) + return G.Main(&t.stdout, &t.stderr, argv) } // Check delegates a check to the check.Check function. @@ -751,7 +756,7 @@ func (t *Tester) ExpectFatalMatches(action func(), expected regex.Pattern) { } // ExpectPanic runs the given action and expects that this action calls -// Pkglint.Assertf or uses some other way to panic. +// assertf or uses some other way to panic. // // Usage: // t.ExpectPanic( @@ -761,6 +766,15 @@ func (t *Tester) ExpectPanic(action func(), expectedMessage string) { t.Check(action, check.Panics, expectedMessage) } +// ExpectAssert runs the given action and expects that this action calls assert. +// +// Usage: +// t.ExpectAssert( +// func() { /* do something that panics */ }) +func (t *Tester) ExpectAssert(action func()) { + t.Check(action, check.Panics, "Pkglint internal error") +} + // NewRawLines creates lines from line numbers and raw text, including newlines. // // Arguments are sequences of either (lineno, orignl) or (lineno, orignl, textnl). @@ -788,14 +802,14 @@ func (t *Tester) NewRawLines(args ...interface{}) []*RawLine { // NewLine creates an in-memory line with the given text. // This line does not correspond to any line in a file. -func (t *Tester) NewLine(filename string, lineno int, text string) Line { +func (t *Tester) NewLine(filename string, lineno int, text string) *Line { textnl := text + "\n" rawLine := RawLine{lineno, textnl, textnl} return NewLine(filename, lineno, text, &rawLine) } // NewMkLine creates an in-memory line in the Makefile format with the given text. -func (t *Tester) NewMkLine(filename string, lineno int, text string) MkLine { +func (t *Tester) NewMkLine(filename string, lineno int, text string) *MkLine { basename := path.Base(filename) assertf( hasSuffix(basename, ".mk") || @@ -815,15 +829,15 @@ func (t *Tester) NewShellLineChecker(text string) *ShellLineChecker { // NewLines returns a list of simple lines that belong together. // // To work with line continuations like in Makefiles, use SetUpFileMkLines. -func (t *Tester) NewLines(filename string, lines ...string) Lines { +func (t *Tester) NewLines(filename string, lines ...string) *Lines { return t.NewLinesAt(filename, 1, lines...) } // NewLinesAt returns a list of simple lines that belong together. // // To work with line continuations like in Makefiles, use SetUpFileMkLines. -func (t *Tester) NewLinesAt(filename string, firstLine int, texts ...string) Lines { - lines := make([]Line, len(texts)) +func (t *Tester) NewLinesAt(filename string, firstLine int, texts ...string) *Lines { + lines := make([]*Line, len(texts)) for i, text := range texts { lines[i] = t.NewLine(filename, i+firstLine, text) } @@ -836,7 +850,7 @@ func (t *Tester) NewLinesAt(filename string, firstLine int, texts ...string) Lin // // No actual file is created for the lines; // see SetUpFileMkLines for loading Makefile fragments with line continuations. -func (t *Tester) NewMkLines(filename string, lines ...string) MkLines { +func (t *Tester) NewMkLines(filename string, lines ...string) *MkLines { basename := path.Base(filename) assertf( hasSuffix(basename, ".mk") || basename == "Makefile" || hasPrefix(basename, "Makefile."), @@ -888,6 +902,25 @@ func (t *Tester) CheckOutputLines(expectedLines ...string) { t.CheckOutput(expectedLines) } +// CheckOutputLinesMatching checks that the lines from the output that match +// the given pattern equal the given lines. +// +// After the comparison, the output buffers are cleared so that later +// calls only check against the newly added output. +// +// See CheckOutputEmpty, CheckOutputLinesIgnoreSpace. +func (t *Tester) CheckOutputLinesMatching(pattern regex.Pattern, expectedLines ...string) { + output := t.Output() + var actualLines []string + actualLines = append(actualLines) + for _, line := range strings.Split(strings.TrimSuffix(output, "\n"), "\n") { + if matches(line, pattern) { + actualLines = append(actualLines, line) + } + } + t.Check(emptyToNil(actualLines), deepEquals, emptyToNil(expectedLines)) +} + // CheckOutputLinesIgnoreSpace checks that the output up to now equals the given lines. // During comparison, each run of whitespace (space, tab, newline) is normalized so that // different line breaks are ignored. This is useful for testing line-wrapped explanations. @@ -982,11 +1015,7 @@ func (t *Tester) CheckOutputMatches(expectedLines ...regex.Pattern) { pattern := `^(?:` + string(expectedLine) + `)$` re, err := regexp.Compile(pattern) - if err != nil { - return false - } - - return re.MatchString(actualLine) + return err == nil && re.MatchString(actualLine) } // If a line matches the corresponding pattern, make them equal in the diff --git a/pkgtools/pkglint/files/cmd/pkglint/main.go b/pkgtools/pkglint/files/cmd/pkglint/main.go index 8b4abedc693..fd612ba3965 100644 --- a/pkgtools/pkglint/files/cmd/pkglint/main.go +++ b/pkgtools/pkglint/files/cmd/pkglint/main.go @@ -8,5 +8,5 @@ import ( var exit = os.Exit func main() { - exit(pkglint.Main()) + exit(pkglint.G.Main(os.Stdout, os.Stderr, os.Args)) } diff --git a/pkgtools/pkglint/files/distinfo.go b/pkgtools/pkglint/files/distinfo.go index 0aca9263a6b..e88e7a8157f 100644 --- a/pkgtools/pkglint/files/distinfo.go +++ b/pkgtools/pkglint/files/distinfo.go @@ -13,12 +13,12 @@ import ( "strings" ) -func CheckLinesDistinfo(pkg *Package, lines Lines) { +func CheckLinesDistinfo(pkg *Package, lines *Lines) { if trace.Tracing { - defer trace.Call1(lines.FileName)() + defer trace.Call1(lines.Filename)() } - filename := lines.FileName + filename := lines.Filename patchdir := "patches" if pkg != nil && dirExists(pkg.File(pkg.Patchdir)) { patchdir = pkg.Patchdir @@ -41,7 +41,7 @@ func CheckLinesDistinfo(pkg *Package, lines Lines) { type distinfoLinesChecker struct { pkg *Package - lines Lines + lines *Lines patchdir string // Relative to pkg distinfoIsCommitted bool @@ -53,7 +53,7 @@ func (ck *distinfoLinesChecker) parse() { lines := ck.lines llex := NewLinesLexer(lines) - if lines.CheckRcsID(0, ``, "") { + if lines.CheckCvsID(0, ``, "") { llex.Skip() } llex.SkipEmptyOrNote() @@ -273,7 +273,7 @@ func (ck *distinfoLinesChecker) checkAlgorithmsDistfile(info distinfoFileInfo) { // that the distfile is the expected one. Now generate the missing hashes // and insert them, in the correct order. - var insertion Line + var insertion *Line var remainingHashes = info.hashes for _, alg := range algorithms { if missing[alg] { @@ -313,7 +313,7 @@ func (ck *distinfoLinesChecker) checkUnrecordedPatches() { for _, file := range patchFiles { patchName := file.Name() if file.Mode().IsRegular() && ck.infos[patchName].isPatch != yes && hasPrefix(patchName, "patch-") { - line := NewLineWhole(ck.lines.FileName) + line := NewLineWhole(ck.lines.Filename) line.Errorf("Patch %q is not recorded. Run %q.", line.PathToFile(ck.pkg.File(ck.patchdir+"/"+patchName)), bmake("makepatchsum")) @@ -381,12 +381,14 @@ func (ck *distinfoLinesChecker) checkUncommittedPatch(info distinfoHash) { } } -func (ck *distinfoLinesChecker) checkPatchSha1(line Line, patchFileName, distinfoSha1Hex string) { - fileSha1Hex, err := computePatchSha1Hex(ck.pkg.File(patchFileName)) - if err != nil { +func (ck *distinfoLinesChecker) checkPatchSha1(line *Line, patchFileName, distinfoSha1Hex string) { + lines := Load(ck.pkg.File(patchFileName), 0) + if lines == nil { line.Errorf("Patch %s does not exist.", patchFileName) return } + + fileSha1Hex := computePatchSha1Hex(lines) if distinfoSha1Hex != fileSha1Hex { fix := line.Autofix() fix.Errorf("SHA1 hash of %s differs (distinfo has %s, patch file has %s).", @@ -408,7 +410,7 @@ type distinfoFileInfo struct { } func (info *distinfoFileInfo) filename() string { return info.hashes[0].filename } -func (info *distinfoFileInfo) line() Line { return info.hashes[0].line } +func (info *distinfoFileInfo) line() *Line { return info.hashes[0].line } func (info *distinfoFileInfo) algorithms() string { var algs []string @@ -419,25 +421,24 @@ func (info *distinfoFileInfo) algorithms() string { } type distinfoHash struct { - line Line + line *Line filename string algorithm string hash string } // Same as in mk/checksum/distinfo.awk:/function patchsum/ -func computePatchSha1Hex(patchFilename string) (string, error) { - patchBytes, err := ioutil.ReadFile(patchFilename) - if err != nil { - return "", err - } +func computePatchSha1Hex(lines *Lines) string { hasher := sha1.New() - skipText := []byte("$" + "NetBSD") - for _, patchLine := range bytes.SplitAfter(patchBytes, []byte("\n")) { - if !bytes.Contains(patchLine, skipText) { - _, _ = hasher.Write(patchLine) + skipText := "$" + "NetBSD" + for _, line := range lines.Lines { + for _, raw := range line.raw { + textnl := raw.orignl + if !contains(textnl, skipText) { + _, _ = hasher.Write([]byte(textnl)) + } } } - return sprintf("%x", hasher.Sum(nil)), nil + return sprintf("%x", hasher.Sum(nil)) } diff --git a/pkgtools/pkglint/files/distinfo_test.go b/pkgtools/pkglint/files/distinfo_test.go index 953969df293..0b896932918 100644 --- a/pkgtools/pkglint/files/distinfo_test.go +++ b/pkgtools/pkglint/files/distinfo_test.go @@ -7,12 +7,12 @@ func (s *Suite) Test_CheckLinesDistinfo__parse_errors(c *check.C) { t.Chdir("category/package") t.CreateFileLines("patches/patch-aa", - RcsID+" line is ignored for computing the SHA1 hash", + CvsID+" line is ignored for computing the SHA1 hash", "patch contents") t.CreateFileLines("patches/patch-ab", "patch contents") lines := t.SetUpFileLines("distinfo", - "should be the RCS ID", + "should be the CVS ID", "should be empty", "MD5 (distfile.tar.gz) = 12345678901234567890123456789012", "SHA1 (distfile.tar.gz) = 1234567890123456789012345678901234567890", @@ -28,7 +28,7 @@ func (s *Suite) Test_CheckLinesDistinfo__parse_errors(c *check.C) { t.CheckOutputLines( "ERROR: distinfo:1: Expected \"$"+"NetBSD$\".", "NOTE: distinfo:1: Empty line expected before this line.", - "ERROR: distinfo:1: Invalid line: should be the RCS ID", + "ERROR: distinfo:1: Invalid line: should be the CVS ID", "ERROR: distinfo:2: Invalid line: should be empty", "ERROR: distinfo:8: Invalid line: Another invalid line", "ERROR: distinfo:3: Expected SHA1, RMD160, SHA512, Size checksums for \"distfile.tar.gz\", got MD5, SHA1.", @@ -41,7 +41,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__nonexistent_distfile_ t.Chdir("category/package") lines := t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "MD5 (patch-5.3.tar.gz) = 12345678901234567890123456789012", "SHA1 (patch-5.3.tar.gz) = 1234567890123456789012345678901234567890") @@ -62,7 +62,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__wrong_distfile_algori t.Chdir("category/package") lines := t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "MD5 (distfile.tar.gz) = 12345678901234567890123456789012", "SHA1 (distfile.tar.gz) = 1234567890123456789012345678901234567890") @@ -86,7 +86,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__ambiguous_distfile(c t.SetUpCommandLine("--explain") t.Chdir("category/package") lines := t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "MD5 (patch-4.2.tar.gz) = 12345678901234567890123456789012") @@ -109,7 +109,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__wrong_patch_algorithm t.Chdir("category/package") t.CreateFileDummyPatch("patches/patch-aa") t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "MD5 (patch-aa) = 12345678901234567890123456789012", "SHA1 (patch-aa) = 1234567890123456789012345678901234567890") @@ -128,7 +128,7 @@ func (s *Suite) Test_distinfoLinesChecker_parse__empty(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("distinfo", - RcsID, + CvsID, "") CheckLinesDistinfo(nil, lines) @@ -150,25 +150,25 @@ func (s *Suite) Test_distinfoLinesChecker_checkGlobalDistfileMismatch(c *check.C t.SetUpPackage("category/package1") t.SetUpPackage("category/package2") t.CreateFileLines("category/package1/distinfo", - RcsID, + CvsID, "", "SHA512 (distfile-1.0.tar.gz) = 1234567811111111", "SHA512 (distfile-1.1.tar.gz) = 1111111111111111", "SHA512 (patch-4.2.tar.gz) = 1234567812345678") t.CreateFileLines("category/package2/distinfo", - RcsID, + CvsID, "", "SHA512 (distfile-1.0.tar.gz) = 1234567822222222", "SHA512 (distfile-1.1.tar.gz) = 1111111111111111", "SHA512 (encoding-error.tar.gz) = 12345678abcdefgh") t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tThis is pkgsrc", "", "SUBDIR+=\tcategory") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tUseful programs", "", @@ -212,7 +212,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__missing_patch_with_di t := s.Init(c) lines := t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ...", "RMD160 (patch-aa) = ...", @@ -234,7 +234,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__existing_patch_with_d t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ...", "RMD160 (patch-aa) = ...", @@ -264,7 +264,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithms__missing_patch_with_wr t.SetUpPackage("category/package") t.SetUpFileLines("category/package/distinfo", - RcsID, + CvsID, "", "RMD160 (patch-aa) = ...") t.FinishSetUp() @@ -286,9 +286,9 @@ func (s *Suite) Test_distinfoLinesChecker_checkUncommittedPatch__bad(c *check.C) t.Chdir("category/package") t.CreateFileDummyPatch("patches/patch-aa") t.CreateFileLines("CVS/Entries", - "/distinfo/...") + "/distinfo//modified//") t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac") t.FinishSetUp() @@ -306,11 +306,11 @@ func (s *Suite) Test_distinfoLinesChecker_checkUncommittedPatch__good(c *check.C t.Chdir("category/package") t.CreateFileDummyPatch("patches/patch-aa") t.CreateFileLines("CVS/Entries", - "/distinfo/...") + "/distinfo//modified//") t.CreateFileLines("patches/CVS/Entries", - "/patch-aa/...") + "/patch-aa//modified//") t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac") t.FinishSetUp() @@ -329,7 +329,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkUnrecordedPatches(c *check.C) { t.CreateFileDummyPatch("patches/patch-aa") t.CreateFileDummyPatch("patches/patch-src-Makefile") t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "SHA1 (distfile.tar.gz) = ...", "RMD160 (distfile.tar.gz) = ...", @@ -358,7 +358,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkPatchSha1__relative_path_in_disti t.CreateFileDummyPatch("devel/patches/patches/patch-aa") t.CreateFileDummyPatch("devel/patches/patches/patch-only-in-patches") t.SetUpFileLines("other/common/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ...", "SHA1 (patch-only-in-distinfo) = ...") @@ -389,7 +389,7 @@ func (s *Suite) Test_CheckLinesDistinfo__distinfo_and_patches_in_separate_direct t.CreateFileDummyPatch("other/common/patches/patch-aa") t.CreateFileDummyPatch("other/common/patches/patch-only-in-patches") t.SetUpFileLines("other/common/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ...", "SHA1 (patch-only-in-distinfo) = ...") @@ -413,7 +413,7 @@ func (s *Suite) Test_CheckLinesDistinfo__manual_patches(c *check.C) { t.Chdir("category/package") t.CreateFileLines("patches/manual-libtool.m4") lines := t.SetUpFileLines("distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ...") @@ -446,7 +446,7 @@ func (s *Suite) Test_CheckLinesDistinfo__missing_php_patches(c *check.C) { t.SetUpCommandLine("-Wall,no-space") t.CreateFileLines("licenses/unknown-license") t.CreateFileLines("lang/php/ext.mk", - MkRcsID, + MkCvsID, "", "PHPEXT_MK= # defined", "PHPPKGSRCDIR= ../../lang/php72", @@ -462,12 +462,12 @@ func (s *Suite) Test_CheckLinesDistinfo__missing_php_patches(c *check.C) { ".endif") t.CreateFileDummyPatch("lang/php72/patches/patch-php72") t.CreateFileLines("lang/php72/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-php72) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac") t.CreateFileLines("archivers/php-bz2/Makefile", - MkRcsID, + MkCvsID, "", "USE_PHP_EXT_PATCHES= yes", "", @@ -478,7 +478,7 @@ func (s *Suite) Test_CheckLinesDistinfo__missing_php_patches(c *check.C) { G.Check(t.File("archivers/php-bz2")) t.CreateFileLines("archivers/php-zlib/Makefile", - MkRcsID, + MkCvsID, "", ".include \"../../lang/php/ext.mk\"", ".include \"../../mk/bsd.pkg.mk\"") @@ -510,7 +510,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__add_missing_h t.SetUpCommandLine("-Wall", "--explain") t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a", "Size (package-1.0.txt) = 13 bytes", @@ -578,7 +578,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__add_missing_h t.SetUpCommandLine("-Wall", "--explain") t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a", "Size (package-1.0.txt) = 13 bytes", @@ -622,7 +622,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__wrong_distfil t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "RMD160 (package-1.0.txt) = 1234wrongHash1234") t.CreateFileLines("distfiles/package-1.0.txt", @@ -645,7 +645,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__no_usual_algo t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "MD5 (package-1.0.txt) = 1234wrongHash1234") t.CreateFileLines("distfiles/package-1.0.txt", @@ -665,7 +665,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__top_algorithm t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "SHA512 (package-1.0.txt) = f65f341b35981fda842b09b2c8af9bcdb7602a4c2e6fa1f7"+ "d41f0974d3e3122f268fc79d5a4af66358f5133885cd1c165c916f80ab25e5d8d95db46f803c782c", @@ -689,7 +689,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__bottom_algori t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "SHA1 (package-1.0.txt) = cd50d19784897085a8d0e3e413f8612b097c03f1", "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a") @@ -724,7 +724,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__algorithms_in t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a", "SHA1 (package-1.0.txt) = cd50d19784897085a8d0e3e413f8612b097c03f1", @@ -750,7 +750,7 @@ func (s *Suite) Test_distinfoLinesChecker_checkAlgorithmsDistfile__some_algorith t.SetUpPackage("category/package") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "RMD160 (package-1.0.txt) = 1a88147a0344137404c63f3b695366eab869a98a", "Size (package-1.0.txt) = 13 bytes", diff --git a/pkgtools/pkglint/files/files.go b/pkgtools/pkglint/files/files.go index 09efcefa92c..76745360d82 100644 --- a/pkgtools/pkglint/files/files.go +++ b/pkgtools/pkglint/files/files.go @@ -16,7 +16,7 @@ const ( LogErrors // ) -func Load(filename string, options LoadOptions) Lines { +func Load(filename string, options LoadOptions) *Lines { if fromCache := G.fileCache.Get(filename, options); fromCache != nil { return fromCache } @@ -54,7 +54,7 @@ func Load(filename string, options LoadOptions) Lines { return result } -func LoadMk(filename string, options LoadOptions) MkLines { +func LoadMk(filename string, options LoadOptions) *MkLines { lines := Load(filename, options|Makefile) if lines == nil { return nil @@ -62,7 +62,7 @@ func LoadMk(filename string, options LoadOptions) MkLines { return NewMkLines(lines) } -func nextLogicalLine(filename string, rawLines []*RawLine, index int) (Line, int) { +func nextLogicalLine(filename string, rawLines []*RawLine, index int) (*Line, int) { { // Handle the common case efficiently rawLine := rawLines[index] textnl := rawLine.textnl @@ -137,7 +137,7 @@ func matchContinuationLine(textnl string) (leadingWhitespace, text, trailingWhit return } -func convertToLogicalLines(filename string, rawText string, joinBackslashLines bool) Lines { +func convertToLogicalLines(filename string, rawText string, joinBackslashLines bool) *Lines { var rawLines []*RawLine for lineno, rawLine := range strings.SplitAfter(rawText, "\n") { if rawLine != "" { @@ -145,7 +145,7 @@ func convertToLogicalLines(filename string, rawText string, joinBackslashLines b } } - var loglines []Line + var loglines []*Line if joinBackslashLines { for lineno := 0; lineno < len(rawLines); { line, nextLineno := nextLogicalLine(filename, rawLines, lineno) diff --git a/pkgtools/pkglint/files/files_test.go b/pkgtools/pkglint/files/files_test.go index 049bd34aab2..f010dd17022 100644 --- a/pkgtools/pkglint/files/files_test.go +++ b/pkgtools/pkglint/files/files_test.go @@ -173,10 +173,40 @@ func (s *Suite) Test_matchContinuationLine(c *check.C) { c.Check(continuation, equals, "\\") } -func (s *Suite) Test_Load__errors(c *check.C) { +func (s *Suite) Test_Load(c *check.C) { t := s.Init(c) - t.CreateFileLines("empty") + nonexistent := t.File("nonexistent") + empty := t.CreateFileLines("empty") + oneLiner := t.CreateFileLines("one-liner", + "hello, world") + + t.Check(Load(nonexistent, 0), check.IsNil) + t.Check(Load(empty, 0).Lines, check.HasLen, 0) + t.Check(Load(oneLiner, 0).Lines[0].Text, equals, "hello, world") + + t.CheckOutputEmpty() + + t.Check(Load(nonexistent, LogErrors), check.IsNil) + t.Check(Load(empty, LogErrors).Lines, check.HasLen, 0) + t.Check(Load(oneLiner, LogErrors).Lines[0].Text, equals, "hello, world") + + t.CheckOutputLines( + "ERROR: ~/nonexistent: Cannot be read.") + + t.Check(Load(nonexistent, NotEmpty), check.IsNil) + t.Check(Load(empty, NotEmpty), check.IsNil) + t.Check(Load(oneLiner, NotEmpty).Lines[0].Text, equals, "hello, world") + + t.CheckOutputEmpty() + + t.Check(Load(nonexistent, NotEmpty|LogErrors), check.IsNil) + t.Check(Load(empty, NotEmpty|LogErrors), check.IsNil) + t.Check(Load(oneLiner, NotEmpty|LogErrors).Lines[0].Text, equals, "hello, world") + + t.CheckOutputLines( + "ERROR: ~/nonexistent: Cannot be read.", + "ERROR: ~/empty: Must not be empty.") t.ExpectFatal( func() { Load(t.File("does-not-exist"), MustSucceed) }, diff --git a/pkgtools/pkglint/files/fuzzer_test.go b/pkgtools/pkglint/files/fuzzer_test.go index df1284c2a16..d35bd2b5e0b 100644 --- a/pkgtools/pkglint/files/fuzzer_test.go +++ b/pkgtools/pkglint/files/fuzzer_test.go @@ -57,7 +57,7 @@ func (f *Fuzzer) addChar(r rune, weight int) { } func (f *Fuzzer) Generate(length int) string { - rs := make([]rune, length, length) + rs := make([]rune, length) for i := 0; i < length; i++ { rs[i] = f.randomChar() } diff --git a/pkgtools/pkglint/files/getopt/getopt_test.go b/pkgtools/pkglint/files/getopt/getopt_test.go index c77f90c3ed5..667b95b0c70 100644 --- a/pkgtools/pkglint/files/getopt/getopt_test.go +++ b/pkgtools/pkglint/files/getopt/getopt_test.go @@ -169,13 +169,17 @@ func (s *Suite) Test_Options_Parse__string_list(c *check.C) { c.Check(includes, check.DeepEquals, []string{"included1", "included2", "included3", "included4"}) c.Check(excludes, check.DeepEquals, []string{"excluded1", "excluded2", "excluded3", "excluded4"}) - args, err = opts.Parse([]string{"progname", "-i"}) + _, err = opts.Parse([]string{"progname", "-i"}) - c.Check(err.Error(), check.Equals, "progname: option requires an argument: -i") + if c.Check(err, check.NotNil) { + c.Check(err.Error(), check.Equals, "progname: option requires an argument: -i") + } - args, err = opts.Parse([]string{"progname", "--include"}) + _, err = opts.Parse([]string{"progname", "--include"}) - c.Check(err.Error(), check.Equals, "progname: option requires an argument: --include") + if c.Check(err, check.NotNil) { + c.Check(err.Error(), check.Equals, "progname: option requires an argument: --include") + } } func (s *Suite) Test_Options_Parse__long_flags(c *check.C) { diff --git a/pkgtools/pkglint/files/intqa/testnames.go b/pkgtools/pkglint/files/intqa/testnames.go index 5ed0683ba3e..02f604739cc 100644 --- a/pkgtools/pkglint/files/intqa/testnames.go +++ b/pkgtools/pkglint/files/intqa/testnames.go @@ -2,13 +2,11 @@ package intqa import ( - "bytes" "fmt" "go/ast" "go/parser" "go/token" "gopkg.in/check.v1" - "io/ioutil" "os" "path/filepath" "sort" @@ -122,35 +120,6 @@ func (ck *TestNameChecker) addElement(elements *[]*testeeElement, decl ast.Decl, } } -// fixTabs replaces literal tabs with proper escape sequences, -// except for the indentation tabs. -// -// It doesn't really belong to this type (TestNameChecker) but -// merely uses its infrastructure. -func (ck *TestNameChecker) fixTabs(filename string) { - if ck.isIgnored(filename) { - return - } - - readBytes, err := ioutil.ReadFile(filename) - ck.c.Assert(err, check.IsNil) - - var fixed bytes.Buffer - for _, line := range strings.SplitAfter(string(readBytes), "\n") { - rest := strings.TrimLeft(line, "\t") - fixed.WriteString(line[:len(line)-len(rest)]) - fixed.WriteString(strings.Replace(rest, "\t", "\\t", -1)) - } - - if fixed.String() != string(readBytes) { - tmpName := filename + ".tmp" - err = ioutil.WriteFile(tmpName, fixed.Bytes(), 0666) - ck.c.Assert(err, check.IsNil) - err = os.Rename(tmpName, filename) - ck.c.Assert(err, check.IsNil) - } -} - // loadAllElements returns all type, function and method names // from the current package, in the form FunctionName or // TypeName.MethodName (omitting the * from the type name). diff --git a/pkgtools/pkglint/files/licenses.go b/pkgtools/pkglint/files/licenses.go index bdf1a7c085c..944ad42d0fa 100644 --- a/pkgtools/pkglint/files/licenses.go +++ b/pkgtools/pkglint/files/licenses.go @@ -3,8 +3,8 @@ package pkglint import "netbsd.org/pkglint/licenses" type LicenseChecker struct { - MkLines MkLines - MkLine MkLine + MkLines *MkLines + MkLine *MkLine } func (lc *LicenseChecker) Check(value string, op MkOperator) { diff --git a/pkgtools/pkglint/files/line.go b/pkgtools/pkglint/files/line.go index cc2c239f089..ed9050ab574 100644 --- a/pkgtools/pkglint/files/line.go +++ b/pkgtools/pkglint/files/line.go @@ -14,6 +14,7 @@ package pkglint // used in the --autofix mode. import ( + "netbsd.org/pkglint/regex" "path" "strconv" ) @@ -60,11 +61,7 @@ func (loc *Location) Linenos() string { } // Line represents a line of text from a file. -// It aliases a pointer type to reduces the number of *Line occurrences in the code. -// Using a type alias is more efficient than an interface type, I guess. -type Line = *LineImpl - -type LineImpl struct { +type Line struct { // TODO: Consider storing pointers to the Filename and Basename instead of strings to save memory. // But first find out where and why pkglint needs so much memory (200 MB for a full recursive run over pkgsrc + wip). Location @@ -82,33 +79,33 @@ type LineImpl struct { // XXX: Filename and Basename could be replaced with a pointer to a Lines object. } -func NewLine(filename string, lineno int, text string, rawLine *RawLine) Line { - assertf(rawLine != nil, "use NewLineMulti for creating a Line with no RawLine attached to it") +func NewLine(filename string, lineno int, text string, rawLine *RawLine) *Line { + assert(rawLine != nil) // Use NewLineMulti for creating a Line with no RawLine attached to it. return NewLineMulti(filename, lineno, lineno, text, []*RawLine{rawLine}) } // NewLineMulti is for logical Makefile lines that end with backslash. -func NewLineMulti(filename string, firstLine, lastLine int, text string, rawLines []*RawLine) Line { - return &LineImpl{NewLocation(filename, firstLine, lastLine), path.Base(filename), text, rawLines, nil, Once{}} +func NewLineMulti(filename string, firstLine, lastLine int, text string, rawLines []*RawLine) *Line { + return &Line{NewLocation(filename, firstLine, lastLine), path.Base(filename), text, rawLines, nil, Once{}} } // NewLineEOF creates a dummy line for logging, with the "line number" EOF. -func NewLineEOF(filename string) Line { +func NewLineEOF(filename string) *Line { return NewLineMulti(filename, -1, 0, "", nil) } // NewLineWhole creates a dummy line for logging messages that affect a file as a whole. -func NewLineWhole(filename string) Line { +func NewLineWhole(filename string) *Line { return NewLineMulti(filename, 0, 0, "", nil) } // RefTo returns a reference to another line, // which can be in the same file or in a different file. -func (line *LineImpl) RefTo(other Line) string { +func (line *Line) RefTo(other *Line) string { return line.RefToLocation(other.Location) } -func (line *LineImpl) RefToLocation(other Location) string { +func (line *Line) RefToLocation(other Location) string { if line.Filename != other.Filename { return line.PathToFile(other.Filename) + ":" + other.Linenos() } @@ -118,15 +115,20 @@ func (line *LineImpl) RefToLocation(other Location) string { // PathToFile returns the relative path from this line to the given file path. // This is typically used for arguments in diagnostics, which should always be // relative to the line with which the diagnostic is associated. -func (line *LineImpl) PathToFile(filePath string) string { +func (line *Line) PathToFile(filePath string) string { return relpath(path.Dir(line.Filename), filePath) } -func (line *LineImpl) IsMultiline() bool { +func (line *Line) IsMultiline() bool { return line.firstLine > 0 && line.firstLine != line.lastLine } -func (line *LineImpl) showSource(out *SeparatorWriter) { +func (line *Line) IsCvsID(prefixRe regex.Pattern) (found bool, expanded bool) { + m, exp := match1(line.Text, `^`+prefixRe+`\$`+`NetBSD(:[^\$]+)?\$$`) + return m, exp != "" +} + +func (line *Line) showSource(out *SeparatorWriter) { if !G.Logger.Opts.ShowSource { return } @@ -172,28 +174,28 @@ func (line *LineImpl) showSource(out *SeparatorWriter) { } } -func (line *LineImpl) Fatalf(format string, args ...interface{}) { +func (line *Line) Fatalf(format string, args ...interface{}) { if trace.Tracing { trace.Stepf("Fatalf: %q, %v", format, args) } G.Logger.Diag(line, Fatal, format, args...) } -func (line *LineImpl) Errorf(format string, args ...interface{}) { +func (line *Line) Errorf(format string, args ...interface{}) { G.Logger.Diag(line, Error, format, args...) } -func (line *LineImpl) Warnf(format string, args ...interface{}) { +func (line *Line) Warnf(format string, args ...interface{}) { G.Logger.Diag(line, Warn, format, args...) } -func (line *LineImpl) Notef(format string, args ...interface{}) { +func (line *Line) Notef(format string, args ...interface{}) { G.Logger.Diag(line, Note, format, args...) } -func (line *LineImpl) Explain(explanation ...string) { G.Logger.Explain(explanation...) } +func (line *Line) Explain(explanation ...string) { G.Logger.Explain(explanation...) } -func (line *LineImpl) String() string { +func (line *Line) String() string { return sprintf("%s:%s: %s", line.Filename, line.Linenos(), line.Text) } @@ -220,7 +222,7 @@ func (line *LineImpl) String() string { // fix.Custom(func(showAutofix, autofix bool) {}) // // fix.Apply() -func (line *LineImpl) Autofix() *Autofix { +func (line *Line) Autofix() *Autofix { if line.autofix == nil { line.autofix = NewAutofix(line) } diff --git a/pkgtools/pkglint/files/line_test.go b/pkgtools/pkglint/files/line_test.go index d0c74fc1152..7a54a6b7b83 100644 --- a/pkgtools/pkglint/files/line_test.go +++ b/pkgtools/pkglint/files/line_test.go @@ -15,9 +15,7 @@ func (s *Suite) Test_RawLine_String(c *check.C) { func (s *Suite) Test_NewLine__assertion(c *check.C) { t := s.Init(c) - t.ExpectPanic( - func() { NewLine("filename", 123, "text", nil) }, - "Pkglint internal error: use NewLineMulti for creating a Line with no RawLine attached to it") + t.ExpectAssert(func() { NewLine("filename", 123, "text", nil) }) } func (s *Suite) Test_Line_IsMultiline(c *check.C) { diff --git a/pkgtools/pkglint/files/linechecker.go b/pkgtools/pkglint/files/linechecker.go index 8fc3b18d6c5..525f26ae079 100644 --- a/pkgtools/pkglint/files/linechecker.go +++ b/pkgtools/pkglint/files/linechecker.go @@ -6,7 +6,7 @@ import ( ) type LineChecker struct { - line Line + line *Line } func (ck LineChecker) CheckLength(maxLength int) { diff --git a/pkgtools/pkglint/files/linelexer.go b/pkgtools/pkglint/files/linelexer.go index b89b5da2131..f74076ed0b6 100644 --- a/pkgtools/pkglint/files/linelexer.go +++ b/pkgtools/pkglint/files/linelexer.go @@ -4,24 +4,24 @@ import "netbsd.org/pkglint/regex" // LinesLexer records the state when checking a list of lines from top to bottom. type LinesLexer struct { - lines Lines + lines *Lines index int } -func NewLinesLexer(lines Lines) *LinesLexer { +func NewLinesLexer(lines *Lines) *LinesLexer { return &LinesLexer{lines, 0} } // CurrentLine returns the line that the lexer is currently looking at. // If it is at the end of file, the line number of the line is EOF. -func (llex *LinesLexer) CurrentLine() Line { +func (llex *LinesLexer) CurrentLine() *Line { if llex.index < llex.lines.Len() { return llex.lines.Lines[llex.index] } - return NewLineEOF(llex.lines.FileName) + return NewLineEOF(llex.lines.Filename) } -func (llex *LinesLexer) PreviousLine() Line { +func (llex *LinesLexer) PreviousLine() *Line { return llex.lines.Lines[llex.index-1] } @@ -115,23 +115,23 @@ func (llex *LinesLexer) SkipContainsOrWarn(text string) bool { // MkLinesLexer records the state when checking a list of Makefile lines from top to bottom. type MkLinesLexer struct { - mklines MkLines + mklines *MkLines LinesLexer } -func NewMkLinesLexer(mklines MkLines) *MkLinesLexer { +func NewMkLinesLexer(mklines *MkLines) *MkLinesLexer { return &MkLinesLexer{mklines, *NewLinesLexer(mklines.lines)} } -func (mlex *MkLinesLexer) PreviousMkLine() MkLine { +func (mlex *MkLinesLexer) PreviousMkLine() *MkLine { return mlex.mklines.mklines[mlex.index-1] } -func (mlex *MkLinesLexer) CurrentMkLine() MkLine { +func (mlex *MkLinesLexer) CurrentMkLine() *MkLine { return mlex.mklines.mklines[mlex.index] } -func (mlex *MkLinesLexer) SkipWhile(pred func(mkline MkLine) bool) { +func (mlex *MkLinesLexer) SkipWhile(pred func(mkline *MkLine) bool) { if trace.Tracing { defer trace.Call(mlex.CurrentMkLine().Text)() } @@ -141,7 +141,7 @@ func (mlex *MkLinesLexer) SkipWhile(pred func(mkline MkLine) bool) { } } -func (mlex *MkLinesLexer) SkipIf(pred func(mkline MkLine) bool) bool { +func (mlex *MkLinesLexer) SkipIf(pred func(mkline *MkLine) bool) bool { if !mlex.EOF() && pred(mlex.CurrentMkLine()) { mlex.Skip() return true diff --git a/pkgtools/pkglint/files/lines.go b/pkgtools/pkglint/files/lines.go index 0d4b9914497..77aef21e675 100644 --- a/pkgtools/pkglint/files/lines.go +++ b/pkgtools/pkglint/files/lines.go @@ -5,46 +5,44 @@ import ( "path" ) -type Lines = *LinesImpl - -type LinesImpl struct { - FileName string +type Lines struct { + Filename string BaseName string - Lines []Line + Lines []*Line } -func NewLines(filename string, lines []Line) Lines { - return &LinesImpl{filename, path.Base(filename), lines} +func NewLines(filename string, lines []*Line) *Lines { + return &Lines{filename, path.Base(filename), lines} } -func (ls *LinesImpl) Len() int { return len(ls.Lines) } +func (ls *Lines) Len() int { return len(ls.Lines) } -func (ls *LinesImpl) LastLine() Line { return ls.Lines[ls.Len()-1] } +func (ls *Lines) LastLine() *Line { return ls.Lines[ls.Len()-1] } -func (ls *LinesImpl) EOFLine() Line { return NewLineMulti(ls.FileName, -1, -1, "", nil) } +func (ls *Lines) EOFLine() *Line { return NewLineMulti(ls.Filename, -1, -1, "", nil) } -func (ls *LinesImpl) Errorf(format string, args ...interface{}) { - NewLineWhole(ls.FileName).Errorf(format, args...) +func (ls *Lines) Errorf(format string, args ...interface{}) { + NewLineWhole(ls.Filename).Errorf(format, args...) } -func (ls *LinesImpl) Warnf(format string, args ...interface{}) { - NewLineWhole(ls.FileName).Warnf(format, args...) +func (ls *Lines) Warnf(format string, args ...interface{}) { + NewLineWhole(ls.Filename).Warnf(format, args...) } -func (ls *LinesImpl) SaveAutofixChanges() bool { +func (ls *Lines) SaveAutofixChanges() bool { return SaveAutofixChanges(ls) } -// CheckRcsID returns true if the expected RCS Id was found. -func (ls *LinesImpl) CheckRcsID(index int, prefixRe regex.Pattern, suggestedPrefix string) bool { +// CheckCvsID returns true if the expected CVS Id was found. +func (ls *Lines) CheckCvsID(index int, prefixRe regex.Pattern, suggestedPrefix string) bool { if trace.Tracing { defer trace.Call(prefixRe, suggestedPrefix)() } line := ls.Lines[index] - if m, expanded := match1(line.Text, `^`+prefixRe+`\$`+`NetBSD(:[^\$]+)?\$$`); m { + if m, expanded := line.IsCvsID(prefixRe); m { - if G.Testing && G.Wip && expanded != "" { + if G.Testing && G.Wip && expanded { fix := line.Autofix() fix.Notef("Expected exactly %q.", suggestedPrefix+"$"+"NetBSD$") fix.Explain( diff --git a/pkgtools/pkglint/files/lines_test.go b/pkgtools/pkglint/files/lines_test.go index 2e758114cb1..3d105d74cd9 100644 --- a/pkgtools/pkglint/files/lines_test.go +++ b/pkgtools/pkglint/files/lines_test.go @@ -2,7 +2,7 @@ package pkglint import "gopkg.in/check.v1" -func (s *Suite) Test_Lines_CheckRcsID(c *check.C) { +func (s *Suite) Test_Lines_CheckCvsID(c *check.C) { t := s.Init(c) lines := t.NewLines("filename", @@ -13,7 +13,7 @@ func (s *Suite) Test_Lines_CheckRcsID(c *check.C) { "$"+"FreeBSD$") for i := range lines.Lines { - lines.CheckRcsID(i, ``, "") + lines.CheckCvsID(i, ``, "") } t.CheckOutputLines( @@ -27,7 +27,7 @@ func (s *Suite) Test_Lines_CheckRcsID(c *check.C) { // "$NetBSD:" is a copy-and-paste mistake rather than an intentional // documentation of the file's history. Therefore, pkgsrc-wip files should // only use the unexpanded form. -func (s *Suite) Test_Lines_CheckRcsID__wip(c *check.C) { +func (s *Suite) Test_Lines_CheckCvsID__wip(c *check.C) { t := s.Init(c) t.SetUpPkgsrc() diff --git a/pkgtools/pkglint/files/logging.go b/pkgtools/pkglint/files/logging.go index 0e727198541..364aae3df85 100644 --- a/pkgtools/pkglint/files/logging.go +++ b/pkgtools/pkglint/files/logging.go @@ -153,7 +153,7 @@ func (l *Logger) shallBeLogged(format string) bool { // and duplicates are suppressed unless the --log-verbose command line option is given. // // See Logf for logging arbitrary messages. -func (l *Logger) Diag(line Line, level *LogLevel, format string, args ...interface{}) { +func (l *Logger) Diag(line *Line, level *LogLevel, format string, args ...interface{}) { if l.Opts.ShowAutofix || l.Opts.Autofix { // In these two cases, the only interesting diagnostics are those that can // be fixed automatically. These are logged by Autofix.Apply. diff --git a/pkgtools/pkglint/files/logging_test.go b/pkgtools/pkglint/files/logging_test.go index f730fef8bdf..737f5936f5d 100644 --- a/pkgtools/pkglint/files/logging_test.go +++ b/pkgtools/pkglint/files/logging_test.go @@ -775,7 +775,7 @@ func (s *Suite) Test_Logger_Diag__source_duplicates(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("category/dependency/patches/patch-aa", - RcsID, + CvsID, "", "--- old file", "+++ new file", diff --git a/pkgtools/pkglint/files/mkline.go b/pkgtools/pkglint/files/mkline.go index c8005c4eaaf..a9f10cc5f38 100644 --- a/pkgtools/pkglint/files/mkline.go +++ b/pkgtools/pkglint/files/mkline.go @@ -12,14 +12,17 @@ import ( // There are several types of lines. // The most common types in pkgsrc are variable assignments, // shell commands and directives like .if and .for. -type MkLine = *MkLineImpl +type MkLine struct { + *Line -type MkLineImpl struct { - Line - data interface{} // One of the following mkLine* types + // One of the following mkLine* types. + // + // For the larger of these types, a pointer is used instead of a direct + // struct because of https://github.com/golang/go/issues/28045. + data interface{} } -type mkLineAssign = *mkLineAssignImpl // See https://github.com/golang/go/issues/28045 -type mkLineAssignImpl struct { + +type mkLineAssign struct { commented bool // Whether the whole variable assignment is commented out varname string // e.g. "HOMEPAGE", "SUBST_SED.perl" varcanon string // e.g. "HOMEPAGE", "SUBST_SED.*" @@ -34,29 +37,33 @@ type mkLineAssignImpl struct { spaceAfterValue string comment string } + type mkLineShell struct { command string } -type mkLineComment struct{} // See mkLineAssignImpl.commented for another type of comment line + +type mkLineComment struct{} // See mkLineAssign.commented for another type of comment line + type mkLineEmpty struct{} -type mkLineDirective = *mkLineDirectiveImpl // See https://github.com/golang/go/issues/28045 -type mkLineDirectiveImpl struct { + +type mkLineDirective struct { indent string // the space between the leading "." and the directive directive string // "if", "else", "for", etc. args string comment string // mainly interesting for .endif and .endfor - elseLine MkLine // for .if (filled in later) - cond MkCond // for .if and .elif (filled in on first access) + elseLine *MkLine // for .if (filled in later) + cond *MkCond // for .if and .elif (filled in on first access) fields []string // the arguments for the .for loop (filled in on first access) } -type mkLineInclude = *mkLineIncludeImpl // See https://github.com/golang/go/issues/28045 -type mkLineIncludeImpl struct { + +type mkLineInclude struct { mustExist bool // for .sinclude, nonexistent files are ignored sys bool // whether the include uses <file.mk> (very rare) instead of "file.mk" indent string // the space between the leading "." and the directive includedFile string // the text between the <brackets> or "quotes" conditionalVars []string // variables on which this inclusion depends (filled in later, as needed) } + type mkLineDependency struct { targets string sources string @@ -68,7 +75,7 @@ type MkLineParser struct{} // it is: variable assignment, include, comment, etc. // // See devel/bmake/parse.c:/^Parse_File/ -func (p MkLineParser) Parse(line Line) *MkLineImpl { +func (p MkLineParser) Parse(line *Line) *MkLine { text := line.Text // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing. @@ -111,10 +118,10 @@ func (p MkLineParser) Parse(line Line) *MkLineImpl { // The %q is deliberate here since it shows possible strange characters. line.Errorf("Unknown Makefile line format: %q.", text) - return &MkLineImpl{line, nil} + return &MkLine{line, nil} } -func (p MkLineParser) parseVarassign(line Line, data mkLineSplitResult) MkLine { +func (p MkLineParser) parseVarassign(line *Line, data mkLineSplitResult) *MkLine { m, a := p.MatchVarassign(line, line.Text, data) if !m { return nil @@ -147,46 +154,46 @@ func (p MkLineParser) parseVarassign(line Line, data mkLineSplitResult) MkLine { "it should be preceded by a space in order to make it more visible.") } - return &MkLineImpl{line, a} + return &MkLine{line, a} } -func (p MkLineParser) parseShellcmd(line Line) MkLine { - return &MkLineImpl{line, mkLineShell{line.Text[1:]}} +func (p MkLineParser) parseShellcmd(line *Line) *MkLine { + return &MkLine{line, mkLineShell{line.Text[1:]}} } -func (p MkLineParser) parseCommentOrEmpty(line Line) MkLine { +func (p MkLineParser) parseCommentOrEmpty(line *Line) *MkLine { trimmedText := trimHspace(line.Text) if strings.HasPrefix(trimmedText, "#") { - return &MkLineImpl{line, mkLineComment{}} + return &MkLine{line, mkLineComment{}} } if trimmedText == "" { - return &MkLineImpl{line, mkLineEmpty{}} + return &MkLine{line, mkLineEmpty{}} } return nil } -func (p MkLineParser) parseInclude(line Line) MkLine { +func (p MkLineParser) parseInclude(line *Line) *MkLine { m, indent, directive, includedFile := MatchMkInclude(line.Text) if !m { return nil } - return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", false, indent, includedFile, nil}} + return &MkLine{line, &mkLineInclude{directive == "include", false, indent, includedFile, nil}} } -func (p MkLineParser) parseSysinclude(line Line) MkLine { +func (p MkLineParser) parseSysinclude(line *Line) *MkLine { m, indent, directive, includedFile := match3(line.Text, `^\.([\t ]*)(s?include)[\t ]+<([^>]+)>[\t ]*(?:#.*)?$`) if !m { return nil } - return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", true, indent, includedFile, nil}} + return &MkLine{line, &mkLineInclude{directive == "include", true, indent, includedFile, nil}} } -func (p MkLineParser) parseDependency(line Line) MkLine { +func (p MkLineParser) parseDependency(line *Line) *MkLine { // XXX: Replace this regular expression with proper parsing. // There might be a ${VAR:M*.c} in these variables, which the below regular expression cannot handle. m, targets, whitespace, sources := match3(line.Text, `^([^\t :]+(?:[\t ]*[^\t :]+)*)([\t ]*):[\t ]*([^#]*?)(?:[\t ]*#.*)?$`) @@ -197,35 +204,37 @@ func (p MkLineParser) parseDependency(line Line) MkLine { if whitespace != "" { line.Notef("Space before colon in dependency line.") } - return &MkLineImpl{line, mkLineDependency{targets, sources}} + return &MkLine{line, mkLineDependency{targets, sources}} } -func (p MkLineParser) parseMergeConflict(line Line) MkLine { +func (p MkLineParser) parseMergeConflict(line *Line) *MkLine { if !matches(line.Text, `^(<<<<<<<|=======|>>>>>>>)`) { return nil } - return &MkLineImpl{line, nil} + return &MkLine{line, nil} } // String returns the filename and line numbers. -func (mkline *MkLineImpl) String() string { +func (mkline *MkLine) String() string { return sprintf("%s:%s", mkline.Filename, mkline.Linenos()) } // IsVarassign returns true for variable assignments of the form VAR=value. // // See IsCommentedVarassign. -func (mkline *MkLineImpl) IsVarassign() bool { - data, ok := mkline.data.(mkLineAssign) +func (mkline *MkLine) IsVarassign() bool { + // See https://github.com/golang/go/issues/28045 for the reason why + // a pointer type is used here instead of a direct struct. + data, ok := mkline.data.(*mkLineAssign) return ok && !data.commented } // IsCommentedVarassign returns true for commented-out variable assignments. // In most cases these are treated as ordinary comments, but in some others // they are treated like variable assignments, just inactive ones. -func (mkline *MkLineImpl) IsCommentedVarassign() bool { - data, ok := mkline.data.(mkLineAssign) +func (mkline *MkLine) IsCommentedVarassign() bool { + data, ok := mkline.data.(*mkLineAssign) return ok && data.commented } @@ -234,18 +243,18 @@ func (mkline *MkLineImpl) IsCommentedVarassign() bool { // // pre-configure: # IsDependency // ${ECHO} # IsShellCommand -func (mkline *MkLineImpl) IsShellCommand() bool { +func (mkline *MkLine) IsShellCommand() bool { _, ok := mkline.data.(mkLineShell) return ok } // IsComment returns true for lines that consist entirely of a comment. -func (mkline *MkLineImpl) IsComment() bool { +func (mkline *MkLine) IsComment() bool { _, ok := mkline.data.(mkLineComment) return ok || mkline.IsCommentedVarassign() } -func (mkline *MkLineImpl) IsEmpty() bool { +func (mkline *MkLine) IsEmpty() bool { _, ok := mkline.data.(mkLineEmpty) return ok } @@ -253,29 +262,29 @@ func (mkline *MkLineImpl) IsEmpty() bool { // IsDirective returns true for conditionals (.if/.elif/.else/.if) or loops (.for/.endfor). // // See IsInclude. -func (mkline *MkLineImpl) IsDirective() bool { - _, ok := mkline.data.(mkLineDirective) +func (mkline *MkLine) IsDirective() bool { + _, ok := mkline.data.(*mkLineDirective) return ok } // IsInclude returns true for lines like: .include "other.mk" // // See IsSysinclude for lines like: .include <sys.mk> -func (mkline *MkLineImpl) IsInclude() bool { - incl, ok := mkline.data.(mkLineInclude) +func (mkline *MkLine) IsInclude() bool { + incl, ok := mkline.data.(*mkLineInclude) return ok && !incl.sys } // IsSysinclude returns true for lines like: .include <sys.mk> // // See IsInclude for lines like: .include "other.mk" -func (mkline *MkLineImpl) IsSysinclude() bool { - incl, ok := mkline.data.(mkLineInclude) +func (mkline *MkLine) IsSysinclude() bool { + incl, ok := mkline.data.(*mkLineInclude) return ok && incl.sys } // IsDependency returns true for dependency lines like "target: source". -func (mkline *MkLineImpl) IsDependency() bool { +func (mkline *MkLine) IsDependency() bool { _, ok := mkline.data.(mkLineDependency) return ok } @@ -285,30 +294,30 @@ func (mkline *MkLineImpl) IsDependency() bool { // // Example: // VARNAME.${param}?= value # Varname is "VARNAME.${param}" -func (mkline *MkLineImpl) Varname() string { return mkline.data.(mkLineAssign).varname } +func (mkline *MkLine) Varname() string { return mkline.data.(*mkLineAssign).varname } // Varcanon applies to variable assignments and returns the canonicalized variable name for parameterized variables. // Examples: // HOMEPAGE => "HOMEPAGE" // SUBST_SED.anything => "SUBST_SED.*" // SUBST_SED.${param} => "SUBST_SED.*" -func (mkline *MkLineImpl) Varcanon() string { return mkline.data.(mkLineAssign).varcanon } +func (mkline *MkLine) Varcanon() string { return mkline.data.(*mkLineAssign).varcanon } // Varparam applies to variable assignments and returns the parameter for parameterized variables. // Examples: // HOMEPAGE => "" // SUBST_SED.anything => "anything" // SUBST_SED.${param} => "${param}" -func (mkline *MkLineImpl) Varparam() string { return mkline.data.(mkLineAssign).varparam } +func (mkline *MkLine) Varparam() string { return mkline.data.(*mkLineAssign).varparam } // Op applies to variable assignments and returns the assignment operator. -func (mkline *MkLineImpl) Op() MkOperator { return mkline.data.(mkLineAssign).op } +func (mkline *MkLine) Op() MkOperator { return mkline.data.(*mkLineAssign).op } // ValueAlign applies to variable assignments and returns all the text // before the variable value, e.g. "VARNAME+=\t". -func (mkline *MkLineImpl) ValueAlign() string { return mkline.data.(mkLineAssign).valueAlign } +func (mkline *MkLine) ValueAlign() string { return mkline.data.(*mkLineAssign).valueAlign } -func (mkline *MkLineImpl) Value() string { return mkline.data.(mkLineAssign).value } +func (mkline *MkLine) Value() string { return mkline.data.(*mkLineAssign).value } // VarassignComment applies to variable assignments and returns the comment. // @@ -318,7 +327,7 @@ func (mkline *MkLineImpl) Value() string { return mkline.data.(mkLineAssign).val // In the above line, the comment is "# comment". // // The leading "#" is included so that pkglint can distinguish between no comment at all and an empty comment. -func (mkline *MkLineImpl) VarassignComment() string { return mkline.data.(mkLineAssign).comment } +func (mkline *MkLine) VarassignComment() string { return mkline.data.(*mkLineAssign).comment } // FirstLineContainsValue returns whether the variable assignment of a // multiline contains a textual value in the first line. @@ -327,9 +336,9 @@ func (mkline *MkLineImpl) VarassignComment() string { return mkline.data.(mkLine // starts in first line // NO_VALUE_IN_FIRST_LINE= \ // value starts in second line -func (mkline *MkLineImpl) FirstLineContainsValue() bool { - assertf(mkline.IsVarassign() || mkline.IsCommentedVarassign(), "Line must be a variable assignment.") - assertf(mkline.IsMultiline(), "Line must be multiline.") +func (mkline *MkLine) FirstLineContainsValue() bool { + assert(mkline.IsVarassign() || mkline.IsCommentedVarassign()) + assert(mkline.IsMultiline()) // Parsing the continuation marker as variable value is cheating but works well. text := strings.TrimSuffix(mkline.raw[0].orignl, "\n") @@ -338,64 +347,64 @@ func (mkline *MkLineImpl) FirstLineContainsValue() bool { return a.value != "\\" } -func (mkline *MkLineImpl) ShellCommand() string { return mkline.data.(mkLineShell).command } +func (mkline *MkLine) ShellCommand() string { return mkline.data.(mkLineShell).command } -func (mkline *MkLineImpl) Indent() string { +func (mkline *MkLine) Indent() string { if mkline.IsDirective() { - return mkline.data.(mkLineDirective).indent + return mkline.data.(*mkLineDirective).indent } else { - return mkline.data.(mkLineInclude).indent + return mkline.data.(*mkLineInclude).indent } } // Directive returns the preprocessing directive, like "if", "for", "endfor", etc. // // See matchMkDirective. -func (mkline *MkLineImpl) Directive() string { return mkline.data.(mkLineDirective).directive } +func (mkline *MkLine) Directive() string { return mkline.data.(*mkLineDirective).directive } // Args returns the arguments from an .if, .ifdef, .ifndef, .elif, .for, .undef. -func (mkline *MkLineImpl) Args() string { return mkline.data.(mkLineDirective).args } +func (mkline *MkLine) Args() string { return mkline.data.(*mkLineDirective).args } // Cond applies to an .if or .elif line and returns the parsed condition. // // If a parse error occurs, it is silently swallowed, returning a // best-effort part of the condition, or even nil. -func (mkline *MkLineImpl) Cond() MkCond { - cond := mkline.data.(mkLineDirective).cond +func (mkline *MkLine) Cond() *MkCond { + cond := mkline.data.(*mkLineDirective).cond if cond == nil { cond = NewMkParser(mkline.Line, mkline.Args(), true).MkCond() - mkline.data.(mkLineDirective).cond = cond + mkline.data.(*mkLineDirective).cond = cond } return cond } // DirectiveComment is the trailing end-of-line comment, typically at a deeply nested .endif or .endfor. -func (mkline *MkLineImpl) DirectiveComment() string { return mkline.data.(mkLineDirective).comment } +func (mkline *MkLine) DirectiveComment() string { return mkline.data.(*mkLineDirective).comment } -func (mkline *MkLineImpl) HasElseBranch() bool { return mkline.data.(mkLineDirective).elseLine != nil } +func (mkline *MkLine) HasElseBranch() bool { return mkline.data.(*mkLineDirective).elseLine != nil } -func (mkline *MkLineImpl) SetHasElseBranch(elseLine MkLine) { - data := mkline.data.(mkLineDirective) +func (mkline *MkLine) SetHasElseBranch(elseLine *MkLine) { + data := mkline.data.(*mkLineDirective) data.elseLine = elseLine mkline.data = data } -func (mkline *MkLineImpl) MustExist() bool { return mkline.data.(mkLineInclude).mustExist } +func (mkline *MkLine) MustExist() bool { return mkline.data.(*mkLineInclude).mustExist } -func (mkline *MkLineImpl) IncludedFile() string { return mkline.data.(mkLineInclude).includedFile } +func (mkline *MkLine) IncludedFile() string { return mkline.data.(*mkLineInclude).includedFile } -func (mkline *MkLineImpl) Targets() string { return mkline.data.(mkLineDependency).targets } +func (mkline *MkLine) Targets() string { return mkline.data.(mkLineDependency).targets } -func (mkline *MkLineImpl) Sources() string { return mkline.data.(mkLineDependency).sources } +func (mkline *MkLine) Sources() string { return mkline.data.(mkLineDependency).sources } // ConditionalVars applies to .include lines and is a space-separated // list of those variable names on which the inclusion depends. // It is initialized later, step by step, when parsing other lines. -func (mkline *MkLineImpl) ConditionalVars() []string { - return mkline.data.(mkLineInclude).conditionalVars +func (mkline *MkLine) ConditionalVars() []string { + return mkline.data.(*mkLineInclude).conditionalVars } -func (mkline *MkLineImpl) SetConditionalVars(varnames []string) { - include := mkline.data.(mkLineInclude) +func (mkline *MkLine) SetConditionalVars(varnames []string) { + include := mkline.data.(*mkLineInclude) include.conditionalVars = varnames mkline.data = include } @@ -415,7 +424,7 @@ func (mkline *MkLineImpl) SetConditionalVars(varnames []string) { // output: [MkToken("${PREFIX}", MkVarUse("PREFIX")), MkToken("/bin abc")] // // See ValueTokens, which is the tokenized version of Value. -func (mkline *MkLineImpl) Tokenize(text string, warn bool) []*MkToken { +func (mkline *MkLine) Tokenize(text string, warn bool) []*MkToken { if trace.Tracing { defer trace.Call(mkline, text)() } @@ -449,8 +458,8 @@ func (mkline *MkLineImpl) Tokenize(text string, warn bool) []*MkToken { // at that point since the colon is inside a variable use. // // When several separators are adjacent, this results in empty words in the output. -func (mkline *MkLineImpl) ValueSplit(value string, separator string) []string { - assertf(separator != "", "Separator must not be empty; use ValueFields to split on whitespace") +func (mkline *MkLine) ValueSplit(value string, separator string) []string { + assert(separator != "") // Separator must not be empty; use ValueFields to split on whitespace. tokens := mkline.Tokenize(value, false) var split []string @@ -508,7 +517,7 @@ var notSpace = textproc.Space.Inverse() // Compare devel/bmake/files/str.c, function brk_string. // // TODO: Compare with brk_string from devel/bmake, especially for backticks. -func (mkline *MkLineImpl) ValueFields(value string) []string { +func (mkline *MkLine) ValueFields(value string) []string { if trace.Tracing { defer trace.Call(mkline, value)() } @@ -543,13 +552,13 @@ func (mkline *MkLineImpl) ValueFields(value string) []string { return words } -func (mkline *MkLineImpl) ValueTokens() ([]*MkToken, string) { +func (mkline *MkLine) ValueTokens() ([]*MkToken, string) { value := mkline.Value() if value == "" { return nil, "" } - assign := mkline.data.(mkLineAssign) + assign := mkline.data.(*mkLineAssign) if assign.valueMk != nil || assign.valueMkRest != "" { return assign.valueMk, assign.valueMkRest } @@ -565,14 +574,14 @@ func (mkline *MkLineImpl) ValueTokens() ([]*MkToken, string) { // Fields applies to variable assignments and .for loops. // For variable assignments, it returns the right-hand side, properly split into words. // For .for loops, it returns all arguments (including variable names), properly split into words. -func (mkline *MkLineImpl) Fields() []string { +func (mkline *MkLine) Fields() []string { if mkline.IsVarassign() || mkline.IsCommentedVarassign() { value := mkline.Value() if value == "" { return nil } - assign := mkline.data.(mkLineAssign) + assign := mkline.data.(*mkLineAssign) if assign.fields != nil { return assign.fields } @@ -587,7 +596,7 @@ func (mkline *MkLineImpl) Fields() []string { return nil } - directive := mkline.data.(mkLineDirective) + directive := mkline.data.(*mkLineDirective) if directive.fields != nil { return directive.fields } @@ -597,7 +606,7 @@ func (mkline *MkLineImpl) Fields() []string { } -func (*MkLineImpl) WithoutMakeVariables(value string) string { +func (*MkLine) WithoutMakeVariables(value string) string { var valueNovar strings.Builder for _, token := range NewMkParser(nil, value, false).MkTokens() { if token.Varuse == nil { @@ -607,7 +616,7 @@ func (*MkLineImpl) WithoutMakeVariables(value string) string { return valueNovar.String() } -func (mkline *MkLineImpl) ResolveVarsInRelativePath(relativePath string) string { +func (mkline *MkLine) ResolveVarsInRelativePath(relativePath string) string { if !contains(relativePath, "$") { return cleanpath(relativePath) } @@ -682,7 +691,7 @@ func (mkline *MkLineImpl) ResolveVarsInRelativePath(relativePath string) string return tmp } -func (mkline *MkLineImpl) ExplainRelativeDirs() { +func (mkline *MkLine) ExplainRelativeDirs() { mkline.Explain( "Directories in the form \"../../category/package\" make it easier to", "move a package around in pkgsrc, for example from pkgsrc-wip to the", @@ -694,7 +703,7 @@ func (mkline *MkLineImpl) ExplainRelativeDirs() { // // If there is a type mismatch when calling this function, try to add ".line" to // either the method receiver or the other line. -func (mkline *MkLineImpl) RefTo(other MkLine) string { +func (mkline *MkLine) RefTo(other *MkLine) string { return mkline.Line.RefTo(other.Line) } @@ -753,7 +762,7 @@ again: return main, lexer.Rest() } - assertf(lexer.EOF(), "unescapeComment(%q): sb = %q, rest = %q", text, main, lexer.Rest()) + assert(lexer.EOF()) return main, "" } @@ -775,7 +784,7 @@ type mkLineSplitResult struct { // This applies to all line types except those starting with a tab, which // contain the shell commands to be associated with make targets. These cannot // have comments. -func (p MkLineParser) split(line Line, text string) mkLineSplitResult { +func (p MkLineParser) split(line *Line, text string) mkLineSplitResult { mainWithSpaces, comment := p.unescapeComment(text) @@ -813,7 +822,7 @@ func (p MkLineParser) split(line Line, text string) mkLineSplitResult { tokens = append(tokens, &MkToken{other, nil}) } else { - assertf(lexer.SkipByte('$'), "Parse error for %q.", text) + assert(lexer.SkipByte('$')) tokens = append(tokens, &MkToken{"$", nil}) } } @@ -840,7 +849,7 @@ func (p MkLineParser) split(line Line, text string) mkLineSplitResult { return mkLineSplitResult{mainTrimmed, tokens, spaceBeforeComment, hasComment, comment} } -func (p MkLineParser) parseDirective(line Line, data mkLineSplitResult) MkLine { +func (p MkLineParser) parseDirective(line *Line, data mkLineSplitResult) *MkLine { text := line.Text if !hasPrefix(text, ".") { return nil @@ -871,7 +880,7 @@ func (p MkLineParser) parseDirective(line Line, data mkLineSplitResult) MkLine { // it must be trimmed. trimmedComment := trimHspace(data.comment) - return &MkLineImpl{line, &mkLineDirectiveImpl{indent, directive, args, trimmedComment, nil, nil, nil}} + return &MkLine{line, &mkLineDirective{indent, directive, args, trimmedComment, nil, nil, nil}} } // VariableNeedsQuoting determines whether the given variable needs the :Q operator @@ -880,7 +889,7 @@ func (p MkLineParser) parseDirective(line Line, data mkLineSplitResult) MkLine { // This decision depends on many factors, such as whether the type of the context is // a list of things, whether the variable is a list, whether it can contain only // safe characters, and so on. -func (mkline *MkLineImpl) VariableNeedsQuoting(mklines MkLines, varuse *MkVarUse, vartype *Vartype, vuc *VarUseContext) (needsQuoting YesNoUnknown) { +func (mkline *MkLine) VariableNeedsQuoting(mklines *MkLines, varuse *MkVarUse, vartype *Vartype, vuc *VarUseContext) (needsQuoting YesNoUnknown) { if trace.Tracing { defer trace.Call(varuse, vartype, vuc, trace.Result(&needsQuoting))() } @@ -986,11 +995,11 @@ func (mkline *MkLineImpl) VariableNeedsQuoting(mklines MkLines, varuse *MkVarUse } // ForEachUsed calls the action for each variable that is used in the line. -func (mkline *MkLineImpl) ForEachUsed(action func(varUse *MkVarUse, time vucTime)) { +func (mkline *MkLine) ForEachUsed(action func(varUse *MkVarUse, time VucTime)) { - var searchIn func(text string, time vucTime) // mutually recursive with searchInVarUse + var searchIn func(text string, time VucTime) // mutually recursive with searchInVarUse - searchInVarUse := func(varuse *MkVarUse, time vucTime) { + searchInVarUse := func(varuse *MkVarUse, time VucTime) { varname := varuse.varname if !varuse.IsExpression() { action(varuse, time) @@ -1001,7 +1010,7 @@ func (mkline *MkLineImpl) ForEachUsed(action func(varUse *MkVarUse, time vucTime } } - searchIn = func(text string, time vucTime) { + searchIn = func(text string, time VucTime) { if !contains(text, "$") { return } @@ -1016,31 +1025,31 @@ func (mkline *MkLineImpl) ForEachUsed(action func(varUse *MkVarUse, time vucTime switch { case mkline.IsVarassign(): - searchIn(mkline.Varname(), vucTimeLoad) + searchIn(mkline.Varname(), VucLoadTime) searchIn(mkline.Value(), mkline.Op().Time()) case mkline.IsDirective() && mkline.Directive() == "for": - searchIn(mkline.Args(), vucTimeLoad) + searchIn(mkline.Args(), VucLoadTime) case mkline.IsDirective() && mkline.Cond() != nil: mkline.Cond().Walk(&MkCondCallback{ VarUse: func(varuse *MkVarUse) { - searchInVarUse(varuse, vucTimeLoad) + searchInVarUse(varuse, VucLoadTime) }}) case mkline.IsShellCommand(): - searchIn(mkline.ShellCommand(), vucTimeRun) + searchIn(mkline.ShellCommand(), VucRunTime) case mkline.IsDependency(): - searchIn(mkline.Targets(), vucTimeLoad) - searchIn(mkline.Sources(), vucTimeLoad) + searchIn(mkline.Targets(), VucLoadTime) + searchIn(mkline.Sources(), VucLoadTime) case mkline.IsInclude(): - searchIn(mkline.IncludedFile(), vucTimeLoad) + searchIn(mkline.IncludedFile(), VucLoadTime) } } -func (*MkLineImpl) UnquoteShell(str string) string { +func (*MkLine) UnquoteShell(str string) string { var sb strings.Builder n := len(str) @@ -1115,11 +1124,11 @@ func (op MkOperator) String() string { // Time returns the time at which the right-hand side of the assignment is // evaluated. -func (op MkOperator) Time() vucTime { +func (op MkOperator) Time() VucTime { if op == opAssignShell || op == opAssignEval { - return vucTimeLoad + return VucLoadTime } - return vucTimeRun + return VucRunTime } // VarUseContext defines the context in which a variable is defined @@ -1138,34 +1147,39 @@ func (op MkOperator) Time() vucTime { // x86_64 doesn't make sense. type VarUseContext struct { vartype *Vartype - time vucTime + time VucTime quoting VucQuoting IsWordPart bool // Example: LOCALBASE=${LOCALBASE} } -// vucTime is the time at which a variable is used. +// VucTime is the time at which a variable is used. // // See ToolTime, which is the same except that there is no unknown. -type vucTime uint8 +type VucTime uint8 const ( - vucTimeUnknown vucTime = iota + VucUnknownTime VucTime = iota + // VucLoadTime marks a variable use that happens directly when + // the Makefile fragment is loaded. + // // When Makefiles are loaded, the operators := and != evaluate their // right-hand side, as well as the directives .if, .elif and .for. // During loading, not all variables are available yet. // Variable values are still subject to change, especially lists. - vucTimeLoad + VucLoadTime - // All files have been read, all variables can be referenced. - // Variable values don't change anymore. + // VucRunTime marks a variable use that happens after all files have been loaded. + // + // At this time, all variables can be referenced. // + // At this time, variable values don't change anymore. // Well, except for the ::= modifier. // But that modifier is usually not used in pkgsrc. - vucTimeRun + VucRunTime ) -func (t vucTime) String() string { return [...]string{"unknown", "parse", "run"}[t] } +func (t VucTime) String() string { return [...]string{"unknown", "load", "run"}[t] } // VucQuoting describes in what level of quoting the variable is used. // Depending on this context, the modifiers :Q or :M can be allowed or not. @@ -1198,19 +1212,20 @@ func (vuc *VarUseContext) String() string { // indentation. By convention, each directive is indented by 2 spaces. // An excepting are multiple-inclusion guards, they don't increase the // indentation. +// +// Indentation starts with 0 spaces. +// Each .if or .for indents all inner directives by 2. +// Except for .if with multiple-inclusion guard, which indents all inner directives by 0. +// Each .elif, .else, .endif, .endfor uses the outer indentation instead. type Indentation struct { levels []indentationLevel } -func NewIndentation() *Indentation { - ind := Indentation{} - ind.Push(nil, 0, "") // Dummy - return &ind -} +func NewIndentation() *Indentation { return &Indentation{} } func (ind *Indentation) String() string { var s strings.Builder - for _, level := range ind.levels[1:] { + for _, level := range ind.levels { _, _ = fmt.Fprintf(&s, " %d", level.depth) if len(level.conditionalVars) > 0 { _, _ = fmt.Fprintf(&s, " (%s)", strings.Join(level.conditionalVars, " ")) @@ -1219,13 +1234,13 @@ func (ind *Indentation) String() string { return "[" + trimHspace(s.String()) + "]" } -func (ind *Indentation) RememberUsedVariables(cond MkCond) { +func (ind *Indentation) RememberUsedVariables(cond *MkCond) { cond.Walk(&MkCondCallback{ VarUse: func(varuse *MkVarUse) { ind.AddVar(varuse.varname) }}) } type indentationLevel struct { - mkline MkLine // The line in which the indentation started; the .if/.for + mkline *MkLine // The line in which the indentation started; the .if/.for depth int // Number of space characters; always a multiple of 2 args string // The arguments from the .if or .for, or the latest .elif conditionalVars []string // Variables on which the current path depends @@ -1238,12 +1253,12 @@ type indentationLevel struct { checkedFiles []string } -func (ind *Indentation) Len() int { - return len(ind.levels) +func (ind *Indentation) Empty() bool { + return len(ind.levels) == 0 } func (ind *Indentation) top() *indentationLevel { - return &ind.levels[ind.Len()-1] + return &ind.levels[len(ind.levels)-1] } // Depth returns the number of space characters by which the directive @@ -1252,18 +1267,23 @@ func (ind *Indentation) top() *indentationLevel { // This is typically two more than the surrounding level, except for // multiple-inclusion guards. func (ind *Indentation) Depth(directive string) int { + i := len(ind.levels) - 1 switch directive { - case "if", "elif", "else", "endfor", "endif": - return ind.levels[imax(0, ind.Len()-2)].depth + case "elif", "else", "endfor", "endif": + i-- + } + if i < 0 { + return 0 } - return ind.top().depth + return ind.levels[i].depth } func (ind *Indentation) Pop() { - ind.levels = ind.levels[:ind.Len()-1] + ind.levels = ind.levels[:len(ind.levels)-1] } -func (ind *Indentation) Push(mkline MkLine, indent int, condition string) { +func (ind *Indentation) Push(mkline *MkLine, indent int, condition string) { + assert(mkline.IsDirective()) ind.levels = append(ind.levels, indentationLevel{mkline, indent, condition, nil, nil}) } @@ -1272,7 +1292,7 @@ func (ind *Indentation) Push(mkline MkLine, indent int, condition string) { // // Variables named *_MK are ignored since they are usually not interesting. func (ind *Indentation) AddVar(varname string) { - if hasSuffix(varname, "_MK") { + if hasSuffix(varname, "_MK") || ind.Empty() { return } @@ -1318,9 +1338,9 @@ func (ind *Indentation) Varnames() []string { var varnames []string for _, level := range ind.levels { for _, levelVarname := range level.conditionalVars { - assertf( - !hasSuffix(levelVarname, "_MK"), - "multiple-inclusion guard must be filtered out earlier.") + // multiple-inclusion guard must be filtered out earlier. + assert(!hasSuffix(levelVarname, "_MK")) + varnames = append(varnames, levelVarname) } } @@ -1337,7 +1357,9 @@ func (ind *Indentation) AddCheckedFile(filename string) { top.checkedFiles = append(top.checkedFiles, filename) } -func (ind *Indentation) IsCheckedFile(filename string) bool { +// HasExists returns whether the given filename has been tested in an +// exists(filename) condition and thus may or may not exist. +func (ind *Indentation) HasExists(filename string) bool { for _, level := range ind.levels { for _, levelFilename := range level.checkedFiles { if filename == levelFilename { @@ -1348,7 +1370,7 @@ func (ind *Indentation) IsCheckedFile(filename string) bool { return false } -func (ind *Indentation) TrackBefore(mkline MkLine) { +func (ind *Indentation) TrackBefore(mkline *MkLine) { if !mkline.IsDirective() { return } @@ -1358,11 +1380,11 @@ func (ind *Indentation) TrackBefore(mkline MkLine) { switch mkline.Directive() { case "for", "if", "ifdef", "ifndef": - ind.Push(mkline, ind.top().depth, mkline.Args()) + ind.Push(mkline, ind.Depth(mkline.Directive()), mkline.Args()) } } -func (ind *Indentation) TrackAfter(mkline MkLine) { +func (ind *Indentation) TrackAfter(mkline *MkLine) { if !mkline.IsDirective() { return } @@ -1385,16 +1407,17 @@ func (ind *Indentation) TrackAfter(mkline MkLine) { case "elif": // Handled here instead of TrackBefore to allow the action to access the previous condition. - ind.top().args = args + if !ind.Empty() { + ind.top().args = args + } case "else": - top := ind.top() - if top.mkline != nil { - top.mkline.SetHasElseBranch(mkline) + if !ind.Empty() { + ind.top().mkline.SetHasElseBranch(mkline) } case "endfor", "endif": - if ind.Len() > 1 { // Can only be false in unbalanced files. + if !ind.Empty() { // Can only be false in unbalanced files. ind.Pop() } } @@ -1422,12 +1445,12 @@ func (ind *Indentation) TrackAfter(mkline MkLine) { } func (ind *Indentation) CheckFinish(filename string) { - if ind.Len() <= 1 { + if ind.Empty() { return } eofLine := NewLineEOF(filename) - for ind.Len() > 1 { - openingMkline := ind.levels[ind.Len()-1].mkline + for !ind.Empty() { + openingMkline := ind.top().mkline eofLine.Errorf(".%s from %s must be closed.", openingMkline.Directive(), eofLine.RefTo(openingMkline.Line)) ind.Pop() } @@ -1448,10 +1471,10 @@ func (ind *Indentation) CheckFinish(filename string) { // of the variable. The square bracket is only allowed in the parameter part. var ( VarbaseBytes = textproc.NewByteSet("A-Za-z_0-9+---") - VarparamBytes = textproc.NewByteSet("A-Za-z_0-9#*+---.[") + VarparamBytes = textproc.NewByteSet("A-Za-z_0-9#*+---./[") ) -func (p MkLineParser) MatchVarassign(line Line, text string, asdfData mkLineSplitResult) (m bool, assignment mkLineAssign) { +func (p MkLineParser) MatchVarassign(line *Line, text string, asdfData mkLineSplitResult) (m bool, assignment *mkLineAssign) { // A commented variable assignment does not have leading whitespace. // Otherwise line 1 of almost every Makefile fragment would need to @@ -1518,7 +1541,7 @@ func (p MkLineParser) MatchVarassign(line Line, text string, asdfData mkLineSpli spaceBeforeComment = "" } - return true, &mkLineAssignImpl{ + return true, &mkLineAssign{ commented: commented, varname: varname, varcanon: varnameCanon(varname), diff --git a/pkgtools/pkglint/files/mkline_test.go b/pkgtools/pkglint/files/mkline_test.go index 455e495af36..be5c27aa521 100644 --- a/pkgtools/pkglint/files/mkline_test.go +++ b/pkgtools/pkglint/files/mkline_test.go @@ -159,7 +159,7 @@ func (s *Suite) Test_MkLineParser_Parse__autofix_space_after_varname(c *check.C) t.SetUpCommandLine("-Wspace") filename := t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "VARNAME +=\t${VARNAME}", "VARNAME+ =\t${VARNAME+}", "VARNAME+ +=\t${VARNAME+}", @@ -182,7 +182,7 @@ func (s *Suite) Test_MkLineParser_Parse__autofix_space_after_varname(c *check.C) "AUTOFIX: ~/Makefile:2: Replacing \"VARNAME +=\" with \"VARNAME+=\".", "AUTOFIX: ~/Makefile:5: Replacing \"VARNAME+ ?=\" with \"VARNAME+?=\".") t.CheckFileLines("Makefile", - MkRcsID+"", + MkCvsID+"", "VARNAME+=\t${VARNAME}", "VARNAME+ =\t${VARNAME+}", "VARNAME+ +=\t${VARNAME+}", @@ -264,7 +264,7 @@ func (s *Suite) Test_MkLine_FirstLineContainsValue(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "VAR=\tvalue", "VAR= value \\", "\tstarts in first line", @@ -275,13 +275,9 @@ func (s *Suite) Test_MkLine_FirstLineContainsValue(c *check.C) { "#VAR= \\", "\tvalue starts in second line") - t.ExpectPanic( - func() { mklines.mklines[0].FirstLineContainsValue() }, - "Pkglint internal error: Line must be a variable assignment.") + t.ExpectAssert(func() { mklines.mklines[0].FirstLineContainsValue() }) - t.ExpectPanic( - func() { mklines.mklines[1].FirstLineContainsValue() }, - "Pkglint internal error: Line must be multiline.") + t.ExpectAssert(func() { mklines.mklines[1].FirstLineContainsValue() }) t.Check(mklines.mklines[2].FirstLineContainsValue(), equals, true) t.Check(mklines.mklines[3].FirstLineContainsValue(), equals, false) @@ -308,7 +304,7 @@ func (s *Suite) Test_VarUseContext_String(c *check.C) { t.SetUpVartypes() vartype := G.Pkgsrc.VariableType(nil, "PKGNAME") - vuc := VarUseContext{vartype, vucTimeUnknown, VucQuotBackt, false} + vuc := VarUseContext{vartype, VucUnknownTime, VucQuotBackt, false} c.Check(vuc.String(), equals, "(Pkgname (package-settable) time:unknown quoting:backt wordpart:false)") } @@ -363,7 +359,7 @@ func (s *Suite) Test_MkLineParser_Parse__infrastructure(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("infra.mk", - MkRcsID, + MkCvsID, " USE_BUILTIN.${_pkg_:S/^-//}:=no", ".error \"Something went wrong\"", ".export WRKDIR", @@ -399,7 +395,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__unknown_rhs(c *check.C) { mkline := t.NewMkLine("filename.mk", 1, "PKGNAME:= ${UNKNOWN}") t.SetUpVartypes() - vuc := VarUseContext{G.Pkgsrc.VariableType(nil, "PKGNAME"), vucTimeLoad, VucQuotUnknown, false} + vuc := VarUseContext{G.Pkgsrc.VariableType(nil, "PKGNAME"), VucLoadTime, VucQuotUnknown, false} nq := mkline.VariableNeedsQuoting(nil, &MkVarUse{"UNKNOWN", nil}, nil, &vuc) c.Check(nq, equals, unknown) @@ -411,11 +407,11 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__append_URL_to_list_of_URLs(c * t.SetUpVartypes() t.SetUpMasterSite("MASTER_SITE_SOURCEFORGE", "http://downloads.sourceforge.net/sourceforge/") mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "MASTER_SITES=\t${HOMEPAGE}") mkline := mklines.mklines[1] - vuc := VarUseContext{G.Pkgsrc.vartypes.Canon("MASTER_SITES"), vucTimeRun, VucQuotPlain, false} + vuc := VarUseContext{G.Pkgsrc.vartypes.Canon("MASTER_SITES"), VucRunTime, VucQuotPlain, false} nq := mkline.VariableNeedsQuoting(nil, &MkVarUse{"HOMEPAGE", nil}, G.Pkgsrc.vartypes.Canon("HOMEPAGE"), &vuc) c.Check(nq, equals, no) @@ -431,7 +427,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__append_list_to_list(c *check.C t.SetUpVartypes() t.SetUpMasterSite("MASTER_SITE_SOURCEFORGE", "http://downloads.sourceforge.net/sourceforge/") mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "MASTER_SITES=\t${MASTER_SITE_SOURCEFORGE:=squirrel-sql/}") MkLineChecker{mklines, mklines.mklines[1]}.checkVarassign() @@ -445,7 +441,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__eval_shell(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("builtin.mk", - MkRcsID, + MkCvsID, "USE_BUILTIN.Xfixes!=\t${PKG_ADMIN} pmatch 'pkg-[0-9]*' ${BUILTIN_PKG.Xfixes:Q}") MkLineChecker{mklines, mklines.mklines[1]}.checkVarassign() @@ -459,7 +455,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_single_quotes(c *ch t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "SUBST_SED.hpath=\t-e 's|^\\(INSTALL[\t:]*=\\).*|\\1${INSTALL}|'") MkLineChecker{mklines, mklines.mklines[1]}.checkVarassign() @@ -477,7 +473,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_command(c *check.C) t.SetUpTool("sort", "SORT", AtRunTime) G.Pkg = NewPackage(t.File("category/pkgbase")) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "GENERATE_PLIST= cd ${DESTDIR}${PREFIX}; ${FIND} * \\( -type f -or -type l \\) | ${SORT};") mklines.collectDefinedVariables() @@ -492,7 +488,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__word_as_part_of_word(c *check. t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "EGDIR=\t${EGDIR}/${MACHINE_GNU_PLATFORM}") MkLineChecker{mklines, mklines.mklines[1]}.Check() @@ -513,7 +509,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_as_command_argument(c t.SetUpTool("bash", "BASH", AtRunTime) t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "\t${RUN} cd ${WRKSRC} && ( ${ECHO} ${PERL5:Q} ; ${ECHO} ) | ${BASH} ./install", "\t${RUN} cd ${WRKSRC} && ( ${ECHO} ${PERL5} ; ${ECHO} ) | ${BASH} ./install") @@ -530,7 +526,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__URL_as_part_of_word_in_list(c t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "MASTER_SITES=${HOMEPAGE}archive/") MkLineChecker{mklines, mklines.mklines[1]}.Check() @@ -543,7 +539,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__MASTER_SITES_and_HOMEPAGE(c *c t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "MASTER_SITES=\t${HOMEPAGE}", "MASTER_SITES=\t${PATH}", // Some nonsense just for branch coverage. "HOMEPAGE=\t${MASTER_SITES}", @@ -568,7 +564,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_subshell(c *check.C t.SetUpTool("awk", "AWK", AtRunTime) t.SetUpTool("echo", "ECHO", AtRunTime) mklines := t.NewMkLines("xpi.mk", - MkRcsID, + MkCvsID, "\t id=$$(${AWK} '{print}' < ${WRKSRC}/idfile) && echo \"$$id\"", "\t id=`${AWK} '{print}' < ${WRKSRC}/idfile` && echo \"$$id\"") @@ -588,7 +584,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__LDFLAGS_in_single_quotes(c *ch t.SetUpVartypes() mklines := t.NewMkLines("x11/mlterm/Makefile", - MkRcsID, + MkCvsID, "SUBST_SED.link=-e 's|(LIBTOOL_LINK).*(LIBS)|& ${LDFLAGS:M*:Q}|g'", "SUBST_SED.link=-e 's|(LIBTOOL_LINK).*(LIBS)|& '${LDFLAGS:M*:Q}'|g'") @@ -605,7 +601,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__package_options(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "PKG_SUGGESTED_OPTIONS+=\t${PKG_DEFAULT_OPTIONS:Mcdecimal} ${PKG_OPTIONS.py-trytond:Mcdecimal}") MkLineChecker{mklines, mklines.mklines[1]}.Check() @@ -621,7 +617,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_quotes_in_subshell_in_ t.SetUpTool("sh", "SH", AtRunTime) t.SetUpVartypes() mklines := t.NewMkLines("x11/labltk/Makefile", - MkRcsID, + MkCvsID, "CONFIGURE_ARGS+=\t-tklibs \"`${SH} -c '${ECHO} $$TK_LD_FLAGS'`\"") MkLineChecker{mklines, mklines.mklines[1]}.Check() @@ -677,7 +673,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__guessed_list_variable_in_quote t.SetUpVartypes() mklines := t.NewMkLines("audio/jack-rack/Makefile", - MkRcsID, + MkCvsID, "LADSPA_PLUGIN_PATH=\t${PREFIX}/lib/ladspa", "CPPFLAGS+=\t\t-DLADSPA_PATH=\"\\\"${LADSPA_PLUGIN_PATH}\\\"\"") @@ -692,7 +688,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__list_in_list(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("x11/eterm/Makefile", - MkRcsID, + MkCvsID, "DISTFILES=\t${DEFAULT_DISTFILES} ${PIXMAP_FILES}") mklines.Check() @@ -708,7 +704,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__PKGNAME_and_URL_list_in_URL_li t.SetUpMasterSite("MASTER_SITE_GNOME", "http://ftp.gnome.org/") t.SetUpVartypes() mklines := t.NewMkLines("x11/gtk3/Makefile", - MkRcsID, + MkCvsID, "MASTER_SITES=\tftp://ftp.gtk.org/${PKGNAME}/ ${MASTER_SITE_GNOME:=subdir/}") MkLineChecker{mklines, mklines.mklines[1]}.checkVarassignRightVaruse() @@ -722,7 +718,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_CONFIGURE_ENV(c *check t.SetUpVartypes() t.SetUpTool("tar", "TAR", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "CONFIGURE_ENV+=\tSYS_TAR_COMMAND_PATH=${TOOLS_TAR:Q}") @@ -742,7 +738,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__backticks(c *check.C) { t.SetUpVartypes() t.SetUpTool("cat", "CAT", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "COMPILE_CMD=\tcc `${CAT} ${WRKDIR}/compileflags`", "COMMENT_CMD=\techo `echo ${COMMENT}`") @@ -770,7 +766,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__only_remove_known(c *check.C) t.SetUpVartypes() mklines := t.SetUpFileMkLines("Makefile", - MkRcsID, + MkCvsID, "", "demo: .PHONY", "\t${ECHO} ${WRKSRC:Q}", @@ -781,7 +777,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__only_remove_known(c *check.C) t.CheckOutputLines( "AUTOFIX: ~/Makefile:4: Replacing \"${WRKSRC:Q}\" with \"${WRKSRC}\".") t.CheckFileLines("Makefile", - MkRcsID, + MkCvsID, "", "demo: .PHONY", "\t${ECHO} ${WRKSRC}", @@ -797,7 +793,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__shellword_part(c *check.C) { t.SetUpVartypes() mklines := t.SetUpFileMkLines("Makefile", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+= class", "SUBST_STAGE.class= pre-configure", @@ -820,7 +816,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_shell_command(c *check t.SetUpTool("bash", "BASH", AtRunTime) mklines := t.SetUpFileMkLines("Makefile", - MkRcsID, + MkCvsID, "", "CONFIG_SHELL= ${BASH}") @@ -838,7 +834,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__uncovered_cases(c *check.C) { t.SetUpVartypes() mklines := t.SetUpFileMkLines("Makefile", - MkRcsID, + MkCvsID, "", "GO_SRCPATH= ${HOMEPAGE:S,https://,,}", "LINKER_RPATH_FLAG:= ${LINKER_RPATH_FLAG:S/-rpath/& /}", @@ -929,7 +925,7 @@ func (s *Suite) Test_MkLine__shell_varuse_in_backt_dquot(c *check.C) { t.SetUpVartypes() t.SetUpTool("grep", "GREP", AtRunTime) mklines := t.NewMkLines("x11/motif/Makefile", - MkRcsID, + MkCvsID, "post-patch:", "\tfiles=`${GREP} -l \".fB$${name}.fP(3)\" *.3`") @@ -945,7 +941,7 @@ func (s *Suite) Test_MkLine__comment_in_comment(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "COMMENT=\tPKCS#5 v2.0 PBKDF2 Module") mklines.Check() @@ -1018,9 +1014,7 @@ func (s *Suite) Test_MkLine_ValueSplit__invalid_argument(c *check.C) { mkline := t.NewMkLine("filename.mk", 123, "VAR=\tvalue") - t.ExpectPanic( - func() { mkline.ValueSplit("value", "") }, - "Pkglint internal error: Separator must not be empty; use ValueFields to split on whitespace") + t.ExpectAssert(func() { mkline.ValueSplit("value", "") }) } func (s *Suite) Test_MkLine_Fields__varassign(c *check.C) { @@ -1272,7 +1266,7 @@ func (s *Suite) Test_MkLine_ValueTokens__warnings(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "ROUND=\t$(ROUND)") mklines.mklines[1].ValueTokens() @@ -1298,7 +1292,7 @@ func (s *Suite) Test_MkLine_ResolveVarsInRelativePath(c *check.C) { t.CreateFileLines("emulators/suse100_base/Makefile") t.CreateFileLines("lang/python36/Makefile") mklines := t.SetUpFileMkLines("Makefile", - MkRcsID) + MkCvsID) mkline := mklines.mklines[0] test := func(before string, after string) { @@ -1330,7 +1324,7 @@ func (s *Suite) Test_MkLine_ResolveVarsInRelativePath__directory_depth(c *check. t.SetUpVartypes() mklines := t.SetUpFileMkLines("multimedia/totem/bla.mk", - MkRcsID, + MkCvsID, "BUILDLINK_PKGSRCDIR.totem?=\t../../multimedia/totem") mklines.Check() @@ -1349,14 +1343,12 @@ func (s *Suite) Test_MkLine_ResolveVarsInRelativePath__without_tracing(c *check. t.DisableTracing() t.SetUpVartypes() mklines := t.SetUpFileMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "BUILDLINK_PKGSRCDIR.totem?=\t../../${PKGPATH.multimedia/totem}") mklines.Check() t.CheckOutputLines( - // FIXME: It's ok to have variable parameters including a slash. - "WARN: ~/buildlink3.mk:2: Invalid part \"/totem\" after variable name \"PKGPATH.multimedia\".", "WARN: ~/buildlink3.mk:2: PKGPATH.multimedia/totem is used but not defined.") } @@ -1372,7 +1364,7 @@ func (s *Suite) Test_MkLineParser_MatchVarassign(c *check.C) { return } - expected := mkLineAssignImpl{ + expected := mkLineAssign{ commented: commented, varname: varname, varcanon: varnameCanon(varname), @@ -1525,7 +1517,7 @@ func (s *Suite) Test_MkLineParser_MatchVarassign(c *check.C) { testInvalid("# VAR=value") testInvalid("#\tVAR=value") - testInvalid(MkRcsID) + testInvalid(MkCvsID) } func (s *Suite) Test_NewMkOperator(c *check.C) { @@ -1547,7 +1539,7 @@ func (s *Suite) Test_Indentation(c *check.C) { ind.Push(mkline, 2, "") - c.Check(ind.Depth("if"), equals, 0) // Because "if" is handled in MkLines.TrackBefore. + c.Check(ind.Depth("if"), equals, 2) c.Check(ind.Depth("endfor"), equals, 0) ind.AddVar("LEVEL1.VAR1") @@ -1579,12 +1571,75 @@ func (s *Suite) Test_Indentation(c *check.C) { c.Check(ind.String(), equals, "[]") } +func (s *Suite) Test_Indentation__realistic(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("filename.mk", + MkCvsID, + "", + ".if 1", + ". if !defined(GUARD_MK)", + ". for var in 1 2 3", + ". if !defined(GUARD_MK)", + ". if 3", + ". endif", + ". endif", + ". endfor", + ". endif", + ".elif 1", + ". for var in 1 2 3", + ". endfor", + ".else", + ". for var in 1 2 3", + ". endfor", + ".endif") + + t.EnableTracingToLog() + + mklines.ForEach(func(mkline *MkLine) {}) + + t.CheckOutputLinesMatching(`Indentation`, + "TRACE: Indentation before line 3: []", + "TRACE: Indentation after line 3: [2]", + "TRACE: Indentation before line 4: [2]", + "TRACE: Indentation after line 4: [2 2]", + "TRACE: Indentation before line 5: [2 2]", + "TRACE: Indentation after line 5: [2 2 4]", + "TRACE: Indentation before line 6: [2 2 4]", + "TRACE: Indentation after line 6: [2 2 4 4]", + "TRACE: Indentation before line 7: [2 2 4 4]", + "TRACE: Indentation after line 7: [2 2 4 4 6]", + "TRACE: Indentation before line 8: [2 2 4 4 6]", + "TRACE: Indentation after line 8: [2 2 4 4]", + "TRACE: Indentation before line 9: [2 2 4 4]", + "TRACE: Indentation after line 9: [2 2 4]", + "TRACE: Indentation before line 10: [2 2 4]", + "TRACE: Indentation after line 10: [2 2]", + "TRACE: Indentation before line 11: [2 2]", + "TRACE: Indentation after line 11: [2]", + "TRACE: Indentation before line 12: [2]", + "TRACE: Indentation after line 12: [2]", + "TRACE: Indentation before line 13: [2]", + "TRACE: Indentation after line 13: [2 4]", + "TRACE: Indentation before line 14: [2 4]", + "TRACE: Indentation after line 14: [2]", + "TRACE: Indentation before line 15: [2]", + "TRACE: Indentation after line 15: [2]", + "TRACE: Indentation before line 16: [2]", + "TRACE: Indentation after line 16: [2 4]", + "TRACE: Indentation before line 17: [2 4]", + "TRACE: Indentation after line 17: [2]", + "TRACE: Indentation before line 18: [2]", + "TRACE: Indentation after line 18: []") +} + func (s *Suite) Test_Indentation_RememberUsedVariables(c *check.C) { t := s.Init(c) mkline := t.NewMkLine("Makefile", 123, ".if ${PKGREVISION} > 0") ind := NewIndentation() + ind.TrackBefore(mkline) ind.RememberUsedVariables(mkline.Cond()) t.CheckOutputEmpty() @@ -1595,7 +1650,7 @@ func (s *Suite) Test_Indentation_TrackAfter__checked_files(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, "", ".if make(other.mk)", ". include \"other.mk\"", @@ -1617,7 +1672,7 @@ func (s *Suite) Test_Indentation_TrackAfter__lonely_else(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, "", ".else") @@ -1659,7 +1714,7 @@ func (s *Suite) Test_MkLine_ForEachUsed(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "VAR=\t${VALUE} # ${varassign.comment}", ".if ${OPSYS:M${endianness}} == ${Hello:L} # ${if.comment}", ".for var in one ${two} three # ${for.comment}", @@ -1676,20 +1731,20 @@ func (s *Suite) Test_MkLine_ForEachUsed(c *check.C) { var varnames []string for _, mkline := range mklines.mklines { - mkline.ForEachUsed(func(varUse *MkVarUse, time vucTime) { + mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) { varnames = append(varnames, time.String()+" "+varUse.varname) }) } c.Check(varnames, deepEquals, []string{ "run VALUE", - "parse OPSYS", - "parse endianness", + "load OPSYS", + "load endianness", // "Hello" is not a variable name, the :L modifier makes it an expression. - "parse two", - "parse TARGETS", - "parse SOURCES", - "parse OTHER_FILE", + "load two", + "load TARGETS", + "load SOURCES", + "load OTHER_FILE", "run VAR.${param}", "run param", @@ -1711,7 +1766,7 @@ func (s *Suite) Test_MkLine_UnquoteShell(c *check.C) { t := s.Init(c) test := func(input, output string) { - unquoted := (*MkLineImpl).UnquoteShell(nil, input) + unquoted := (*MkLine).UnquoteShell(nil, input) t.Check(unquoted, equals, output) } diff --git a/pkgtools/pkglint/files/mklinechecker.go b/pkgtools/pkglint/files/mklinechecker.go index 479e37fb215..6383c26410b 100644 --- a/pkgtools/pkglint/files/mklinechecker.go +++ b/pkgtools/pkglint/files/mklinechecker.go @@ -11,8 +11,8 @@ import ( // MkLineChecker provides checks for a single line from a Makefile fragment. type MkLineChecker struct { - MkLines MkLines - MkLine MkLine + MkLines *MkLines + MkLine *MkLine } func (ck MkLineChecker) Check() { @@ -181,19 +181,26 @@ func (ck MkLineChecker) checkDirectiveEnd(ind *Indentation) { directive := mkline.Directive() comment := mkline.DirectiveComment() - if directive == "endif" && comment != "" { + if ind.Empty() { + mkline.Errorf("Unmatched .%s.", directive) + return + } + + if comment == "" { + return + } + + if directive == "endif" { if args := ind.Args(); !contains(args, comment) { mkline.Warnf("Comment %q does not match condition %q.", comment, args) } } - if directive == "endfor" && comment != "" { + + if directive == "endfor" { if args := ind.Args(); !contains(args, comment) { mkline.Warnf("Comment %q does not match loop %q.", comment, args) } } - if ind.Len() <= 1 { - mkline.Errorf("Unmatched .%s.", directive) - } } func (ck MkLineChecker) checkDirectiveFor(forVars map[string]bool, indentation *Indentation) { @@ -225,8 +232,8 @@ func (ck MkLineChecker) checkDirectiveFor(forVars map[string]bool, indentation * // running pkglint over the whole pkgsrc tree did not produce any different result // whether guessed was true or false. forLoopType := NewVartype(btForLoop, List, NewACLEntry("*", aclpAllRead)) - forLoopContext := VarUseContext{forLoopType, vucTimeLoad, VucQuotPlain, false} - mkline.ForEachUsed(func(varUse *MkVarUse, time vucTime) { + forLoopContext := VarUseContext{forLoopType, VucLoadTime, VucQuotPlain, false} + mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) { ck.CheckVaruse(varUse, &forLoopContext) }) } @@ -383,7 +390,7 @@ func (ck MkLineChecker) explainPermissions(varname string, vartype *Vartype, int for _, rule := range vartype.aclEntries { perms := rule.permissions.HumanString() - files := rule.glob + files := rule.matcher.originalPattern if files == "*" { files = "any file" } @@ -407,13 +414,13 @@ func (ck MkLineChecker) checkVarassignLeftRationale() { return } - isRationale := func(mkline MkLine) bool { + isRationale := func(mkline *MkLine) bool { return mkline.IsComment() && !hasPrefix(mkline.Text, "# $") && !mkline.IsCommentedVarassign() } - needsRationale := func(mkline MkLine) bool { + needsRationale := func(mkline *MkLine) bool { if !mkline.IsVarassign() && !mkline.IsCommentedVarassign() { return false } @@ -506,7 +513,7 @@ func (ck MkLineChecker) checkVarUseBuildDefs(varname string) { return } - if !(!ck.MkLines.buildDefs[varname] && ck.MkLines.FirstTimeSlice("BUILD_DEFS", varname)) { + if !(!ck.MkLines.buildDefs[varname] && ck.MkLines.once.FirstTimeSlice("BUILD_DEFS", varname)) { return } @@ -548,7 +555,7 @@ func (ck MkLineChecker) checkVaruseUndefined(vartype *Vartype, varname string) { case G.Pkgsrc.vartypes.DefinedCanon(varname): return - case !ck.MkLines.FirstTimeSlice("used but not defined: ", varname): + case !ck.MkLines.once.FirstTimeSlice("used but not defined: ", varname): return } @@ -671,7 +678,7 @@ func (ck MkLineChecker) checkVarusePermissions(varname string, vartype *Vartype, // be used at load time somewhere in the future because it is // assigned to another variable, and that variable is allowed // to be used at load time. - directly := vuc.time == vucTimeLoad + directly := vuc.time == VucLoadTime indirectly := !directly && vuc.vartype != nil && vuc.vartype.Union().Contains(aclpUseLoadtime) @@ -700,13 +707,13 @@ func (ck MkLineChecker) checkVarusePermissions(varname string, vartype *Vartype, } } - if ck.MkLines.FirstTimeSlice("checkVarusePermissions", varname) { - ck.warnVarusePermissions(varname, vartype, directly, indirectly) + if ck.MkLines.once.FirstTimeSlice("checkVarusePermissions", varname) { + ck.warnVarusePermissions(vuc.vartype, varname, vartype, directly, indirectly) } } func (ck MkLineChecker) warnVarusePermissions( - varname string, vartype *Vartype, directly, indirectly bool) { + vucVartype *Vartype, varname string, vartype *Vartype, directly, indirectly bool) { mkline := ck.MkLine @@ -718,6 +725,13 @@ func (ck MkLineChecker) warnVarusePermissions( } if indirectly { + // Some of the guessed variables may be used at load time. But since the + // variable type and these permissions are guessed, pkglint should not + // issue the following warning, since it is often wrong. + if vucVartype.Guessed() { + return + } + mkline.Warnf("%s should not be used indirectly at load time (via %s).", varname, mkline.Varname()) ck.explainPermissions(varname, vartype, @@ -841,14 +855,35 @@ func (ck MkLineChecker) checkVarUseQuoting(varUse *MkVarUse, vartype *Vartype, v mkline := ck.MkLine if mod == ":M*:Q" && !needMstar { - mkline.Notef("The :M* modifier is not needed here.") + if !vartype.Guessed() { + mkline.Notef("The :M* modifier is not needed here.") + } } else if needsQuoting == yes { modNoQ := strings.TrimSuffix(mod, ":Q") modNoM := strings.TrimSuffix(modNoQ, ":M*") correctMod := modNoM + ifelseStr(needMstar, ":M*:Q", ":Q") if correctMod == mod+":Q" && vuc.IsWordPart && !vartype.IsShell() { - if vartype.List() { + + isSingleWordConstant := func() bool { + if G.Pkg == nil { + return false + } + + varinfo := G.Pkg.redundant.vars[varname] + if varinfo == nil || !varinfo.vari.Constant() { + return false + } + + value := varinfo.vari.ConstantValue() + return len(mkline.ValueFields(value)) == 1 + } + + if vartype.List() && isSingleWordConstant() { + // Do not warn in this special case, which typically occurs + // for BUILD_DIRS or similar package-settable variables. + + } else if vartype.List() { mkline.Warnf("The list variable %s should not be embedded in a word.", varname) mkline.Explain( "When a list variable has multiple elements, this expression expands", @@ -982,7 +1017,7 @@ func (ck MkLineChecker) checkVarassign() { // checkVarassignLeft checks everything to the left of the assignment operator. func (ck MkLineChecker) checkVarassignLeft() { varname := ck.MkLine.Varname() - if hasPrefix(varname, "_") && !G.Infrastructure { + if hasPrefix(varname, "_") && !G.Infrastructure && G.Pkgsrc.vartypes.Canon(varname) == nil { ck.MkLine.Warnf("Variable names starting with an underscore (%s) are reserved for internal pkgsrc use.", varname) } @@ -997,7 +1032,7 @@ func (ck MkLineChecker) checkVarassignLeft() { ck.checkTextVarUse( ck.MkLine.Varname(), NewVartype(BtVariableName, NoVartypeOptions, NewACLEntry("*", aclpAll)), - vucTimeLoad) + VucLoadTime) } func (ck MkLineChecker) checkVarassignOp() { @@ -1112,7 +1147,7 @@ func (ck MkLineChecker) checkVarassignLeftNotUsed() { return } - if !ck.MkLines.FirstTimeSlice("defined but not used: ", varname) { + if !ck.MkLines.once.FirstTimeSlice("defined but not used: ", varname) { return } @@ -1138,9 +1173,9 @@ func (ck MkLineChecker) checkVarassignRightVaruse() { mkline := ck.MkLine op := mkline.Op() - time := vucTimeRun + time := VucRunTime if op == opAssignEval || op == opAssignShell { - time = vucTimeLoad + time = VucLoadTime } vartype := G.Pkgsrc.VariableType(ck.MkLines, mkline.Varname()) @@ -1155,7 +1190,7 @@ func (ck MkLineChecker) checkVarassignRightVaruse() { } } -func (ck MkLineChecker) checkTextVarUse(text string, vartype *Vartype, time vucTime) { +func (ck MkLineChecker) checkTextVarUse(text string, vartype *Vartype, time VucTime) { if !contains(text, "$") { return } @@ -1178,7 +1213,7 @@ func (ck MkLineChecker) checkTextVarUse(text string, vartype *Vartype, time vucT // checkVarassignVaruseShell is very similar to checkVarassignRightVaruse, they just differ // in the way they determine isWordPart. -func (ck MkLineChecker) checkVarassignVaruseShell(vartype *Vartype, time vucTime) { +func (ck MkLineChecker) checkVarassignVaruseShell(vartype *Vartype, time VucTime) { if trace.Tracing { defer trace.Call(vartype, time)() } @@ -1229,7 +1264,8 @@ func (ck MkLineChecker) checkVarassignMisc() { "It is this meaning that should be described.") } - if varname == "DIST_SUBDIR" || varname == "WRKSRC" { + switch varname { + case "DIST_SUBDIR", "WRKSRC", "MASTER_SITES": // TODO: Replace regex with proper VarUse. if m, revVarname := match1(value, `\$\{(PKGNAME|PKGVERSION)[:\}]`); m { mkline.Warnf("%s should not be used in %s as it includes the PKGREVISION. "+ @@ -1265,7 +1301,7 @@ func (ck MkLineChecker) checkVarassignLeftBsdPrefs() { G.Infrastructure || mkline.Op() != opAssignDefault || ck.MkLines.Tools.SeenPrefs || - !ck.MkLines.FirstTime("include bsd.prefs.mk before using ?=") { + !ck.MkLines.once.FirstTime("include bsd.prefs.mk before using ?=") { return } @@ -1457,14 +1493,14 @@ func (ck MkLineChecker) checkDirectiveCond() { checkVarUse := func(varuse *MkVarUse) { var vartype *Vartype // TODO: Insert a better type guess here. - vuc := VarUseContext{vartype, vucTimeLoad, VucQuotPlain, false} + vuc := VarUseContext{vartype, VucLoadTime, VucQuotPlain, false} ck.CheckVaruse(varuse, &vuc) } // Skip subconditions that have already been handled as part of the !(...). done := make(map[interface{}]bool) - checkNotEmpty := func(not MkCond) { + checkNotEmpty := func(not *MkCond) { empty := not.Empty if empty != nil { ck.checkDirectiveCondEmpty(empty, true, true, not == cond.Not) @@ -1683,7 +1719,7 @@ func (ck MkLineChecker) CheckRelativePath(relativePath string, mustExist bool) { abs := path.Dir(mkline.Filename) + "/" + resolvedPath if _, err := os.Stat(abs); err != nil { - if mustExist && !ck.MkLines.indentation.IsCheckedFile(resolvedPath) { + if mustExist && !ck.MkLines.indentation.HasExists(resolvedPath) { mkline.Errorf("Relative path %q does not exist.", resolvedPath) } return diff --git a/pkgtools/pkglint/files/mklinechecker_test.go b/pkgtools/pkglint/files/mklinechecker_test.go index 402bcf0f629..985942097b4 100644 --- a/pkgtools/pkglint/files/mklinechecker_test.go +++ b/pkgtools/pkglint/files/mklinechecker_test.go @@ -9,10 +9,10 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeft(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "_VARNAME=\tvalue") // Only to prevent "defined but not used". - mklines.vars.Use("_VARNAME", mklines.mklines[1], vucTimeRun) + mklines.vars.Use("_VARNAME", mklines.mklines[1], VucRunTime) mklines.Check() @@ -26,7 +26,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftNotUsed__procedure_call(c * t.CreateFileLines("mk/pkg-build-options.mk") mklines := t.SetUpFileMkLines("category/package/filename.mk", - MkRcsID, + MkCvsID, "", "pkgbase := glib2", ".include \"../../mk/pkg-build-options.mk\"", @@ -53,7 +53,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftNotUsed__procedure_call_no_ t.DisableTracing() // Just for code coverage t.CreateFileLines("mk/pkg-build-options.mk") mklines := t.SetUpFileMkLines("category/package/filename.mk", - MkRcsID, + MkCvsID, "", "pkgbase := glib2", ".include \"../../mk/pkg-build-options.mk\"") @@ -67,7 +67,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftNotUsed__infra(c *check.C) t := s.Init(c) t.CreateFileLines("mk/infra.mk", - MkRcsID, + MkCvsID, "#", "# Package-settable variables:", "#", @@ -98,8 +98,9 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeft__infrastructure(c *check.C t.SetUpPkgsrc() t.CreateFileLines("mk/infra.mk", - MkRcsID, - "_VARNAME=\tvalue") + MkCvsID, + "_VARNAME=\t\tvalue", + "_SORTED_VARS.group=\tVARNAME") t.FinishSetUp() G.Check(t.File("mk/infra.mk")) @@ -108,6 +109,20 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeft__infrastructure(c *check.C "WARN: ~/mk/infra.mk:2: _VARNAME is defined but not used.") } +func (s *Suite) Test_MkLineChecker_checkVarassignLeft__documented_underscore(c *check.C) { + t := s.Init(c) + + t.SetUpPkgsrc() + t.CreateFileLines("category/package/filename.mk", + MkCvsID, + "_SORTED_VARS.group=\tVARNAME") + t.FinishSetUp() + + G.Check(t.File("category/package/filename.mk")) + + t.CheckOutputEmpty() +} + func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable(c *check.C) { t := s.Init(c) @@ -125,7 +140,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable(c *check.C) { "COMMENTED_SAME?=\tdefault", // commented default, same value as default "COMMENTED_DIFF?=\tpkg") // commented default, differs from default value t.CreateFileLines("mk/defaults/mk.conf", - MkRcsID, + MkCvsID, "ASSIGN_DIFF?=default", "ASSIGN_DIFF2?=default", "ASSIGN_SAME?=default", @@ -161,7 +176,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable__before_prefs( "BEFORE=\tvalue", ".include \"../../mk/bsd.prefs.mk\"") t.CreateFileLines("mk/defaults/mk.conf", - MkRcsID, + MkCvsID, "BEFORE?=\tvalue") t.Chdir("category/package") t.FinishSetUp() @@ -184,7 +199,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable__after_prefs(c ".include \"../../mk/bsd.prefs.mk\"", "AFTER=\tvalue") t.CreateFileLines("mk/defaults/mk.conf", - MkRcsID, + MkCvsID, "AFTER?=\t\tvalue") t.Chdir("category/package") t.FinishSetUp() @@ -199,7 +214,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftUserSettable__vartype_nil(c t := s.Init(c) t.CreateFileLines("category/package/vars.mk", - MkRcsID, + MkCvsID, "#", "# User-settable variables:", "#", @@ -227,7 +242,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftBsdPrefs__vartype_nil(c *ch t := s.Init(c) mklines := t.NewMkLines("builtin.mk", - MkRcsID, + MkCvsID, "VAR_SH?=\tvalue") mklines.Check() @@ -243,7 +258,7 @@ func (s *Suite) Test_MkLineChecker_Check__url2pkg(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "# url2pkg-marker") mklines.Check() @@ -260,7 +275,7 @@ func (s *Suite) Test_MkLineChecker_Check__buildlink3_include_prefs(c *check.C) { t.CreateFileLines("mk/bsd.prefs.mk") t.CreateFileLines("mk/bsd.fast.prefs.mk") mklines := t.SetUpFileMkLines("category/package/buildlink3.mk", - MkRcsID, + MkCvsID, ".include \"../../mk/bsd.prefs.mk\"", ".include \"../../mk/bsd.fast.prefs.mk\"") @@ -287,7 +302,7 @@ func (s *Suite) Test_MkLineChecker_checkInclude(c *check.C) { t.CreateFileLines("devel/intltool/buildlink3.mk") t.CreateFileLines("devel/intltool/builtin.mk") mklines := t.SetUpFileMkLines("category/package/filename.mk", - MkRcsID, + MkCvsID, "", ".include \"../../pkgtools/x11-links/buildlink3.mk\"", ".include \"../../graphics/jpeg/buildlink3.mk\"", @@ -314,7 +329,7 @@ func (s *Suite) Test_MkLineChecker_checkInclude__Makefile(c *check.C) { t := s.Init(c) mklines := t.NewMkLines(t.File("Makefile"), - MkRcsID, + MkCvsID, ".include \"../../other/package/Makefile\"") mklines.Check() @@ -336,7 +351,7 @@ func (s *Suite) Test_MkLineChecker_checkInclude__Makefile_exists(c *check.C) { G.checkdirPackage(t.File("category/package")) t.CheckOutputLines( - "ERROR: ~/category/package/Makefile:20: Cannot read \"../../other/existing/Makefile\".") + "ERROR: ~/category/package/Makefile:21: Cannot read \"../../other/not-found/Makefile\".") } func (s *Suite) Test_MkLineChecker_checkInclude__hacks(c *check.C) { @@ -344,11 +359,11 @@ func (s *Suite) Test_MkLineChecker_checkInclude__hacks(c *check.C) { t.SetUpPackage("category/package") t.CreateFileLines("category/package/hacks.mk", - MkRcsID, + MkCvsID, ".include \"../../category/package/nonexistent.mk\"", ".include \"../../category/package/builtin.mk\"") t.CreateFileLines("category/package/builtin.mk", - MkRcsID) + MkCvsID) t.FinishSetUp() G.checkdirPackage(t.File("category/package")) @@ -366,7 +381,7 @@ func (s *Suite) Test_MkLineChecker__permissions_in_hacks_mk(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("hacks.mk", - MkRcsID, + MkCvsID, "OPSYS=\t${PKGREVISION}") mklines.Check() @@ -383,7 +398,7 @@ func (s *Suite) Test_MkLineChecker_checkDirective(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("category/package/filename.mk", - MkRcsID, + MkCvsID, "", ".for", ".endfor", @@ -422,7 +437,7 @@ func (s *Suite) Test_MkLineChecker_checkDirective__for_loop_varname(c *check.C) t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "", ".for VAR in a b c", // Should be lowercase. ".endfor", @@ -450,7 +465,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveEnd__ending_comments(c *check.C t.SetUpVartypes() mklines := t.NewMkLines("opsys.mk", - MkRcsID, + MkCvsID, "", ".for i in 1 2 3 4 5", ". if ${OPSYS} == NetBSD", @@ -488,12 +503,32 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveEnd__ending_comments(c *check.C "WARN: opsys.mk:24: Comment \"ii\" does not match loop \"jj in 1 2\".") } +// After removing the dummy indentation in commit d5a926af, +// there was a panic: runtime error: index out of range, +// in wip/jacorb-lib/buildlink3.mk. +func (s *Suite) Test_MkLineChecker_checkDirectiveEnd__unbalanced(c *check.C) { + t := s.Init(c) + + t.SetUpVartypes() + mklines := t.NewMkLines("filename.mk", + MkCvsID, + "", + ".endfor # comment", + ".endif # comment") + + mklines.Check() + + t.CheckOutputLines( + "ERROR: filename.mk:3: Unmatched .endfor.", + "ERROR: filename.mk:4: Unmatched .endif.") +} + func (s *Suite) Test_MkLineChecker_checkDirectiveFor(c *check.C) { t := s.Init(c) t.SetUpVartypes() mklines := t.NewMkLines("for.mk", - MkRcsID, + MkCvsID, ".for dir in ${PATH:C,:, ,g}", ".endfor", "", @@ -523,7 +558,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveFor__infrastructure(c *check.C) t.SetUpPkgsrc() t.CreateFileLines("mk/file.mk", - MkRcsID, + MkCvsID, ".for i = 1 2 3", // The "=" should rather be "in". ".endfor", "", @@ -544,7 +579,7 @@ func (s *Suite) Test_MkLineChecker_checkDependencyRule(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("category/package/filename.mk", - MkRcsID, + MkCvsID, "", ".PHONY: target-1", "target-2: .PHONY", @@ -573,7 +608,7 @@ func (s *Suite) Test_MkLineChecker_checkVartype__simple_type(c *check.C) { c.Check(vartype.List(), equals, false) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "COMMENT=\tA nice package") mklines.Check() @@ -586,7 +621,7 @@ func (s *Suite) Test_MkLineChecker_checkVartype(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "DISTNAME=\tgcc-${GCC_VERSION}") mklines.vars.Define("GCC_VERSION", mklines.mklines[1]) @@ -600,7 +635,7 @@ func (s *Suite) Test_MkLineChecker_checkVartype__append_to_non_list(c *check.C) t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "DISTNAME+=\tsuffix", "COMMENT=\tComment for", "COMMENT+=\tthe package") @@ -618,7 +653,7 @@ func (s *Suite) Test_MkLineChecker_checkVartype__no_tracing(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "UNKNOWN=\tvalue", "CUR_DIR!=\tpwd") t.DisableTracing() @@ -638,7 +673,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassign__URL_with_shell_special_charac G.Pkg = NewPackage(t.File("graphics/gimp-fix-ca")) t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "MASTER_SITES=\thttp://registry.gimp.org/file/fix-ca.c?action=download&id=9884&file=") mklines.Check() @@ -653,7 +688,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassign__list(c *check.C) { t.SetUpVartypes() t.SetUpCommandLine("-Wall", "--explain") mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "SITES.distfile=\t-${MASTER_SITE_GITHUB:=project/}") mklines.Check() @@ -688,7 +723,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveCond(c *check.C) { test := func(cond string, output ...string) { mklines := t.NewMkLines("filename.mk", cond) - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { MkLineChecker{mklines, mkline}.checkDirectiveCond() }) t.CheckOutput(output) @@ -759,17 +794,17 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveCond(c *check.C) { "TRACE: 1 + (*MkParser).mkCondAtom(\"${VAR:Mpattern1:Mpattern2} == comparison\")", "TRACE: 1 - (*MkParser).mkCondAtom(\"${VAR:Mpattern1:Mpattern2} == comparison\")", "TRACE: 1 checkCompareVarStr ${VAR:Mpattern1:Mpattern2} == comparison", - "TRACE: 1 + MkLineChecker.CheckVaruse(filename.mk:1, ${VAR:Mpattern1:Mpattern2}, (no-type time:parse quoting:plain wordpart:false))", + "TRACE: 1 + MkLineChecker.CheckVaruse(filename.mk:1, ${VAR:Mpattern1:Mpattern2}, (no-type time:load quoting:plain wordpart:false))", "TRACE: 1 2 + (*Pkgsrc).VariableType(\"VAR\")", "TRACE: 1 2 3 No type definition found for \"VAR\".", "TRACE: 1 2 - (*Pkgsrc).VariableType(\"VAR\", \"=>\", (*pkglint.Vartype)(nil))", "WARN: filename.mk:1: VAR is used but not defined.", - "TRACE: 1 2 + MkLineChecker.checkVarusePermissions(\"VAR\", (no-type time:parse quoting:plain wordpart:false))", + "TRACE: 1 2 + MkLineChecker.checkVarusePermissions(\"VAR\", (no-type time:load quoting:plain wordpart:false))", "TRACE: 1 2 3 No type definition found for \"VAR\".", - "TRACE: 1 2 - MkLineChecker.checkVarusePermissions(\"VAR\", (no-type time:parse quoting:plain wordpart:false))", - "TRACE: 1 2 + (*MkLineImpl).VariableNeedsQuoting(${VAR:Mpattern1:Mpattern2}, (*pkglint.Vartype)(nil), (no-type time:parse quoting:plain wordpart:false))", - "TRACE: 1 2 - (*MkLineImpl).VariableNeedsQuoting(${VAR:Mpattern1:Mpattern2}, (*pkglint.Vartype)(nil), (no-type time:parse quoting:plain wordpart:false), \"=>\", unknown)", - "TRACE: 1 - MkLineChecker.CheckVaruse(filename.mk:1, ${VAR:Mpattern1:Mpattern2}, (no-type time:parse quoting:plain wordpart:false))", + "TRACE: 1 2 - MkLineChecker.checkVarusePermissions(\"VAR\", (no-type time:load quoting:plain wordpart:false))", + "TRACE: 1 2 + (*MkLine).VariableNeedsQuoting(${VAR:Mpattern1:Mpattern2}, (*pkglint.Vartype)(nil), (no-type time:load quoting:plain wordpart:false))", + "TRACE: 1 2 - (*MkLine).VariableNeedsQuoting(${VAR:Mpattern1:Mpattern2}, (*pkglint.Vartype)(nil), (no-type time:load quoting:plain wordpart:false), \"=>\", unknown)", + "TRACE: 1 - MkLineChecker.CheckVaruse(filename.mk:1, ${VAR:Mpattern1:Mpattern2}, (no-type time:load quoting:plain wordpart:false))", "TRACE: - MkLineChecker.checkDirectiveCond(\"${VAR:Mpattern1:Mpattern2} == comparison\")", "TRACE: + (*MkParser).mkCondAtom(\"${VAR:Mpattern1:Mpattern2} == comparison\")", "TRACE: - (*MkParser).mkCondAtom(\"${VAR:Mpattern1:Mpattern2} == comparison\")", @@ -783,7 +818,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassign(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "ac_cv_libpari_libs+=\t-L${BUILDLINK_PREFIX.pari}/lib") // From math/clisp-pari/Makefile, rev. 1.8 mklines.Check() @@ -803,7 +838,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions(c *check.C) { "options.mk: set", "*.mk: default, set") mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, "PKG_DEVELOPER?=\tyes", "BUILD_DEFS?=\tVARBASE", "USE_TOOLS:=\t${USE_TOOLS:Nunwanted-tool}", @@ -842,7 +877,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions__no_tracing(c * t.SetUpVartypes() t.DisableTracing() // Just to reach branch coverage for unknown permissions. mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, "COMMENT=\tShort package description") mklines.Check() @@ -858,7 +893,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions__license_defaul t.SetUpPkgsrc() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "LICENSE?=\tgnu-gpl-v2") t.FinishSetUp() @@ -877,7 +912,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftPermissions__infrastructure t.SetUpVartypes() t.CreateFileLines("mk/infra.mk", - MkRcsID, + MkCvsID, "", "PKG_DEVELOPER?=\tyes") t.CreateFileLines("mk/bsd.pkg.mk") @@ -901,13 +936,13 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftRationale(c *check.C) { t.CheckOutput(diagnostics) } test := func(lines []string, diagnostics ...string) { - testLines(append([]string{MkRcsID, ""}, lines...), diagnostics...) + testLines(append([]string{MkCvsID, ""}, lines...), diagnostics...) } lines := func(lines ...string) []string { return lines } test( lines( - MkRcsID, + MkCvsID, "ONLY_FOR_PLATFORM=\t*-*-*", // The CVS Id above is not a rationale. "NOT_FOR_PLATFORM=\t*-*-*", // Neither does this line have a rationale. ), @@ -962,7 +997,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftRationale(c *check.C) { lines( "NOT_FOR_PLATFORM=\t*-*-*", "NOT_FOR_PLATFORM=\t*-*-*"), - sprintf("ERROR: filename.mk:1: Expected %q.", MkRcsID), + sprintf("ERROR: filename.mk:1: Expected %q.", MkCvsID), "WARN: filename.mk:1: Setting variable NOT_FOR_PLATFORM should have a rationale.", "WARN: filename.mk:2: Setting variable NOT_FOR_PLATFORM should have a rationale.") @@ -971,7 +1006,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignLeftRationale(c *check.C) { test( lines( - MkRcsID, + MkCvsID, "ONLY_FOR_PLATFORM=\t*-*-*", // The CVS Id above is not a rationale. "NOT_FOR_PLATFORM=\t*-*-*", // Neither does this line have a rationale. ), @@ -987,7 +1022,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignOpShell(c *check.C) { t.SetUpPackage("category/package", ".include \"standalone.mk\"") t.CreateFileLines("category/package/standalone.mk", - MkRcsID, + MkCvsID, "", ".include \"../../mk/bsd.prefs.mk\"", "", @@ -1060,7 +1095,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignRightVaruse(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "PLIST_SUBST+=\tLOCALBASE=${LOCALBASE:Q}") mklines.Check() @@ -1075,7 +1110,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, "COMMENT=\t${GAMES_USER}", "COMMENT:=\t${PKGBASE}", "PYPKGPREFIX=\t${PKGBASE}") @@ -1096,7 +1131,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__explain(c *check.C) { t.SetUpCommandLine("-Wall", "--explain") t.SetUpVartypes() mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, "COMMENT=\t${GAMES_USER}", "COMMENT:=\t${PKGBASE}", "PYPKGPREFIX=\t${PKGBASE}") @@ -1149,7 +1184,7 @@ func (s *Suite) Test_MkLineChecker_explainPermissions(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "AUTO_MKDIRS=\tyes") mklines.Check() @@ -1181,7 +1216,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time(c *check.C) t.SetUpVartypes() mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, "WRKSRC:=${.CURDIR}", ".if ${PKG_SYSCONFDIR.gdm} != \"etc\"", ".endif") @@ -1205,7 +1240,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_in_conditio "special:filename.mk: use") mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, ".if ${LOAD_TIME} && ${RUN_TIME}", ".endif") @@ -1224,7 +1259,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_in_for_loop "special:filename.mk: use") mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, ".for pattern in ${LOAD_TIME} ${RUN_TIME}", ".endfor") @@ -1240,7 +1275,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_guessed(c * t.SetUpVartypes() t.SetUpTool("install", "", AtRunTime) mklines := t.NewMkLines("install-docfiles.mk", - MkRcsID, + MkCvsID, "DOCFILES=\ta b c", "do-install:", ".for f in ${DOCFILES}", @@ -1280,7 +1315,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time_run_time(c "*.mk: set") mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, ".if ${LOAD_TIME} && ${RUN_TIME} && ${WRITE_ONLY}", ".elif ${LOAD_TIME_ELSEWHERE} && ${RUN_TIME_ELSEWHERE}", ".endif") @@ -1304,7 +1339,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__PKGREVISION(c *check. t.SetUpVartypes() mklines := t.NewMkLines("any.mk", - MkRcsID, + MkCvsID, ".if defined(PKGREVISION)", ".endif") @@ -1321,7 +1356,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__indirectly(c *check.C t.SetUpVartypes() mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, "IGNORE_PKG.package=\t${ONLY_FOR_UNPRIVILEGED}") mklines.Check() @@ -1338,7 +1373,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__indirectly_tool(c *ch t.SetUpVartypes() mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, "USE_TOOLS+=\t${PKGREVISION}") mklines.Check() @@ -1352,7 +1387,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__write_only_usable_in_ t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "VAR=\t${VAR} ${AUTO_MKDIRS}") mklines.Check() @@ -1370,7 +1405,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__usable_only_at_loadti G.Pkgsrc.vartypes.DefineParse("VAR", BtFileName, NoVartypeOptions, "*: set, use-loadtime") mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "VAR=\t${VAR}") mklines.Check() @@ -1392,7 +1427,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__assigned_to_infrastru "buildlink3.mk: none", "*: use") mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "INFRA=\t${VAR}") mklines.Check() @@ -1429,7 +1464,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__assigned_to_load_time "buildlink3.mk: none", "*.mk: use") mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "LOAD_TIME=\t${VAR}") mklines.Check() @@ -1444,7 +1479,7 @@ func (s *Suite) Test_MkLineChecker_checkVarusePermissions__multiple_times_per_fi t.SetUpVartypes() mklines := t.NewMkLines("buildlink3.mk", - MkRcsID, + MkCvsID, "VAR=\t${VAR} ${AUTO_MKDIRS} ${AUTO_MKDIRS} ${PKGREVISION} ${PKGREVISION}", "VAR=\t${VAR} ${AUTO_MKDIRS} ${AUTO_MKDIRS} ${PKGREVISION} ${PKGREVISION}") @@ -1466,9 +1501,14 @@ func (s *Suite) Test_MkLineChecker_warnVarusePermissions__not_directly_and_no_al t.SetUpVartypes() mklines := t.NewMkLines("mk-c.mk", - MkRcsID, + MkCvsID, "", - "TOOL_DEPENDS+=\t${BUILDLINK_API_DEPENDS.mk-c}:${BUILDLINK_PKGSRCDIR.mk-c}") + "# GUESSED_FLAGS", + "#\tDocumented here to suppress the \"defined but not used\"", + "#\twarning.", + "", + "TOOL_DEPENDS+=\t${BUILDLINK_API_DEPENDS.mk-c}:${BUILDLINK_PKGSRCDIR.mk-c}", + "GUESSED_FLAGS+=\t${BUILDLINK_CPPFLAGS}") mklines.Check() @@ -1484,9 +1524,9 @@ func (s *Suite) Test_MkLineChecker_warnVarusePermissions__not_directly_and_no_al t.Check(apiDependsType.AlternativeFiles(aclpUseLoadtime), equals, "buildlink3.mk or builtin.mk only") t.CheckOutputLines( - "WARN: mk-c.mk:3: BUILDLINK_API_DEPENDS.mk-c should not be used in any file.", - "WARN: mk-c.mk:3: The list variable BUILDLINK_API_DEPENDS.mk-c should not be embedded in a word.", - "WARN: mk-c.mk:3: BUILDLINK_PKGSRCDIR.mk-c should not be used in any file.") + "WARN: mk-c.mk:7: BUILDLINK_API_DEPENDS.mk-c should not be used in any file.", + "WARN: mk-c.mk:7: The list variable BUILDLINK_API_DEPENDS.mk-c should not be embedded in a word.", + "WARN: mk-c.mk:7: BUILDLINK_PKGSRCDIR.mk-c should not be used in any file.") } func (s *Suite) Test_MkLineChecker_checkVarassignDecreasingVersions(c *check.C) { @@ -1494,7 +1534,7 @@ func (s *Suite) Test_MkLineChecker_checkVarassignDecreasingVersions(c *check.C) t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "PYTHON_VERSIONS_ACCEPTED=\t36 __future__ # rationale", "PYTHON_VERSIONS_ACCEPTED=\t36 -13 # rationale", "PYTHON_VERSIONS_ACCEPTED=\t36 ${PKGVERSION_NOREV} # rationale", @@ -1534,7 +1574,7 @@ func (s *Suite) Test_MkLineChecker_warnVaruseToolLoadTime(c *check.C) { t.SetUpTool("after-prefs", "AFTER_PREFS", AfterPrefsMk) t.SetUpTool("at-runtime", "AT_RUNTIME", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, ".if ${NOWHERE} && ${AFTER_PREFS} && ${AT_RUNTIME} && ${MK_TOOL}", ".endif", "", @@ -1566,7 +1606,7 @@ func (s *Suite) Test_MkLineChecker_warnVaruseToolLoadTime__local_tool(c *check.C t.SetUpVartypes() t.CreateFileLines("mk/bsd.prefs.mk") mklines := t.SetUpFileMkLines("category/package/Makefile", - MkRcsID, + MkCvsID, ".include \"../../mk/bsd.prefs.mk\"", "", "TOOLS_CREATE+=\t\tmk-tool", @@ -1588,7 +1628,7 @@ func (s *Suite) Test_MkLineChecker_Check__warn_varuse_LOCALBASE(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, "PKGNAME=\t${LOCALBASE}") mklines.Check() @@ -1607,7 +1647,7 @@ func (s *Suite) Test_MkLineChecker_CheckRelativePkgdir(c *check.C) { mklines := t.SetUpFileMkLines("category/package/Makefile", "# dummy") - checkRelativePkgdir := func(mkline MkLine) { + checkRelativePkgdir := func(mkline *MkLine) { MkLineChecker{mklines, mkline}.CheckRelativePkgdir(relativePkgdir) } @@ -1632,7 +1672,7 @@ func (s *Suite) Test_MkLineChecker__unclosed_varuse(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d") mklines.Check() @@ -1678,7 +1718,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparison_with_shell_com t.SetUpVartypes() mklines := t.NewMkLines("security/openssl/Makefile", - MkRcsID, + MkCvsID, ".if ${PKGSRC_COMPILER} == \"gcc\" && ${CC} == \"cc\"", ".endif") @@ -1697,7 +1737,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveCond__compare_pattern_with_empt t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, ".if ${X11BASE:Npattern} == \"\"", ".endif", "", @@ -1725,7 +1765,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveCondEmpty(c *check.C) { test := func(before string, diagnosticsAndAfter ...string) { mklines := t.SetUpFileMkLines("module.mk", - MkRcsID, + MkCvsID, before, ".endif") ck := MkLineChecker{mklines, mklines.mklines[1]} @@ -1942,7 +1982,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparing_PKGSRC_COMPILER t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, ".if ${PKGSRC_COMPILER} == \"clang\"", ".elif ${PKGSRC_COMPILER} != \"gcc\"", ".endif") @@ -1974,7 +2014,7 @@ func (s *Suite) Test_MkLineChecker_checkVartype__CFLAGS_with_backticks(c *check. t.SetUpVartypes() mklines := t.NewMkLines("chat/pidgin-icb/Makefile", - MkRcsID, + MkCvsID, "CFLAGS+=\t`pkg-config pidgin --cflags`") mkline := mklines.mklines[1] @@ -1997,14 +2037,14 @@ func (s *Suite) Test_MkLineChecker_checkVartype__CFLAGS(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "CPPFLAGS.SunOS+=\t-DPIPECOMMAND=\\\"/usr/sbin/sendmail -bs %s\\\"") mklines.Check() t.CheckOutputLines( - "WARN: Makefile:2: Unknown compiler flag \"-bs\".", - "WARN: Makefile:2: Compiler flag \"%s\\\\\\\"\" should start with a hyphen.") + "WARN: Makefile:2: Compiler flag \"-DPIPECOMMAND=\\\\\\\"/usr/sbin/sendmail\" has unbalanced double quotes.", + "WARN: Makefile:2: Compiler flag \"%s\\\\\\\"\" has unbalanced double quotes.") } func (s *Suite) Test_MkLineChecker_checkDirectiveIndentation__autofix(c *check.C) { @@ -2012,7 +2052,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveIndentation__autofix(c *check.C t.SetUpCommandLine("--autofix", "-Wspace") lines := t.SetUpFileLines("filename.mk", - MkRcsID, + MkCvsID, ".if defined(A)", ".for a in ${A}", ".if defined(C)", @@ -2046,7 +2086,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveIndentation__autofix_multiline( t.SetUpCommandLine("-Wall", "--autofix") t.SetUpVartypes() mklines := t.SetUpFileMkLines("options.mk", - MkRcsID, + MkCvsID, ".if ${PKGNAME} == pkgname", ".if \\", " ${PLATFORM:MNetBSD-4.*}", @@ -2060,7 +2100,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveIndentation__autofix_multiline( "AUTOFIX: ~/options.mk:5: Replacing \".\" with \". \".") t.CheckFileLines("options.mk", - MkRcsID, + MkCvsID, ".if ${PKGNAME} == pkgname", ". if \\", " ${PLATFORM:MNetBSD-4.*}", @@ -2073,7 +2113,7 @@ func (s *Suite) Test_MkLineChecker_checkVarUseQuoting(c *check.C) { t.SetUpVartypes() mklines := t.SetUpFileMkLines("options.mk", - MkRcsID, + MkCvsID, "GOPATH=\t${WRKDIR}", "do-build:", "\tcd ${WRKSRC} && GOPATH=${GOPATH} PATH=${PATH} :") @@ -2097,7 +2137,7 @@ func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__mstar(c *check.C) { t.SetUpCommandLine("-Wall,no-space") t.SetUpVartypes() mklines := t.SetUpFileMkLines("options.mk", - MkRcsID, + MkCvsID, "CONFIGURE_ARGS+= CFLAGS=${CFLAGS:Q}", "CONFIGURE_ARGS+= CFLAGS=${CFLAGS:M*:Q}", "CONFIGURE_ARGS+= ADA_FLAGS=${ADA_FLAGS:Q}", @@ -2146,6 +2186,79 @@ func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__q_not_needed(c *check.C) "NOTE: ~/category/package/Makefile:6: The :Q operator isn't necessary for ${HOMEPAGE} here.") } +func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__undefined_list_in_word_in_shell_command(c *check.C) { + t := s.Init(c) + + pkg := t.SetUpPackage("category/package", + "\t${ECHO} ./${DISTFILES}") + t.FinishSetUp() + + G.Check(pkg) + + // The variable DISTFILES is declared by the infrastructure. + // It is not defined by this package, therefore it doesn't + // appear in the RedundantScope. + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:20: The list variable DISTFILES should not be embedded in a word.") +} + +func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__list_variable_with_single_constant_value(c *check.C) { + t := s.Init(c) + + pkg := t.SetUpPackage("category/package", + "BUILD_DIRS=\tonly-dir", + "", + "do-install:", + "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/") + t.FinishSetUp() + + G.Check(pkg) + + // Don't warn here since BUILD_DIRS, although being a list + // variable, contains only a single value. + t.CheckOutputEmpty() +} + +func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__list_variable_with_single_conditional_value(c *check.C) { + t := s.Init(c) + + pkg := t.SetUpPackage("category/package", + "BUILD_DIRS=\tonly-dir", + ".if 0", + "BUILD_DIRS=\tother-dir", + ".endif", + "", + "do-install:", + "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/") + t.FinishSetUp() + + G.Check(pkg) + + // TODO: Don't warn here since BUILD_DIRS, although being a list + // variable, contains only a single value. + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:26: " + + "The list variable BUILD_DIRS should not be embedded in a word.") +} + +func (s *Suite) Test_MkLineChecker_checkVarUseQuoting__list_variable_with_two_constant_words(c *check.C) { + t := s.Init(c) + + pkg := t.SetUpPackage("category/package", + "BUILD_DIRS=\tfirst-dir second-dir", + "", + "do-install:", + "\t${INSTALL_PROGRAM} ${WRKSRC}/${BUILD_DIRS}/program ${DESTDIR}${PREFIX}/bin/") + t.FinishSetUp() + + G.Check(pkg) + + // Since BUILD_DIRS consists of two words, it would destroy the installation command. + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:23: " + + "The list variable BUILD_DIRS should not be embedded in a word.") +} + // 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) { @@ -2154,7 +2267,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__eq_nonlist(c *check.C) { t.SetUpVartypes() t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/") mklines := t.SetUpFileMkLines("options.mk", - MkRcsID, + MkCvsID, "WRKSRC=\t\t${WRKDIR:=/subdir}", "MASTER_SITES=\t${MASTER_SITE_GITHUB:=organization/}") @@ -2170,7 +2283,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__for(c *check.C) { t.SetUpVartypes() t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/") mklines := t.SetUpFileMkLines("options.mk", - MkRcsID, + MkCvsID, ".for var in a b c", "\t: ${var}", ".endfor") @@ -2190,12 +2303,12 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__varcanon(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("mk/sys-vars.mk", - MkRcsID, + MkCvsID, "CPPPATH.Linux=\t/usr/bin/cpp") t.FinishSetUp() mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "COMMENT=\t${CPPPATH.SunOS}") ck := MkLineChecker{mklines, mklines.mklines[1]} @@ -2206,7 +2319,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__varcanon(c *check.C) { options: Guessed, aclEntries: nil, }, - time: vucTimeRun, + time: VucRunTime, quoting: VucQuotPlain, IsWordPart: false, }) @@ -2223,11 +2336,11 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__defined_in_infrastructure(c *che t.SetUpPkgsrc() t.CreateFileLines("mk/deeply/nested/infra.mk", - MkRcsID, + MkCvsID, "INFRA_VAR?=\tvalue") t.FinishSetUp() mklines := t.SetUpFileMkLines("category/package/module.mk", - MkRcsID, + MkCvsID, "do-fetch:", "\t: ${INFRA_VAR} ${UNDEFINED}") @@ -2249,7 +2362,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__build_defs(c *check.C) { t.FinishSetUp() mklines := t.SetUpFileMkLines("options.mk", - MkRcsID, + MkCvsID, "COMMENT= ${VARBASE} ${X11_TYPE}", "PKG_FAIL_REASON+= ${VARBASE} ${X11_TYPE}", "BUILD_DEFS+= X11_TYPE") @@ -2267,7 +2380,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__LOCALBASE_in_infrastructure(c *c t.SetUpPkgsrc() t.CreateFileLines("mk/infra.mk", - MkRcsID, + MkCvsID, "LOCALBASE?=\t${PREFIX}", "DEFAULT_PREFIX=\t${LOCALBASE}") t.FinishSetUp() @@ -2291,7 +2404,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__user_defined_variable_and_BUILD_ "VARBASE?=\t${PREFIX}/var", "PYTHON_VER?=\t36") mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, "BUILD_DEFS+=\tPYTHON_VER", "\t: ${VARBASE}", "\t: ${VARBASE}", @@ -2309,7 +2422,7 @@ func (s *Suite) Test_MkLineChecker_checkVaruseModifiersSuffix(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, "\t: ${HOMEPAGE:=subdir/:Q}", // wrong "\t: ${BUILD_DIRS:=subdir/}", // correct "\t: ${BIN_PROGRAMS:=.exe}") // unknown since BIN_PROGRAMS doesn't have a type @@ -2327,7 +2440,7 @@ func (s *Suite) Test_MkLineChecker_checkVaruseModifiersRange(c *check.C) { t.SetUpCommandLine("--show-autofix", "--source") t.SetUpVartypes() mklines := t.NewMkLines("mk/compiler/gcc.mk", - MkRcsID, + MkCvsID, "CC:=\t${CC:C/^/_asdf_/1:M_asdf_*:S/^_asdf_//}") mklines.Check() @@ -2342,7 +2455,7 @@ func (s *Suite) Test_MkLineChecker_checkVaruseModifiersRange(c *check.C) { // Now go through all the "almost" cases, to reach full branch coverage. mklines = t.NewMkLines("gcc.mk", - MkRcsID, + MkCvsID, "\t: ${CC:M1:M2:M3}", "\t: ${CC:C/^begin//:M2:M3}", // M1 pattern not exactly ^ "\t: ${CC:C/^/_asdf_/g:M2:M3}", // M1 options != "1" @@ -2366,7 +2479,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__deprecated_PKG_DEBUG(c *check.C) G.Pkgsrc.initDeprecatedVars() mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "\t${_PKG_SILENT}${_PKG_DEBUG} :") mklines.Check() @@ -2380,7 +2493,7 @@ func (s *Suite) Test_MkLineChecker_checkVaruseUndefined(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("mk/infra.mk", - MkRcsID, + MkCvsID, "#", "# User-settable variables:", "#", @@ -2391,7 +2504,7 @@ func (s *Suite) Test_MkLineChecker_checkVaruseUndefined(c *check.C) { t.FinishSetUp() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "", "do-build:", "\t: ${ASSIGNED} ${COMMENTED} ${DOCUMENTED} ${UNKNOWN}") @@ -2408,7 +2521,7 @@ func (s *Suite) Test_MkLineChecker_checkVaruseUndefined__indirect_variables(c *c t.SetUpTool("echo", "ECHO", AfterPrefsMk) mklines := t.NewMkLines("net/uucp/Makefile", - MkRcsID, + MkCvsID, "\techo ${UUCP_${var}}") mklines.Check() @@ -2433,7 +2546,7 @@ func (s *Suite) Test_MkLineChecker_checkVaruseUndefined__documented(c *check.C) t.SetUpVartypes() mklines := t.NewMkLines("interpreter.mk", - MkRcsID, + MkCvsID, "#", "# Package-settable variables:", "#", @@ -2457,13 +2570,15 @@ func (s *Suite) Test_MkLineChecker_checkVarassignMisc(c *check.C) { t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://download.github.com/") t.SetUpCommandLine("-Wall,no-space") mklines := t.SetUpFileMkLines("module.mk", - MkRcsID, + MkCvsID, "EGDIR= ${PREFIX}/etc/rc.d", "RPMIGNOREPATH+= ${PREFIX}/etc/rc.d", "_TOOLS_VARNAME.sed= SED", "DIST_SUBDIR= ${PKGNAME}", "WRKSRC= ${PKGNAME}", - "SITES_distfile.tar.gz= ${MASTER_SITE_GITHUB:=user/}") + "SITES_distfile.tar.gz= ${MASTER_SITE_GITHUB:=user/}", + "MASTER_SITES= https://cdn.example.org/${PKGNAME}/", + "MASTER_SITES= https://cdn.example.org/distname-${PKGVERSION}/") t.FinishSetUp() mklines.Check() @@ -2476,7 +2591,9 @@ func (s *Suite) Test_MkLineChecker_checkVarassignMisc(c *check.C) { "WARN: ~/module.mk:5: PKGNAME should not be used in DIST_SUBDIR as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.", "WARN: ~/module.mk:6: PKGNAME should not be used in WRKSRC as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.", "WARN: ~/module.mk:7: SITES_distfile.tar.gz is defined but not used.", - "WARN: ~/module.mk:7: SITES_* is deprecated. Please use SITES.* instead.") + "WARN: ~/module.mk:7: SITES_* is deprecated. Please use SITES.* instead.", + "WARN: ~/module.mk:8: PKGNAME should not be used in MASTER_SITES as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.", + "WARN: ~/module.mk:9: PKGVERSION should not be used in MASTER_SITES as it includes the PKGREVISION. Please use PKGVERSION_NOREV instead.") } func (s *Suite) Test_MkLineChecker_checkVarassignMisc__multiple_inclusion_guards(c *check.C) { @@ -2484,18 +2601,18 @@ func (s *Suite) Test_MkLineChecker_checkVarassignMisc__multiple_inclusion_guards t.SetUpPkgsrc() t.CreateFileLines("filename.mk", - MkRcsID, + MkCvsID, ".if !defined(FILENAME_MK)", "FILENAME_MK=\t# defined", ".endif") t.CreateFileLines("Makefile.common", - MkRcsID, + MkCvsID, ".if !defined(MAKEFILE_COMMON)", "MAKEFILE_COMMON=\t# defined", "", ".endif") t.CreateFileLines("other.mk", - MkRcsID, + MkCvsID, "COMMENT=\t# defined") t.FinishSetUp() @@ -2517,7 +2634,7 @@ func (s *Suite) Test_MkLineChecker_checkText(c *check.C) { t.SetUpCommandLine("-Wall,no-space") mklines := t.SetUpFileMkLines("module.mk", - MkRcsID, + MkCvsID, "CFLAGS+= -Wl,--rpath,${PREFIX}/lib", "PKG_FAIL_REASON+= \"Group ${GAMEGRP} doesn't exist.\"") t.FinishSetUp() @@ -2534,7 +2651,7 @@ func (s *Suite) Test_MkLineChecker_checkText__WRKSRC(c *check.C) { t.SetUpCommandLine("-Wall", "--explain") mklines := t.SetUpFileMkLines("module.mk", - MkRcsID, + MkCvsID, "pre-configure:", "\tcd ${WRKSRC}/..") @@ -2566,7 +2683,7 @@ func (s *Suite) Test_MkLineChecker_CheckRelativePath(c *check.C) { t.CreateFileLines("wip/package/Makefile") t.CreateFileLines("wip/package/module.mk") mklines := t.SetUpFileMkLines("category/package/module.mk", - MkRcsID, + MkCvsID, "DEPENDS+= wip-package-[0-9]*:../../wip/package", ".include \"../../wip/package/module.mk\"", "", @@ -2603,7 +2720,7 @@ func (s *Suite) Test_MkLineChecker_CheckRelativePath__absolute_path(c *check.C) t.SetUpPkgsrc() mklines := t.SetUpFileMkLines("category/package/module.mk", - MkRcsID, + MkCvsID, "DISTINFO_FILE=\t"+absPath) t.FinishSetUp() @@ -2617,7 +2734,7 @@ func (s *Suite) Test_MkLineChecker_CheckRelativePath__include_if_exists(c *check t := s.Init(c) mklines := t.SetUpFileMkLines("filename.mk", - MkRcsID, + MkCvsID, ".include \"included.mk\"", ".sinclude \"included.mk\"") @@ -2632,9 +2749,9 @@ func (s *Suite) Test_MkLineChecker_CheckRelativePath__wip_mk(c *check.C) { t := s.Init(c) t.CreateFileLines("wip/mk/git-package.mk", - MkRcsID) + MkCvsID) t.CreateFileLines("wip/other/version.mk", - MkRcsID) + MkCvsID) t.SetUpPackage("wip/package", ".include \"../mk/git-package.mk\"", ".include \"../other/version.mk\"") diff --git a/pkgtools/pkglint/files/mklines.go b/pkgtools/pkglint/files/mklines.go index bb578adcdf6..b140b536ded 100644 --- a/pkgtools/pkglint/files/mklines.go +++ b/pkgtools/pkglint/files/mklines.go @@ -5,28 +5,26 @@ import ( ) // MkLines contains data for the Makefile (or *.mk) that is currently checked. -type MkLines = *MkLinesImpl - -type MkLinesImpl struct { - mklines []MkLine - lines Lines - target string // Current make(1) target; only available during checkAll - vars Scope // - buildDefs map[string]bool // Variables that are registered in BUILD_DEFS, to ensure that all user-defined variables are added to it. - plistVarAdded map[string]MkLine // Identifiers that are added to PLIST_VARS. - plistVarSet map[string]MkLine // Identifiers for which PLIST.${id} is defined. - plistVarSkip bool // True if any of the PLIST_VARS identifiers refers to a variable. - Tools *Tools // Tools defined in file scope. - indentation *Indentation // Indentation depth of preprocessing directives; only available during MkLines.ForEach. - forVars map[string]bool // The variables currently used in .for loops; only available during MkLines.checkAll. - Once +type MkLines struct { + mklines []*MkLine + lines *Lines + target string // Current make(1) target; only available during checkAll + vars Scope // + buildDefs map[string]bool // Variables that are registered in BUILD_DEFS, to ensure that all user-defined variables are added to it. + plistVarAdded map[string]*MkLine // Identifiers that are added to PLIST_VARS. + plistVarSet map[string]*MkLine // Identifiers for which PLIST.${id} is defined. + plistVarSkip bool // True if any of the PLIST_VARS identifiers refers to a variable. + Tools *Tools // Tools defined in file scope. + indentation *Indentation // Indentation depth of preprocessing directives; only available during MkLines.ForEach. + forVars map[string]bool // The variables currently used in .for loops; only available during MkLines.checkAll. + once Once // TODO: Consider extracting plistVarAdded, plistVarSet, plistVarSkip into an own type. // TODO: Describe where each of the above fields is valid. } -func NewMkLines(lines Lines) MkLines { - mklines := make([]MkLine, lines.Len()) +func NewMkLines(lines *Lines) *MkLines { + mklines := make([]*MkLine, lines.Len()) for i, line := range lines.Lines { mklines[i] = MkLineParser{}.Parse(line) } @@ -34,14 +32,14 @@ func NewMkLines(lines Lines) MkLines { tools := NewTools() tools.Fallback(G.Pkgsrc.Tools) - return &MkLinesImpl{ + return &MkLines{ mklines, lines, "", NewScope(), make(map[string]bool), - make(map[string]MkLine), - make(map[string]MkLine), + make(map[string]*MkLine), + make(map[string]*MkLine), false, tools, nil, @@ -74,16 +72,16 @@ func NewMkLines(lines Lines) MkLines { // UseVar remembers that the given variable is used in the given line. // This controls the "defined but not used" warning. -func (mklines *MkLinesImpl) UseVar(mkline MkLine, varname string, time vucTime) { +func (mklines *MkLines) UseVar(mkline *MkLine, varname string, time VucTime) { mklines.vars.Use(varname, mkline, time) if G.Pkg != nil { G.Pkg.vars.Use(varname, mkline, time) } } -func (mklines *MkLinesImpl) Check() { +func (mklines *MkLines) Check() { if trace.Tracing { - defer trace.Call1(mklines.lines.FileName)() + defer trace.Call1(mklines.lines.Filename)() } // In the first pass, all additions to BUILD_DEFS and USE_TOOLS @@ -99,7 +97,7 @@ func (mklines *MkLinesImpl) Check() { SaveAutofixChanges(mklines.lines) } -func (mklines *MkLinesImpl) checkAll() { +func (mklines *MkLines) checkAll() { allowedTargets := map[string]bool{ "pre-fetch": true, "do-fetch": true, "post-fetch": true, "pre-extract": true, "do-extract": true, "post-extract": true, @@ -113,13 +111,13 @@ func (mklines *MkLinesImpl) checkAll() { "pre-package": true, "do-package": true, "post-package": true, "pre-clean": true, "do-clean": true, "post-clean": true} - mklines.lines.CheckRcsID(0, `#[\t ]+`, "# ") + mklines.lines.CheckCvsID(0, `#[\t ]+`, "# ") substContext := NewSubstContext() var varalign VaralignBlock isHacksMk := mklines.lines.BaseName == "hacks.mk" - lineAction := func(mkline MkLine) bool { + lineAction := func(mkline *MkLine) bool { if isHacksMk { // Needs to be set here because it is reset in MkLines.ForEach. mklines.Tools.SeenPrefs = true @@ -160,8 +158,8 @@ func (mklines *MkLinesImpl) checkAll() { return true } - atEnd := func(mkline MkLine) { - mklines.indentation.CheckFinish(mklines.lines.FileName) + atEnd := func(mkline *MkLine) { + mklines.indentation.CheckFinish(mklines.lines.Filename) } if trace.Tracing { @@ -175,7 +173,7 @@ func (mklines *MkLinesImpl) checkAll() { CheckLinesTrailingEmptyLines(mklines.lines) } -func (mklines *MkLinesImpl) checkVarassignPlist(mkline MkLine) { +func (mklines *MkLines) checkVarassignPlist(mkline *MkLine) { switch mkline.Varcanon() { case "PLIST_VARS": for _, id := range mkline.ValueFields(resolveVariableRefs(mklines, mkline.Value())) { @@ -192,22 +190,57 @@ func (mklines *MkLinesImpl) checkVarassignPlist(mkline MkLine) { } } +func (mklines *MkLines) SplitToParagraphs() []*Paragraph { + var paras []*Paragraph + + lines := mklines.mklines + isEmpty := func(i int) bool { + if lines[i].IsEmpty() { + return true + } + return lines[i].IsComment() && + lines[i].Text == "#" && + (i == 0 || lines[i-1].IsComment()) && + (i == len(lines)-1 || lines[i+1].IsComment()) + } + + i := 0 + for i < len(lines) { + from := i + for from < len(lines) && isEmpty(from) { + from++ + } + + to := from + for to < len(lines) && !isEmpty(to) { + to++ + } + + if from != to { + paras = append(paras, NewParagraph(mklines, from, to)) + } + i = to + } + + return paras +} + // ForEach calls the action for each line, until the action returns false. // It keeps track of the indentation (see MkLines.indentation) // and all conditional variables (see Indentation.IsConditional). -func (mklines *MkLinesImpl) ForEach(action func(mkline MkLine)) { +func (mklines *MkLines) ForEach(action func(mkline *MkLine)) { mklines.ForEachEnd( - func(mkline MkLine) bool { action(mkline); return true }, - func(mkline MkLine) {}) + func(mkline *MkLine) bool { action(mkline); return true }, + func(mkline *MkLine) {}) } // ForEachEnd calls the action for each line, until the action returns false. // It keeps track of the indentation and all conditional variables. // At the end, atEnd is called with the last line as its argument. -func (mklines *MkLinesImpl) ForEachEnd(action func(mkline MkLine) bool, atEnd func(lastMkline MkLine)) { +func (mklines *MkLines) ForEachEnd(action func(mkline *MkLine) bool, atEnd func(lastMkline *MkLine)) { // XXX: To avoid looping over the lines multiple times, it would - // be nice to have an interface LinesChecker that checks a single thing. + // be nice to have an interface LinesChecker that checks a single topic. // Multiple of these line checkers could be run in parallel, so that // the diagnostics appear in the correct order, from top to bottom. @@ -222,7 +255,9 @@ func (mklines *MkLinesImpl) ForEachEnd(action func(mkline MkLine) bool, atEnd fu mklines.indentation.TrackAfter(mkline) } - atEnd(mklines.mklines[len(mklines.mklines)-1]) + if len(mklines.mklines) > 0 { + atEnd(mklines.mklines[len(mklines.mklines)-1]) + } mklines.indentation = nil } @@ -230,8 +265,8 @@ func (mklines *MkLinesImpl) ForEachEnd(action func(mkline MkLine) bool, atEnd fu // variable and returns a slice containing all its values, fully // expanded. // -// It can only be used during a active ForEach call. -func (mklines *MkLinesImpl) ExpandLoopVar(varname string) []string { +// It can only be used during an active ForEach call. +func (mklines *MkLines) ExpandLoopVar(varname string) []string { // From the inner loop to the outer loop, just in case // that two loops should ever use the same variable. @@ -239,14 +274,14 @@ func (mklines *MkLinesImpl) ExpandLoopVar(varname string) []string { ind := mklines.indentation.levels[i] mkline := ind.mkline - if mkline == nil || !mkline.IsDirective() || mkline.Directive() != "for" { + if mkline.Directive() != "for" { continue } // TODO: If needed, add support for multi-variable .for loops. resolved := resolveVariableRefs(mklines, mkline.Args()) words := mkline.ValueFields(resolved) - if 1 < len(words) && words[0] == varname && words[1] == "in" { + if len(words) >= 3 && words[0] == varname && words[1] == "in" { return words[2:] } } @@ -254,7 +289,7 @@ func (mklines *MkLinesImpl) ExpandLoopVar(varname string) []string { return nil } -func (mklines *MkLinesImpl) collectDefinedVariables() { +func (mklines *MkLines) collectDefinedVariables() { // FIXME: This method has a wrong name. It collects not only the defined // variables but also the used ones. @@ -262,7 +297,7 @@ func (mklines *MkLinesImpl) collectDefinedVariables() { defer trace.Call0()() } - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { mklines.Tools.ParseToolLine(mklines, mkline, false, true) if !mkline.IsVarassign() && !mkline.IsCommentedVarassign() { @@ -323,14 +358,14 @@ func (mklines *MkLinesImpl) collectDefinedVariables() { } // defineVar marks a variable as defined in both the current package and the current file. -func (mklines *MkLinesImpl) defineVar(pkg *Package, mkline MkLine, varname string) { +func (mklines *MkLines) defineVar(pkg *Package, mkline *MkLine, varname string) { mklines.vars.Define(varname, mkline) if pkg != nil { pkg.vars.Define(varname, mkline) } } -func (mklines *MkLinesImpl) collectPlistVars() { +func (mklines *MkLines) collectPlistVars() { // TODO: The PLIST_VARS code above looks very similar. for _, mkline := range mklines.mklines { if mkline.IsVarassign() { @@ -355,15 +390,15 @@ func (mklines *MkLinesImpl) collectPlistVars() { } } -func (mklines *MkLinesImpl) collectElse() { +func (mklines *MkLines) collectElse() { // Make a dry-run over the lines, which sets data.elseLine (in mkline.go) as a side-effect. - mklines.ForEach(func(mkline MkLine) {}) + mklines.ForEach(func(mkline *MkLine) {}) // TODO: Check whether this ForEach is redundant because it is already run somewhere else. } -func (mklines *MkLinesImpl) collectUsedVariables() { +func (mklines *MkLines) collectUsedVariables() { for _, mkline := range mklines.mklines { - mkline.ForEachUsed(func(varUse *MkVarUse, time vucTime) { + mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) { mklines.UseVar(mkline, varUse.varname, time) }) } @@ -375,7 +410,7 @@ func (mklines *MkLinesImpl) collectUsedVariables() { // documentation of the Makefile fragments from the pkgsrc infrastructure. // // Loosely based on mk/help/help.awk, revision 1.28, but much simpler. -func (mklines *MkLinesImpl) collectDocumentedVariables() { +func (mklines *MkLines) collectDocumentedVariables() { scope := NewScope() commentLines := 0 relevant := true @@ -390,7 +425,7 @@ func (mklines *MkLinesImpl) collectDocumentedVariables() { if commentLines >= 3 && relevant { for varname, mkline := range scope.used { mklines.vars.Define(varname, mkline) - mklines.vars.Use(varname, mkline, vucTimeRun) + mklines.vars.Use(varname, mkline, VucRunTime) } } @@ -426,7 +461,7 @@ func (mklines *MkLinesImpl) collectDocumentedVariables() { varcanon := varnameCanon(varname) if varcanon == strings.ToUpper(varcanon) && matches(varcanon, `[A-Z]`) && parser.EOF() { scope.Define(varcanon, mkline) - scope.Use(varcanon, mkline, vucTimeRun) + scope.Use(varcanon, mkline, VucRunTime) } if words[1] == "Copyright" { @@ -441,52 +476,101 @@ func (mklines *MkLinesImpl) collectDocumentedVariables() { finish() } -// CheckForUsedComment checks that this file (a Makefile.common) has the given +// CheckUsedBy checks that this file (a Makefile.common) has the given // relativeName in one of the "# used by" comments at the beginning of the file. -func (mklines *MkLinesImpl) CheckForUsedComment(relativeName string) { +func (mklines *MkLines) CheckUsedBy(relativeName string) { lines := mklines.lines if lines.Len() < 3 { return } + paras := mklines.SplitToParagraphs() + expected := "# used by " + relativeName - for _, line := range lines.Lines { - if line.Text == expected { - return + found := false + var usedParas []*Paragraph + + determineUsedParas := func() { + for _, para := range paras { + var hasUsedBy bool + var hasOther bool + var conflict *MkLine + + para.ForEach(func(mkline *MkLine) { + if ok, _ := mkline.IsCvsID(`#[\t ]+`); ok { + return + } + if hasPrefix(mkline.Text, "# used by ") && len(strings.Fields(mkline.Text)) == 4 { + if mkline.Text == expected { + found = true + } + hasUsedBy = true + if hasOther && conflict == nil { + conflict = mkline + } + } else { + hasOther = true + if hasUsedBy && conflict == nil { + conflict = mkline + } + } + }) + + if conflict != nil { + conflict.Warnf("The \"used by\" lines should be in a separate paragraph.") + } else if hasUsedBy { + usedParas = append(usedParas, para) + } } } + determineUsedParas() - i := 0 - for i < 2 && hasPrefix(lines.Lines[i].Text, "#") { - i++ + if len(usedParas) > 1 { + usedParas[1].FirstLine().Warnf("There should only be a single \"used by\" paragraph per file.") + } + + var prevLine *MkLine + if len(usedParas) > 0 { + prevLine = usedParas[0].LastLine() + } else { + prevLine = paras[0].LastLine() + if paras[0].to > 1 { + fix := prevLine.Autofix() + fix.Notef(SilentAutofixFormat) + fix.InsertAfter("") + fix.Apply() + } } // TODO: Sort the comments. // TODO: Discuss whether these comments are actually helpful. + // TODO: Remove lines that don't apply anymore. - fix := lines.Lines[i].Autofix() - fix.Warnf("Please add a line %q here.", expected) - fix.Explain( - "Since Makefile.common files usually don't have any comments and", - "therefore not a clearly defined purpose, they should at least", - "contain references to all files that include them, so that it is", - "easier to see what effects future changes may have.", - "", - "If there are more than five packages that use a Makefile.common,", - "that file should have a clearly defined and documented purpose,", - "and the filename should reflect that purpose.", - "Typical names are module.mk, plugin.mk or version.mk.") - fix.InsertBefore(expected) - fix.Apply() + if !found { + fix := prevLine.Autofix() + fix.Warnf("Please add a line %q here.", expected) + fix.Explain( + "Since Makefile.common files usually don't have any comments and", + "therefore not a clearly defined purpose, they should at least", + "contain references to all files that include them, so that it is", + "easier to see what effects future changes may have.", + "", + "If there are more than five packages that use a Makefile.common,", + "that file should have a clearly defined and documented purpose,", + "and the filename should reflect that purpose.", + "Typical names are module.mk, plugin.mk or version.mk.") + fix.InsertAfter(expected) + fix.Apply() + } SaveAutofixChanges(lines) } -func (mklines *MkLinesImpl) SaveAutofixChanges() { +func (mklines *MkLines) SaveAutofixChanges() { mklines.lines.SaveAutofixChanges() } -func (mklines *MkLinesImpl) EOFLine() MkLine { +func (mklines *MkLines) EOFLine() *MkLine { return MkLineParser{}.Parse(mklines.lines.EOFLine()) } @@ -504,7 +588,7 @@ type VaralignBlock struct { } type varalignBlockInfo struct { - mkline MkLine + mkline *MkLine varnameOp string // Variable name + assignment operator varnameOpWidth int // Screen width of varnameOp space string // Whitespace between varnameOp and the variable value @@ -512,7 +596,7 @@ type varalignBlockInfo struct { continuation bool // A continuation line with no value in the first line. } -func (va *VaralignBlock) Process(mkline MkLine) { +func (va *VaralignBlock) Process(mkline *MkLine) { switch { case !G.Opts.WarnSpace: return @@ -534,7 +618,7 @@ func (va *VaralignBlock) Process(mkline MkLine) { } } -func (va *VaralignBlock) processVarassign(mkline MkLine) { +func (va *VaralignBlock) processVarassign(mkline *MkLine) { switch { case mkline.Op() == opAssignEval && matches(mkline.Varname(), `^[a-z]`): // Arguments to procedures do not take part in block alignment. @@ -561,7 +645,8 @@ func (va *VaralignBlock) processVarassign(mkline MkLine) { text := strings.TrimSuffix(mkline.raw[0].orignl, "\n") data := MkLineParser{}.split(nil, text) m, a := MkLineParser{}.MatchVarassign(mkline.Line, text, data) - continuation = m && a.value == "\\" + assert(m) + continuation = a.value == "\\" } valueAlign := mkline.ValueAlign() @@ -663,7 +748,7 @@ func (va *VaralignBlock) optimalWidth(infos []*varalignBlockInfo) int { return (minVarnameOpWidth & -8) + 8 } -func (va *VaralignBlock) realign(mkline MkLine, varnameOp, oldSpace string, continuation bool, newWidth int) { +func (va *VaralignBlock) realign(mkline *MkLine, varnameOp, oldSpace string, continuation bool, newWidth int) { hasSpace := contains(oldSpace, " ") newSpace := "" @@ -688,7 +773,7 @@ func (va *VaralignBlock) realign(mkline MkLine, varnameOp, oldSpace string, cont } } -func (va *VaralignBlock) realignInitialLine(mkline MkLine, varnameOp string, oldSpace string, newSpace string, hasSpace bool, newWidth int) { +func (va *VaralignBlock) realignInitialLine(mkline *MkLine, varnameOp string, oldSpace string, newSpace string, hasSpace bool, newWidth int) { wrongColumn := tabWidth(varnameOp+oldSpace) != tabWidth(varnameOp+newSpace) fix := mkline.Autofix() @@ -726,7 +811,7 @@ func (va *VaralignBlock) realignInitialLine(mkline MkLine, varnameOp string, old fix.Apply() } -func (va *VaralignBlock) realignContinuationLines(mkline MkLine, newWidth int) { +func (va *VaralignBlock) realignContinuationLines(mkline *MkLine, newWidth int) { indentation := strings.Repeat("\t", newWidth/8) + strings.Repeat(" ", newWidth%8) fix := mkline.Autofix() fix.Notef("This line should be aligned with %q.", indentation) diff --git a/pkgtools/pkglint/files/mklines_test.go b/pkgtools/pkglint/files/mklines_test.go index f4102e1adec..283c6af18c8 100644 --- a/pkgtools/pkglint/files/mklines_test.go +++ b/pkgtools/pkglint/files/mklines_test.go @@ -3,6 +3,7 @@ package pkglint import ( "gopkg.in/check.v1" "sort" + "strings" ) func (s *Suite) Test_MkLines_Check__unusual_target(c *check.C) { @@ -11,7 +12,7 @@ func (s *Suite) Test_MkLines_Check__unusual_target(c *check.C) { t.SetUpVartypes() t.SetUpTool("cc", "CC", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "echo: echo.c", "\tcc -o ${.TARGET} ${.IMPSRC}") @@ -28,7 +29,7 @@ func (s *Suite) Test_MkLines__quoting_LDFLAGS_for_GNU_configure(c *check.C) { t.SetUpVartypes() G.Pkg = NewPackage(t.File("category/pkgbase")) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "GNU_CONFIGURE=\tyes", "CONFIGURE_ENV+=\tX_LIBS=${X11_LDFLAGS:Q}") @@ -46,7 +47,7 @@ func (s *Suite) Test_MkLines__for_loop_multiple_variables(c *check.C) { t.SetUpTool("find", "FIND", AtRunTime) t.SetUpTool("pax", "PAX", AtRunTime) mklines := t.NewMkLines("Makefile", // From audio/squeezeboxserver - MkRcsID, + MkCvsID, "", "SBS_COPY=\tsource target", "", @@ -70,7 +71,7 @@ func (s *Suite) Test_MkLines__comparing_YesNo_variable_to_string(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("databases/gdbm_compat/builtin.mk", - MkRcsID, + MkCvsID, ".if ${USE_BUILTIN.gdbm} == \"no\"", ".endif", ".if ${USE_BUILTIN.gdbm:tu} == \"no\"", // Can never be true, since "no" is not uppercase. @@ -90,19 +91,19 @@ func (s *Suite) Test_MkLines__varuse_sh_modifier(c *check.C) { t.SetUpVartypes() t.SetUpTool("sed", "SED", AfterPrefsMk) mklines := t.NewMkLines("lang/qore/module.mk", - MkRcsID, + MkCvsID, "qore-version=\tqore --short-version | ${SED} -e s/-.*//", "PLIST_SUBST+=\tQORE_VERSION=\"${qore-version:sh}\"") var vars2 []string - mklines.mklines[1].ForEachUsed(func(varUse *MkVarUse, time vucTime) { + mklines.mklines[1].ForEachUsed(func(varUse *MkVarUse, time VucTime) { vars2 = append(vars2, varUse.varname) }) c.Check(vars2, deepEquals, []string{"SED"}) var vars3 []string - mklines.mklines[2].ForEachUsed(func(varUse *MkVarUse, time vucTime) { + mklines.mklines[2].ForEachUsed(func(varUse *MkVarUse, time VucTime) { vars3 = append(vars3, varUse.varname) }) @@ -125,7 +126,7 @@ func (s *Suite) Test_MkLines__varuse_parameterized(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("converters/wv2/Makefile", - MkRcsID, + MkCvsID, "CONFIGURE_ARGS+=\t\t${CONFIGURE_ARGS.${ICONV_TYPE}-iconv}", "CONFIGURE_ARGS.gnu-iconv=\t--with-libiconv=${BUILDLINK_PREFIX.iconv}") @@ -162,7 +163,7 @@ func (s *Suite) Test_MkLines__loop_modifier(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("chat/xchat/Makefile", - MkRcsID, + MkCvsID, "GCONF_SCHEMAS=\tapps_xchat_url_handler.schemas", "post-install:", "\t${GCONF_SCHEMAS:@s@"+ @@ -179,7 +180,7 @@ func (s *Suite) Test_MkLines__PKG_SKIP_REASON_depending_on_OPSYS(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "PKG_SKIP_REASON+=\t\"Fails everywhere\"", ".if ${OPSYS} == \"Cygwin\"", "PKG_SKIP_REASON+=\t\"Fails on Cygwin\"", @@ -197,7 +198,7 @@ func (s *Suite) Test_MkLines_Check__use_list_variable_as_part_of_word(c *check.C t.SetUpVartypes() t.SetUpTool("tr", "", AtRunTime) mklines := t.NewMkLines("converters/chef/Makefile", - MkRcsID, + MkCvsID, "\tcd ${WRKSRC} && tr '\\r' '\\n' < ${DISTDIR}/${DIST_SUBDIR}/${DISTFILES} > chef.l") mklines.Check() @@ -211,7 +212,7 @@ func (s *Suite) Test_MkLines_Check__absolute_pathname_depending_on_OPSYS(c *chec t.SetUpVartypes() mklines := t.NewMkLines("games/heretic2-demo/Makefile", - MkRcsID, + MkCvsID, ".if ${OPSYS} == \"DragonFly\"", "TAR_CMD=\t/usr/bin/bsdtar", ".endif", @@ -229,7 +230,7 @@ func (s *Suite) Test_MkLines_Check__absolute_pathname_depending_on_OPSYS(c *chec "WARN: games/heretic2-demo/Makefile:5: Unknown shell command \"/usr/bin/bsdtar\".") } -func (s *Suite) Test_MkLines_CheckForUsedComment(c *check.C) { +func (s *Suite) Test_MkLines_CheckUsedBy__show_autofix(c *check.C) { t := s.Init(c) t.SetUpCommandLine("--show-autofix") @@ -237,7 +238,7 @@ func (s *Suite) Test_MkLines_CheckForUsedComment(c *check.C) { test := func(pkgpath string, lines []string, diagnostics []string) { mklines := t.NewMkLines("Makefile.common", lines...) - mklines.CheckForUsedComment(pkgpath) + mklines.CheckUsedBy(pkgpath) t.CheckOutput(diagnostics) } @@ -255,14 +256,14 @@ func (s *Suite) Test_MkLines_CheckForUsedComment(c *check.C) { test( "category/package", lines( - MkRcsID), + MkCvsID), diagnostics()) // Still too short. test( "category/package", lines( - MkRcsID, + MkCvsID, ""), diagnostics()) @@ -270,40 +271,210 @@ func (s *Suite) Test_MkLines_CheckForUsedComment(c *check.C) { test( "sysutils/mc", lines( - MkRcsID, + MkCvsID, "", "# used by sysutils/mc"), diagnostics()) // This file is not correctly mentioned, therefore the line is inserted. - // TODO: Since the following line is of a different type, an additional empty line should be inserted. test( "category/package", lines( - MkRcsID, + MkCvsID, "", "VARNAME=\tvalue"), diagnostics( - "WARN: Makefile.common:2: Please add a line \"# used by category/package\" here.", - "AUTOFIX: Makefile.common:2: Inserting a line \"# used by category/package\" before this line.")) + "WARN: Makefile.common:1: Please add a line \"# used by category/package\" here.", + "AUTOFIX: Makefile.common:1: Inserting a line \"# used by category/package\" after this line.")) // The "used by" comments may either start in line 2 or in line 3. test( "category/package", lines( - MkRcsID, + MkCvsID, "#", "#"), diagnostics( - "WARN: Makefile.common:3: Please add a line \"# used by category/package\" here.", - "AUTOFIX: Makefile.common:3: Inserting a line \"# used by category/package\" before this line.")) + "WARN: Makefile.common:1: Please add a line \"# used by category/package\" here.", + "AUTOFIX: Makefile.common:1: Inserting a line \"# used by category/package\" after this line.")) // TODO: What if there is an introductory comment first? That should stay at the top of the file. // TODO: What if the "used by" comments appear in the second paragraph, preceded by only comments and empty lines? + // Since the first paragraph already has some comments, the "used by" + // comments need their separate paragraph, which is inserted after + // the first paragraph. + test("category/package", + lines( + MkCvsID, + "# A normal comment", + "# that spans", + "# several lines"), + diagnostics( + "AUTOFIX: Makefile.common:4: Inserting a line \"\" after this line.", + "WARN: Makefile.common:4: Please add a line \"# used by category/package\" here.", + "AUTOFIX: Makefile.common:4: Inserting a line \"# used by category/package\" after this line.")) + c.Check(G.Logger.autofixAvailable, equals, true) } +func (s *Suite) Test_MkLines_CheckUsedBy(c *check.C) { + t := s.Init(c) + + test := func(pkgpath string, lines []string, diagnostics []string) { + mklines := t.NewMkLines("Makefile.common", lines...) + + mklines.CheckUsedBy(pkgpath) + + t.CheckOutput(diagnostics) + } + + lines := func(lines ...string) []string { return lines } + diagnostics := func(diagnostics ...string) []string { return diagnostics } + + // The including package is already mentioned in the single "used by" + // paragraph. Nothing needs to be changed. + test("category/package2/Makefile", + lines( + MkCvsID, + "# This Makefile fragment is", + "# used by category/package1/Makefile, as well as", // looks similar to the formal "used by". + "# some others.", + "", + "# used by category/package2/Makefile"), + diagnostics()) + + // The including file is not yet mentioned. There is a single "used by" + // paragraph, and the including file needs to be added to that paragraph. + // It is added in the correct sorting order. The entries are simply + // sorted alphabetically. + test("category/package/Makefile", + lines( + MkCvsID, + "# This Makefile fragment is", + "# used by category/package1/Makefile, as well as", // looks similar to the formal "used by". + "# some others.", + "", + "# used by category/package2/Makefile"), + diagnostics( + "WARN: Makefile.common:6: Please add a line \"# used by category/package/Makefile\" here.")) + + // There are two separate paragraphs with "used by" lines. The first of + // them is the interesting one. The new line is added to the first paragraph. + test("category/package", + lines( + MkCvsID, + "# used by category/package1", + "", + "# used by category/package2"), + diagnostics( + "WARN: Makefile.common:4: There should only be a single \"used by\" paragraph per file.", + "WARN: Makefile.common:2: Please add a line \"# used by category/package\" here.")) + + // The empty comment also separates the two paragraphs, like in the + // previous test case. + test("category/package", + lines( + MkCvsID, + "# used by category/package1", + "#", + "# used by category/package2"), + diagnostics( + "WARN: Makefile.common:4: There should only be a single \"used by\" paragraph per file.", + "WARN: Makefile.common:2: Please add a line \"# used by category/package\" here.")) + + c.Check(G.Logger.autofixAvailable, equals, true) +} + +func (s *Suite) Test_MkLines_CheckUsedBy__separate_paragraph(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("Makefile.common", + MkCvsID, + "# a comment", + "# used by category/package", + "# a comment") + + mklines.CheckUsedBy("category/package") + + t.CheckOutputLines( + "WARN: Makefile.common:3: The \"used by\" lines should be in a separate paragraph.") +} + +func (s *Suite) Test_MkLines_ExpandLoopVar(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("filename.mk", + MkCvsID, + "", + ".for file in a b c d e f g h", + ". for rank in 1 2 3 4 5 6 7 8", + "CHESS_BOARD+=\t${file}${rank}", + ". endfor", + ".endfor") + + var files []string + var ranks []string + var diagonals []string + mklines.ForEach(func(mkline *MkLine) { + if mkline.IsVarassign() { + ranks = mklines.ExpandLoopVar("rank") + files = mklines.ExpandLoopVar("file") + diagonals = mklines.ExpandLoopVar("diagonals") + } + }) + + t.Check(files, deepEquals, strings.Split("abcdefgh", "")) + t.Check(ranks, deepEquals, strings.Split("12345678", "")) + t.Check(diagonals, check.HasLen, 0) +} + +func (s *Suite) Test_MkLines_ExpandLoopVar__multi(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("filename.mk", + MkCvsID, + "", + ".if 1", + ". for key value in 1 one 2 two 3 three", + "VAR.${key}=\t${value}", + ". endfor", + ".endif") + + var keys []string + var values []string + mklines.ForEach(func(mkline *MkLine) { + if mkline.IsVarassign() { + keys = mklines.ExpandLoopVar("key") + values = mklines.ExpandLoopVar("value") + } + }) + + // As of June 2019, multi-variable .for loops are not yet implemented. + t.Check(keys, check.HasLen, 0) + t.Check(values, check.HasLen, 0) +} + +func (s *Suite) Test_MkLines_ExpandLoopVar__malformed_for(c *check.C) { + t := s.Init(c) + + mklines := t.NewMkLines("filename.mk", + MkCvsID, + "", + ".for var in", + "VAR=\t${var}", + ".endfor") + + var values = []string{"uninitialized"} + mklines.ForEach(func(mkline *MkLine) { + if mkline.IsVarassign() { + values = mklines.ExpandLoopVar("key") + } + }) + + t.Check(values, check.HasLen, 0) +} + func (s *Suite) Test_MkLines_collectDefinedVariables(c *check.C) { t := s.Init(c) @@ -312,7 +483,7 @@ func (s *Suite) Test_MkLines_collectDefinedVariables(c *check.C) { t.CreateFileLines("mk/tools/defaults.mk", "USE_TOOLS+= autoconf autoconf213") mklines := t.NewMkLines("determine-defined-variables.mk", - MkRcsID, + MkCvsID, "", "USE_TOOLS+= autoconf213 autoconf", "", @@ -347,9 +518,9 @@ func (s *Suite) Test_MkLines_collectDefinedVariables__BUILTIN_FIND_FILES_VAR(c * t.SetUpCommandLine("-Wall,no-space") t.SetUpPackage("category/package") t.CreateFileLines("mk/buildlink3/bsd.builtin.mk", - MkRcsID) + MkCvsID) mklines := t.SetUpFileMkLines("category/package/builtin.mk", - MkRcsID, + MkCvsID, "", "BUILTIN_FIND_FILES_VAR:= H_XFT2", "BUILTIN_FIND_FILES.H_XFT2= ${X11BASE}/include/X11/Xft/Xft.h", @@ -370,7 +541,7 @@ func (s *Suite) Test_MkLines_collectDefinedVariables__no_tracing(c *check.C) { t := s.Init(c) mklines := t.SetUpFileMkLines("filename.mk", - MkRcsID, + MkCvsID, "", "BUILD_DEFS+=\tVAR1", "PLIST_VARS+=\tvar2", @@ -391,7 +562,7 @@ func (s *Suite) Test_MkLines_collectUsedVariables__simple(c *check.C) { mklines.collectUsedVariables() - c.Check(mklines.vars.used, deepEquals, map[string]MkLine{"VAR": mkline}) + c.Check(mklines.vars.used, deepEquals, map[string]*MkLine{"VAR": mkline}) c.Check(mklines.vars.FirstUse("VAR"), equals, mkline) } @@ -399,7 +570,7 @@ func (s *Suite) Test_MkLines_collectUsedVariables__nested(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "", "LHS.${lparam}=\tRHS.${rparam}", "", @@ -423,7 +594,7 @@ func (s *Suite) Test_MkLines__private_tool_undefined(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "", "\tmd5sum filename") @@ -440,7 +611,7 @@ func (s *Suite) Test_MkLines__private_tool_defined(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "TOOLS_CREATE+=\tmd5sum", "", "\tmd5sum filename") @@ -455,7 +626,7 @@ func (s *Suite) Test_MkLines_Check__indentation(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, ". if !defined(GUARD_MK)", ". if ${OPSYS} == ${OPSYS}", ". for i in ${FILES}", @@ -507,7 +678,7 @@ func (s *Suite) Test_MkLines_Check__indentation_include(c *check.C) { t.SetUpVartypes() t.CreateFileLines("included.mk") mklines := t.SetUpFileMkLines("module.mk", - MkRcsID, + MkCvsID, "", ".if ${PKGPATH} == \"category/package\"", ".include \"included.mk\"", @@ -528,7 +699,7 @@ func (s *Suite) Test_MkLines_Check__unfinished_directives(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("opsys.mk", - MkRcsID, + MkCvsID, "", ".for i in 1 2 3 4 5", ". if ${OPSYS} == NetBSD", @@ -549,7 +720,7 @@ func (s *Suite) Test_MkLines_Check__unbalanced_directives(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("opsys.mk", - MkRcsID, + MkCvsID, "", ".for i in 1 2 3 4 5", ". if ${OPSYS} == NetBSD", @@ -571,7 +742,7 @@ func (s *Suite) Test_MkLines_Check__incomplete_subst_at_end(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("subst.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+=\tclass") @@ -594,7 +765,7 @@ func (s *Suite) Test_MkLines__wip_category_Makefile(c *check.C) { t.SetUpTool("rm", "RM", AtRunTime) t.CreateFileLines("mk/misc/category.mk") mklines := t.SetUpFileMkLines("wip/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tWIP pkgsrc packages", "", @@ -634,7 +805,7 @@ func (s *Suite) Test_MkLines_collectDocumentedVariables(c *check.C) { t.SetUpVartypes() t.SetUpTool("rm", "RM", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "#", "# Copyright 2000-2018", "#", @@ -662,7 +833,15 @@ func (s *Suite) Test_MkLines_collectDocumentedVariables(c *check.C) { "", "# VARBASE1.<param1>", "# VARBASE2.*", - "# VARBASE3.${id}") + "# VARBASE3.${id}", + "", + "# NETBSD/amd64", + "#\tThis is not a variable name.", + "#\tThe slash must not appear in a variable name.", + "", + "# _____", + "#\tThis is not a variable name.", + "#\tVariable names must have at least one letter.") // The variables that appear in the documentation are marked as // both used and defined, to prevent the "defined but not used" warnings. @@ -689,7 +868,7 @@ func (s *Suite) Test_MkLines__shell_command_indentation(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "#", "pre-configure:", "\tcd 'indented correctly'", @@ -709,7 +888,7 @@ func (s *Suite) Test_MkLines__unknown_options(c *check.C) { t.SetUpVartypes() t.SetUpOption("known", "") mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, "#", "PKG_OPTIONS_VAR=\tPKG_OPTIONS.pkgbase", "PKG_SUPPORTED_OPTIONS=\tknown unknown", @@ -732,7 +911,7 @@ func (s *Suite) Test_MkLines_Check__PLIST_VARS(c *check.C) { t.CreateFileLines("mk/bsd.options.mk") mklines := t.SetUpFileMkLines("category/package/options.mk", - MkRcsID, + MkCvsID, "", "PKG_OPTIONS_VAR= PKG_OPTIONS.pkg", "PKG_SUPPORTED_OPTIONS= both only-added only-defined", @@ -766,7 +945,7 @@ func (s *Suite) Test_MkLines_Check__PLIST_VARS_indirect(c *check.C) { t.SetUpOption("option2", "") mklines := t.SetUpFileMkLines("module.mk", - MkRcsID, + MkCvsID, "", "MY_PLIST_VARS= option1 option2", "PLIST_VARS+= ${MY_PLIST_VARS}", @@ -803,7 +982,7 @@ func (s *Suite) Test_MkLines_Check__PLIST_VARS_indirect_2(c *check.C) { t.SetUpOption("c", "") mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "", "PKG_SUPPORTED_OPTIONS= a b c", "PLIST_VARS+= ${PKG_SUPPORTED_OPTIONS:S,a,,g}", @@ -827,7 +1006,7 @@ func (s *Suite) Test_MkLines_collectElse(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "", ".if 0", ".endif", @@ -854,7 +1033,7 @@ func (s *Suite) Test_MkLines_Check__defined_and_used_variables(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "", ".for lang in de fr", "PLIST_VARS+= ${lang}", @@ -880,7 +1059,7 @@ func (s *Suite) Test_MkLines_Check__hacks_mk(c *check.C) { t.SetUpCommandLine("-Wall,no-space") t.SetUpVartypes() mklines := t.NewMkLines("hacks.mk", - MkRcsID, + MkCvsID, "", "PKGNAME?= pkgbase-1.0") @@ -898,7 +1077,7 @@ func (s *Suite) Test_MkLines_Check__MASTER_SITE_in_HOMEPAGE(c *check.C) { t.SetUpMasterSite("MASTER_SITE_GITHUB", "https://github.com/") t.SetUpVartypes() mklines := t.NewMkLines("devel/catch/Makefile", - MkRcsID, + MkCvsID, "HOMEPAGE=\t${MASTER_SITE_GITHUB:=philsquared/Catch/}", "HOMEPAGE=\t${MASTER_SITE_GITHUB}", "HOMEPAGE=\t${MASTER_SITES}", @@ -915,13 +1094,70 @@ func (s *Suite) Test_MkLines_Check__MASTER_SITE_in_HOMEPAGE(c *check.C) { "WARN: devel/catch/Makefile:5: HOMEPAGE should not be defined in terms of MASTER_SITEs.") } +// Up to June 2019, pkglint wrongly replaced the HOMEPAGE +// with an empty string. +func (s *Suite) Test_MkLines_Check__autofix_MASTER_SITE_in_HOMEPAGE(c *check.C) { + t := s.Init(c) + + test := func(diagnostics ...string) { + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "MASTER_SITES= \\", + "\thttps://cdn1.example.org/ \\", + "\thttps://cdn2.example.org/", + "", + "HOMEPAGE=\t${MASTER_SITES}") + + mklines.Check() + + t.CheckOutput(diagnostics) + } + + t.SetUpVartypes() + + t.SetUpCommandLine("-Wall") + test( + "WARN: Makefile:7: HOMEPAGE should not be defined in terms of MASTER_SITEs.") + + t.SetUpCommandLine("-Wall", "--autofix") + test( + nil...) + +} + +func (s *Suite) Test_MkLines_Check__autofix_MASTER_SITE_in_HOMEPAGE_in_package(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("-Wall", "--autofix") + t.SetUpPackage("category/package", + "MASTER_SITES=\thttps://cdn1.example.org/ https://cdn2.example.org/", + "HOMEPAGE=\t${MASTER_SITES}") + + t.Main("-Wall", "-q", "category/package") + + // When MASTER_SITES consists of several URLs, take the first one, + // assuming that it is the most appropriate. + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:9: " + + "HOMEPAGE should not be defined in terms of MASTER_SITEs. " + + "Use https://cdn1.example.org/ directly.") + + t.Main("-Wall", "-q", "--autofix", "category/package") + + t.CheckOutputLines( + "AUTOFIX: ~/category/package/Makefile:9: " + + "Replacing \"${MASTER_SITES}\" " + + "with \"https://cdn1.example.org/\".") +} + func (s *Suite) Test_MkLines_Check__VERSION_as_word_part_in_MASTER_SITES(c *check.C) { t := s.Init(c) t.SetUpVartypes() t.SetUpMasterSite("MASTER_SITE_SOURCEFORGE", "https://download.sf.net/") mklines := t.NewMkLines("geography/viking/Makefile", - MkRcsID, + MkCvsID, "MASTER_SITES=\t${MASTER_SITE_SOURCEFORGE:=viking/}${VERSION}/") mklines.Check() @@ -937,7 +1173,7 @@ func (s *Suite) Test_MkLines_Check__shell_command_as_word_part_in_ENV_list(c *ch t.SetUpVartypes() mklines := t.NewMkLines("x11/lablgtk1/Makefile", - MkRcsID, + MkCvsID, "CONFIGURE_ENV+=\tCC=${CC}") mklines.Check() @@ -953,7 +1189,7 @@ func (s *Suite) Test_MkLines_Check__extra_warnings(c *check.C) { t.SetUpVartypes() G.Pkg = NewPackage(t.File("category/pkgbase")) mklines := t.NewMkLines("options.mk", - MkRcsID, + MkCvsID, "", ".for word in ${PKG_FAIL_REASON}", "CONFIGURE_ARGS+=\t--sharedir=${PREFIX}/share/kde", @@ -974,6 +1210,48 @@ func (s *Suite) Test_MkLines_Check__extra_warnings(c *check.C) { "NOTE: options.mk:11: You can use \"../build\" instead of \"${WRKSRC}/../build\".") } +func (s *Suite) Test_MkLines_SplitToParagraphs(c *check.C) { + t := s.Init(c) + + type lineRange struct { + from, to int + } + + test := func(mklines *MkLines, ranges ...lineRange) { + paras := mklines.SplitToParagraphs() + + var exp []*Paragraph + for _, r := range ranges { + exp = append(exp, NewParagraph(mklines, r.from, r.to)) + } + + t.Check(paras, deepEquals, exp) + } + + para := func(from, to int) lineRange { return lineRange{from, to} } + + test( + t.NewMkLines("filename.mk", + MkCvsID, + "", + "# paragraph 2", + "#", + "VAR=\tstill paragraph 2", + "", + "# paragraph 3", + "#", + "# paragraph 4"), + para(0, 1), + para(2, 5), + para(6, 7), + para(8, 9)) + + test( + t.NewMkLines("filename.mk", + ""), + nil...) +} + // Ensures that during MkLines.ForEach, the conditional variables in // MkLines.Indentation are correctly updated for each line. func (s *Suite) Test_MkLines_ForEach__conditional_variables(c *check.C) { @@ -982,7 +1260,7 @@ func (s *Suite) Test_MkLines_ForEach__conditional_variables(c *check.C) { t.SetUpCommandLine("-Wall,no-space") t.SetUpVartypes() mklines := t.NewMkLines("conditional.mk", - MkRcsID, + MkCvsID, "", ".if defined(PKG_DEVELOPER)", "DEVELOPER=\tyes", @@ -995,7 +1273,7 @@ func (s *Suite) Test_MkLines_ForEach__conditional_variables(c *check.C) { seenDeveloper := false seenUsesGettext := false - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { if mkline.IsVarassign() { switch mkline.Varname() { case "DEVELOPER": @@ -1020,7 +1298,7 @@ func (s *Suite) Test_MkLines_checkVarassignPlist__indirect(c *check.C) { t.SetUpVartypes() mklines := t.SetUpFileMkLines("plist.mk", - MkRcsID, + MkCvsID, "", "MY_PLIST_VARS=\tvar1 var2", "PLIST_VARS+=\t${MY_PLIST_VARS}", diff --git a/pkgtools/pkglint/files/mkparser.go b/pkgtools/pkglint/files/mkparser.go index b2ae6e598d8..daa9919b1aa 100644 --- a/pkgtools/pkglint/files/mkparser.go +++ b/pkgtools/pkglint/files/mkparser.go @@ -9,7 +9,7 @@ import ( // MkParser wraps a Parser and provides methods for parsing // things related to Makefiles. type MkParser struct { - Line Line + Line *Line lexer *textproc.Lexer EmitWarnings bool } @@ -28,8 +28,8 @@ func (p *MkParser) Rest() string { // The text argument is assumed to be after unescaping the # character, // which means the # is a normal character and does not introduce a Makefile comment. // For VarUse, this distinction is irrelevant. -func NewMkParser(line Line, text string, emitWarnings bool) *MkParser { - assertf((line != nil) == emitWarnings, "line must be given iff emitWarnings is set") +func NewMkParser(line *Line, text string, emitWarnings bool) *MkParser { + assert((line != nil) == emitWarnings) // line must be given iff emitWarnings is set return &MkParser{line, textproc.NewLexer(text), emitWarnings} } @@ -183,118 +183,121 @@ func (p *MkParser) varUseAlnum() *MkVarUse { func (p *MkParser) VarUseModifiers(varname string, closing byte) []MkVarUseModifier { lexer := p.lexer - // TODO: Split into VarUseModifier for parsing a single modifier. - var modifiers []MkVarUseModifier - appendModifier := func(s string) { modifiers = append(modifiers, MkVarUseModifier{s}) } - // The :S and :C modifiers may be chained without using the : as separator. mayOmitColon := false for lexer.SkipByte(':') || mayOmitColon { - mayOmitColon = false - modifierMark := lexer.Mark() - - switch lexer.PeekByte() { - case 'E', 'H', 'L', 'O', 'Q', 'R', 'T', 's', 't', 'u': - mod := lexer.NextBytesSet(textproc.Alnum) - switch mod { - - case - "E", // Extension, e.g. path/file.suffix => suffix - "H", // Head, e.g. dir/subdir/file.suffix => dir/subdir - "L", // XXX: Shouldn't this be handled specially? - "O", // Order alphabetically - "Ox", // Shuffle - "Q", // Quote shell meta-characters - "R", // Strip the file suffix, e.g. path/file.suffix => file - "T", // Basename, e.g. path/file.suffix => file.suffix - "sh", // Evaluate the variable value as shell command - "tA", // Try to convert to absolute path - "tW", // Causes the value to be treated as a single word - "tl", // To lowercase - "tu", // To uppercase - "tw", // Causes the value to be treated as list of words - "u": // Remove adjacent duplicate words (like uniq(1)) - appendModifier(mod) - continue - - case "ts": - // See devel/bmake/files/var.c:/case 't' - sep := p.varUseText(closing) - switch { - case sep == "": - lexer.SkipString(":") - case len(sep) == 1: - break - case matches(sep, `^\\\d+`): - break - default: - if p.EmitWarnings { - p.Line.Warnf("Invalid separator %q for :ts modifier of %q.", sep, varname) - } - } - appendModifier(lexer.Since(modifierMark)) - continue - } - - case '=', 'D', 'M', 'N', 'U': - lexer.Skip(1) - re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^$:\\}]|\$\$|\\.)+`, `^([^$:\\)]|\$\$|\\.)+`))) - for p.VarUse() != nil || lexer.SkipRegexp(re) { - } - arg := lexer.Since(modifierMark) - appendModifier(strings.Replace(arg, "\\:", ":", -1)) - continue - - case 'C', 'S': - if ok, _, _, _, _ := p.varUseModifierSubst(closing); ok { - appendModifier(lexer.Since(modifierMark)) - mayOmitColon = true - continue - } + modifier := p.varUseModifier(varname, closing) + if modifier != "" { + modifiers = append(modifiers, MkVarUseModifier{modifier}) + } + mayOmitColon = modifier != "" && (modifier[0] == 'S' || modifier[0] == 'C') + } + return modifiers +} - case '@': - if p.varUseModifierAt(lexer, varname) { - appendModifier(lexer.Since(modifierMark)) - continue - } +// varUseModifier parses a single variable modifier such as :Q or :S,from,to,. +// The actual parsing starts after the leading colon. +func (p *MkParser) varUseModifier(varname string, closing byte) string { + lexer := p.lexer + mark := lexer.Mark() - case '[': - if lexer.SkipRegexp(G.res.Compile(`^\[(?:[-.\d]+|#)\]`)) { - appendModifier(lexer.Since(modifierMark)) - continue + switch lexer.PeekByte() { + case 'E', 'H', 'L', 'O', 'Q', 'R', 'T', 's', 't', 'u': + mod := lexer.NextBytesSet(textproc.Alnum) + + switch mod { + case + "E", // Extension, e.g. path/file.suffix => suffix + "H", // Head, e.g. dir/subdir/file.suffix => dir/subdir + "L", // XXX: Shouldn't this be handled specially? + "O", // Order alphabetically + "Ox", // Shuffle + "Q", // Quote shell meta-characters + "R", // Strip the file suffix, e.g. path/file.suffix => file + "T", // Basename, e.g. path/file.suffix => file.suffix + "sh", // Evaluate the variable value as shell command + "tA", // Try to convert to absolute path + "tW", // Causes the value to be treated as a single word + "tl", // To lowercase + "tu", // To uppercase + "tw", // Causes the value to be treated as list of words + "u": // Remove adjacent duplicate words (like uniq(1)) + return mod + } + + if hasPrefix(mod, "ts") { + // See devel/bmake/files/var.c:/case 't' + sep := mod[2:] + p.varUseText(closing) + switch { + case sep == "": + lexer.SkipString(":") + case len(sep) == 1: + break + case matches(sep, `^\\\d+`): + break + default: + if p.EmitWarnings { + p.Line.Warnf("Invalid separator %q for :ts modifier of %q.", sep, varname) + p.Line.Explain( + "The separator for the :ts modifier must be either a single character", + "or an escape sequence like \\t or \\n or an octal or decimal escape", + "sequence; see the bmake man page for further details.") + } } + return lexer.Since(mark) + } - case '?': - lexer.Skip(1) - p.varUseText(closing) - if lexer.SkipByte(':') { - p.varUseText(closing) - appendModifier(lexer.Since(modifierMark)) - continue - } + case '=', 'D', 'M', 'N', 'U': + lexer.Skip(1) + re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^$:\\}]|\$\$|\\.)+`, `^([^$:\\)]|\$\$|\\.)+`))) + for p.VarUse() != nil || lexer.SkipRegexp(re) { } + arg := lexer.Since(mark) + return strings.Replace(arg, "\\:", ":", -1) - lexer.Reset(modifierMark) + case 'C', 'S': + if ok, _, _, _, _ := p.varUseModifierSubst(closing); ok { + return lexer.Since(mark) + } - re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^:$}]|\$\$)+`, `^([^:$)]|\$\$)+`))) - for p.VarUse() != nil || lexer.SkipRegexp(re) { + case '@': + if p.varUseModifierAt(lexer, varname) { + return lexer.Since(mark) } - modifier := lexer.Since(modifierMark) - // ${SOURCES:%.c=%.o} or ${:!uname -a:[2]} - if contains(modifier, "=") || (hasPrefix(modifier, "!") && hasSuffix(modifier, "!")) { - appendModifier(modifier) - continue + case '[': + if lexer.SkipRegexp(G.res.Compile(`^\[(?:[-.\d]+|#)\]`)) { + return lexer.Since(mark) } - if p.EmitWarnings && modifier != "" { - p.Line.Warnf("Invalid variable modifier %q for %q.", modifier, varname) + case '?': + lexer.Skip(1) + p.varUseText(closing) + if lexer.SkipByte(':') { + p.varUseText(closing) + return lexer.Since(mark) } + } + lexer.Reset(mark) + + re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^:$}]|\$\$)+`, `^([^:$)]|\$\$)+`))) + for p.VarUse() != nil || lexer.SkipRegexp(re) { } - return modifiers + modifier := lexer.Since(mark) + + // ${SOURCES:%.c=%.o} or ${:!uname -a:[2]} + if contains(modifier, "=") || (hasPrefix(modifier, "!") && hasSuffix(modifier, "!")) { + return modifier + } + + if p.EmitWarnings && modifier != "" { + p.Line.Warnf("Invalid variable modifier %q for %q.", modifier, varname) + } + + return "" } // varUseText parses any text up to the next colon or closing mark. @@ -331,10 +334,24 @@ func (p *MkParser) varUseModifierSubst(closing byte) (ok bool, regex bool, from } skipOther := func() { - for p.VarUse() != nil || - lexer.SkipString("$$") || - (len(lexer.Rest()) >= 2 && lexer.PeekByte() == '\\' && separator != '\\' && lexer.Skip(2)) || - lexer.NextBytesFunc(isOther) != "" { + for { + switch { + + case p.VarUse() != nil: + break + + case lexer.SkipString("$$"): + break + + case len(lexer.Rest()) >= 2 && lexer.PeekByte() == '\\' && separator != '\\': + _ = lexer.Skip(2) + + case lexer.NextBytesFunc(isOther) != "": + break + + default: + return + } } } @@ -388,13 +405,13 @@ func (p *MkParser) varUseModifierAt(lexer *textproc.Lexer, varname string) bool // MkCond parses a condition like ${OPSYS} == "NetBSD". // // See devel/bmake/files/cond.c. -func (p *MkParser) MkCond() MkCond { +func (p *MkParser) MkCond() *MkCond { and := p.mkCondAnd() if and == nil { return nil } - ands := []MkCond{and} + ands := []*MkCond{and} for { mark := p.lexer.Mark() p.lexer.SkipHspace() @@ -411,16 +428,16 @@ func (p *MkParser) MkCond() MkCond { if len(ands) == 1 { return and } - return &mkCond{Or: ands} + return &MkCond{Or: ands} } -func (p *MkParser) mkCondAnd() MkCond { +func (p *MkParser) mkCondAnd() *MkCond { atom := p.mkCondAtom() if atom == nil { return nil } - atoms := []MkCond{atom} + atoms := []*MkCond{atom} for { mark := p.lexer.Mark() p.lexer.SkipHspace() @@ -437,10 +454,10 @@ func (p *MkParser) mkCondAnd() MkCond { if len(atoms) == 1 { return atom } - return &mkCond{And: atoms} + return &MkCond{And: atoms} } -func (p *MkParser) mkCondAtom() MkCond { +func (p *MkParser) mkCondAtom() *MkCond { if trace.Tracing { defer trace.Call1(p.Rest())() } @@ -452,7 +469,7 @@ func (p *MkParser) mkCondAtom() MkCond { case lexer.SkipByte('!'): cond := p.mkCondAtom() if cond != nil { - return &mkCond{Not: cond} + return &MkCond{Not: cond} } case lexer.SkipByte('('): @@ -482,28 +499,28 @@ func (p *MkParser) mkCondAtom() MkCond { lexer.SkipHspace() if m := lexer.NextRegexp(G.res.Compile(`^(<|<=|==|!=|>=|>)[\t ]*(0x[0-9A-Fa-f]+|\d+(?:\.\d+)?)`)); m != nil { - return &mkCond{CompareVarNum: &MkCondCompareVarNum{lhs, m[1], m[2]}} + return &MkCond{CompareVarNum: &MkCondCompareVarNum{lhs, m[1], m[2]}} } m := lexer.NextRegexp(G.res.Compile(`^(?:<|<=|==|!=|>=|>)`)) if m == nil { - return &mkCond{Var: lhs} // See devel/bmake/files/cond.c:/\* For \.if \$/ + return &MkCond{Var: lhs} // See devel/bmake/files/cond.c:/\* For \.if \$/ } lexer.SkipHspace() op := m[0] if op == "==" || op == "!=" { if mrhs := lexer.NextRegexp(G.res.Compile(`^"([^"\$\\]*)"`)); mrhs != nil { - return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, mrhs[1]}} + return &MkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, mrhs[1]}} } } if str := lexer.NextBytesSet(textproc.AlnumU); str != "" { - return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, str}} + return &MkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, str}} } if rhs := p.VarUse(); rhs != nil { - return &mkCond{CompareVarVar: &MkCondCompareVarVar{lhs, op, rhs}} + return &MkCond{CompareVarVar: &MkCondCompareVarVar{lhs, op, rhs}} } if lexer.PeekByte() == '"' { @@ -511,7 +528,7 @@ func (p *MkParser) mkCondAtom() MkCond { lexer.Skip(1) if quotedRHS := p.VarUse(); quotedRHS != nil { if lexer.SkipByte('"') { - return &mkCond{CompareVarVar: &MkCondCompareVarVar{lhs, op, quotedRHS}} + return &MkCond{CompareVarVar: &MkCondCompareVarVar{lhs, op, quotedRHS}} } } lexer.Reset(mark) @@ -532,7 +549,7 @@ func (p *MkParser) mkCondAtom() MkCond { rhsText.WriteByte(lexer.Since(m)[1]) case lexer.SkipByte('"'): - return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, rhsText.String()}} + return &MkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, rhsText.String()}} default: break loop } @@ -543,14 +560,14 @@ func (p *MkParser) mkCondAtom() MkCond { // See devel/bmake/files/cond.c:/^CondCvtArg if m := lexer.NextRegexp(G.res.Compile(`^(?:0x[0-9A-Fa-f]+|\d+(?:\.\d+)?)`)); m != nil { - return &mkCond{Num: m[0]} + return &MkCond{Num: m[0]} } } lexer.Reset(mark) return nil } -func (p *MkParser) mkCondFunc() *mkCond { +func (p *MkParser) mkCondFunc() *MkCond { lexer := p.lexer mark := lexer.Mark() @@ -564,14 +581,14 @@ func (p *MkParser) mkCondFunc() *mkCond { case "defined": varname := p.Varname() if varname != "" && lexer.SkipByte(')') { - return &mkCond{Defined: varname} + return &MkCond{Defined: varname} } case "empty": if varname := p.Varname(); varname != "" { modifiers := p.VarUseModifiers(varname, ')') if lexer.SkipByte(')') { - return &mkCond{Empty: &MkVarUse{varname, modifiers}} + return &MkCond{Empty: &MkVarUse{varname, modifiers}} } } @@ -585,7 +602,7 @@ func (p *MkParser) mkCondFunc() *mkCond { } arg := lexer.Since(argMark) if lexer.SkipByte(')') { - return &mkCond{Call: &MkCondCall{funcName, arg}} + return &MkCond{Call: &MkCondCall{funcName, arg}} } } @@ -608,28 +625,27 @@ func (p *MkParser) Varname() string { return lexer.Since(mark) } -func (p *MkParser) PkgbasePattern() string { +// isPkgbasePart returns whether str, when following a hyphen, +// continues the package base (as in "mysql-client"), or whether it +// starts the version (as in "mysql-1.0"). +func (*MkParser) isPkgbasePart(str string) bool { + lexer := textproc.NewLexer(str) - // isVersion returns true for "1.2", "[0-9]*", "${PKGVERSION}", "${PKGNAME:C/^.*-//}", - // but not for "client", "${PKGNAME}", "[a-z]". - isVersion := func(s string) bool { - lexer := textproc.NewLexer(s) + lexer.SkipByte('{') + lexer.SkipByte('[') + if lexer.NextByteSet(textproc.Alpha) != -1 { + return true + } - lexer.SkipByte('[') - if lexer.NextByteSet(textproc.Digit) != -1 { - return true - } + varUse := NewMkParser(nil, lexer.Rest(), false).VarUse() + if varUse != nil { + return !contains(varUse.varname, "VER") && len(varUse.modifiers) == 0 + } - lookaheadParser := NewMkParser(nil, lexer.Rest(), false) - varUse := lookaheadParser.VarUse() - if varUse != nil { - if contains(varUse.varname, "VER") || len(varUse.modifiers) > 0 { - return true - } - } + return false +} - return false - } +func (p *MkParser) PkgbasePattern() string { lexer := p.lexer start := lexer.Mark() @@ -641,11 +657,11 @@ func (p *MkParser) PkgbasePattern() string { continue } - if lexer.PeekByte() != '-' || isVersion(lexer.Rest()[1:]) { + if lexer.PeekByte() == '-' && p.isPkgbasePart(lexer.Rest()[1:]) { + lexer.Skip(1) + } else { break } - - lexer.Skip(1 /* the hyphen */) } pkgbase := lexer.Since(start) @@ -748,12 +764,6 @@ func (p *MkParser) Dependency() *DependencyPattern { return &dp } - if hasSuffix(dp.Pkgbase, "-*") { - dp.Pkgbase = strings.TrimSuffix(dp.Pkgbase, "-*") - dp.Wildcard = "*" - return &dp - } - lexer.Reset(mark) return nil } @@ -776,12 +786,10 @@ func ToVarUse(str string) *MkVarUse { // Unnecessary parentheses are omitted in this representation, // but !empty(VARNAME) is represented differently from ${VARNAME} != "". // For higher level analysis, a unified representation might be better. -type MkCond = *mkCond - -type mkCond struct { - Or []*mkCond - And []*mkCond - Not *mkCond +type MkCond struct { + Or []*MkCond + And []*MkCond + Not *MkCond Defined string Empty *MkVarUse @@ -813,7 +821,7 @@ type MkCondCall struct { } type MkCondCallback struct { - Not func(cond MkCond) + Not func(cond *MkCond) Defined func(varname string) Empty func(empty *MkVarUse) CompareVarNum func(varuse *MkVarUse, op string, num string) @@ -824,13 +832,13 @@ type MkCondCallback struct { VarUse func(varuse *MkVarUse) } -func (cond *mkCond) Walk(callback *MkCondCallback) { +func (cond *MkCond) Walk(callback *MkCondCallback) { (&MkCondWalker{}).Walk(cond, callback) } type MkCondWalker struct{} -func (w *MkCondWalker) Walk(cond MkCond, callback *MkCondCallback) { +func (w *MkCondWalker) Walk(cond *MkCond, callback *MkCondCallback) { switch { case cond.Or != nil: for _, or := range cond.Or { diff --git a/pkgtools/pkglint/files/mkparser_test.go b/pkgtools/pkglint/files/mkparser_test.go index f2428cc28b3..92ff27ab65a 100644 --- a/pkgtools/pkglint/files/mkparser_test.go +++ b/pkgtools/pkglint/files/mkparser_test.go @@ -5,6 +5,15 @@ import ( "strings" ) +func (s *Suite) Test_NewMkParser__invalid_arguments(c *check.C) { + t := s.Init(c) + + line := t.NewLine("filename.mk", 123, "") + + t.ExpectAssert(func() { NewMkParser(line, "", false) }) + t.ExpectAssert(func() { NewMkParser(nil, "", true) }) +} + func (s *Suite) Test_MkParser_MkTokens(c *check.C) { t := s.Init(c) @@ -89,7 +98,9 @@ func (s *Suite) Test_MkParser_VarUse(c *check.C) { testRest := func(input string, expectedTokens []*MkToken, expectedRest string, diagnostics ...string) { line := t.NewLines("Test_MkParser_VarUse.mk", input).Lines[0] p := NewMkParser(line, input, true) + actualTokens := p.MkTokens() + c.Check(actualTokens, deepEquals, expectedTokens) for i, expectedToken := range expectedTokens { if i < len(actualTokens) { @@ -243,6 +254,10 @@ func (s *Suite) Test_MkParser_VarUse(c *check.C) { test("${RUBY_RAILS_SUPPORTED:[#]}", varuse("RUBY_RAILS_SUPPORTED", "[#]")) + test("${GZIP_CMD:[asdf]:Q}", + varuseText("${GZIP_CMD:[asdf]:Q}", "GZIP_CMD", "Q"), + "WARN: Test_MkParser_VarUse.mk:1: Invalid variable modifier \"[asdf]\" for \"GZIP_CMD\".") + test("${DISTNAME:C/-[0-9]+$$//:C/_/-/}", varuse("DISTNAME", "C/-[0-9]+$$//", "C/_/-/")) @@ -367,6 +382,142 @@ func (s *Suite) Test_MkParser_VarUse(c *check.C) { "WARN: Test_MkParser_VarUse.mk:1: Missing closing \"}\" for \"${\".") } +func (s *Suite) Test_MkParser_varUseModifier__invalid_ts_modifier_with_warning(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("-Wall", "--explain") + line := t.NewLine("filename.mk", 123, "${VAR:tsabc}") + p := NewMkParser(line, "tsabc}", true) + + modifier := p.varUseModifier("VAR", '}') + + t.Check(modifier, equals, "tsabc") + t.Check(p.Rest(), equals, "}") + t.CheckOutputLines( + "WARN: filename.mk:123: Invalid separator \"abc\" for :ts modifier of \"VAR\".", + "", + "\tThe separator for the :ts modifier must be either a single character", + "\tor an escape sequence like \\t or \\n or an octal or decimal escape", + "\tsequence; see the bmake man page for further details.", + "") +} + +func (s *Suite) Test_MkParser_varUseModifier__invalid_ts_modifier_without_warning(c *check.C) { + t := s.Init(c) + + p := NewMkParser(nil, "tsabc}", false) + + modifier := p.varUseModifier("VAR", '}') + + t.Check(modifier, equals, "tsabc") + t.Check(p.Rest(), equals, "}") +} + +func (s *Suite) Test_MkParser_varUseModifier__square_bracket(c *check.C) { + t := s.Init(c) + + line := t.NewLine("filename.mk", 123, "\t${VAR:[asdf]}") + p := NewMkParser(line, "[asdf]", true) + + modifier := p.varUseModifier("VAR", '}') + + t.Check(modifier, equals, "") + t.Check(p.Rest(), equals, "") + + t.CheckOutputLines( + "WARN: filename.mk:123: Invalid variable modifier \"[asdf]\" for \"VAR\".") +} + +func (s *Suite) Test_MkParser_varUseModifier__condition_without_colon(c *check.C) { + t := s.Init(c) + + line := t.NewLine("filename.mk", 123, "${${VAR}:?yes:no}${${VAR}:?yes}") + p := NewMkParser(line, line.Text, true) + + varUse1 := p.VarUse() + varUse2 := p.VarUse() + + t.Check(varUse1, deepEquals, NewMkVarUse("${VAR}", "?yes:no")) + t.Check(varUse2, deepEquals, NewMkVarUse("${VAR}")) + t.Check(p.Rest(), equals, "") + + t.CheckOutputLines( + "WARN: filename.mk:123: Invalid variable modifier \"?yes\" for \"${VAR}\".") +} + +func (s *Suite) Test_MkParser_varUseModifier__malformed_in_parentheses(c *check.C) { + t := s.Init(c) + + line := t.NewLine("filename.mk", 123, "$(${VAR}:?yes)") + p := NewMkParser(line, line.Text, true) + + varUse := p.VarUse() + + t.Check(varUse, deepEquals, NewMkVarUse("${VAR}")) + t.Check(p.Rest(), equals, "") + + t.CheckOutputLines( + "WARN: filename.mk:123: Invalid variable modifier \"?yes\" for \"${VAR}\".", + "WARN: filename.mk:123: Please use curly braces {} instead of round parentheses () for ${VAR}.") +} + +func (s *Suite) Test_MkParser_varUseModifier__varuse_in_malformed_modifier(c *check.C) { + t := s.Init(c) + + line := t.NewLine("filename.mk", 123, "${${VAR}:?yes${INNER}}") + p := NewMkParser(line, line.Text, true) + + varUse := p.VarUse() + + t.Check(varUse, deepEquals, NewMkVarUse("${VAR}")) + t.Check(p.Rest(), equals, "") + + t.CheckOutputLines( + "WARN: filename.mk:123: Invalid variable modifier \"?yes${INNER}\" for \"${VAR}\".") +} + +func (s *Suite) Test_MkParser_varUseModifierAt__missing_at_after_variable_name(c *check.C) { + t := s.Init(c) + + line := t.NewLine("filename.mk", 123, "${VAR:@varname}") + p := NewMkParser(line, line.Text, true) + + varUse := p.VarUse() + + t.Check(varUse, deepEquals, NewMkVarUse("VAR")) + t.Check(p.Rest(), equals, "") + t.CheckOutputLines( + "WARN: filename.mk:123: Invalid variable modifier \"@varname\" for \"VAR\".") +} + +func (s *Suite) Test_MkParser_varUseModifierAt__dollar(c *check.C) { + t := s.Init(c) + + line := t.NewLine("filename.mk", 123, "${VAR:@var@$$var@}") + p := NewMkParser(line, line.Text, true) + + varUse := p.VarUse() + + t.Check(varUse, deepEquals, NewMkVarUse("VAR", "@var@$$var@")) + t.Check(p.Rest(), equals, "") + t.CheckOutputEmpty() +} + +func (s *Suite) Test_MkParser_varUseModifierAt__incomplete_without_warning(c *check.C) { + t := s.Init(c) + + p := NewMkParser(nil, "${VAR:@var@$$var}rest", false) + + varUse := p.VarUse() + + // TODO: It's inconsistent that this syntax error still produces a + // variable modifier, while most other syntax errors don't. + // FIXME: The } must not be part of the variable modifier. + t.Check(varUse, deepEquals, NewMkVarUse("VAR", "@var@$$var}rest")) + t.Check(p.Rest(), equals, "") + t.CheckOutputEmpty() +} + func (s *Suite) Test_MkParser_VarUse__ambiguous(c *check.C) { t := s.Init(c) @@ -400,13 +551,13 @@ func (s *Suite) Test_MkParser_VarUse__ambiguous(c *check.C) { func (s *Suite) Test_MkParser_MkCond(c *check.C) { t := s.Init(c) - testRest := func(input string, expectedTree MkCond, expectedRest string) { + testRest := func(input string, expectedTree *MkCond, expectedRest string) { p := NewMkParser(nil, input, false) actualTree := p.MkCond() c.Check(actualTree, deepEquals, expectedTree) c.Check(p.Rest(), equals, expectedRest) } - test := func(input string, expectedTree MkCond) { + test := func(input string, expectedTree *MkCond) { testRest(input, expectedTree, "") } varuse := NewMkVarUse @@ -414,119 +565,122 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) { t.Use(testRest, test, varuse) test("${OPSYS:MNetBSD}", - &mkCond{Var: varuse("OPSYS", "MNetBSD")}) + &MkCond{Var: varuse("OPSYS", "MNetBSD")}) test("defined(VARNAME)", - &mkCond{Defined: "VARNAME"}) + &MkCond{Defined: "VARNAME"}) test("empty(VARNAME)", - &mkCond{Empty: varuse("VARNAME")}) + &MkCond{Empty: varuse("VARNAME")}) test("!empty(VARNAME)", - &mkCond{Not: &mkCond{Empty: varuse("VARNAME")}}) + &MkCond{Not: &MkCond{Empty: varuse("VARNAME")}}) test("!empty(VARNAME:M[yY][eE][sS])", - &mkCond{Not: &mkCond{Empty: varuse("VARNAME", "M[yY][eE][sS]")}}) + &MkCond{Not: &MkCond{Empty: varuse("VARNAME", "M[yY][eE][sS]")}}) // Colons are unescaped at this point because they cannot be mistaken for separators anymore. test("!empty(USE_TOOLS:Mautoconf\\:run)", - &mkCond{Not: &mkCond{Empty: varuse("USE_TOOLS", "Mautoconf:run")}}) + &MkCond{Not: &MkCond{Empty: varuse("USE_TOOLS", "Mautoconf:run")}}) test("${VARNAME} != \"Value\"", - &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}}) + &MkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}}) test("${VARNAME:Mi386} != \"Value\"", - &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME", "Mi386"), "!=", "Value"}}) + &MkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME", "Mi386"), "!=", "Value"}}) test("${VARNAME} != Value", - &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}}) + &MkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}}) test("\"${VARNAME}\" != Value", - &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}}) + &MkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}}) test("${pkg} == \"${name}\"", - &mkCond{CompareVarVar: &MkCondCompareVarVar{varuse("pkg"), "==", varuse("name")}}) + &MkCond{CompareVarVar: &MkCondCompareVarVar{varuse("pkg"), "==", varuse("name")}}) test("\"${pkg}\" == \"${name}\"", - &mkCond{CompareVarVar: &MkCondCompareVarVar{varuse("pkg"), "==", varuse("name")}}) + &MkCond{CompareVarVar: &MkCondCompareVarVar{varuse("pkg"), "==", varuse("name")}}) // The right-hand side is not analyzed further to keep the data types simple. test("${ABC} == \"${A}B${C}\"", - &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("ABC"), "==", "${A}B${C}"}}) + &MkCond{CompareVarStr: &MkCondCompareVarStr{varuse("ABC"), "==", "${A}B${C}"}}) test("${ABC} == \"${A}\\\"${B}\\\\${C}$${shellvar}${D}\"", - &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("ABC"), "==", "${A}\"${B}\\${C}$${shellvar}${D}"}}) + &MkCond{CompareVarStr: &MkCondCompareVarStr{varuse("ABC"), "==", "${A}\"${B}\\${C}$${shellvar}${D}"}}) test("exists(/etc/hosts)", - &mkCond{Call: &MkCondCall{"exists", "/etc/hosts"}}) + &MkCond{Call: &MkCondCall{"exists", "/etc/hosts"}}) test("exists(${PREFIX}/var)", - &mkCond{Call: &MkCondCall{"exists", "${PREFIX}/var"}}) + &MkCond{Call: &MkCondCall{"exists", "${PREFIX}/var"}}) test("${OPSYS} == \"NetBSD\" || ${OPSYS} == \"OpenBSD\"", - &mkCond{Or: []*mkCond{ + &MkCond{Or: []*MkCond{ {CompareVarStr: &MkCondCompareVarStr{varuse("OPSYS"), "==", "NetBSD"}}, {CompareVarStr: &MkCondCompareVarStr{varuse("OPSYS"), "==", "OpenBSD"}}}}) test("${OPSYS} == \"NetBSD\" && ${MACHINE_ARCH} == \"i386\"", - &mkCond{And: []*mkCond{ + &MkCond{And: []*MkCond{ {CompareVarStr: &MkCondCompareVarStr{varuse("OPSYS"), "==", "NetBSD"}}, {CompareVarStr: &MkCondCompareVarStr{varuse("MACHINE_ARCH"), "==", "i386"}}}}) test("defined(A) && defined(B) || defined(C) && defined(D)", - &mkCond{Or: []*mkCond{ - {And: []*mkCond{ + &MkCond{Or: []*MkCond{ + {And: []*MkCond{ {Defined: "A"}, {Defined: "B"}}}, - {And: []*mkCond{ + {And: []*MkCond{ {Defined: "C"}, {Defined: "D"}}}}}) test("${MACHINE_ARCH:Mi386} || ${MACHINE_OPSYS:MNetBSD}", - &mkCond{Or: []*mkCond{ + &MkCond{Or: []*MkCond{ {Var: varuse("MACHINE_ARCH", "Mi386")}, {Var: varuse("MACHINE_OPSYS", "MNetBSD")}}}) + test("${VAR} == \"${VAR}suffix\"", + &MkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VAR"), "==", "${VAR}suffix"}}) + // Exotic cases // ".if 0" can be used to skip over a block of code. test("0", - &mkCond{Num: "0"}) + &MkCond{Num: "0"}) test("0xCAFEBABE", - &mkCond{Num: "0xCAFEBABE"}) + &MkCond{Num: "0xCAFEBABE"}) test("${VAR} == 0xCAFEBABE", - &mkCond{ + &MkCond{ CompareVarNum: &MkCondCompareVarNum{ Var: varuse("VAR"), Op: "==", Num: "0xCAFEBABE"}}) test("! ( defined(A) && empty(VARNAME) )", - &mkCond{Not: &mkCond{ - And: []*mkCond{ + &MkCond{Not: &MkCond{ + And: []*MkCond{ {Defined: "A"}, {Empty: varuse("VARNAME")}}}}) test("${REQD_MAJOR} > ${MAJOR}", - &mkCond{CompareVarVar: &MkCondCompareVarVar{varuse("REQD_MAJOR"), ">", varuse("MAJOR")}}) + &MkCond{CompareVarVar: &MkCondCompareVarVar{varuse("REQD_MAJOR"), ">", varuse("MAJOR")}}) test("${OS_VERSION} >= 6.5", - &mkCond{CompareVarNum: &MkCondCompareVarNum{varuse("OS_VERSION"), ">=", "6.5"}}) + &MkCond{CompareVarNum: &MkCondCompareVarNum{varuse("OS_VERSION"), ">=", "6.5"}}) test("${OS_VERSION} == 5.3", - &mkCond{CompareVarNum: &MkCondCompareVarNum{varuse("OS_VERSION"), "==", "5.3"}}) + &MkCond{CompareVarNum: &MkCondCompareVarNum{varuse("OS_VERSION"), "==", "5.3"}}) test("!empty(${OS_VARIANT:MIllumos})", // Probably not intended - &mkCond{Not: &mkCond{Empty: varuse("${OS_VARIANT:MIllumos}")}}) + &MkCond{Not: &MkCond{Empty: varuse("${OS_VARIANT:MIllumos}")}}) // There may be whitespace before the parenthesis; see devel/bmake/files/cond.c:^compare_function. test("defined (VARNAME)", - &mkCond{Defined: "VARNAME"}) + &MkCond{Defined: "VARNAME"}) test("${\"${PKG_OPTIONS:Moption}\":?--enable-option:--disable-option}", - &mkCond{Var: varuse("\"${PKG_OPTIONS:Moption}\"", "?--enable-option:--disable-option")}) + &MkCond{Var: varuse("\"${PKG_OPTIONS:Moption}\"", "?--enable-option:--disable-option")}) // Contrary to most other programming languages, the == operator binds // more tightly that the ! operator. @@ -534,16 +688,40 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) { // TODO: Since this operator precedence is surprising there should be a warning, // suggesting to replace "!${VAR} == value" with "${VAR} != value". test("!${VAR} == value", - &mkCond{Not: &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VAR"), "==", "value"}}}) + &MkCond{Not: &MkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VAR"), "==", "value"}}}) // Errors + testRest("defined()", + nil, + "defined()") + + testRest("empty()", + nil, + "empty()") + + testRest("empty(UNFINISHED", + nil, + "empty(UNFINISHED") + + testRest("empty(UNFINISHED:Mpattern", + nil, + "empty(UNFINISHED:Mpattern") + + testRest("exists(/$$sys)", + nil, + "exists(/$$sys)") + + testRest("exists(/unfinished", + nil, + "exists(/unfinished") + testRest("!empty(PKG_OPTIONS:Msndfile) || defined(PKG_OPTIONS:Msamplerate)", - &mkCond{Not: &mkCond{Empty: varuse("PKG_OPTIONS", "Msndfile")}}, + &MkCond{Not: &MkCond{Empty: varuse("PKG_OPTIONS", "Msndfile")}}, "|| defined(PKG_OPTIONS:Msamplerate)") testRest("${LEFT} &&", - &mkCond{Var: varuse("LEFT")}, + &MkCond{Var: varuse("LEFT")}, "&&") testRest("\"unfinished string literal", @@ -562,6 +740,61 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) { testRest("${VAR} == \"unfinished string literal", nil, "${VAR} == \"unfinished string literal") + + // A logical not must always be followed by an expression. + testRest("!<", + nil, + "!<") + + // Empty parentheses are a syntax error. + testRest("()", + nil, + "()") + + // Unfinished conditions are a syntax error. + testRest("(${VAR}", + nil, + "(${VAR}") + + // The left-hand side of the comparison can only be a variable. + // FIXME: bmake accepts this, and so should pkglint. + testRest("\"${VAR}suffix\" == value", + nil, + "\"${VAR}suffix\" == value") +} + +func (s *Suite) Test_MkParser_Varname(c *check.C) { + t := s.Init(c) + + test := func(text string) { + line := t.NewLine("filename.mk", 1, text) + p := NewMkParser(line, text, true) + + varname := p.Varname() + + t.Check(varname, equals, text) + t.Check(p.Rest(), equals, "") + } + + testRest := func(text string, expectedVarname string, expectedRest string) { + line := t.NewLine("filename.mk", 1, text) + p := NewMkParser(line, text, true) + + varname := p.Varname() + + t.Check(varname, equals, expectedVarname) + t.Check(p.Rest(), equals, expectedRest) + } + + test("VARNAME") + test("VARNAME.param") + test("VARNAME.${param}") + test("SITES_${param}") + test("SITES_distfile-1.0.tar.gz") + test("SITES.gtk+-2.0") + test("PKGPATH.category/package") + + testRest("VARNAME/rest", "VARNAME", "/rest") } // Pkglint can replace $(VAR) with ${VAR}. It doesn't look at all components @@ -574,7 +807,7 @@ func (s *Suite) Test_MkParser_VarUse__parentheses_autofix(c *check.C) { t.SetUpCommandLine("--autofix") t.SetUpVartypes() lines := t.SetUpFileLines("Makefile", - MkRcsID, + MkCvsID, "COMMENT=$(P1) $(P2)) $(P3:Q) ${BRACES} $(A.$(B.$(C)))") mklines := NewMkLines(lines) @@ -586,7 +819,7 @@ func (s *Suite) Test_MkParser_VarUse__parentheses_autofix(c *check.C) { "AUTOFIX: ~/Makefile:2: Replacing \"$(P3:Q)\" with \"${P3:Q}\".", "AUTOFIX: ~/Makefile:2: Replacing \"$(C)\" with \"${C}\".") t.CheckFileLines("Makefile", - MkRcsID, + MkCvsID, "COMMENT=${P1} ${P2}) ${P3:Q} ${BRACES} $(A.$(B.${C}))") } @@ -718,28 +951,54 @@ func (s *Suite) Test_MkParser_varUseModifierAt(c *check.C) { "") } +func (s *Suite) Test_MkParser_isPkgbasePart(c *check.C) { + + test := func(str string, expected bool) { + actual := (*MkParser)(nil).isPkgbasePart(str) + + c.Check(actual, equals, expected) + } + + test("X11", true) + test("client", true) + test("${PKGNAME}", true) + test("[a-z]", true) + test("{client,server}", true) + + test("1.2", false) + test("[0-9]*", false) + test("{5.[1-7].*,6.[0-9]*}", false) + test("${PKGVERSION}", false) + test("${PKGNAME:C/^.*-//}", false) + test(">=1.0", false) + test("_client", false) // The combination foo-_client looks strange. +} + func (s *Suite) Test_MkParser_PkgbasePattern(c *check.C) { - testRest := func(pattern, expected, rest string) { + test := func(pattern, expected, rest string) { parser := NewMkParser(nil, pattern, false) + actual := parser.PkgbasePattern() + c.Check(actual, equals, expected) c.Check(parser.Rest(), equals, rest) } - testRest("fltk", "fltk", "") - testRest("fltk|", "fltk", "|") - testRest("boost-build-1.59.*", "boost-build", "-1.59.*") - testRest("${PHP_PKG_PREFIX}-pdo-5.*", "${PHP_PKG_PREFIX}-pdo", "-5.*") - testRest("${PYPKGPREFIX}-metakit-[0-9]*", "${PYPKGPREFIX}-metakit", "-[0-9]*") + test("fltk", "fltk", "") + test("fltk-", "fltk", "-") + test("fltk|", "fltk", "|") + test("boost-build-1.59.*", "boost-build", "-1.59.*") + test("${PHP_PKG_PREFIX}-pdo-5.*", "${PHP_PKG_PREFIX}-pdo", "-5.*") + test("${PYPKGPREFIX}-metakit-[0-9]*", "${PYPKGPREFIX}-metakit", "-[0-9]*") - testRest("pkgbase-[0-9]*", "pkgbase", "-[0-9]*") + test("pkgbase-[0-9]*", "pkgbase", "-[0-9]*") - testRest("pkgbase-client-[0-9]*", "pkgbase-client", "-[0-9]*") + test("pkgbase-client-[0-9]*", "pkgbase-client", "-[0-9]*") - testRest("pkgbase-${VARIANT}-[0-9]*", "pkgbase-${VARIANT}", "-[0-9]*") + test("pkgbase-${VARIANT}-[0-9]*", "pkgbase-${VARIANT}", "-[0-9]*") - testRest("pkgbase-${VERSION}-[0-9]*", "pkgbase", "-${VERSION}-[0-9]*") + test("pkgbase-${VERSION}-[0-9]*", "pkgbase", "-${VERSION}-[0-9]*") // This PKGNAME pattern is the one from bsd.pkg.mk. // The pattern assumes that the version number does not contain a hyphen, @@ -748,16 +1007,16 @@ func (s *Suite) Test_MkParser_PkgbasePattern(c *check.C) { // Since variable substitutions are more common for version numbers // than for parts of the package name, pkglint treats the PKGNAME // as a version number. - testRest("pkgbase-${PKGNAME:C/^.*-//}-[0-9]*", "pkgbase", "-${PKGNAME:C/^.*-//}-[0-9]*") + test("pkgbase-${PKGNAME:C/^.*-//}-[0-9]*", "pkgbase", "-${PKGNAME:C/^.*-//}-[0-9]*") // Using the [a-z] pattern in the package base is only rarely seen in the wild. - testRest("pkgbase-[a-z]*-1.0", "pkgbase-[a-z]*", "-1.0") + test("pkgbase-[a-z]*-1.0", "pkgbase-[a-z]*", "-1.0") // This is a valid dependency pattern, but it's more complicated // than the patterns pkglint can handle as of January 2019. // // This pattern doesn't have a single package base, which means it cannot be parsed at all. - testRest("{ssh{,6}-[0-9]*,openssh-[0-9]*}", "", "{ssh{,6}-[0-9]*,openssh-[0-9]*}") + test("{ssh{,6}-[0-9]*,openssh-[0-9]*}", "", "{ssh{,6}-[0-9]*,openssh-[0-9]*}") } func (s *Suite) Test_MkParser_Dependency(c *check.C) { @@ -783,6 +1042,18 @@ func (s *Suite) Test_MkParser_Dependency(c *check.C) { testRest(pattern, expected, "") } + test("pkgbase>=1.0", + DependencyPattern{"pkgbase", ">=", "1.0", "", "", ""}) + + test("pkgbase>1.0", + DependencyPattern{"pkgbase", ">", "1.0", "", "", ""}) + + test("pkgbase<=1.0", + DependencyPattern{"pkgbase", "", "", "<=", "1.0", ""}) + + test("pkgbase<1.0", + DependencyPattern{"pkgbase", "", "", "<", "1.0", ""}) + test("fltk>=1.1.5rc1<1.3", DependencyPattern{"fltk", ">=", "1.1.5rc1", "<", "1.3", ""}) @@ -828,6 +1099,12 @@ func (s *Suite) Test_MkParser_Dependency(c *check.C) { testRest("gnome-control-center>=2.20.1{,nb*}", DependencyPattern{"gnome-control-center", ">=", "2.20.1", "", "", ""}, "{,nb*}") + testNil("pkgbase") + + testNil("pkgbase-") + + testNil("pkgbase-client") + testNil(">=2.20.1{,nb*}") testNil("pkgbase<=") @@ -861,7 +1138,7 @@ func (s *Suite) Test_MkCondWalker_Walk(c *check.C) { var events []string varuseStr := func(varuse *MkVarUse) string { - strs := make([]string, 1+len(varuse.modifiers), 1+len(varuse.modifiers)) + strs := make([]string, 1+len(varuse.modifiers)) strs[0] = varuse.varname for i, mod := range varuse.modifiers { strs[1+i] = mod.Text @@ -922,3 +1199,23 @@ func (s *Suite) Test_MkCondWalker_Walk(c *check.C) { " var NONEMPTY", " varUse NONEMPTY"}) } + +// Ensure that the code works even if none of the callbacks are set. +// This is only for code coverage. +func (s *Suite) Test_MkCondWalker_Walk__empty_callbacks(c *check.C) { + t := s.Init(c) + + mkline := t.NewMkLine("Makefile", 4, ""+ + ".if ${VAR:Mmatch} == ${OTHER} || "+ + "${STR} == Str || "+ + "${VAR} == \"${PRE}text${POST}\" || "+ + "${NUM} == 3 && "+ + "defined(VAR) && "+ + "!exists(file.mk) && "+ + "exists(${FILE}) && "+ + "(((${NONEMPTY})))") + + mkline.Cond().Walk(&MkCondCallback{}) + + t.CheckOutputEmpty() +} diff --git a/pkgtools/pkglint/files/mkshparser.go b/pkgtools/pkglint/files/mkshparser.go index 726d272ca3f..fd0297f2faf 100644 --- a/pkgtools/pkglint/files/mkshparser.go +++ b/pkgtools/pkglint/files/mkshparser.go @@ -2,7 +2,7 @@ package pkglint import "fmt" -func parseShellProgram(line Line, program string) (*MkShList, error) { +func parseShellProgram(line *Line, program string) (*MkShList, error) { if trace.Tracing { defer trace.Call(program)() } @@ -16,7 +16,7 @@ func parseShellProgram(line Line, program string) (*MkShList, error) { switch { case succeeded == 0 && lexer.error == "": return lexer.result, nil - case succeeded == 0 && rest != "": + case succeeded == 0: return nil, fmt.Errorf("splitIntoShellTokens couldn't parse %q", rest) default: return nil, &ParseError{append([]string{lexer.current}, lexer.remaining...)} diff --git a/pkgtools/pkglint/files/mkshparser_test.go b/pkgtools/pkglint/files/mkshparser_test.go index 452ca81c087..8a45a37f9cc 100644 --- a/pkgtools/pkglint/files/mkshparser_test.go +++ b/pkgtools/pkglint/files/mkshparser_test.go @@ -12,7 +12,7 @@ func (s *Suite) Test_parseShellProgram__parse_error_for_dollar(c *check.C) { test := func(text string, expProgram *MkShList, expError error, expDiagnostics ...string) { mklines := t.NewMkLines("module.mk", "\t"+text) - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { program, err := parseShellProgram(mkline.Line, text) if err == nil { diff --git a/pkgtools/pkglint/files/mkshtypes.go b/pkgtools/pkglint/files/mkshtypes.go index 4046c82b701..ea778fe3986 100644 --- a/pkgtools/pkglint/files/mkshtypes.go +++ b/pkgtools/pkglint/files/mkshtypes.go @@ -225,16 +225,11 @@ func (c *StrCommand) AnyArgMatches(pattern regex.Pattern) bool { } func (c *StrCommand) String() string { - var strs []string - for _, assignment := range c.Assignments { - strs = append(strs, assignment) - } + strs := append([]string(nil), c.Assignments...) if c.Name != "" { strs = append(strs, c.Name) } - for _, arg := range c.Args { - strs = append(strs, arg) - } + strs = append(strs, c.Args...) return strings.Join(strs, " ") } diff --git a/pkgtools/pkglint/files/mkshwalker.go b/pkgtools/pkglint/files/mkshwalker.go index 5782626ea98..012b88dcbd6 100644 --- a/pkgtools/pkglint/files/mkshwalker.go +++ b/pkgtools/pkglint/files/mkshwalker.go @@ -71,9 +71,10 @@ func NewMkShWalker() *MkShWalker { func (w *MkShWalker) Path() string { var path []string for _, level := range w.Context { - typeName := reflect.TypeOf(level.Element).Elem().Name() - if typeName == "" && reflect.TypeOf(level.Element).Kind() == reflect.Slice { - typeName = "[]" + reflect.TypeOf(level.Element).Elem().Elem().Name() + elementType := reflect.TypeOf(level.Element) + typeName := elementType.Elem().Name() + if typeName == "" { + typeName = "[]" + elementType.Elem().Elem().Name() } abbreviated := strings.TrimPrefix(typeName, "MkSh") if level.Index == -1 { @@ -91,8 +92,8 @@ func (w *MkShWalker) Path() string { func (w *MkShWalker) Walk(list *MkShList) { w.walkList(-1, list) - // If this fails, the calls to w.push and w.pop are unbalanced. - assertf(len(w.Context) == 0, "MkShWalker.Walk %v", w.Context) + // The calls to w.push and w.pop must be balanced. + assert(len(w.Context) == 0) } func (w *MkShWalker) walkList(index int, list *MkShList) { diff --git a/pkgtools/pkglint/files/mkshwalker_test.go b/pkgtools/pkglint/files/mkshwalker_test.go index a638dd1e347..4052f9cb825 100644 --- a/pkgtools/pkglint/files/mkshwalker_test.go +++ b/pkgtools/pkglint/files/mkshwalker_test.go @@ -7,9 +7,7 @@ func (s *Suite) Test_MkShWalker_Walk(c *check.C) { pathFor := map[string]bool{} outputPathFor := func(kinds ...string) { - for key := range pathFor { - pathFor[key] = false - } + pathFor = make(map[string]bool) for _, kind := range kinds { pathFor[kind] = true } @@ -212,7 +210,7 @@ func (s *Suite) Test_MkShWalker_Walk(c *check.C) { " Word 2") outputPathFor("Redirects", "Redirect", "Word") - test(""+ + test( "echo 'hello world' 1>/dev/null 2>&1 0</dev/random", " List with 1 andOrs", @@ -240,3 +238,55 @@ func (s *Suite) Test_MkShWalker_Walk(c *check.C) { " Word /dev/random", " Path List.AndOr[0].Pipeline[0].Command[0].SimpleCommand.[]MkShRedirection.Redirection[2].ShToken[2]") } + +func (s *Suite) Test_MkShWalker_Walk__empty_callback(c *check.C) { + + test := func(program string) { + list, err := parseShellProgram(dummyLine, program) + assertNil(err, "") + + walker := NewMkShWalker() + walker.Walk(list) + + c.Check(walker.Parent(0), equals, nil) + } + + test("" + + "if condition; then action; else case selector in pattern) case-item-action ;; esac; fi; " + + "set -e; " + + "cd ${WRKSRC}/locale; " + + "for lang in *.po; do " + + " [ \"$${lang}\" = \"wxstd.po\" ] && continue; " + + " ${TOOLS_PATH.msgfmt} -c -o \"$${lang%.po}.mo\" \"$${lang}\"; " + + "done; " + + "while :; do fun() { :; } 1>&2; done") + + test( + "echo 'hello world' 1>/dev/null 2>&1 0</dev/random") +} + +func (s *Suite) Test_MkShWalker_Walk__assertion(c *check.C) { + t := s.Init(c) + + list, err := parseShellProgram(dummyLine, "echo \"hello, world\"") + assertNil(err, "") + + walker := NewMkShWalker() + + // This callback intentionally breaks the assertion. + walker.Callback.Word = func(word *ShToken) { walker.push(0, "extra word") } + + t.ExpectAssert(func() { walker.Walk(list) }) +} + +// Just for code coverage, to keep the main code symmetrical. +func (s *Suite) Test_MkShWalker_walkCommand__empty(c *check.C) { + walker := NewMkShWalker() + walker.walkCommand(0, &MkShCommand{}) +} + +// Just for code coverage, to keep the main code symmetrical. +func (s *Suite) Test_MkShWalker_walkCompoundCommand__empty(c *check.C) { + walker := NewMkShWalker() + walker.walkCompoundCommand(0, &MkShCompoundCommand{}) +} diff --git a/pkgtools/pkglint/files/mktokenslexer_test.go b/pkgtools/pkglint/files/mktokenslexer_test.go index 00926c1e369..b52156463b5 100644 --- a/pkgtools/pkglint/files/mktokenslexer_test.go +++ b/pkgtools/pkglint/files/mktokenslexer_test.go @@ -225,7 +225,6 @@ func (s *Suite) Test_MkTokensLexer__constructor_uses_shared_array(c *check.C) { tokens[0].Text = "modified text" tokens[0].Varuse = NewMkVarUse("MODIFIED", "Mpattern") - tokens = tokens[0:0] t.Check(lexer.Rest(), equals, "modified text") } diff --git a/pkgtools/pkglint/files/mktypes_test.go b/pkgtools/pkglint/files/mktypes_test.go index 0fc7ebb6d9b..70fb3cae63b 100644 --- a/pkgtools/pkglint/files/mktypes_test.go +++ b/pkgtools/pkglint/files/mktypes_test.go @@ -64,7 +64,7 @@ func (s *Suite) Test_MkVarUseModifier_ChangesWords__empty(c *check.C) { mkline := t.NewMkLine("filename.mk", 123, "\t${VAR:}") n := 0 - mkline.ForEachUsed(func(varUse *MkVarUse, time vucTime) { + mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) { n += 100 for _, mod := range varUse.modifiers { mod.ChangesWords() @@ -151,3 +151,16 @@ func (s *Suite) Test_MkVarUseModifier_Subst__no_tracing(c *check.C) { c.Check(ok, equals, true) c.Check(result, equals, "to a to b") } + +// Since the replacement text is not a simple string, the :C modifier +// cannot be treated like the :S modifier. The variable could contain +// one of the special characters that would need to be escaped in the +// replacement text. +func (s *Suite) Test_MkVarUseModifier_Subst__C_with_complex_replacement(c *check.C) { + mod := MkVarUseModifier{"C,from,${VAR},"} + + result, ok := mod.Subst("from a to b") + + c.Check(ok, equals, false) + c.Check(result, equals, "") +} diff --git a/pkgtools/pkglint/files/options.go b/pkgtools/pkglint/files/options.go index e25d4357888..62e044ae9c3 100755 --- a/pkgtools/pkglint/files/options.go +++ b/pkgtools/pkglint/files/options.go @@ -1,10 +1,10 @@ package pkglint -func CheckLinesOptionsMk(mklines MkLines) { +func CheckLinesOptionsMk(mklines *MkLines) { ck := OptionsLinesChecker{ mklines, - make(map[string]MkLine), - make(map[string]MkLine), + make(map[string]*MkLine), + make(map[string]*MkLine), nil} ck.Check() @@ -14,10 +14,10 @@ func CheckLinesOptionsMk(mklines MkLines) { // // See mk/bsd.options.mk for a detailed description. type OptionsLinesChecker struct { - mklines MkLines + mklines *MkLines - declaredOptions map[string]MkLine - handledOptions map[string]MkLine + declaredOptions map[string]*MkLine + handledOptions map[string]*MkLine optionsInDeclarationOrder []string } @@ -27,7 +27,7 @@ func (ck *OptionsLinesChecker) Check() { mklines.Check() mlex := NewMkLinesLexer(mklines) - mlex.SkipWhile(func(mkline MkLine) bool { return mkline.IsComment() || mkline.IsEmpty() }) + mlex.SkipWhile(func(mkline *MkLine) bool { return mkline.IsComment() || mkline.IsEmpty() }) if !ck.lookingAtPkgOptionsVar(mlex) { return @@ -70,7 +70,7 @@ func (ck *OptionsLinesChecker) lookingAtPkgOptionsVar(mlex *MkLinesLexer) bool { // checkLineUpper checks a line from the upper part of an options.mk file, // before bsd.options.mk is included. -func (ck *OptionsLinesChecker) handleUpperLine(mkline MkLine) bool { +func (ck *OptionsLinesChecker) handleUpperLine(mkline *MkLine) bool { switch { case mkline.IsComment(): break @@ -110,7 +110,7 @@ func (ck *OptionsLinesChecker) handleUpperLine(mkline MkLine) bool { return true } -func (ck *OptionsLinesChecker) handleLowerLine(mkline MkLine) { +func (ck *OptionsLinesChecker) handleLowerLine(mkline *MkLine) { if mkline.IsDirective() { directive := mkline.Directive() if directive == "if" || directive == "elif" { @@ -122,7 +122,7 @@ func (ck *OptionsLinesChecker) handleLowerLine(mkline MkLine) { } } -func (ck *OptionsLinesChecker) handleLowerCondition(mkline MkLine, cond MkCond) { +func (ck *OptionsLinesChecker) handleLowerCondition(mkline *MkLine, cond *MkCond) { recordUsedOption := func(varuse *MkVarUse) { if varuse.varname == "PKG_OPTIONS" && len(varuse.modifiers) == 1 { diff --git a/pkgtools/pkglint/files/options_test.go b/pkgtools/pkglint/files/options_test.go index 47b5374b20f..d57114b0559 100755 --- a/pkgtools/pkglint/files/options_test.go +++ b/pkgtools/pkglint/files/options_test.go @@ -16,10 +16,10 @@ func (s *Suite) Test_CheckLinesOptionsMk(c *check.C) { t.SetUpOption("x11", "") t.CreateFileLines("mk/bsd.options.mk", - MkRcsID) + MkCvsID) mklines := t.SetUpFileMkLines("category/package/options.mk", - MkRcsID, + MkCvsID, "", "PKG_OPTIONS_VAR= PKG_OPTIONS.mc", "PKG_OPTIONS_REQUIRED_GROUPS= screen", @@ -76,13 +76,13 @@ func (s *Suite) Test_CheckLinesOptionsMk__edge_cases(c *check.C) { t.SetUpVartypes() t.SetUpOption("option1", "Description for option1") t.CreateFileLines("mk/compiler.mk", - MkRcsID) + MkCvsID) t.CreateFileLines("mk/bsd.options.mk", - MkRcsID) + MkCvsID) t.DisableTracing() mklines := t.SetUpFileMkLines("category/package/options.mk", - MkRcsID) + MkCvsID) CheckLinesOptionsMk(mklines) @@ -90,7 +90,7 @@ func (s *Suite) Test_CheckLinesOptionsMk__edge_cases(c *check.C) { "WARN: ~/category/package/options.mk:EOF: Expected definition of PKG_OPTIONS_VAR.") mklines = t.SetUpFileMkLines("category/package/options.mk", - MkRcsID, + MkCvsID, "PKG_SUPPORTED_OPTIONS=\toption1") CheckLinesOptionsMk(mklines) @@ -99,7 +99,7 @@ func (s *Suite) Test_CheckLinesOptionsMk__edge_cases(c *check.C) { "WARN: ~/category/package/options.mk:2: Expected definition of PKG_OPTIONS_VAR.") mklines = t.SetUpFileMkLines("category/package/options.mk", - MkRcsID, + MkCvsID, "PKG_OPTIONS_VAR=\tPKG_OPTIONS.pkgbase", "PKG_SUPPORTED_OPTIONS=\toption1", ".include \"../../mk/compiler.mk\"") @@ -111,7 +111,7 @@ func (s *Suite) Test_CheckLinesOptionsMk__edge_cases(c *check.C) { "Option \"option1\" should be handled below in an .if block.") mklines = t.SetUpFileMkLines("category/package/options.mk", - MkRcsID, + MkCvsID, "PKG_OPTIONS_VAR=\tPKG_OPTIONS.pkgbase", "PKG_SUPPORTED_OPTIONS=\toption1", ".include \"../../mk/bsd.options.mk\"", @@ -142,10 +142,10 @@ func (s *Suite) Test_CheckLinesOptionsMk__unexpected_line(c *check.C) { t.SetUpVartypes() t.CreateFileLines("mk/bsd.options.mk", - MkRcsID) + MkCvsID) mklines := t.SetUpFileMkLines("category/package/options.mk", - MkRcsID, + MkCvsID, "", "PKG_OPTIONS_VAR= PKG_OPTIONS.mc", "", @@ -169,10 +169,10 @@ func (s *Suite) Test_CheckLinesOptionsMk__malformed_condition(c *check.C) { t.SetUpOption("x11", "") t.CreateFileLines("mk/bsd.options.mk", - MkRcsID) + MkCvsID) mklines := t.SetUpFileMkLines("category/package/options.mk", - MkRcsID, + MkCvsID, "", "PKG_OPTIONS_VAR= PKG_OPTIONS.mc", "PKG_SUPPORTED_OPTIONS= # none", @@ -202,7 +202,7 @@ func (s *Suite) Test_CheckLinesOptionsMk__PLIST_VARS_based_on_PKG_SUPPORTED_OPTI t.SetUpPackage("category/package") t.CreateFileLines("mk/bsd.options.mk") t.SetUpFileMkLines("category/package/options.mk", - MkRcsID, + MkCvsID, "", "PKG_OPTIONS_VAR=\tPKG_OPTIONS.package", "PKG_SUPPORTED_OPTIONS+=\tone", @@ -246,7 +246,7 @@ func (s *Suite) Test_OptionsLinesChecker_handleLowerCondition__foreign_variable( t.SetUpPackage("category/package", ".include \"options.mk\"") t.CreateFileLines("category/package/options.mk", - MkRcsID, + MkCvsID, "", "PKG_OPTIONS_VAR=\tPKG_OPTIONS.package", "PKG_SUPPORTED_OPTIONS=\topt", @@ -273,7 +273,7 @@ func (s *Suite) Test_CheckLinesOptionsMk__autofix(c *check.C) { t.SetUpPackage("category/package", ".include \"options.mk\"") t.CreateFileLines("category/package/options.mk", - MkRcsID, + MkCvsID, "", "PKG_OPTIONS_VAR=\tPKG_OPTIONS.package", "PKG_SUPPORTED_OPTIONS=\t# none", @@ -312,7 +312,7 @@ func (s *Suite) Test_CheckLinesOptionsMk__autofix(c *check.C) { "AUTOFIX: options.mk:10: Replacing \".\" with \". \".") t.CheckFileLinesDetab("options.mk", - MkRcsID, + MkCvsID, "", "PKG_OPTIONS_VAR= PKG_OPTIONS.package", "PKG_SUPPORTED_OPTIONS= # none", diff --git a/pkgtools/pkglint/files/package.go b/pkgtools/pkglint/files/package.go index 9c9fa93cdda..cae544b7d44 100644 --- a/pkgtools/pkglint/files/package.go +++ b/pkgtools/pkglint/files/package.go @@ -26,11 +26,13 @@ type Package struct { EffectivePkgname string // PKGNAME or DISTNAME from the package Makefile, including nb13 EffectivePkgbase string // EffectivePkgname without the version EffectivePkgversion string // The version part of the effective PKGNAME, excluding nb13 - EffectivePkgnameLine MkLine // The origin of the three Effective* values + EffectivePkgnameLine *MkLine // The origin of the three Effective* values Plist PlistContent // Files and directories mentioned in the PLIST files - vars Scope - bl3 map[string]MkLine // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included. + vars Scope + redundant *RedundantScope + + bl3 map[string]*MkLine // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included. // Remembers the Makefile fragments that have already been included. // The key to the map is the filename relative to the package directory. @@ -43,25 +45,24 @@ type Package struct { // TODO: Set an upper limit, to prevent denial of service. included Once - seenMakefileCommon bool // Does the package have any .includes? + // Does the package have any .includes? + seenInclude bool // Files from .include lines that are nested inside .if. // They often depend on OPSYS or on the existence of files in the build environment. - conditionalIncludes map[string]MkLine + conditionalIncludes map[string]*MkLine // Files from .include lines that are not nested. // These are cross-checked with buildlink3.mk whether they are unconditional there, too. - unconditionalIncludes map[string]MkLine + unconditionalIncludes map[string]*MkLine - once Once IgnoreMissingPatches bool // In distinfo, don't warn about patches that cannot be found. } func NewPackage(dir string) *Package { pkgpath := G.Pkgsrc.ToRel(dir) - if strings.Count(pkgpath, "/") != 1 { - assertf(false, "Package directory %q must be two subdirectories below the pkgsrc root %q.", - dir, G.Pkgsrc.File(".")) - } + + // Package directory must be two subdirectories below the pkgsrc root. + assert(strings.Count(pkgpath, "/") == 1) pkg := Package{ dir: dir, @@ -72,10 +73,10 @@ func NewPackage(dir string) *Package { DistinfoFile: "${PKGDIR}/distinfo", // TODO: Redundant, see the vars.Fallback below. Plist: NewPlistContent(), vars: NewScope(), - bl3: make(map[string]MkLine), + bl3: make(map[string]*MkLine), included: Once{}, - conditionalIncludes: make(map[string]MkLine), - unconditionalIncludes: make(map[string]MkLine), + conditionalIncludes: make(map[string]*MkLine), + unconditionalIncludes: make(map[string]*MkLine), } pkg.vars.DefineAll(G.Pkgsrc.UserDefinedVars) @@ -101,6 +102,16 @@ func (pkg *Package) File(relativeFileName string) string { return cleanpath(resolveVariableRefs(nil, pkg.dir+"/"+relativeFileName)) } +// Rel returns the path by which the given filename (as seen from the +// current working directory) can be reached as a relative path from +// the package directory. +// +// Example: +// NewPackage("category/package").Rel("other/package") == "../../other/package" +func (pkg *Package) Rel(filename string) string { + return relpath(pkg.dir, filename) +} + func (pkg *Package) checkPossibleDowngrade() { if trace.Tracing { defer trace.Call0()() @@ -121,14 +132,14 @@ func (pkg *Package) checkPossibleDowngrade() { return } - if change.Action == "Updated" { + if change.Action == Updated { pkgversionNorev := replaceAll(pkgversion, `nb\d+$`, "") - changeNorev := replaceAll(change.Version, `nb\d+$`, "") + changeNorev := replaceAll(change.Version(), `nb\d+$`, "") cmp := pkgver.Compare(pkgversionNorev, changeNorev) switch { case cmp < 0: mkline.Warnf("The package is being downgraded from %s (see %s) to %s.", - change.Version, mkline.Line.RefToLocation(change.Location), pkgversion) + change.Version(), mkline.Line.RefToLocation(change.Location), pkgversion) mkline.Explain( "The files in doc/CHANGES-*, in which all version changes are", "recorded, have a higher version number than what the package says.", @@ -137,9 +148,11 @@ func (pkg *Package) checkPossibleDowngrade() { case cmp > 0 && !isLocallyModified(mkline.Filename): mkline.Notef("Package version %q is greater than the latest %q from %s.", - pkgversion, change.Version, mkline.Line.RefToLocation(change.Location)) + pkgversion, change.Version(), mkline.Line.RefToLocation(change.Location)) mkline.Explain( "Each update to a package should be mentioned in the doc/CHANGES file.", + "That file is used for the quarterly statistics of updated packages.", + "", "To do this after updating a package, run", sprintf("%q,", bmake("cce")), "which is the abbreviation for commit-changes-entry.") @@ -150,13 +163,13 @@ func (pkg *Package) checkPossibleDowngrade() { // checkLinesBuildlink3Inclusion checks whether the package Makefile and // the corresponding buildlink3.mk agree for all included buildlink3.mk // files whether they are included conditionally or unconditionally. -func (pkg *Package) checkLinesBuildlink3Inclusion(mklines MkLines) { +func (pkg *Package) checkLinesBuildlink3Inclusion(mklines *MkLines) { if trace.Tracing { defer trace.Call0()() } // Collect all the included buildlink3.mk files from the file. - includedFiles := make(map[string]MkLine) + includedFiles := make(map[string]*MkLine) for _, mkline := range mklines.mklines { if mkline.IsInclude() { includedFile := mkline.IncludedFile() @@ -178,7 +191,7 @@ func (pkg *Package) checkLinesBuildlink3Inclusion(mklines MkLines) { } } -func (pkg *Package) load() ([]string, MkLines, MkLines) { +func (pkg *Package) load() ([]string, *MkLines, *MkLines) { // Load the package Makefile and all included files, // to collect all used and defined variables and similar data. mklines, allLines := pkg.loadPackageMakefile() @@ -190,9 +203,6 @@ func (pkg *Package) load() ([]string, MkLines, MkLines) { if pkg.Pkgdir != "." { files = append(files, dirglob(pkg.File(pkg.Pkgdir))...) } - if G.Opts.CheckExtra { - files = append(files, dirglob(pkg.File(pkg.Filesdir))...) - } files = append(files, dirglob(pkg.File(pkg.Patchdir))...) if pkg.DistinfoFile != pkg.vars.fallback["DISTINFO_FILE"] { files = append(files, pkg.File(pkg.DistinfoFile)) @@ -206,9 +216,8 @@ func (pkg *Package) load() ([]string, MkLines, MkLines) { !matches(filename, `patch-`) && !contains(filename, pkg.Pkgdir+"/") && !contains(filename, pkg.Filesdir+"/") { - if fragmentMklines := LoadMk(filename, MustSucceed); fragmentMklines != nil { - fragmentMklines.collectUsedVariables() - } + fragmentMklines := LoadMk(filename, MustSucceed) + fragmentMklines.collectUsedVariables() } if hasPrefix(basename, "PLIST") { pkg.loadPlistDirs(filename) @@ -218,7 +227,7 @@ func (pkg *Package) load() ([]string, MkLines, MkLines) { return files, mklines, allLines } -func (pkg *Package) check(filenames []string, mklines, allLines MkLines) { +func (pkg *Package) check(filenames []string, mklines, allLines *MkLines) { haveDistinfo := false havePatches := false @@ -233,19 +242,19 @@ func (pkg *Package) check(filenames []string, mklines, allLines MkLines) { st, err := os.Lstat(filename) switch { case err != nil: - // For missing custom distinfo file, an error message is already generated + // For a missing custom distinfo file, an error message is already generated // for the line where DISTINFO_FILE is defined. // // For all other cases it is next to impossible to reach this branch // since all those files come from calls to dirglob. break - case path.Base(filename) == "Makefile": + case path.Base(filename) == "Makefile" && strings.Count(G.Pkgsrc.ToRel(filename), "/") == 2: G.checkExecutable(filename, st.Mode()) pkg.checkfilePackageMakefile(filename, mklines, allLines) default: - G.checkDirent(filename, st.Mode()) + pkg.checkDirent(filename, st.Mode()) } if contains(filename, "/patches/patch-") { @@ -253,7 +262,8 @@ func (pkg *Package) check(filenames []string, mklines, allLines MkLines) { } else if hasSuffix(filename, "/distinfo") { haveDistinfo = true } - pkg.checkLocallyModified(filename) + pkg.checkOwnerMaintainer(filename) + pkg.checkFreeze(filename) } if pkg.Pkgdir == "." { @@ -267,16 +277,59 @@ func (pkg *Package) check(filenames []string, mklines, allLines MkLines) { } } -func (pkg *Package) loadPackageMakefile() (MkLines, MkLines) { +// checkDirent checks a directory entry based on its filename and its mode +// (regular file, directory, symlink). +func (pkg *Package) checkDirent(dirent string, mode os.FileMode) { + // TODO: merge duplicate code in Pkglint.checkMode + + basename := path.Base(dirent) + + switch { + + case mode.IsRegular(): + pkgsrcRel := G.Pkgsrc.ToRel(dirent) + depth := strings.Count(pkgsrcRel, "/") + G.checkReg(dirent, basename, depth) + + case hasPrefix(basename, "work"): + if G.Opts.Import { + NewLineWhole(dirent).Errorf("Must be cleaned up before committing the package.") + } + return + + case mode.IsDir(): + switch { + case basename == "files", + basename == "patches", + matches(dirent, `(?:^|/)files/[^/]*$`), + isEmptyDir(dirent): + break + + default: + NewLineWhole(dirent).Warnf("Unknown directory name.") + } + + case mode&os.ModeSymlink != 0: + NewLineWhole(dirent).Warnf("Invalid symlink name.") + + default: + NewLineWhole(dirent).Errorf("Only files and directories are allowed in pkgsrc.") + } +} + +func (pkg *Package) loadPackageMakefile() (*MkLines, *MkLines) { filename := pkg.File("Makefile") if trace.Tracing { defer trace.Call1(filename)() } - mainLines := NewMkLines(NewLines(filename, nil)) + mainLines := LoadMk(filename, NotEmpty|LogErrors) + if mainLines == nil { + return nil, nil + } + allLines := NewMkLines(NewLines("", nil)) - if _, result := pkg.readMakefile(filename, mainLines, allLines, ""); !result { - LoadMk(filename, NotEmpty|LogErrors) // Just for the LogErrors. + if !pkg.parse(mainLines, allLines, "") { return nil, nil } @@ -330,162 +383,182 @@ func (pkg *Package) loadPackageMakefile() (MkLines, MkLines) { } // TODO: What is allLines used for, is it still necessary? Would it be better as a field in Package? -func (pkg *Package) readMakefile(filename string, mainLines MkLines, allLines MkLines, includingFileForUsedCheck string) (exists bool, result bool) { +func (pkg *Package) parse(mklines *MkLines, allLines *MkLines, includingFileForUsedCheck string) bool { if trace.Tracing { - defer trace.Call1(filename)() + defer trace.Call1(mklines.lines.Filename)() } - fileMklines := LoadMk(filename, NotEmpty) // TODO: Document why omitting LogErrors is correct here. - if fileMklines == nil { - return false, false - } + result := true - exists = true - result = true + lineAction := func(mkline *MkLine) bool { + result = pkg.parseLine(mklines, mkline, allLines) + return result + } - isMainMakefile := len(mainLines.mklines) == 0 + atEnd := func(mkline *MkLine) {} + mklines.ForEachEnd(lineAction, atEnd) - handleIncludeLine := func(mkline MkLine) YesNoUnknown { - includedFile, incDir, incBase := pkg.findIncludedFile(mkline, filename) + if includingFileForUsedCheck != "" { + mklines.CheckUsedBy(G.Pkgsrc.ToRel(includingFileForUsedCheck)) + } - if includedFile == "" { - return unknown + // For every included buildlink3.mk, include the corresponding builtin.mk + // automatically since the pkgsrc infrastructure does the same. + filename := mklines.lines.Filename + if path.Base(filename) == "buildlink3.mk" { + builtin := cleanpath(path.Dir(filename) + "/builtin.mk") + builtinRel := relpath(pkg.dir, builtin) + if pkg.included.FirstTime(builtinRel) && fileExists(builtin) { + builtinMkLines := LoadMk(builtin, MustSucceed|LogErrors) + pkg.parse(builtinMkLines, allLines, "") } + } - dirname, _ := path.Split(filename) - dirname = cleanpath(dirname) - fullIncluded := dirname + "/" + includedFile - relIncludedFile := relpath(pkg.dir, fullIncluded) - - if !pkg.diveInto(filename, includedFile) { - return unknown - } + return result +} - if !pkg.included.FirstTime(relIncludedFile) { - return unknown - } +func (pkg *Package) parseLine(mklines *MkLines, mkline *MkLine, allLines *MkLines) bool { + allLines.mklines = append(allLines.mklines, mkline) + allLines.lines.Lines = append(allLines.lines.Lines, mkline.Line) - pkg.collectUsedBy(mkline, incDir, incBase, includedFile) + if mkline.IsInclude() { + includingFile := mkline.Filename + includedFile := mkline.IncludedFile() + includedMkLines, skip := pkg.loadIncluded(mkline, includingFile) - if trace.Tracing { - trace.Step1("Including %q.", fullIncluded) + if includedMkLines == nil { + if skip || mklines.indentation.HasExists(includedFile) { + return true // See https://github.com/rillig/pkglint/issues/1 + } + mkline.Errorf("Cannot read %q.", includedFile) + return false } - fullIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", filename, "") - innerExists, innerResult := pkg.readMakefile(fullIncluded, mainLines, allLines, fullIncluding) - if !innerExists { - if fileMklines.indentation.IsCheckedFile(includedFile) { - return yes // See https://github.com/rillig/pkglint/issues/1 - } + filenameForUsedCheck := "" + dir, base := path.Split(includedFile) + if dir != "" && base == "Makefile.common" && dir != "../../"+pkg.Pkgpath+"/" { + filenameForUsedCheck = includingFile + } + if !pkg.parse(includedMkLines, allLines, filenameForUsedCheck) { + 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 + "/" + includedFile - innerExists, innerResult = pkg.readMakefile(fullIncludedFallback, mainLines, allLines, fullIncluding) - } + if mkline.IsVarassign() { + varname, op, value := mkline.Varname(), mkline.Op(), mkline.Value() - if !innerExists { - mkline.Errorf("Cannot read %q.", includedFile) + if op != opAssignDefault || !pkg.vars.Defined(varname) { + if trace.Tracing { + trace.Stepf("varassign(%q, %q, %q)", varname, op, value) } + pkg.vars.Define(varname, mkline) } + } + return true +} - if !innerResult { - result = false - return no - } +// loadIncluded loads the lines from the file given by the .include directive +// in mkline. +// +// The returned lines may be nil in two different cases: if skip is true, +// the included file is not processed further for whatever reason. But if +// skip is false, the file could not be read and an appropriate error message +// has already been logged. +func (pkg *Package) loadIncluded(mkline *MkLine, includingFile string) (includedMklines *MkLines, skip bool) { + includedFile := pkg.resolveIncludedFile(mkline, includingFile) - return unknown + if includedFile == "" { + return nil, true } - lineAction := func(mkline MkLine) bool { - if isMainMakefile { - mainLines.mklines = append(mainLines.mklines, mkline) - mainLines.lines.Lines = append(mainLines.lines.Lines, mkline.Line) - } - allLines.mklines = append(allLines.mklines, mkline) - allLines.lines.Lines = append(allLines.lines.Lines, mkline.Line) + dirname, _ := path.Split(includingFile) + dirname = cleanpath(dirname) + fullIncluded := dirname + "/" + includedFile + relIncludedFile := relpath(pkg.dir, fullIncluded) - if mkline.IsInclude() { - includeResult := handleIncludeLine(mkline) - if includeResult != unknown { - return includeResult == yes - } - } + if !pkg.diveInto(includingFile, includedFile) { + return nil, true + } - if mkline.IsVarassign() { - varname, op, value := mkline.Varname(), mkline.Op(), mkline.Value() + if !pkg.included.FirstTime(relIncludedFile) { + return nil, true + } - if op != opAssignDefault || !pkg.vars.Defined(varname) { - if trace.Tracing { - trace.Stepf("varassign(%q, %q, %q)", varname, op, value) - } - pkg.vars.Define(varname, mkline) - } - } - return true + pkg.collectSeenInclude(mkline, includedFile) + + if trace.Tracing { + trace.Step1("Including %q.", fullIncluded) + } + includedMklines = LoadMk(fullIncluded, 0) + if includedMklines != nil { + return includedMklines, false } - atEnd := func(mkline MkLine) {} - fileMklines.ForEachEnd(lineAction, atEnd) + // Only look in the directory relative to the current file + // and in the package directory; see + // devel/bmake/files/parse.c, function Parse_include_file. + // + // Bmake has a list of include directories that can be specified + // on the command line using the -I option, but pkgsrc doesn't + // make use of that, so pkglint also doesn't need this extra + // complexity. + pkgBasedir := pkg.File(".") - if includingFileForUsedCheck != "" { - fileMklines.CheckForUsedComment(G.Pkgsrc.ToRel(includingFileForUsedCheck)) + // Prevent unnecessary syscalls + if dirname == pkgBasedir { + return nil, false } - // For every included buildlink3.mk, include the corresponding builtin.mk - // automatically since the pkgsrc infrastructure does the same. - if path.Base(filename) == "buildlink3.mk" { - builtin := cleanpath(path.Dir(filename) + "/builtin.mk") - builtinRel := relpath(pkg.dir, builtin) - if pkg.included.FirstTime(builtinRel) && fileExists(builtin) { - pkg.readMakefile(builtin, mainLines, allLines, "") - } + dirname = pkgBasedir + + fullIncludedFallback := dirname + "/" + includedFile + includedMklines = LoadMk(fullIncludedFallback, 0) + if includedMklines == nil { + return nil, false } - return + mkline.Notef("The path to the included file should be %q.", + relpath(path.Dir(mkline.Filename), fullIncludedFallback)) + mkline.Explain( + "The .include directive first searches the file relative to the including file.", + "And if that doesn't exist, falls back to the current directory, which in the", + "case of a pkgsrc package is the package directory.", + "", + "This fallback mechanism is not necessary for pkgsrc, therefore it should not", + "be used. One less thing to learn for package developers.") + + return includedMklines, false } -func (pkg *Package) diveInto(includingFile string, includedFile string) bool { +// diveInto decides whether to load the includedFile. +// +// The includingFile is relative to the current working directory, +// the includedFile is taken directly from the .include directive. +func (*Package) diveInto(includingFile string, includedFile string) bool { - // The variables that appear in these files are largely modeled by - // pkglint in the file vardefs.go. Therefore parsing these files again - // doesn't make much sense. if hasSuffix(includedFile, "/bsd.pkg.mk") || IsPrefs(includedFile) { return false } - // All files that are included from outside of the pkgsrc infrastructure - // are relevant. This is typically mk/compiler.mk or the various - // mk/*.buildlink3.mk files. if !contains(includingFile, "/mk/") { return true } - // The mk/*.buildlink3.mk files often come with a companion file called - // mk/*.builtin.mk, which also defines variables that are visible from - // the package. - // - // This case is needed for getting the redundancy check right. Without it - // there will be warnings about redundant assignments to the - // BUILTIN_CHECK.pthread variable. - if contains(includingFile, "buildlink3.mk") && contains(includedFile, "builtin.mk") { + if hasSuffix(includingFile, "buildlink3.mk") && hasSuffix(includedFile, "builtin.mk") { return true } return false } -func (pkg *Package) collectUsedBy(mkline MkLine, incDir string, incBase string, includedFile string) { +func (pkg *Package) collectSeenInclude(mkline *MkLine, includedFile string) { + if mkline.Basename != "Makefile" { + return + } + + incDir, incBase := path.Split(includedFile) switch { case - mkline.Basename != "Makefile", hasPrefix(incDir, "../../mk/"), incBase == "buildlink3.mk", incBase == "builtin.mk", @@ -494,53 +567,45 @@ func (pkg *Package) collectUsedBy(mkline MkLine, incDir string, incBase string, } if trace.Tracing { - trace.Step1("Including %q sets seenMakefileCommon.", includedFile) + trace.Step1("Including %q sets seenInclude.", includedFile) } - pkg.seenMakefileCommon = true + pkg.seenInclude = true } -func (pkg *Package) findIncludedFile(mkline MkLine, includingFilename string) (includedFile, incDir, incBase string) { +// resolveIncludedFile resolves Makefile variables such as ${PKGPATH} to +// their actual values. +func (pkg *Package) resolveIncludedFile(mkline *MkLine, includingFilename string) string { // TODO: resolveVariableRefs uses G.Pkg implicitly. It should be made explicit. // TODO: Try to combine resolveVariableRefs and ResolveVarsInRelativePath. - includedFile = resolveVariableRefs(nil, mkline.ResolveVarsInRelativePath(mkline.IncludedFile())) + includedFile := resolveVariableRefs(nil, mkline.ResolveVarsInRelativePath(mkline.IncludedFile())) if containsVarRef(includedFile) { if trace.Tracing && !contains(includingFilename, "/mk/") { - trace.Stepf("%s:%s: Skipping include file %q. This may result in false warnings.", + trace.Stepf("%s:%s: Skipping unresolvable include file %q.", mkline.Filename, mkline.Linenos(), includedFile) } - includedFile = "" + return "" } - incDir, incBase = path.Split(includedFile) - if includedFile != "" { - if mkline.Basename != "buildlink3.mk" { - if matches(includedFile, `^\.\./\.\./(.*)/buildlink3\.mk$`) { - pkg.bl3[includedFile] = mkline - if trace.Tracing { - trace.Step1("Buildlink3 file in package: %q", includedFile) - } + if mkline.Basename != "buildlink3.mk" { + if matches(includedFile, `^\.\./\.\./(.*)/buildlink3\.mk$`) { + pkg.bl3[includedFile] = mkline + if trace.Tracing { + trace.Step1("Buildlink3 file in package: %q", includedFile) } } } - return + return includedFile } -func (pkg *Package) checkfilePackageMakefile(filename string, mklines MkLines, allLines MkLines) { +func (pkg *Package) checkfilePackageMakefile(filename string, mklines *MkLines, allLines *MkLines) { if trace.Tracing { defer trace.Call1(filename)() } vars := pkg.vars - if !vars.Defined("PLIST_SRC") && - !vars.Defined("GENERATE_PLIST") && - !vars.Defined("META_PACKAGE") && - !fileExists(pkg.File(pkg.Pkgdir+"/PLIST")) && - !fileExists(pkg.File(pkg.Pkgdir+"/PLIST.common")) { - // TODO: Move these technical details into the explanation, making space for an understandable warning. - NewLineWhole(filename).Warnf("Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.") - } + pkg.checkPlist() if (vars.Defined("NO_CHECKSUM") || vars.Defined("META_PACKAGE")) && isEmptyDir(pkg.File(pkg.Patchdir)) { @@ -570,7 +635,7 @@ func (pkg *Package) checkfilePackageMakefile(filename string, mklines MkLines, a } } - if !vars.Defined("LICENSE") && !vars.Defined("META_PACKAGE") && pkg.once.FirstTime("LICENSE") { + if !vars.Defined("LICENSE") && !vars.Defined("META_PACKAGE") { line := NewLineWhole(filename) line.Errorf("Each package must define its LICENSE.") // TODO: Explain why the LICENSE is necessary. @@ -579,9 +644,9 @@ func (pkg *Package) checkfilePackageMakefile(filename string, mklines MkLines, a sprintf("run %q.", bmake("guess-license"))) } - scope := NewRedundantScope() - scope.Check(allLines) // Updates the variables in the scope - pkg.checkGnuConfigureUseLanguages(scope) + pkg.redundant = NewRedundantScope() + pkg.redundant.Check(allLines) // Updates the variables in the scope + pkg.checkGnuConfigureUseLanguages() pkg.checkUseLanguagesCompilerMk(allLines) pkg.determineEffectivePkgVars() @@ -600,14 +665,68 @@ func (pkg *Package) checkfilePackageMakefile(filename string, mklines MkLines, a } pkg.checkUpdate() + allLines.collectDefinedVariables() // To get the tool definitions mklines.Tools = allLines.Tools // TODO: also copy the other collected data mklines.Check() + pkg.CheckVarorder(mklines) + SaveAutofixChanges(mklines.lines) } -func (pkg *Package) checkGnuConfigureUseLanguages(s *RedundantScope) { +// checkPlist checks whether the package needs a PLIST file, +// or whether that file should be omitted since it is autogenerated. +func (pkg *Package) checkPlist() { + vars := pkg.vars + if vars.Defined("PLIST_SRC") || vars.Defined("GENERATE_PLIST") { + return + } + + needsPlist, line := pkg.needsPlist() + hasPlist := fileExists(pkg.File(pkg.Pkgdir+"/PLIST")) || + fileExists(pkg.File(pkg.Pkgdir+"/PLIST.common")) + + if needsPlist && !hasPlist { + line.Warnf("This package should have a PLIST file.") + line.Explain( + "The PLIST file provides the list of files that will be", + "installed by the package. Having this list ensures that", + "a package update doesn't accidentally modify the list", + "of installed files.", + "", + seeGuide("PLIST issues", "plist")) + } + + if hasPlist && !needsPlist { + line.Warnf("This package should not have a PLIST file.") + } +} + +func (pkg *Package) needsPlist() (bool, *Line) { + vars := pkg.vars + + // TODO: In the below code, it shouldn't be necessary to mention + // each variable name twice. + + if vars.Defined("PERL5_PACKLIST") { + return false, vars.LastDefinition("PERL5_PACKLIST").Line + } + + if vars.Defined("PERL5_USE_PACKLIST") { + needed := strings.ToLower(vars.LastValue("PERL5_USE_PACKLIST")) == "no" + return needed, vars.LastDefinition("PERL5_USE_PACKLIST").Line + } + + if vars.Defined("META_PACKAGE") { + return false, vars.LastDefinition("META_PACKAGE").Line + } + + return true, NewLineWhole(pkg.File("Makefile")) +} + +func (pkg *Package) checkGnuConfigureUseLanguages() { + s := pkg.redundant gnuConfigure := s.vars["GNU_CONFIGURE"] if gnuConfigure == nil || !gnuConfigure.vari.Constant() { @@ -619,7 +738,7 @@ func (pkg *Package) checkGnuConfigureUseLanguages(s *RedundantScope) { return } - var wrongLines []MkLine + var wrongLines []*MkLine for _, mkline := range useLanguages.vari.WriteLocations() { if G.Pkgsrc.IsInfra(mkline.Line.Filename) { @@ -684,7 +803,7 @@ func (pkg *Package) determineEffectivePkgVars() { } } - if pkgname != "" && (pkgname == distname || pkgname == "${DISTNAME}") { + if pkgnameLine != nil && (pkgname == distname || pkgname == "${DISTNAME}") { if pkgnameLine.VarassignComment() == "" { pkgnameLine.Notef("This assignment is probably redundant " + "since PKGNAME is ${DISTNAME} by default.") @@ -693,7 +812,7 @@ func (pkg *Package) determineEffectivePkgVars() { } } - if pkgname == "" && distname != "" && !containsVarRef(distname) && !matches(distname, rePkgname) { + if pkgname == "" && distnameLine != nil && !containsVarRef(distname) && !matches(distname, rePkgname) { distnameLine.Warnf("As DISTNAME is not a valid package name, please define the PKGNAME explicitly.") } @@ -798,12 +917,12 @@ func (pkg *Package) checkUpdate() { // the most common variables appear in a fixed order. // The order itself is a little arbitrary but provides // at least a bit of consistency. -func (pkg *Package) CheckVarorder(mklines MkLines) { +func (pkg *Package) CheckVarorder(mklines *MkLines) { if trace.Tracing { defer trace.Call0()() } - if pkg.seenMakefileCommon { + if pkg.seenInclude { return } @@ -818,77 +937,70 @@ func (pkg *Package) CheckVarorder(mklines MkLines) { ) type Variable struct { - varname string - repetition Repetition + Name string + Repetition Repetition } - type Section struct { - repetition Repetition - vars []Variable - } - - variable := func(name string, repetition Repetition) Variable { return Variable{name, repetition} } - section := func(repetition Repetition, vars ...Variable) Section { return Section{repetition, vars} } + emptyLine := Variable{"", once} // See doc/Makefile-example. // See https://netbsd.org/docs/pkgsrc/pkgsrc.html#components.Makefile. - var sections = []Section{ - section(once, - variable("GITHUB_PROJECT", optional), // either here or below MASTER_SITES - variable("GITHUB_TAG", optional), - variable("DISTNAME", optional), - variable("PKGNAME", optional), - variable("PKGREVISION", optional), - variable("CATEGORIES", once), - variable("MASTER_SITES", many), - variable("GITHUB_PROJECT", optional), // either here or at the very top - variable("GITHUB_TAG", optional), - variable("DIST_SUBDIR", optional), - variable("EXTRACT_SUFX", optional), - variable("DISTFILES", many), - variable("SITES.*", many)), - section(optional, - variable("PATCH_SITES", optional), // or once? - variable("PATCH_SITE_SUBDIR", optional), - variable("PATCHFILES", optional), // or once? - variable("PATCH_DIST_ARGS", optional), - variable("PATCH_DIST_STRIP", optional), - variable("PATCH_DIST_CAT", optional)), - section(once, - variable("MAINTAINER", optional), - variable("OWNER", optional), - variable("HOMEPAGE", optional), - variable("COMMENT", once), - variable("LICENSE", once)), - section(optional, - variable("LICENSE_FILE", optional), - variable("RESTRICTED", optional), - variable("NO_BIN_ON_CDROM", optional), - variable("NO_BIN_ON_FTP", optional), - variable("NO_SRC_ON_CDROM", optional), - variable("NO_SRC_ON_FTP", optional)), - section(optional, - variable("BROKEN_EXCEPT_ON_PLATFORM", many), - variable("BROKEN_ON_PLATFORM", many), - variable("NOT_FOR_PLATFORM", many), - variable("ONLY_FOR_PLATFORM", many), - variable("NOT_FOR_COMPILER", many), - variable("ONLY_FOR_COMPILER", many), - variable("NOT_FOR_UNPRIVILEGED", optional), - variable("ONLY_FOR_UNPRIVILEGED", optional)), - section(optional, - variable("BUILD_DEPENDS", many), - variable("TOOL_DEPENDS", many), - variable("DEPENDS", many))} - - relevantLines := (func() []MkLine { + var variables = []Variable{ + {"GITHUB_PROJECT", optional}, // either here or below MASTER_SITES + {"GITHUB_TAG", optional}, + {"DISTNAME", optional}, + {"PKGNAME", optional}, + {"PKGREVISION", optional}, + {"CATEGORIES", once}, + {"MASTER_SITES", many}, + {"GITHUB_PROJECT", optional}, // either here or at the very top + {"GITHUB_TAG", optional}, + {"DIST_SUBDIR", optional}, + {"EXTRACT_SUFX", optional}, + {"DISTFILES", many}, + {"SITES.*", many}, + emptyLine, + {"PATCH_SITES", optional}, // or once? + {"PATCH_SITE_SUBDIR", optional}, + {"PATCHFILES", optional}, // or once? + {"PATCH_DIST_ARGS", optional}, + {"PATCH_DIST_STRIP", optional}, + {"PATCH_DIST_CAT", optional}, + emptyLine, + {"MAINTAINER", optional}, + {"OWNER", optional}, + {"HOMEPAGE", optional}, + {"COMMENT", once}, + {"LICENSE", once}, + emptyLine, + {"LICENSE_FILE", optional}, + {"RESTRICTED", optional}, + {"NO_BIN_ON_CDROM", optional}, + {"NO_BIN_ON_FTP", optional}, + {"NO_SRC_ON_CDROM", optional}, + {"NO_SRC_ON_FTP", optional}, + emptyLine, + {"BROKEN_EXCEPT_ON_PLATFORM", many}, + {"BROKEN_ON_PLATFORM", many}, + {"NOT_FOR_PLATFORM", many}, + {"ONLY_FOR_PLATFORM", many}, + {"NOT_FOR_COMPILER", many}, + {"ONLY_FOR_COMPILER", many}, + {"NOT_FOR_UNPRIVILEGED", optional}, + {"ONLY_FOR_UNPRIVILEGED", optional}, + emptyLine, + {"BUILD_DEPENDS", many}, + {"TOOL_DEPENDS", many}, + {"DEPENDS", many}} + + relevantLines := (func() []*MkLine { firstRelevant := -1 lastRelevant := -1 relevantVars := make(map[string]bool) - for _, section := range sections { - for _, variable := range section.vars { - relevantVars[variable.varname] = true + for _, variable := range variables { + if variable != emptyLine { + relevantVars[variable.Name] = true } } @@ -931,6 +1043,8 @@ func (pkg *Package) CheckVarorder(mklines MkLines) { return mklines.mklines[firstRelevant : lastRelevant+1] })() + // If there are foreign variables, skip the whole check. + // The check is only intended for the most simple packages. skip := func() bool { interesting := relevantLines @@ -938,78 +1052,88 @@ func (pkg *Package) CheckVarorder(mklines MkLines) { for len(interesting) > 0 && interesting[0].IsComment() { interesting = interesting[1:] } - if len(interesting) > 0 && (interesting[0].IsVarassign() || interesting[0].IsCommentedVarassign()) { + + if len(interesting) > 0 && interesting[0].IsVarassign() { return interesting[0].Varcanon() } return "" } - for _, section := range sections { - for _, variable := range section.vars { - switch variable.repetition { - case optional: - if varcanon() == variable.varname { - interesting = interesting[1:] - } - case once: - if varcanon() == variable.varname { - interesting = interesting[1:] - } else if section.repetition == once { - if variable.varname != "LICENSE" { - if trace.Tracing { - trace.Stepf("Wrong varorder because %s is missing.", variable.varname) - } - return false - } - } - case many: - for varcanon() == variable.varname { - interesting = interesting[1:] - } + for _, variable := range variables { + if variable == emptyLine { + for len(interesting) > 0 && (interesting[0].IsEmpty() || interesting[0].IsComment()) { + interesting = interesting[1:] } + continue } - for len(interesting) > 0 && (interesting[0].IsEmpty() || interesting[0].IsComment()) { - interesting = interesting[1:] + switch variable.Repetition { + case optional: + if varcanon() == variable.Name { + interesting = interesting[1:] + } + case once: + if varcanon() == variable.Name { + interesting = interesting[1:] + } else if variable.Name != "LICENSE" { + if trace.Tracing { + trace.Stepf("Wrong varorder because %s is missing.", variable.Name) + } + return false + } + case many: + for varcanon() == variable.Name { + interesting = interesting[1:] + } } } return len(interesting) == 0 } - if len(relevantLines) == 0 || skip() { - return - } + // canonical returns the canonical ordering of the variables. It mentions all the + // variables that occur in the relevant section, as well as the "once" variables. + canonical := func() string { + var canonical []string + for _, variable := range variables { + if variable == emptyLine { + if canonical[len(canonical)-1] != "empty line" { + canonical = append(canonical, "empty line") + } + continue + } - var canonical []string - for _, section := range sections { - for _, variable := range section.vars { found := false for _, mkline := range relevantLines { - if mkline.IsVarassign() || mkline.IsCommentedVarassign() { - if mkline.Varcanon() == variable.varname { - canonical = append(canonical, mkline.Varname()) - found = true - } + if (mkline.IsVarassign() || mkline.IsCommentedVarassign()) && + mkline.Varcanon() == variable.Name { + + canonical = append(canonical, mkline.Varname()) + found = true + break } } - if !found && section.repetition == once && variable.repetition == once { - canonical = append(canonical, variable.varname) + + if !found && variable.Repetition == once { + canonical = append(canonical, variable.Name) } } - if len(canonical) > 0 && canonical[len(canonical)-1] != "empty line" { - canonical = append(canonical, "empty line") + + if canonical[len(canonical)-1] == "empty line" { + canonical = canonical[:len(canonical)-1] } + return strings.Join(canonical, ", ") } - if len(canonical) > 0 && canonical[len(canonical)-1] == "empty line" { - canonical = canonical[:len(canonical)-1] + + if len(relevantLines) == 0 || skip() { + return } // TODO: This leads to very long and complicated warnings. // Those parts that are correct should not be mentioned, // except if they are helpful for locating the mistakes. mkline := relevantLines[0] - mkline.Warnf("The canonical order of the variables is %s.", strings.Join(canonical, ", ")) + mkline.Warnf("The canonical order of the variables is %s.", canonical()) mkline.Explain( "In simple package Makefiles, some common variables should be", "arranged in a specific order.", @@ -1040,13 +1164,13 @@ func (pkg *Package) checkFileMakefileExt(filename string) { sprintf("content can be queried using %q.", makeHelp("help"))) } -// checkLocallyModified checks files that are about to be committed. +// checkOwnerMaintainer checks files that are about to be committed. // Depending on whether the package has a MAINTAINER or an OWNER, // the wording differs. // // Pkglint assumes that the local username is the same as the NetBSD // username, which fits most scenarios. -func (pkg *Package) checkLocallyModified(filename string) { +func (pkg *Package) checkOwnerMaintainer(filename string) { if trace.Tracing { defer trace.Call(filename)() } @@ -1069,7 +1193,7 @@ func (pkg *Package) checkLocallyModified(filename string) { return } - if !isLocallyModified(filename) || !fileExists(filename) { + if !isLocallyModified(filename) { return } @@ -1089,40 +1213,52 @@ func (pkg *Package) checkLocallyModified(filename string) { } } -func (pkg *Package) checkIncludeConditionally(mkline MkLine, indentation *Indentation) { - conditionalVars := mkline.ConditionalVars() - if len(conditionalVars) == 0 { - conditionalVars = indentation.Varnames() - mkline.SetConditionalVars(conditionalVars) +func (pkg *Package) checkFreeze(filename string) { + freezeStart := G.Pkgsrc.FreezeStart + if freezeStart == "" { + return } - if path.Dir(abspath(mkline.Filename)) == abspath(pkg.File(".")) { - includedFile := mkline.IncludedFile() + if !isLocallyModified(filename) { + return + } - if indentation.IsConditional() { - pkg.conditionalIncludes[includedFile] = mkline - if other := pkg.unconditionalIncludes[includedFile]; other != nil { - mkline.Warnf( - "%q is included conditionally here (depending on %s) "+ - "and unconditionally in %s.", - cleanpath(includedFile), strings.Join(mkline.ConditionalVars(), ", "), mkline.RefTo(other)) - } + line := NewLineWhole(filename) + line.Notef("Pkgsrc is frozen since %s.", freezeStart) + line.Explain( + "During a pkgsrc freeze, changes to pkgsrc should only be made very carefully.", + "See https://www.netbsd.org/developers/pkgsrc/ for the exact rules.") +} - } else { - pkg.unconditionalIncludes[includedFile] = mkline - if other := pkg.conditionalIncludes[includedFile]; other != nil { - mkline.Warnf( - "%q is included unconditionally here "+ - "and conditionally in %s (depending on %s).", - cleanpath(includedFile), mkline.RefTo(other), strings.Join(other.ConditionalVars(), ", ")) - } +func (pkg *Package) checkIncludeConditionally(mkline *MkLine, indentation *Indentation) { + mkline.SetConditionalVars(indentation.Varnames()) + + includedFile := mkline.IncludedFile() + key := pkg.Rel(mkline.IncludedFile()) + + if indentation.IsConditional() { + pkg.conditionalIncludes[key] = mkline + if other := pkg.unconditionalIncludes[key]; other != nil { + mkline.Warnf( + "%q is included conditionally here (depending on %s) "+ + "and unconditionally in %s.", + cleanpath(includedFile), strings.Join(mkline.ConditionalVars(), ", "), mkline.RefTo(other)) } - // TODO: Check whether the conditional variables are the same on both places. - // Ideally they should match, but there may be some differences in internal - // variables, which need to be filtered out before comparing them, like it is - // already done with *_MK variables. + } else { + pkg.unconditionalIncludes[key] = mkline + if other := pkg.conditionalIncludes[key]; other != nil { + mkline.Warnf( + "%q is included unconditionally here "+ + "and conditionally in %s (depending on %s).", + cleanpath(includedFile), mkline.RefTo(other), strings.Join(other.ConditionalVars(), ", ")) + } } + + // TODO: Check whether the conditional variables are the same on both places. + // Ideally they should match, but there may be some differences in internal + // variables, which need to be filtered out before comparing them, like it is + // already done with *_MK variables. } func (pkg *Package) loadPlistDirs(plistFilename string) { @@ -1155,11 +1291,11 @@ func (pkg *Package) AutofixDistinfo(oldSha1, newSha1 string) { // checkUseLanguagesCompilerMk checks that after including mk/compiler.mk // or mk/endian.mk for the first time, there are no more changes to // USE_LANGUAGES, as these would be ignored by the pkgsrc infrastructure. -func (pkg *Package) checkUseLanguagesCompilerMk(mklines MkLines) { +func (pkg *Package) checkUseLanguagesCompilerMk(mklines *MkLines) { var seen Once - handleVarassign := func(mkline MkLine) { + handleVarassign := func(mkline *MkLine) { if mkline.Varname() != "USE_LANGUAGES" { return } @@ -1179,7 +1315,7 @@ func (pkg *Package) checkUseLanguagesCompilerMk(mklines MkLines) { "The file compiler.mk guards itself against multiple inclusion.") } - handleInclude := func(mkline MkLine) { + handleInclude := func(mkline *MkLine) { dirname, _ := path.Split(mkline.Filename) dirname = cleanpath(dirname) fullIncluded := dirname + "/" + mkline.IncludedFile() @@ -1188,7 +1324,7 @@ func (pkg *Package) checkUseLanguagesCompilerMk(mklines MkLines) { seen.FirstTime(relIncludedFile) } - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { switch { case mkline.IsVarassign(): handleVarassign(mkline) diff --git a/pkgtools/pkglint/files/package_test.go b/pkgtools/pkglint/files/package_test.go index 4e0db10e589..34778304f43 100644 --- a/pkgtools/pkglint/files/package_test.go +++ b/pkgtools/pkglint/files/package_test.go @@ -2,6 +2,7 @@ package pkglint import ( "gopkg.in/check.v1" + "os" "strings" ) @@ -9,11 +10,13 @@ func (s *Suite) Test_Package_checkLinesBuildlink3Inclusion__file_but_not_package t := s.Init(c) t.CreateFileLines("category/dependency/buildlink3.mk") + t.CreateFileLines("category/dependency/module.mk") G.Pkg = NewPackage(t.File("category/package")) mklines := t.NewMkLines("category/package/buildlink3.mk", - MkRcsID, + MkCvsID, "", - ".include \"../../category/dependency/buildlink3.mk\"") + ".include \"../../category/dependency/buildlink3.mk\"", + ".include \"../../category/dependency/module.mk\"") G.Pkg.checkLinesBuildlink3Inclusion(mklines) @@ -31,7 +34,7 @@ func (s *Suite) Test_Package_checkLinesBuildlink3Inclusion__package_but_not_file G.Pkg.bl3["../../category/dependency/buildlink3.mk"] = t.NewMkLine("../../category/dependency/buildlink3.mk", 1, "") mklines := t.NewMkLines("category/package/buildlink3.mk", - MkRcsID) + MkCvsID) t.EnableTracingToLog() G.Pkg.checkLinesBuildlink3Inclusion(mklines) @@ -50,8 +53,7 @@ func (s *Suite) Test_Package_checkLinesBuildlink3Inclusion__package_but_not_file func (s *Suite) Test_Package_checkLinesBuildlink3Inclusion__no_tracing(c *check.C) { t := s.Init(c) - t.SetUpPackage("category/package", - "PKGNAME=\tpackage-1.0") + t.SetUpPackage("category/package") t.CreateFileDummyBuildlink3("category/package/buildlink3.mk") t.FinishSetUp() @@ -118,7 +120,7 @@ func (s *Suite) Test_Package_CheckVarorder__only_required_variables(c *check.C) pkg := NewPackage(t.File("x11/9term")) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "DISTNAME=9term", "CATEGORIES=x11", @@ -137,7 +139,7 @@ func (s *Suite) Test_Package_CheckVarorder__with_optional_variables(c *check.C) pkg := NewPackage(t.File("x11/9term")) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "GITHUB_PROJECT=project", "DISTNAME=9term", @@ -158,7 +160,7 @@ func (s *Suite) Test_Package_CheckVarorder__no_tracing(c *check.C) { pkg := NewPackage(t.File("x11/9term")) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "DISTNAME=9term", "CATEGORIES=x11", @@ -179,9 +181,8 @@ func (s *Suite) Test_Package_CheckVarorder__comments_do_not_crash(c *check.C) { t := s.Init(c) pkg := NewPackage(t.File("x11/9term")) - - pkg.CheckVarorder(t.NewMkLines("Makefile", - MkRcsID, + mklines := t.NewMkLines("Makefile", + MkCvsID, "", "GITHUB_PROJECT=project", "", @@ -189,7 +190,9 @@ func (s *Suite) Test_Package_CheckVarorder__comments_do_not_crash(c *check.C) { "", "DISTNAME=9term", "# comment", - "CATEGORIES=x11")) + "CATEGORIES=x11") + + pkg.CheckVarorder(mklines) t.CheckOutputLines( "WARN: Makefile:3: The canonical order of the variables is " + @@ -201,9 +204,8 @@ func (s *Suite) Test_Package_CheckVarorder__comments_are_ignored(c *check.C) { t := s.Init(c) pkg := NewPackage(t.File("x11/9term")) - - pkg.CheckVarorder(t.NewMkLines("Makefile", - MkRcsID, + mklines := t.NewMkLines("Makefile", + MkCvsID, "", "DISTNAME=\tdistname-1.0", "CATEGORIES=\tsysutils", @@ -211,17 +213,62 @@ func (s *Suite) Test_Package_CheckVarorder__comments_are_ignored(c *check.C) { "MAINTAINER=\tpkgsrc-users@NetBSD.org", "# comment", "COMMENT=\tComment", - "LICENSE=\tgnu-gpl-v2")) + "LICENSE=\tgnu-gpl-v2") + + pkg.CheckVarorder(mklines) t.CheckOutputEmpty() } +func (s *Suite) Test_Package_CheckVarorder__commented_variable_assignment(c *check.C) { + t := s.Init(c) + + pkg := NewPackage(t.File("x11/9term")) + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "DISTNAME=\tdistname-1.0", + "CATEGORIES=\tsysutils", + "", + "MAINTAINER=\tpkgsrc-users@NetBSD.org", + "#HOMEPAGE=\thttps://example.org/", + "COMMENT=\tComment", + "LICENSE=\tgnu-gpl-v2") + + pkg.CheckVarorder(mklines) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_CheckVarorder__skip_because_of_foreign_variable(c *check.C) { + t := s.Init(c) + + pkg := NewPackage(t.File("x11/9term")) + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "DISTNAME=\tdistname-1.0", + "USE_TOOLS+=gmake", + "CATEGORIES=\tsysutils", + "", + "MAINTAINER=\tpkgsrc-users@NetBSD.org", + "#HOMEPAGE=\thttps://example.org/", + "COMMENT=\tComment", + "LICENSE=\tgnu-gpl-v2") + + t.EnableTracingToLog() + pkg.CheckVarorder(mklines) + + t.CheckOutputLinesMatching(`.*varorder.*`, + "TRACE: 1 Skipping varorder because of line 4.") +} + func (s *Suite) Test_Package_CheckVarorder__skip_if_there_are_directives(c *check.C) { t := s.Init(c) pkg := NewPackage(t.File("category/package")) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "DISTNAME=\tdistname-1.0", "CATEGORIES=\tsysutils", @@ -249,9 +296,8 @@ func (s *Suite) Test_Package_CheckVarorder__GITHUB_PROJECT_at_the_top(c *check.C t := s.Init(c) pkg := NewPackage(t.File("x11/9term")) - - pkg.CheckVarorder(t.NewMkLines("Makefile", - MkRcsID, + mklines := t.NewMkLines("Makefile", + MkCvsID, "", "GITHUB_PROJECT=\t\tautocutsel", "DISTNAME=\t\tautocutsel-0.10.0", @@ -260,7 +306,9 @@ func (s *Suite) Test_Package_CheckVarorder__GITHUB_PROJECT_at_the_top(c *check.C "GITHUB_TAG=\t\t${PKGVERSION_NOREV}", "", "COMMENT=\tComment", - "LICENSE=\tgnu-gpl-v2")) + "LICENSE=\tgnu-gpl-v2") + + pkg.CheckVarorder(mklines) t.CheckOutputEmpty() } @@ -269,9 +317,8 @@ func (s *Suite) Test_Package_CheckVarorder__GITHUB_PROJECT_at_the_bottom(c *chec t := s.Init(c) pkg := NewPackage(t.File("x11/9term")) - - pkg.CheckVarorder(t.NewMkLines("Makefile", - MkRcsID, + mklines := t.NewMkLines("Makefile", + MkCvsID, "", "DISTNAME=\t\tautocutsel-0.10.0", "CATEGORIES=\t\tx11", @@ -280,7 +327,9 @@ func (s *Suite) Test_Package_CheckVarorder__GITHUB_PROJECT_at_the_bottom(c *chec "GITHUB_TAG=\t\t${PKGVERSION_NOREV}", "", "COMMENT=\tComment", - "LICENSE=\tgnu-gpl-v2")) + "LICENSE=\tgnu-gpl-v2") + + pkg.CheckVarorder(mklines) t.CheckOutputEmpty() } @@ -289,10 +338,10 @@ func (s *Suite) Test_Package_CheckVarorder__license(c *check.C) { t := s.Init(c) t.CreateFileLines("mk/bsd.pkg.mk", "# dummy") - t.CreateFileLines("x11/Makefile", MkRcsID) - t.CreateFileLines("x11/9term/PLIST", PlistRcsID, "bin/9term") + t.CreateFileLines("x11/Makefile", MkCvsID) + t.CreateFileLines("x11/9term/PLIST", PlistCvsID, "bin/9term") t.CreateFileLines("x11/9term/Makefile", - MkRcsID, + MkCvsID, "", "DISTNAME=\t9term-1.0", "CATEGORIES=\tx11", @@ -318,9 +367,8 @@ func (s *Suite) Test_Package_CheckVarorder__MASTER_SITES(c *check.C) { t := s.Init(c) pkg := NewPackage(t.File("category/package")) - - pkg.CheckVarorder(t.NewMkLines("Makefile", - MkRcsID, + mklines := t.NewMkLines("Makefile", + MkCvsID, "", "PKGNAME=\tpackage-1.0", "CATEGORIES=\tcategory", @@ -328,7 +376,9 @@ func (s *Suite) Test_Package_CheckVarorder__MASTER_SITES(c *check.C) { "MASTER_SITES+=\thttp://mirror.example.org/", "", "COMMENT=\tComment", - "LICENSE=\tgnu-gpl-v2")) + "LICENSE=\tgnu-gpl-v2") + + pkg.CheckVarorder(mklines) // No warning that "MASTER_SITES appears too late" t.CheckOutputEmpty() @@ -339,9 +389,8 @@ func (s *Suite) Test_Package_CheckVarorder__diagnostics(c *check.C) { t.SetUpVartypes() pkg := NewPackage(t.File("category/package")) - - pkg.CheckVarorder(t.NewMkLines("Makefile", - MkRcsID, + mklines := t.NewMkLines("Makefile", + MkCvsID, "", "CATEGORIES= net", "", @@ -357,7 +406,9 @@ func (s *Suite) Test_Package_CheckVarorder__diagnostics(c *check.C) { "MAINTAINER= maintainer@example.org", "HOMEPAGE= https://github.com/project/pkgbase/", "", - ".include \"../../mk/bsd.pkg.mk\"")) + ".include \"../../mk/bsd.pkg.mk\"") + + pkg.CheckVarorder(mklines) t.CheckOutputLines( "WARN: Makefile:3: The canonical order of the variables is " + @@ -366,8 +417,8 @@ func (s *Suite) Test_Package_CheckVarorder__diagnostics(c *check.C) { "MAINTAINER, HOMEPAGE, COMMENT, LICENSE.") // After moving the variables according to the warning: - pkg.CheckVarorder(t.NewMkLines("Makefile", - MkRcsID, + mklines = t.NewMkLines("Makefile", + MkCvsID, "", "GITHUB_PROJECT= pkgbase", "DISTNAME= v1.0", @@ -381,11 +432,131 @@ func (s *Suite) Test_Package_CheckVarorder__diagnostics(c *check.C) { "COMMENT= Comment", "LICENSE= gnu-gpl-v3", "", - ".include \"../../mk/bsd.pkg.mk\"")) + ".include \"../../mk/bsd.pkg.mk\"") + + pkg.CheckVarorder(mklines) t.CheckOutputEmpty() } +func (s *Suite) Test_Package_CheckVarorder__comment_at_end_of_section(c *check.C) { + t := s.Init(c) + + t.SetUpVartypes() + pkg := NewPackage(t.File("category/package")) + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "CATEGORIES= net", + "SITES.*= # none", + "# comment after the last variable of a section", + "", + "MAINTAINER= maintainer@example.org", + "HOMEPAGE= https://github.com/project/pkgbase/", + "COMMENT= Comment", + "LICENSE= gnu-gpl-v3", + "", + ".include \"../../mk/bsd.pkg.mk\"") + + t.EnableTracingToLog() + pkg.CheckVarorder(mklines) + + // The varorder code is not skipped, not even because of the comment + // after SITES.*. + t.CheckOutputLinesMatching(`.*varorder.*`, + nil...) +} + +func (s *Suite) Test_Package_CheckVarorder__comments_between_sections(c *check.C) { + t := s.Init(c) + + t.SetUpVartypes() + pkg := NewPackage(t.File("category/package")) + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "CATEGORIES= net", + "", + "# comment 1", + "", + "# comment 2", + "", + "MAINTAINER= maintainer@example.org", + "HOMEPAGE= https://github.com/project/pkgbase/", + "COMMENT= Comment", + "LICENSE= gnu-gpl-v3", + "", + ".include \"../../mk/bsd.pkg.mk\"") + + pkg.CheckVarorder(mklines) + + // The empty line between the comments is not treated as a section separator. + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_CheckVarorder__commented_varassign(c *check.C) { + t := s.Init(c) + + t.SetUpVartypes() + pkg := NewPackage(t.File("category/package")) + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "CATEGORIES= net", + "#MASTER_SITES= # none", + "", + "HOMEPAGE= https://github.com/project/pkgbase/", + "#HOMEPAGE= https://github.com/project/pkgbase/", + "#HOMEPAGE= https://github.com/project/pkgbase/", + "#HOMEPAGE= https://github.com/project/pkgbase/", + "#HOMEPAGE= https://github.com/project/pkgbase/", + "LICENSE= gnu-gpl-v3", + "COMMENT= Comment", + "", + ".include \"../../mk/bsd.pkg.mk\"") + + pkg.CheckVarorder(mklines) + + // The order of the variables LICENSE and COMMENT is intentionally + // wrong to force the warning. + // + // Up to June 2019 (308099138a62) pkglint mentioned in the warning + // each commented variable assignment, even repeatedly for the same + // variable name. + // + // These variable assignments should be in the correct order, even + // if they are commented out. It's not necessary though to list a + // variable more than once. + t.CheckOutputLines( + "WARN: Makefile:3: The canonical order of the variables is " + + "CATEGORIES, MASTER_SITES, empty line, HOMEPAGE, COMMENT, LICENSE.") +} + +func (s *Suite) Test_Package_CheckVarorder__DEPENDS(c *check.C) { + t := s.Init(c) + + t.SetUpVartypes() + pkg := NewPackage(t.File("category/package")) + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "CATEGORIES= net", + "", + "COMMENT= Comment", + "LICENSE= license", + "MAINTAINER= maintainer@example.org", // In wrong order + "", + "DEPENDS+= dependency>=1.0:../../category/dependency", + "", + ".include \"../../mk/bsd.pkg.mk\"") + + pkg.CheckVarorder(mklines) + + t.CheckOutputLines( + "WARN: Makefile:3: The canonical order of the variables is " + + "CATEGORIES, empty line, MAINTAINER, COMMENT, LICENSE, empty line, DEPENDS.") +} + func (s *Suite) Test_Package_nbPart(c *check.C) { t := s.Init(c) @@ -450,6 +621,19 @@ func (s *Suite) Test_Package_determineEffectivePkgVars__simple_reference(c *chec "This assignment is probably redundant since PKGNAME is ${DISTNAME} by default.") } +func (s *Suite) Test_Package_determineEffectivePkgVars__commented(c *check.C) { + t := s.Init(c) + + pkg := t.SetUpPackage("category/package", + "DISTNAME=\tdistname-1.0", + "PKGNAME=\t${DISTNAME} # intentionally") + t.FinishSetUp() + + G.Check(pkg) + + t.CheckOutputEmpty() +} + func (s *Suite) Test_Package_determineEffectivePkgVars__invalid_DISTNAME(c *check.C) { t := s.Init(c) @@ -464,6 +648,20 @@ func (s *Suite) Test_Package_determineEffectivePkgVars__invalid_DISTNAME(c *chec "As DISTNAME is not a valid package name, please define the PKGNAME explicitly.") } +func (s *Suite) Test_Package_determineEffectivePkgVars__indirect_DISTNAME(c *check.C) { + t := s.Init(c) + + pkg := t.SetUpPackage("category/package", + "DISTNAME=\t${DISTFILES:[1]:C,\\..*,,}") + t.FinishSetUp() + + G.Check(pkg) + + // No warning since the case of DISTNAME being dependent on another + // variable is too difficult to analyze. + t.CheckOutputEmpty() +} + func (s *Suite) Test_Package_determineEffectivePkgVars__C_modifier(c *check.C) { t := s.Init(c) @@ -516,13 +714,62 @@ func (s *Suite) Test_Package_checkPossibleDowngrade(c *check.C) { t.CheckOutputLines( "WARN: Makefile:5: The package is being downgraded from 1.8 (see ../../doc/CHANGES-2018:1) to 1.0nb15.") - G.Pkgsrc.LastChange["category/pkgbase"].Version = "1.0nb22" + G.Pkgsrc.LastChange["category/pkgbase"].target = "1.0nb22" G.Pkg.checkPossibleDowngrade() t.CheckOutputEmpty() } +func (s *Suite) Test_Package_checkPossibleDowngrade__moved(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/pkgbase", + "PKGNAME=\tpackage-1.0") + t.CreateFileLines("doc/CHANGES-2018", + "\tUpdated category/old-package to 1.8 [committer 2018-01-05]", + "\tMoved category/old-package to category/pkgbase [committer 2018-01-05]") + t.FinishSetUp() + + pkg := NewPackage(t.File("category/pkgbase")) + pkg.load() + pkg.determineEffectivePkgVars() + pkg.checkPossibleDowngrade() + + t.Check(G.Pkgsrc.LastChange["category/pkgbase"].Action, equals, Moved) + // No warning because the latest action is not Updated. + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkPossibleDowngrade__locally_modified_update(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + "PKGNAME=\tpackage-1.8") + t.CreateFileLines("doc/CHANGES-2018", + "\tUpdated category/package to 1.0 [committer 2018-01-05]") + t.CreateFileLines("category/package/CVS/Entries", + "/Makefile//modified//") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + // Since the Makefile is locally modified, pkglint doesn't issue + // any warning since it assumes the package is being upgraded. + t.CheckOutputEmpty() + + // When the Makefile is no longer locally modified, the warning + // is activated again. + t.Remove("category/package/CVS/Entries") + G.cvsEntriesDir = "" + + G.Check(t.File("category/package")) + + t.CheckOutputLines( + "NOTE: ~/category/package/Makefile:4: Package version \"1.8\" " + + "is greater than the latest \"1.0\" from ../../doc/CHANGES-2018:1.") +} + func (s *Suite) Test_Package_loadPackageMakefile__dump(c *check.C) { t := s.Init(c) @@ -530,17 +777,17 @@ func (s *Suite) Test_Package_loadPackageMakefile__dump(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("category/Makefile") t.CreateFileLines("category/package/PLIST", - PlistRcsID, + PlistCvsID, "bin/program") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "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, + MkCvsID, "", "CATEGORIES=category", "", @@ -554,7 +801,7 @@ func (s *Suite) Test_Package_loadPackageMakefile__dump(c *check.C) { t.CheckOutputLines( "Whole Makefile (with all included files) follows:", - "~/category/package/Makefile:1: "+MkRcsID, + "~/category/package/Makefile:1: "+MkCvsID, "~/category/package/Makefile:2: ", "~/category/package/Makefile:3: CATEGORIES=category", "~/category/package/Makefile:4: ", @@ -577,7 +824,7 @@ func (s *Suite) Test_Package__varuse_at_load_time(c *check.C) { "_TOOLS_VARNAME.nice=NICE") t.CreateFileLines("category/pkgbase/Makefile", - MkRcsID, + MkCvsID, "", "PKGNAME= loadtime-vartest-1.0", "CATEGORIES= misc", @@ -652,11 +899,102 @@ func (s *Suite) Test_Package__varuse_at_load_time(c *check.C) { "NOTE: ~/category/pkgbase/Makefile:26: Consider the :sh modifier instead of != for \"echo true=${TRUE:Q}\".") } +// Demonstrates that Makefile fragments are handled differently, +// depending on the directory they are in. +func (s *Suite) Test_Package_load__extra_files(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + "PKGDIR=\t../../category/other") + t.SetUpPackage("category/other") + t.Chdir("category/package") + t.CreateFileLines("gnu-style.mk", + "ifeq ($(CC),gcc)", + "IS_GCC=\tyes", + "else", + "IS_GCC=\tno", + "endif") + t.CreateFileLines("patches/patch-Makefile.mk", + CvsID, + "", + "Documentation", + "", + "--- Makefile.mk.orig", + "--- Makefile.mk", + "@@ -1,1 +1,1 @@", + "- old", + "+ new") + t.CreateFileLines("patches/readme.mk", + "This is not a BSD-style Makefile.") + t.Copy("gnu-style.mk", "files/gnu-style.mk") + t.Copy("gnu-style.mk", "../../category/other/gnu-style.mk") + + t.FinishSetUp() + + G.Check(".") + + t.CheckOutputLines( + // All *.mk files in the package directory are assumed + // to be BSD-style Makefiles, therefore the many warnings. + "WARN: gnu-style.mk:1: Please use curly braces {} instead of round parentheses () for CC.", + "ERROR: gnu-style.mk:1: Unknown Makefile line format: \"ifeq ($(CC),gcc)\".", + "ERROR: gnu-style.mk:3: Unknown Makefile line format: \"else\".", + "ERROR: gnu-style.mk:5: Unknown Makefile line format: \"endif\".", + + // Since the patches directory should contain only patches, + // each other file is treated as a file belonging to pkgsrc, + // therefore *.mk is interpreted as a Makefile fragment. + "ERROR: patches/readme.mk:1: Unknown Makefile line format: \"This is not a BSD-style Makefile.\".", + "ERROR: distinfo: Patch \"patches/patch-Makefile.mk\" is not recorded. Run \""+confMake+" makepatchsum\".", + + // The following diagnostics are duplicated because the files from + // the package directory are loaded once during Package.load, just + // for collecting the used variables. And then a second time in + // Package.check to perform the actual checks. + // + // The above diagnostics are only those from parsing the file, to + // correctly classify the lines. This is because the main purpose + // of Package.load above is to load the files and collect some + // data, not to perform the actual checks. + // + // Therefore, the below lines contain two more diagnostics. + "WARN: gnu-style.mk:1: Please use curly braces {} instead of round parentheses () for CC.", + "ERROR: gnu-style.mk:1: Unknown Makefile line format: \"ifeq ($(CC),gcc)\".", + "ERROR: gnu-style.mk:3: Unknown Makefile line format: \"else\".", + "ERROR: gnu-style.mk:5: Unknown Makefile line format: \"endif\".", + "ERROR: gnu-style.mk:1: Expected \"# $NetBSD: package_test.go,v 1.48 2019/06/30 20:56:19 rillig Exp $\".", + "WARN: gnu-style.mk:2: IS_GCC is defined but not used.", + + // There is no warning about files/gnu-style.mk since pkglint + // doesn't even attempt at guessing the file type. Files placed + // in this directory can have an arbitrary format. + + "ERROR: ../../category/other/distinfo: Patch \"../../category/package/patches/"+ + "patch-Makefile.mk\" is not recorded. Run \""+confMake+" makepatchsum\".", + + // All *.mk files from PKGDIR are loaded to see which variables + // they define, in order to make the check for unused variables + // more reliable. + // + // All files that belong to the package itself, and not to pkgsrc + // should therefore be placed in the files/ directory. + "WARN: ../../category/other/gnu-style.mk:1: "+ + "Please use curly braces {} instead of round parentheses () for CC.", + "ERROR: ../../category/other/gnu-style.mk:1: Unknown Makefile line format: \"ifeq ($(CC),gcc)\".", + "ERROR: ../../category/other/gnu-style.mk:3: Unknown Makefile line format: \"else\".", + "ERROR: ../../category/other/gnu-style.mk:5: Unknown Makefile line format: \"endif\".", + "ERROR: ../../category/other/gnu-style.mk:1: Expected \"# $NetBSD: package_test.go,v 1.48 2019/06/30 20:56:19 rillig Exp $\".", + "WARN: ../../category/other/gnu-style.mk:2: IS_GCC is defined but not used.", + + "ERROR: patches/patch-Makefile.mk: Contains no patch.", + "WARN: patches/readme.mk: Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.") +} + func (s *Suite) Test_Package_loadPackageMakefile(c *check.C) { t := s.Init(c) t.CreateFileLines("category/package/Makefile", - MkRcsID, + MkCvsID, "", "PKGNAME=pkgname-1.67", "DISTNAME=distfile_1_67", @@ -680,7 +1018,7 @@ func (s *Suite) Test_Package__relative_included_filenames_in_same_directory(c *c "DISTNAME=\tdistfile_1_67", ".include \"../../category/package/other.mk\"") t.CreateFileLines("category/package/other.mk", - MkRcsID, + MkCvsID, "PKGNAME=\tpkgname-1.67", "DISTNAME=\tdistfile_1_67", ".include \"../../category/package/other.mk\"") @@ -706,7 +1044,7 @@ func (s *Suite) Test_Package_loadPackageMakefile__PECL_VERSION(c *check.C) { t := s.Init(c) t.CreateFileLines("lang/php/ext.mk", - MkRcsID, + MkCvsID, "", "PHPEXT_MK= # defined", "PHPPKGSRCDIR= ../../lang/php72", @@ -728,6 +1066,109 @@ func (s *Suite) Test_Package_loadPackageMakefile__PECL_VERSION(c *check.C) { G.Check(pkg) } +func (s *Suite) Test_Package_check__files_Makefile(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.CreateFileLines("category/package/files/Makefile", + "This file may contain anything.") + + t.Main("category/package/files/Makefile") + + // Since there is nothing to check in files/*, pkglint could + // as well report this as a usage error. + // + // Until June 2019, checking individual files in FILESDIR had + // been enabled by the -Call command line option. + t.CheckOutputLines( + "Looks fine.") + + t.Main("category/package") + + t.CheckOutputLines( + "Looks fine.") +} + +func (s *Suite) Test_Package_check__patches_Makefile(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.CreateFileLines("category/package/patches/Makefile", + "This file may contain anything.") + + t.Main("category/package") + + t.CheckOutputLines( + "WARN: ~/category/package/patches/Makefile: Patch files should be "+ + "named \"patch-\", followed by letters, '-', '_', '.', and digits only.", + "0 errors and 1 warning found.") +} + +func (s *Suite) Test_Package_checkDirent__errors(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("-Call", "-Wall,no-space") + t.SetUpPkgsrc() + t.CreateFileLines("category/package/files/subdir/file") + t.CreateFileLines("category/package/files/subdir/subsub/file") + t.FinishSetUp() + + pkg := NewPackage(t.File("category/package")) + pkg.checkDirent(t.File("category/package/options.mk"), 0444) + pkg.checkDirent(t.File("category/package/files/subdir"), 0555|os.ModeDir) + pkg.checkDirent(t.File("category/package/files/subdir/subsub"), 0555|os.ModeDir) + pkg.checkDirent(t.File("category/package/files"), 0555|os.ModeDir) + + t.CheckOutputLines( + "ERROR: ~/category/package/options.mk: Cannot be read.", + "WARN: ~/category/package/files/subdir/subsub: Unknown directory name.") +} + +func (s *Suite) Test_Package_checkDirent__file_selection(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("-Call", "-Wall,no-space") + t.SetUpPkgsrc() + t.CreateFileLines("doc/CHANGES-2018", + CvsID) + t.CreateFileLines("category/package/buildlink3.mk", + MkCvsID) + t.CreateFileLines("category/package/unexpected.txt", + CvsID) + t.FinishSetUp() + + pkg := NewPackage(t.File("category/package")) + pkg.checkDirent(t.File("doc/CHANGES-2018"), 0444) + pkg.checkDirent(t.File("category/package/buildlink3.mk"), 0444) + pkg.checkDirent(t.File("category/package/unexpected.txt"), 0444) + + t.CheckOutputLines( + "WARN: ~/category/package/buildlink3.mk:EOF: Expected a BUILDLINK_TREE line.", + "WARN: ~/category/package/unexpected.txt: Unexpected file found.") +} + +// Since all required information is passed to G.checkDirent via parameters, +// this test produces the expected results even though none of these files actually exists. +func (s *Suite) Test_Package_checkDirent__skipped(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.FinishSetUp() + t.Chdir("category/package") + pkg := NewPackage(".") + + pkg.checkDirent("work", os.ModeSymlink) + pkg.checkDirent("work.i386", os.ModeSymlink) + pkg.checkDirent("work.hostname", os.ModeSymlink) + pkg.checkDirent("other", os.ModeSymlink) + + pkg.checkDirent("device", os.ModeDevice) + + t.CheckOutputLines( + "WARN: other: Invalid symlink name.", + "ERROR: device: Only files and directories are allowed in pkgsrc.") +} + func (s *Suite) Test_Package_checkIncludeConditionally__conditional_and_unconditional_include(c *check.C) { t := s.Init(c) @@ -741,7 +1182,7 @@ func (s *Suite) Test_Package_checkIncludeConditionally__conditional_and_uncondit t.CreateFileLines("sysutils/coreutils/buildlink3.mk", "") t.CreateFileLines("category/package/options.mk", - MkRcsID, + MkCvsID, "", ".if !empty(PKG_OPTIONS:Mzlib)", ". include \"../../devel/zlib/buildlink3.mk\"", @@ -762,6 +1203,66 @@ func (s *Suite) Test_Package_checkIncludeConditionally__conditional_and_uncondit "WARN: options.mk:3: Expected definition of PKG_OPTIONS_VAR.") } +func (s *Suite) Test_Package_checkIncludeConditionally__mixed(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.Chdir("category/package") + t.CreateFileLines("including.mk", + MkCvsID, + "", + ".include \"included.mk\"", + ".if ${OPSYS} == \"Linux\"", + ".include \"included.mk\"", + ".endif", + "", + ".include \"included.mk\"", + ".if ${OPSYS} == \"Linux\"", + ".include \"included.mk\"", + ".endif") + t.CreateFileLines("included.mk", + MkCvsID) + t.FinishSetUp() + + G.Check(".") + + t.CheckOutputLines( + "WARN: including.mk:5: \"included.mk\" is included "+ + "conditionally here (depending on OPSYS) and unconditionally in line 3.", + "WARN: including.mk:8: \"included.mk\" is included "+ + "unconditionally here and conditionally in line 5 (depending on OPSYS).", + "WARN: including.mk:10: \"included.mk\" is included "+ + "conditionally here (depending on OPSYS) and unconditionally in line 8.") +} + +func (s *Suite) Test_Package_checkIncludeConditionally__other_directory(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"../../category/package-base/including.mk\"") + t.CreateFileLines("category/package-base/including.mk", + MkCvsID, + "", + ".include \"included.mk\"", + ".if ${OPSYS} == \"Linux\"", + ".include \"included.mk\"", + ".endif", + "", + ".include \"included.mk\"", + ".if ${OPSYS} == \"Linux\"", + ".include \"included.mk\"", + ".endif") + t.CreateFileLines("category/package-base/included.mk", + MkCvsID) + + t.Main("-Wall", "-Call", "category/package") + + // TODO: Understand why ../../category/package-base/including.mk is + // not checked for (un)conditional includes. + t.CheckOutputLines( + "Looks fine.") +} + // See https://github.com/rillig/pkglint/issues/1 func (s *Suite) Test_Package__include_without_exists(c *check.C) { t := s.Init(c) @@ -793,7 +1294,7 @@ func (s *Suite) Test_Package__include_after_exists(c *check.C) { } // See https://github.com/rillig/pkglint/issues/1 -func (s *Suite) Test_Package_readMakefile__include_other_after_exists(c *check.C) { +func (s *Suite) Test_Package_parse__include_other_after_exists(c *check.C) { t := s.Init(c) t.SetUpPackage("category/package", @@ -815,7 +1316,7 @@ func (s *Suite) Test_Package__redundant_master_sites(c *check.C) { t.SetUpPkgsrc() t.SetUpMasterSite("MASTER_SITE_R_CRAN", "http://cran.r-project.org/src/") t.CreateFileLines("math/R/Makefile.extension", - MkRcsID, + MkCvsID, "", "PKGNAME?=\tR-${R_PKGNAME}-${R_PKGVER}", "MASTER_SITES?=\t${MASTER_SITE_R_CRAN:=contrib/}", @@ -823,7 +1324,7 @@ func (s *Suite) Test_Package__redundant_master_sites(c *check.C) { "NO_CHECKSUM=\tyes", "LICENSE?=\tgnu-gpl-v2") t.CreateFileLines("math/R-date/Makefile", - MkRcsID, + MkCvsID, "", "R_PKGNAME=\tdate", "R_PKGVER=\t1.2.3", @@ -888,13 +1389,10 @@ func (s *Suite) Test_NewPackage(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("category/Makefile", - MkRcsID) + MkCvsID) t.FinishSetUp() - c.Check( - func() { NewPackage("category") }, - check.PanicMatches, - `Pkglint internal error: Package directory "category" must be two subdirectories below the pkgsrc root ".*".`) + t.ExpectAssert(func() { NewPackage("category") }) } // Before 2018-09-09, the .CURDIR variable did not have a fallback value. @@ -909,17 +1407,17 @@ func (s *Suite) Test__distinfo_from_other_package(c *check.C) { t.SetUpPkgsrc() t.Chdir(".") t.CreateFileLines("x11/gst-x11/Makefile", - MkRcsID, + MkCvsID, ".include \"../../multimedia/gst-base/Makefile.common\"", ".include \"../../mk/bsd.pkg.mk\"") t.CreateFileLines("multimedia/gst-base/Makefile.common", - MkRcsID, + MkCvsID, ".include \"plugins.mk\"") t.CreateFileLines("multimedia/gst-base/plugins.mk", - MkRcsID, + MkCvsID, "DISTINFO_FILE=\t${.CURDIR}/../../multimedia/gst-base/distinfo") t.CreateFileLines("multimedia/gst-base/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = 1234") t.FinishSetUp() @@ -927,7 +1425,7 @@ func (s *Suite) Test__distinfo_from_other_package(c *check.C) { G.Check("x11/gst-x11") t.CheckOutputLines( - "WARN: x11/gst-x11/Makefile: Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.", + "WARN: x11/gst-x11/Makefile: This package should have a PLIST file.", "ERROR: x11/gst-x11/Makefile: Each package must define its LICENSE.", "WARN: x11/gst-x11/Makefile: Each package should define a COMMENT.", "WARN: x11/gst-x11/../../multimedia/gst-base/distinfo:3: Patch file \"patch-aa\" does not exist in directory \"../../x11/gst-x11/patches\".") @@ -989,10 +1487,33 @@ func (s *Suite) Test_Package_checkfilePackageMakefile__META_PACKAGE_with_distinf G.Check(pkg) t.CheckOutputLines( - "WARN: ~/category/package/distinfo: " + + "WARN: ~/category/package/Makefile:20: This package should not have a PLIST file.", + "WARN: ~/category/package/distinfo: "+ "This file should not exist since NO_CHECKSUM or META_PACKAGE is set.") } +func (s *Suite) Test_Package_checkfilePackageMakefile__META_PACKAGE_with_patch(c *check.C) { + t := s.Init(c) + + pkg := t.SetUpPackage("category/package", + "META_PACKAGE=\tyes") + t.Remove("category/package/PLIST") + t.CreateFileDummyPatch("category/package/patches/patch-aa") + t.CreateFileLines("category/package/distinfo", + CvsID, + "", + "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac") + + t.FinishSetUp() + + G.Check(pkg) + + // At first it may sound strange to have a META_PACKAGE with patches. + // As of June 2019, there are 21 meta packages having a patches + // directory, being referred to by PATCHDIR. + t.CheckOutputEmpty() +} + func (s *Suite) Test_Package_checkfilePackageMakefile__USE_IMAKE_and_USE_X11(c *check.C) { t := s.Init(c) @@ -1007,6 +1528,64 @@ func (s *Suite) Test_Package_checkfilePackageMakefile__USE_IMAKE_and_USE_X11(c * "NOTE: ~/category/package/Makefile:21: USE_IMAKE makes USE_X11 in line 20 redundant.") } +func (s *Suite) Test_Package_checkfilePackageMakefile__USE_IMAKE_without_USE_X11(c *check.C) { + t := s.Init(c) + + pkg := t.SetUpPackage("category/package", + "USE_IMAKE=\tyes") + t.FinishSetUp() + + G.Check(pkg) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkfilePackageMakefile__USE_IMAKE_and_USE_X11_in_infra(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("mk/x11.buildlink3.mk", + MkCvsID, + "USE_X11=\tyes") + pkg := t.SetUpPackage("category/package", + ".include \"../../mk/x11.buildlink3.mk\"", + "USE_IMAKE=\tyes") + t.FinishSetUp() + + G.Check(pkg) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkfilePackageMakefile__PLIST_common(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.Copy("category/package/PLIST", "category/package/PLIST.common") + t.Remove("category/package/PLIST") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + // No warning about missing PLIST file. + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkfilePackageMakefile__files_Makefile(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("-Wall", "-Call") + t.SetUpPackage("category/package", + "#LICENSE=\t# none") + t.CreateFileLines("category/package/files/Makefile", + "#") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputLines( + "ERROR: ~/category/package/Makefile: Each package must define its LICENSE.") +} + func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__no_C(c *check.C) { t := s.Init(c) @@ -1059,7 +1638,7 @@ func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__realistic_compiler_m "", ".include \"../../mk/compiler.mk\"") t.CreateFileLines("mk/compiler.mk", - MkRcsID, + MkCvsID, ".include \"bsd.prefs.mk\"", "", "USE_LANGUAGES?=\tc", @@ -1116,6 +1695,55 @@ func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__ok(c *check.C) { t.CheckOutputEmpty() } +func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__not_constant_1(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".if 0", + "GNU_CONFIGURE=\tyes", + ".endif", + "USE_LANGUAGES=\tc++ objc") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkGnuConfigureUseLanguages__not_constant_2(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + "GNU_CONFIGURE=\tyes", + ".if 0", + "USE_LANGUAGES=\tc++ objc", + ".endif") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_loadPlistDirs(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.CreateFileLines("category/package/PLIST.common", + PlistCvsID, + "@exec echo hello", + "${PLIST.condition}dir/subdir/file", + "@unexec echo bye") + t.FinishSetUp() + + pkg := NewPackage(t.File("category/package")) + pkg.load() + + // FIXME: dir/subdir should also be included in pkg.Plist.Dirs. + t.Check(pkg.Plist.Dirs, deepEquals, map[string]bool{ + "bin": true}) +} + func (s *Suite) Test_Package_checkUseLanguagesCompilerMk__too_late(c *check.C) { t := s.Init(c) @@ -1123,7 +1751,7 @@ func (s *Suite) Test_Package_checkUseLanguagesCompilerMk__too_late(c *check.C) { ".include \"../../mk/compiler.mk\"", "USE_LANGUAGES=\tc c99 fortran ada c++14") t.CreateFileLines("mk/compiler.mk", - MkRcsID) + MkCvsID) t.FinishSetUp() G.Check(t.File("category/package")) @@ -1137,14 +1765,40 @@ func (s *Suite) Test_Package_checkUseLanguagesCompilerMk__compiler_mk(c *check.C t := s.Init(c) t.SetUpPackage("category/package", - ".include \"compiler.mk\"", "USE_LANGUAGES=\tc c99 fortran ada c++14", ".include \"../../mk/compiler.mk\"", + ".include \"compiler.mk\"", "USE_LANGUAGES=\tc c99 fortran ada c++14") t.CreateFileLines("category/package/compiler.mk", - MkRcsID) + MkCvsID, + "USE_LANGUAGES=\tc++") t.CreateFileLines("mk/compiler.mk", - MkRcsID) + MkCvsID) + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:20: "+ + "Variable USE_LANGUAGES is overwritten in compiler.mk:2.", + "WARN: ~/category/package/compiler.mk:2: "+ + "Modifying USE_LANGUAGES after including ../../mk/compiler.mk has no effect.", + "WARN: ~/category/package/Makefile:23: "+ + "Modifying USE_LANGUAGES after including ../../mk/compiler.mk has no effect.") +} + +func (s *Suite) Test_Package_checkUseLanguagesCompilerMk__endian_mk(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"endian.mk\"", + "USE_LANGUAGES=\tc c99 fortran ada c++14", + ".include \"../../mk/endian.mk\"", + "USE_LANGUAGES=\tc c99 fortran ada c++14") + t.CreateFileLines("category/package/endian.mk", + MkCvsID) + t.CreateFileLines("mk/endian.mk", + MkCvsID) t.FinishSetUp() G.Check(t.File("category/package")) @@ -1156,7 +1810,164 @@ func (s *Suite) Test_Package_checkUseLanguagesCompilerMk__compiler_mk(c *check.C "Modifying USE_LANGUAGES after including ../../mk/compiler.mk has no effect.") } -func (s *Suite) Test_Package_readMakefile__skipping(c *check.C) { +func (s *Suite) Test_Package_parse__simple(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.Chdir("category/package") + t.FinishSetUp() + + G.Pkg = NewPackage(".") + G.Pkg.included.Trace = true + G.Pkg.load() + + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk") +} + +func (s *Suite) Test_Package_parse__nonexistent_Makefile(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.Chdir("category/package") + t.Remove("Makefile") + t.FinishSetUp() + + G.Pkg = NewPackage(".") + G.Pkg.included.Trace = true + G.Pkg.load() + + t.CheckOutputLines( + "ERROR: Makefile: Cannot be read.") +} + +func (s *Suite) Test_Package_parse__include_in_same_directory(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"version.mk\"") + t.Chdir("category/package") + t.CreateFileLines("version.mk", + MkCvsID) + t.FinishSetUp() + + G.Pkg = NewPackage(".") + G.Pkg.included.Trace = true + G.Pkg.load() + + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk", + "FirstTime: version.mk") +} + +func (s *Suite) Test_Package_parse__nonexistent_include(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"version.mk\"") + t.Chdir("category/package") + t.FinishSetUp() + + G.Pkg = NewPackage(".") + G.Pkg.included.Trace = true + G.Pkg.load() + + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk", + "FirstTime: version.mk", + "ERROR: Makefile:20: Cannot read \"version.mk\".") +} + +// When reading the package Makefile, pkglint loads and interprets each +// file only once. This is especially important for packages with a large +// dependency graph containing many common subdependencies. +func (s *Suite) Test_Package_parse__include_twice(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"version.mk\"", + ".include \"version.mk\"") + t.Chdir("category/package") + t.CreateFileLines("version.mk", + MkCvsID) + t.FinishSetUp() + + G.Pkg = NewPackage(".") + G.Pkg.included.Trace = true + G.Pkg.load() + + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk", + "FirstTime: version.mk") +} + +func (s *Suite) Test_Package_parse__include_in_other_directory(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"../../category/other/version.mk\"") + t.Chdir("category/package") + t.CreateFileLines("../../category/other/version.mk", + MkCvsID) + t.FinishSetUp() + + G.Pkg = NewPackage(".") + G.Pkg.included.Trace = true + G.Pkg.load() + + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk", + "FirstTime: ../../category/other/version.mk") +} + +// Demonstrates that Package.included contains the file paths of the +// included files, relative to the package directory. +func (s *Suite) Test_Package_parse__includes_in_other_directory(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"../../category/other/module.mk\"") + t.Chdir("category/package") + t.CreateFileLines("../../category/other/module.mk", + MkCvsID, + ".include \"version.mk\"") + t.CreateFileLines("../../category/other/version.mk", + MkCvsID) + t.FinishSetUp() + + G.Pkg = NewPackage(".") + G.Pkg.included.Trace = true + G.Pkg.load() + + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk", + "FirstTime: ../../category/other/module.mk", + "FirstTime: ../../category/other/version.mk") +} + +func (s *Suite) Test_Package_parse__nonexistent_in_other_directory(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"../../category/other/module.mk\"") + t.Chdir("category/package") + t.CreateFileLines("../../category/other/module.mk", + MkCvsID, + ".include \"version.mk\"") + t.FinishSetUp() + + G.Pkg = NewPackage(".") + G.Pkg.included.Trace = true + G.Pkg.load() + + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk", + "FirstTime: ../../category/other/module.mk", + "FirstTime: ../../category/other/version.mk", + "ERROR: ../../category/other/module.mk:2: Cannot read \"version.mk\".") +} + +func (s *Suite) Test_Package_parse__skipping(c *check.C) { t := s.Init(c) t.SetUpCommandLine("-Wall,no-space") @@ -1184,11 +1995,10 @@ func (s *Suite) Test_Package_readMakefile__skipping(c *check.C) { c.Check(relevant, deepEquals, []string{ "TRACE: 1 2 3 4 ~/category/package/Makefile:20: " + - "Skipping include file \"${MYSQL_PKGSRCDIR:S/-client$/-server/}/buildlink3.mk\". " + - "This may result in false warnings."}) + "Skipping unresolvable include file \"${MYSQL_PKGSRCDIR:S/-client$/-server/}/buildlink3.mk\"."}) } -func (s *Suite) Test_Package_readMakefile__not_found(c *check.C) { +func (s *Suite) Test_Package_parse__not_found(c *check.C) { t := s.Init(c) pkg := t.SetUpPackage("category/package", @@ -1203,11 +2013,11 @@ func (s *Suite) Test_Package_readMakefile__not_found(c *check.C) { "ERROR: ~/devel/zlib/buildlink3.mk:1: Cannot read \"../../enoent/enoent/buildlink3.mk\".") } -func (s *Suite) Test_Package_readMakefile__relative(c *check.C) { +func (s *Suite) Test_Package_parse__relative(c *check.C) { t := s.Init(c) t.CreateFileLines("category/package/extra.mk", - MkRcsID) + MkCvsID) pkg := t.SetUpPackage("category/package", ".include \"../package/extra.mk\"") t.FinishSetUp() @@ -1223,7 +2033,7 @@ func (s *Suite) Test_Package_readMakefile__relative(c *check.C) { // When a buildlink3.mk file is included, the corresponding builtin.mk // file is included by the pkgsrc infrastructure. Therefore all variables // declared in the builtin.mk file become known in the package. -func (s *Suite) Test_Package_readMakefile__builtin_mk(c *check.C) { +func (s *Suite) Test_Package_parse__builtin_mk(c *check.C) { t := s.Init(c) t.SetUpTool("echo", "ECHO", AtRunTime) @@ -1234,7 +2044,7 @@ func (s *Suite) Test_Package_readMakefile__builtin_mk(c *check.C) { "\techo ${VAR_FROM_BUILTIN} ${OTHER_VAR}") t.CreateFileDummyBuildlink3("category/lib1/buildlink3.mk") t.CreateFileLines("category/lib1/builtin.mk", - MkRcsID, + MkCvsID, "VAR_FROM_BUILTIN=\t# defined") t.FinishSetUp() @@ -1247,7 +2057,7 @@ func (s *Suite) Test_Package_readMakefile__builtin_mk(c *check.C) { // Ensures that the paths in Package.included are indeed relative to the // package directory. This hadn't been the case until March 2019. -func (s *Suite) Test_Package_readMakefile__included(c *check.C) { +func (s *Suite) Test_Package_parse__included(c *check.C) { t := s.Init(c) t.SetUpPackage("category/package", @@ -1256,127 +2066,332 @@ func (s *Suite) Test_Package_readMakefile__included(c *check.C) { t.SetUpPackage("devel/library") t.CreateFileDummyBuildlink3("devel/library/buildlink3.mk") t.CreateFileLines("devel/library/builtin.mk", - MkRcsID) + MkCvsID) t.CreateFileLines("lang/language/module.mk", - MkRcsID, + MkCvsID, ".include \"version.mk\"") t.CreateFileLines("lang/language/version.mk", - MkRcsID) + MkCvsID) t.FinishSetUp() - pkg := NewPackage(t.File("category/package")) + t.Chdir("category/package") + pkg := NewPackage(".") + pkg.included.Trace = true pkg.loadPackageMakefile() - expected := []string{ - "../../devel/library/buildlink3.mk", - "../../devel/library/builtin.mk", - "../../lang/language/module.mk", - "../../lang/language/version.mk", - "suppress-varorder.mk"} - - seen := pkg.included - for _, filename := range expected { - if !seen.Seen(filename) { - c.Errorf("File %q is not seen.", filename) - } + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk", + "FirstTime: ../../devel/library/buildlink3.mk", + "FirstTime: ../../devel/library/builtin.mk", + "FirstTime: ../../lang/language/module.mk", + "FirstTime: ../../lang/language/version.mk") +} + +func (s *Suite) Test_Package_parse__include_Makefile_common_same_directory(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/dependency") + t.CreateFileLines("category/dependency/Makefile.common", + MkCvsID, + "#", + "#") + t.SetUpPackage("category/package", + ".include \"../../category/dependency/Makefile.common\"", + ".include \"Makefile.common\"") + t.CreateFileLines("category/package/Makefile.common", + MkCvsID, + "#", + "#") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputLines( + "WARN: ~/category/dependency/Makefile.common:1: " + + "Please add a line \"# used by category/package/Makefile\" here.") +} + +func (s *Suite) Test_Package_parse__include_Makefile_common_explicit(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/dependency") + t.CreateFileLines("category/dependency/Makefile.common", + MkCvsID, + "#", + "#") + t.SetUpPackage("category/package", + ".include \"../../category/dependency/Makefile.common\"", + ".include \"../../category/package/Makefile.common\"") + t.CreateFileLines("category/package/Makefile.common", + MkCvsID, + "#", + "#") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputLines( + "WARN: ~/category/dependency/Makefile.common:1: " + + "Please add a line \"# used by category/package/Makefile\" here.") +} + +func (s *Suite) Test_Package_parse__fallback_lookup_in_package_directory(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("mk/pthread.buildlink3.mk", + MkCvsID, + ".include \"../../mk/pthread.builtin.mk\"") + t.CreateFileLines("mk/pthread.builtin.mk", + MkCvsID) + t.SetUpPackage("category/package", + ".include \"../../mk/pthread.buildlink3.mk\"") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputLines( + "NOTE: ~/mk/pthread.buildlink3.mk:2: " + + "The path to the included file should be \"pthread.builtin.mk\".") +} + +func (s *Suite) Test_Package_collectSeenInclude__builtin_mk(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"builtin.mk\"") + t.CreateFileLines("category/package/builtin.mk", + MkCvsID) + t.FinishSetUp() + + pkg := NewPackage(t.File("category/package")) + pkg.load() + + t.Check(pkg.seenInclude, equals, true) +} + +func (s *Suite) Test_Package_diveInto(c *check.C) { + t := s.Init(c) + + test := func(including, included string, expected bool) { + actual := (*Package)(nil).diveInto(including, included) + t.Check(actual, equals, expected) } - t.Check(seen.seen, check.HasLen, 5) + + // The variables that appear in these files are largely modeled by + // pkglint in the file vardefs.go. Therefore parsing these files again + // doesn't make much sense. + test("Makefile", "../../mk/bsd.pkg.mk", false) + test("Makefile", "../../mk/bsd.prefs.mk", false) + test("Makefile", "../../mk/bsd.fast.prefs.mk", false) + + // All files that are included from outside of the pkgsrc infrastructure + // are relevant. This is typically mk/compiler.mk or the various + // mk/*.buildlink3.mk files. + test("Makefile", "Makefile.common", true) + test("Makefile", "../../mk/compiler.mk", true) + + // The mk/*.buildlink3.mk files often come with a companion file called + // mk/*.builtin.mk, which also defines variables that are visible from + // the package. + // + // This case is needed for getting the redundancy check right. Without it + // there will be warnings about redundant assignments to the + // BUILTIN_CHECK.pthread variable. + test("pthread.buildlink3.mk", "pthread.builtin.mk", true) + test("../../mk/pthread.buildlink3.mk", "pthread.builtin.mk", true) + test("../../mk/pthread.buildlink3.mk", "../../mk/pthread.builtin.mk", true) + + // Files other than the companion builtin.mk are ignored. + test("../../mk/pthread.buildlink3.mk", "pthread.internals.mk", false) + + // Files that are included from within the pkgsrc infrastructure are not + // interesting since their content is largely modeled by pkglint in the + // file vardefs.go. + test("../../mk/one.mk", "two.mk", false) + test("../../mk/one.mk", "../../mk/two.mk", false) + test("../../mk/one.mk", "../lang/go/version.mk", false) +} + +func (s *Suite) Test_Package_collectSeenInclude__multiple(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"001.mk\"", + ".include \"002.mk\"") + t.CreateFileLines("category/package/001.mk", + MkCvsID) + t.CreateFileLines("category/package/002.mk", + MkCvsID) + t.FinishSetUp() + + t.EnableTracingToLog() + G.Check(t.File("category/package")) + t.EnableSilentTracing() + + // TODO: It's not necessary to trace this message three times. + t.CheckOutputLinesMatching(`^TRACE: .*seenInclude`, + "TRACE: 1 2 3 4 Including \"suppress-varorder.mk\" sets seenInclude.", + "TRACE: 1 2 3 4 Including \"001.mk\" sets seenInclude.", + "TRACE: 1 2 3 4 Including \"002.mk\" sets seenInclude.") } // Just for code coverage. -func (s *Suite) Test_Package_findIncludedFile__no_tracing(c *check.C) { +func (s *Suite) Test_Package_resolveIncludedFile__no_tracing(c *check.C) { t := s.Init(c) t.SetUpPackage("category/package", + ".include \"../../mk/${UNKNOWN_PKGPATH}.mk\"", ".include \"../../${UNKNOWN_PKGPATH}/buildlink3.mk\"", ".include \"../../lang/language/buildlink3.mk\"") t.CreateFileLines("lang/language/buildlink3.mk", - MkRcsID) + MkCvsID) t.FinishSetUp() pkg := NewPackage(t.File("category/package")) t.DisableTracing() + pkg.included.Trace = true pkg.loadPackageMakefile() - expected := []string{ - "../../lang/language/buildlink3.mk", - "../../lang/language/builtin.mk", - "suppress-varorder.mk"} + t.CheckOutputLines( + "FirstTime: suppress-varorder.mk", + "FirstTime: ../../lang/language/buildlink3.mk", + "FirstTime: ../../lang/language/builtin.mk") +} - seen := pkg.included - for _, filename := range expected { - if !seen.Seen(filename) { - c.Errorf("File %q is not seen.", filename) - } - } - t.Check(seen.seen, check.HasLen, 3) +func (s *Suite) Test_Package_resolveIncludedFile__skipping(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"../../mk/known.mk\"", + ".include \"../../${UNKNOWN_PKGPATH}/buildlink3.mk\"", + ".include \"../../lang/language/buildlink3.mk\"") + t.CreateFileLines("mk/known.mk", + MkCvsID, + ".include \"${UNKNOWN}.mk\"") + t.CreateFileLines("lang/language/buildlink3.mk", + MkCvsID) + t.FinishSetUp() + pkg := NewPackage(t.File("category/package")) + + t.EnableTracingToLog() + pkg.loadPackageMakefile() + + // The trace log does not contain the message that mk/known.mk includes + // a file that is skipped. This is because most package authors are not + // involved in the pkgsrc infrastructure, therefore there's no point in + // logging anything about these files. + t.CheckOutputLinesMatching(`.*Skipping.*`, + "TRACE: 1 2 ~/category/package/Makefile:21: "+ + "Skipping unresolvable include file \"../../${UNKNOWN_PKGPATH}/buildlink3.mk\".") } -func (s *Suite) Test_Package_checkLocallyModified(c *check.C) { +// In packages without specific MAINTAINER, everyone may commit changes. +func (s *Suite) Test_Package_checkOwnerMaintainer__no_maintainer(c *check.C) { t := s.Init(c) G.Username = "example-user" t.CreateFileLines("category/package/CVS/Entries", "/Makefile//modified//") + t.SetUpPackage("category/package", + "MAINTAINER=\tpkgsrc-users@NetBSD.org") + t.FinishSetUp() + + G.Check(t.File("category/package")) - // In packages without specific MAINTAINER, everyone may commit changes. + t.CheckOutputEmpty() +} - pkg := t.SetUpPackage("category/package", - "MAINTAINER=\tpkgsrc-users@NetBSD.org") +// A package with a MAINTAINER may be edited by the maintainer itself. +func (s *Suite) Test_Package_checkOwnerMaintainer__maintainer_equal(c *check.C) { + t := s.Init(c) + + G.Username = "maintainer" + t.CreateFileLines("category/package/CVS/Entries", + "/Makefile//modified//") + t.SetUpPackage("category/package", + "MAINTAINER=\tmaintainer@example.org") t.FinishSetUp() - G.Check(pkg) + G.Check(t.File("category/package")) t.CheckOutputEmpty() +} - // A package with a MAINTAINER may be edited with care. +// A package with a MAINTAINER may be edited by everyone, with care. +func (s *Suite) Test_Package_checkOwnerMaintainer__maintainer_unequal(c *check.C) { + t := s.Init(c) + G.Username = "example-user" + t.CreateFileLines("category/package/CVS/Entries", + "/Makefile//modified//") t.SetUpPackage("category/package", "MAINTAINER=\tmaintainer@example.org") + t.FinishSetUp() - G.Check(pkg) + G.Check(t.File("category/package")) 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. +// A package with an OWNER may be edited by the owner itself. +func (s *Suite) Test_Package_checkOwnerMaintainer__owner_equal(c *check.C) { + t := s.Init(c) - pkg = t.SetUpPackage("category/package", - "#MAINTAINER=\t# undefined", + G.Username = "owner" + t.CreateFileLines("category/package/CVS/Entries", + "/Makefile//modified//") + t.SetUpPackage("category/package", "OWNER=\towner@example.org") + t.FinishSetUp() - G.Check(pkg) + G.Check(t.File("category/package")) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkOwnerMaintainer__owner_unequal(c *check.C) { + t := s.Init(c) + + G.Username = "example-user" + t.CreateFileLines("category/package/CVS/Entries", + "/Makefile//modified//") + t.SetUpPackage("category/package", + "OWNER=\towner@example.org") + t.FinishSetUp() + + G.Check(t.File("category/package")) t.CheckOutputLines( "WARN: ~/category/package/Makefile: " + "Don't commit changes to this file without asking the OWNER, owner@example.org.") +} - // In a package with both OWNER and MAINTAINER, OWNER wins. +// In a package with both OWNER and MAINTAINER, OWNER wins. +func (s *Suite) Test_Package_checkOwnerMaintainer__both(c *check.C) { + t := s.Init(c) - pkg = t.SetUpPackage("category/package", + G.Username = "example-user" + t.CreateFileLines("category/package/CVS/Entries", + "/Makefile//modified//") + t.SetUpPackage("category/package", "MAINTAINER=\tmaintainer@example.org", "OWNER=\towner@example.org") + t.FinishSetUp() - G.Check(pkg) + G.Check(t.File("category/package")) t.CheckOutputLines( "WARN: ~/category/package/Makefile: "+ "Don't commit changes to this file without asking the OWNER, owner@example.org.", + // FIXME: OWNER is stronger than MAINTAINER, therefore this note should disappear. "NOTE: ~/category/package/Makefile: "+ "Please only commit changes that maintainer@example.org would approve.") - - // ... unless you are the owner, of course. - - G.Username = "owner" - - G.Check(pkg) - - t.CheckOutputEmpty() } // Just for code coverage. -func (s *Suite) Test_Package_checkLocallyModified__no_tracing(c *check.C) { +func (s *Suite) Test_Package_checkOwnerMaintainer__no_tracing(c *check.C) { t := s.Init(c) G.Username = "example-user" @@ -1395,7 +2410,7 @@ func (s *Suite) Test_Package_checkLocallyModified__no_tracing(c *check.C) { "that maintainer@example.org would approve.") } -func (s *Suite) Test_Package_checkLocallyModified__directory(c *check.C) { +func (s *Suite) Test_Package_checkOwnerMaintainer__directory(c *check.C) { t := s.Init(c) G.Username = "example-user" @@ -1407,7 +2422,7 @@ func (s *Suite) Test_Package_checkLocallyModified__directory(c *check.C) { pkg := t.SetUpPackage("category/package", "MAINTAINER=\tmaintainer@example.org") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac") t.FinishSetUp() @@ -1420,6 +2435,28 @@ func (s *Suite) Test_Package_checkLocallyModified__directory(c *check.C) { "maintainer@example.org would approve.") } +func (s *Suite) Test_Package_checkFreeze(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("-Wall", "--explain") + pkg := t.SetUpPackage("category/package") + t.CreateFileLines("category/package/CVS/Entries", + "/Makefile//modified//") + t.CreateFileLines("doc/CHANGES-2018", + "\tmk/bsd.pkg.mk: started freeze for 2018Q2 [freezer 2018-03-20]") + t.FinishSetUp() + + G.Check(pkg) + + t.CheckOutputLines( + "NOTE: ~/category/package/Makefile: Pkgsrc is frozen since 2018-03-20.", + "", + "\tDuring a pkgsrc freeze, changes to pkgsrc should only be made very", + "\tcarefully. See https://www.netbsd.org/developers/pkgsrc/ for the", + "\texact rules.", + "") +} + // In practice the distinfo file can always be autofixed since it has // just been read successfully and the corresponding patch file could // also be autofixed right before. @@ -1441,14 +2478,14 @@ func (s *Suite) Test_Package__using_common_Makefile_overriding_DISTINFO_FILE(c * t.SetUpPackage("security/pinentry") t.CreateFileLines("security/pinentry/Makefile.common", - MkRcsID, + MkCvsID, "DISTINFO_FILE=\t${.CURDIR}/../../security/pinentry/distinfo") t.SetUpPackage("security/pinentry-fltk", ".include \"../../security/pinentry/Makefile.common\"", "DISTINFO_FILE=\t${.CURDIR}/distinfo") t.CreateFileDummyPatch("security/pinentry-fltk/patches/patch-aa") t.CreateFileLines("security/pinentry-fltk/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac") t.FinishSetUp() @@ -1472,10 +2509,10 @@ func (s *Suite) Test_Package__redundant_variable_in_unrelated_files(c *check.C) ".include \"../../devel/py-trytond/Makefile.common\"", ".include \"../../lang/python/egg.mk\"") t.CreateFileLines("devel/py-trytond/Makefile.common", - MkRcsID, + MkCvsID, "PY_PATCHPLIST=\tyes") t.CreateFileLines("lang/python/egg.mk", - MkRcsID, + MkCvsID, "PY_PATCHPLIST=\tyes") t.FinishSetUp() @@ -1494,7 +2531,7 @@ func (s *Suite) Test_Package__redundant_variable_in_unrelated_files(c *check.C) // This is necessary to load the correct variable assignments for the // redundancy check, in particular variable assignments that serve as // arguments to "procedure calls", such as mk/find-files.mk. -func (s *Suite) Test_Package_readMakefile__include_infrastructure(c *check.C) { +func (s *Suite) Test_Package_parse__include_infrastructure(c *check.C) { t := s.Init(c) t.SetUpCommandLine("--dumpmakefile") @@ -1515,9 +2552,9 @@ func (s *Suite) Test_Package_readMakefile__include_infrastructure(c *check.C) { t.CheckOutputLines( "Whole Makefile (with all included files) follows:", - "~/category/package/Makefile:1: "+MkRcsID, + "~/category/package/Makefile:1: "+MkCvsID, "~/category/package/Makefile:2: ", - "~/category/package/Makefile:3: DISTNAME=\tdistname-1.0", + "~/category/package/Makefile:3: DISTNAME=\tpackage-1.0", "~/category/package/Makefile:4: #PKGNAME=\tpackage-1.0", "~/category/package/Makefile:5: CATEGORIES=\tcategory", "~/category/package/Makefile:6: MASTER_SITES=\t# none", @@ -1528,7 +2565,7 @@ func (s *Suite) Test_Package_readMakefile__include_infrastructure(c *check.C) { "~/category/package/Makefile:11: LICENSE=\t2-clause-bsd", "~/category/package/Makefile:12: ", "~/category/package/Makefile:13: .include \"suppress-varorder.mk\"", - "~/category/package/suppress-varorder.mk:1: "+MkRcsID, + "~/category/package/suppress-varorder.mk:1: "+MkCvsID, "~/category/package/Makefile:14: # empty", "~/category/package/Makefile:15: # empty", "~/category/package/Makefile:16: # empty", @@ -1556,13 +2593,13 @@ func (s *Suite) Test_Package__Makefile_files(c *check.C) { t.SetUpPackage("category/package") t.CreateFileLines("category/package/Makefile.common", - MkRcsID) + MkCvsID) t.CreateFileLines("category/package/Makefile.orig", - MkRcsID) + MkCvsID) t.CreateFileLines("category/package/Makefile.php", - MkRcsID) + MkCvsID) t.CreateFileLines("category/package/ext.mk", - MkRcsID) + MkCvsID) t.FinishSetUp() G.Check(t.File("category/package")) @@ -1573,3 +2610,88 @@ func (s *Suite) Test_Package__Makefile_files(c *check.C) { "NOTE: ~/category/package/Makefile.php: " + "Consider renaming \"Makefile.php\" to \"php.mk\".") } + +func (s *Suite) Test_Package__patch_in_FILESDIR(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine("-Wall", "-Call") + t.SetUpPackage("category/package") + t.CreateFileLines("category/package/files/patch-aa", + "This file can contain anything, no matter what the filename says.") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + // No warnings. The files in FILESDIR are independent of pkgsrc + // and may contain anything. There are no naming conventions or + // anything else. + t.CheckOutputEmpty() +} + +// When a package defines PLIST_SRC, it may or may not use the +// PLIST file from the package directory. Therefore the check +// is skipped completely. +func (s *Suite) Test_Package_checkPlist__PLIST_SRC(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + "PLIST_SRC=\t${WRKDIR}/PLIST") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkPlist__META_PACKAGE(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + "META_PACKAGE=\tyes") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputLines( + "WARN: ~/category/package/Makefile:20: This package should not have a PLIST file.", + "WARN: ~/category/package/distinfo: This file should not exist "+ + "since NO_CHECKSUM or META_PACKAGE is set.") +} + +func (s *Suite) Test_Package_checkPlist__Perl5_packlist(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/p5-Packlist", + "PERL5_PACKLIST=\tauto/Packlist/.packlist") + t.FinishSetUp() + + G.Check(t.File("category/p5-Packlist")) + + t.CheckOutputLines( + "WARN: ~/category/p5-Packlist/Makefile:20: This package should not have a PLIST file.") +} + +func (s *Suite) Test_Package_checkPlist__PERL5_USE_PACKLIST_no(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/p5-NoPacklist", + "PERL5_USE_PACKLIST=\tno") + t.FinishSetUp() + + G.Check(t.File("category/p5-NoPacklist")) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_Package_checkPlist__PERL5_USE_PACKLIST_yes(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/p5-Packlist", + "PERL5_USE_PACKLIST=\tyes") + t.FinishSetUp() + + G.Check(t.File("category/p5-Packlist")) + + t.CheckOutputLines( + "WARN: ~/category/p5-Packlist/Makefile:20: This package should not have a PLIST file.") +} diff --git a/pkgtools/pkglint/files/paragraph.go b/pkgtools/pkglint/files/paragraph.go index 26c701e3fd8..2b457202093 100644 --- a/pkgtools/pkglint/files/paragraph.go +++ b/pkgtools/pkglint/files/paragraph.go @@ -9,24 +9,25 @@ import "strings" // If the paragraph adds an identifier to SUBST_CLASSES, the rest of the SUBST // block should be defined in the same paragraph. type Paragraph struct { - mklines []MkLine + mklines *MkLines + from int + to int } -func NewParagraph(mklines []MkLine) *Paragraph { - return &Paragraph{mklines} +func NewParagraph(mklines *MkLines, from, to int) *Paragraph { + for i := from; i < to; i++ { + assert(!mklines.mklines[i].IsEmpty()) + } + return &Paragraph{mklines, from, to} } -func (p *Paragraph) Clear() { - p.mklines = nil -} +func (p *Paragraph) FirstLine() *MkLine { return p.mklines.mklines[p.from] } +func (p *Paragraph) LastLine() *MkLine { return p.mklines.mklines[p.to-1] } -func (p *Paragraph) Add(mkline MkLine) { - assertf(!mkline.IsEmpty(), "A paragraph must not contain empty lines.") - p.mklines = append(p.mklines, mkline) -} +func (p *Paragraph) MkLines() []*MkLine { return p.mklines.mklines[p.from:p.to] } -func (p *Paragraph) ForEach(action func(mkline MkLine)) { - for _, mkline := range p.mklines { +func (p *Paragraph) ForEach(action func(mkline *MkLine)) { + for _, mkline := range p.MkLines() { action(mkline) } } @@ -44,21 +45,21 @@ func (p *Paragraph) Align() { // No warning or note is logged. Therefore this method must only be used to // realign the whole paragraph after one of its lines has been modified. func (p *Paragraph) AlignTo(column int) { - for _, mkline := range p.mklines { + p.ForEach(func(mkline *MkLine) { if !mkline.IsVarassign() { - continue + return } align := mkline.ValueAlign() oldWidth := tabWidth(align) if tabWidth(rtrimHspace(align)) > column { - continue + return } if oldWidth == column && !hasSuffix(strings.TrimRight(align, "\t"), " ") { - continue + return } if mkline.IsMultiline() && !mkline.FirstLineContainsValue() { - continue + return } trimmed := strings.TrimRightFunc(align, isHspaceRune) @@ -68,5 +69,5 @@ func (p *Paragraph) AlignTo(column int) { fix.Notef(SilentAutofixFormat) fix.ReplaceAfter(trimmed, align[len(trimmed):], newSpace) fix.Apply() - } + }) } diff --git a/pkgtools/pkglint/files/paragraph_test.go b/pkgtools/pkglint/files/paragraph_test.go index 0873945eac8..97bae968dc4 100644 --- a/pkgtools/pkglint/files/paragraph_test.go +++ b/pkgtools/pkglint/files/paragraph_test.go @@ -2,34 +2,13 @@ package pkglint import "gopkg.in/check.v1" -func (s *Suite) Test_Paragraph_Clear(c *check.C) { +func (s *Suite) Test_Paragraph__empty_line(c *check.C) { t := s.Init(c) - para := NewParagraph(nil) + mklines := t.NewMkLines("filename.mk", + "") - para.Clear() - - t.Check(para.mklines, check.IsNil) - - para.Add(t.NewMkLine("filename.mk", 123, "#")) - - para.Clear() - - t.Check(para.mklines, check.IsNil) -} - -func (s *Suite) Test_Paragraph_Add__empty_line(c *check.C) { - t := s.Init(c) - - para := NewParagraph(nil) - - para.Clear() - - t.Check(para.mklines, check.IsNil) - - t.ExpectPanic( - func() { para.Add(t.NewMkLine("filename.mk", 123, "")) }, - "Pkglint internal error: A paragraph must not contain empty lines.") + t.ExpectAssert(func() { _ = NewParagraph(mklines, 0, 1) }) } func (s *Suite) Test_Paragraph_Align(c *check.C) { @@ -37,15 +16,10 @@ func (s *Suite) Test_Paragraph_Align(c *check.C) { t.SetUpCommandLine("-Wall", "--autofix") mklines := t.SetUpFileMkLines("filename.mk", - MkRcsID, + MkCvsID, "VAR=value", "VAR=\t\t\tvalue") - para := NewParagraph(nil) - for _, mkline := range mklines.mklines { - // Strictly speaking, lines 1 and 2 don't belong to the paragraph, - // but aligning the lines works nevertheless. - para.Add(mkline) - } + para := NewParagraph(mklines, 1, 3) para.Align() mklines.SaveAutofixChanges() @@ -55,7 +29,7 @@ func (s *Suite) Test_Paragraph_Align(c *check.C) { "AUTOFIX: ~/filename.mk:3: Replacing \"\\t\\t\\t\" with \"\\t\".") t.CheckFileLinesDetab("filename.mk", - MkRcsID, + MkCvsID, "VAR= value", "VAR= value") } @@ -65,30 +39,27 @@ func (s *Suite) Test_Paragraph_AlignTo(c *check.C) { t.SetUpCommandLine("-Wall", "--autofix") mklines := t.SetUpFileMkLines("filename.mk", - MkRcsID, + MkCvsID, "VAR=value", "VAR=\t\tvalue", + "# comment between the variable assignments", "VAR=\t \tvalue", "VAR=\t\t\tvalue") - para := NewParagraph(nil) - for _, mkline := range mklines.mklines { - // Strictly speaking, lines 1 and 2 don't belong to the paragraph, - // but aligning the lines works nevertheless. - para.Add(mkline) - } + para := NewParagraph(mklines, 1, 6) para.AlignTo(16) mklines.SaveAutofixChanges() t.CheckOutputLines( "AUTOFIX: ~/filename.mk:2: Replacing \"\" with \"\\t\\t\".", - "AUTOFIX: ~/filename.mk:4: Replacing \"\\t \\t\" with \"\\t\\t\".", - "AUTOFIX: ~/filename.mk:5: Replacing \"\\t\\t\\t\" with \"\\t\\t\".") + "AUTOFIX: ~/filename.mk:5: Replacing \"\\t \\t\" with \"\\t\\t\".", + "AUTOFIX: ~/filename.mk:6: Replacing \"\\t\\t\\t\" with \"\\t\\t\".") t.CheckFileLinesDetab("filename.mk", - MkRcsID, + MkCvsID, "VAR= value", "VAR= value", + "# comment between the variable assignments", "VAR= value", "VAR= value") } @@ -98,16 +69,13 @@ func (s *Suite) Test_Paragraph_AlignTo__continued_lines(c *check.C) { t.SetUpCommandLine("-Wall", "--autofix") mklines := t.SetUpFileMkLines("filename.mk", - MkRcsID, + MkCvsID, "VAR= \\", " value", "VAR= value1 \\", "value2 \\", "\t\tvalue3") - para := NewParagraph(nil) - for _, mkline := range mklines.mklines { - para.Add(mkline) - } + para := NewParagraph(mklines, 1, 3) para.AlignTo(16) mklines.SaveAutofixChanges() @@ -116,7 +84,7 @@ func (s *Suite) Test_Paragraph_AlignTo__continued_lines(c *check.C) { "AUTOFIX: ~/filename.mk:4: Replacing \" \" with \"\\t\\t\".") t.CheckFileLinesDetab("filename.mk", - MkRcsID, + MkCvsID, // Since this line does not contain the actual value, it doesn't need to be aligned. "VAR= \\", " value", @@ -131,13 +99,10 @@ func (s *Suite) Test_Paragraph_AlignTo__outlier(c *check.C) { t.SetUpCommandLine("-Wall", "--autofix") mklines := t.SetUpFileMkLines("filename.mk", - MkRcsID, + MkCvsID, "VAR= value", "VERY_LONG_VARIABLE_NAME= value1") - para := NewParagraph(nil) - for _, mkline := range mklines.mklines { - para.Add(mkline) - } + para := NewParagraph(mklines, 1, 3) para.AlignTo(8) mklines.SaveAutofixChanges() @@ -146,7 +111,7 @@ func (s *Suite) Test_Paragraph_AlignTo__outlier(c *check.C) { "AUTOFIX: ~/filename.mk:2: Replacing \" \" with \"\\t\".") t.CheckFileLinesDetab("filename.mk", - MkRcsID, + MkCvsID, "VAR= value", // The space is preserved since this line is an outlier. "VERY_LONG_VARIABLE_NAME= value1") diff --git a/pkgtools/pkglint/files/patches.go b/pkgtools/pkglint/files/patches.go index db9b760c5ff..ad2532e4f3f 100644 --- a/pkgtools/pkglint/files/patches.go +++ b/pkgtools/pkglint/files/patches.go @@ -7,16 +7,12 @@ import ( "strings" ) -func CheckLinesPatch(lines Lines) { - if trace.Tracing { - defer trace.Call1(lines.FileName)() - } - +func CheckLinesPatch(lines *Lines) { (&PatchChecker{lines, NewLinesLexer(lines), false, false}).Check() } type PatchChecker struct { - lines Lines + lines *Lines llex *LinesLexer seenDocumentation bool previousLineEmpty bool @@ -29,11 +25,7 @@ const ( ) func (ck *PatchChecker) Check() { - if trace.Tracing { - defer trace.Call0()() - } - - if ck.lines.CheckRcsID(0, ``, "") { + if ck.lines.CheckCvsID(0, ``, "") { ck.llex.Skip() } if ck.llex.EOF() { @@ -94,25 +86,17 @@ func (ck *PatchChecker) Check() { } CheckLinesTrailingEmptyLines(ck.lines) - sha1Before, err := computePatchSha1Hex(ck.lines.FileName) - if SaveAutofixChanges(ck.lines) && G.Pkg != nil && err == nil { - sha1After, err := computePatchSha1Hex(ck.lines.FileName) - if err == nil { - G.Pkg.AutofixDistinfo(sha1Before, sha1After) - } + sha1Before := computePatchSha1Hex(ck.lines) + if SaveAutofixChanges(ck.lines) && G.Pkg != nil { + linesAfter := Load(ck.lines.Filename, 0) + sha1After := computePatchSha1Hex(linesAfter) + G.Pkg.AutofixDistinfo(sha1Before, sha1After) } } // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html func (ck *PatchChecker) checkUnifiedDiff(patchedFile string) { - if trace.Tracing { - defer trace.Call0()() - } - - patchedFileType := guessFileType(patchedFile) - if trace.Tracing { - trace.Stepf("guessFileType(%q) = %s", patchedFile, patchedFileType) - } + isConfigure := ck.isConfigure(patchedFile) hasHunks := false for { @@ -125,12 +109,9 @@ func (ck *PatchChecker) checkUnifiedDiff(patchedFile string) { hasHunks = true linesToDel := toInt(m[2], 1) linesToAdd := toInt(m[4], 1) - if trace.Tracing { - trace.Stepf("hunk -%d +%d", linesToDel, linesToAdd) - } ck.checktextUniHunkCr() - ck.checktextRcsid(text) + ck.checktextCvsID(text) for !ck.llex.EOF() && (linesToDel > 0 || linesToAdd > 0 || hasPrefix(ck.llex.CurrentLine().Text, "\\")) { line := ck.llex.CurrentLine() @@ -149,15 +130,15 @@ func (ck *PatchChecker) checkUnifiedDiff(patchedFile string) { case hasPrefix(text, " "), hasPrefix(text, "\t"): linesToDel-- linesToAdd-- - ck.checklineContext(text[1:], patchedFileType) + ck.checktextCvsID(text) case hasPrefix(text, "-"): linesToDel-- case hasPrefix(text, "+"): linesToAdd-- - ck.checktextRcsid(text) - ck.checklineAdded(text[1:], patchedFileType) + ck.checktextCvsID(text) + ck.checkConfigure(text[1:], isConfigure) case hasPrefix(text, "\\"): // \ No newline at end of file (or a translation of that message) @@ -196,11 +177,7 @@ func (ck *PatchChecker) checkUnifiedDiff(patchedFile string) { } } -func (ck *PatchChecker) checkBeginDiff(line Line, patchedFiles int) { - if trace.Tracing { - defer trace.Call0()() - } - +func (ck *PatchChecker) checkBeginDiff(line *Line, patchedFiles int) { if !ck.seenDocumentation && patchedFiles == 0 { line.Errorf("Each patch must be documented.") line.Explain( @@ -222,6 +199,7 @@ func (ck *PatchChecker) checkBeginDiff(line Line, patchedFiles int) { "After submitting a patch upstream, the corresponding bug report should", "be mentioned in this file, to prevent duplicate work.") } + if G.Opts.WarnSpace && !ck.previousLineEmpty { fix := line.Autofix() fix.Notef("Empty line expected.") @@ -230,64 +208,48 @@ func (ck *PatchChecker) checkBeginDiff(line Line, patchedFiles int) { } } -func (ck *PatchChecker) checklineContext(text string, patchedFileType FileType) { - if trace.Tracing { - defer trace.Call2(text, patchedFileType.String())() - } - - ck.checktextRcsid(text) - - if G.Opts.WarnExtra { - ck.checklineAdded(text, patchedFileType) +func (ck *PatchChecker) checkConfigure(addedText string, isConfigure bool) { + if !isConfigure { + return } -} - -func (ck *PatchChecker) checklineAdded(addedText string, patchedFileType FileType) { - if trace.Tracing { - defer trace.Call2(addedText, patchedFileType.String())() + if !hasSuffix(addedText, ": Avoid regenerating within pkgsrc") { + return } line := ck.llex.PreviousLine() - switch patchedFileType { - case ftConfigure: - if hasSuffix(addedText, ": Avoid regenerating within pkgsrc") { - line.Errorf("This code must not be included in patches.") - line.Explain( - "It is generated automatically by pkgsrc after the patch phase.", - "", - "For more details, look for \"configure-scripts-override\" in", - "mk/configure/gnu-configure.mk.") - } - } + line.Errorf("This code must not be included in patches.") + line.Explain( + "It is generated automatically by pkgsrc after the patch phase.", + "", + "For more details, look for \"configure-scripts-override\" in", + "mk/configure/gnu-configure.mk.") } func (ck *PatchChecker) checktextUniHunkCr() { - if trace.Tracing { - defer trace.Call0()() - } - line := ck.llex.PreviousLine() - if hasSuffix(line.Text, "\r") { - // This code has been introduced around 2006. - // As of 2018, this might be fixed by now. - fix := line.Autofix() - fix.Errorf("The hunk header must not end with a CR character.") - fix.Explain( - "The MacOS X patch utility cannot handle these.") - fix.Replace("\r\n", "\n") - fix.Apply() + if !hasSuffix(line.Text, "\r") { + return } + + // This code has been introduced around 2006. + // As of 2018, this might be fixed by now. + fix := line.Autofix() + fix.Errorf("The hunk header must not end with a CR character.") + fix.Explain( + "The MacOS X patch utility cannot handle these.") + fix.Replace("\r\n", "\n") + fix.Apply() } -func (ck *PatchChecker) checktextRcsid(text string) { +func (ck *PatchChecker) checktextCvsID(text string) { if strings.IndexByte(text, '$') == -1 { return } if m, tagname := match1(text, `\$(Author|Date|Header|Id|Locker|Log|Name|RCSfile|Revision|Source|State|NetBSD)(?::[^\$]*)?\$`); m { if matches(text, rePatchUniHunk) { - ck.llex.PreviousLine().Warnf("Found RCS tag \"$%s$\". Please remove it.", tagname) + ck.llex.PreviousLine().Warnf("Found CVS tag \"$%s$\". Please remove it.", tagname) } else { - ck.llex.PreviousLine().Warnf("Found RCS tag \"$%s$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".", tagname) + ck.llex.PreviousLine().Warnf("Found CVS tag \"$%s$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".", tagname) } } } @@ -303,32 +265,10 @@ func (ck *PatchChecker) isEmptyLine(text string) bool { hasPrefix(text, "=============") } -type FileType uint8 - -const ( - ftConfigure FileType = iota - ftUnknown -) - -func (ft FileType) String() string { - return [...]string{ - "configure file", - "unknown", - }[ft] -} - -// This is used to select the proper subroutine for detecting absolute pathnames. -func guessFileType(filename string) (fileType FileType) { - if trace.Tracing { - defer trace.Call(filename, trace.Result(&fileType))() - } - - basename := path.Base(filename) - basename = strings.TrimSuffix(basename, ".in") // doesn't influence the content type - - switch { - case basename == "configure" || basename == "configure.ac": - return ftConfigure +func (*PatchChecker) isConfigure(filename string) bool { + switch path.Base(filename) { + case "configure", "configure.in", "configure.ac": + return true } - return ftUnknown + return false } diff --git a/pkgtools/pkglint/files/patches_test.go b/pkgtools/pkglint/files/patches_test.go index ab78bf9e194..e9713b7f246 100644 --- a/pkgtools/pkglint/files/patches_test.go +++ b/pkgtools/pkglint/files/patches_test.go @@ -7,7 +7,7 @@ func (s *Suite) Test_CheckLinesPatch__with_comment(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-WithComment", - RcsID, + CvsID, "", "This part describes:", "* the purpose of the patch,", @@ -35,7 +35,7 @@ func (s *Suite) Test_CheckLinesPatch__without_empty_line__autofix(c *check.C) { t.Chdir("category/package") patchLines := t.SetUpFileLines("patch-WithoutEmptyLines", - RcsID, + CvsID, "Text", "--- file.orig", "+++ file", @@ -45,7 +45,7 @@ func (s *Suite) Test_CheckLinesPatch__without_empty_line__autofix(c *check.C) { "+new line", " context after") t.CreateFileLines("distinfo", - RcsID, + CvsID, "", // The hash is taken from a breakpoint at the beginning of AutofixDistinfo, oldSha1 "SHA1 (some patch) = 49abd735b7e706ea9ed6671628bb54e91f7f5ffb") @@ -62,7 +62,7 @@ func (s *Suite) Test_CheckLinesPatch__without_empty_line__autofix(c *check.C) { "with \"4938fc8c0b483dc2e33e741b0da883d199e78164\".") t.CheckFileLines("patch-WithoutEmptyLines", - RcsID, + CvsID, "", "Text", "", @@ -74,7 +74,7 @@ func (s *Suite) Test_CheckLinesPatch__without_empty_line__autofix(c *check.C) { "+new line", " context after") t.CheckFileLines("distinfo", - RcsID, + CvsID, "", "SHA1 (some patch) = 4938fc8c0b483dc2e33e741b0da883d199e78164") } @@ -83,7 +83,7 @@ func (s *Suite) Test_CheckLinesPatch__no_comment_and_no_empty_lines(c *check.C) t := s.Init(c) patchLines := t.SetUpFileLines("patch-WithoutEmptyLines", - RcsID, + CvsID, "--- file.orig", "+++ file", "@@ -1,1 +1,1 @@", @@ -107,7 +107,7 @@ func (s *Suite) Test_CheckLinesPatch__without_comment(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-WithoutComment", - RcsID, + CvsID, "", "--- file.orig", "+++ file", @@ -123,16 +123,19 @@ func (s *Suite) Test_CheckLinesPatch__without_comment(c *check.C) { "ERROR: patch-WithoutComment:3: Each patch must be documented.") } -// Autogenerated git "comments" don't count as real comments since they -// don't convey any intention of a human developer. -func (s *Suite) Test_CheckLinesPatch__git_without_comment(c *check.C) { +// Autogenerated "comments" from Git or other tools don't count as real +// comments since they don't convey any intention of a human developer. +func (s *Suite) Test_PatchChecker_isEmptyLine(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-aa", - RcsID, + CvsID, "", "diff --git a/aa b/aa", "index 1234567..1234567 100644", + "Index: from Subversion", + "============= separator or conflict marker", + "", "--- a/aa", "+++ b/aa", "@@ -1,1 +1,1 @@", @@ -142,7 +145,7 @@ func (s *Suite) Test_CheckLinesPatch__git_without_comment(c *check.C) { CheckLinesPatch(lines) t.CheckOutputLines( - "ERROR: patch-aa:5: Each patch must be documented.") + "ERROR: patch-aa:8: Each patch must be documented.") } // The output of BSD Make typically contains "*** Error code". @@ -152,7 +155,7 @@ func (s *Suite) Test_CheckLinesPatch__error_code(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-ErrorCode", - RcsID, + CvsID, "", "*** Error code 1", // Looks like a context diff but isn't. "", @@ -173,7 +176,7 @@ func (s *Suite) Test_CheckLinesPatch__wrong_header_order(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-WrongOrder", - RcsID, + CvsID, "", "Text", "Text", @@ -197,7 +200,7 @@ func (s *Suite) Test_CheckLinesPatch__context_diff(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-ctx", - RcsID, + CvsID, "", "diff -cr history.c.orig history.c", "*** history.c.orig", @@ -214,7 +217,7 @@ func (s *Suite) Test_CheckLinesPatch__no_patch(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-aa", - RcsID, + CvsID, "", "-- oldfile", "++ newfile") @@ -229,7 +232,7 @@ func (s *Suite) Test_CheckLinesPatch__two_patched_files(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-aa", - RcsID, + CvsID, "", "A single patch file can apply to more than one file at a time.", "It shouldn't though, to keep the relation between patch files", @@ -257,7 +260,7 @@ func (s *Suite) Test_CheckLinesPatch__documentation_that_looks_like_patch_lines( t := s.Init(c) lines := t.NewLines("patch-aa", - RcsID, + CvsID, "", "--- oldfile", "", @@ -275,7 +278,7 @@ func (s *Suite) Test_CheckLinesPatch__only_unified_header_but_no_content(c *chec t := s.Init(c) lines := t.NewLines("patch-unified", - RcsID, + CvsID, "", "Documentation for the patch", "", @@ -292,7 +295,7 @@ func (s *Suite) Test_CheckLinesPatch__only_context_header_but_no_content(c *chec t := s.Init(c) lines := t.NewLines("patch-context", - RcsID, + CvsID, "", "Documentation for the patch", "", @@ -311,7 +314,7 @@ func (s *Suite) Test_CheckLinesPatch__no_newline_with_text_following(c *check.C) t := s.Init(c) lines := t.NewLines("patch-aa", - RcsID, + CvsID, "", "comment", "", @@ -334,7 +337,7 @@ func (s *Suite) Test_CheckLinesPatch__no_newline(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-aa", - RcsID, + CvsID, "", "comment", "", @@ -357,7 +360,7 @@ func (s *Suite) Test_CheckLinesPatch__empty_lines_left_out_at_eof(c *check.C) { t := s.Init(c) lines := t.NewLines("patch-aa", - RcsID, + CvsID, "", "comment", "", @@ -382,7 +385,7 @@ func (s *Suite) Test_CheckLinesPatch__context_lines_with_tab_instead_of_space(c t := s.Init(c) lines := t.NewLines("patch-aa", - RcsID, + CvsID, "", "comment", "", @@ -406,7 +409,7 @@ func (s *Suite) Test_CheckLinesPatch__autofix_empty_patch(c *check.C) { t.SetUpCommandLine("-Wall", "--autofix") lines := t.NewLines("patch-aa", - RcsID) + CvsID) CheckLinesPatch(lines) @@ -420,7 +423,7 @@ func (s *Suite) Test_CheckLinesPatch__autofix_long_empty_patch(c *check.C) { t.SetUpCommandLine("-Wall", "--autofix") lines := t.NewLines("patch-aa", - RcsID, + CvsID, "") CheckLinesPatch(lines) @@ -433,7 +436,7 @@ func (s *Suite) Test_CheckLinesPatch__crlf_autofix(c *check.C) { t.SetUpCommandLine("-Wall", "--autofix") lines := t.SetUpFileLines("patch-aa", - RcsID, + CvsID, "", "Documentation", "", @@ -456,7 +459,7 @@ func (s *Suite) Test_CheckLinesPatch__autogenerated(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("patch-aa", - RcsID, + CvsID, "", "Documentation", "", @@ -476,7 +479,7 @@ func (s *Suite) Test_CheckLinesPatch__empty_context_lines_in_hunk(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("patch-aa", - RcsID, + CvsID, "", "Documentation", "", @@ -501,7 +504,7 @@ func (s *Suite) Test_CheckLinesPatch__invalid_line_in_hunk(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("patch-aa", - RcsID, + CvsID, "", "Documentation", "", @@ -519,91 +522,119 @@ func (s *Suite) Test_CheckLinesPatch__invalid_line_in_hunk(c *check.C) { "ERROR: ~/patch-aa:10: Invalid line in unified patch hunk: <<<<<<<<") } -// Just for code coverage. -func (s *Suite) Test_PatchChecker_checklineContext__no_tracing(c *check.C) { +func (s *Suite) Test_PatchChecker_Check__missing_CVS_Id(c *check.C) { t := s.Init(c) - lines := t.NewLines("patch-WithComment", - RcsID, + lines := t.SetUpFileLines("patch-aa", + "This first line is missing the CVS Id", + "", + "Documentation") + + CheckLinesPatch(lines) + + t.CheckOutputLines( + sprintf("ERROR: ~/patch-aa:1: Expected %q.", CvsID), + "NOTE: ~/patch-aa:1: Empty line expected before this line.", + "ERROR: ~/patch-aa: Contains no patch.") +} + +func (s *Suite) Test_PatchChecker_checkUnifiedDiff__lines_at_end(c *check.C) { + t := s.Init(c) + + lines := t.SetUpFileLines("patch-aa", + CvsID, "", "Documentation", "", - "--- file.orig", - "+++ file", - "@@ -5,3 +5,3 @@", - " context before", - "-old line", - "+new line", - " context after") - t.DisableTracing() + "--- old", + "+++ new", + "@@ -1,1 +1,1 @@", + "- old", + "+ new", + "", + "This line is not part of the patch. Since it is separated from", + "the patch by an empty line, there is no reason for a warning.") CheckLinesPatch(lines) t.CheckOutputEmpty() } -func (s *Suite) Test_PatchChecker_checklineAdded__shell(c *check.C) { +func (s *Suite) Test_PatchChecker_checkBeginDiff__multiple_patches_without_documentation(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("patch-aa", - RcsID, + CvsID, "", - "Documentation", + "--- old", + "+++ new", + "@@ -1,1 +1,1 @@", + "- old", + "+ new", "", - "--- configure.sh.orig", - "+++ configure.sh", + "--- old", + "+++ new", "@@ -1,1 +1,1 @@", - "-old line", - "+new line") + "- old", + "+ new") CheckLinesPatch(lines) - t.CheckOutputEmpty() + // The "must be documented" error message is only given before the first + // patch since that's the only place where the documentation is expected. + // Since each pkgsrc patch should only patch a single file, this situation + // is an edge case anyway. + t.CheckOutputLines( + "ERROR: ~/patch-aa:3: Each patch must be documented.", + "WARN: ~/patch-aa: Contains patches for 2 files, should be only one.") } -func (s *Suite) Test_PatchChecker_checklineAdded__text(c *check.C) { +func (s *Suite) Test_PatchChecker_checkConfigure__no_GNU(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("patch-aa", - RcsID, + CvsID, "", "Documentation", "", - "--- configure.tex.orig", - "+++ configure.tex", + "--- configure.sh.orig", + "+++ configure.sh", "@@ -1,1 +1,1 @@", "-old line", - "+new line") + "+: Avoid regenerating within pkgsrc") CheckLinesPatch(lines) + // No warning since configure.sh is probably not a GNU-style + // configure file. t.CheckOutputEmpty() } -func (s *Suite) Test_PatchChecker_checklineAdded__unknown(c *check.C) { +func (s *Suite) Test_PatchChecker_checkConfigure__GNU(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("patch-aa", - RcsID, + CvsID, "", "Documentation", "", - "--- configure.unknown.orig", - "+++ configure.unknown", + "--- configure.orig", + "+++ configure", "@@ -1,1 +1,1 @@", "-old line", - "+new line") + "+: Avoid regenerating within pkgsrc") CheckLinesPatch(lines) - t.CheckOutputEmpty() + t.CheckOutputLines( + "ERROR: ~/patch-aa:9: This code must not be included in patches.") } -func (s *Suite) Test_PatchChecker_checktextRcsid(c *check.C) { +func (s *Suite) Test_PatchChecker_checktextCvsID(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("patch-aa", - RcsID, + CvsID, "", "Documentation", "", @@ -612,17 +643,13 @@ func (s *Suite) Test_PatchChecker_checktextRcsid(c *check.C) { "@@ -1,3 +1,3 @@ $"+"Id$", " $"+"Id$", "-old line", - "+new line", + "+new line $varname", " $"+"Author: authorship $") CheckLinesPatch(lines) t.CheckOutputLines( - "WARN: ~/patch-aa:7: Found RCS tag \"$"+"Id$\". Please remove it.", - "WARN: ~/patch-aa:8: Found RCS tag \"$"+"Id$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".", - "WARN: ~/patch-aa:11: Found RCS tag \"$"+"Author$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".") -} - -func (s *Suite) Test_FileType_String(c *check.C) { - c.Check(ftUnknown.String(), equals, "unknown") + "WARN: ~/patch-aa:7: Found CVS tag \"$"+"Id$\". Please remove it.", + "WARN: ~/patch-aa:8: Found CVS tag \"$"+"Id$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".", + "WARN: ~/patch-aa:11: Found CVS tag \"$"+"Author$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".") } diff --git a/pkgtools/pkglint/files/pkglint.1 b/pkgtools/pkglint/files/pkglint.1 index 85e02e03a0b..88839eaf136 100644 --- a/pkgtools/pkglint/files/pkglint.1 +++ b/pkgtools/pkglint/files/pkglint.1 @@ -1,4 +1,4 @@ -.\" $NetBSD: pkglint.1,v 1.55 2019/03/10 19:01:50 rillig Exp $ +.\" $NetBSD: pkglint.1,v 1.56 2019/06/30 20:56:19 rillig Exp $ .\" From FreeBSD: portlint.1,v 1.8 1997/11/25 14:53:14 itojun Exp .\" .\" Copyright (c) 1997 by Jun-ichiro Itoh <itojun@itojun.org>. @@ -8,7 +8,7 @@ .\" Thomas Klausner <wiz@NetBSD.org>, 2012. .\" Roland Illig <rillig@NetBSD.org>, 2015-2019. .\" -.Dd January 14, 2018 +.Dd June 17, 2019 .Dt PKGLINT 1 .Os .Sh NAME @@ -88,8 +88,6 @@ For a list of warnings, see below. Enable all checks. .It Cm none Disable all checks. -.It Cm [no-]extra -Check remaining files in the package directory. .It Cm [no-]global Check inter-package consistency for distfile hashes and used licenses. .El diff --git a/pkgtools/pkglint/files/pkglint.go b/pkgtools/pkglint/files/pkglint.go index f88412eb77f..122fe89de61 100644 --- a/pkgtools/pkglint/files/pkglint.go +++ b/pkgtools/pkglint/files/pkglint.go @@ -2,6 +2,7 @@ package pkglint import ( "fmt" + "io" "netbsd.org/pkglint/getopt" "netbsd.org/pkglint/histogram" "netbsd.org/pkglint/regex" @@ -25,13 +26,14 @@ type Pkglint struct { Pkgsrc Pkgsrc // Global data, mostly extracted from mk/*. Pkg *Package // The package that is currently checked, or nil. - Todo []string // The files or directories that still need to be checked. - Wip bool // Is the currently checked file or package from pkgsrc-wip? - Infrastructure bool // Is the currently checked file from the pkgsrc infrastructure? - Testing bool // Is pkglint in self-testing mode (only during development)? - Username string // For checking against OWNER and MAINTAINER - cvsEntriesDir string // Cached to avoid I/O - cvsEntriesLines Lines + Todo []string // The files or directories that still need to be checked. + Wip bool // Is the currently checked file or package from pkgsrc-wip? + Infrastructure bool // Is the currently checked file from the pkgsrc infrastructure? + Testing bool // Is pkglint in self-testing mode (only during development)? + Username string // For checking against OWNER and MAINTAINER + + cvsEntriesDir string // Cached to avoid I/O + cvsEntries map[string]CvsEntry Logger Logger @@ -63,8 +65,6 @@ func NewPkglint() Pkglint { // This is to ensure that tests are properly initialized and shut down. func unusablePkglint() Pkglint { return Pkglint{} } -func (pkglint *Pkglint) usable() bool { return pkglint != nil } - type InterPackage struct { hashes map[string]*Hash // Maps "alg:filename" => hash (inter-package check). usedLicenses map[string]struct{} // Maps "license name" => true (inter-package check). @@ -118,7 +118,6 @@ func (ip *InterPackage) Bl3(name string, loc *Location) *Location { } type CmdOpts struct { - CheckExtra, CheckGlobal bool // TODO: Are these Warn* options really all necessary? @@ -160,86 +159,104 @@ var ( trace tracePkg.Tracer ) -func Main() int { - G.Logger.out = NewSeparatorWriter(os.Stdout) - G.Logger.err = NewSeparatorWriter(os.Stderr) - trace.Out = os.Stdout - exitCode := G.Main(os.Args...) - if G.Opts.Profiling { - G = unusablePkglint() // Free all memory. - runtime.GC() // For detecting possible memory leaks; see qa-pkglint. - } - return exitCode -} - // Main runs the main program with the given arguments. -// argv[0] is the program name. +// args[0] is the program name. // // Note: during tests, calling this method disables tracing // because the getopt parser resets all options before the actual parsing. // One of these options is trace.Tracing, which is connected to --debug. // // It also discards the -Wall option that is used by default in other tests. -func (pkglint *Pkglint) Main(argv ...string) (exitCode int) { +func (pkglint *Pkglint) Main(stdout io.Writer, stderr io.Writer, args []string) (exitCode int) { + G.Logger.out = NewSeparatorWriter(stdout) + G.Logger.err = NewSeparatorWriter(stderr) + trace.Out = stdout + defer func() { if r := recover(); r != nil { - if _, ok := r.(pkglintFatal); ok { - exitCode = 1 - } else { - panic(r) - } + _ = r.(pkglintFatal) + exitCode = 1 } }() - if exitcode := pkglint.ParseCommandLine(argv); exitcode != -1 { + if exitcode := pkglint.ParseCommandLine(args); exitcode != -1 { return exitcode } if pkglint.Opts.Profiling { + defer pkglint.setUpProfiling()() + } - defer func() { - pkglint.fileCache.table = nil - pkglint.fileCache.mapping = nil - runtime.GC() - - fd, err := os.Create("pkglint.heapdump") - assertNil(err, "heapDump.create") + pkglint.prepareMainLoop() - debug.WriteHeapDump(fd.Fd()) + for len(pkglint.Todo) > 0 { + item := pkglint.Todo[0] + pkglint.Todo = pkglint.Todo[1:] + pkglint.Check(item) + } - err = fd.Close() - assertNil(err, "heapDump.close") - }() + pkglint.Pkgsrc.checkToplevelUnusedLicenses() - f, err := os.Create("pkglint.pprof") - if err != nil { - dummyLine.Fatalf("Cannot create profiling file: %s", err) - } - defer f.Close() + pkglint.Logger.ShowSummary() + if pkglint.Logger.errors != 0 { + return 1 + } + return 0 +} - err = pprof.StartCPUProfile(f) - assertNil(err, "Cannot start profiling") - defer pprof.StopCPUProfile() +func (pkglint *Pkglint) setUpProfiling() func() { - pkglint.res.Profiling() - pkglint.Logger.histo = histogram.New() - pkglint.loaded = histogram.New() - defer func() { - pkglint.Logger.out.Write("") - pkglint.Logger.histo.PrintStats(pkglint.Logger.out.out, "loghisto", -1) - pkglint.res.PrintStats(pkglint.Logger.out.out) - pkglint.loaded.PrintStats(pkglint.Logger.out.out, "loaded", 10) - pkglint.Logger.out.WriteLine(sprintf("fileCache: %d hits, %d misses", pkglint.fileCache.hits, pkglint.fileCache.misses)) - }() + var cleanups []func() + atExit := func(cleanup func()) { + cleanups = append(cleanups, cleanup) } - for _, arg := range pkglint.Opts.args { - pkglint.Todo = append(pkglint.Todo, filepath.ToSlash(arg)) - } - if len(pkglint.Todo) == 0 { - pkglint.Todo = []string{"."} + atExit(func() { + pkglint.fileCache.table = nil + pkglint.fileCache.mapping = nil + runtime.GC() + + fd, err := os.Create("pkglint.heapdump") + assertNil(err, "heapDump.create") + + debug.WriteHeapDump(fd.Fd()) + + err = fd.Close() + assertNil(err, "heapDump.close") + + G = unusablePkglint() // Free all memory. + runtime.GC() // For detecting possible memory leaks; see qa-pkglint. + }) + + f, err := os.Create("pkglint.pprof") + if err != nil { + dummyLine.Fatalf("Cannot create profiling file: %s", err) + } + atExit(func() { assertNil(f.Close(), "") }) + + err = pprof.StartCPUProfile(f) + assertNil(err, "Cannot start profiling") + atExit(pprof.StopCPUProfile) + + pkglint.res.Profiling() + pkglint.Logger.histo = histogram.New() + pkglint.loaded = histogram.New() + atExit(func() { + pkglint.Logger.out.Write("") + pkglint.Logger.histo.PrintStats(pkglint.Logger.out.out, "loghisto", -1) + pkglint.res.PrintStats(pkglint.Logger.out.out) + pkglint.loaded.PrintStats(pkglint.Logger.out.out, "loaded", 10) + pkglint.Logger.out.WriteLine(sprintf("fileCache: %d hits, %d misses", pkglint.fileCache.hits, pkglint.fileCache.misses)) + }) + + return func() { + for i := range cleanups { + cleanups[len(cleanups)-1-i]() + } } +} +func (pkglint *Pkglint) prepareMainLoop() { firstDir := pkglint.Todo[0] if fileExists(firstDir) { firstDir = path.Dir(firstDir) @@ -262,20 +279,6 @@ func (pkglint *Pkglint) Main(argv ...string) (exitCode int) { assertNil(err, "user.Current") // On Windows, this is `Computername\Username`. pkglint.Username = replaceAll(currentUser.Username, `^.*\\`, "") - - for len(pkglint.Todo) > 0 { - item := pkglint.Todo[0] - pkglint.Todo = pkglint.Todo[1:] - pkglint.Check(item) - } - - pkglint.Pkgsrc.checkToplevelUnusedLicenses() - - pkglint.Logger.ShowSummary() - if pkglint.Logger.errors != 0 { - return 1 - } - return 0 } func (pkglint *Pkglint) ParseCommandLine(args []string) int { @@ -301,7 +304,6 @@ func (pkglint *Pkglint) ParseCommandLine(args []string) int { opts.AddFlagVar('V', "version", &gopts.ShowVersion, false, "show the version number of pkglint") warn := opts.AddFlagGroup('W', "warning", "warning,...", "enable or disable groups of warnings") - check.AddFlagVar("extra", &gopts.CheckExtra, false, "check various additional files") check.AddFlagVar("global", &gopts.CheckGlobal, false, "inter-package checks") warn.AddFlagVar("extra", &gopts.WarnExtra, false, "enable some extra warnings") @@ -330,6 +332,14 @@ func (pkglint *Pkglint) ParseCommandLine(args []string) int { return 0 } + pkglint.Todo = nil + for _, arg := range pkglint.Opts.args { + pkglint.Todo = append(pkglint.Todo, filepath.ToSlash(arg)) + } + if len(pkglint.Todo) == 0 { + pkglint.Todo = []string{"."} + } + return -1 } @@ -349,12 +359,22 @@ func (pkglint *Pkglint) Check(dirent string) { } st, err := os.Lstat(dirent) - if err != nil || !st.Mode().IsDir() && !st.Mode().IsRegular() { + if err != nil { + NewLineWhole(dirent).Errorf("No such file or directory.") + return + } + + pkglint.checkMode(dirent, st.Mode()) +} + +func (pkglint *Pkglint) checkMode(dirent string, mode os.FileMode) { + // TODO: merge duplicate code in Package.checkDirent + isDir := mode.IsDir() + isReg := mode.IsRegular() + if !isDir && !isReg { NewLineWhole(dirent).Errorf("No such file or directory.") return } - isDir := st.Mode().IsDir() - isReg := st.Mode().IsRegular() dir := dirent if !isDir { @@ -374,12 +394,12 @@ func (pkglint *Pkglint) Check(dirent string) { if isReg { depth := strings.Count(pkgsrcRel, "/") - pkglint.checkExecutable(dirent, st.Mode()) + pkglint.checkExecutable(dirent, mode) pkglint.checkReg(dirent, basename, depth) return } - if isDir && isEmptyDir(dirent) { + if isEmptyDir(dirent) { return } @@ -395,42 +415,6 @@ func (pkglint *Pkglint) Check(dirent string) { } } -// checkDirent checks a directory entry based on its filename and its mode -// (regular file, directory, symlink). -func (pkglint *Pkglint) checkDirent(dirent string, mode os.FileMode) { - basename := path.Base(dirent) - - switch { - - case mode.IsRegular(): - pkgsrcRel := pkglint.Pkgsrc.ToRel(dirent) - depth := strings.Count(pkgsrcRel, "/") - pkglint.checkReg(dirent, basename, depth) - - case hasPrefix(basename, "work"): - if pkglint.Opts.Import { - NewLineWhole(dirent).Errorf("Must be cleaned up before committing the package.") - } - return - - case mode.IsDir(): - switch { - case basename == "files" || basename == "patches" || isIgnoredFilename(basename): - // Ok - case matches(dirent, `(?:^|/)files/[^/]*$`): - // Ok - case !isEmptyDir(dirent): - NewLineWhole(dirent).Warnf("Unknown directory name.") - } - - case mode&os.ModeSymlink != 0: - NewLineWhole(dirent).Warnf("Invalid symlink name.") - - default: - NewLineWhole(dirent).Errorf("Only files and directories are allowed in pkgsrc.") - } -} - // checkdirPackage checks a complete pkgsrc package, including each // of the files individually, and also when seen in combination. func (pkglint *Pkglint) checkdirPackage(dir string) { @@ -456,7 +440,7 @@ func findPkgsrcTopdir(dirname string) string { return "" } -func resolveVariableRefs(mklines MkLines, text string) (resolved string) { +func resolveVariableRefs(mklines *MkLines, text string) (resolved string) { // TODO: How does this fit into the Scope type, which is newer than this function? if !contains(text, "${") { @@ -507,9 +491,9 @@ func CheckFileOther(filename string) { } } -func CheckLinesDescr(lines Lines) { +func CheckLinesDescr(lines *Lines) { if trace.Tracing { - defer trace.Call1(lines.FileName)() + defer trace.Call1(lines.Filename)() } for _, line := range lines.Lines { @@ -541,9 +525,9 @@ func CheckLinesDescr(lines Lines) { SaveAutofixChanges(lines) } -func CheckLinesMessage(lines Lines) { +func CheckLinesMessage(lines *Lines) { if trace.Tracing { - defer trace.Call1(lines.FileName)() + defer trace.Call1(lines.Filename)() } // For now, skip all checks when the MESSAGE may be built from multiple @@ -558,7 +542,7 @@ func CheckLinesMessage(lines Lines) { explanation := func() []string { return []string{ "A MESSAGE file should consist of a header line, having 75 \"=\"", - "characters, followed by a line containing only the RCS Id, then an", + "characters, followed by a line containing only the CVS Id, then an", "empty line, your text and finally the footer line, which is the", "same as the header line."} } @@ -577,9 +561,9 @@ func CheckLinesMessage(lines Lines) { fix.Explain(explanation()...) fix.InsertBefore(hline) fix.Apply() - lines.CheckRcsID(0, ``, "") - } else if 1 < lines.Len() { - lines.CheckRcsID(1, ``, "") + lines.CheckCvsID(0, ``, "") + } else { + lines.CheckCvsID(1, ``, "") } for _, line := range lines.Lines { ck := LineChecker{line} @@ -685,8 +669,7 @@ func (pkglint *Pkglint) checkReg(filename, basename string, depth int) { NewLineWhole(filename).Warnf("Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.") case (hasPrefix(basename, "Makefile") || hasSuffix(basename, ".mk")) && - !(hasPrefix(filename, "files/") || contains(filename, "/files/")) && - !(hasPrefix(filename, "patches/") || contains(filename, "/patches/")): + !pathContainsDir(filename, "files"): CheckFileMk(filename) case hasPrefix(basename, "PLIST"): @@ -699,7 +682,7 @@ func (pkglint *Pkglint) checkReg(filename, basename string, depth int) { _ = pkglint.Pkgsrc.loadDocChangesFromFile(filename) case matches(filename, `(?:^|/)files/[^/]*$`): - // Skip + // Skip files directly in the files/ directory, but not those further down. case basename == "spec": if !hasPrefix(pkglint.Pkgsrc.ToRel(filename), "regress/") { @@ -711,9 +694,6 @@ func (pkglint *Pkglint) checkReg(filename, basename string, depth int) { default: NewLineWhole(filename).Warnf("Unexpected file found.") - if pkglint.Opts.CheckExtra { - CheckFileOther(filename) - } } } @@ -759,7 +739,7 @@ func (pkglint *Pkglint) checkExecutable(filename string, mode os.FileMode) { fix.Apply() } -func CheckLinesTrailingEmptyLines(lines Lines) { +func CheckLinesTrailingEmptyLines(lines *Lines) { max := lines.Len() last := max @@ -776,30 +756,16 @@ func CheckLinesTrailingEmptyLines(lines Lines) { // The command can be "sed" or "gsed" or "${SED}". // If a tool is returned, usable tells whether that tool has been added // to USE_TOOLS in the current scope (file or package). -func (pkglint *Pkglint) Tool(mklines MkLines, command string, time ToolTime) (tool *Tool, usable bool) { - varname := "" - if varUse := ToVarUse(command); varUse != nil { - varname = varUse.varname - } - +func (pkglint *Pkglint) Tool(mklines *MkLines, command string, time ToolTime) (tool *Tool, usable bool) { tools := pkglint.tools(mklines) - if t := tools.ByName(command); t != nil { - if tools.Usable(t, time) { - return t, true - } - tool = t + if varUse := ToVarUse(command); varUse != nil { + tool = tools.ByVarname(varUse.varname) + } else { + tool = tools.ByName(command) } - if t := tools.ByVarname(varname); t != nil { - if tools.Usable(t, time) { - return t, true - } - if tool == nil { - tool = t - } - } - return + return tool, tool != nil && tools.Usable(tool, time) } // ToolByVarname looks up the tool by its variable name, e.g. "SED". @@ -808,11 +774,11 @@ func (pkglint *Pkglint) Tool(mklines MkLines, command string, time ToolTime) (to // It is not guaranteed to be usable (added to USE_TOOLS), only defined; // that must be checked by the calling code, // see Tool.UsableAtLoadTime and Tool.UsableAtRunTime. -func (pkglint *Pkglint) ToolByVarname(mklines MkLines, varname string) *Tool { +func (pkglint *Pkglint) ToolByVarname(mklines *MkLines, varname string) *Tool { return pkglint.tools(mklines).ByVarname(varname) } -func (pkglint *Pkglint) tools(mklines MkLines) *Tools { +func (pkglint *Pkglint) tools(mklines *MkLines) *Tools { if mklines != nil { return mklines.Tools } else { @@ -820,18 +786,53 @@ func (pkglint *Pkglint) tools(mklines MkLines) *Tools { } } -func (pkglint *Pkglint) loadCvsEntries(filename string) Lines { +func (pkglint *Pkglint) loadCvsEntries(filename string) map[string]CvsEntry { dir := path.Dir(filename) if dir == pkglint.cvsEntriesDir { - return pkglint.cvsEntriesLines + return pkglint.cvsEntries + } + + var entries map[string]CvsEntry + + handle := func(line *Line, add bool, text string) { + if !hasPrefix(text, "/") { + return + } + + fields := strings.Split(text, "/") + if len(fields) != 6 { + line.Errorf("Invalid line: %s", line.Text) + return + } + + if add { + entries[fields[1]] = CvsEntry{fields[1], fields[2], fields[3], fields[4], fields[5]} + } else { + delete(entries, fields[1]) + } } lines := Load(dir+"/CVS/Entries", 0) - if lines == nil { - return nil + if lines != nil { + entries = make(map[string]CvsEntry) + for _, line := range lines.Lines { + handle(line, true, line.Text) + } + + logLines := Load(dir+"/CVS/Entries.Log", 0) + if logLines != nil { + for _, line := range logLines.Lines { + text := line.Text + if hasPrefix(text, "A ") { + handle(line, true, text[2:]) + } else if hasPrefix(text, "R ") { + handle(line, false, text[2:]) + } + } + } } pkglint.cvsEntriesDir = dir - pkglint.cvsEntriesLines = lines - return lines + pkglint.cvsEntries = entries + return entries } diff --git a/pkgtools/pkglint/files/pkglint_test.go b/pkgtools/pkglint/files/pkglint_test.go index 8c8f0a4aaf8..0c0d07ac537 100644 --- a/pkgtools/pkglint/files/pkglint_test.go +++ b/pkgtools/pkglint/files/pkglint_test.go @@ -9,6 +9,8 @@ import ( "strings" ) +func (pkglint *Pkglint) usable() bool { return pkglint.fileCache != nil } + func (s *Suite) Test_Pkglint_Main__help(c *check.C) { t := s.Init(c) @@ -39,7 +41,6 @@ func (s *Suite) Test_Pkglint_Main__help(c *check.C) { " Flags for -C, --check:", " all all of the following", " none none of the following", - " extra check various additional files (disabled)", " global inter-package checks (disabled)", "", " Flags for -W, --warning:", @@ -102,19 +103,6 @@ func (s *Suite) Test_Pkglint_Main__unknown_option(c *check.C) { // See Test_Pkglint_Main__help for the complete output. } -// This test covers the code path for unexpected panics. -func (s *Suite) Test_Pkglint_Main__panic(c *check.C) { - t := s.Init(c) - - pkg := t.SetUpPackage("category/package") - - G.Logger.out = nil // Force an error that cannot happen in practice. - - c.Check( - func() { t.Main(pkg) }, - check.PanicMatches, `(?s).*\bnil pointer\b.*`) -} - // Demonstrates which infrastructure files are necessary to actually run // pkglint in a realistic scenario. // @@ -131,7 +119,7 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("doc/CHANGES-2018", - RcsID, + CvsID, "", "Changes to the packages collection and infrastructure in 2018:", "", @@ -139,7 +127,7 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { // See Pkgsrc.loadSuggestedUpdates. t.CreateFileLines("doc/TODO", - RcsID, + CvsID, "", "Suggested package updates", "", @@ -148,7 +136,7 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { // The MASTER_SITES in the package Makefile are searched here. // See Pkgsrc.loadMasterSites. t.CreateFileLines("mk/fetch/sites.mk", - MkRcsID, + MkCvsID, "", "MASTER_SITE_GITHUB+=\thttps://github.com/") @@ -163,7 +151,7 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { // so that it can be used in CATEGORIES in the package Makefile. // The category "tools" on the other hand is not valid. t.CreateFileLines("sysutils/Makefile", - MkRcsID) + MkCvsID) // The package Makefile in this test is quite simple, containing just the // standard variable definitions. The data for checking the variable @@ -171,7 +159,7 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { // (as defined in the previous lines), and partly in the pkglint // code directly. Many details can be found in vartypecheck.go. t.CreateFileLines("sysutils/checkperms/Makefile", - MkRcsID, + MkCvsID, "", "DISTNAME=\tcheckperms-1.11", "CATEGORIES=\tsysutils tools", @@ -186,14 +174,14 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { t.CreateFileLines("sysutils/checkperms/MESSAGE", "===========================================================================", - RcsID, + CvsID, "", "After installation, this package has to be configured in a special way.", "", "===========================================================================") t.CreateFileLines("sysutils/checkperms/PLIST", - PlistRcsID, + PlistCvsID, "bin/checkperms", "man/man1/checkperms.1") @@ -204,7 +192,7 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { "Make the package work on MS-DOS") t.CreateFileLines("sysutils/checkperms/patches/patch-checkperms.c", - RcsID, + CvsID, "", "A simple patch demonstrating that pkglint checks for missing", "removed lines. The hunk headers says that one line is to be", @@ -217,7 +205,7 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { "+// Header 2", "+// Header 3") t.CreateFileLines("sysutils/checkperms/distinfo", - RcsID, + CvsID, "", "SHA1 (checkperms-1.12.tar.gz) = 34c084b4d06bcd7a8bba922ff57677e651eeced5", "RMD160 (checkperms-1.12.tar.gz) = cd95029aa930b6201e9580b3ab7e36dd30b8f925", @@ -248,6 +236,20 @@ func (s *Suite) Test_Pkglint_Main__complete_package(c *check.C) { "(Run \"pkglint -F\" to automatically fix some issues.)") } +func (s *Suite) Test_Pkglint_Main__autofix_exitcode(c *check.C) { + t := s.Init(c) + + t.SetUpPkgsrc() + t.CreateFileLines("filename.mk", + "") + + exitcode := t.Main("-Wall", "--autofix", t.File("filename.mk")) + + t.CheckOutputLines( + "AUTOFIX: ~/filename.mk:1: Inserting a line \"# $NetBSD: pkglint_test.go,v 1.44 2019/06/30 20:56:19 rillig Exp $\" before this line.") + t.Check(exitcode, equals, 0) +} + // Run pkglint in a realistic environment. // // env \ @@ -277,10 +279,7 @@ func (s *Suite) Test_Pkglint__realistic(c *check.C) { cmdline := os.Getenv("PKGLINT_TESTCMDLINE") if cmdline != "" { - G.Logger.out = NewSeparatorWriter(os.Stdout) - G.Logger.err = NewSeparatorWriter(os.Stderr) - trace.Out = os.Stdout - G.Main(append([]string{"pkglint"}, strings.Fields(cmdline)...)...) + G.Main(os.Stdout, os.Stderr, append([]string{"pkglint"}, strings.Fields(cmdline)...)) } } @@ -385,7 +384,7 @@ func (s *Suite) Test_Pkglint_Check(c *check.C) { t.CreateFileLines("mk/bsd.pkg.mk") t.CreateFileLines("category/package/Makefile") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tCategory\u0007", "", @@ -393,7 +392,7 @@ func (s *Suite) Test_Pkglint_Check(c *check.C) { "", ".include \"../mk/misc/category.mk\"") t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "COMMENT=\tToplevel\u0005") G.Check(t.File(".")) @@ -418,20 +417,31 @@ func (s *Suite) Test_Pkglint_Check(c *check.C) { "ERROR: ~/category/package/nonexistent: No such file or directory.") } +func (s *Suite) Test_Pkglint_checkMode__neither_file_nor_directory(c *check.C) { + t := s.Init(c) + + G.checkMode("/dev/null", os.ModeDevice) + + t.CheckOutputLines( + "ERROR: /dev/null: No such file or directory.") +} + // Pkglint must never be trapped in an endless loop, even when // resolving the value of a variable that refers back to itself. func (s *Suite) Test_resolveVariableRefs__circular_reference(c *check.C) { t := s.Init(c) - mkline := t.NewMkLine("filename.mk", 1, "GCC_VERSION=${GCC_VERSION}") + mkline := t.NewMkLine("filename.mk", 1, "VAR=\t1:${VAR}+ 2:${VAR}") G.Pkg = NewPackage(t.File("category/pkgbase")) - G.Pkg.vars.Define("GCC_VERSION", mkline) + G.Pkg.vars.Define("VAR", mkline) // TODO: It may be better to define MkLines.Resolve and Package.Resolve, // to clearly state the scope of the involved variables. - resolved := resolveVariableRefs(nil, "gcc-${GCC_VERSION}") + resolved := resolveVariableRefs(nil, "the a:${VAR} b:${VAR}") - c.Check(resolved, equals, "gcc-${GCC_VERSION}") + // TODO: The ${VAR} after "b:" should also be expanded since there + // is no recursion. + c.Check(resolved, equals, "the a:1:${VAR}+ 2:${VAR} b:${VAR}") } func (s *Suite) Test_resolveVariableRefs__multilevel(c *check.C) { @@ -495,7 +505,7 @@ func (s *Suite) Test_CheckLinesDescr(c *check.C) { "WARN: DESCR:25: File too long (should be no more than 24 lines).") } -func (s *Suite) Test_CheckLinesMessage__short(c *check.C) { +func (s *Suite) Test_CheckLinesMessage__one_line_of_text(c *check.C) { t := s.Init(c) lines := t.NewLines("MESSAGE", @@ -507,6 +517,18 @@ func (s *Suite) Test_CheckLinesMessage__short(c *check.C) { "WARN: MESSAGE:1: File too short.") } +func (s *Suite) Test_CheckLinesMessage__one_hline(c *check.C) { + t := s.Init(c) + + lines := t.NewLines("MESSAGE", + strings.Repeat("=", 75)) + + CheckLinesMessage(lines) + + t.CheckOutputLines( + "WARN: MESSAGE:1: File too short.") +} + func (s *Suite) Test_CheckLinesMessage__malformed(c *check.C) { t := s.Init(c) @@ -546,7 +568,7 @@ func (s *Suite) Test_CheckLinesMessage__autofix(c *check.C) { "=============================================\" after this line.") t.CheckFileLines("MESSAGE", "===========================================================================", - RcsID, + CvsID, "1", "2", "3", @@ -564,7 +586,7 @@ func (s *Suite) Test_CheckLinesMessage__common(c *check.C) { "MESSAGE_SRC+=\t${.CURDIR}/MESSAGE") t.CreateFileLines("category/package/MESSAGE.common", hline, - RcsID, + CvsID, "common line") t.CreateFileLines("category/package/MESSAGE", hline) @@ -584,7 +606,7 @@ func (s *Suite) Test_Pkglint_checkReg__alternatives(c *check.C) { lines := t.SetUpFileLines("category/package/ALTERNATIVES", "bin/tar bin/gnu-tar") - t.Main(lines.FileName) + t.Main(lines.Filename) t.CheckOutputLines( "ERROR: ~/category/package/ALTERNATIVES:1: Alternative implementation \"bin/gnu-tar\" must be an absolute path.", @@ -679,7 +701,7 @@ func (s *Suite) Test_Pkglint_Tool__prefer_mk_over_pkgsrc(c *check.C) { t := s.Init(c) mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue") - mklines := t.NewMkLines("Makefile", MkRcsID) + mklines := t.NewMkLines("Makefile", MkCvsID) global := G.Pkgsrc.Tools.Define("tool", "TOOL", mkline) local := mklines.Tools.Define("tool", "TOOL", mkline) @@ -698,7 +720,7 @@ func (s *Suite) Test_Pkglint_Tool__prefer_mk_over_pkgsrc(c *check.C) { func (s *Suite) Test_Pkglint_Tool__lookup_by_name_fallback(c *check.C) { t := s.Init(c) - mklines := t.NewMkLines("Makefile", MkRcsID) + mklines := t.NewMkLines("Makefile", MkCvsID) t.SetUpTool("tool", "", Nowhere) loadTimeTool, loadTimeUsable := G.Tool(mklines, "tool", LoadTime) @@ -718,7 +740,7 @@ func (s *Suite) Test_Pkglint_Tool__lookup_by_varname(c *check.C) { t := s.Init(c) mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue") - mklines := t.NewMkLines("Makefile", MkRcsID) + mklines := t.NewMkLines("Makefile", MkCvsID) global := G.Pkgsrc.Tools.Define("tool", "TOOL", mkline) local := mklines.Tools.Define("tool", "TOOL", mkline) @@ -738,7 +760,7 @@ func (s *Suite) Test_Pkglint_Tool__lookup_by_varname(c *check.C) { func (s *Suite) Test_Pkglint_Tool__lookup_by_varname_fallback(c *check.C) { t := s.Init(c) - mklines := t.NewMkLines("Makefile", MkRcsID) + mklines := t.NewMkLines("Makefile", MkCvsID) G.Pkgsrc.Tools.def("tool", "TOOL", false, Nowhere, nil) loadTimeTool, loadTimeUsable := G.Tool(mklines, "${TOOL}", LoadTime) @@ -754,7 +776,7 @@ func (s *Suite) Test_Pkglint_Tool__lookup_by_varname_fallback(c *check.C) { func (s *Suite) Test_Pkglint_Tool__lookup_by_varname_fallback_runtime(c *check.C) { t := s.Init(c) - mklines := t.NewMkLines("Makefile", MkRcsID) + mklines := t.NewMkLines("Makefile", MkCvsID) G.Pkgsrc.Tools.def("tool", "TOOL", false, AtRunTime, nil) loadTimeTool, loadTimeUsable := G.Tool(mklines, "${TOOL}", LoadTime) @@ -770,7 +792,7 @@ func (s *Suite) Test_Pkglint_ToolByVarname__prefer_mk_over_pkgsrc(c *check.C) { t := s.Init(c) mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue") - mklines := t.NewMkLines("Makefile", MkRcsID) + mklines := t.NewMkLines("Makefile", MkCvsID) global := G.Pkgsrc.Tools.Define("tool", "TOOL", mkline) local := mklines.Tools.Define("tool", "TOOL", mkline) @@ -783,7 +805,7 @@ func (s *Suite) Test_Pkglint_ToolByVarname__prefer_mk_over_pkgsrc(c *check.C) { func (s *Suite) Test_Pkglint_ToolByVarname(c *check.C) { t := s.Init(c) - mklines := t.NewMkLines("Makefile", MkRcsID) + mklines := t.NewMkLines("Makefile", MkCvsID) G.Pkgsrc.Tools.def("tool", "TOOL", false, AtRunTime, nil) c.Check(G.ToolByVarname(mklines, "TOOL").String(), equals, "tool:TOOL::AtRunTime") @@ -825,71 +847,30 @@ func (s *Suite) Test_Pkglint_Check__invalid_files_before_import(c *check.C) { "ERROR: ~/category/package/work: Must be cleaned up before committing the package.") } -func (s *Suite) Test_Pkglint_checkDirent__errors(c *check.C) { - t := s.Init(c) - - t.SetUpCommandLine("-Call", "-Wall,no-space") - t.SetUpPkgsrc() - t.CreateFileLines("category/package/files/subdir/file") - t.CreateFileLines("category/package/files/subdir/subsub/file") - t.FinishSetUp() - - G.checkDirent(t.File("category/package/options.mk"), 0444) - G.checkDirent(t.File("category/package/files/subdir"), 0555|os.ModeDir) - G.checkDirent(t.File("category/package/files/subdir/subsub"), 0555|os.ModeDir) - G.checkDirent(t.File("category/package/files"), 0555|os.ModeDir) - - t.CheckOutputLines( - "ERROR: ~/category/package/options.mk: Cannot be read.", - "WARN: ~/category/package/files/subdir/subsub: Unknown directory name.") -} - -func (s *Suite) Test_Pkglint_checkDirent__file_selection(c *check.C) { - t := s.Init(c) - - t.SetUpCommandLine("-Call", "-Wall,no-space") - t.SetUpPkgsrc() - t.CreateFileLines("doc/CHANGES-2018", - RcsID) - t.CreateFileLines("category/package/buildlink3.mk", - MkRcsID) - t.CreateFileLines("category/package/unexpected.txt", - RcsID) - t.FinishSetUp() - - G.checkDirent(t.File("doc/CHANGES-2018"), 0444) - G.checkDirent(t.File("category/package/buildlink3.mk"), 0444) - G.checkDirent(t.File("category/package/unexpected.txt"), 0444) - - t.CheckOutputLines( - "WARN: ~/category/package/buildlink3.mk:EOF: Expected a BUILDLINK_TREE line.", - "WARN: ~/category/package/unexpected.txt: Unexpected file found.") -} - func (s *Suite) Test_Pkglint_checkReg__readme_and_todo(c *check.C) { t := s.Init(c) t.CreateFileLines("category/Makefile", - MkRcsID) + MkCvsID) t.CreateFileLines("category/package/files/README", "Extra file that is installed later.") t.CreateFileDummyPatch("category/package/patches/patch-README") t.CreateFileLines("category/package/Makefile", - MkRcsID, + MkCvsID, "CATEGORIES=category", "", "COMMENT=Comment", "LICENSE=2-clause-bsd") t.CreateFileLines("category/package/PLIST", - PlistRcsID, + PlistCvsID, "bin/program") t.CreateFileLines("category/package/README", "This package ...") t.CreateFileLines("category/package/TODO", "Make this package work.") t.CreateFileLines("category/package/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-README) = ebbf34b0641bcb508f17d5a27f2bf2a536d810ac") @@ -942,6 +923,17 @@ func (s *Suite) Test_Pkglint_checkReg__unknown_file_in_patches(c *check.C) { "Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.") } +func (s *Suite) Test_Pkglint_checkReg__patch_for_Makefile_fragment(c *check.C) { + t := s.Init(c) + + t.CreateFileDummyPatch("category/package/patches/patch-compiler.mk") + t.Chdir("category/package") + + G.checkReg(t.File("patches/patch-compiler.mk"), "patch-compiler.mk", 3) + + t.CheckOutputEmpty() +} + func (s *Suite) Test_Pkglint_checkReg__file_in_files(c *check.C) { t := s.Init(c) @@ -966,23 +958,6 @@ func (s *Suite) Test_Pkglint_checkReg__spec(c *check.C) { "WARN: ~/category/package/spec: Only packages in regress/ may have spec files.") } -// Since all required information is passed to G.checkDirent via parameters, -// this test produces the expected results even though none of these files actually exists. -func (s *Suite) Test_Pkglint_checkDirent__skipped(c *check.C) { - t := s.Init(c) - - G.checkDirent("work", os.ModeSymlink) - G.checkDirent("work.i386", os.ModeSymlink) - G.checkDirent("work.hostname", os.ModeSymlink) - G.checkDirent("other", os.ModeSymlink) - - G.checkDirent("device", os.ModeDevice) - - t.CheckOutputLines( - "WARN: other: Invalid symlink name.", - "ERROR: device: Only files and directories are allowed in pkgsrc.") -} - // A package that is very incomplete may produce lots of warnings. // This case is unrealistic since most packages are either generated by url2pkg // or copied from an existing working package. @@ -991,12 +966,12 @@ func (s *Suite) Test_Pkglint_checkdirPackage(c *check.C) { t.Chdir("category/package") t.CreateFileLines("Makefile", - MkRcsID) + MkCvsID) G.checkdirPackage(".") t.CheckOutputLines( - "WARN: Makefile: Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.", + "WARN: Makefile: This package should have a PLIST file.", "WARN: distinfo: A package that downloads files should have a distinfo file.", "ERROR: Makefile: Each package must define its LICENSE.", "WARN: Makefile: Each package should define a COMMENT.") @@ -1008,19 +983,19 @@ func (s *Suite) Test_Pkglint_checkdirPackage__PKGDIR(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("category/Makefile") t.CreateFileLines("other/package/Makefile", - MkRcsID) + MkCvsID) t.CreateFileLines("other/package/PLIST", - PlistRcsID, + PlistCvsID, "bin/program") t.CreateFileLines("other/package/distinfo", - RcsID, + CvsID, "", "SHA1 (patch-aa) = da39a3ee5e6b4b0d3255bfef95601890afd80709") t.CreateFileLines("category/package/patches/patch-aa", - RcsID) + CvsID) t.Chdir("category/package") t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "CATEGORIES=\tcategory", "", @@ -1057,7 +1032,7 @@ func (s *Suite) Test_Pkglint_checkdirPackage__meta_package_without_license(c *ch t.Chdir("category/package") t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "META_PACKAGE=\tyes") t.SetUpVartypes() @@ -1156,6 +1131,34 @@ func (s *Suite) Test_CheckFileOther__no_tracing(c *check.C) { func (s *Suite) Test_Pkglint_checkExecutable(c *check.C) { t := s.Init(c) + filename := t.CreateFileLines("file.mk") + err := os.Chmod(filename, 0555) + assertNil(err, "") + + G.checkExecutable(filename, 0555) + + t.CheckOutputLines( + "WARN: ~/file.mk: Should not be executable.") + + t.SetUpCommandLine("--autofix") + + G.checkExecutable(filename, 0555) + + t.CheckOutputMatches( + "AUTOFIX: ~/file.mk: Clearing executable bits") + + // On Windows, this is effectively a no-op test since there is no + // execute-bit. The only relevant permissions bit is whether a + // file is readonly or not. + st, err := os.Lstat(filename) + if t.Check(err, check.IsNil) { + t.Check(st.Mode()&0111, equals, os.FileMode(0)) + } +} + +func (s *Suite) Test_Pkglint_checkExecutable__error(c *check.C) { + t := s.Init(c) + filename := t.File("file.mk") G.checkExecutable(filename, 0555) @@ -1176,7 +1179,7 @@ func (s *Suite) Test_Pkglint_checkExecutable__already_committed(c *check.C) { t := s.Init(c) t.CreateFileLines("CVS/Entries", - "/file.mk/modified////") + "/file.mk//modified//") filename := t.File("file.mk") G.checkExecutable(filename, 0555) @@ -1185,7 +1188,7 @@ func (s *Suite) Test_Pkglint_checkExecutable__already_committed(c *check.C) { t.CheckOutputEmpty() } -func (s *Suite) Test_Main(c *check.C) { +func (s *Suite) Test_Pkglint_Main(c *check.C) { t := s.Init(c) out, err := os.Create(t.CreateFileLines("out")) @@ -1198,19 +1201,7 @@ func (s *Suite) Test_Main(c *check.C) { t.FinishSetUp() runMain := func(out *os.File, commandLine ...string) { - args := os.Args - stdout := os.Stdout - stderr := os.Stderr - defer func() { - os.Stderr = stderr - os.Stdout = stdout - os.Args = args - }() - os.Args = commandLine - os.Stdout = out - os.Stderr = out - - exitCode := Main() + exitCode := G.Main(out, out, commandLine) c.Check(exitCode, equals, 0) } @@ -1246,3 +1237,43 @@ func (s *Suite) Test_InterPackage_Bl3__same_identifier(c *check.C) { "ERROR: category/package2/buildlink3.mk:3: Duplicate package identifier " + "\"package1\" already appeared in ../../category/package1/buildlink3.mk:3.") } + +func (s *Suite) Test_Pkglint_loadCvsEntries(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("CVS/Entries", + "/invalid/", + "must be silently ignored", + "/name/revision/timestamp/options/tagdate") + + t.Check(isCommitted(t.File("name")), equals, true) + + t.CheckOutputLines( + "ERROR: ~/CVS/Entries:1: Invalid line: /invalid/") +} + +func (s *Suite) Test_Pkglint_loadCvsEntries__with_Entries_Log(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("CVS/Entries", + "/invalid/", + "must be silently ignored", + "/name//modified//", + "/removed//modified//") + + t.CreateFileLines("CVS/Entries.Log", + "A /invalid/", + "A /added//modified//", + "must be silently ignored", + "R /invalid/", + "R /removed//modified//") + + t.Check(isCommitted(t.File("name")), equals, true) + t.Check(isCommitted(t.File("added")), equals, true) + t.Check(isCommitted(t.File("removed")), equals, false) + + t.CheckOutputLines( + "ERROR: ~/CVS/Entries:1: Invalid line: /invalid/", + "ERROR: ~/CVS/Entries.Log:1: Invalid line: A /invalid/", + "ERROR: ~/CVS/Entries.Log:4: Invalid line: R /invalid/") +} diff --git a/pkgtools/pkglint/files/pkgsrc.go b/pkgtools/pkglint/files/pkgsrc.go index 680c52e9d76..d40ce28c361 100644 --- a/pkgtools/pkglint/files/pkgsrc.go +++ b/pkgtools/pkglint/files/pkgsrc.go @@ -33,10 +33,13 @@ type Pkgsrc struct { PkgOptions map[string]string // "x11" => "Provides X11 support" - suggestedUpdates []SuggestedUpdate // - suggestedWipUpdates []SuggestedUpdate // - LastChange map[string]*Change // - listVersions map[string][]string // See ListVersions + suggestedUpdates []SuggestedUpdate + suggestedWipUpdates []SuggestedUpdate + + LastChange map[string]*Change + FreezeStart string // e.g. "2018-01-01", or "" + + listVersions map[string][]string // See Pkgsrc.ListVersions // Variables that may be overridden by the pkgsrc user. // They are typically defined in mk/defaults/mk.conf. @@ -60,6 +63,7 @@ func NewPkgsrc(dir string) Pkgsrc { nil, nil, make(map[string]*Change), + "", make(map[string][]string), NewScope(), make(map[string]string), @@ -184,9 +188,9 @@ func (src *Pkgsrc) Latest(category string, re regex.Pattern, repl string) string // => {"php-53", "php-56", "php-73"} func (src *Pkgsrc) ListVersions(category string, re regex.Pattern, repl string, errorIfEmpty bool) []string { if G.Testing { - assertf( - hasPrefix(string(re), "^") && hasSuffix(string(re), "$"), - "Regular expression %q must be anchored at both ends.", re) + // Regular expression must be anchored at both ends, to avoid typos. + assert(hasPrefix(string(re), "^")) + assert(hasSuffix(string(re), "$")) } // TODO: Maybe convert cache key to a struct, to save allocations. @@ -243,7 +247,7 @@ func (src *Pkgsrc) ListVersions(category string, re regex.Pattern, repl string, return naturalLess(names[i], names[j]) }) - var repls = make([]string, len(names), len(names)) + var repls = make([]string, len(names)) for i, name := range names { repls[i] = replaceAll(name, re, repl) } @@ -262,9 +266,7 @@ func (src *Pkgsrc) checkToplevelUnusedLicenses() { licenseName := licenseFile.Name() if !G.InterPackage.LicenseUsed(licenseName) { licensePath := licensesDir + "/" + licenseName - if fileExists(licensePath) { - NewLineWhole(licensePath).Warnf("This license seems to be unused.") - } + NewLineWhole(licensePath).Warnf("This license seems to be unused.") } } } @@ -291,24 +293,15 @@ func (src *Pkgsrc) loadTools() { } // TODO: parse bsd.prefs.mk and bsd.pkg.mk instead of hardcoding this. - toolDefs := [...]struct { - Name string - Varname string - Validity Validity - }{ - {"echo", "ECHO", AfterPrefsMk}, - {"echo -n", "ECHO_N", AfterPrefsMk}, - {"false", "FALSE", AtRunTime}, // from bsd.pkg.mk - {"test", "TEST", AfterPrefsMk}, - {"true", "TRUE", AfterPrefsMk}} - - for _, toolDef := range toolDefs { - tools.def(toolDef.Name, toolDef.Varname, true, toolDef.Validity, nil) - } + tools.def("echo", "ECHO", true, AfterPrefsMk, nil) + tools.def("echo -n", "ECHO_N", true, AfterPrefsMk, nil) + tools.def("false", "FALSE", true, AtRunTime, nil) // from bsd.pkg.mk + tools.def("test", "TEST", true, AfterPrefsMk, nil) + tools.def("true", "TRUE", true, AfterPrefsMk, nil) for _, basename := range toolFiles { mklines := src.LoadMk("mk/tools/"+basename, MustSucceed|NotEmpty) - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { tools.ParseToolLine(mklines, mkline, true, !mklines.indentation.IsConditional()) }) } @@ -316,7 +309,7 @@ func (src *Pkgsrc) loadTools() { for _, relativeName := range [...]string{"mk/bsd.prefs.mk", "mk/bsd.pkg.mk"} { mklines := src.LoadMk(relativeName, MustSucceed|NotEmpty) - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { if mkline.IsVarassign() { varname := mkline.Varname() switch varname { @@ -349,7 +342,7 @@ func (src *Pkgsrc) loadUntypedVars() { // Setting guessed to false prevents the vartype.guessed case in MkLineChecker.CheckVaruse. unknownType := NewVartype(BtUnknown, NoVartypeOptions, NewACLEntry("*", aclpAll)) - define := func(varcanon string, mkline MkLine) { + define := func(varcanon string, mkline *MkLine) { switch { case src.vartypes.DefinedCanon(varcanon): // Already defined, can also be a tool. @@ -376,23 +369,21 @@ func (src *Pkgsrc) loadUntypedVars() { } handleMkFile := func(path string) { - mklines := LoadMk(path, 0) - if mklines != nil && len(mklines.mklines) > 0 { - mklines.collectDefinedVariables() - mklines.collectUsedVariables() - for varname, mkline := range mklines.vars.firstDef { - define(varnameCanon(varname), mkline) - } - for varname, mkline := range mklines.vars.used { - define(varnameCanon(varname), mkline) - } + mklines := LoadMk(path, MustSucceed) + mklines.collectDefinedVariables() + mklines.collectUsedVariables() + for varname, mkline := range mklines.vars.firstDef { + define(varnameCanon(varname), mkline) + } + for varname, mkline := range mklines.vars.used { + define(varnameCanon(varname), mkline) } } handleFile := func(pathName string, info os.FileInfo, err error) error { assertNil(err, "handleFile %q", pathName) baseName := info.Name() - if hasSuffix(baseName, ".mk") || baseName == "mk.conf" { + if info.Mode().IsRegular() && (hasSuffix(baseName, ".mk") || baseName == "mk.conf") { handleMkFile(filepath.ToSlash(pathName)) } return nil @@ -402,7 +393,7 @@ func (src *Pkgsrc) loadUntypedVars() { assertNil(err, "Walk error in pkgsrc infrastructure") } -func (src *Pkgsrc) parseSuggestedUpdates(lines Lines) []SuggestedUpdate { +func (src *Pkgsrc) parseSuggestedUpdates(lines *Lines) []SuggestedUpdate { if lines == nil { return nil } @@ -444,79 +435,77 @@ func (src *Pkgsrc) loadSuggestedUpdates() { src.suggestedWipUpdates = src.parseSuggestedUpdates(Load(src.File("wip/TODO"), NotEmpty)) } -func (src *Pkgsrc) loadDocChangesFromFile(filename string) []*Change { - - warn := !G.Wip - - parseChange := func(line Line) *Change { - lex := textproc.NewLexer(line.Text) - - space := lex.NextHspace() - if space == "" { - return nil - } - - if space != "\t" { - if warn { - line.Warnf("Package changes should be indented using a single tab, not %q.", space) - line.Explain( - "To avoid this formatting mistake in the future, just run", - sprintf("%q", bmake("cce")), - "after committing the update to the package.") - } +func (*Pkgsrc) parseDocChange(line *Line, warn bool) *Change { + lex := textproc.NewLexer(line.Text) - return nil - } + space := lex.NextHspace() + if space == "" { + return nil + } - f := strings.Fields(lex.Rest()) - n := len(f) - if n != 4 && n != 6 { - return nil + if space != "\t" { + if warn { + line.Warnf("Package changes should be indented using a single tab, not %q.", space) + line.Explain( + "To avoid this formatting mistake in the future, just run", + sprintf("%q", bmake("cce")), + "after committing the update to the package.") } - action, pkgpath, author, date := f[0], f[1], f[len(f)-2], f[len(f)-1] - if !hasPrefix(author, "[") || !hasSuffix(date, "]") { - return nil - } - author, date = author[1:], date[:len(date)-1] - - newChange := func(version string) *Change { - return &Change{ - Location: line.Location, - Action: intern(action), - Pkgpath: intern(pkgpath), - Version: intern(version), - Author: intern(author), - Date: intern(date), - } - } + return nil + } - switch { - case action == "Added" && f[2] == "version" && n == 6: - return newChange(f[3]) + f := strings.Fields(lex.Rest()) + n := len(f) + if n != 4 && n != 6 { + return nil + } - case (action == "Updated" || action == "Downgraded") && f[2] == "to" && n == 6: - return newChange(f[3]) + action := ParseChangeAction(f[0]) + pkgpath := f[1] + author := f[len(f)-2] + date := f[len(f)-1] - case action == "Removed" && (n == 6 && f[2] == "successor" || n == 4): - return newChange("") + if !hasPrefix(author, "[") || !hasSuffix(date, "]") { + return nil + } + author, date = author[1:], date[:len(date)-1] - case (action == "Renamed" || action == "Moved") && f[2] == "to" && n == 6: - return newChange("") + switch { + case + action == Added && f[2] == "version", + action == Updated && f[2] == "to", + action == Downgraded && f[2] == "to", + action == Removed && (f[2] == "successor" || n == 4), + (action == Renamed || action == Moved) && f[2] == "to": + return &Change{ + Location: line.Location, + Action: action, + Pkgpath: intern(pkgpath), + target: intern(ifelseStr(n == 6, f[3], "")), + Author: intern(author), + Date: intern(date), } + } + if warn { line.Warnf("Unknown doc/CHANGES line: %s", line.Text) line.Explain( "See mk/misc/developer.mk for the rules.") - - return nil } + return nil +} + +func (src *Pkgsrc) loadDocChangesFromFile(filename string) []*Change { + + warn := !G.Wip + // Each date in the file should be from the same year as the filename says. // This check has been added in 2018. // For years earlier than 2018 pkglint doesn't care because it's not a big issue anyway. year := "" - if m, yyyy := match1(filename, `-(\d+)$`); m && yyyy >= "2018" { + if _, yyyy := match1(filename, `-(\d\d\d\d)$`); yyyy >= "2018" { year = yyyy } @@ -527,6 +516,13 @@ func (src *Pkgsrc) loadDocChangesFromFile(filename string) []*Change { if hasPrefix(line.Text, "\tmk/") { infra = true + if hasPrefix(line.Text, "\tmk/bsd.pkg.mk: started freeze for") { + if m, freezeDate := match1(line.Text, `(\d\d\d\d-\d\d-\d\d)\]$`); m { + src.FreezeStart = freezeDate + } + } else if hasPrefix(line.Text, "\tmk/bsd.pkg.mk: freeze ended for") { + src.FreezeStart = "" + } } if infra { if hasSuffix(line.Text, "]") { @@ -535,7 +531,7 @@ func (src *Pkgsrc) loadDocChangesFromFile(filename string) []*Change { continue } - change := parseChange(line) + change := src.parseDocChange(line, warn) if change == nil { continue } @@ -546,14 +542,14 @@ func (src *Pkgsrc) loadDocChangesFromFile(filename string) []*Change { continue } - if year != "" && change.Date[0:4] != year { - line.Warnf("Year %s for %s does not match the filename %s.", + if year != "" && len(change.Date) >= 4 && change.Date[0:4] != year { + line.Warnf("Year %q for %s does not match the filename %s.", change.Date[0:4], change.Pkgpath, filename) } if len(changes) >= 2 && year != "" { if prev := changes[len(changes)-2]; change.Date < prev.Date { - line.Warnf("Date %s for %s is earlier than %s in %s.", + line.Warnf("Date %q for %s is earlier than %q in %s.", change.Date, change.Pkgpath, prev.Date, line.RefToLocation(prev.Location)) line.Explain( "The entries in doc/CHANGES should be in chronological order, and", @@ -597,12 +593,14 @@ func (src *Pkgsrc) loadDocChanges() { } } - sort.Strings(filenames) src.LastChange = make(map[string]*Change) for _, filename := range filenames { changes := src.loadDocChangesFromFile(docDir + "/" + filename) for _, change := range changes { src.LastChange[change.Pkgpath] = change + if change.Action == Renamed || change.Action == Moved { + src.LastChange[change.Target()] = change + } } } } @@ -782,17 +780,18 @@ func (src *Pkgsrc) initDeprecatedVars() { } // Load loads the file relative to the pkgsrc top directory. -func (src *Pkgsrc) Load(filename string, options LoadOptions) Lines { +func (src *Pkgsrc) Load(filename string, options LoadOptions) *Lines { return Load(src.File(filename), options) } // LoadMk loads the Makefile relative to the pkgsrc top directory. -func (src *Pkgsrc) LoadMk(filename string, options LoadOptions) MkLines { +func (src *Pkgsrc) LoadMk(filename string, options LoadOptions) *MkLines { return LoadMk(src.File(filename), options) } -// ReadDir reads the file listing from the given directory (relative to the pkgsrc root), -// filtering out any ignored files (CVS/*) and empty directories. +// ReadDir lists the files and subdirectories from the given directory +// (relative to the pkgsrc root), filtering out any ignored files (CVS/*) +// and empty directories. func (src *Pkgsrc) ReadDir(dirName string) []os.FileInfo { dir := src.File(dirName) files, err := ioutil.ReadDir(dir) @@ -901,7 +900,7 @@ func (src *Pkgsrc) loadPkgOptions() { // VariableType returns the type of the variable // (possibly guessed based on the variable name), // or nil if the type cannot even be guessed. -func (src *Pkgsrc) VariableType(mklines MkLines, varname string) (vartype *Vartype) { +func (src *Pkgsrc) VariableType(mklines *MkLines, varname string) (vartype *Vartype) { if trace.Tracing { defer trace.Call(varname, trace.Result(&vartype))() } @@ -976,6 +975,8 @@ func (src *Pkgsrc) guessVariableType(varname string) (vartype *Vartype) { return listType(BtCFlag, aclpAllRuntime) case hasSuffix(varname, "_LDFLAGS"): return listType(BtLdFlag, aclpAllRuntime) + case hasSuffix(varname, "FLAGS"): + return listType(BtShellWord, aclpAll) case hasSuffix(varbase, "_MK"): // TODO: Add BtGuard for inclusion guards, since these variables may only be checked using defined(). return plainType(BtUnknown, aclpAll) @@ -1005,13 +1006,64 @@ func (src *Pkgsrc) guessVariableType(varname string) (vartype *Vartype) { // Change describes a modification to a single package, from the doc/CHANGES-* files. type Change struct { Location Location - Action string - Pkgpath string - Version string + Action ChangeAction // Added, Updated, Downgraded, Renamed, Moved, Removed + Pkgpath string // For renamed or moved packages, the previous PKGPATH + target string Author string Date string } +// Version returns the version number for an Added, Updated or Downgraded package. +func (ch *Change) Version() string { + assert(ch.Action == Added || ch.Action == Updated || ch.Action == Downgraded) + return ch.target +} + +// Target returns the target PKGPATH for a Renamed or Moved package. +func (ch *Change) Target() string { + assert(ch.Action == Renamed || ch.Action == Moved) + return ch.target +} + +// Successor returns the successor for a Removed package. +func (ch *Change) Successor() string { + assert(ch.Action == Removed) + return ch.target +} + +type ChangeAction uint8 + +const ( + Added ChangeAction = 1 + iota + Updated + Downgraded + Renamed + Moved + Removed +) + +func ParseChangeAction(s string) ChangeAction { + switch s { + case "Added": + return Added + case "Updated": + return Updated + case "Downgraded": + return Downgraded + case "Renamed": + return Renamed + case "Moved": + return Moved + case "Removed": + return Removed + } + return 0 +} + +func (ca ChangeAction) String() string { + return [...]string{"", "Added", "Updated", "Downgraded", "Renamed", "Moved", "Removed"}[ca] +} + // SuggestedUpdate describes a desired package update, from the doc/TODO file. type SuggestedUpdate struct { Line Location diff --git a/pkgtools/pkglint/files/pkgsrc_test.go b/pkgtools/pkglint/files/pkgsrc_test.go index e61c672de77..a57fcc7d0c0 100644 --- a/pkgtools/pkglint/files/pkgsrc_test.go +++ b/pkgtools/pkglint/files/pkgsrc_test.go @@ -10,12 +10,17 @@ func (s *Suite) Test_Pkgsrc_loadMasterSites(c *check.C) { t := s.Init(c) t.CreateFileLines("mk/fetch/sites.mk", - MkRcsID, + MkCvsID, "", "MASTER_SITE_A+= https://example.org/distfiles/", "MASTER_SITE_B+= https://b.example.org/distfiles/ \\", " https://b2.example.org/distfiles/", - "MASTER_SITE_A+= https://a.example.org/distfiles/") + "MASTER_SITE_A+= https://a.example.org/distfiles/ ${other}", + "", + "MASTER_SITE_BACKUP+=\t", + "\thttps://backup.example.org/", + "", + "OTHER_VARIABLE=\tyes # only for code coverage") G.Pkgsrc.loadMasterSites() @@ -25,6 +30,11 @@ func (s *Suite) Test_Pkgsrc_loadMasterSites(c *check.C) { c.Check(G.Pkgsrc.MasterSiteURLToVar["https://a.example.org/distfiles/"], equals, "MASTER_SITE_A") c.Check(G.Pkgsrc.MasterSiteVarToURL["MASTER_SITE_A"], equals, "https://example.org/distfiles/") c.Check(G.Pkgsrc.MasterSiteVarToURL["MASTER_SITE_B"], equals, "https://b.example.org/distfiles/") + + // Ignored entries: + c.Check(G.Pkgsrc.MasterSiteURLToVar["${other}"], equals, "") + c.Check(G.Pkgsrc.MasterSiteURLToVar["https://backup.example.org/"], equals, "") + c.Check(G.Pkgsrc.MasterSiteVarToURL["MASTER_SITE_BACKUP"], equals, "") } func (s *Suite) Test_Pkgsrc_parseSuggestedUpdates(c *check.C) { @@ -57,26 +67,30 @@ func (s *Suite) Test_Pkgsrc_checkToplevelUnusedLicenses(c *check.C) { t.CreateFileLines("licenses/gnu-gpl-v3") t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "SUBDIR+=\tcategory") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "COMMENT=\tExample category", "", "SUBDIR+=\tpackage", + "SUBDIR+=\tpackage2", "", ".include \"../mk/misc/category.mk\"") t.SetUpPackage("category/package", "LICENSE=\t2-clause-bsd") + t.SetUpPackage("category/package2", + "LICENSE=\tmissing") t.Main("-r", "-Cglobal", t.File(".")) t.CheckOutputLines( + "WARN: ~/category/package2/Makefile:11: License file ~/licenses/missing does not exist.", "WARN: ~/licenses/gnu-gpl-v2: This license seems to be unused.", // Added by Tester.SetUpPkgsrc "WARN: ~/licenses/gnu-gpl-v3: This license seems to be unused.", - "0 errors and 2 warnings found.") + "0 errors and 3 warnings found.") } func (s *Suite) Test_Pkgsrc_loadUntypedVars(c *check.C) { @@ -85,7 +99,7 @@ func (s *Suite) Test_Pkgsrc_loadUntypedVars(c *check.C) { t.SetUpPkgsrc() t.SetUpTool("echo", "ECHO", AtRunTime) t.CreateFileLines("mk/infra.mk", - MkRcsID, + MkCvsID, "#", "# System-provided variables:", "#", @@ -109,7 +123,7 @@ func (s *Suite) Test_Pkgsrc_loadUntypedVars(c *check.C) { t.FinishSetUp() mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "", "do-build:", "\t: ${INFRA_MK} ${UNTYPED.three} ${ECHO}", @@ -125,12 +139,25 @@ func (s *Suite) Test_Pkgsrc_loadUntypedVars(c *check.C) { "WARN: filename.mk:6: INDIRECT_param is used but not defined.") } +func (s *Suite) Test_Pkgsrc_loadUntypedVars__badly_named_directory(c *check.C) { + t := s.Init(c) + + t.SetUpPkgsrc() + t.CreateFileLines("mk/subdir.mk/file.mk", + MkCvsID) + t.FinishSetUp() + + // Even when a directory is named *.mk, pkglint doesn't crash. + t.CheckOutputEmpty() +} + func (s *Suite) Test_Pkgsrc_loadTools(c *check.C) { t := s.Init(c) t.CreateFileLines("mk/tools/bsd.tools.mk", ".include \"flex.mk\"", ".include \"gettext.mk\"", + ".include \"../nonexistent.mk\"", // Is skipped because of the slash. ".include \"strip.mk\"", ".include \"replace.mk\"") t.CreateFileLines("mk/tools/defaults.mk", @@ -197,10 +224,10 @@ func (s *Suite) Test_Pkgsrc_loadTools__BUILD_DEFS(c *check.C) { "pre-configure:", "\t@${ECHO} ${PKG_SYSCONFDIR} ${VARBASE}") t.CreateFileLines("mk/bsd.pkg.mk", - MkRcsID, + MkCvsID, "_BUILD_DEFS+=\tPKG_SYSCONFBASEDIR PKG_SYSCONFDIR") t.CreateFileLines("mk/defaults/mk.conf", - MkRcsID, + MkCvsID, "", "VARBASE=\t\t/var/pkg", "PKG_SYSCONFBASEDIR=\t/usr/pkg/etc", @@ -217,6 +244,21 @@ func (s *Suite) Test_Pkgsrc_loadTools__BUILD_DEFS(c *check.C) { "The user-defined variable VARBASE is used but not added to BUILD_DEFS.") } +func (s *Suite) Test_Pkgsrc_loadDocChanges(c *check.C) { + t := s.Init(c) + + t.SetUpPkgsrc() + t.CreateFileLines("doc/CHANGES-2018", + CvsID, + "", + "\tUpdated pkgpath to 1.0 [author 2018-01-01]", + "\tRenamed pkgpath to new-pkg [author 2018-02-01]", + "\tMoved pkgpath to category/new-pkg [author 2018-03-01]") + t.FinishSetUp() + + t.Check(G.Pkgsrc.LastChange["pkgpath"].Action, equals, Moved) +} + func (s *Suite) Test_Pkgsrc_loadDocChanges__not_found(c *check.C) { t := s.Init(c) @@ -241,6 +283,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile(c *check.C) { "\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]", + "\tReworked category/package to 1.2 [author8 2018-01-08]", "", "\ttoo few fields", "\ttoo many many many many many fields", @@ -251,24 +294,25 @@ func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile(c *check.C) { c.Assert(len(changes), equals, 7) c.Check(*changes[0], equals, Change{changes[0].Location, - "Added", "category/package", "1.0", "author1", "2015-01-01"}) + Added, "category/package", "1.0", "author1", "2015-01-01"}) c.Check(*changes[1], equals, Change{changes[1].Location, - "Updated", "category/package", "1.5", "author2", "2018-01-02"}) + Updated, "category/package", "1.5", "author2", "2018-01-02"}) c.Check(*changes[2], equals, Change{changes[2].Location, - "Renamed", "category/package", "", "author3", "2018-01-03"}) + Renamed, "category/package", "category/pkg", "author3", "2018-01-03"}) c.Check(*changes[3], equals, Change{changes[3].Location, - "Moved", "category/package", "", "author4", "2018-01-04"}) + Moved, "category/package", "other/package", "author4", "2018-01-04"}) c.Check(*changes[4], equals, Change{changes[4].Location, - "Removed", "category/package", "", "author5", "2018-01-09"}) + Removed, "category/package", "", "author5", "2018-01-09"}) c.Check(*changes[5], equals, Change{changes[5].Location, - "Removed", "category/package", "", "author6", "2018-01-06"}) + Removed, "category/package", "category/package2", "author6", "2018-01-06"}) c.Check(*changes[6], equals, Change{changes[6].Location, - "Downgraded", "category/package", "1.2", "author7", "2018-01-07"}) + Downgraded, "category/package", "1.2", "author7", "2018-01-07"}) t.CheckOutputLines( - "WARN: ~/doc/CHANGES-2018:1: Year 2015 for category/package does not match the filename ~/doc/CHANGES-2018.", - "WARN: ~/doc/CHANGES-2018:6: Date 2018-01-06 for category/package is earlier than 2018-01-09 in line 5.", - "WARN: ~/doc/CHANGES-2018:12: Unknown doc/CHANGES line: \tAdded another [new package]") + "WARN: ~/doc/CHANGES-2018:1: Year \"2015\" for category/package does not match the filename ~/doc/CHANGES-2018.", + "WARN: ~/doc/CHANGES-2018:6: Date \"2018-01-06\" for category/package is earlier than \"2018-01-09\" in line 5.", + "WARN: ~/doc/CHANGES-2018:8: Unknown doc/CHANGES line: \tReworked category/package to 1.2 [author8 2018-01-08]", + "WARN: ~/doc/CHANGES-2018:13: Unknown doc/CHANGES line: \tAdded another [new package]") } func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__not_found(c *check.C) { @@ -287,12 +331,14 @@ func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__wip_suppresses_warnings(c *c t.SetUpPackage("wip/package") t.CreateFileLines("doc/CHANGES-2018", - RcsID, + CvsID, "", "Changes to the packages collection and infrastructure in 2018:", "", "\tUpdated sysutils/checkperms to 1.10 [rillig 2018-01-05]", - "\tUpdated sysutils/checkperms to 1.11 [rillig 2018-01-01]") + "\tUpdated sysutils/checkperms to 1.11 [rillig 2018-01-01]", + "\t\tWrong indentation", + "\tInvalid pkgpath to 1.16 [rillig 2019-06-16]") t.Main(t.File("wip/package")) @@ -305,7 +351,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__wrong_indentation(c *check.C t.SetUpPackage("category/package") t.CreateFileLines("doc/CHANGES-2018", - RcsID, + CvsID, "", "Changes to the packages collection and infrastructure in 2018:", "", @@ -328,7 +374,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__infrastructure(c *check.C) { t.SetUpPackage("category/package") t.CreateFileLines("doc/CHANGES-2018", - RcsID, + CvsID, "", "Changes to the packages collection and infrastructure in 2018:", "", @@ -345,13 +391,135 @@ func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__infrastructure(c *check.C) { "Looks fine.") } +func (s *Suite) Test_Pkgsrc_parseDocChange(c *check.C) { + t := s.Init(c) + + test := func(text string, diagnostics ...string) { + line := t.NewLine("doc/CHANGES-2019", 123, text) + _ = (*Pkgsrc)(nil).parseDocChange(line, true) + t.CheckOutput(diagnostics) + } + + test(CvsID, + nil...) + test("", + nil...) + test("Changes to the packages collection and infrastructure in 2019:", + nil...) + + test("\tAdded something [author date]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \tAdded something [author date]") + + test("\t\tToo large indentation", + "WARN: doc/CHANGES-2019:123: Package changes should be indented using a single tab, not \"\\t\\t\".") + test("\t Too large indentation", + "WARN: doc/CHANGES-2019:123: Package changes should be indented using a single tab, not \"\\t \".") + + // TODO: Add a warning here, since it's easy to forget a bracket. + test("\t1 2 3 4", + nil...) + test("\t1 2 3 4 5", + nil...) + test("\t1 2 3 4 5 6", + nil...) + test("\t1 2 3 4 5 6 7", + nil...) + test("\t1 2 [3 4", + nil...) + test("\t1 2 [3 4]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \t1 2 [3 4]") + test("\tAdded 2 [3 4]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \tAdded 2 [3 4]") + + test("\tAdded pkgpath version 1.0 [author date]", + nil...) + // "to" is wrong + test("\tAdded pkgpath to 1.0 [author date]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \tAdded pkgpath to 1.0 [author date]") + + test("\tUpdated pkgpath to 1.0 [author date]", + nil...) + // "from" is wrong + test("\tUpdated pkgpath from 1.0 [author date]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \tUpdated pkgpath from 1.0 [author date]") + + test("\tDowngraded pkgpath to 1.0 [author date]", + nil...) + // "from" is wrong + test("\tDowngraded pkgpath from 1.0 [author date]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \tDowngraded pkgpath from 1.0 [author date]") + + test("\tRemoved pkgpath [author date]", + nil...) + test("\tRemoved pkgpath successor pkgpath [author date]", + nil...) + // "and" is wrong + test("\tRemoved pkgpath and pkgpath [author date]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \tRemoved pkgpath and pkgpath [author date]") + + test("\tRenamed pkgpath to other [author date]", + nil...) + // "from" is wrong + test("\tRenamed pkgpath from previous [author date]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \tRenamed pkgpath from previous [author date]") + + test("\tMoved pkgpath to other [author date]", + nil...) + // "from" is wrong + test("\tMoved pkgpath from previous [author date]", + "WARN: doc/CHANGES-2019:123: Unknown doc/CHANGES line: \tMoved pkgpath from previous [author date]") + + // "Split" is wrong + // TODO: Add a warning since this is probably a typo. + test("\tSplit pkgpath into a and b [author date]", + nil...) +} + +func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__old(c *check.C) { + t := s.Init(c) + + t.SetUpPkgsrc() + t.CreateFileLines("doc/CHANGES-2010", + CvsID, + "", + "Changes to the packages collection and infrastructure in 2015:", + "", + "\tInvalid line [3 4]") + t.CreateFileLines("doc/CHANGES-2015", + CvsID, + "", + "Changes to the packages collection and infrastructure in 2015:", + "", + "\tUpdated pkgpath to 1.0 [author 2015-07-01]", + "\tInvalid line [3 4]", + // The date of the below entry is earlier than that of the above entry; + // this error is ignored because the 2015 file is too old. + "\tUpdated pkgpath to 1.2 [author 2015-02-01]") + t.CreateFileLines("doc/CHANGES-2018", + CvsID, + "", + "Changes to the packages collection and infrastructure in 2018:", + "", + "\tUpdated pkgpath to 1.0 [author date]", + "\tUpdated pkgpath to 1.0 [author d]") + t.FinishSetUp() + + // The 2010 file is so old that it is skipped completely. + // The 2015 file is so old that the date is not checked. + // Since 2018, each date in the file must match the filename. + t.CheckOutputLines( + "WARN: ~/doc/CHANGES-2015:6: Unknown doc/CHANGES line: \tInvalid line [3 4]", + "WARN: ~/doc/CHANGES-2018:5: Year \"date\" for pkgpath does not match the filename ~/doc/CHANGES-2018.", + "WARN: ~/doc/CHANGES-2018:6: Date \"d\" for pkgpath is earlier than \"date\" in line 5.") +} + func (s *Suite) Test_Pkgsrc_parseSuggestedUpdates__wip(c *check.C) { t := s.Init(c) pkg := t.SetUpPackage("wip/package", "DISTNAME=\tpackage-1.11") t.CreateFileLines("wip/TODO", - RcsID, + CvsID, "", "Suggested package updates", "", @@ -372,7 +540,7 @@ func (s *Suite) Test_Pkgsrc__deprecated(c *check.C) { t.SetUpVartypes() G.Pkgsrc.initDeprecatedVars() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "USE_PERL5=\t\tyes", "SUBST_POSTCMD.class=\t${ECHO}", "CPPFLAGS+=\t\t${BUILDLINK_CPPFLAGS.${PKG_JVM}}") @@ -576,9 +744,13 @@ func (s *Suite) Test_Pkgsrc_ListVersions__go(c *check.C) { func (s *Suite) Test_Pkgsrc_ListVersions__invalid_argument(c *check.C) { t := s.Init(c) - t.ExpectPanic( - func() { G.Pkgsrc.ListVersions("databases", `postgresql[0-9]+`, "$0", true) }, - "Pkglint internal error: Regular expression \"postgresql[0-9]+\" must be anchored at both ends.") + t.ExpectAssert(func() { G.Pkgsrc.ListVersions("databases", `postgresql[0-9]+`, "$0", true) }) + t.ExpectAssert(func() { G.Pkgsrc.ListVersions("databases", `^postgresql[0-9]+`, "$0", true) }) + + G.Testing = false + versions := G.Pkgsrc.ListVersions("databases", `^postgresql[0-9]+`, "$0", false) + + t.Check(versions, check.HasLen, 0) } func (s *Suite) Test_Pkgsrc_loadPkgOptions(c *check.C) { @@ -612,7 +784,7 @@ func (s *Suite) Test_Pkgsrc_loadTools__no_tools_found(c *check.C) { "FATAL: ~/mk/tools/bsd.tools.mk: Must not be empty.") t.CreateFileLines("mk/tools/bsd.tools.mk", - MkRcsID) + MkCvsID) t.ExpectFatal( G.Pkgsrc.loadTools, @@ -624,6 +796,7 @@ func (s *Suite) Test_Pkgsrc_VariableType(c *check.C) { t := s.Init(c) t.SetUpVartypes() + t.SetUpTool("echo", "ECHO", AtRunTime) test := func(varname string, vartype string) { actualType := G.Pkgsrc.VariableType(nil, varname) @@ -647,6 +820,8 @@ func (s *Suite) Test_Pkgsrc_VariableType(c *check.C) { test("MY_CMD_CFLAGS", "CFlag (list, guessed)") test("MY_CMD_LDFLAGS", "LdFlag (list, guessed)") test("PLIST.abcde", "Yes (package-settable)") + test("TOOLS_ECHO", "Pathname") + test("TOOLS_UNKNOWN", "") } // Guessing the variable type works for both plain and parameterized variable names. @@ -678,7 +853,7 @@ func (s *Suite) Test_Pkgsrc_VariableType__from_mk(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("mk/sys-vars.mk", - MkRcsID, + MkCvsID, "", "PKGSRC_MAKE_ENV?=\t# none", "CPPPATH?=\tcpp", @@ -717,7 +892,7 @@ func (s *Suite) Test_Pkgsrc_guessVariableType__SKIP(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "MY_CHECK_SKIP=\t*.c \"bad*pathname\"", "MY_CHECK_SKIP+=\t*.cpp", ".if ${MY_CHECK_SKIP}", @@ -741,3 +916,106 @@ func (s *Suite) Test_Pkgsrc_guessVariableType__SKIP(c *check.C) { "WARN: filename.mk:2: The pathname pattern \"\\\"bad*pathname\\\"\" " + "contains the invalid characters \"\\\"\\\"\".") } + +func (s *Suite) Test_Pkgsrc__frozen(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.CreateFileLines("doc/CHANGES-2018", + "\tmk/bsd.pkg.mk: started freeze for pkgsrc-2018Q2 branch [freezer 2018-03-25]") + t.FinishSetUp() + + t.Check(G.Pkgsrc.FreezeStart, equals, "2018-03-25") +} + +func (s *Suite) Test_Pkgsrc__not_frozen(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.CreateFileLines("doc/CHANGES-2018", + "\tmk/bsd.pkg.mk: started freeze for pkgsrc-2018Q2 branch [freezer 2018-03-25]", + "\tmk/bsd.pkg.mk: freeze ended for pkgsrc-2018Q2 branch [freezer 2018-03-27]") + t.FinishSetUp() + + t.Check(G.Pkgsrc.FreezeStart, equals, "") +} + +func (s *Suite) Test_Pkgsrc__frozen_with_typo(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.CreateFileLines("doc/CHANGES-2018", + // The closing bracket is missing. + "\tmk/bsd.pkg.mk: started freeze for pkgsrc-2018Q2 branch [freezer 2018-03-25") + t.FinishSetUp() + + t.Check(G.Pkgsrc.FreezeStart, equals, "") +} + +func (s *Suite) Test_Change_Version(c *check.C) { + t := s.Init(c) + + loc := Location{"doc/CHANGES-2019", 5, 5} + added := Change{loc, Added, "category/path", "1.0", "author", "2019-01-01"} + updated := Change{loc, Updated, "category/path", "1.0", "author", "2019-01-01"} + downgraded := Change{loc, Downgraded, "category/path", "1.0", "author", "2019-01-01"} + removed := Change{loc, Removed, "category/path", "1.0", "author", "2019-01-01"} + + t.Check(added.Version(), equals, "1.0") + t.Check(updated.Version(), equals, "1.0") + t.Check(downgraded.Version(), equals, "1.0") + t.ExpectAssert(func() { removed.Version() }) +} + +func (s *Suite) Test_Change_Target(c *check.C) { + t := s.Init(c) + + loc := Location{"doc/CHANGES-2019", 5, 5} + renamed := Change{loc, Renamed, "category/path", "category/other", "author", "2019-01-01"} + moved := Change{loc, Moved, "category/path", "category/other", "author", "2019-01-01"} + downgraded := Change{loc, Downgraded, "category/path", "1.0", "author", "2019-01-01"} + + t.Check(renamed.Target(), equals, "category/other") + t.Check(moved.Target(), equals, "category/other") + t.ExpectAssert(func() { downgraded.Target() }) +} + +func (s *Suite) Test_Change_Successor(c *check.C) { + t := s.Init(c) + + loc := Location{"doc/CHANGES-2019", 5, 5} + removed := Change{loc, Removed, "category/path", "", "author", "2019-01-01"} + removedSucc := Change{loc, Removed, "category/path", "category/successor", "author", "2019-01-01"} + downgraded := Change{loc, Downgraded, "category/path", "1.0", "author", "2019-01-01"} + + t.Check(removed.Successor(), equals, "") + t.Check(removedSucc.Successor(), equals, "category/successor") + t.ExpectAssert(func() { downgraded.Successor() }) +} + +func (s *Suite) Test_ChangeAction_String(c *check.C) { + t := s.Init(c) + + t.Check(Added.String(), equals, "Added") + t.Check(Removed.String(), equals, "Removed") +} + +func (s *Suite) Test_Pkgsrc_ReadDir(c *check.C) { + t := s.Init(c) + + t.CreateFileLines("dir/aaa-subdir/file") + t.CreateFileLines("dir/subdir/file") + t.CreateFileLines("dir/file") + t.CreateFileLines("dir/.git/file") + t.CreateFileLines("dir/CVS/Entries") + t.CreateFileLines("dir/empty/empty/empty/empty/CVS/Entries") + + infos := G.Pkgsrc.ReadDir("dir") + + var names []string + for _, info := range infos { + names = append(names, info.Name()) + } + + t.Check(names, deepEquals, []string{"aaa-subdir", "file", "subdir"}) +} diff --git a/pkgtools/pkglint/files/plist.go b/pkgtools/pkglint/files/plist.go index 09c17fc2737..da941944e0d 100644 --- a/pkgtools/pkglint/files/plist.go +++ b/pkgtools/pkglint/files/plist.go @@ -7,14 +7,14 @@ import ( "strings" ) -func CheckLinesPlist(pkg *Package, lines Lines) { +func CheckLinesPlist(pkg *Package, lines *Lines) { if trace.Tracing { - defer trace.Call1(lines.FileName)() + defer trace.Call1(lines.Filename)() } - lines.CheckRcsID(0, `@comment `, "@comment ") + idOk := lines.CheckCvsID(0, `@comment `, "@comment ") - if lines.Len() == 1 { + if idOk && lines.Len() == 1 { line := lines.Lines[0] line.Warnf("PLIST files shouldn't be empty.") line.Explain( @@ -26,6 +26,7 @@ func CheckLinesPlist(pkg *Package, lines Lines) { "", "Meta packages also don't need a PLIST file", "since their only purpose is to declare dependencies.") + return } ck := PlistChecker{ @@ -48,12 +49,12 @@ type PlistChecker struct { } type PlistLine struct { - Line + *Line conditions []string // e.g. PLIST.docs text string // Line.Text without any conditions of the form ${PLIST.cond} } -func (ck *PlistChecker) Check(plainLines Lines) { +func (ck *PlistChecker) Check(plainLines *Lines) { plines := ck.NewLines(plainLines) ck.collectFilesAndDirs(plines) @@ -77,7 +78,7 @@ func (ck *PlistChecker) Check(plainLines Lines) { } } -func (ck *PlistChecker) NewLines(lines Lines) []*PlistLine { +func (ck *PlistChecker) NewLines(lines *Lines) []*PlistLine { plines := make([]*PlistLine, lines.Len()) for i, line := range lines.Lines { var conditions []string @@ -306,28 +307,14 @@ func (ck *PlistChecker) checkPathInfo(pline *PlistLine, dirname, basename string } func (ck *PlistChecker) checkPathLib(pline *PlistLine, dirname, basename string) { - pkg := ck.pkg switch { - case pkg != nil && pkg.EffectivePkgbase != "" && hasPrefix(pline.text, "lib/"+pkg.EffectivePkgbase+"/"): - return - - case pline.text == "lib/charset.alias" && (pkg == nil || pkg.Pkgpath != "converters/libiconv"): - pline.Errorf("Only the libiconv package may install lib/charset.alias.") - return case hasPrefix(pline.text, "lib/locale/"): pline.Errorf("\"lib/locale\" must not be listed. Use ${PKGLOCALEDIR}/locale and set USE_PKGLOCALEDIR instead.") return } - switch ext := path.Ext(basename); ext { - case ".la": - if pkg != nil && !pkg.vars.Defined("USE_LIBTOOL") && ck.once.FirstTime("USE_LIBTOOL") { - pline.Warnf("Packages that install libtool libraries should define USE_LIBTOOL.") - } - } - if contains(basename, ".a") || contains(basename, ".so") { if m, noext := match1(pline.text, `^(.*)(?:\.a|\.so[0-9.]*)$`); m { if laLine := ck.allFiles[noext+".la"]; laLine != nil { @@ -335,6 +322,21 @@ func (ck *PlistChecker) checkPathLib(pline *PlistLine, dirname, basename string) } } } + + pkg := ck.pkg + if pkg == nil { + return + } + + if pline.text == "lib/charset.alias" && pkg.Pkgpath != "converters/libiconv" { + pline.Errorf("Only the libiconv package may install lib/charset.alias.") + } + + if hasSuffix(basename, ".la") && !pkg.vars.Defined("USE_LIBTOOL") { + if ck.once.FirstTime("USE_LIBTOOL") { + pline.Warnf("Packages that install libtool libraries should define USE_LIBTOOL.") + } + } } func (ck *PlistChecker) checkPathMan(pline *PlistLine) { @@ -380,43 +382,12 @@ func (ck *PlistChecker) checkPathShare(pline *PlistLine) { text := pline.text switch { - case hasPrefix(text, "share/icons/") && pkg != nil: - if hasPrefix(text, "share/icons/hicolor/") && pkg.Pkgpath != "graphics/hicolor-icon-theme" { - f := "../../graphics/hicolor-icon-theme/buildlink3.mk" - if !pkg.included.Seen(f) && ck.once.FirstTime("hicolor-icon-theme") { - pline.Errorf("Packages that install hicolor icons must include %q in the Makefile.", f) - } - } - - if text == "share/icons/hicolor/icon-theme.cache" && pkg.Pkgpath != "graphics/hicolor-icon-theme" { - pline.Errorf("The file icon-theme.cache must not appear in any PLIST file.") - pline.Explain( - "Remove this line and add the following line to the package Makefile.", - "", - ".include \"../../graphics/hicolor-icon-theme/buildlink3.mk\"") - } - - if hasPrefix(text, "share/icons/gnome") && pkg.Pkgpath != "graphics/gnome-icon-theme" { - f := "../../graphics/gnome-icon-theme/buildlink3.mk" - if !pkg.included.Seen(f) { - pline.Errorf("The package Makefile must include %q.", f) - pline.Explain( - "Packages that install GNOME icons must maintain the icon theme", - "cache.") - } - } - - if contains(text[12:], "/") && !pkg.vars.Defined("ICON_THEMES") && ck.once.FirstTime("ICON_THEMES") { - pline.Warnf("Packages that install icon theme files should set ICON_THEMES.") - } + case pkg != nil && hasPrefix(text, "share/icons/"): + ck.checkPathShareIcons(pline) case hasPrefix(text, "share/doc/html/"): pline.Warnf("Use of \"share/doc/html\" is deprecated. Use \"share/doc/${PKGBASE}\" instead.") - case pkg != nil && pkg.EffectivePkgbase != "" && (hasPrefix(text, "share/doc/"+pkg.EffectivePkgbase+"/") || - hasPrefix(text, "share/examples/"+pkg.EffectivePkgbase+"/")): - // Fine. - case hasPrefix(text, "share/info/"): pline.Warnf("Info pages should be installed into info/, not share/info/.") pline.Explain( @@ -427,6 +398,40 @@ func (ck *PlistChecker) checkPathShare(pline *PlistLine) { } } +func (ck *PlistChecker) checkPathShareIcons(pline *PlistLine) { + pkg := ck.pkg + text := pline.text + + if hasPrefix(text, "share/icons/hicolor/") && pkg.Pkgpath != "graphics/hicolor-icon-theme" { + f := "../../graphics/hicolor-icon-theme/buildlink3.mk" + if !pkg.included.Seen(f) && ck.once.FirstTime("hicolor-icon-theme") { + pline.Errorf("Packages that install hicolor icons must include %q in the Makefile.", f) + } + } + + if text == "share/icons/hicolor/icon-theme.cache" && pkg.Pkgpath != "graphics/hicolor-icon-theme" { + pline.Errorf("The file icon-theme.cache must not appear in any PLIST file.") + pline.Explain( + "Remove this line and add the following line to the package Makefile.", + "", + ".include \"../../graphics/hicolor-icon-theme/buildlink3.mk\"") + } + + if hasPrefix(text, "share/icons/gnome") && pkg.Pkgpath != "graphics/gnome-icon-theme" { + f := "../../graphics/gnome-icon-theme/buildlink3.mk" + if !pkg.included.Seen(f) { + pline.Errorf("The package Makefile must include %q.", f) + pline.Explain( + "Packages that install GNOME icons must maintain the icon theme", + "cache.") + } + } + + if contains(text[12:], "/") && !pkg.vars.Defined("ICON_THEMES") && ck.once.FirstTime("ICON_THEMES") { + pline.Warnf("Packages that install icon theme files should set ICON_THEMES.") + } +} + func (pline *PlistLine) CheckTrailingWhitespace() { if hasSuffix(pline.text, " ") || hasSuffix(pline.text, "\t") { pline.Errorf("Pkgsrc does not support filenames ending in whitespace.") @@ -499,7 +504,7 @@ type plistLineSorter struct { header []*PlistLine // Does not take part in sorting middle []*PlistLine // Only this part is sorted footer []*PlistLine // Does not take part in sorting, typically contains @exec or @pkgdir - unsortable Line // Some lines are so difficult to sort that only humans can do that + unsortable *Line // Some lines are so difficult to sort that only humans can do that changed bool // Whether the sorting actually changed something autofixed bool // Whether the newly sorted file has been written to disk } @@ -518,7 +523,7 @@ func NewPlistLineSorter(plines []*PlistLine) *plistLineSorter { header := plines[0:headerEnd] middle := plines[headerEnd:footerStart] footer := plines[footerStart:] - var unsortable Line + var unsortable *Line for _, pline := range middle { if unsortable == nil && (hasPrefix(pline.text, "@") || contains(pline.text, "$")) { @@ -564,7 +569,7 @@ func (s *plistLineSorter) Sort() { fix.Describef(int(firstLine.firstLine), "Sorting the whole file.") fix.Apply() - var lines []Line + var lines []*Line for _, pline := range s.header { lines = append(lines, pline.Line) } diff --git a/pkgtools/pkglint/files/plist_test.go b/pkgtools/pkglint/files/plist_test.go index 7340f3e038f..9e68c6b23b7 100644 --- a/pkgtools/pkglint/files/plist_test.go +++ b/pkgtools/pkglint/files/plist_test.go @@ -48,6 +48,18 @@ func (s *Suite) Test_CheckLinesPlist(c *check.C) { "ERROR: PLIST:18: Duplicate filename \"share/tzinfo\", already appeared in line 17.") } +func (s *Suite) Test_CheckLinesPlist__single_file_no_comment(c *check.C) { + t := s.Init(c) + + lines := t.NewLines("PLIST", + "bin/program") + + CheckLinesPlist(nil, lines) + + t.CheckOutputLines( + "ERROR: PLIST:1: Expected \"" + PlistCvsID + "\".") +} + // When a PLIST contains multiple libtool libraries, USE_LIBTOOL needs only // be defined once in the package Makefile. Therefore, a single warning is enough. func (s *Suite) Test_CheckLinesPlist__multiple_libtool_libraries(c *check.C) { @@ -55,7 +67,7 @@ func (s *Suite) Test_CheckLinesPlist__multiple_libtool_libraries(c *check.C) { G.Pkg = NewPackage(t.File("category/pkgbase")) lines := t.NewLines("PLIST", - PlistRcsID, + PlistCvsID, "lib/libc.la", "lib/libm.la") @@ -69,7 +81,7 @@ func (s *Suite) Test_CheckLinesPlist__empty(c *check.C) { t := s.Init(c) lines := t.NewLines("PLIST", - PlistRcsID) + PlistCvsID) CheckLinesPlist(nil, lines) @@ -81,10 +93,22 @@ func (s *Suite) Test_CheckLinesPlist__common_end(c *check.C) { t := s.Init(c) t.CreateFileLines("PLIST.common", - PlistRcsID, + PlistCvsID, "bin/common") lines := t.SetUpFileLines("PLIST.common_end", - PlistRcsID, + PlistCvsID, + "sbin/common_end") + + CheckLinesPlist(nil, lines) + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_CheckLinesPlist__common_end_without_common(c *check.C) { + t := s.Init(c) + + lines := t.SetUpFileLines("PLIST.common_end", + PlistCvsID, "sbin/common_end") CheckLinesPlist(nil, lines) @@ -97,7 +121,7 @@ func (s *Suite) Test_CheckLinesPlist__condition(c *check.C) { G.Pkg = NewPackage(t.File("category/pkgbase")) lines := t.NewLines("PLIST", - PlistRcsID, + PlistCvsID, "${PLIST.bincmds}bin/subdir/command") CheckLinesPlist(G.Pkg, lines) @@ -110,7 +134,7 @@ func (s *Suite) Test_CheckLinesPlist__sorting(c *check.C) { t := s.Init(c) lines := t.NewLines("PLIST", - PlistRcsID, + PlistCvsID, "@comment Do not remove", "sbin/i386/6c", "sbin/program", @@ -137,7 +161,7 @@ func (s *Suite) Test_plistLineSorter_Sort(c *check.C) { t.SetUpCommandLine("--autofix") lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "@comment Do not remove", "A", "b", @@ -163,7 +187,7 @@ func (s *Suite) Test_plistLineSorter_Sort(c *check.C) { cleanedLines := append(append(lines.Lines[0:5], lines.Lines[6:8]...), lines.Lines[9:]...) // Remove ${UNKNOWN} and @exec sorter2 := NewPlistLineSorter((&PlistChecker{nil, nil, nil, "", Once{}, false}). - NewLines(NewLines(lines.FileName, cleanedLines))) + NewLines(NewLines(lines.Filename, cleanedLines))) c.Check(sorter2.unsortable, check.IsNil) @@ -172,7 +196,7 @@ func (s *Suite) Test_plistLineSorter_Sort(c *check.C) { t.CheckOutputLines( "AUTOFIX: ~/PLIST:3: Sorting the whole file.") t.CheckFileLines("PLIST", - PlistRcsID, + PlistCvsID, "@comment Do not remove", // The header ends here "A", "C", @@ -193,7 +217,7 @@ func (s *Suite) Test_PlistChecker_checkLine(c *check.C) { t := s.Init(c) lines := t.NewLines("PLIST", - PlistRcsID, + PlistCvsID, "bin/program", "${PLIST.var}bin/conditional-program", "${PLIST.linux}${PLIST.arm}bin/arm-linux-only", @@ -227,7 +251,7 @@ func (s *Suite) Test_PlistChecker_checkPathMan__gz(c *check.C) { G.Pkg = NewPackage(t.File("category/pkgbase")) lines := t.NewLines("PLIST", - PlistRcsID, + PlistCvsID, "man/man3/strerror.3.gz") CheckLinesPlist(G.Pkg, lines) @@ -240,7 +264,7 @@ func (s *Suite) Test_PlistChecker_checkPath__PKGMANDIR(c *check.C) { t := s.Init(c) lines := t.NewLines("PLIST", - PlistRcsID, + PlistCvsID, "${PKGMANDIR}/man1/sh.1") CheckLinesPlist(nil, lines) @@ -253,7 +277,7 @@ func (s *Suite) Test_PlistChecker_checkPath__python_egg(c *check.C) { t := s.Init(c) lines := t.NewLines("PLIST", - PlistRcsID, + PlistCvsID, "${PYSITELIB}/gdspy-${PKGVERSION}-py${PYVERSSUFFIX}.egg-info/PKG-INFO") CheckLinesPlist(nil, lines) @@ -266,7 +290,7 @@ func (s *Suite) Test_PlistChecker__autofix(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "lib/libvirt/connection-driver/libvirt_driver_storage.la", "${PLIST.hal}lib/libvirt/connection-driver/libvirt_driver_nodedev.la", "${PLIST.xen}lib/libvirt/connection-driver/libvirt_driver_libxl.la", @@ -304,7 +328,7 @@ func (s *Suite) Test_PlistChecker__autofix(c *check.C) { "AUTOFIX: ~/PLIST:6: Replacing \"${PKGMANDIR}/\" with \"man/\".", "AUTOFIX: ~/PLIST:2: Sorting the whole file.") t.CheckFileLines("PLIST", - PlistRcsID, + PlistCvsID, "${PLIST.xen}lib/libvirt/connection-driver/libvirt_driver_libxl.la", "${PLIST.hal}lib/libvirt/connection-driver/libvirt_driver_nodedev.la", "lib/libvirt/connection-driver/libvirt_driver_storage.la", @@ -334,7 +358,7 @@ func (s *Suite) Test_PlistChecker__remove_same_entries(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "${PLIST.option1}bin/true", "bin/true", "${PLIST.option1}bin/true", @@ -363,7 +387,7 @@ func (s *Suite) Test_PlistChecker__remove_same_entries(c *check.C) { "AUTOFIX: ~/PLIST:8: Deleting this line.", "AUTOFIX: ~/PLIST:2: Sorting the whole file.") t.CheckFileLines("PLIST", - PlistRcsID, + PlistCvsID, "${PLIST.option2}bin/false", "${PLIST.option3}bin/false", "bin/true") @@ -378,7 +402,7 @@ func (s *Suite) Test_PlistChecker__autofix_with_only(c *check.C) { t.SetUpCommandLine("-Wall", "--autofix", "--only", "matches nothing") lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "sbin/program", "bin/program") @@ -386,7 +410,7 @@ func (s *Suite) Test_PlistChecker__autofix_with_only(c *check.C) { t.CheckOutputEmpty() t.CheckFileLines("PLIST", - PlistRcsID, + PlistCvsID, "sbin/program", "bin/program") } @@ -395,7 +419,7 @@ func (s *Suite) Test_PlistChecker__exec_MKDIR(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "bin/program", "@exec ${MKDIR} %D/share/mk/subdir") @@ -408,7 +432,7 @@ func (s *Suite) Test_PlistChecker__empty_line(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "", "bin/program") @@ -424,7 +448,7 @@ func (s *Suite) Test_PlistChecker__empty_line(c *check.C) { t.CheckOutputLines( "AUTOFIX: ~/PLIST:2: Deleting this line.") t.CheckFileLines("PLIST", - PlistRcsID, + PlistCvsID, "bin/program") } @@ -432,7 +456,7 @@ func (s *Suite) Test_PlistChecker__invalid_line_type(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "---invalid", "+++invalid", "<<<<<<<< merge conflict", @@ -454,7 +478,7 @@ func (s *Suite) Test_PlistChecker_checkPathNonAscii(c *check.C) { t.SetUpCommandLine("-Wall", "--explain") lines := t.NewLines("PLIST", - PlistRcsID, + PlistCvsID, "dir1/fr\xFCher", // German, "back then", encoded in ISO 8859-1 @@ -506,7 +530,7 @@ func (s *Suite) Test_PlistChecker__doc(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "doc/html/index.html") CheckLinesPlist(nil, lines) @@ -518,22 +542,52 @@ func (s *Suite) Test_PlistChecker__doc(c *check.C) { func (s *Suite) Test_PlistChecker__PKGLOCALEDIR(c *check.C) { t := s.Init(c) - lines := t.SetUpFileLines("PLIST", - PlistRcsID, + t.SetUpPackage("category/package") + t.CreateFileLines("category/package/PLIST", + PlistCvsID, "${PKGLOCALEDIR}/file") - G.Pkg = NewPackage(t.File("category/package")) + t.FinishSetUp() - CheckLinesPlist(G.Pkg, lines) + G.Check(t.File("category/package")) t.CheckOutputLines( - "WARN: ~/PLIST:2: PLIST contains ${PKGLOCALEDIR}, but USE_PKGLOCALEDIR is not set in the package Makefile.") + "WARN: ~/category/package/PLIST:2: PLIST contains ${PKGLOCALEDIR}, " + + "but USE_PKGLOCALEDIR is not set in the package Makefile.") +} + +func (s *Suite) Test_PlistChecker__PKGLOCALEDIR_with_USE_PKGLOCALEDIR(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + "USE_PKGLOCALEDIR=\tyes") + t.CreateFileLines("category/package/PLIST", + PlistCvsID, + "${PKGLOCALEDIR}/file") + t.FinishSetUp() + + G.Check(t.File("category/package")) +} + +func (s *Suite) Test_PlistChecker__PKGLOCALEDIR_without_package(c *check.C) { + t := s.Init(c) + + lines := t.SetUpFileLines("PLIST", + PlistCvsID, + "${PKGLOCALEDIR}/file") + + CheckLinesPlist(nil, lines) + + // When a PLIST file is checked on its own, outside of checking a + // package, there can be no warning that USE_PKGLOCALEDIR is missing + // in the package. + t.CheckOutputEmpty() } func (s *Suite) Test_PlistChecker_checkPath__unwanted_entries(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "share/perllocal.pod", "share/pkgbase/CVS/Entries", "share/pkgbase/Makefile.orig") @@ -549,23 +603,55 @@ func (s *Suite) Test_PlistChecker_checkPath__unwanted_entries(c *check.C) { func (s *Suite) Test_PlistChecker_checkPathInfo(c *check.C) { t := s.Init(c) - lines := t.SetUpFileLines("PLIST", - PlistRcsID, + t.SetUpPackage("category/package") + t.Chdir("category/package") + t.CreateFileLines("PLIST", + PlistCvsID, "info/gmake.1.info") - G.Pkg = NewPackage(t.File("category/package")) + t.FinishSetUp() - CheckLinesPlist(G.Pkg, lines) + G.Check(".") t.CheckOutputLines( - "WARN: ~/PLIST:2: Packages that install info files should set INFO_FILES in the Makefile.") + "WARN: PLIST:2: Packages that install info files should set INFO_FILES in the Makefile.") +} + +func (s *Suite) Test_PlistChecker_checkPathInfo__with_package(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + "INFO_FILES=\tyes") + t.Chdir("category/package") + t.CreateFileLines("PLIST", + PlistCvsID, + "info/gmake.1.info") + t.FinishSetUp() + + G.Check(".") + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_PlistChecker_checkPathInfo__without_package(c *check.C) { + t := s.Init(c) + + lines := t.SetUpFileLines("PLIST", + PlistCvsID, + "info/gmake.1.info") + + CheckLinesPlist(nil, lines) + + t.CheckOutputEmpty() } func (s *Suite) Test_PlistChecker_checkPathLib(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "lib/charset.alias", + "lib/liberty-1.0.a", + "lib/liberty-1.0.archive", "lib/liberty-1.0.la", "lib/locale/de_DE/liberty.mo", "lib/package/liberty-1.0.so") @@ -576,34 +662,69 @@ func (s *Suite) Test_PlistChecker_checkPathLib(c *check.C) { t.CheckOutputLines( "ERROR: ~/PLIST:2: Only the libiconv package may install lib/charset.alias.", - "WARN: ~/PLIST:3: Packages that install libtool libraries should define USE_LIBTOOL.", - "ERROR: ~/PLIST:4: \"lib/locale\" must not be listed. "+ + "WARN: ~/PLIST:3: Redundant library found. The libtool library is in line 5.", + "WARN: ~/PLIST:5: Packages that install libtool libraries should define USE_LIBTOOL.", + "ERROR: ~/PLIST:6: \"lib/locale\" must not be listed. "+ "Use ${PKGLOCALEDIR}/locale and set USE_PKGLOCALEDIR instead.") } +func (s *Suite) Test_PlistChecker_checkPathLib__libiconv(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("converters/libiconv") + t.Chdir("converters/libiconv") + t.CreateFileLines("PLIST", + PlistCvsID, + "lib/charset.alias") + t.FinishSetUp() + + G.Check(".") + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_PlistChecker_checkPathLib__libtool(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + "USE_LIBTOOL=\tyes") + t.Chdir("category/package") + t.CreateFileLines("PLIST", + PlistCvsID, + "lib/libname.la") + t.FinishSetUp() + + G.Check(".") + + t.CheckOutputEmpty() +} + func (s *Suite) Test_PlistChecker_checkPathMan(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, + "man/cat1/formatted.0", + "man/man1/formatted.1", "man/man1/program.8", "man/manx/program.x") CheckLinesPlist(nil, lines) t.CheckOutputLines( - "WARN: ~/PLIST:2: Mismatch between the section (1) and extension (8) of the manual page.", - "WARN: ~/PLIST:3: Unknown section \"x\" for manual page.") + "WARN: ~/PLIST:4: Mismatch between the section (1) and extension (8) of the manual page.", + "WARN: ~/PLIST:5: Unknown section \"x\" for manual page.") } func (s *Suite) Test_PlistChecker_checkPathShare(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "share/doc/html/package/index.html", "share/doc/package/index.html", "share/icons/hicolor/icon-theme.cache", + "share/icons/open_24x24.svg", "share/info/program.1.info", "share/man/man1/program.1") G.Pkg = NewPackage(t.File("category/package")) @@ -616,11 +737,11 @@ func (s *Suite) Test_PlistChecker_checkPathShare(c *check.C) { "ERROR: ~/PLIST:4: Packages that install hicolor icons must include \"../../graphics/hicolor-icon-theme/buildlink3.mk\" in the Makefile.", "ERROR: ~/PLIST:4: The file icon-theme.cache must not appear in any PLIST file.", "WARN: ~/PLIST:4: Packages that install icon theme files should set ICON_THEMES.", - "WARN: ~/PLIST:5: Info pages should be installed into info/, not share/info/.", - "WARN: ~/PLIST:6: Man pages should be installed into man/, not share/man/.") + "WARN: ~/PLIST:6: Info pages should be installed into info/, not share/info/.", + "WARN: ~/PLIST:7: Man pages should be installed into man/, not share/man/.") } -func (s *Suite) Test_PlistChecker_checkPathShare__gnome_icon_theme(c *check.C) { +func (s *Suite) Test_PlistChecker_checkPathShareIcons__using_gnome_icon_theme(c *check.C) { t := s.Init(c) t.CreateFileDummyBuildlink3("graphics/gnome-icon-theme/buildlink3.mk") @@ -628,7 +749,7 @@ func (s *Suite) Test_PlistChecker_checkPathShare__gnome_icon_theme(c *check.C) { "ICON_THEMES=\tyes", ".include \"../../graphics/gnome-icon-theme/buildlink3.mk\"") t.CreateFileLines("graphics/gnome-icon-theme-extras/PLIST", - PlistRcsID, + PlistCvsID, "share/icons/gnome/16x16/devices/media-optical-cd-audio.png", "share/icons/gnome/16x16/devices/media-optical-dvd.png") t.FinishSetUp() @@ -648,42 +769,126 @@ func (s *Suite) Test_PlistChecker_checkPathShare__gnome_icon_theme(c *check.C) { t.CheckOutputEmpty() } +func (s *Suite) Test_PlistChecker_checkPathShareIcons__gnome_icon_theme_itself(c *check.C) { + t := s.Init(c) + + t.CreateFileDummyBuildlink3("graphics/gnome-icon-theme/buildlink3.mk", + "ICON_THEMES=\tyes") + t.SetUpPackage("graphics/gnome-icon-theme", + ".include \"../../graphics/gnome-icon-theme/buildlink3.mk\"") + t.CreateFileLines("graphics/gnome-icon-theme/PLIST", + PlistCvsID, + "share/icons/gnome/16x16/devices/media-optical-cd-audio.png", + "share/icons/gnome/16x16/devices/media-optical-dvd.png") + t.FinishSetUp() + t.Chdir(".") + + G.Check("graphics/gnome-icon-theme") + + t.CheckOutputEmpty() +} + +func (s *Suite) Test_PlistChecker_checkPathShareIcons(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("graphics/hicolor-icon-theme") + t.CreateFileLines("graphics/hicolor-icon-theme/PLIST", + PlistCvsID, + "share/icons/hicolor/icon-theme.cache", + "share/icons/hicolor/open.svg") + t.SetUpPackage("graphics/other") + t.Copy("graphics/hicolor-icon-theme/PLIST", "graphics/other/PLIST") + t.Chdir(".") + t.FinishSetUp() + + G.Check("graphics/hicolor-icon-theme") + G.Check("graphics/other") + + t.CheckOutputLines( + "WARN: graphics/hicolor-icon-theme/PLIST:2: "+ + "Packages that install icon theme files should set ICON_THEMES.", + "ERROR: graphics/other/PLIST:2: Packages that install hicolor icons "+ + "must include \"../../graphics/hicolor-icon-theme/buildlink3.mk\" in the Makefile.", + "ERROR: graphics/other/PLIST:2: The file icon-theme.cache must not appear in any PLIST file.", + "WARN: graphics/other/PLIST:2: "+ + "Packages that install icon theme files should set ICON_THEMES.") +} + +func (s *Suite) Test_PlistChecker_checkPathShareIcons__hicolor_ok(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package", + ".include \"../../graphics/hicolor-icon-theme/buildlink3.mk\"") + t.CreateFileLines("category/package/PLIST", + PlistCvsID, + "share/icons/hicolor/open.svg") + t.CreateFileLines("graphics/hicolor-icon-theme/buildlink3.mk", + MkCvsID, + "ICON_THEMES=\tyes") + t.FinishSetUp() + + G.Check(t.File("category/package")) + + t.CheckOutputEmpty() +} + func (s *Suite) Test_PlistLine_CheckTrailingWhitespace(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, - "bin/program \t") + PlistCvsID, + "bin/space ", + "bin/space-tab \t", + "bin/tab\t") CheckLinesPlist(nil, lines) t.CheckOutputLines( - "WARN: ~/PLIST:2: Non-ASCII filename \"bin/program \\t\".", - "ERROR: ~/PLIST:2: Pkgsrc does not support filenames ending in whitespace.") + "ERROR: ~/PLIST:2: Pkgsrc does not support filenames ending in whitespace.", + "WARN: ~/PLIST:3: Non-ASCII filename \"bin/space-tab \\t\".", + "ERROR: ~/PLIST:3: Pkgsrc does not support filenames ending in whitespace.", + "ERROR: ~/PLIST:4: Pkgsrc does not support filenames ending in whitespace.") } func (s *Suite) Test_PlistLine_CheckDirective(c *check.C) { t := s.Init(c) lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "@unexec rmdir %D/bin", + "@unexec rmdir %D/bin || true", + "@unexec rmdir %D/bin || ${TRUE}", + "@unexec echo 'uninstalling'", "@exec ldconfig", + "@exec ldconfig || /usr/bin/true", "@comment This is a comment", "@dirrm %D/bin", "@imake-man 1 2 3 4", "@imake-man 1 2 ${IMAKE_MANNEWSUFFIX}", + "@imake-man 1 2 3", "@unknown") CheckLinesPlist(nil, lines) t.CheckOutputLines( "WARN: ~/PLIST:2: Please remove this line. It is no longer necessary.", - "ERROR: ~/PLIST:3: The ldconfig command must be used with \"||/usr/bin/true\".", - "WARN: ~/PLIST:5: @dirrm is obsolete. Please remove this line.", - "WARN: ~/PLIST:6: Invalid number of arguments for imake-man, should be 3.", - "WARN: ~/PLIST:7: IMAKE_MANNEWSUFFIX is not meant to appear in PLISTs.", - "WARN: ~/PLIST:8: Unknown PLIST directive \"@unknown\".") + "ERROR: ~/PLIST:6: The ldconfig command must be used with \"||/usr/bin/true\".", + "WARN: ~/PLIST:9: @dirrm is obsolete. Please remove this line.", + "WARN: ~/PLIST:10: Invalid number of arguments for imake-man, should be 3.", + "WARN: ~/PLIST:11: IMAKE_MANNEWSUFFIX is not meant to appear in PLISTs.", + "WARN: ~/PLIST:13: Unknown PLIST directive \"@unknown\".") +} + +func (s *Suite) Test_NewPlistLineSorter__only_comments(c *check.C) { + t := s.Init(c) + + lines := t.NewLines("PLIST", + PlistCvsID, + "@comment intentionally left empty") + + CheckLinesPlist(nil, lines) + + t.CheckOutputEmpty() } func (s *Suite) Test_plistLineSorter__unsortable(c *check.C) { @@ -691,7 +896,7 @@ func (s *Suite) Test_plistLineSorter__unsortable(c *check.C) { t.SetUpCommandLine("-Wall", "--show-autofix") lines := t.SetUpFileLines("PLIST", - PlistRcsID, + PlistCvsID, "bin/program${OPSYS}", "@exec true", "bin/program1") @@ -701,8 +906,8 @@ func (s *Suite) Test_plistLineSorter__unsortable(c *check.C) { t.CheckOutputLines( "TRACE: + CheckLinesPlist(\"~/PLIST\")", - "TRACE: 1 + (*LinesImpl).CheckRcsID(\"@comment \", \"@comment \")", - "TRACE: 1 - (*LinesImpl).CheckRcsID(\"@comment \", \"@comment \")", + "TRACE: 1 + (*Lines).CheckCvsID(\"@comment \", \"@comment \")", + "TRACE: 1 - (*Lines).CheckCvsID(\"@comment \", \"@comment \")", "TRACE: 1 ~/PLIST:2: bin/program${OPSYS}: This line prevents pkglint from sorting the PLIST automatically.", "TRACE: 1 + SaveAutofixChanges()", "TRACE: 1 - SaveAutofixChanges()", diff --git a/pkgtools/pkglint/files/redundantscope.go b/pkgtools/pkglint/files/redundantscope.go index f40d923dd63..104ad1a9560 100644 --- a/pkgtools/pkglint/files/redundantscope.go +++ b/pkgtools/pkglint/files/redundantscope.go @@ -27,13 +27,13 @@ func NewRedundantScope() *RedundantScope { return &RedundantScope{vars: make(map[string]*redundantScopeVarinfo)} } -func (s *RedundantScope) Check(mklines MkLines) { - mklines.ForEach(func(mkline MkLine) { +func (s *RedundantScope) Check(mklines *MkLines) { + mklines.ForEach(func(mkline *MkLine) { s.checkLine(mklines, mkline) }) } -func (s *RedundantScope) checkLine(mklines MkLines, mkline MkLine) { +func (s *RedundantScope) checkLine(mklines *MkLines, mkline *MkLine) { s.updateIncludePath(mkline) switch { @@ -44,7 +44,7 @@ func (s *RedundantScope) checkLine(mklines MkLines, mkline MkLine) { s.handleVarUse(mkline) } -func (s *RedundantScope) updateIncludePath(mkline MkLine) { +func (s *RedundantScope) updateIncludePath(mkline *MkLine) { if mkline.firstLine == 1 { s.includePath.push(mkline.Location.Filename) } else { @@ -52,7 +52,7 @@ func (s *RedundantScope) updateIncludePath(mkline MkLine) { } } -func (s *RedundantScope) handleVarassign(mkline MkLine, ind *Indentation) { +func (s *RedundantScope) handleVarassign(mkline *MkLine, ind *Indentation) { varname := mkline.Varname() info := s.get(varname) @@ -154,10 +154,10 @@ func (s *RedundantScope) handleVarassign(mkline MkLine, ind *Indentation) { } } -func (s *RedundantScope) handleVarUse(mkline MkLine) { +func (s *RedundantScope) handleVarUse(mkline *MkLine) { switch { case mkline.IsVarassign(): - mkline.ForEachUsed(func(varUse *MkVarUse, time vucTime) { + mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) { varname := varUse.varname info := s.get(varname) info.vari.Read(mkline) @@ -195,7 +195,7 @@ func (s *RedundantScope) access(varname string) { info.includePaths = append(info.includePaths, s.includePath.copy()) } -func (s *RedundantScope) onRedundant(redundant MkLine, because MkLine) { +func (s *RedundantScope) onRedundant(redundant *MkLine, because *MkLine) { if redundant.Op() == opAssignDefault { redundant.Notef("Default assignment of %s has no effect because of %s.", because.Varname(), redundant.RefTo(because)) @@ -205,7 +205,7 @@ func (s *RedundantScope) onRedundant(redundant MkLine, because MkLine) { } } -func (s *RedundantScope) onOverwrite(overwritten MkLine, by MkLine) { +func (s *RedundantScope) onOverwrite(overwritten *MkLine, by *MkLine) { overwritten.Warnf("Variable %s is overwritten in %s.", overwritten.Varname(), overwritten.RefTo(by)) overwritten.Explain( diff --git a/pkgtools/pkglint/files/redundantscope_test.go b/pkgtools/pkglint/files/redundantscope_test.go index 868f8c2eec4..de1eef2b74a 100644 --- a/pkgtools/pkglint/files/redundantscope_test.go +++ b/pkgtools/pkglint/files/redundantscope_test.go @@ -1118,7 +1118,7 @@ func (s *Suite) Test_RedundantScope__procedure_call_implemented_package(c *check t.SetUpPackage("x11/Xaos", ".include \"../../devel/gettext-lib/buildlink3.mk\"") t.CreateFileLines("devel/gettext-lib/builtin.mk", - MkRcsID, + MkCvsID, "", ".include \"../../mk/bsd.fast.prefs.mk\"", "", @@ -1126,7 +1126,7 @@ func (s *Suite) Test_RedundantScope__procedure_call_implemented_package(c *check ".if !empty(CHECK_BUILTIN.gettext:M[nN][oO])", ".endif") t.CreateFileLines("devel/gettext-lib/buildlink3.mk", - MkRcsID, + MkCvsID, "CHECK_BUILTIN.gettext:= yes", ".include \"builtin.mk\"", "CHECK_BUILTIN.gettext:= no") @@ -1148,12 +1148,12 @@ func (s *Suite) Test_RedundantScope__procedure_call_infrastructure(c *check.C) { t.SetUpPackage("x11/alacarte", ".include \"../../mk/pthread.buildlink3.mk\"") t.CreateFileLines("mk/pthread.buildlink3.mk", - MkRcsID, + MkCvsID, "CHECK_BUILTIN.gettext:= yes", ".include \"pthread.builtin.mk\"", "CHECK_BUILTIN.gettext:= no") t.CreateFileLines("mk/pthread.builtin.mk", - MkRcsID, + MkCvsID, "CHECK_BUILTIN.gettext?= no", ".if !empty(CHECK_BUILTIN.gettext:M[nN][oO])", ".endif") @@ -1248,7 +1248,7 @@ func (s *Suite) Test_RedundantScope__included_OPSYS_variable(c *check.C) { t.SetUpPackage("category/dependency") t.CreateFileDummyBuildlink3("category/dependency/buildlink3.mk") t.CreateFileLines("category/dependency/builtin.mk", - MkRcsID, + MkCvsID, "CONFIGURE_ARGS.Darwin+= darwin") t.FinishSetUp() @@ -1501,7 +1501,7 @@ func (s *Suite) Test_RedundantScope_handleVarassign__conditional(c *check.C) { t.Check( writeLocations, deepEquals, - []MkLine{mklines.mklines[0], mklines.mklines[2]}) + []*MkLine{mklines.mklines[0], mklines.mklines[2]}) } // Ensures that commented variables do not influence the redundancy check. diff --git a/pkgtools/pkglint/files/shell.go b/pkgtools/pkglint/files/shell.go index 905546ec992..8d044b1d49d 100644 --- a/pkgtools/pkglint/files/shell.go +++ b/pkgtools/pkglint/files/shell.go @@ -16,15 +16,16 @@ import ( // Or it is a variable assignment line from a Makefile with a left-hand // side variable that is of some shell-like type; see Vartype.IsShell. type ShellLineChecker struct { - MkLines MkLines - mkline MkLine + MkLines *MkLines + mkline *MkLine // checkVarUse is set to false when checking a single shell word // in order to skip duplicate warnings in variable assignments. checkVarUse bool } -func NewShellLineChecker(mklines MkLines, mkline MkLine) *ShellLineChecker { +func NewShellLineChecker(mklines *MkLines, mkline *MkLine) *ShellLineChecker { + assertNotNil(mklines) return &ShellLineChecker{mklines, mkline, true} } @@ -36,7 +37,7 @@ func (ck *ShellLineChecker) Explain(explanation ...string) { } var shellCommandsType = NewVartype(BtShellCommands, NoVartypeOptions, NewACLEntry("*", aclpAllRuntime)) -var shellWordVuc = &VarUseContext{shellCommandsType, vucTimeUnknown, VucQuotPlain, false} +var shellWordVuc = &VarUseContext{shellCommandsType, VucUnknownTime, VucQuotPlain, false} func (ck *ShellLineChecker) CheckWord(token string, checkQuoting bool, time ToolTime) { if trace.Tracing { @@ -216,7 +217,7 @@ func (ck *ShellLineChecker) checkVaruseToken(atoms *[]*ShAtom, quoting ShQuoting } if ck.checkVarUse { - vuc := VarUseContext{shellCommandsType, vucTimeUnknown, quoting.ToVarUseContext(), true} + vuc := VarUseContext{shellCommandsType, VucUnknownTime, quoting.ToVarUseContext(), true} MkLineChecker{ck.MkLines, ck.mkline}.CheckVaruse(varuse, &vuc) } @@ -291,10 +292,7 @@ func (ck *ShellLineChecker) variableNeedsQuoting(shVarname string) bool { case "d", "f", "i", "id", "file", "src", "dst", "prefix": return false // Probably ok } - if hasSuffix(shVarname, "dir") { - return false // Probably ok - } - return true + return !hasSuffix(shVarname, "dir") // Probably ok } func (ck *ShellLineChecker) CheckShellCommandLine(shelltext string) { @@ -485,9 +483,7 @@ func (scc *SimpleCommandChecker) checkCommandStart() { scc.checkInstallCommand(shellword) switch { - case shellword == "${RUN}" || shellword == "": - // FIXME: ${RUN} must never appear as a simple command. - // It should always be trimmed before passing the shell program to the SimpleCommandChecker. + case shellword == "": break case scc.handleForbiddenCommand(): break @@ -504,7 +500,7 @@ func (scc *SimpleCommandChecker) checkCommandStart() { case scc.handleComment(): break default: - if G.Opts.WarnExtra && !(scc.MkLines != nil && scc.MkLines.indentation.DependsOn("OPSYS")) { + if G.Opts.WarnExtra && !scc.MkLines.indentation.DependsOn("OPSYS") { scc.mkline.Warnf("Unknown shell command %q.", shellword) scc.mkline.Explain( "To make the package portable to all platforms that pkgsrc supports,", @@ -560,24 +556,25 @@ func (scc *SimpleCommandChecker) handleCommandVariable() bool { } shellword := scc.strcmd.Name - if varuse := ToVarUse(shellword); varuse != nil { - varname := varuse.varname + varuse := NewMkParser(nil, shellword, false).VarUse() + if varuse == nil { + return false + } - if vartype := G.Pkgsrc.VariableType(scc.MkLines, varname); vartype != nil && vartype.basicType.name == "ShellCommand" { - scc.checkInstallCommand(shellword) - return true - } + varname := varuse.varname - // When the package author has explicitly defined a command - // variable, assume it to be valid. - if scc.MkLines != nil && scc.MkLines.vars.DefinedSimilar(varname) { - return true - } - if G.Pkg != nil && G.Pkg.vars.DefinedSimilar(varname) { - return true - } + if vartype := G.Pkgsrc.VariableType(scc.MkLines, varname); vartype != nil && vartype.basicType.name == "ShellCommand" { + scc.checkInstallCommand(shellword) + return true } - return false + + // When the package author has explicitly defined a command + // variable, assume it to be valid. + if scc.MkLines.vars.DefinedSimilar(varname) { + return true + } + + return G.Pkg != nil && G.Pkg.vars.DefinedSimilar(varname) } func (scc *SimpleCommandChecker) handleShellBuiltin() bool { @@ -919,8 +916,9 @@ func (spc *ShellProgramChecker) canFail(cmd *MkShCommand) bool { if simple.Name == nil { for _, assignment := range simple.Assignments { - if contains(assignment.MkText, "`") || contains(assignment.MkText, "$(") { - if !contains(assignment.MkText, "|| ${TRUE}") { + text := assignment.MkText + if contains(text, "`") || contains(text, "$(") { + if !contains(text, "|| ${TRUE}") && !contains(text, "|| true") { return true } } @@ -929,7 +927,7 @@ func (spc *ShellProgramChecker) canFail(cmd *MkShCommand) bool { } for _, redirect := range simple.Redirections { - if redirect.Target != nil && !hasSuffix(redirect.Op, "&") { + if !hasSuffix(redirect.Op, "&") { return true } } @@ -955,7 +953,7 @@ func (spc *ShellProgramChecker) canFail(cmd *MkShCommand) bool { args := simple.Args argc := len(args) switch toolName { - case "echo", "env", "printf", "tr": + case "basename", "dirname", "echo", "env", "printf", "tr": return false case "sed", "gsed": if argc == 2 && args[0].MkText == "-e" { @@ -1062,7 +1060,7 @@ func (ck *ShellLineChecker) checkInstallCommand(shellcmd string) { // Example: "word1 word2;;;" => "word1", "word2", ";;", ";" // // TODO: Document what this function should be used for. -func splitIntoShellTokens(line Line, text string) (tokens []string, rest string) { +func splitIntoShellTokens(line *Line, text string) (tokens []string, rest string) { if trace.Tracing { defer trace.Call(line, text)() } diff --git a/pkgtools/pkglint/files/shell_test.go b/pkgtools/pkglint/files/shell_test.go index dee5358868d..3a127740a61 100644 --- a/pkgtools/pkglint/files/shell_test.go +++ b/pkgtools/pkglint/files/shell_test.go @@ -144,7 +144,7 @@ func (s *Suite) Test_ShellLineChecker_CheckShellCommandLine(c *check.C) { "\t"+shellCommand) ck := NewShellLineChecker(mklines, mklines.mklines[0]) - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { ck.CheckShellCommandLine(ck.mkline.ShellCommand()) }) @@ -262,7 +262,7 @@ func (s *Suite) Test_ShellLineChecker_CheckShellCommandLine__strip(c *check.C) { mklines := t.NewMkLines("filename.mk", "\t"+shellCommand) - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { ck := NewShellLineChecker(mklines, mkline) ck.CheckShellCommandLine(mkline.ShellCommand()) }) @@ -352,7 +352,8 @@ func (s *Suite) Test_ShellProgramChecker_checkPipeExitcode(c *check.C) { "\t sed s,s,s < input | right-side", "\t ./unknown | right-side", "\t var=value | right-side", - "\t if :; then :; fi | right-side") + "\t if :; then :; fi | right-side", + "\t var=`cat file` | right-side") for _, mkline := range mklines.mklines { ck := NewShellLineChecker(mklines, mkline) @@ -366,7 +367,8 @@ func (s *Suite) Test_ShellProgramChecker_checkPipeExitcode(c *check.C) { "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: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.") + "WARN: Makefile:11: The exitcode of the command at the left of the | operator is ignored.", + "WARN: Makefile:12: The exitcode of the command at the left of the | operator is ignored.") } // TODO: Document the exact purpose of this test, or split it into useful tests. @@ -386,12 +388,12 @@ func (s *Suite) Test_ShellLineChecker_CheckShellCommandLine__implementation(c *c c.Check(tokens, deepEquals, []string{text}) c.Check(rest, equals, "") - mklines.ForEach(func(mkline MkLine) { ck.CheckWord(text, false, RunTime) }) + mklines.ForEach(func(mkline *MkLine) { ck.CheckWord(text, false, RunTime) }) t.CheckOutputLines( "WARN: filename.mk:1: Unknown shell command \"echo\".") - mklines.ForEach(func(mkline MkLine) { ck.CheckShellCommandLine(text) }) + mklines.ForEach(func(mkline *MkLine) { ck.CheckShellCommandLine(text) }) // No parse errors t.CheckOutputLines( @@ -530,7 +532,7 @@ func (s *Suite) Test_ShellLineChecker_CheckWord__PKGMANDIR(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("chat/ircII/Makefile", - MkRcsID, + MkCvsID, "CONFIGURE_ARGS+=--mandir=${DESTDIR}${PREFIX}/man", "CONFIGURE_ARGS+=--mandir=${DESTDIR}${PREFIX}/${PKGMANDIR}") @@ -542,11 +544,26 @@ func (s *Suite) Test_ShellLineChecker_CheckWord__PKGMANDIR(c *check.C) { "NOTE: chat/ircII/Makefile:3: This variable value should be aligned to column 25.") } +func (s *Suite) Test_ShellLineChecker_CheckWord__empty(c *check.C) { + t := s.Init(c) + + t.SetUpVartypes() + + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "JAVA_CLASSPATH=\t# empty") + + mklines.Check() + + t.CheckOutputEmpty() +} + func (s *Suite) Test_ShellLineChecker_unescapeBackticks__unfinished(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "", "pre-configure:", "\t`${VAR}", // Error in first shell word @@ -565,17 +582,19 @@ func (s *Suite) Test_ShellLineChecker_unescapeBackticks__unfinished(c *check.C) func (s *Suite) Test_ShellLineChecker_unescapeBackticks__unfinished_direct(c *check.C) { t := s.Init(c) - mkline := t.NewMkLine("dummy.mk", 123, "\t# shell command") + mklines := t.NewMkLines("dummy.mk", + MkCvsID, + "\t# shell command") // This call is unrealistic. It doesn't happen in practice, and this // direct, forcing test is only to reach the code coverage. atoms := []*ShAtom{ NewShAtom(shtText, "`", shqBackt)} - NewShellLineChecker(nil, mkline). + NewShellLineChecker(mklines, mklines.mklines[1]). unescapeBackticks(&atoms, shqBackt) t.CheckOutputLines( - "ERROR: dummy.mk:123: Unfinished backticks after \"\".") + "ERROR: dummy.mk:2: Unfinished backticks after \"\".") } func (s *Suite) Test_ShellLineChecker_variableNeedsQuoting(c *check.C) { @@ -616,7 +635,7 @@ func (s *Suite) Test_ShellLineChecker_variableNeedsQuoting__integration(c *check t.SetUpVartypes() t.SetUpTool("cp", "", AtRunTime) mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "", // It's a bit silly to use shell variables in CONFIGURE_ARGS, // but as of January 2019 that's the only way to run ShellLine.variableNeedsQuoting. @@ -661,7 +680,7 @@ func (s *Suite) Test_ShellLineChecker_CheckShellCommandLine__shell_variables(c * t.SetUpTool("sed", "SED", AtRunTime) text := "for f in *.pl; do ${SED} s,@PREFIX@,${PREFIX}, < $f > $f.tmp && ${MV} $f.tmp $f; done" mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "\t"+text) @@ -776,7 +795,7 @@ func (s *Suite) Test_ShellLineChecker__shell_comment_with_line_continuation(c *c t := s.Init(c) mklines := t.SetUpFileMkLines("Makefile", - MkRcsID, + MkCvsID, "pre-install:", "\t"+"# comment\\", "\t"+"echo \"hello\"") @@ -839,6 +858,57 @@ func (s *Suite) Test_ShellLineChecker_checkWordQuoting(c *check.C) { test( "$$$$", nil...) + + test( + "``", + nil...) +} + +func (s *Suite) Test_ShellLineChecker_checkShVarUsePlain__default_warning_level(c *check.C) { + t := s.Init(c) + + t.SetUpCommandLine( /* none */ ) + t.SetUpVartypes() + t.SetUpTool("echo", "", AtRunTime) + + mklines := t.NewMkLines("filename.mk", + MkCvsID, + "CONFIGURE_ARGS+=\techo $$@ $$var", + "", + "pre-configure:", + "\techo $$@ $$var") + + mklines.Check() + + // Using $@ outside of double quotes is so obviously wrong that + // the warning is issued by default. + t.CheckOutputLines( + "WARN: filename.mk:2: The $@ shell variable should only be used in double quotes.", + "WARN: filename.mk:5: The $@ shell variable should only be used in double quotes.") +} + +func (s *Suite) Test_ShellLineChecker_checkShVarUsePlain__Wall(c *check.C) { + t := s.Init(c) + + t.SetUpVartypes() + t.SetUpTool("echo", "", AtRunTime) + + mklines := t.NewMkLines("filename.mk", + MkCvsID, + "CONFIGURE_ARGS+=\techo $$@ $$var", + "", + "pre-configure:", + "\techo $$@ $$var") + + mklines.Check() + + // FIXME: It is inconsistent that the check for unquoted shell + // variables is enabled for CONFIGURE_ARGS (where shell variables + // don't make sense at all) but not for real shell commands. + t.CheckOutputLines( + "WARN: filename.mk:2: The $@ shell variable should only be used in double quotes.", + "WARN: filename.mk:2: Unquoted shell variable \"var\".", + "WARN: filename.mk:5: The $@ shell variable should only be used in double quotes.") } func (s *Suite) Test_ShellLineChecker_unescapeBackticks(c *check.C) { @@ -907,7 +977,7 @@ func (s *Suite) Test_ShellLineChecker_unescapeBackticks__dquotBacktDquot(c *chec t.SetUpTool("echo", "", AtRunTime) mklines := t.NewMkLines("dummy.mk", - MkRcsID, + MkCvsID, "\t var=\"`echo \"\"`\"") mklines.Check() @@ -921,7 +991,7 @@ func (s *Suite) Test_ShellLineChecker__variable_outside_quotes(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("dummy.mk", - MkRcsID, + MkCvsID, "GZIP=\t${ECHO} $$comment") mklines.Check() @@ -938,7 +1008,7 @@ func (s *Suite) Test_ShellLineChecker_CheckShellCommand__cd_inside_if(c *check.C t.SetUpVartypes() t.SetUpTool("echo", "ECHO", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "\t${RUN} if cd /bin; then echo \"/bin exists.\"; fi") @@ -955,7 +1025,7 @@ func (s *Suite) Test_ShellLineChecker_CheckShellCommand__negated_pipe(c *check.C t.SetUpTool("echo", "ECHO", AtRunTime) t.SetUpTool("test", "TEST", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "\t${RUN} if ! test -f /etc/passwd; then echo \"passwd is missing.\"; fi") @@ -971,7 +1041,7 @@ func (s *Suite) Test_ShellLineChecker_CheckShellCommand__subshell(c *check.C) { t.SetUpTool("echo", "ECHO", AtRunTime) t.SetUpTool("expr", "EXPR", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "pre-configure:", "\t@(echo ok)", @@ -998,7 +1068,7 @@ func (s *Suite) Test_ShellLineChecker_CheckShellCommand__case_patterns_from_vari t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "pre-configure:", "\tcase $$file in ${CHECK_PERMS_SKIP:@pattern@${pattern}) ;;@} *) continue; esac") @@ -1017,15 +1087,23 @@ func (s *Suite) Test_ShellLineChecker_checkHiddenAndSuppress(c *check.C) { t.SetUpTool("echo", "ECHO", AtRunTime) t.SetUpTool("ls", "LS", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "show-all-targets: .PHONY", "\t@echo 'hello'", - "\t@ls -l") + "\t@ls -l", + "", + "anything-message: .PHONY", + "\t@echo 'may be hidden'", + "\t@ls 'may be hidden'", + "", + "pre-configure:", + "\t@") mklines.Check() - // No warning about the hidden ls since the target name starts with "show-". + // No warning about the hidden ls since the target names start + // with "show-" or end with "-message". t.CheckOutputEmpty() } @@ -1034,7 +1112,7 @@ func (s *Suite) Test_ShellLineChecker_checkHiddenAndSuppress__no_tracing(c *chec t.SetUpTool("ls", "LS", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "pre-configure:", "\t@ls -l") @@ -1050,7 +1128,7 @@ func (s *Suite) Test_SimpleCommandChecker_handleForbiddenCommand(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "\t${RUN} mktexlsr; texconfig") @@ -1067,19 +1145,65 @@ func (s *Suite) Test_SimpleCommandChecker_handleCommandVariable(c *check.C) { t.SetUpTool("perl", "PERL5", AtRunTime) t.SetUpTool("perl6", "PERL6", Nowhere) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "PERL5_VARS_CMD=\t${PERL5:Q}", - "PERL5_VARS_CMD=\t${PERL6:Q}") + "PERL5_VARS_CMD=\t${PERL6:Q}", + "", + "pre-configure:", + "\t${PERL5_VARS_CMD} -e 'print 12345'") mklines.Check() // 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:Q}\" tool is used but not added to USE_TOOLS.") } +func (s *Suite) Test_SimpleCommandChecker_handleCommandVariable__parameterized(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + G.Pkg = NewPackage(t.File("category/package")) + t.FinishSetUp() + + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "MY_TOOL.i386=\t${PREFIX}/bin/tool-i386", + "MY_TOOL.x86_64=\t${PREFIX}/bin/tool-x86_64", + "", + "pre-configure:", + "\t${MY_TOOL.amd64} -e 'print 12345'", + "\t${UNKNOWN_TOOL}") + + mklines.Check() + + t.CheckOutputLines( + "WARN: Makefile:8: Unknown shell command \"${UNKNOWN_TOOL}\".", + "WARN: Makefile:8: UNKNOWN_TOOL is used but not defined.") +} + +func (s *Suite) Test_SimpleCommandChecker_handleCommandVariable__followed_by_literal(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + G.Pkg = NewPackage(t.File("category/package")) + t.FinishSetUp() + + mklines := t.NewMkLines("Makefile", + MkCvsID, + "", + "QTDIR=\t${PREFIX}", + "", + "pre-configure:", + "\t${QTDIR}/bin/release") + + mklines.Check() + + t.CheckOutputEmpty() +} + // 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. @@ -1099,7 +1223,7 @@ func (s *Suite) Test_SimpleCommandChecker_handleCommandVariable__from_package(c "", ".include \"extra.mk\"") t.CreateFileLines("category/package/extra.mk", - MkRcsID, + MkCvsID, "PYTHON_BIN=\tmy_cmd") t.FinishSetUp() @@ -1112,7 +1236,7 @@ func (s *Suite) Test_SimpleCommandChecker_handleComment(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, "\t# comment; continuation") mklines.Check() @@ -1128,7 +1252,7 @@ func (s *Suite) Test_SimpleCommandChecker_checkInstallMulti(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("install.mk", - MkRcsID, + MkCvsID, "", "do-install:", "\t${INSTALL_PROGRAM_DIR} -m 0555 -g ${APACHE_GROUP} -o ${APACHE_USER} \\", @@ -1147,7 +1271,7 @@ func (s *Suite) Test_SimpleCommandChecker_checkPaxPe(c *check.C) { t.SetUpVartypes() t.SetUpTool("pax", "PAX", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "do-install:", "\t${RUN} pax -pe ${WRKSRC} ${DESTDIR}${PREFIX}", @@ -1166,7 +1290,7 @@ func (s *Suite) Test_SimpleCommandChecker_checkEchoN(c *check.C) { t.SetUpTool("echo", "ECHO", AtRunTime) t.SetUpTool("echo -n", "ECHO_N", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", "do-install:", "\t${RUN} ${ECHO} -n 'Computing...'", @@ -1182,22 +1306,27 @@ func (s *Suite) Test_SimpleCommandChecker_checkEchoN(c *check.C) { func (s *Suite) Test_ShellProgramChecker_checkConditionalCd(c *check.C) { t := s.Init(c) - t.SetUpTool("ls", "LS", AtRunTime) - t.SetUpTool("printf", "PRINTF", AtRunTime) + t.SetUpTool("ls", "", AtRunTime) + t.SetUpTool("printf", "", AtRunTime) + t.SetUpTool("wc", "", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "pre-configure:", "\t${RUN} while cd ..; do printf .; done", + "\t${RUN} while cd .. && cd ..; do printf .; done", // Unusual, therefore no warning. "\t${RUN} if cd ..; then printf .; fi", "\t${RUN} ! cd ..", - "\t${RUN} if cd .. && cd ..; then printf .; fi") // For code coverage + "\t${RUN} if cd ..; printf 'ok\\n'; then printf .; fi", + "\t${RUN} if cd .. | wc -l; then printf .; fi", // Unusual, therefore no warning. + "\t${RUN} if cd .. && cd ..; then printf .; fi") // Unusual, therefore no warning. mklines.Check() t.CheckOutputLines( "ERROR: Makefile:3: The Solaris /bin/sh cannot handle \"cd\" inside conditionals.", - "ERROR: Makefile:4: The Solaris /bin/sh cannot handle \"cd\" inside conditionals.", - "WARN: Makefile:5: The Solaris /bin/sh does not support negation of shell commands.") + "ERROR: Makefile:5: The Solaris /bin/sh cannot handle \"cd\" inside conditionals.", + "WARN: Makefile:6: The Solaris /bin/sh does not support negation of shell commands.", + "WARN: Makefile:8: The exitcode of \"cd\" at the left of the | operator is ignored.") } func (s *Suite) Test_SimpleCommandChecker_checkRegexReplace(c *check.C) { @@ -1207,7 +1336,7 @@ func (s *Suite) Test_SimpleCommandChecker_checkRegexReplace(c *check.C) { t.SetUpTool("pax", "PAX", AtRunTime) t.SetUpTool("sed", "SED", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "pre-configure:", "\t"+cmd) @@ -1252,6 +1381,59 @@ func (s *Suite) Test_SimpleCommandChecker_checkRegexReplace(c *check.C) { G.Testing = true } +func (s *Suite) Test_SimpleCommandChecker_checkAutoMkdirs(c *check.C) { + t := s.Init(c) + + t.SetUpVartypes() + t.SetUpTool("awk", "AWK", AtRunTime) + t.SetUpTool("cp", "CP", AtRunTime) + t.SetUpTool("echo", "", AtRunTime) + t.SetUpTool("mkdir", "MKDIR", AtRunTime) // This is actually "mkdir -p". + t.SetUpTool("unzip", "UNZIP_CMD", AtRunTime) + + test := func(shellCommand string, diagnostics ...string) { + mklines := t.NewMkLines("filename.mk", + "\t"+shellCommand) + ck := NewShellLineChecker(mklines, mklines.mklines[0]) + + mklines.ForEach(func(mkline *MkLine) { + ck.CheckShellCommandLine(ck.mkline.ShellCommand()) + }) + + t.CheckOutput(diagnostics) + } + + // AUTO_MKDIRS applies only when installing directories. + test("${RUN} ${INSTALL} -c ${WRKSRC}/file ${PREFIX}/bin/", + nil...) + + // TODO: Warn that ${INSTALL} -d can only handle a single directory. + test("${RUN} ${INSTALL} -m 0755 -d ${PREFIX}/first ${PREFIX}/second", + "NOTE: filename.mk:1: You can use \"INSTALLATION_DIRS+= first\" instead of \"${INSTALL} -d\".", + "NOTE: filename.mk:1: You can use \"INSTALLATION_DIRS+= second\" instead of \"${INSTALL} -d\".") + + G.Pkg = NewPackage(t.File("category/pkgbase")) + G.Pkg.Plist.Dirs["share/pkgbase"] = true + + // A directory that is found in the PLIST. + // TODO: Add a test for using this command inside a conditional; + // the note should not appear then. + test("${RUN} ${INSTALL_DATA_DIR} share/pkgbase ${PREFIX}/share/pkgbase", + "NOTE: filename.mk:1: You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= share/pkgbase\" "+ + "instead of \"${INSTALL_DATA_DIR}\".", + "WARN: filename.mk:1: The INSTALL_*_DIR commands can only handle one directory at a time.") + + // Directories from .for loops are too dynamic to be replaced with AUTO_MKDIRS. + // TODO: Expand simple .for loops. + test("${RUN} ${INSTALL_DATA_DIR} ${PREFIX}/${dir}", + "WARN: filename.mk:1: dir is used but not defined.") + + // A directory that is not found in the PLIST would not be created by AUTO_MKDIRS, + // therefore only INSTALLATION_DIRS is suggested. + test("${RUN} ${INSTALL_DATA_DIR} ${PREFIX}/share/other", + "NOTE: filename.mk:1: You can use \"INSTALLATION_DIRS+= share/other\" instead of \"${INSTALL_DATA_DIR}\".") +} + func (s *Suite) Test_ShellProgramChecker_checkSetE__simple_commands(c *check.C) { t := s.Init(c) @@ -1259,7 +1441,7 @@ func (s *Suite) Test_ShellProgramChecker_checkSetE__simple_commands(c *check.C) t.SetUpTool("rm", "", AtRunTime) t.SetUpTool("touch", "", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "pre-configure:", "\techo 1; echo 2; echo 3", "\techo 1; touch file; rm file", @@ -1278,7 +1460,7 @@ func (s *Suite) Test_ShellProgramChecker_checkSetE__compound_commands(c *check.C t.SetUpTool("echo", "", AtRunTime) t.SetUpTool("touch", "", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "pre-configure:", "\ttouch file; for f in file; do echo \"$$f\"; done", "\tfor f in file; do echo \"$$f\"; done; touch file", @@ -1298,7 +1480,7 @@ func (s *Suite) Test_ShellProgramChecker_checkSetE__no_tracing(c *check.C) { t.SetUpTool("touch", "", AtRunTime) mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "pre-configure:", "\ttouch 1; touch 2") t.DisableTracing() @@ -1314,6 +1496,8 @@ func (s *Suite) Test_ShellProgramChecker_canFail(c *check.C) { t := s.Init(c) t.SetUpVartypes() + t.SetUpTool("basename", "", AtRunTime) + t.SetUpTool("dirname", "", AtRunTime) t.SetUpTool("echo", "", AtRunTime) t.SetUpTool("env", "", AtRunTime) t.SetUpTool("grep", "GREP", AtRunTime) @@ -1321,36 +1505,86 @@ func (s *Suite) Test_ShellProgramChecker_canFail(c *check.C) { t.SetUpTool("touch", "", AtRunTime) t.SetUpTool("tr", "tr", AtRunTime) t.SetUpTool("true", "TRUE", AtRunTime) - 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'", - "\tenv | grep '^PATH='") - mklines.Check() + test := func(cmd string, diagnostics ...string) { + mklines := t.NewMkLines("Makefile", + MkCvsID, + "pre-configure:", + "\t"+cmd+" ; echo 'done.'") - t.CheckOutputLines( + mklines.Check() + + t.CheckOutput(diagnostics) + } + + test("socklen=`${GREP} 'expr' ${WRKSRC}/config.h`", + "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon "+ + "(after \"socklen=`${GREP} 'expr' ${WRKSRC}/config.h`\") to separate commands.") + + test("socklen=`${GREP} 'expr' ${WRKSRC}/config.h || ${TRUE}`", + nil...) + + test("socklen=$$(expr 16)", + "WARN: Makefile:3: Invoking subshells via $(...) is not portable enough.", + "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon "+ + "(after \"socklen=$$(expr 16)\") to separate commands.") + + test("socklen=$$(expr 16 || true)", + "WARN: Makefile:3: Invoking subshells via $(...) is not portable enough.") + + test("socklen=$$(expr 16 || ${TRUE})", + "WARN: Makefile:3: Invoking subshells via $(...) is not portable enough.") + + test("${ECHO_MSG} \"Message\"", + nil...) + + test("${FAIL_MSG} \"Failure\"", + "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon "+ + "(after \"${FAIL_MSG} \\\"Failure\\\"\") to separate commands.") + + test("set -x", + "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon "+ + "(after \"set -x\") to separate commands.") + + test("echo 'input' | sed -e s,in,out,", + nil...) + + test("sed -e s,in,out,", + nil...) + + test("sed s,in,out,", + nil...) + + test("grep input", + nil...) + + test("grep pattern file...", + "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon "+ + "(after \"grep pattern file...\") to separate commands.") + + test("touch file", + "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon "+ + "(after \"touch file\") to separate commands.") + + test("echo 'starting'", + nil...) + + test("echo 'logging' > log", "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.") + + test("echo 'to stderr' 1>&2", + nil...) + + test("echo 'hello' | tr -d 'aeiou'", + nil...) + + test("env | grep '^PATH='", + nil...) + + test("basename dir/file", + nil...) + + test("dirname dir/file", + nil...) } diff --git a/pkgtools/pkglint/files/shtokenizer.go b/pkgtools/pkglint/files/shtokenizer.go index a8b0cad7d7e..71bbd806065 100644 --- a/pkgtools/pkglint/files/shtokenizer.go +++ b/pkgtools/pkglint/files/shtokenizer.go @@ -6,7 +6,7 @@ type ShTokenizer struct { parser *MkParser } -func NewShTokenizer(line Line, text string, emitWarnings bool) *ShTokenizer { +func NewShTokenizer(line *Line, text string, emitWarnings bool) *ShTokenizer { // TODO: Switching to NewMkParser is nontrivial since emitWarnings must equal (line != nil). p := MkParser{line, textproc.NewLexer(text), emitWarnings} return &ShTokenizer{&p} @@ -428,7 +428,6 @@ func (p *ShTokenizer) ShToken() *ShToken { lexer := p.parser.lexer initialMark := lexer.Mark() - var atoms []*ShAtom for peek() != nil && peek().Type == shtSpace { skip() @@ -439,20 +438,20 @@ func (p *ShTokenizer) ShToken() *ShToken { return nil } - if atom := peek(); !atom.Type.IsWord() && atom.Quoting != shqSubsh { - return NewShToken(atom.MkText, atom) + if !curr.Type.IsWord() && q != shqSubsh { + return NewShToken(curr.MkText, curr) } + var atoms []*ShAtom for { mark := lexer.Mark() - atom := peek() - if atom != nil && (atom.Type.IsWord() || q != shqPlain || prevQ == shqSubsh) { - skip() - atoms = append(atoms, atom) - continue + peek() + if curr == nil || !curr.Type.IsWord() && q == shqPlain && prevQ != shqSubsh { + lexer.Reset(mark) + break } - lexer.Reset(mark) - break + atoms = append(atoms, curr) + skip() } if q != shqPlain { @@ -460,7 +459,6 @@ func (p *ShTokenizer) ShToken() *ShToken { return nil } - assertf(len(atoms) > 0, "ShTokenizer.ShToken") return NewShToken(lexer.Since(initialMark), atoms...) } diff --git a/pkgtools/pkglint/files/shtokenizer_test.go b/pkgtools/pkglint/files/shtokenizer_test.go index 38c7e44847a..78364bce396 100644 --- a/pkgtools/pkglint/files/shtokenizer_test.go +++ b/pkgtools/pkglint/files/shtokenizer_test.go @@ -580,7 +580,7 @@ func (s *Suite) Test_ShTokenizer__examples_from_fuzzing(c *check.C) { test := func(input string, diagnostics ...string) { mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "\t"+input) mklines.Check() t.CheckOutput(diagnostics) diff --git a/pkgtools/pkglint/files/shtypes.go b/pkgtools/pkglint/files/shtypes.go index 4580ab861bc..e12d320d46a 100644 --- a/pkgtools/pkglint/files/shtypes.go +++ b/pkgtools/pkglint/files/shtypes.go @@ -133,6 +133,8 @@ type ShToken struct { } func NewShToken(mkText string, atoms ...*ShAtom) *ShToken { + assert(mkText != "") + assert(len(atoms) > 0) return &ShToken{mkText, atoms} } diff --git a/pkgtools/pkglint/files/shtypes_test.go b/pkgtools/pkglint/files/shtypes_test.go index ca890e0ec37..aff0a6d3d03 100644 --- a/pkgtools/pkglint/files/shtypes_test.go +++ b/pkgtools/pkglint/files/shtypes_test.go @@ -29,6 +29,13 @@ func (s *Suite) Test_ShQuoting_String(c *check.C) { c.Check(shqDquotBacktSquot.String(), equals, "dbs") } +func (s *Suite) Test_NewShToken__no_atoms(c *check.C) { + t := s.Init(c) + + t.ExpectAssert(func() { NewShToken("", NewShAtom(shtText, "text", shqPlain)) }) + t.ExpectAssert(func() { NewShToken(" ", nil...) }) +} + func (s *Suite) Test_ShToken_String(c *check.C) { tokenizer := NewShTokenizer(dummyLine, "${ECHO} \"hello, world\"", false) diff --git a/pkgtools/pkglint/files/substcontext.go b/pkgtools/pkglint/files/substcontext.go index a648b181972..81e84c758ee 100644 --- a/pkgtools/pkglint/files/substcontext.go +++ b/pkgtools/pkglint/files/substcontext.go @@ -44,7 +44,7 @@ func (st *SubstContextStats) Or(other SubstContextStats) { st.seenTransform = st.seenTransform || other.seenTransform } -func (ctx *SubstContext) Process(mkline MkLine) { +func (ctx *SubstContext) Process(mkline *MkLine) { switch { case mkline.IsEmpty(): ctx.Finish(mkline) @@ -55,7 +55,7 @@ func (ctx *SubstContext) Process(mkline MkLine) { } } -func (ctx *SubstContext) Varassign(mkline MkLine) { +func (ctx *SubstContext) Varassign(mkline *MkLine) { if trace.Tracing { trace.Stepf("SubstContext.Varassign curr=%v all=%v", ctx.curr, ctx.inAllBranches) } @@ -181,7 +181,7 @@ func (ctx *SubstContext) Varassign(mkline MkLine) { } } -func (ctx *SubstContext) Directive(mkline MkLine) { +func (ctx *SubstContext) Directive(mkline *MkLine) { if ctx.id == "" { return } @@ -214,7 +214,7 @@ func (ctx *SubstContext) IsComplete() bool { return ctx.stage != "" && ctx.curr.seenFiles && ctx.curr.seenTransform } -func (ctx *SubstContext) Finish(mkline MkLine) { +func (ctx *SubstContext) Finish(mkline *MkLine) { if ctx.id == "" { return } @@ -233,21 +233,21 @@ func (ctx *SubstContext) Finish(mkline MkLine) { *ctx = *NewSubstContext() } -func (*SubstContext) dupString(mkline MkLine, pstr *string, varname, value string) { +func (*SubstContext) dupString(mkline *MkLine, pstr *string, varname, value string) { if *pstr != "" { mkline.Warnf("Duplicate definition of %q.", varname) } *pstr = value } -func (*SubstContext) dupBool(mkline MkLine, flag *bool, varname string, op MkOperator, value string) { +func (*SubstContext) dupBool(mkline *MkLine, flag *bool, varname string, op MkOperator, value string) { if *flag && op != opAssignAppend { mkline.Warnf("All but the first %q lines should use the \"+=\" operator.", varname) } *flag = true } -func (ctx *SubstContext) suggestSubstVars(mkline MkLine) { +func (ctx *SubstContext) suggestSubstVars(mkline *MkLine) { tokens, _ := splitIntoShellTokens(mkline.Line, mkline.Value()) for _, token := range tokens { diff --git a/pkgtools/pkglint/files/substcontext_test.go b/pkgtools/pkglint/files/substcontext_test.go index fbb07a8f867..5178f1c007a 100644 --- a/pkgtools/pkglint/files/substcontext_test.go +++ b/pkgtools/pkglint/files/substcontext_test.go @@ -305,7 +305,7 @@ func (s *Suite) Test_SubstContext__pre_patch(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("os.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+= os", "SUBST_STAGE.os= pre-patch", @@ -326,7 +326,7 @@ func (s *Suite) Test_SubstContext__post_patch(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("os.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+= os", "SUBST_STAGE.os= post-patch", @@ -395,7 +395,7 @@ func (s *Suite) Test_SubstContext__adjacent(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("os.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+= 1", "SUBST_STAGE.1= pre-configure", @@ -420,7 +420,7 @@ func (s *Suite) Test_SubstContext__do_patch(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("os.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+= os", "SUBST_STAGE.os= do-patch", @@ -443,7 +443,7 @@ func (s *Suite) Test_SubstContext__SUBST_VARS_defined_in_block(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("os.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+= os", "SUBST_STAGE.os= pre-configure", @@ -468,7 +468,7 @@ func (s *Suite) Test_SubstContext__SUBST_VARS_in_next_paragraph(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("os.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+= os", "SUBST_STAGE.os= pre-configure", @@ -491,7 +491,7 @@ func (s *Suite) Test_SubstContext__multiple_SUBST_VARS(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("os.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+= os", "SUBST_STAGE.os= pre-configure", @@ -513,7 +513,7 @@ func (s *Suite) Test_SubstContext_Directive__before_SUBST_CLASSES(c *check.C) { t.DisableTracing() // Just for branch coverage. mklines := t.NewMkLines("os.mk", - MkRcsID, + MkCvsID, "", ".if 0", ".endif", @@ -536,7 +536,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars(c *check.C) { t.SetUpTool("sh", "SH", AtRunTime) mklines := t.NewMkLines("subst.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+=\t\ttest", "SUBST_STAGE.test=\tpre-configure", @@ -626,7 +626,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__plus(c *check.C) { t.SetUpTool("sh", "SH", AtRunTime) mklines := t.NewMkLines("subst.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+=\t\tgtk+", "SUBST_STAGE.gtk+ =\tpre-configure", @@ -667,7 +667,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__autofix_realign_paragraph(c t.Chdir(".") mklines := t.SetUpFileMkLines("subst.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+=\t\tpfx", "SUBST_STAGE.pfx=\tpre-configure", @@ -694,7 +694,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__autofix_realign_paragraph(c "with \"SUBST_VARS.pfx+=\\tPREFIX\".") t.CheckFileLinesDetab("subst.mk", - "# $NetBSD: substcontext_test.go,v 1.26 2019/06/10 19:51:57 rillig Exp $", + "# $NetBSD: substcontext_test.go,v 1.27 2019/06/30 20:56:19 rillig Exp $", "", "SUBST_CLASSES+= pfx", "SUBST_STAGE.pfx= pre-configure", @@ -710,7 +710,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__autofix_plus_sed(c *check.C) t.Chdir(".") mklines := t.SetUpFileMkLines("subst.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+=\t\tpfx", "SUBST_STAGE.pfx=\tpre-configure", @@ -733,7 +733,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__autofix_plus_sed(c *check.C) "with \"SUBST_VARS.pfx=\\t\\tPREFIX\".") t.CheckFileLinesDetab("subst.mk", - "# $NetBSD: substcontext_test.go,v 1.26 2019/06/10 19:51:57 rillig Exp $", + "# $NetBSD: substcontext_test.go,v 1.27 2019/06/30 20:56:19 rillig Exp $", "", "SUBST_CLASSES+= pfx", "SUBST_STAGE.pfx= pre-configure", @@ -752,7 +752,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__autofix_plus_vars(c *check.C t.Chdir(".") mklines := t.SetUpFileMkLines("subst.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+=\tid", "SUBST_STAGE.id=\tpre-configure", @@ -767,7 +767,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__autofix_plus_vars(c *check.C "with \"SUBST_VARS.id=\\tPREFIX\".") t.CheckFileLinesDetab("subst.mk", - "# $NetBSD: substcontext_test.go,v 1.26 2019/06/10 19:51:57 rillig Exp $", + "# $NetBSD: substcontext_test.go,v 1.27 2019/06/30 20:56:19 rillig Exp $", "", "SUBST_CLASSES+= id", "SUBST_STAGE.id= pre-configure", @@ -786,7 +786,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__autofix_indentation(c *check t.Chdir(".") mklines := t.SetUpFileMkLines("subst.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+=\t\t\tfix-paths", "SUBST_STAGE.fix-paths=\t\tpre-configure", @@ -801,7 +801,7 @@ func (s *Suite) Test_SubstContext_suggestSubstVars__autofix_indentation(c *check "with \"SUBST_VARS.fix-paths=\\t\\tPREFIX\".") t.CheckFileLinesDetab("subst.mk", - "# $NetBSD: substcontext_test.go,v 1.26 2019/06/10 19:51:57 rillig Exp $", + "# $NetBSD: substcontext_test.go,v 1.27 2019/06/30 20:56:19 rillig Exp $", "", "SUBST_CLASSES+= fix-paths", "SUBST_STAGE.fix-paths= pre-configure", @@ -866,7 +866,7 @@ func (s *Suite) Test_SubstContext__unusual_variable_order(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("subst.mk", - MkRcsID, + MkCvsID, "", "SUBST_CLASSES+=\t\tid", "SUBST_SED.id=\t\t-e /deleteme/d", diff --git a/pkgtools/pkglint/files/tools.go b/pkgtools/pkglint/files/tools.go index 0aecc1e6b67..6222a3228c9 100644 --- a/pkgtools/pkglint/files/tools.go +++ b/pkgtools/pkglint/files/tools.go @@ -135,7 +135,7 @@ func NewTools() *Tools { // // After this tool is added to USE_TOOLS, it may be used by this name // (e.g. "awk") or by its variable (e.g. ${AWK}). -func (tr *Tools) Define(name, varname string, mkline MkLine) *Tool { +func (tr *Tools) Define(name, varname string, mkline *MkLine) *Tool { if trace.Tracing { trace.Stepf("Tools.Define: %q %q in %s", name, varname, mkline) } @@ -150,7 +150,7 @@ func (tr *Tools) Define(name, varname string, mkline MkLine) *Tool { } func (tr *Tools) def(name, varname string, mustUseVarForm bool, validity Validity, aliases []string) *Tool { - assertf(tr.IsValidToolName(name), "Invalid tool name %q", name) + assert(tr.IsValidToolName(name)) fresh := Tool{name, varname, mustUseVarForm, validity, aliases} @@ -225,7 +225,7 @@ func (tr *Tools) Trace() { // // If addToUseTools is true, a USE_TOOLS line makes a tool immediately // usable. This should only be done if the current line is unconditional. -func (tr *Tools) ParseToolLine(mklines MkLines, mkline MkLine, fromInfrastructure bool, addToUseTools bool) { +func (tr *Tools) ParseToolLine(mklines *MkLines, mkline *MkLine, fromInfrastructure bool, addToUseTools bool) { switch { case mkline.IsVarassign(): @@ -303,7 +303,7 @@ func (tr *Tools) addAlias(tool *Tool, alias string) { // This can be done only in the pkgsrc infrastructure files, where the // actual definition is assumed to be in some other file. In packages // though, this assumption cannot be made and pkglint needs to be strict. -func (tr *Tools) parseUseTools(mkline MkLine, createIfAbsent bool, addToUseTools bool) { +func (tr *Tools) parseUseTools(mkline *MkLine, createIfAbsent bool, addToUseTools bool) { value := mkline.Value() if containsVarRef(value) { return @@ -391,7 +391,7 @@ func (tr *Tools) Usable(tool *Tool, time ToolTime) bool { } func (tr *Tools) Fallback(other *Tools) { - assertf(tr.fallback == nil, "Tools.Fallback must only be called once.") + assert(tr.fallback == nil) // Must only be called once. tr.fallback = other } diff --git a/pkgtools/pkglint/files/tools_test.go b/pkgtools/pkglint/files/tools_test.go index f1e6554f689..1d7cec63635 100644 --- a/pkgtools/pkglint/files/tools_test.go +++ b/pkgtools/pkglint/files/tools_test.go @@ -40,7 +40,7 @@ func (s *Suite) Test_Tools_ParseToolLine__opsys(c *check.C) { t.SetUpTool("tool1", "", Nowhere) t.SetUpVartypes() t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "USE_TOOLS.NetBSD+=\ttool1") @@ -55,7 +55,7 @@ func (s *Suite) Test_Tools_ParseToolLine__invalid_tool_name(c *check.C) { t.SetUpVartypes() mklines := t.NewMkLines("Makefile", - MkRcsID, + MkCvsID, "", ".for t in abc ${UNDEFINED}", "TOOLS_CREATE+=\t\t${t}", @@ -78,7 +78,7 @@ func (s *Suite) Test_Tools_parseUseTools(c *check.C) { t.SetUpPkgsrc() t.CreateFileLines("mk/triple-tool.mk", - MkRcsID, + MkCvsID, "", "USE_TOOLS+=\tunknown unknown unknown") t.FinishSetUp() @@ -128,7 +128,7 @@ func (s *Suite) Test_Tools__USE_TOOLS_predefined_sed(c *check.C) { t.CreateFileLines("mk/tools/defaults.mk", "_TOOLS_VARNAME.sed=\tSED") t.CreateFileLines("module.mk", - MkRcsID, + MkCvsID, "", "do-build:", "\t${SED} < input > output", @@ -174,12 +174,12 @@ func (s *Suite) Test_Tools__load_from_infrastructure(c *check.C) { dummyMklines := t.NewMkLines("dummy.mk") createMklines := t.NewMkLines("create.mk", - MkRcsID, + MkCvsID, "TOOLS_CREATE+= load", "TOOLS_CREATE+= run", "TOOLS_CREATE+= nowhere") - createMklines.ForEach(func(mkline MkLine) { + createMklines.ForEach(func(mkline *MkLine) { tools.ParseToolLine(createMklines, mkline, true, false) }) @@ -197,12 +197,12 @@ func (s *Suite) Test_Tools__load_from_infrastructure(c *check.C) { // The variable name RUN is reserved by pkgsrc, therefore RUN_CMD. varnamesMklines := t.NewMkLines("varnames.mk", - MkRcsID, + MkCvsID, "_TOOLS_VARNAME.load= LOAD", "_TOOLS_VARNAME.run= RUN_CMD", "_TOOLS_VARNAME.nowhere= NOWHERE") - varnamesMklines.ForEach(func(mkline MkLine) { + varnamesMklines.ForEach(func(mkline *MkLine) { tools.ParseToolLine(varnamesMklines, mkline, true, false) }) @@ -314,7 +314,7 @@ func (s *Suite) Test_Tools__builtin_mk(c *check.C) { // may be used in any file at load time. mklines := t.SetUpFileMkLines("category/package/builtin.mk", - MkRcsID, + MkCvsID, "", "VAR!= ${ECHO} 'too early'", "VAR!= ${LOAD} 'too early'", @@ -347,7 +347,7 @@ func (s *Suite) Test_Tools__implicit_definition_in_bsd_pkg_mk(c *check.C) { t.SetUpPkgsrc() t.SetUpCommandLine("-Wall,no-space") t.CreateFileLines("mk/tools/defaults.mk", - MkRcsID) // None + MkCvsID) // None t.CreateFileLines("mk/bsd.prefs.mk", "USE_TOOLS+= load") t.CreateFileLines("mk/bsd.pkg.mk", @@ -368,7 +368,7 @@ func (s *Suite) Test_Tools__both_prefs_and_pkg_mk(c *check.C) { t.SetUpPkgsrc() t.SetUpCommandLine("-Wall,no-space") t.CreateFileLines("mk/tools/defaults.mk", - MkRcsID) + MkCvsID) t.CreateFileLines("mk/bsd.prefs.mk", "USE_TOOLS+= both") t.CreateFileLines("mk/bsd.pkg.mk", @@ -458,7 +458,7 @@ func (s *Suite) Test_Tools__var(c *check.C) { t.FinishSetUp() mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "", "pre-configure:", "\t${LN} from to") @@ -552,16 +552,14 @@ func (s *Suite) Test_Tools_Fallback__called_twice(c *check.C) { tools.Fallback(fallback) - t.ExpectPanic( - func() { tools.Fallback(fallback) }, - "Pkglint internal error: Tools.Fallback must only be called once.") + t.ExpectAssert(func() { tools.Fallback(fallback) }) } func (s *Suite) Test_Tools__aliases(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("mk/tools/replace.mk", - MkRcsID, + MkCvsID, "TOOLS_CREATE+=\tsed", "TOOLS_PATH.sed=\t/bin/sed", "", @@ -570,7 +568,7 @@ func (s *Suite) Test_Tools__aliases(c *check.C) { "TOOLS_ALIASES.gsed=\tsed ${tool}") infraTools := NewTools() - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { infraTools.ParseToolLine(mklines, mkline, false, false) }) @@ -595,7 +593,7 @@ func (s *Suite) Test_Tools__aliases_in_for_loop(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("mk/tools/replace.mk", - MkRcsID, + MkCvsID, "_TOOLS_GREP=\tgrep egrep fgrep", "TOOLS_CREATE+=\tgrep egrep fgrep ggrep", ".for t in ${_TOOLS_GREP}", @@ -685,7 +683,7 @@ func (s *Suite) Test_Tools_IsValidToolName(c *check.C) { t.SetUpTool("[", "", AtRunTime) t.SetUpTool("echo -n", "ECHO_N", AtRunTime) mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, "", "USE_TOOLS+=\t[") diff --git a/pkgtools/pkglint/files/toplevel.go b/pkgtools/pkglint/files/toplevel.go index f8bdb12687e..8a72a372882 100644 --- a/pkgtools/pkglint/files/toplevel.go +++ b/pkgtools/pkglint/files/toplevel.go @@ -35,7 +35,7 @@ func CheckdirToplevel(dir string) { } } -func (ctx *Toplevel) checkSubdir(mkline MkLine) { +func (ctx *Toplevel) checkSubdir(mkline *MkLine) { subdir := mkline.Value() if mkline.IsCommentedVarassign() && (mkline.VarassignComment() == "#" || mkline.VarassignComment() == "") { diff --git a/pkgtools/pkglint/files/toplevel_test.go b/pkgtools/pkglint/files/toplevel_test.go index 9477b559067..c4608823466 100644 --- a/pkgtools/pkglint/files/toplevel_test.go +++ b/pkgtools/pkglint/files/toplevel_test.go @@ -6,7 +6,7 @@ func (s *Suite) Test_CheckdirToplevel(c *check.C) { t := s.Init(c) t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "SUBDIR+= x11", "SUBDIR+=\tarchivers", @@ -39,7 +39,7 @@ func (s *Suite) Test_Toplevel_checkSubdir__sorting_x11(c *check.C) { t := s.Init(c) t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "SUBDIR+=\tx11", "SUBDIR+=\tsysutils", @@ -60,7 +60,7 @@ func (s *Suite) Test_Toplevel_checkSubdir__commented_without_reason(c *check.C) t := s.Init(c) t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "#SUBDIR+=\taaa", "#SUBDIR+=\tbbb\t#", @@ -81,15 +81,15 @@ func (s *Suite) Test_CheckdirToplevel__recursive(c *check.C) { t := s.Init(c) t.CreateFileLines("mk/misc/category.mk", - MkRcsID) + MkCvsID) t.SetUpPackage("category/package", "UNKNOWN=\tvalue") t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "SUBDIR+=\tcategory") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tThe category", "", @@ -109,15 +109,15 @@ func (s *Suite) Test_CheckdirToplevel__recursive_inter_package(c *check.C) { t := s.Init(c) t.CreateFileLines("mk/misc/category.mk", - MkRcsID) + MkCvsID) t.SetUpPackage("category/package", "UNKNOWN=\tvalue") t.CreateFileLines("Makefile", - MkRcsID, + MkCvsID, "", "SUBDIR+=\tcategory") t.CreateFileLines("category/Makefile", - MkRcsID, + MkCvsID, "", "COMMENT=\tThe category", "", diff --git a/pkgtools/pkglint/files/trace/tracing.go b/pkgtools/pkglint/files/trace/tracing.go index 947f5ad2d09..926344cbdf5 100644 --- a/pkgtools/pkglint/files/trace/tracing.go +++ b/pkgtools/pkglint/files/trace/tracing.go @@ -78,7 +78,7 @@ func (t *Tracer) Call(args ...interface{}) func() { return t.traceCall(args...) } -// http://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go +// https://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go func isNil(a interface{}) bool { defer func() { _ = recover() diff --git a/pkgtools/pkglint/files/util.go b/pkgtools/pkglint/files/util.go index 68660eabf27..55a9338dc90 100644 --- a/pkgtools/pkglint/files/util.go +++ b/pkgtools/pkglint/files/util.go @@ -9,6 +9,7 @@ import ( "os" "path" "path/filepath" + "reflect" "regexp" "sort" "strconv" @@ -159,6 +160,30 @@ func assertNil(err error, format string, args ...interface{}) { } } +func assertNotNil(obj interface{}) { + + // https://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go + isNil := func() bool { + defer func() { _ = recover() }() + return reflect.ValueOf(obj).IsNil() + } + + if obj == nil || isNil() { + panic("Pkglint internal error: unexpected nil pointer") + } +} + +// assert 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 dummyLine.Fatalf. +func assert(cond bool) { + if !cond { + panic("Pkglint internal error") + } +} + // assertf checks that the condition is true. Otherwise it terminates the // process with a fatal error message, prefixed with "Pkglint internal error". // @@ -177,7 +202,7 @@ func isEmptyDir(filename string) bool { dirents, err := ioutil.ReadDir(filename) if err != nil { - return true + return true // XXX: Why not false? } for _, dirent := range dirents { @@ -233,46 +258,47 @@ func dirglob(dirname string) []string { // Checks whether a file is already committed to the CVS repository. func isCommitted(filename string) bool { - lines := G.loadCvsEntries(filename) - if lines == nil { - return false - } - needle := "/" + path.Base(filename) + "/" - for _, line := range lines.Lines { - if hasPrefix(line.Text, needle) { - return true - } - } - return false + entries := G.loadCvsEntries(filename) + _, found := entries[path.Base(filename)] + return found } +// isLocallyModified tests whether a file (not a directory) is modified, +// as seen by CVS. +// +// There is no corresponding test for Git (as used by pkgsrc-wip) since that +// is more difficult to implement than simply reading a CVS/Entries file. func isLocallyModified(filename string) bool { - baseName := path.Base(filename) - - lines := G.loadCvsEntries(filename) - if lines == nil { + entries := G.loadCvsEntries(filename) + entry, found := entries[path.Base(filename)] + if !found { return false } - for _, line := range lines.Lines { - fields := strings.Split(line.Text, "/") - if 3 < len(fields) && fields[1] == baseName { - st, err := os.Stat(filename) - if err != nil { - return true - } - - // Following http://cvsman.com/cvs-1.12.12/cvs_19.php, format both timestamps. - cvsModTime := fields[3] - fsModTime := st.ModTime().UTC().Format(time.ANSIC) - if trace.Tracing { - trace.Stepf("cvs.time=%q fs.time=%q", cvsModTime, fsModTime) - } + st, err := os.Stat(filename) + if err != nil { + return true + } - return cvsModTime != fsModTime - } + // Following http://cvsman.com/cvs-1.12.12/cvs_19.php, format both timestamps. + cvsModTime := entry.Timestamp + fsModTime := st.ModTime().UTC().Format(time.ANSIC) + if trace.Tracing { + trace.Stepf("cvs.time=%q fs.time=%q", cvsModTime, fsModTime) } - return false + + return cvsModTime != fsModTime +} + +// CvsEntry is one of the entries in a CVS/Entries file. +// +// See http://cvsman.com/cvs-1.12.12/cvs_19.php. +type CvsEntry struct { + Name string + Revision string + Timestamp string + Options string + TagDate string } // Returns the number of columns that a string occupies when printed with @@ -488,6 +514,34 @@ func cleanpath(filename string) string { return strings.Join(parts, "/") } +func pathContains(haystack, needle string) bool { + n0 := needle[0] + for i := 0; i < 1+len(haystack)-len(needle); i++ { + if haystack[i] == n0 && hasPrefix(haystack[i:], needle) { + if i == 0 || haystack[i-1] == '/' { + if i+len(needle) == len(haystack) || haystack[i+len(needle)] == '/' { + return true + } + } + } + } + return false +} + +func pathContainsDir(haystack, needle string) bool { + n0 := needle[0] + for i := 0; i < 1+len(haystack)-len(needle); i++ { + if haystack[i] == n0 && hasPrefix(haystack[i:], needle) { + if i == 0 || haystack[i-1] == '/' { + if i+len(needle) < len(haystack) && haystack[i+len(needle)] == '/' { + return true + } + } + } + } + return false +} + func containsVarRef(s string) bool { return contains(s, "${") } @@ -561,26 +615,26 @@ func (o *Once) check(key uint64) bool { // TODO: Merge this code with Var, which defines essentially the // same features. type Scope struct { - firstDef map[string]MkLine // TODO: Can this be removed? - lastDef map[string]MkLine + firstDef map[string]*MkLine // TODO: Can this be removed? + lastDef map[string]*MkLine value map[string]string - used map[string]MkLine + used map[string]*MkLine usedAtLoadTime map[string]bool fallback map[string]string } func NewScope() Scope { return Scope{ - make(map[string]MkLine), - make(map[string]MkLine), + make(map[string]*MkLine), + make(map[string]*MkLine), make(map[string]string), - make(map[string]MkLine), + make(map[string]*MkLine), make(map[string]bool), make(map[string]string)} } // Define marks the variable and its canonicalized form as defined. -func (s *Scope) Define(varname string, mkline MkLine) { +func (s *Scope) Define(varname string, mkline *MkLine) { def := func(name string) { if s.firstDef[name] == nil { s.firstDef[name] = mkline @@ -621,7 +675,7 @@ func (s *Scope) Fallback(varname string, value string) { } // Use marks the variable and its canonicalized form as used. -func (s *Scope) Use(varname string, line MkLine, time vucTime) { +func (s *Scope) Use(varname string, line *MkLine, time VucTime) { use := func(name string) { if s.used[name] == nil { s.used[name] = line @@ -629,7 +683,7 @@ func (s *Scope) Use(varname string, line MkLine, time vucTime) { trace.Step2("Using %q in %s", name, line.String()) } } - if time == vucTimeLoad { + if time == VucLoadTime { s.usedAtLoadTime[name] = true } } @@ -642,7 +696,7 @@ func (s *Scope) Use(varname string, line MkLine, time vucTime) { // - defined, // - mentioned in a commented variable assignment, // - mentioned in a documentation comment. -func (s *Scope) Mentioned(varname string) MkLine { +func (s *Scope) Mentioned(varname string) *MkLine { return s.firstDef[varname] } @@ -703,7 +757,7 @@ func (s *Scope) UsedAtLoadTime(varname string) bool { // value, and the including file later overrides that value. Or the other way // round: the including file sets a value first, and the included file then // assigns a default value using ?=. -func (s *Scope) FirstDefinition(varname string) MkLine { +func (s *Scope) FirstDefinition(varname string) *MkLine { mkline := s.firstDef[varname] if mkline != nil && mkline.IsVarassign() { lastLine := s.LastDefinition(varname) @@ -724,7 +778,7 @@ func (s *Scope) FirstDefinition(varname string) MkLine { // value, and the including file later overrides that value. Or the other way // round: the including file sets a value first, and the included file then // assigns a default value using ?=. -func (s *Scope) LastDefinition(varname string) MkLine { +func (s *Scope) LastDefinition(varname string) *MkLine { mkline := s.lastDef[varname] if mkline != nil && mkline.IsVarassign() { return mkline @@ -735,8 +789,8 @@ func (s *Scope) LastDefinition(varname string) MkLine { // Commented returns whether the variable has only been defined in commented // variable assignments. These are ignored by bmake but used heavily in // mk/defaults/mk.conf for documentation. -func (s *Scope) Commented(varname string) MkLine { - var mklines []MkLine +func (s *Scope) Commented(varname string) *MkLine { + var mklines []*MkLine if first := s.firstDef[varname]; first != nil { mklines = append(mklines, first) } @@ -759,7 +813,7 @@ func (s *Scope) Commented(varname string) MkLine { return nil } -func (s *Scope) FirstUse(varname string) MkLine { +func (s *Scope) FirstUse(varname string) *MkLine { return s.used[varname] } @@ -908,7 +962,7 @@ type fileCacheEntry struct { count int key string options LoadOptions - lines Lines + lines *Lines } func NewFileCache(size int) *FileCache { @@ -919,7 +973,7 @@ func NewFileCache(size int) *FileCache { 0} } -func (c *FileCache) Put(filename string, options LoadOptions, lines Lines) { +func (c *FileCache) Put(filename string, options LoadOptions, lines *Lines) { key := c.key(filename) entry := c.mapping[key] @@ -973,14 +1027,14 @@ func (c *FileCache) removeOldEntries() { } } -func (c *FileCache) Get(filename string, options LoadOptions) Lines { +func (c *FileCache) Get(filename string, options LoadOptions) *Lines { key := c.key(filename) entry, found := c.mapping[key] if found && entry.options == options { c.hits++ entry.count++ - lines := make([]Line, entry.lines.Len()) + lines := make([]*Line, entry.lines.Len()) for i, line := range entry.lines.Lines { lines[i] = NewLineMulti(filename, int(line.firstLine), int(line.lastLine), line.Text, line.raw) } @@ -1164,6 +1218,48 @@ func joinSkipEmptyOxford(conn string, elements ...string) string { return strings.Join(nonempty, ", ") } +type pathMatcher struct { + matchType pathMatchType + pattern string + originalPattern string +} + +func newPathMatcher(pattern string) *pathMatcher { + assert(strings.IndexByte(pattern, '[') == -1) + assert(strings.IndexByte(pattern, '?') == -1) + + stars := strings.Count(pattern, "*") + assert(stars == 0 || stars == 1) + switch { + case stars == 0: + return &pathMatcher{pmExact, pattern, pattern} + case pattern[0] == '*': + return &pathMatcher{pmSuffix, pattern[1:], pattern} + default: + assert(pattern[len(pattern)-1] == '*') + return &pathMatcher{pmPrefix, pattern[:len(pattern)-1], pattern} + } +} + +func (m pathMatcher) matches(subject string) bool { + switch m.matchType { + case pmPrefix: + return hasPrefix(subject, m.pattern) + case pmSuffix: + return hasSuffix(subject, m.pattern) + default: + return subject == m.pattern + } +} + +type pathMatchType uint8 + +const ( + pmExact pathMatchType = iota + pmPrefix + pmSuffix +) + // StringInterner collects commonly used strings to avoid wasting heap memory // by duplicated strings. type StringInterner struct { diff --git a/pkgtools/pkglint/files/util_test.go b/pkgtools/pkglint/files/util_test.go index abfef4d88cf..fc29cefdb9c 100644 --- a/pkgtools/pkglint/files/util_test.go +++ b/pkgtools/pkglint/files/util_test.go @@ -18,6 +18,19 @@ func (s *Suite) Test_assertNil(c *check.C) { "Pkglint internal error: Oops: unexpected error") } +func (s *Suite) Test_assertNotNil(c *check.C) { + t := s.Init(c) + + assertNotNil("this string is not nil") + + t.ExpectPanic( + func() { assertNotNil(nil) }, + "Pkglint internal error: unexpected nil pointer") + t.ExpectPanic( + func() { var ptr *string; assertNotNil(ptr) }, + "Pkglint internal error: unexpected nil pointer") +} + func (s *Suite) Test_YesNoUnknown_String(c *check.C) { c.Check(yes.String(), equals, "yes") c.Check(no.String(), equals, "no") @@ -175,6 +188,80 @@ func (s *Suite) Test_relpath__quick(c *check.C) { test("some/dir/.", ".", "../..") } +func (s *Suite) Test_pathContains(c *check.C) { + t := s.Init(c) + + test := func(haystack, needle string, expected bool) { + actual := pathContains(haystack, needle) + t.Check(actual, equals, expected) + } + + testPanic := func(haystack, needle string) { + t.c.Check( + func() { _ = pathContains(haystack, needle) }, + check.PanicMatches, + `runtime error: index out of range`) + } + + testPanic("", "") + testPanic("a", "") + testPanic("a/b/c", "") + + test("a", "a", true) + test("a", "b", false) + test("a", "A", false) + test("a/b/c", "a", true) + test("a/b/c", "b", true) + test("a/b/c", "c", true) + test("a/b/c", "a/b", true) + test("a/b/c", "b/c", true) + test("a/b/c", "a/b/c", true) + test("aa/bb/cc", "a/b", false) + test("aa/bb/cc", "a/bb", false) + test("aa/bb/cc", "aa/b", false) + test("aa/bb/cc", "aa/bb", true) + test("aa/bb/cc", "a", false) + test("aa/bb/cc", "b", false) + test("aa/bb/cc", "c", false) +} + +func (s *Suite) Test_pathContainsDir(c *check.C) { + t := s.Init(c) + + test := func(haystack, needle string, expected bool) { + actual := pathContainsDir(haystack, needle) + t.Check(actual, equals, expected) + } + + testPanic := func(haystack, needle string) { + t.c.Check( + func() { _ = pathContainsDir(haystack, needle) }, + check.PanicMatches, + `runtime error: index out of range`) + } + + testPanic("", "") + testPanic("a", "") + testPanic("a/b/c", "") + + test("a", "a", false) + test("a", "b", false) + test("a", "A", false) + test("a/b/c", "a", true) + test("a/b/c", "b", true) + test("a/b/c", "c", false) + test("a/b/c", "a/b", true) + test("a/b/c", "b/c", false) + test("a/b/c", "a/b/c", false) + test("aa/bb/cc", "a/b", false) + test("aa/bb/cc", "a/bb", false) + test("aa/bb/cc", "aa/b", false) + test("aa/bb/cc", "aa/bb", true) + test("aa/bb/cc", "a", false) + test("aa/bb/cc", "b", false) + test("aa/bb/cc", "c", false) +} + func (s *Suite) Test_fileExists(c *check.C) { t := s.Init(c) @@ -394,7 +481,6 @@ func (s *Suite) Test_isLocallyModified(c *check.C) { modified := t.CreateFileLines("modified") t.CreateFileLines("CVS/Entries", - "//", // Just for code coverage. "/unmodified//"+modTime.Format(time.ANSIC)+"//", "/modified//"+modTime.Format(time.ANSIC)+"//", "/enoent//"+modTime.Format(time.ANSIC)+"//") @@ -448,7 +534,7 @@ func (s *Suite) Test_Scope_Used(c *check.C) { scope := NewScope() mkline := t.NewMkLine("file.mk", 1, "\techo ${VAR.param}") - scope.Use("VAR.param", mkline, vucTimeRun) + scope.Use("VAR.param", mkline, VucRunTime) c.Check(scope.Used("VAR.param"), equals, true) c.Check(scope.Used("VAR.other"), equals, false) @@ -500,7 +586,7 @@ func (s *Suite) Test_Scope_LastValue(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("file.mk", - MkRcsID, + MkCvsID, "VAR=\tfirst", "VAR=\tsecond", ".if 1", @@ -637,7 +723,7 @@ func (s *Suite) Test_FileCache(c *check.C) { cache := NewFileCache(3) lines := t.NewLines("Makefile", - MkRcsID, + MkCvsID, "# line 2") c.Check(cache.Get("Makefile", 0), check.IsNil) @@ -648,13 +734,13 @@ func (s *Suite) Test_FileCache(c *check.C) { c.Check(cache.Get("Makefile", MustSucceed|LogErrors), check.IsNil) // Wrong LoadOptions. linesFromCache := cache.Get("Makefile", 0) - c.Check(linesFromCache.FileName, equals, "Makefile") + c.Check(linesFromCache.Filename, equals, "Makefile") c.Check(linesFromCache.Lines, check.HasLen, 2) c.Check(linesFromCache.Lines[0].Filename, equals, "Makefile") // Cache keys are normalized using path.Clean. linesFromCache2 := cache.Get("./Makefile", 0) - c.Check(linesFromCache2.FileName, equals, "./Makefile") + c.Check(linesFromCache2.Filename, equals, "./Makefile") c.Check(linesFromCache2.Lines, check.HasLen, 2) c.Check(linesFromCache2.Lines[0].Filename, equals, "./Makefile") @@ -700,7 +786,7 @@ func (s *Suite) Test_FileCache_removeOldEntries__branch_coverage(c *check.C) { G.Testing = false lines := t.NewLines("filename.mk", - MkRcsID) + MkCvsID) cache := NewFileCache(3) cache.Put("filename1.mk", 0, lines) cache.Put("filename2.mk", 0, lines) @@ -721,7 +807,7 @@ func (s *Suite) Test_FileCache_removeOldEntries__no_tracing(c *check.C) { t.DisableTracing() lines := t.NewLines("filename.mk", - MkRcsID) + MkCvsID) cache := NewFileCache(3) cache.Put("filename1.mk", 0, lines) cache.Put("filename2.mk", 0, lines) @@ -738,7 +824,7 @@ func (s *Suite) Test_FileCache_removeOldEntries__zero_capacity(c *check.C) { t := s.Init(c) lines := t.NewLines("filename.mk", - MkRcsID) + MkCvsID) cache := NewFileCache(1) cache.Put("filename1.mk", 0, lines) @@ -751,7 +837,7 @@ func (s *Suite) Test_FileCache_Evict__sort(c *check.C) { t := s.Init(c) lines := t.NewLines("filename.mk", - MkRcsID) + MkCvsID) cache := NewFileCache(10) cache.Put("filename0.mk", 0, lines) cache.Put("filename1.mk", 0, lines) @@ -939,6 +1025,56 @@ func (s *Suite) Test_joinSkipEmptyOxford(c *check.C) { "one, two, and three") } +func (s *Suite) Test_newPathMatcher(c *check.C) { + t := s.Init(c) + + test := func(pattern string, matchType pathMatchType, matchPattern string) { + c.Check(*newPathMatcher(pattern), equals, pathMatcher{matchType, matchPattern, pattern}) + } + + testPanic := func(pattern string) { + t.ExpectPanic( + func() { _ = newPathMatcher(pattern) }, + "Pkglint internal error") + } + + testPanic("*.[0123456]") + testPanic("file.???") + testPanic("*.???") + test("", pmExact, "") + test("exact", pmExact, "exact") + test("*.mk", pmSuffix, ".mk") + test("Makefile.*", pmPrefix, "Makefile.") + testPanic("*.*") + testPanic("**") + testPanic("a*b") + testPanic("[") + testPanic("malformed[") +} + +func (s *Suite) Test_pathMatcher_matches(c *check.C) { + + test := func(pattern string, subject string, expected bool) { + matcher := newPathMatcher(pattern) + c.Check(matcher.matches(subject), equals, expected) + } + + test("", "", true) + test("", "any", false) + test("exact", "exact", true) + test("exact", "different", false) + + test("*.mk", "filename.mk", true) + test("*.mk", "filename.txt", false) + test("*.mk", "filename.mkx", false) + test("*.mk", ".mk", true) + + test("Makefile.*", "Makefile", false) + test("Makefile.*", "Makefile.", true) + test("Makefile.*", "Makefile.txt", true) + test("Makefile.*", "makefile.txt", false) +} + func (s *Suite) Test_StringInterner(c *check.C) { t := s.Init(c) diff --git a/pkgtools/pkglint/files/var.go b/pkgtools/pkglint/files/var.go index 38cc48a87fd..13f2a278e94 100644 --- a/pkgtools/pkglint/files/var.go +++ b/pkgtools/pkglint/files/var.go @@ -31,8 +31,8 @@ type Var struct { value string valueInfra string - readLocations []MkLine - writeLocations []MkLine + readLocations []*MkLine + writeLocations []*MkLine conditional bool conditionalVars StringSet @@ -106,7 +106,7 @@ func (v *Var) Constant() bool { // Variable assignments in the pkgsrc infrastructure are taken into account // for determining the constant value. func (v *Var) ConstantValue() string { - assertf(v.Constant(), "Variable must be constant.") + assert(v.Constant()) return v.constantValue } @@ -146,7 +146,7 @@ func (v *Var) ValueInfra() string { // are not listed. // // Variable uses in the pkgsrc infrastructure are taken into account. -func (v *Var) ReadLocations() []MkLine { +func (v *Var) ReadLocations() []*MkLine { return v.readLocations } @@ -156,11 +156,11 @@ func (v *Var) ReadLocations() []MkLine { // reachable in practice. // // Variable assignments in the pkgsrc infrastructure are taken into account. -func (v *Var) WriteLocations() []MkLine { +func (v *Var) WriteLocations() []*MkLine { return v.writeLocations } -func (v *Var) Read(mkline MkLine) { +func (v *Var) Read(mkline *MkLine) { v.readLocations = append(v.readLocations, mkline) v.constantState = [...]uint8{3, 2, 2, 3}[v.constantState] } @@ -169,8 +169,8 @@ func (v *Var) Read(mkline MkLine) { // Only standard assignments (VAR=value) are handled. // Side-effect assignments (${VAR::=value}) are not handled here since // they don't occur in practice. -func (v *Var) Write(mkline MkLine, conditional bool, conditionVarnames ...string) { - assertf(mkline.Varname() == v.Name, "wrong variable name") +func (v *Var) Write(mkline *MkLine, conditional bool, conditionVarnames ...string) { + assert(mkline.Varname() == v.Name) v.writeLocations = append(v.writeLocations, mkline) @@ -179,7 +179,7 @@ func (v *Var) Write(mkline MkLine, conditional bool, conditionVarnames ...string } v.conditionalVars.AddAll(conditionVarnames) - mkline.ForEachUsed(func(varUse *MkVarUse, time vucTime) { + mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) { v.refs.Add(varUse.varname) }) v.refs.AddAll(conditionVarnames) @@ -192,7 +192,7 @@ func (v *Var) Write(mkline MkLine, conditional bool, conditionVarnames ...string v.updateConstantValue(mkline) } -func (v *Var) update(mkline MkLine, update *string) { +func (v *Var) update(mkline *MkLine, update *string) { firstWrite := len(v.writeLocations) == 1 if v.Conditional() && !firstWrite { return @@ -218,7 +218,7 @@ func (v *Var) update(mkline MkLine, update *string) { } } -func (v *Var) updateConstantValue(mkline MkLine) { +func (v *Var) updateConstantValue(mkline *MkLine) { if v.constantState == 3 { return } diff --git a/pkgtools/pkglint/files/var_test.go b/pkgtools/pkglint/files/var_test.go index 7dfc2968319..ebc5579322a 100644 --- a/pkgtools/pkglint/files/var_test.go +++ b/pkgtools/pkglint/files/var_test.go @@ -216,13 +216,13 @@ func (s *Suite) Test_Var_Write__conditional_without_variables(c *check.C) { t := s.Init(c) mklines := t.NewMkLines("filename.mk", - MkRcsID, + MkCvsID, ".if exists(/usr/bin)", "VAR=\tvalue", ".endif") scope := NewRedundantScope() - mklines.ForEach(func(mkline MkLine) { + mklines.ForEach(func(mkline *MkLine) { if mkline.IsVarassign() { t.Check(scope.get("VAR").vari.Conditional(), equals, false) } @@ -239,11 +239,8 @@ func (s *Suite) Test_Var_Write__assertion(c *check.C) { t := s.Init(c) v := NewVar("VAR") - t.ExpectPanic( - func() { - v.Write(t.NewMkLine("filename.mk", 1, "OTHER=value"), false, nil...) - }, - "Pkglint internal error: wrong variable name") + t.ExpectAssert( + func() { v.Write(t.NewMkLine("filename.mk", 1, "OTHER=value"), false, nil...) }) } func (s *Suite) Test_Var_Value__conditional_write_after_unconditional(c *check.C) { @@ -321,7 +318,7 @@ func (s *Suite) Test_Var_ReadLocations(c *check.C) { mkline123 := t.NewMkLine("read.mk", 123, "OTHER=\t${VAR}") v.Read(mkline123) - t.Check(v.ReadLocations(), deepEquals, []MkLine{mkline123}) + t.Check(v.ReadLocations(), deepEquals, []*MkLine{mkline123}) mkline124 := t.NewMkLine("read.mk", 124, "OTHER=\t${VAR} ${VAR}") v.Read(mkline124) @@ -329,7 +326,7 @@ func (s *Suite) Test_Var_ReadLocations(c *check.C) { // For now, count every read of the variable. I'm not yet sure // whether that's the best way or whether to make the lines unique. - t.Check(v.ReadLocations(), deepEquals, []MkLine{mkline123, mkline124, mkline124}) + t.Check(v.ReadLocations(), deepEquals, []*MkLine{mkline123, mkline124, mkline124}) } func (s *Suite) Test_Var_WriteLocations(c *check.C) { @@ -342,7 +339,7 @@ func (s *Suite) Test_Var_WriteLocations(c *check.C) { mkline123 := t.NewMkLine("write.mk", 123, "VAR=\tvalue") v.Write(mkline123, false) - t.Check(v.WriteLocations(), deepEquals, []MkLine{mkline123}) + t.Check(v.WriteLocations(), deepEquals, []*MkLine{mkline123}) // Multiple writes from the same line may happen because of a .for loop. mkline125 := t.NewMkLine("write.mk", 125, "VAR+=\t${var}") @@ -351,7 +348,7 @@ func (s *Suite) Test_Var_WriteLocations(c *check.C) { // For now, count every write of the variable. I'm not yet sure // whether that's the best way or whether to make the lines unique. - t.Check(v.WriteLocations(), deepEquals, []MkLine{mkline123, mkline125, mkline125}) + t.Check(v.WriteLocations(), deepEquals, []*MkLine{mkline123, mkline125, mkline125}) } func (s *Suite) Test_Var_Refs(c *check.C) { diff --git a/pkgtools/pkglint/files/vardefs.go b/pkgtools/pkglint/files/vardefs.go index 409c804d41f..3b28609b916 100644 --- a/pkgtools/pkglint/files/vardefs.go +++ b/pkgtools/pkglint/files/vardefs.go @@ -58,7 +58,7 @@ func (reg *VarTypeRegistry) DefineType(varcanon string, vartype *Vartype) { func (reg *VarTypeRegistry) Define(varname string, basicType *BasicType, options vartypeOptions, aclEntries ...ACLEntry) { m, varbase, varparam := match2(varname, `^([A-Z_.][A-Z0-9_]*|@)(|\*|\.\*)$`) - assertf(m, "invalid variable name") + assert(m) // invalid variable name vartype := NewVartype(basicType, options, aclEntries...) @@ -180,14 +180,6 @@ func (reg *VarTypeRegistry) pkgappendbl3(varname string, basicType *BasicType) { "Makefile, Makefile.*, *.mk: default, set, append, use") } -// Like pkgappend, but always needs a rationale. -func (reg *VarTypeRegistry) pkgappendrat(varname string, basicType *BasicType) { - reg.acl(varname, basicType, - PackageSettable|NeedsRationale, - "buildlink3.mk, builtin.mk: none", - "Makefile, Makefile.*, *.mk: default, set, append, use") -} - // Some package-defined variables may be modified in buildlink3.mk files. // These variables are typically related to compiling and linking files // from C and related languages. @@ -313,7 +305,7 @@ func (reg *VarTypeRegistry) cmdline(varname string, basicType *BasicType) { func (reg *VarTypeRegistry) infralist(varname string, basicType *BasicType) { reg.acllist(varname, basicType, List, - "*: append") + "*: set, append") } // compilerLanguages reads the available languages that are typically @@ -809,7 +801,7 @@ func (reg *VarTypeRegistry) Init(src *Pkgsrc) { reg.pkglist("BOOTSTRAP_DEPENDS", BtDependencyWithPath) reg.pkg("BOOTSTRAP_PKG", BtYesNo) // BROKEN should better be a list of messages instead of a simple string. - reg.pkgappendrat("BROKEN", BtMessage) + reg.pkgappend("BROKEN", BtMessage) reg.pkg("BROKEN_GETTEXT_DETECTION", BtYesNo) reg.pkglistrat("BROKEN_EXCEPT_ON_PLATFORM", BtMachinePlatformPattern) reg.pkglistrat("BROKEN_ON_PLATFORM", BtMachinePlatformPattern) @@ -1132,7 +1124,7 @@ func (reg *VarTypeRegistry) Init(src *Pkgsrc) { reg.pkgload("HAS_CONFIGURE", BtYes) reg.pkglist("HEADER_TEMPLATES", BtPathname) reg.pkg("HOMEPAGE", BtHomepage) - reg.pkg("ICON_THEMES", BtYes) + reg.pkgbl3("ICON_THEMES", BtYes) reg.acl("IGNORE_PKG.*", BtYes, PackageSettable, "*: set, use-loadtime") @@ -1232,16 +1224,20 @@ func (reg *VarTypeRegistry) Init(src *Pkgsrc) { reg.pkglist("MASTER_SITES", BtFetchURL) for _, filename := range []string{"mk/fetch/sites.mk", "mk/fetch/fetch.mk"} { - sitesMk := LoadMk(src.File(filename), NotEmpty) + loadOptions := NotEmpty | MustSucceed + if G.Testing { + loadOptions = NotEmpty + } + sitesMk := LoadMk(src.File(filename), loadOptions) if sitesMk != nil { - sitesMk.ForEach(func(mkline MkLine) { + sitesMk.ForEach(func(mkline *MkLine) { if mkline.IsVarassign() && hasPrefix(mkline.Varname(), "MASTER_SITE_") { reg.syslist(mkline.Varname(), BtFetchURL) } }) - } else { - // During tests, use t.SetUpMasterSite instead to declare these variables. } + + // During tests, use t.SetUpMasterSite instead to declare these variables. } reg.pkglist("MESSAGE_SRC", BtPathname) @@ -1670,11 +1666,13 @@ func (reg *VarTypeRegistry) Init(src *Pkgsrc) { reg.pkglist("_WRAP_EXTRA_ARGS.*", BtShellWord) reg.infralist("_VARGROUPS", BtIdentifier) - reg.infralist("_USER_VARS.*", BtIdentifier) - reg.infralist("_PKG_VARS.*", BtIdentifier) - reg.infralist("_SYS_VARS.*", BtIdentifier) - reg.infralist("_DEF_VARS.*", BtIdentifier) - reg.infralist("_USE_VARS.*", BtIdentifier) + reg.infralist("_USER_VARS.*", BtVariableName) + reg.infralist("_PKG_VARS.*", BtVariableName) + reg.infralist("_SYS_VARS.*", BtVariableName) + reg.infralist("_DEF_VARS.*", BtVariableName) + reg.infralist("_USE_VARS.*", BtVariableName) + reg.infralist("_SORTED_VARS.*", BtVariableNamePattern) + reg.infralist("_LISTED_VARS.*", BtVariableNamePattern) } func enum(values string) *BasicType { @@ -1692,7 +1690,7 @@ func enum(values string) *BasicType { func (reg *VarTypeRegistry) parseACLEntries(varname string, aclEntries ...string) []ACLEntry { - assertf(len(aclEntries) > 0, "At least one ACL entry must be given.") + assert(len(aclEntries) > 0) // TODO: Use separate rules for infrastructure files. // These rules would have the "infra:" prefix @@ -1727,8 +1725,7 @@ func (reg *VarTypeRegistry) parseACLEntries(varname string, aclEntries ...string } } for _, prev := range result { - matched, err := path.Match(prev.glob, glob) - assertNil(err, "Invalid ACL pattern %q for %q", glob, varname) + matched := prev.matcher.matches(glob) assertf(!matched, "Unreachable ACL pattern %q for %q.", glob, varname) } result = append(result, NewACLEntry(glob, permissions)) diff --git a/pkgtools/pkglint/files/vardefs_test.go b/pkgtools/pkglint/files/vardefs_test.go index b965b46b163..9ddfc80b35e 100644 --- a/pkgtools/pkglint/files/vardefs_test.go +++ b/pkgtools/pkglint/files/vardefs_test.go @@ -16,18 +16,18 @@ func (s *Suite) Test_VarTypeRegistry_enumFrom(c *check.C) { t := s.Init(c) t.CreateFileLines("editors/emacs/modules.mk", - MkRcsID, + MkCvsID, "", "_EMACS_VERSIONS_ALL= emacs31", "_EMACS_VERSIONS_ALL+= emacs29") t.CreateFileLines("mk/java-vm.mk", - MkRcsID, + MkCvsID, "", "_PKG_JVMS.8= openjdk8 oracle-jdk8", "_PKG_JVMS.7= ${_PKG_JVMS.8} openjdk7 sun-jdk7", "_PKG_JVMS.6= ${_PKG_JVMS.7} jdk16") t.CreateFileLines("mk/compiler.mk", - MkRcsID, + MkCvsID, "", "_COMPILERS= gcc ido mipspro-ucode \\", " sunpro", @@ -63,7 +63,7 @@ func (s *Suite) Test_VarTypeRegistry_enumFrom__no_tracing(c *check.C) { t := s.Init(c) t.CreateFileLines("mk/existing.mk", - MkRcsID, + MkCvsID, "VAR=\tfirst second") reg := NewVarTypeRegistry() t.DisableTracing() @@ -82,8 +82,8 @@ func (s *Suite) Test_VarTypeRegistry_enumFromDirs(c *check.C) { // To make the test useful, these directories must differ from the // PYPKGPREFIX default value in vardefs.go. - t.CreateFileLines("lang/python28/Makefile", MkRcsID) - t.CreateFileLines("lang/python33/Makefile", MkRcsID) + t.CreateFileLines("lang/python28/Makefile", MkCvsID) + t.CreateFileLines("lang/python33/Makefile", MkCvsID) t.SetUpVartypes() @@ -146,9 +146,7 @@ func (s *Suite) Test_VarTypeRegistry_parseACLEntries__invalid_arguments(c *check func() { parseACLEntries("VARNAME", "too: many: colons") }, "Pkglint internal error: ACL entry \"too: many: colons\" must have exactly 1 colon.") - t.ExpectPanic( - func() { parseACLEntries("VAR") }, - "Pkglint internal error: At least one ACL entry must be given.") + t.ExpectAssert(func() { parseACLEntries("VAR") }) } func (s *Suite) Test_VarTypeRegistry_Init__LP64PLATFORMS(c *check.C) { @@ -168,7 +166,7 @@ func (s *Suite) Test_VarTypeRegistry_Init__no_tracing(c *check.C) { t := s.Init(c) t.CreateFileLines("editors/emacs/modules.mk", - MkRcsID, + MkCvsID, "", "_EMACS_VERSIONS_ALL= emacs31", "_EMACS_VERSIONS_ALL+= emacs29") @@ -179,11 +177,22 @@ func (s *Suite) Test_VarTypeRegistry_Init__no_tracing(c *check.C) { t.CheckOutputEmpty() } +func (s *Suite) Test_VarTypeRegistry_Init__no_testing(c *check.C) { + t := s.Init(c) + + t.SetUpPackage("category/package") + t.Remove("mk/fetch/sites.mk") + G.Testing = false + t.ExpectFatal( + t.FinishSetUp, + "FATAL: ~/mk/fetch/sites.mk: Cannot be read.") +} + func (s *Suite) Test_VarTypeRegistry_Init__MASTER_SITES(c *check.C) { t := s.Init(c) t.CreateFileLines("mk/fetch/sites.mk", - MkRcsID, + MkCvsID, "", "MASTER_SITE_GITHUB=\thttps://github.com/", "", diff --git a/pkgtools/pkglint/files/vartype.go b/pkgtools/pkglint/files/vartype.go index ed432465cda..8586d00ca35 100644 --- a/pkgtools/pkglint/files/vartype.go +++ b/pkgtools/pkglint/files/vartype.go @@ -14,11 +14,6 @@ type Vartype struct { } func NewVartype(basicType *BasicType, options vartypeOptions, aclEntries ...ACLEntry) *Vartype { - for _, aclEntry := range aclEntries { - _, err := path.Match(aclEntry.glob, "") - assertNil(err, "path.Match") - } - return &Vartype{basicType, options, aclEntries} } @@ -47,12 +42,12 @@ const ( ) type ACLEntry struct { - glob string // Examples: "Makefile", "*.mk" + matcher *pathMatcher permissions ACLPermissions } func NewACLEntry(glob string, permissions ACLPermissions) ACLEntry { - return ACLEntry{glob, permissions} + return ACLEntry{newPathMatcher(glob), permissions} } type ACLPermissions uint8 @@ -109,7 +104,7 @@ func (vt *Vartype) NeedsRationale() bool { return vt.options&NeedsRationale func (vt *Vartype) EffectivePermissions(basename string) ACLPermissions { for _, aclEntry := range vt.aclEntries { - if m, _ := path.Match(aclEntry.glob, basename); m { + if aclEntry.matcher.matches(basename) { return aclEntry.permissions } } @@ -157,9 +152,9 @@ func (vt *Vartype) AlternativeFiles(perms ACLPermissions) string { for _, aclEntry := range vt.aclEntries { if aclEntry.permissions.Contains(perms) { - pos = append(pos, aclEntry.glob) + pos = append(pos, aclEntry.matcher.originalPattern) } else { - neg = append(neg, aclEntry.glob) + neg = append(neg, aclEntry.matcher.originalPattern) } } @@ -356,6 +351,7 @@ var ( BtURL = &BasicType{"URL", (*VartypeCheck).URL} BtUserGroupName = &BasicType{"UserGroupName", (*VartypeCheck).UserGroupName} BtVariableName = &BasicType{"VariableName", (*VartypeCheck).VariableName} + BtVariableNamePattern = &BasicType{"VariableNamePattern", (*VartypeCheck).VariableNamePattern} BtVersion = &BasicType{"Version", (*VartypeCheck).Version} BtWrapperReorder = &BasicType{"WrapperReorder", (*VartypeCheck).WrapperReorder} BtWrapperTransform = &BasicType{"WrapperTransform", (*VartypeCheck).WrapperTransform} diff --git a/pkgtools/pkglint/files/vartype_test.go b/pkgtools/pkglint/files/vartype_test.go index 8b23a2b397d..442f8227590 100644 --- a/pkgtools/pkglint/files/vartype_test.go +++ b/pkgtools/pkglint/files/vartype_test.go @@ -62,7 +62,7 @@ func (s *Suite) Test_Vartype_AlternativeFiles(c *check.C) { test( rules( "buildlink3.mk: set", - "special:b*.mk: set, append", + "special:*3.mk: set, append", "*.mk: set", "Makefile: set, append", "Makefile.*: set"), @@ -72,7 +72,7 @@ func (s *Suite) Test_Vartype_AlternativeFiles(c *check.C) { test( rules( "buildlink3.mk: set", - "special:b*.mk: set, append", + "special:*3.mk: set, append", "*.mk: set", "Makefile: set, append", "Makefile.*: set", @@ -99,12 +99,12 @@ func (s *Suite) Test_Vartype_AlternativeFiles(c *check.C) { test( rules( "buildlink3.mk: none", - "special:b*.mk: set", + "special:*3.mk: set", "*.mk: none", "Makefile: set", "Makefile.*: none", "*: set"), - "b*.mk, Makefile or *, but not buildlink3.mk, *.mk or Makefile.*") + "*3.mk, Makefile or *, but not buildlink3.mk, *.mk or Makefile.*") test( rules( diff --git a/pkgtools/pkglint/files/vartypecheck.go b/pkgtools/pkglint/files/vartypecheck.go index 7c18bb2afcd..48d1cfd3c68 100644 --- a/pkgtools/pkglint/files/vartypecheck.go +++ b/pkgtools/pkglint/files/vartypecheck.go @@ -9,16 +9,16 @@ import ( // VartypeCheck groups together the various checks for variables of the different types. type VartypeCheck struct { - MkLines MkLines + MkLines *MkLines // Note: if "go vet" or "go test" complains about a "variable with invalid type", update to go1.11.4. // See https://github.com/golang/go/issues/28972. // That doesn't help though since pkglint contains these "more convoluted alias declarations" // mentioned in https://github.com/golang/go/commit/6971090515ba. - // Therefore MkLine is declared as *MkLineImpl here. + // Therefore MkLine is declared as *MkLine here. // Ideally the "more convoluted cyclic type declaration" should be broken up. - MkLine *MkLineImpl + MkLine *MkLine // The name of the variable being checked. // @@ -201,21 +201,19 @@ func (cv *VartypeCheck) CFlag() { if cv.Op == opUseMatch { return } + cflag := cv.Value switch { - case matches(cflag, `^-[DILOUWfgm]`), - hasPrefix(cflag, "-std="), - cflag == "-c99", - cflag == "-c", - cflag == "-no-integrated-as", - cflag == "-pthread", - hasPrefix(cflag, "`") && hasSuffix(cflag, "`"), - containsVarRef(cflag): - return - case hasPrefix(cflag, "-"): - cv.Warnf("Unknown compiler flag %q.", cflag) - default: - cv.Warnf("Compiler flag %q should start with a hyphen.", cflag) + case hasPrefix(cflag, "-l"), hasPrefix(cflag, "-L"): + cv.Warnf("%q is a linker flag and belong to LDFLAGS, LIBS or LDADD instead of %s.", + cflag, cv.Varname) + } + + if strings.Count(cflag, "\"")%2 != 0 { + cv.Warnf("Compiler flag %q has unbalanced double quotes.", cflag) + } + if strings.Count(cflag, "'")%2 != 0 { + cv.Warnf("Compiler flag %q has unbalanced single quotes.", cflag) } } @@ -612,34 +610,45 @@ func (cv *VartypeCheck) GccReqd() { func (cv *VartypeCheck) Homepage() { cv.URL() - if m, wrong, sitename, subdir := match3(cv.Value, `^(\$\{(MASTER_SITE\w+)(?::=([\w\-/]+))?\})`); m { - baseURL := G.Pkgsrc.MasterSiteVarToURL[sitename] - if sitename == "MASTER_SITES" && G.Pkg != nil { - if mkline := G.Pkg.vars.FirstDefinition("MASTER_SITES"); mkline != nil { - if masterSites := mkline.Value(); !containsVarRef(masterSites) { - baseURL = masterSites + m, wrong, sitename, subdir := match3(cv.Value, `^(\$\{(MASTER_SITE\w+)(?::=([\w\-/]+))?\})`) + if !m { + return + } + + baseURL := G.Pkgsrc.MasterSiteVarToURL[sitename] + if sitename == "MASTER_SITES" && G.Pkg != nil { + mkline := G.Pkg.vars.FirstDefinition("MASTER_SITES") + if mkline != nil { + if !containsVarRef(mkline.Value()) { + masterSites := cv.MkLine.ValueFields(mkline.Value()) + if len(masterSites) > 0 { + baseURL = masterSites[0] } } } + } - fixedURL := baseURL + subdir + fixedURL := baseURL + subdir - fix := cv.Autofix() - if baseURL != "" { - fix.Warnf("HOMEPAGE should not be defined in terms of MASTER_SITEs. Use %s directly.", fixedURL) - } else { - fix.Warnf("HOMEPAGE should not be defined in terms of MASTER_SITEs.") - } - fix.Explain( - "The HOMEPAGE is a single URL, while MASTER_SITES is a list of URLs.", - "As long as this list has exactly one element, this works, but as", - "soon as another site is added, the HOMEPAGE would not be a valid", - "URL anymore.", - "", - "Defining MASTER_SITES=${HOMEPAGE} is ok, though.") + fix := cv.Autofix() + if baseURL != "" { + fix.Warnf("HOMEPAGE should not be defined in terms of MASTER_SITEs. Use %s directly.", fixedURL) + } else { + fix.Warnf("HOMEPAGE should not be defined in terms of MASTER_SITEs.") + } + fix.Explain( + "The HOMEPAGE is a single URL, while MASTER_SITES is a list of URLs.", + "As long as this list has exactly one element, this works, but as", + "soon as another site is added, the HOMEPAGE would not be a valid", + "URL anymore.", + "", + "Defining MASTER_SITES=${HOMEPAGE} is ok, though.") + if baseURL != "" { fix.Replace(wrong, fixedURL) - fix.Apply() + } else { + fix.Anyway() } + fix.Apply() } // Identifier checks for valid identifiers in various contexts, limiting the @@ -679,6 +688,7 @@ func (cv *VartypeCheck) LdFlag() { if cv.Op == opUseMatch { return } + ldflag := cv.Value if m, rpathFlag := match1(ldflag, `^(-Wl,(?:-R|-rpath|--rpath))`); m { cv.Warnf("Please use \"${COMPILER_RPATH_FLAG}\" instead of %q.", rpathFlag) @@ -686,19 +696,12 @@ func (cv *VartypeCheck) LdFlag() { } switch { - case hasPrefix(ldflag, "-L"), - hasPrefix(ldflag, "-l"), - ldflag == "-pthread", - ldflag == "-static", - hasPrefix(ldflag, "-static-"), - hasPrefix(ldflag, "-Wl,-"), - hasPrefix(ldflag, "`") && hasSuffix(ldflag, "`"), - ldflag != cv.ValueNoVar: - return - case hasPrefix(ldflag, "-"): - cv.Warnf("Unknown linker flag %q.", cv.Value) - default: - cv.Warnf("Linker flag %q should start with a hyphen.", cv.Value) + case ldflag == "-P", + ldflag == "-E", + hasPrefix(ldflag, "-D"), + hasPrefix(ldflag, "-U"), + hasPrefix(ldflag, "-I"): + cv.Warnf("%q is a compiler flag and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of %s.", cv.Value, cv.Varname) } } @@ -791,7 +794,7 @@ func (cv *VartypeCheck) Option() { } if m, optname := match1(value, `^-?([a-z][-0-9a-z+]*)$`); m { - if !cv.MkLines.FirstTimeSlice("option:", optname) { + if !cv.MkLines.once.FirstTimeSlice("option:", optname) { return } @@ -820,7 +823,7 @@ func (cv *VartypeCheck) Pathlist() { value := cv.Value // Sometimes, variables called PATH contain a single pathname, - // especially those with auto-guessed type from MkLineImpl.VariableType. + // especially those with auto-guessed type from MkLine.VariableType. if !contains(value, ":") && cv.Guessed { cv.Pathname() return @@ -1095,7 +1098,7 @@ func (cv *VartypeCheck) SedCommands() { } func (cv *VartypeCheck) ShellCommand() { - if cv.Op == opUseMatch || cv.Op == opUseCompare { + if cv.Op == opUseMatch || cv.Op == opUseCompare || cv.Op == opAssignAppend { return } setE := true @@ -1200,6 +1203,7 @@ func (cv *VartypeCheck) UserGroupName() { // VariableName checks that the value is a valid variable name to be used in Makefiles. func (cv *VartypeCheck) VariableName() { + // TODO: sync with MkParser.Varname if cv.Value == cv.ValueNoVar && !matches(cv.Value, `^[A-Z_][0-9A-Z_]*(?:[.].*)?$`) { cv.Warnf("%q is not a valid variable name.", cv.Value) cv.Explain( @@ -1213,6 +1217,30 @@ func (cv *VartypeCheck) VariableName() { } } +func (cv *VartypeCheck) VariableNamePattern() { + if cv.Value != cv.ValueNoVar { + return + } + + // TODO: sync with MkParser.Varname + if matches(cv.Value, `^[*A-Z_][*0-9A-Z_]*(?:[.].*)?$`) { + return + } + + cv.Warnf("%q is not a valid variable name pattern.", cv.Value) + cv.Explain( + "Variable names are restricted to only uppercase letters and the", + "underscore in the basename, and arbitrary characters in the", + "parameterized part, following the dot.", + "", + "In addition to these characters, variable name patterns may use", + "the * placeholder.", + "", + "Examples:", + "* PKGNAME", + "* PKG_OPTIONS.gtk+-2.0") +} + func (cv *VartypeCheck) Version() { value := cv.Value diff --git a/pkgtools/pkglint/files/vartypecheck_test.go b/pkgtools/pkglint/files/vartypecheck_test.go index 434628bce99..5afd7d42779 100644 --- a/pkgtools/pkglint/files/vartypecheck_test.go +++ b/pkgtools/pkglint/files/vartypecheck_test.go @@ -101,12 +101,26 @@ func (s *Suite) Test_VartypeCheck_CFlag(c *check.C) { "-no-integrated-as", "-pthread", "`pkg-config`_plus") + vt.OutputEmpty() + + vt.Values( + "-L${PREFIX}/lib", + "-L${PREFIX}/lib64", + "-lncurses", + "-DMACRO=\\\"", + "-DMACRO=\\'") vt.Output( - "WARN: filename.mk:2: Compiler flag \"/W3\" should start with a hyphen.", - "WARN: filename.mk:3: Compiler flag \"target:sparc64\" should start with a hyphen.", - "WARN: filename.mk:5: Unknown compiler flag \"-XX:+PrintClassHistogramAfterFullGC\".", - "WARN: filename.mk:11: Compiler flag \"`pkg-config`_plus\" should start with a hyphen.") + "WARN: filename.mk:21: \"-L${PREFIX}/lib\" is a linker flag "+ + "and belong to LDFLAGS, LIBS or LDADD instead of CFLAGS.", + "WARN: filename.mk:22: \"-L${PREFIX}/lib64\" is a linker flag "+ + "and belong to LDFLAGS, LIBS or LDADD instead of CFLAGS.", + "WARN: filename.mk:23: \"-lncurses\" is a linker flag "+ + "and belong to LDFLAGS, LIBS or LDADD instead of CFLAGS.", + "WARN: filename.mk:24: Compiler flag \"-DMACRO=\\\\\\\"\" "+ + "has unbalanced double quotes.", + "WARN: filename.mk:25: Compiler flag \"-DMACRO=\\\\'\" "+ + "has unbalanced single quotes.") vt.Op(opUseMatch) vt.Values( @@ -420,7 +434,7 @@ func (s *Suite) Test_VartypeCheck_Enum__use_match(c *check.C) { t.SetUpCommandLine("-Wall", "--explain") mklines := t.NewMkLines("module.mk", - MkRcsID, + MkCvsID, "", ".if !empty(MACHINE_ARCH:Mi386) || ${MACHINE_ARCH} == i386", ".endif", @@ -740,16 +754,28 @@ func (s *Suite) Test_VartypeCheck_LdFlag(c *check.C) { "-static-something", "${LDFLAGS.NetBSD}", "-l${LIBNCURSES}", - "`pkg-config`_plus") + "`pkg-config`_plus", + "-DMACRO", + "-UMACRO", + "-P", + "-E", + "-I${PREFIX}/include") vt.Op(opUseMatch) vt.Values( "anything") vt.Output( - "WARN: filename.mk:4: Unknown linker flag \"-unknown\".", - "WARN: filename.mk:5: Linker flag \"no-hyphen\" should start with a hyphen.", "WARN: filename.mk:6: Please use \"${COMPILER_RPATH_FLAG}\" instead of \"-Wl,--rpath\".", - "WARN: filename.mk:12: Linker flag \"`pkg-config`_plus\" should start with a hyphen.") + "WARN: filename.mk:13: \"-DMACRO\" is a compiler flag "+ + "and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.", + "WARN: filename.mk:14: \"-UMACRO\" is a compiler flag "+ + "and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.", + "WARN: filename.mk:15: \"-P\" is a compiler flag "+ + "and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.", + "WARN: filename.mk:16: \"-E\" is a compiler flag "+ + "and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.", + "WARN: filename.mk:17: \"-I${PREFIX}/include\" is a compiler flag "+ + "and belongs on CFLAGS, CPPFLAGS, CXXFLAGS or FFLAGS instead of LDFLAGS.") } func (s *Suite) Test_VartypeCheck_License(c *check.C) { @@ -762,7 +788,7 @@ func (s *Suite) Test_VartypeCheck_License(c *check.C) { G.Pkg = NewPackage(t.File("category/package")) mklines := t.NewMkLines("perl5.mk", - MkRcsID, + MkCvsID, "PERL5_LICENSE= gnu-gpl-v2 OR artistic") // Also registers the PERL5_LICENSE variable in the package. mklines.collectDefinedVariables() @@ -1181,6 +1207,12 @@ func (s *Suite) Test_VartypeCheck_ShellCommand(c *check.C) { vt.Values("*") vt.OutputEmpty() + + vt.Varname("CC") + vt.Op(opAssignAppend) + vt.Values("-ggdb") + + vt.OutputEmpty() } func (s *Suite) Test_VartypeCheck_ShellCommands(c *check.C) { @@ -1346,6 +1378,23 @@ func (s *Suite) Test_VartypeCheck_VariableName(c *check.C) { "WARN: filename.mk:2: \"VarBase\" is not a valid variable name.") } +func (s *Suite) Test_VartypeCheck_VariableNamePattern(c *check.C) { + vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).VariableNamePattern) + + vt.Varname("_SORTED_VARS.group") + vt.Values( + "VARBASE", + "VarBase", + "PKG_OPTIONS_VAR.pkgbase", + "${INDIRECT}", + "*_DIRS", + "VAR.*", + "***") + + vt.Output( + "WARN: filename.mk:2: \"VarBase\" is not a valid variable name pattern.") +} + func (s *Suite) Test_VartypeCheck_Version(c *check.C) { vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).Version) @@ -1565,7 +1614,7 @@ func (vt *VartypeCheckTester) Values(values ...string) { return varname + space + opStr + value } - test := func(mklines MkLines, mkline MkLine, value string) { + test := func(mklines *MkLines, mkline *MkLine, value string) { varname := vt.varname comment := "" if mkline.IsVarassign() { @@ -1599,10 +1648,10 @@ func (vt *VartypeCheckTester) Values(values ...string) { text := toText(value) line := vt.tester.NewLine(vt.filename, vt.lineno, text) - mklines := NewMkLines(NewLines(vt.filename, []Line{line})) + mklines := NewMkLines(NewLines(vt.filename, []*Line{line})) vt.lineno++ - mklines.ForEach(func(mkline MkLine) { test(mklines, mkline, value) }) + mklines.ForEach(func(mkline *MkLine) { test(mklines, mkline, value) }) } } |