diff options
author | rillig <rillig@pkgsrc.org> | 2016-01-31 17:18:54 +0000 |
---|---|---|
committer | rillig <rillig@pkgsrc.org> | 2016-01-31 17:18:54 +0000 |
commit | 1a4a9a096ed8227b9019fd8484d76a90e297bc36 (patch) | |
tree | 2cea5cda3a43d1e9538d94a1ad05b89d113cb290 /pkgtools | |
parent | 72f7889e530a4709b7bac8c88dcf635ff9f781a0 (diff) | |
download | pkgsrc-1a4a9a096ed8227b9019fd8484d76a90e297bc36.tar.gz |
Updated pkglint to 5.3.5
Changes since 5.3.4:
* Added parser for Makefile conditionals
* Variables that are matched using the :M modifier are checked whether
the matched value is sensible
* Reworded and explained warning for variable ordering in packages
* Fixed bug in Tree.String
* Fixed a few variable types
Diffstat (limited to 'pkgtools')
-rw-r--r-- | pkgtools/pkglint/Makefile | 4 | ||||
-rw-r--r-- | pkgtools/pkglint/TODO | 5 | ||||
-rw-r--r-- | pkgtools/pkglint/files/globaldata.go | 3 | ||||
-rw-r--r-- | pkgtools/pkglint/files/mkline.go | 72 | ||||
-rw-r--r-- | pkgtools/pkglint/files/mkline_test.go | 40 | ||||
-rw-r--r-- | pkgtools/pkglint/files/mklines.go | 30 | ||||
-rw-r--r-- | pkgtools/pkglint/files/package.go | 5 | ||||
-rw-r--r-- | pkgtools/pkglint/files/package_test.go | 4 | ||||
-rw-r--r-- | pkgtools/pkglint/files/parser.go | 133 | ||||
-rw-r--r-- | pkgtools/pkglint/files/parser_test.go | 77 | ||||
-rw-r--r-- | pkgtools/pkglint/files/tree.go | 54 | ||||
-rw-r--r-- | pkgtools/pkglint/files/tree_test.go | 19 | ||||
-rw-r--r-- | pkgtools/pkglint/files/util.go | 3 | ||||
-rw-r--r-- | pkgtools/pkglint/files/vardefs.go | 34 | ||||
-rw-r--r-- | pkgtools/pkglint/files/vartypecheck.go | 91 | ||||
-rw-r--r-- | pkgtools/pkglint/files/vartypecheck_test.go | 35 |
16 files changed, 421 insertions, 188 deletions
diff --git a/pkgtools/pkglint/Makefile b/pkgtools/pkglint/Makefile index 69b87d96636..af4a7fb08b0 100644 --- a/pkgtools/pkglint/Makefile +++ b/pkgtools/pkglint/Makefile @@ -1,6 +1,6 @@ -# $NetBSD: Makefile,v 1.480 2016/01/27 21:55:50 rillig Exp $ +# $NetBSD: Makefile,v 1.481 2016/01/31 17:18:54 rillig Exp $ -PKGNAME= pkglint-5.3.4 +PKGNAME= pkglint-5.3.5 DISTFILES= # none CATEGORIES= pkgtools diff --git a/pkgtools/pkglint/TODO b/pkgtools/pkglint/TODO index 8291cf30fe2..af556720868 100644 --- a/pkgtools/pkglint/TODO +++ b/pkgtools/pkglint/TODO @@ -1,11 +1,10 @@ -$NetBSD: TODO,v 1.83 2015/11/25 13:29:07 rillig Exp $ +$NetBSD: TODO,v 1.84 2016/01/31 17:18:54 rillig Exp $ Please add your own entries at the bottom of this file. If possible, include the name of an example package where a warning should occur. * warn about the use of ${WRKDIR:=...}, as this construct should only be used with lists. -* Add checks for binary packages. See Debian/lintian for ideas. * Of the user-defined variables, some may be used at load-time and some don't. Find out how pkglint can distinguish them. * Make sure that no variable is modified at load-time after it has been @@ -19,8 +18,6 @@ include the name of an example package where a warning should occur. depend on the same option in the buildlink3.mk file. * Complain about ${PKGSRC_COMPILER} == "sunpro", which should be !empty(PKGSRC_COMPILER:Msunpro). -* If USE_TOOLS has autoconf213, and the package does stuff like - cd ${WRKSRC} && autoconf, then an incorrect warning is issued. * don't complain about "procedure calls", like for pkg-build-options in the various buildlink3.mk files. * if package A conflicts with B, then B should also conflict with A. diff --git a/pkgtools/pkglint/files/globaldata.go b/pkgtools/pkglint/files/globaldata.go index d86f67e8952..5d2d33a4433 100644 --- a/pkgtools/pkglint/files/globaldata.go +++ b/pkgtools/pkglint/files/globaldata.go @@ -532,5 +532,8 @@ func (gd *GlobalData) loadDeprecatedVars() { // October 2014 "SVR4_PKGNAME": "Just remove it.", "PKG_INSTALLATION_TYPES": "Just remove it.", + + // January 2016 + "SUBST_POSTCMD.*": "Has been removed, as it seemed unused.", } } diff --git a/pkgtools/pkglint/files/mkline.go b/pkgtools/pkglint/files/mkline.go index 599fbb0d539..aa51d9d4fe2 100644 --- a/pkgtools/pkglint/files/mkline.go +++ b/pkgtools/pkglint/files/mkline.go @@ -811,32 +811,37 @@ func (mkline *MkLine) CheckText(text string) { } } -func (mkline *MkLine) CheckIf() { +func (mkline *MkLine) CheckCond() { if G.opts.DebugTrace { defer tracecall0()() } - tree := mkline.parseMkCond(mkline.Args()) + p := NewParser(mkline.Args()) + cond := p.MkCond() + if !p.EOF() { + mkline.Warn1("Invalid conditional %q.", mkline.Args()) + return + } - { - var pvarname, ppattern *string - if tree.Match(NewTree("not", NewTree("empty", NewTree("match", &pvarname, &ppattern)))) { - vartype := mkline.getVariableType(*pvarname) - if vartype != nil && vartype.checker.IsEnum() { - if !matches(*ppattern, `[\$\[*]`) && !vartype.checker.HasEnum(*ppattern) { - mkline.Warn2("Invalid :M value %q. Only { %s } are allowed.", *ppattern, vartype.checker.AllowedEnums()) - } + cond.Visit("empty", func(node *Tree) { + varuse := node.args[0].(MkVarUse) + varname := varuse.varname + for _, modifier := range varuse.modifiers { + if modifier[0] == 'M' || modifier[0] == 'N' { + mkline.CheckVartype(varname, opUseMatch, modifier[1:], "") } - return } - } - - { - var pop, pvarname, pvalue *string - if tree.Match(NewTree("compareVarStr", &pvarname, &pop, &pvalue)) { - mkline.CheckVartype(*pvarname, opUse, *pvalue, "") + }) + + cond.Visit("compareVarStr", func(node *Tree) { + varuse := node.args[0].(MkVarUse) + varname := varuse.varname + varmods := varuse.modifiers + value := node.args[2].(string) + if len(varmods) == 0 || len(varmods) == 1 && matches(varmods[0], `^[MN]`) && value != "" { + mkline.CheckVartype(varname, opUseMatch, value, "") } - } + }) } func (mkline *MkLine) CheckValidCharactersInValue(reValid string) { @@ -950,37 +955,6 @@ func matchMkCond(text string) (m bool, indent, directive, args string) { return } -func (mkline *MkLine) parseMkCond(cond string) *Tree { - if G.opts.DebugTrace { - defer tracecall1(cond)() - } - - const ( - repartVarname = `[A-Z_][A-Z0-9_]*(?:\.[\w_+\-]+)?` - reDefined = `^defined\((` + repartVarname + `)\)` - reEmpty = `^empty\((` + repartVarname + `)\)` - reEmptyMatch = `^empty\((` + repartVarname + `):M([^\$:{})]+)\)` - reCompare = `^\$\{(` + repartVarname + `)\}\s+(==|!=)\s+"([^"\$\\]*)"` - ) - - if m, rest := replaceFirst(cond, `^!`, ""); m != nil { - return NewTree("not", mkline.parseMkCond(rest)) - } - if m, rest := replaceFirst(cond, reDefined, ""); m != nil { - return NewTree("defined", mkline.parseMkCond(rest)) - } - if m, _ := replaceFirst(cond, reEmpty, ""); m != nil { - return NewTree("empty", m[1]) - } - if m, _ := replaceFirst(cond, reEmptyMatch, ""); m != nil { - return NewTree("empty", NewTree("match", m[1], m[2])) - } - if m, _ := replaceFirst(cond, reCompare, ""); m != nil { - return NewTree("compareVarStr", m[1], m[2], m[3]) - } - return NewTree("unknown", cond) -} - type NeedsQuoting uint8 const ( diff --git a/pkgtools/pkglint/files/mkline_test.go b/pkgtools/pkglint/files/mkline_test.go index 74b01ddbd44..2f71f5d066e 100644 --- a/pkgtools/pkglint/files/mkline_test.go +++ b/pkgtools/pkglint/files/mkline_test.go @@ -305,39 +305,31 @@ func (s *Suite) TestMkLine_checkVarassign(c *check.C) { c.Check(s.Output(), equals, "") } -func (s *Suite) TestParseMkCond_NotEmptyMatch(c *check.C) { - mkline := NewMkLine(NewLine("fname", 1, ".if !empty(USE_LIBTOOL:M[Yy][Ee][Ss])", nil)) - - cond := mkline.parseMkCond(mkline.Args()) - - c.Check(cond, check.DeepEquals, NewTree("not", NewTree("empty", NewTree("match", "USE_LIBTOOL", "[Yy][Ee][Ss]")))) -} - -func (s *Suite) TestParseMkCond_Compare(c *check.C) { - mkline := NewMkLine(NewLine("fname", 1, ".if ${VARNAME} != \"Value\"", nil)) - - cond := mkline.parseMkCond(mkline.Args()) - - c.Check(cond, check.DeepEquals, NewTree("compareVarStr", "VARNAME", "!=", "Value")) -} - func (s *Suite) TestChecklineMkCondition(c *check.C) { s.UseCommandLine(c, "-Wtypes") G.globalData.InitVartypes() - NewMkLine(NewLine("fname", 1, ".if !empty(PKGSRC_COMPILER:Mmycc)", nil)).CheckIf() + NewMkLine(NewLine("fname", 1, ".if !empty(PKGSRC_COMPILER:Mmycc)", nil)).CheckCond() - c.Check(s.Stdout(), equals, "WARN: fname:1: Invalid :M value \"mycc\". "+ - "Only { ccache ccc clang distcc f2c gcc hp icc ido gcc mipspro "+ - "mipspro-ucode pcc sunpro xlc } are allowed.\n") + c.Check(s.Stdout(), equals, "WARN: fname:1: The pattern \"mycc\" cannot match any of "+ + "{ ccache ccc clang distcc f2c gcc hp icc ido "+ + "gcc mipspro mipspro-ucode pcc sunpro xlc } for PKGSRC_COMPILER.\n") - NewMkLine(NewLine("fname", 1, ".elif ${A} != ${B}", nil)).CheckIf() + NewMkLine(NewLine("fname", 1, ".elif ${A} != ${B}", nil)).CheckCond() - c.Check(s.Stdout(), equals, "") // Unknown condition types are silently ignored + c.Check(s.Stdout(), equals, "") - NewMkLine(NewLine("fname", 1, ".if ${HOMEPAGE} == \"mailto:someone@example.org\"", nil)).CheckIf() + NewMkLine(NewLine("fname", 1, ".if ${HOMEPAGE} == \"mailto:someone@example.org\"", nil)).CheckCond() c.Check(s.Output(), equals, "WARN: fname:1: \"mailto:someone@example.org\" is not a valid URL.\n") + + NewMkLine(NewLine("fname", 1, ".if !empty(PKGSRC_RUN_TEST:M[Y][eE][sS])", nil)).CheckCond() + + c.Check(s.Output(), equals, "WARN: fname:1: PKGSRC_RUN_TEST should be matched against \"[yY][eE][sS]\" or \"[nN][oO]\", not \"[Y][eE][sS]\".\n") + + NewMkLine(NewLine("fname", 1, ".if !empty(IS_BUILTIN.Xfixes:M[yY][eE][sS])", nil)).CheckCond() + + c.Check(s.Output(), equals, "") } func (s *Suite) TestMkLine_variableNeedsQuoting(c *check.C) { @@ -443,7 +435,7 @@ func (s *Suite) TestMkLine_CheckVarusePermissions(c *check.C) { "WARN: options.mk:2: The user-defined variable GAMES_USER is used but not added to BUILD_DEFS.\n"+ "WARN: options.mk:3: PKGBASE should not be evaluated at load time.\n"+ "WARN: options.mk:4: The variable PYPKGPREFIX may not be set in this file; it would be ok in pyversion.mk.\n"+ - "WARN: options.mk:4: \"${PKGBASE}\" is not valid for PYPKGPREFIX. Use one of { py27 py33 py34 } instead.\n"+ + "WARN: options.mk:4: \"${PKGBASE}\" is not valid for PYPKGPREFIX. Use one of { py27 py33 py34 py35 } instead.\n"+ "WARN: options.mk:4: PKGBASE should not be evaluated indirectly at load time.\n"+ "NOTE: options.mk:4: This variable value should be aligned to column 17.\n") } diff --git a/pkgtools/pkglint/files/mklines.go b/pkgtools/pkglint/files/mklines.go index be9ac0ff354..eda58e239a1 100644 --- a/pkgtools/pkglint/files/mklines.go +++ b/pkgtools/pkglint/files/mklines.go @@ -42,13 +42,13 @@ func NewMkLines(lines []*Line) *MkLines { tools} } -func (mklines *MkLines) IndentDepth() int { +func (mklines *MkLines) indentDepth() int { return mklines.indentation[len(mklines.indentation)-1] } -func (mklines *MkLines) PopIndent() { +func (mklines *MkLines) popIndent() { mklines.indentation = mklines.indentation[:len(mklines.indentation)-1] } -func (mklines *MkLines) PushIndent(indent int) { +func (mklines *MkLines) pushIndent(indent int) { mklines.indentation = append(mklines.indentation, indent) } @@ -88,8 +88,8 @@ func (mklines *MkLines) Check() { defer func() { G.Mk = nil }() allowedTargets := make(map[string]bool) - prefixes := splitOnSpace("pre do post") - actions := splitOnSpace("fetch extract patch tools wrapper configure build test install package clean") + prefixes := [...]string{"pre", "do", "post"} + actions := [...]string{"fetch", "extract", "patch", "tools", "wrapper", "configure", "build", "test", "install", "package", "clean"} for _, prefix := range prefixes { for _, action := range actions { allowedTargets[prefix+"-"+action] = true @@ -105,8 +105,8 @@ func (mklines *MkLines) Check() { mklines.lines[0].CheckRcsid(`#\s+`, "# ") - substcontext := new(SubstContext) - varalign := new(VaralignBlock) + var substcontext SubstContext + var varalign VaralignBlock for _, mkline := range mklines.mklines { mkline.Line.CheckTrailingWhitespace() mkline.Line.CheckValidCharacters(`[\t -~]`) @@ -143,8 +143,8 @@ func (mklines *MkLines) Check() { ChecklinesTrailingEmptyLines(mklines.lines) - if len(mklines.indentation) != 1 && mklines.IndentDepth() != 0 { - lastMkline.Line.Errorf("Directive indentation is not 0, but %d.", mklines.IndentDepth()) + if len(mklines.indentation) != 1 && mklines.indentDepth() != 0 { + lastMkline.Line.Errorf("Directive indentation is not 0, but %d.", mklines.indentDepth()) } SaveAutofixChanges(mklines.lines) @@ -223,24 +223,24 @@ func (mklines *MkLines) checklineCond(mkline *MkLine) { switch directive { case "endif", "endfor", "elif", "else": if len(mklines.indentation) > 1 { - mklines.PopIndent() + mklines.popIndent() } else { mkline.Error1("Unmatched .%s.", directive) } } // Check the indentation - if expected := strings.Repeat(" ", mklines.IndentDepth()); indent != expected { + if expected := strings.Repeat(" ", mklines.indentDepth()); indent != expected { if G.opts.WarnSpace && !mkline.Line.AutofixReplace("."+indent, "."+expected) { - mkline.Line.Notef("This directive should be indented by %d spaces.", mklines.IndentDepth()) + mkline.Line.Notef("This directive should be indented by %d spaces.", mklines.indentDepth()) } } if directive == "if" && matches(args, `^!defined\([\w]+_MK\)$`) { - mklines.PushIndent(mklines.IndentDepth()) + mklines.pushIndent(mklines.indentDepth()) } else if matches(directive, `^(?:if|ifdef|ifndef|for|elif|else)$`) { - mklines.PushIndent(mklines.IndentDepth() + 2) + mklines.pushIndent(mklines.indentDepth() + 2) } reDirectivesWithArgs := `^(?:if|ifdef|ifndef|elif|for|undef)$` @@ -255,7 +255,7 @@ func (mklines *MkLines) checklineCond(mkline *MkLine) { } } else if directive == "if" || directive == "elif" { - mkline.CheckIf() + mkline.CheckCond() } else if directive == "ifdef" || directive == "ifndef" { if matches(args, `\s`) { diff --git a/pkgtools/pkglint/files/package.go b/pkgtools/pkglint/files/package.go index a2205397407..4419db20092 100644 --- a/pkgtools/pkglint/files/package.go +++ b/pkgtools/pkglint/files/package.go @@ -683,7 +683,10 @@ func (pkg *Package) ChecklinesPackageMakefileVarorder(mklines *MkLines) { default: for varindex < len(vars) { if vars[varindex].count == once && !maySkipSection { - line.Warn1("%s should be set here.", vars[varindex].varname) + line.Warn1("The canonical position for the required variable %s is here.", vars[varindex].varname) + Explain( + "See doc/Makefile-example or the pkgsrc guide, section", + "\"Package components\", subsection \"Makefile\" for more information.") } below[vars[varindex].varname] = belowWhat varindex++ diff --git a/pkgtools/pkglint/files/package_test.go b/pkgtools/pkglint/files/package_test.go index ebebfa9bb67..cf753fe17e9 100644 --- a/pkgtools/pkglint/files/package_test.go +++ b/pkgtools/pkglint/files/package_test.go @@ -39,8 +39,8 @@ func (s *Suite) TestChecklinesPackageMakefileVarorder(c *check.C) { ".include \"../../mk/bsd.pkg.mk\"")) c.Check(s.Output(), equals, ""+ - "WARN: Makefile:6: COMMENT should be set here.\n"+ - "WARN: Makefile:6: LICENSE should be set here.\n") + "WARN: Makefile:6: The canonical position for the required variable COMMENT is here.\n"+ + "WARN: Makefile:6: The canonical position for the required variable LICENSE is here.\n") } func (s *Suite) TestGetNbpart(c *check.C) { diff --git a/pkgtools/pkglint/files/parser.go b/pkgtools/pkglint/files/parser.go index 49fadbcc402..0b8b3b98206 100644 --- a/pkgtools/pkglint/files/parser.go +++ b/pkgtools/pkglint/files/parser.go @@ -280,3 +280,136 @@ func (p *Parser) VarUseModifiers(closing string) []string { } return modifiers } + +func (p *Parser) MkCond() *Tree { + return p.mkCondOr() +} + +func (p *Parser) mkCondOr() *Tree { + and := p.mkCondAnd() + if and == nil { + return nil + } + + ands := append([]interface{}(nil), and) + for { + mark := p.repl.Mark() + if !p.repl.AdvanceRegexp(`^\s*\|\|\s*`) { + break + } + next := p.mkCondAnd() + if next == nil { + p.repl.Reset(mark) + break + } + ands = append(ands, next) + } + if len(ands) == 1 { + return and + } + return NewTree("or", ands...) +} + +func (p *Parser) mkCondAnd() *Tree { + atom := p.mkCondAtom() + if atom == nil { + return nil + } + + atoms := append([]interface{}(nil), atom) + for { + mark := p.repl.Mark() + if !p.repl.AdvanceRegexp(`^\s*&&\s*`) { + break + } + next := p.mkCondAtom() + if next == nil { + p.repl.Reset(mark) + break + } + atoms = append(atoms, next) + } + if len(atoms) == 1 { + return atom + } + return NewTree("and", atoms...) +} + +func (p *Parser) mkCondAtom() *Tree { + if G.opts.DebugTrace { + defer tracecall1(p.Rest())() + } + + repl := p.repl + mark := repl.Mark() + repl.SkipSpace() + switch { + case repl.AdvanceStr("!"): + cond := p.mkCondAtom() + if cond != nil { + return NewTree("not", cond) + } + case repl.AdvanceStr("("): + cond := p.MkCond() + if cond != nil { + repl.SkipSpace() + if repl.AdvanceStr(")") { + return cond + } + } + case repl.AdvanceRegexp(`^defined\s*\(`): + if varname := p.Varname(); varname != "" { + if repl.AdvanceStr(")") { + return NewTree("defined", varname) + } + } + case repl.AdvanceRegexp(`^empty\s*\(`): + if varname := p.Varname(); varname != "" { + modifiers := p.VarUseModifiers(")") + if repl.AdvanceStr(")") { + return NewTree("empty", MkVarUse{varname, modifiers}) + } + } + case repl.AdvanceRegexp(`^(commands|exists|make|target)\s*\(`): + funcname := repl.m[1] + argMark := repl.Mark() + for p.VarUse() != nil || repl.AdvanceRegexp(`^[^$)]+`) { + } + arg := repl.Since(argMark) + if repl.AdvanceStr(")") { + return NewTree(funcname, arg) + } + default: + lhs := p.VarUse() + mark := repl.Mark() + if lhs == nil && repl.AdvanceStr("\"") { + if quotedLhs := p.VarUse(); quotedLhs != nil && repl.AdvanceStr("\"") { + lhs = quotedLhs + } else { + repl.Reset(mark) + } + } + if lhs != nil { + if repl.AdvanceRegexp(`^\s*(<|<=|==|!=|>=|>)\s*(\d+(?:\.\d+)?)`) { + return NewTree("compareVarNum", *lhs, repl.m[1], repl.m[2]) + } + if repl.AdvanceRegexp(`^\s*(<|<=|==|!=|>=|>)\s*`) { + op := repl.m[1] + if (op == "!=" || op == "==") && repl.AdvanceRegexp(`^"([^"\$\\]*)"`) { + return NewTree("compareVarStr", *lhs, op, repl.m[1]) + } else if repl.AdvanceRegexp(`^\w+`) { + return NewTree("compareVarStr", *lhs, op, repl.m[0]) + } else if rhs := p.VarUse(); rhs != nil { + return NewTree("compareVarVar", *lhs, op, *rhs) + } + } else { + return NewTree("not", NewTree("empty", *lhs)) // See devel/bmake/files/cond.c:/\* For \.if \$/ + } + } + if repl.AdvanceRegexp(`^\d+(?:\.\d+)?`) { + return NewTree("literalNum", repl.m[0]) + } + } + repl.Reset(mark) + return nil +} diff --git a/pkgtools/pkglint/files/parser_test.go b/pkgtools/pkglint/files/parser_test.go index 6c7dad7d263..72dd8b8d12f 100644 --- a/pkgtools/pkglint/files/parser_test.go +++ b/pkgtools/pkglint/files/parser_test.go @@ -138,3 +138,80 @@ func (s *Suite) TestParser_MkTokens(c *check.C) { parse("${VAR)", nil, "${VAR)") // Opening brace, closing parenthesis parse("$(VAR}", nil, "$(VAR}") // Opening parenthesis, closing brace } + +func (s *Suite) TestParser_MkCond_Basics(c *check.C) { + condrest := func(input string, expectedTree *Tree, expectedRest string) { + p := NewParser(input) + actualTree := p.MkCond() + c.Check(actualTree, deepEquals, expectedTree) + c.Check(p.Rest(), equals, expectedRest) + } + cond := func(input string, expectedTree *Tree) { + condrest(input, expectedTree, "") + } + literal := func(literal string) MkToken { + return MkToken{literal: literal} + } + varuse := func(varname string, modifiers ...string) MkVarUse { + return MkVarUse{varname: varname, modifiers: modifiers} + } + _, _ = literal, varuse + + cond("${OPSYS:MNetBSD}", + NewTree("not", NewTree("empty", varuse("OPSYS", "MNetBSD")))) + cond("defined(VARNAME)", + NewTree("defined", "VARNAME")) + cond("empty(VARNAME)", + NewTree("empty", varuse("VARNAME"))) + cond("!empty(VARNAME)", + NewTree("not", NewTree("empty", varuse("VARNAME")))) + cond("!empty(VARNAME:M[yY][eE][sS])", + NewTree("not", NewTree("empty", varuse("VARNAME", "M[yY][eE][sS]")))) + cond("${VARNAME} != \"Value\"", + NewTree("compareVarStr", varuse("VARNAME"), "!=", "Value")) + cond("${VARNAME:Mi386} != \"Value\"", + NewTree("compareVarStr", varuse("VARNAME", "Mi386"), "!=", "Value")) + cond("${VARNAME} != Value", + NewTree("compareVarStr", varuse("VARNAME"), "!=", "Value")) + cond("\"${VARNAME}\" != Value", + NewTree("compareVarStr", varuse("VARNAME"), "!=", "Value")) + cond("(defined(VARNAME))", + NewTree("defined", "VARNAME")) + cond("exists(/etc/hosts)", + NewTree("exists", "/etc/hosts")) + cond("exists(${PREFIX}/var)", + NewTree("exists", "${PREFIX}/var")) + cond("${OPSYS} == \"NetBSD\" || ${OPSYS} == \"OpenBSD\"", + NewTree("or", + NewTree("compareVarStr", varuse("OPSYS"), "==", "NetBSD"), + NewTree("compareVarStr", varuse("OPSYS"), "==", "OpenBSD"))) + cond("${OPSYS} == \"NetBSD\" && ${MACHINE_ARCH} == \"i386\"", + NewTree("and", + NewTree("compareVarStr", varuse("OPSYS"), "==", "NetBSD"), + NewTree("compareVarStr", varuse("MACHINE_ARCH"), "==", "i386"))) + cond("defined(A) && defined(B) || defined(C) && defined(D)", + NewTree("or", + NewTree("and", + NewTree("defined", "A"), + NewTree("defined", "B")), + NewTree("and", + NewTree("defined", "C"), + NewTree("defined", "D")))) + + // Exotic cases + cond("0", + NewTree("literalNum", "0")) + cond("! ( defined(A) && empty(VARNAME) )", + NewTree("not", NewTree("and", NewTree("defined", "A"), NewTree("empty", varuse("VARNAME"))))) + cond("${REQD_MAJOR} > ${MAJOR}", + NewTree("compareVarVar", varuse("REQD_MAJOR"), ">", varuse("MAJOR"))) + cond("${OS_VERSION} >= 6.5", + NewTree("compareVarNum", varuse("OS_VERSION"), ">=", "6.5")) + cond("${OS_VERSION} == 5.3", + NewTree("compareVarNum", varuse("OS_VERSION"), "==", "5.3")) + + // Errors + condrest("!empty(PKG_OPTIONS:Msndfile) || defined(PKG_OPTIONS:Msamplerate)", + NewTree("not", NewTree("empty", varuse("PKG_OPTIONS", "Msndfile"))), + " || defined(PKG_OPTIONS:Msamplerate)") +} diff --git a/pkgtools/pkglint/files/tree.go b/pkgtools/pkglint/files/tree.go index b1377f88523..4ceeb525e07 100644 --- a/pkgtools/pkglint/files/tree.go +++ b/pkgtools/pkglint/files/tree.go @@ -13,46 +13,6 @@ func NewTree(name string, args ...interface{}) *Tree { return &Tree{name, args} } -// Checks whether this tree matches the given pattern, and if so, -// copies the corresponding nodes from the tree to the pattern. -// If the match is partially successful, some or all of the variables -// may have been copied or not. -func (t *Tree) Match(pattern *Tree) bool { - if G.opts.DebugTrace { - defer tracecall(t, pattern)() - } - if t.name != pattern.name || len(t.args) != len(pattern.args) { - return false - } - - for i, targ := range t.args { - parg := pattern.args[i] - switch parg := parg.(type) { - case *Tree: - if targ, ok := targ.(*Tree); ok { - if !targ.Match(parg) { - return false - } - } else { - return false - } - case **string: - if targ, ok := targ.(string); ok { - if *parg == nil { - *parg = &targ - } else if **parg != targ { - return false - } - } else { - return false - } - default: - return false - } - } - return true -} - func (t *Tree) String() string { s := "(" + t.name for _, arg := range t.args { @@ -63,9 +23,19 @@ func (t *Tree) String() string { if arg, ok := arg.(string); ok { s += fmt.Sprintf(" %q", arg) continue - } else { - s += fmt.Sprintf(" %v", arg) } + s += fmt.Sprintf(" %v", arg) } return s + ")" } + +func (t *Tree) Visit(nodename string, action func(t *Tree)) { + if t.name == nodename { + action(t) + } + for _, arg := range t.args { + if child, ok := arg.(*Tree); ok { + child.Visit(nodename, action) + } + } +} diff --git a/pkgtools/pkglint/files/tree_test.go b/pkgtools/pkglint/files/tree_test.go index 4bf0ccf8f3c..d7d8716287a 100644 --- a/pkgtools/pkglint/files/tree_test.go +++ b/pkgtools/pkglint/files/tree_test.go @@ -4,25 +4,6 @@ import ( check "gopkg.in/check.v1" ) -func (s *Suite) TestTreeMatch_successful(c *check.C) { - var varname *string - pattern := NewTree("not", NewTree("empty", &varname)) - tree := NewTree("not", NewTree("empty", "VARNAME")) - - c.Check(tree.Match(pattern), equals, true) - c.Assert(varname, check.Not(equals), nil) - c.Check(*varname, equals, "VARNAME") -} - -func (s *Suite) TestTreeMatch_fails(c *check.C) { - var varname *string - pattern := NewTree("not", NewTree("empty", &varname)) - tree := NewTree("not", NewTree("full", "VARNAME")) - - c.Check(tree.Match(pattern), equals, false) - c.Check(varname, equals, (*string)(nil)) -} - func (s *Suite) TestTreeString(c *check.C) { c.Check(NewTree("not", NewTree("empty", "varname")).String(), equals, "(not (empty \"varname\"))") } diff --git a/pkgtools/pkglint/files/util.go b/pkgtools/pkglint/files/util.go index e323091c426..55d4dc8f62e 100644 --- a/pkgtools/pkglint/files/util.go +++ b/pkgtools/pkglint/files/util.go @@ -357,6 +357,9 @@ func (pr *PrefixReplacer) Reset(mark string) { func (pr *PrefixReplacer) Skip(n int) { pr.rest = pr.rest[n:] } +func (pr *PrefixReplacer) SkipSpace() { + pr.rest = strings.TrimLeft(pr.rest, " \t") +} func (pr *PrefixReplacer) Since(mark string) string { return mark[:len(mark)-len(pr.rest)] } diff --git a/pkgtools/pkglint/files/vardefs.go b/pkgtools/pkglint/files/vardefs.go index b73307a7be6..3352c07ee91 100644 --- a/pkgtools/pkglint/files/vardefs.go +++ b/pkgtools/pkglint/files/vardefs.go @@ -29,7 +29,7 @@ func (gd *GlobalData) InitVartypes() { usr("PKGSRC_SLEEPSECS", lkNone, CheckvarInteger) usr("USETBL", lkNone, CheckvarYes) usr("ABI", lkNone, enum("32 64")) - usr("PKG_DEVELOPER", lkNone, CheckvarYes) + usr("PKG_DEVELOPER", lkNone, CheckvarYesNo) usr("USE_ABI_DEPENDS", lkNone, CheckvarYesNo) usr("PKG_REGISTER_SHELLS", lkNone, enum("YES NO")) usr("PKGSRC_COMPILER", lkShell, enum("ccache ccc clang distcc f2c gcc hp icc ido gcc mipspro mipspro-ucode pcc sunpro xlc")) @@ -270,11 +270,11 @@ func (gd *GlobalData) InitVartypes() { sys("EMULSUBDIR", lkNone, CheckvarPathname) sys("OPSYS_EMULDIR", lkNone, CheckvarPathname) sys("EMULSUBDIRSLASH", lkNone, CheckvarPathname) - sys("EMUL_ARCH", lkNone, enum("i386 none x86_64")) + sys("EMUL_ARCH", lkNone, enum("arm i386 m68k none ns32k sparc vax x86_64")) sys("EMUL_DISTRO", lkNone, CheckvarIdentifier) sys("EMUL_IS_NATIVE", lkNone, CheckvarYes) pkg("EMUL_MODULES.*", lkShell, CheckvarIdentifier) - sys("EMUL_OPSYS", lkNone, enum("freebsd hpux irix linux osf1 solaris sunos none")) + sys("EMUL_OPSYS", lkNone, enum("darwin freebsd hpux irix linux osf1 solaris sunos none")) pkg("EMUL_PKG_FMT", lkNone, enum("plain rpm")) usr("EMUL_PLATFORM", lkNone, CheckvarEmulPlatform) pkg("EMUL_PLATFORMS", lkShell, CheckvarEmulPlatform) @@ -537,7 +537,7 @@ func (gd *GlobalData) InitVartypes() { acl("PKG_HACKS", lkShell, CheckvarIdentifier, "hacks.mk: append") sys("PKG_INFO", lkNone, CheckvarShellCommand) sys("PKG_JAVA_HOME", lkNone, CheckvarPathname) - jvms := enum("blackdown-jdk13 jdk jdk14 kaffe run-jdk13 sun-jdk14 sun-jdk15 sun-jdk6 openjdk7 openjdk7-bin sun-jdk7") + jvms := enum("blackdown-jdk13 jdk jdk14 kaffe run-jdk13 sun-jdk14 sun-jdk15 sun-jdk6 openjdk7 openjdk7-bin sun-jdk7 openjdk8 oracle-jdk8") sys("PKG_JVM", lkNone, jvms) acl("PKG_JVMS_ACCEPTED", lkShell, jvms, "Makefile: set; Makefile.common: default, set") usr("PKG_JVM_DEFAULT", lkNone, jvms) @@ -587,7 +587,7 @@ func (gd *GlobalData) InitVartypes() { acl("PTHREAD_OPTS", lkShell, enum("native optional require"), "Makefile: set, append; Makefile.common: append; buildlink3.mk: append") sys("PTHREAD_TYPE", lkNone, CheckvarIdentifier) // Or "native" or "none". pkg("PY_PATCHPLIST", lkNone, CheckvarYes) - acl("PYPKGPREFIX", lkNone, enum("py27 py33 py34"), "pyversion.mk: set; *: use-loadtime, use") + acl("PYPKGPREFIX", lkNone, enum("py27 py33 py34 py35"), "pyversion.mk: set; *: use-loadtime, use") pkg("PYTHON_FOR_BUILD_ONLY", lkNone, CheckvarYes) pkglist("REPLACE_PYTHON", lkShell, CheckvarPathmask) pkg("PYTHON_VERSIONS_ACCEPTED", lkShell, CheckvarVersion) @@ -670,6 +670,7 @@ func (gd *GlobalData) InitVartypes() { acl("USE_BUILTIN.*", lkNone, CheckvarYesNoIndirectly, "builtin.mk: set") pkg("USE_CMAKE", lkNone, CheckvarYes) acl("USE_CROSSBASE", lkNone, CheckvarYes, "Makefile: set") + usr("USE_DESTDIR", lkNone, CheckvarYes) pkg("USE_FEATURES", lkShell, CheckvarIdentifier) pkg("USE_GCC_RUNTIME", lkNone, CheckvarYesNo) pkg("USE_GNU_CONFIGURE_HOST", lkNone, CheckvarYesNo) @@ -705,9 +706,26 @@ func enum(values string) *VarChecker { vmap[value] = true } name := "enum: " + values + " " // See IsEnum - return &VarChecker{name, func(ctx *VartypeCheck) { - if !vmap[ctx.value] { - ctx.line.Warnf("%q is not valid for %s. Use one of { %s } instead.", ctx.value, ctx.varname, values) + return &VarChecker{name, func(cv *VartypeCheck) { + if cv.op == opUseMatch { + if !vmap[cv.value] && cv.value == cv.valueNovar { + canMatch := false + for value := range vmap { + if ok, err := path.Match(cv.value, value); err != nil { + cv.line.Warnf("Invalid match pattern %q.", cv.value) + } else if ok { + canMatch = true + } + } + if !canMatch { + cv.line.Warnf("The pattern %q cannot match any of { %s } for %s.", cv.value, values, cv.varname) + } + } + return + } + + if !vmap[cv.value] { + cv.line.Warnf("%q is not valid for %s. Use one of { %s } instead.", cv.value, cv.varname, values) } }} } diff --git a/pkgtools/pkglint/files/vartypecheck.go b/pkgtools/pkglint/files/vartypecheck.go index defc1ca3b55..a91a1ce372d 100644 --- a/pkgtools/pkglint/files/vartypecheck.go +++ b/pkgtools/pkglint/files/vartypecheck.go @@ -25,8 +25,9 @@ const ( opAssignEval // := opAssignAppend // += opAssignDefault // ?= - opUseLoadtime - opUse + opUse // + opUseLoadtime // + opUseMatch // Used in the :M operator ) func NewMkOperator(op string) MkOperator { @@ -46,7 +47,7 @@ func NewMkOperator(op string) MkOperator { } func (op MkOperator) String() string { - return [...]string{"=", "!=", ":=", "+=", "?=", "use", "use-loadtime"}[op] + return [...]string{"=", "!=", ":=", "+=", "?=", "use", "use-loadtime", "use-match"}[op] } func (cv *VartypeCheck) AwkCommand() { @@ -91,6 +92,9 @@ func (cv *VartypeCheck) Category() { // A single option to the C/C++ compiler. func (cv *VartypeCheck) CFlag() { + if cv.op == opUseMatch { + return + } cflag := cv.value switch { case matches(cflag, `^-[DILOUWfgm]`), @@ -291,6 +295,8 @@ func (cv *VartypeCheck) FetchURL() { // See http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_169 func (cv *VartypeCheck) Filename() { switch { + case cv.op == opUseMatch: + break case contains(cv.valueNovar, "/"): cv.line.Warn0("A filename should not contain a slash.") case !matches(cv.valueNovar, `^[-0-9@A-Za-z.,_~+%]*$`): @@ -299,6 +305,9 @@ func (cv *VartypeCheck) Filename() { } func (cv *VartypeCheck) Filemask() { + if cv.op == opUseMatch { + return + } if !matches(cv.valueNovar, `^[-0-9A-Za-z._~+%*?]*$`) { cv.line.Warn1("%q is not a valid filename mask.", cv.value) } @@ -316,6 +325,12 @@ func (cv *VartypeCheck) FileMode() { } func (cv *VartypeCheck) Identifier() { + if cv.op == opUseMatch { + if cv.value == cv.valueNovar && !matches(cv.value, `^[\w*?]`) { + cv.line.Warn2("Invalid identifier pattern %q for %s.", cv.value, cv.varname) + } + return + } if cv.value != cv.valueNovar { //line.logWarning("Identifiers should be given directly.") } @@ -336,6 +351,9 @@ func (cv *VartypeCheck) Integer() { } func (cv *VartypeCheck) LdFlag() { + if cv.op == opUseMatch { + return + } ldflag := cv.value if m, rpathFlag := match1(ldflag, `^(-Wl,(?:-R|-rpath|--rpath))`); m { cv.line.Warn1("Please use \"${COMPILER_RPATH_FLAG}\" instead of %q.", rpathFlag) @@ -452,6 +470,9 @@ func (cv *VartypeCheck) Pathlist() { // Shell globbing including slashes. // See Filemask func (cv *VartypeCheck) Pathmask() { + if cv.op == opUseMatch { + return + } if !matches(cv.valueNovar, `^[#\-0-9A-Za-z._~+%*?/\[\]]*`) { cv.line.Warn1("%q is not a valid pathname mask.", cv.value) } @@ -461,6 +482,9 @@ func (cv *VartypeCheck) Pathmask() { // Like Filename, but including slashes // See http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266 func (cv *VartypeCheck) Pathname() { + if cv.op == opUseMatch { + return + } if !matches(cv.valueNovar, `^[#\-0-9A-Za-z._~+%/]*$`) { cv.line.Warn1("%q is not a valid pathname.", cv.value) } @@ -474,7 +498,7 @@ func (cv *VartypeCheck) Perl5Packlist() { } func (cv *VartypeCheck) PkgName() { - if cv.value == cv.valueNovar && !matches(cv.value, rePkgname) { + if cv.op != opUseMatch && cv.value == cv.valueNovar && !matches(cv.value, rePkgname) { cv.line.Warn1("%q is not a valid package name. A valid package name has the form packagename-version, where version consists only of digits, letters and dots.", cv.value) } } @@ -642,6 +666,9 @@ func (cv *VartypeCheck) SedCommands() { } func (cv *VartypeCheck) ShellCommand() { + if cv.op == opUseMatch { + return + } setE := true NewShellLine(cv.mkline).CheckShellCommand(cv.value, &setE) } @@ -680,7 +707,7 @@ func (cv *VartypeCheck) Tool() { default: cv.line.Error1("Unknown tool dependency %q. Use one of \"build\", \"pkgsrc\" or \"run\".", tooldep) } - } else { + } else if cv.op != opUseMatch { cv.line.Error1("Invalid tool syntax: %q.", cv.value) } } @@ -741,7 +768,11 @@ func (cv *VartypeCheck) Varname() { } func (cv *VartypeCheck) Version() { - if !matches(cv.value, `^([\d.])+$`) { + if cv.op == opUseMatch { + if !matches(cv.value, `^[\d?\[][\w\-.*?\[\]]+$`) { + cv.line.Warn1("Invalid version number pattern %q.", cv.value) + } + } else if cv.value == cv.valueNovar && !matches(cv.value, `^\d[\w.]+$`) { cv.line.Warn1("Invalid version number %q.", cv.value) } } @@ -786,31 +817,47 @@ func (cv *VartypeCheck) WrksrcSubdirectory() { } } -// Used for variables that are checked using `.if defined(VAR)`. func (cv *VartypeCheck) Yes() { - if !matches(cv.value, `^(?:YES|yes)(?:\s+#.*)?$`) { - cv.line.Warn1("%s should be set to YES or yes.", cv.varname) - Explain4( - "This variable means \"yes\" if it is defined, and \"no\" if it is", - "undefined. Even when it has the value \"no\", this means \"yes\".", - "Therefore when it is defined, its value should correspond to its", - "meaning.") + switch cv.op { + case opUseMatch: + cv.line.Warn1("%s should only be used in a \".if defined(...)\" conditional.", cv.varname) + Explain( + "This variable can have only two values: defined or undefined.", + "When it is defined, it means \"yes\", even when its value is", + "\"no\" or the empty string.", + "", + "Therefore, it should not be checked by comparing its value", + "but using \".if defined(VARNAME)\" alone.") + + default: + if !matches(cv.value, `^(?:YES|yes)(?:\s+#.*)?$`) { + cv.line.Warn1("%s should be set to YES or yes.", cv.varname) + Explain4( + "This variable means \"yes\" if it is defined, and \"no\" if it is", + "undefined. Even when it has the value \"no\", this means \"yes\".", + "Therefore when it is defined, its value should correspond to its", + "meaning.") + } } } -// The type YesNo is used for variables that are checked using -// .if defined(VAR) && !empty(VAR:M[Yy][Ee][Ss]) -// func (cv *VartypeCheck) YesNo() { - if !matches(cv.value, `^(?:YES|yes|NO|no)(?:\s+#.*)?$`) { + if cv.op == opUseMatch { + switch cv.value { + case "[yY][eE][sS]": + case "[Yy][Ee][Ss]": + case "[nN][oO]": + case "[Nn][Oo]": + default: + cv.line.Warnf("%s should be matched against %q or %q, not %q.", cv.varname, "[yY][eE][sS]", "[nN][oO]", cv.value) + } + } else if !matches(cv.value, `^(?:YES|yes|NO|no)(?:\s+#.*)?$`) { cv.line.Warn1("%s should be set to YES, yes, NO, or no.", cv.varname) } } -// Like YesNo, but the value may be produced by a shell command using the -// != operator. func (cv *VartypeCheck) YesNoIndirectly() { - if cv.valueNovar != "" && !matches(cv.value, `^(?:YES|yes|NO|no)(?:\s+#.*)?$`) { - cv.line.Warn1("%s should be set to YES, yes, NO, or no.", cv.varname) + if cv.valueNovar != "" { + cv.YesNo() } } diff --git a/pkgtools/pkglint/files/vartypecheck_test.go b/pkgtools/pkglint/files/vartypecheck_test.go index cbcb313c3b8..4cf8a54c4f7 100644 --- a/pkgtools/pkglint/files/vartypecheck_test.go +++ b/pkgtools/pkglint/files/vartypecheck_test.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + check "gopkg.in/check.v1" ) @@ -169,6 +171,16 @@ func (s *Suite) TestVartypeCheck_EmulPlatform(c *check.C) { "WARN: fname:3: \"${LINUX}\" is not a valid emulation platform.\n") } +func (s *Suite) TestVartypeCheck_Enum(c *check.C) { + runVartypeMatchChecks("JDK", enum("jdk1 jdk2 jdk4").checker, + "*", + "jdk*", + "sun-jdk*", + "${JDKNAME}") + + c.Check(s.Output(), equals, "WARN: fname:3: The pattern \"sun-jdk*\" cannot match any of { jdk1 jdk2 jdk4 } for JDK.\n") +} + func (s *Suite) TestVartypeCheck_FetchURL(c *check.C) { G.globalData.MasterSiteUrls = map[string]string{ "https://github.com/": "MASTER_SITE_GITHUB", @@ -363,6 +375,16 @@ func (s *Suite) TestVartypeCheck_Yes(c *check.C) { c.Check(s.Output(), equals, ""+ "WARN: fname:2: APACHE_MODULE should be set to YES or yes.\n"+ "WARN: fname:3: APACHE_MODULE should be set to YES or yes.\n") + + runVartypeMatchChecks("PKG_DEVELOPER", (*VartypeCheck).Yes, + "yes", + "no", + "${YESVAR}") + + c.Check(s.Output(), equals, ""+ + "WARN: fname:1: PKG_DEVELOPER should only be used in a \".if defined(...)\" conditional.\n"+ + "WARN: fname:2: PKG_DEVELOPER should only be used in a \".if defined(...)\" conditional.\n"+ + "WARN: fname:3: PKG_DEVELOPER should only be used in a \".if defined(...)\" conditional.\n") } func (s *Suite) TestVartypeCheck_YesNo(c *check.C) { @@ -389,6 +411,9 @@ func (s *Suite) TestVartypeCheck_YesNoIndirectly(c *check.C) { } func runVartypeChecks(varname string, op MkOperator, checker func(*VartypeCheck), values ...string) { + if !contains(op.String(), "=") { + panic("runVartypeChecks needs an assignment operator") + } for i, value := range values { mkline := NewMkLine(NewLine("fname", i+1, varname+op.String()+value, nil)) valueNovar := mkline.withoutMakeVariables(mkline.Value(), true) @@ -397,6 +422,16 @@ func runVartypeChecks(varname string, op MkOperator, checker func(*VartypeCheck) } } +func runVartypeMatchChecks(varname string, checker func(*VartypeCheck), values ...string) { + for i, value := range values { + text := fmt.Sprintf(".if ${%s:M%s} == \"\"", varname, value) + mkline := NewMkLine(NewLine("fname", i+1, text, nil)) + valueNovar := mkline.withoutMakeVariables(value, true) + vc := &VartypeCheck{mkline, mkline.Line, varname, opUseMatch, value, valueNovar, "", true, false} + checker(vc) + } +} + func runVartypeChecksFname(fname, varname string, op MkOperator, checker func(*VartypeCheck), values ...string) { for i, value := range values { mkline := NewMkLine(NewLine(fname, i+1, varname+op.String()+value, nil)) |