summaryrefslogtreecommitdiff
path: root/pkgtools
diff options
context:
space:
mode:
authorrillig <rillig@pkgsrc.org>2019-10-26 09:51:47 +0000
committerrillig <rillig@pkgsrc.org>2019-10-26 09:51:47 +0000
commitf912ae87030db74981604ce0d83470f3cc882357 (patch)
tree6691dc23038b4d7ca06188943e0835af07040837 /pkgtools
parentf06306b52fd7dc53b41e73845a01af35211f34f4 (diff)
downloadpkgsrc-f912ae87030db74981604ce0d83470f3cc882357.tar.gz
pkgtools/pkglint: update to 19.3.2
Changes since 19.3.1: * Pkglint no longer warns about a missing :Q modifier if there is also a :D modifier, since the latter hides the original variable value from the expression value. * Variable names like .CURDIR are now allowed in the _VARGROUPS section. * In dependency lines like "${_COOKIE.extract}:", pkglint no longer warns about the unknown target. No matter whether this is a file name or even a list of other targets, there's no chance for a typo here. * If some dependencies are included conditionally, and the package Makefile and buildlink3.mk disagree, and the conditions depend on PKG_OPTIONS, pkglint outputs a helpful explanation. * The check for including builtin.mk directly can be disabled by giving a reason in a comment at the end of the line.
Diffstat (limited to 'pkgtools')
-rw-r--r--pkgtools/pkglint/Makefile5
-rw-r--r--pkgtools/pkglint/PLIST4
-rw-r--r--pkgtools/pkglint/files/buildlink3_test.go31
-rw-r--r--pkgtools/pkglint/files/category.go2
-rw-r--r--pkgtools/pkglint/files/check_test.go2
-rw-r--r--pkgtools/pkglint/files/files_test.go4
-rw-r--r--pkgtools/pkglint/files/mkline.go485
-rw-r--r--pkgtools/pkglint/files/mkline_test.go1165
-rw-r--r--pkgtools/pkglint/files/mklinechecker.go64
-rw-r--r--pkgtools/pkglint/files/mklinechecker_test.go27
-rw-r--r--pkgtools/pkglint/files/mklineparser.go450
-rw-r--r--pkgtools/pkglint/files/mklineparser_test.go1147
-rw-r--r--pkgtools/pkglint/files/mklines.go4
-rw-r--r--pkgtools/pkglint/files/mkparser_test.go180
-rw-r--r--pkgtools/pkglint/files/mktokenslexer_test.go104
-rw-r--r--pkgtools/pkglint/files/mktypes_test.go29
-rw-r--r--pkgtools/pkglint/files/package.go14
-rw-r--r--pkgtools/pkglint/files/shtokenizer_test.go2
-rw-r--r--pkgtools/pkglint/files/substcontext.go2
-rw-r--r--pkgtools/pkglint/files/toplevel.go3
-rw-r--r--pkgtools/pkglint/files/util.go9
-rw-r--r--pkgtools/pkglint/files/varalignblock.go2
-rw-r--r--pkgtools/pkglint/files/vargroups_test.go9
-rw-r--r--pkgtools/pkglint/files/vartypecheck.go4
-rw-r--r--pkgtools/pkglint/files/vartypecheck_test.go5
25 files changed, 1990 insertions, 1763 deletions
diff --git a/pkgtools/pkglint/Makefile b/pkgtools/pkglint/Makefile
index d1cd68d1168..5df1338bee5 100644
--- a/pkgtools/pkglint/Makefile
+++ b/pkgtools/pkglint/Makefile
@@ -1,7 +1,6 @@
-# $NetBSD: Makefile,v 1.601 2019/10/18 14:58:55 bsiegert Exp $
+# $NetBSD: Makefile,v 1.602 2019/10/26 09:51:47 rillig Exp $
-PKGNAME= pkglint-19.3.1
-PKGREVISION= 1
+PKGNAME= pkglint-19.3.2
CATEGORIES= pkgtools
DISTNAME= tools
MASTER_SITES= ${MASTER_SITE_GITHUB:=golang/}
diff --git a/pkgtools/pkglint/PLIST b/pkgtools/pkglint/PLIST
index e5bc128d6f4..00571ffb5cc 100644
--- a/pkgtools/pkglint/PLIST
+++ b/pkgtools/pkglint/PLIST
@@ -1,4 +1,4 @@
-@comment $NetBSD: PLIST,v 1.14 2019/09/08 22:47:47 rillig Exp $
+@comment $NetBSD: PLIST,v 1.15 2019/10/26 09:51:47 rillig Exp $
bin/pkglint
gopkg/pkg/${GO_PLATFORM}/netbsd.org/pkglint.a
gopkg/pkg/${GO_PLATFORM}/netbsd.org/pkglint/getopt.a
@@ -52,6 +52,8 @@ gopkg/src/netbsd.org/pkglint/mkline.go
gopkg/src/netbsd.org/pkglint/mkline_test.go
gopkg/src/netbsd.org/pkglint/mklinechecker.go
gopkg/src/netbsd.org/pkglint/mklinechecker_test.go
+gopkg/src/netbsd.org/pkglint/mklineparser.go
+gopkg/src/netbsd.org/pkglint/mklineparser_test.go
gopkg/src/netbsd.org/pkglint/mklines.go
gopkg/src/netbsd.org/pkglint/mklines_test.go
gopkg/src/netbsd.org/pkglint/mkparser.go
diff --git a/pkgtools/pkglint/files/buildlink3_test.go b/pkgtools/pkglint/files/buildlink3_test.go
index c2e50d5e122..865f6a4ddf3 100644
--- a/pkgtools/pkglint/files/buildlink3_test.go
+++ b/pkgtools/pkglint/files/buildlink3_test.go
@@ -196,7 +196,7 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch__lib(c *check.C) {
t.CheckOutputEmpty()
}
-func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch__version(c *check.C) {
+func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch__version_ok(c *check.C) {
t := s.Init(c)
t.SetUpPackage("editors/emacs22",
@@ -229,6 +229,35 @@ func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch__version(c *check.C)
t.CheckOutputEmpty()
}
+func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch__version_bad(c *check.C) {
+ t := s.Init(c)
+
+ t.SetUpPackage("editors/emacs-client",
+ "PKGNAME=\temacs-client-22.0")
+ t.CreateFileLines("editors/emacs-client/buildlink3.mk",
+ MkCvsID,
+ "",
+ "BUILDLINK_TREE+=\temacs",
+ "",
+ ".if !defined(EMACS_BUILDLINK3_MK)",
+ "EMACS_BUILDLINK3_MK:=",
+ "",
+ "BUILDLINK_API_DEPENDS.emacs+=\temacs-client>=1.0",
+ "BUILDLINK_ABI_DEPENDS.emacs+=\temacs-client>=1.0",
+ "",
+ ".endif\t# EMACS_BUILDLINK3_MK",
+ "",
+ "BUILDLINK_TREE+=\t-emacs")
+ t.FinishSetUp()
+
+ G.Check(t.File("editors/emacs-client"))
+
+ t.CheckOutputLines(
+ "ERROR: ~/editors/emacs-client/buildlink3.mk:3: " +
+ "Package name mismatch between \"emacs\" in this file " +
+ "and \"emacs-client\" from Makefile:4.")
+}
+
func (s *Suite) Test_CheckLinesBuildlink3Mk__name_mismatch_multiple_inclusion(c *check.C) {
t := s.Init(c)
diff --git a/pkgtools/pkglint/files/category.go b/pkgtools/pkglint/files/category.go
index 1dc165a8348..f068419edc5 100644
--- a/pkgtools/pkglint/files/category.go
+++ b/pkgtools/pkglint/files/category.go
@@ -68,7 +68,7 @@ func CheckdirCategory(dir string) {
mlex.Skip()
name := mkline.Value()
- if mkline.IsCommentedVarassign() && mkline.VarassignComment() == "" {
+ if mkline.IsCommentedVarassign() && !mkline.HasComment() {
mkline.Warnf("%q commented out without giving a reason.", name)
}
diff --git a/pkgtools/pkglint/files/check_test.go b/pkgtools/pkglint/files/check_test.go
index 1c315e306a9..d310e867c28 100644
--- a/pkgtools/pkglint/files/check_test.go
+++ b/pkgtools/pkglint/files/check_test.go
@@ -852,7 +852,7 @@ func (t *Tester) NewMkLine(filename string, lineno int, text string) *MkLine {
basename == "mk.conf",
"filename %q must be realistic, otherwise the variable permissions are wrong", filename)
- return MkLineParser{}.Parse(t.NewLine(filename, lineno, text))
+ return NewMkLineParser().Parse(t.NewLine(filename, lineno, text))
}
func (t *Tester) NewShellLineChecker(text string) *ShellLineChecker {
diff --git a/pkgtools/pkglint/files/files_test.go b/pkgtools/pkglint/files/files_test.go
index b59de6f2ff5..48ca6babd5f 100644
--- a/pkgtools/pkglint/files/files_test.go
+++ b/pkgtools/pkglint/files/files_test.go
@@ -105,7 +105,7 @@ func (s *Suite) Test_convertToLogicalLines__comments(c *check.C) {
func (s *Suite) Test_nextLogicalLine__commented_multi(c *check.C) {
t := s.Init(c)
- mklines := t.SetUpFileMkLines("filename.mk",
+ mklines := t.NewMkLines("filename.mk",
"#COMMENTED= \\",
"#\tcontinuation 1 \\",
"#\tcontinuation 2")
@@ -113,7 +113,7 @@ func (s *Suite) Test_nextLogicalLine__commented_multi(c *check.C) {
// The leading comments are stripped from the continuation lines as well.
t.CheckEquals(mkline.Value(), "continuation 1 \tcontinuation 2")
- t.CheckEquals(mkline.VarassignComment(), "")
+ t.CheckEquals(mkline.HasComment(), false)
}
func (s *Suite) Test_convertToLogicalLines__missing_newline_at_eof(c *check.C) {
diff --git a/pkgtools/pkglint/files/mkline.go b/pkgtools/pkglint/files/mkline.go
index 6ec5b6525ac..a951e6989c1 100644
--- a/pkgtools/pkglint/files/mkline.go
+++ b/pkgtools/pkglint/files/mkline.go
@@ -15,6 +15,8 @@ import (
type MkLine struct {
*Line
+ splitResult mkLineSplitResult
+
// One of the following mkLine* types.
//
// For the larger of these types, a pointer is used instead of a direct
@@ -34,8 +36,6 @@ type mkLineAssign struct {
valueMk []*MkToken // The value, sent through splitIntoMkWords
valueMkRest string // nonempty in case of parse errors
fields []string // The value, space-separated according to shell quoting rules
- spaceAfterValue string
- comment string
}
type mkLineShell struct {
@@ -69,164 +69,28 @@ type mkLineDependency struct {
sources string
}
-type MkLineParser struct{}
-
-// Parse parses the text of a Makefile line to see what kind of line
-// it is: variable assignment, include, comment, etc.
-//
-// See devel/bmake/parse.c:/^Parse_File/
-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.
- if hasPrefix(text, " ") && line.Basename != "bsd.buildlink3.mk" {
- line.Warnf("Makefile lines should not start with space characters.")
- line.Explain(
- "If this line should be a shell command connected to a target, use a tab character for indentation.",
- "Otherwise remove the leading whitespace.")
- }
-
- // Check for shell commands first because these cannot have comments
- // at the end of the line.
- if hasPrefix(text, "\t") {
- lex := textproc.NewLexer(text)
- for lex.SkipByte('\t') {
- }
-
- // Just for the side effects of the warnings.
- _ = p.split(line, lex.Rest())
-
- return p.parseShellcmd(line)
- }
-
- data := p.split(line, text)
-
- if mkline := p.parseVarassign(line); mkline != nil {
- return mkline
- }
- if mkline := p.parseCommentOrEmpty(line); mkline != nil {
- return mkline
- }
- if mkline := p.parseDirective(line, data); mkline != nil {
- return mkline
- }
- if mkline := p.parseInclude(line); mkline != nil {
- return mkline
- }
- if mkline := p.parseSysinclude(line); mkline != nil {
- return mkline
- }
- if mkline := p.parseDependency(line); mkline != nil {
- return mkline
- }
- if mkline := p.parseMergeConflict(line); mkline != nil {
- return mkline
- }
-
- // The %q is deliberate here since it shows possible strange characters.
- line.Errorf("Unknown Makefile line format: %q.", text)
- return &MkLine{line, nil}
-}
-
-func (p MkLineParser) parseVarassign(line *Line) *MkLine {
- m, a := p.MatchVarassign(line, line.Text)
- if !m {
- return nil
- }
-
- if a.spaceAfterVarname != "" {
- varname := a.varname
- op := a.op
- switch {
- case hasSuffix(varname, "+") && (op == opAssign || op == opAssignAppend):
- break
- case matches(varname, `^[a-z]`) && op == opAssignEval:
- break
- default:
- fix := line.Autofix()
- fix.Notef("Unnecessary space after variable name %q.", varname)
- fix.Replace(varname+a.spaceAfterVarname+op.String(), varname+op.String())
- fix.Apply()
- }
- }
-
- if a.comment != "" && a.value != "" && a.spaceAfterValue == "" {
- line.Warnf("The # character starts a Makefile comment.")
- line.Explain(
- "In a variable assignment, an unescaped # starts a comment that",
- "continues until the end of the line.",
- "To escape the #, write \\#.",
- "",
- "If this # character intentionally starts a comment,",
- "it should be preceded by a space in order to make it more visible.")
- }
-
- return &MkLine{line, a}
-}
-
-func (p MkLineParser) parseShellcmd(line *Line) *MkLine {
- return &MkLine{line, mkLineShell{line.Text[1:]}}
-}
-
-func (p MkLineParser) parseCommentOrEmpty(line *Line) *MkLine {
- trimmedText := trimHspace(line.Text)
-
- if strings.HasPrefix(trimmedText, "#") {
- return &MkLine{line, mkLineComment{}}
- }
-
- if trimmedText == "" {
- return &MkLine{line, mkLineEmpty{}}
- }
-
- return nil
-}
-
-func (p MkLineParser) parseInclude(line *Line) *MkLine {
- m, indent, directive, includedFile := MatchMkInclude(line.Text)
- if !m {
- return nil
- }
-
- return &MkLine{line, &mkLineInclude{directive == "include", false, indent, includedFile, nil}}
-}
-
-func (p MkLineParser) parseSysinclude(line *Line) *MkLine {
- m, indent, directive, includedFile := match3(line.Text, `^\.([\t ]*)(s?include)[\t ]+<([^>]+)>[\t ]*(?:#.*)?$`)
- if !m {
- return nil
- }
-
- return &MkLine{line, &mkLineInclude{directive == "include", true, indent, includedFile, nil}}
-}
-
-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 ]*#.*)?$`)
- if !m {
- return nil
- }
-
- if whitespace != "" {
- line.Notef("Space before colon in dependency line.")
- }
- return &MkLine{line, mkLineDependency{targets, sources}}
-}
-
-func (p MkLineParser) parseMergeConflict(line *Line) *MkLine {
- if !matches(line.Text, `^(<<<<<<<|=======|>>>>>>>)`) {
- return nil
- }
-
- return &MkLine{line, nil}
-}
-
// String returns the filename and line numbers.
func (mkline *MkLine) String() string {
return sprintf("%s:%s", mkline.Filename, mkline.Linenos())
}
+func (mkline *MkLine) HasComment() bool { return mkline.splitResult.hasComment }
+
+// Comment returns the comment after the first unescaped #.
+//
+// A special case are variable assignments. If these are commented out
+// entirely, they still count as variable assignments, which means that
+// their comment is the one after the value, if any.
+//
+// Shell commands (lines that start with a tab) cannot have comments, as
+// the # characters are passed uninterpreted to the shell.
+//
+// Example:
+// VAR=value # comment
+//
+// In the above line, the comment is " comment", including the leading space.
+func (mkline *MkLine) Comment() string { return mkline.splitResult.comment }
+
// IsVarassign returns true for variable assignments of the form VAR=value.
//
// See IsCommentedVarassign.
@@ -348,16 +212,6 @@ func (mkline *MkLine) ValueAlign() string { return mkline.data.(*mkLineAssign).v
func (mkline *MkLine) Value() string { return mkline.data.(*mkLineAssign).value }
-// VarassignComment applies to variable assignments and returns the comment.
-//
-// Example:
-// VAR=value # comment
-//
-// 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 *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.
//
@@ -371,7 +225,9 @@ func (mkline *MkLine) FirstLineContainsValue() bool {
// Parsing the continuation marker as variable value is cheating but works well.
text := strings.TrimSuffix(mkline.raw[0].orignl, "\n")
- _, a := MkLineParser{}.MatchVarassign(mkline.Line, text)
+ parser := NewMkLineParser()
+ splitResult := parser.split(nil, text, true)
+ _, a := parser.MatchVarassign(mkline.Line, text, &splitResult)
return a.value != "\\"
}
@@ -787,180 +643,6 @@ var (
unescapeMkCommentSafeChars = textproc.NewByteSet("\\#[\n").Inverse()
)
-// unescapeComment takes a Makefile line, as written in a file, and splits
-// it into the main part and the comment.
-//
-// The comment starts at the first #. Except if it is preceded by an odd number
-// of backslashes. Or by an opening bracket.
-//
-// The main text is returned including leading and trailing whitespace. Any
-// escaped # is returned in its unescaped form, that is, \# becomes #.
-//
-// The comment is returned including the leading "#", if any. If the line has
-// no comment, it is an empty string.
-func (MkLineParser) unescapeComment(text string) (main, comment string) {
- var sb strings.Builder
-
- lexer := textproc.NewLexer(text)
-
-again:
- if plain := lexer.NextBytesSet(unescapeMkCommentSafeChars); plain != "" {
- sb.WriteString(plain)
- goto again
- }
-
- switch {
- case lexer.SkipString("\\#"):
- sb.WriteByte('#')
-
- case lexer.PeekByte() == '\\' && len(lexer.Rest()) >= 2:
- sb.WriteString(lexer.Rest()[:2])
- lexer.Skip(2)
-
- case lexer.SkipByte('\\'):
- sb.WriteByte('\\')
-
- case lexer.SkipString("[#"):
- // See devel/bmake/files/parse.c:/as in modifier/
- sb.WriteString("[#")
-
- case lexer.SkipByte('['):
- sb.WriteByte('[')
-
- default:
- main = sb.String()
- if lexer.PeekByte() == '#' {
- return main, lexer.Rest()
- }
-
- assert(lexer.EOF())
- return main, ""
- }
-
- goto again
-}
-
-type mkLineSplitResult struct {
- // The text of the line, without the comment at the end of the line,
- // and with # signs unescaped.
- main string
- tokens []*MkToken
- spaceBeforeComment string
- hasComment bool
- comment string
-}
-
-// split parses a logical line from a Makefile (that is, after joining
-// the lines that end in a backslash) into two parts: the main part and the
-// comment.
-//
-// 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.
-//
-// If line is given, it is used for logging parse errors and warnings
-// about round parentheses instead of curly braces, as well as ambiguous
-// variables of the form $v instead of ${v}.
-func (MkLineParser) split(line *Line, text string) mkLineSplitResult {
- assert(!hasPrefix(text, "\t"))
-
- mainWithSpaces, comment := MkLineParser{}.unescapeComment(text)
-
- parser := NewMkParser(line, mainWithSpaces)
- lexer := parser.lexer
-
- parseOther := func() string {
- var sb strings.Builder
-
- for !lexer.EOF() {
- if lexer.SkipString("$$") {
- sb.WriteString("$$")
- continue
- }
-
- other := lexer.NextBytesFunc(func(b byte) bool { return b != '$' })
- if other == "" {
- break
- }
-
- sb.WriteString(other)
- }
-
- return sb.String()
- }
-
- var tokens []*MkToken
- for !lexer.EOF() {
- mark := lexer.Mark()
-
- if varUse := parser.VarUse(); varUse != nil {
- tokens = append(tokens, &MkToken{lexer.Since(mark), varUse})
-
- } else if other := parseOther(); other != "" {
- tokens = append(tokens, &MkToken{other, nil})
-
- } else {
- assert(lexer.SkipByte('$'))
- tokens = append(tokens, &MkToken{"$", nil})
- }
- }
-
- hasComment := comment != ""
- if hasComment {
- comment = comment[1:]
- }
-
- mainTrimmed := rtrimHspace(mainWithSpaces)
- spaceBeforeComment := mainWithSpaces[len(mainTrimmed):]
- if spaceBeforeComment != "" {
- tokenText := &tokens[len(tokens)-1].Text
- *tokenText = rtrimHspace(*tokenText)
- if *tokenText == "" {
- if len(tokens) == 1 {
- tokens = nil
- } else {
- tokens = tokens[:len(tokens)-1]
- }
- }
- }
-
- return mkLineSplitResult{mainTrimmed, tokens, spaceBeforeComment, hasComment, comment}
-}
-
-func (p MkLineParser) parseDirective(line *Line, data mkLineSplitResult) *MkLine {
- text := line.Text
- if !hasPrefix(text, ".") {
- return nil
- }
-
- lexer := textproc.NewLexer(data.main[1:])
-
- indent := lexer.NextHspace()
- directive := lexer.NextBytesSet(LowerDash)
- switch directive {
- case "if", "else", "elif", "endif",
- "ifdef", "ifndef",
- "for", "endfor", "undef",
- "error", "warning", "info",
- "export", "export-env", "unexport", "unexport-env":
- break
- default:
- // Intentionally not supported are: ifmake ifnmake elifdef elifndef elifmake elifnmake.
- return nil
- }
-
- lexer.SkipHspace()
-
- args := lexer.Rest()
-
- // In .if and .endif lines the space surrounding the comment is irrelevant.
- // Especially for checking that the .endif comment matches the .if condition,
- // it must be trimmed.
- trimmedComment := trimHspace(data.comment)
-
- return &MkLine{line, &mkLineDirective{indent, directive, args, trimmedComment, nil, nil, nil}}
-}
-
// VariableNeedsQuoting determines whether the given variable needs the :Q
// modifier in the given context.
//
@@ -975,10 +657,10 @@ func (mkline *MkLine) VariableNeedsQuoting(mklines *MkLines, varuse *MkVarUse, v
// TODO: Systematically test this function, each and every case, from top to bottom.
// TODO: Re-check the order of all these if clauses whether it really makes sense.
- if varuse.HasModifier("D") && varuse.HasModifier("U") {
- // Take the simple way for now. Handling this kind of
- // conditional expressions correctly and completely would
- // require a larger rewrite.
+ if varuse.HasModifier("D") {
+ // The :D modifier discards the value of the original variable and
+ // replaces it with the expression from the :D modifier.
+ // Therefore the original variable does not need to be quoted.
return unknown
}
@@ -1587,119 +1269,6 @@ var (
VarparamBytes = textproc.NewByteSet("A-Za-z_0-9#*+---./[")
)
-func (p MkLineParser) MatchVarassign(line *Line, text string) (bool, *mkLineAssign) {
-
- // A commented variable assignment does not have leading whitespace.
- // Otherwise line 1 of almost every Makefile fragment would need to
- // be scanned for a variable assignment even though it only contains
- // the $NetBSD CVS Id.
- clex := textproc.NewLexer(text)
- commented := clex.SkipByte('#')
- if commented && clex.SkipHspace() || clex.EOF() {
- return false, nil
- }
-
- withoutLeadingComment := text
- if commented {
- withoutLeadingComment = withoutLeadingComment[1:]
- }
-
- data := p.split(nil, withoutLeadingComment)
-
- lexer := NewMkTokensLexer(data.tokens)
- mainStart := lexer.Mark()
-
- for !commented && lexer.SkipByte(' ') {
- }
-
- varnameStart := lexer.Mark()
- // TODO: duplicated code in MkParser.Varname
- for lexer.NextBytesSet(VarbaseBytes) != "" || lexer.NextVarUse() != nil {
- }
- if lexer.SkipByte('.') || hasPrefix(data.main, "SITES_") {
- for lexer.NextBytesSet(VarparamBytes) != "" || lexer.NextVarUse() != nil {
- }
- }
-
- varname := lexer.Since(varnameStart)
-
- if varname == "" {
- return false, nil
- }
-
- spaceAfterVarname := lexer.NextHspace()
-
- opStart := lexer.Mark()
- switch lexer.PeekByte() {
- case '!', '+', ':', '?':
- lexer.Skip(1)
- }
- if !lexer.SkipByte('=') {
- return false, nil
- }
- op := NewMkOperator(lexer.Since(opStart))
-
- if hasSuffix(varname, "+") && op == opAssign && spaceAfterVarname == "" {
- varname = varname[:len(varname)-1]
- op = opAssignAppend
- }
-
- lexer.SkipHspace()
-
- value := trimHspace(lexer.Rest())
- parsedValueAlign := condStr(commented, "#", "") + lexer.Since(mainStart)
- valueAlign := p.getRawValueAlign(line.raw[0].orignl, parsedValueAlign)
- spaceBeforeComment := data.spaceBeforeComment
- if value == "" {
- valueAlign += spaceBeforeComment
- spaceBeforeComment = ""
- }
-
- return true, &mkLineAssign{
- commented: commented,
- varname: varname,
- varcanon: varnameCanon(varname),
- varparam: varnameParam(varname),
- spaceAfterVarname: spaceAfterVarname,
- op: op,
- valueAlign: valueAlign,
- value: value,
- valueMk: nil, // filled in lazily
- valueMkRest: "", // filled in lazily
- fields: nil, // filled in lazily
- spaceAfterValue: spaceBeforeComment,
- comment: condStr(data.hasComment, "#", "") + data.comment,
- }
-}
-
-func (MkLineParser) getRawValueAlign(raw, parsed string) string {
- r := textproc.NewLexer(raw)
- p := textproc.NewLexer(parsed)
- mark := r.Mark()
-
- for !p.EOF() {
- pch := p.PeekByte()
- rch := r.PeekByte()
-
- switch {
- case pch == rch:
- p.Skip(1)
- r.Skip(1)
-
- case pch == ' ', pch == '\t':
- p.SkipHspace()
- r.SkipHspace()
-
- default:
- assert(pch == '#')
- assert(r.SkipString("\\#"))
- p.Skip(1)
- }
- }
-
- return r.Since(mark)
-}
-
func MatchMkInclude(text string) (m bool, indentation, directive, filename string) {
lexer := textproc.NewLexer(text)
if lexer.SkipByte('.') {
@@ -1717,7 +1286,7 @@ func MatchMkInclude(text string) (m bool, indentation, directive, filename strin
filename = lexer.NextBytesFunc(func(c byte) bool { return c != '"' })
if filename != "" && lexer.SkipByte('"') {
lexer.NextHspace()
- if lexer.EOF() || lexer.SkipByte('#') {
+ if lexer.EOF() {
m = true
return
}
diff --git a/pkgtools/pkglint/files/mkline_test.go b/pkgtools/pkglint/files/mkline_test.go
index 49729c99fd5..544f768ed99 100644
--- a/pkgtools/pkglint/files/mkline_test.go
+++ b/pkgtools/pkglint/files/mkline_test.go
@@ -2,273 +2,8 @@ package pkglint
import (
"gopkg.in/check.v1"
- "strings"
)
-func (s *Suite) Test_MkLineParser_Parse__varassign(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- "VARNAME.param?=value # varassign comment")
-
- t.CheckEquals(mkline.IsVarassign(), true)
- t.CheckEquals(mkline.Varname(), "VARNAME.param")
- t.CheckEquals(mkline.Varcanon(), "VARNAME.*")
- t.CheckEquals(mkline.Varparam(), "param")
- t.CheckEquals(mkline.Op(), opAssignDefault)
- t.CheckEquals(mkline.Value(), "value")
- t.CheckEquals(mkline.VarassignComment(), "# varassign comment")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__varassign_empty_multiline(c *check.C) {
- t := s.Init(c)
-
- mklines := t.NewMkLines("test.mk",
- "VAR=\t\\",
- "\t\\",
- "\t\\",
- "\t# nothing",
- "",
- "VAR=\t1\\",
- "\t\\",
- "\t\\",
- "\t# a single letter")
-
- // Bmake and pkglint agree that the variable value is an empty string.
- // They don't agree on the exact whitespace in the line, though,
- // but this doesn't matter in practice. To see the difference, run:
- // bmake -dA 2>&1 | grep 'ParseReadLine.*VAR'
- // See devel/bmake/files/parse.c:/non-comment, non-blank line/
- t.CheckEquals(mklines.mklines[0].Text, "VAR= # nothing")
- t.CheckEquals(mklines.mklines[2].Text, "VAR=\t1 # a single letter")
-
- mkline := mklines.mklines[0]
- t.CheckEquals(mkline.IsVarassign(), true)
- t.CheckEquals(mkline.Varname(), "VAR")
- t.CheckEquals(mkline.Op(), opAssign)
- t.CheckEquals(mkline.Value(), "")
- t.CheckEquals(mkline.VarassignComment(), "# nothing")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__varassign_space_around_operator(c *check.C) {
- t := s.Init(c)
-
- t.SetUpCommandLine("--show-autofix", "--source")
- t.NewMkLine("test.mk", 101,
- "pkgbase = package")
-
- t.CheckOutputLines(
- "NOTE: test.mk:101: Unnecessary space after variable name \"pkgbase\".",
- "AUTOFIX: test.mk:101: Replacing \"pkgbase =\" with \"pkgbase=\".",
- "-\tpkgbase = package",
- "+\tpkgbase= package")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__shellcmd(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- "\tshell command # shell comment")
-
- t.CheckEquals(mkline.IsShellCommand(), true)
- t.CheckEquals(mkline.ShellCommand(), "shell command # shell comment")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__comment(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- "# whole line comment")
-
- t.CheckEquals(mkline.IsComment(), true)
-}
-
-func (s *Suite) Test_MkLineParser_Parse__empty(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101, "")
-
- t.CheckEquals(mkline.IsEmpty(), true)
-}
-
-func (s *Suite) Test_MkLineParser_Parse__directive(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- ". if !empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[\\#]} == 1 # directive comment")
-
- t.CheckEquals(mkline.IsDirective(), true)
- t.CheckEquals(mkline.Indent(), " ")
- t.CheckEquals(mkline.Directive(), "if")
- t.CheckEquals(mkline.Args(), "!empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[#]} == 1")
- t.CheckEquals(mkline.DirectiveComment(), "directive comment")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__include(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- ". include \"../../mk/bsd.prefs.mk\" # include comment")
-
- t.CheckEquals(mkline.IsInclude(), true)
- t.CheckEquals(mkline.Indent(), " ")
- t.CheckEquals(mkline.MustExist(), true)
- t.CheckEquals(mkline.IncludedFile(), "../../mk/bsd.prefs.mk")
-
- t.CheckEquals(mkline.IsSysinclude(), false)
-}
-
-func (s *Suite) Test_MkLineParser_Parse__sysinclude(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- ". include <subdir.mk> # sysinclude comment")
-
- t.CheckEquals(mkline.IsSysinclude(), true)
- t.CheckEquals(mkline.Indent(), " ")
- t.CheckEquals(mkline.MustExist(), true)
- t.CheckEquals(mkline.IncludedFile(), "subdir.mk")
-
- t.CheckEquals(mkline.IsInclude(), false)
-}
-
-func (s *Suite) Test_MkLineParser_Parse__dependency(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- "target1 target2: source1 source2")
-
- t.CheckEquals(mkline.IsDependency(), true)
- t.CheckEquals(mkline.Targets(), "target1 target2")
- t.CheckEquals(mkline.Sources(), "source1 source2")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__dependency_space(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- "target : source")
-
- t.CheckEquals(mkline.Targets(), "target")
- t.CheckEquals(mkline.Sources(), "source")
- t.CheckOutputLines(
- "NOTE: test.mk:101: Space before colon in dependency line.")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__varassign_append(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- "VARNAME+=value")
-
- t.CheckEquals(mkline.IsVarassign(), true)
- t.CheckEquals(mkline.Varname(), "VARNAME")
- t.CheckEquals(mkline.Varcanon(), "VARNAME")
- t.CheckEquals(mkline.Varparam(), "")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__merge_conflict(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("test.mk", 101,
- "<<<<<<<<<<<<<<<<<")
-
- // Merge conflicts are of neither type.
- t.CheckEquals(mkline.IsVarassign(), false)
- t.CheckEquals(mkline.IsDirective(), false)
- t.CheckEquals(mkline.IsInclude(), false)
- t.CheckEquals(mkline.IsEmpty(), false)
- t.CheckEquals(mkline.IsComment(), false)
- t.CheckEquals(mkline.IsDependency(), false)
- t.CheckEquals(mkline.IsShellCommand(), false)
- t.CheckEquals(mkline.IsSysinclude(), false)
-}
-
-func (s *Suite) Test_MkLineParser_Parse__autofix_space_after_varname(c *check.C) {
- t := s.Init(c)
-
- t.SetUpCommandLine("-Wspace")
- filename := t.CreateFileLines("Makefile",
- MkCvsID,
- "VARNAME +=\t${VARNAME}",
- "VARNAME+ =\t${VARNAME+}",
- "VARNAME+ +=\t${VARNAME+}",
- "VARNAME+ ?=\t${VARNAME}",
- "pkgbase := pkglint")
-
- CheckFileMk(filename)
-
- t.CheckOutputLines(
- "NOTE: ~/Makefile:2: Unnecessary space after variable name \"VARNAME\".",
-
- // The assignment operators other than = and += cannot lead to ambiguities.
- "NOTE: ~/Makefile:5: Unnecessary space after variable name \"VARNAME+\".")
-
- t.SetUpCommandLine("-Wspace", "--autofix")
-
- CheckFileMk(filename)
-
- t.CheckOutputLines(
- "AUTOFIX: ~/Makefile:2: Replacing \"VARNAME +=\" with \"VARNAME+=\".",
- "AUTOFIX: ~/Makefile:5: Replacing \"VARNAME+ ?=\" with \"VARNAME+?=\".")
- t.CheckFileLines("Makefile",
- MkCvsID+"",
- "VARNAME+=\t${VARNAME}",
- "VARNAME+ =\t${VARNAME+}",
- "VARNAME+ +=\t${VARNAME+}",
- "VARNAME+?=\t${VARNAME}",
- "pkgbase := pkglint")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__varname_with_hash(c *check.C) {
- t := s.Init(c)
-
- mkline := t.NewMkLine("Makefile", 123, "VARNAME.#=\tvalue")
-
- // Parse error because the # starts a comment.
- t.CheckEquals(mkline.IsVarassign(), false)
-
- mkline2 := t.NewMkLine("Makefile", 124, "VARNAME.\\#=\tvalue")
-
- t.CheckEquals(mkline2.IsVarassign(), true)
- t.CheckEquals(mkline2.Varname(), "VARNAME.#")
-
- t.CheckOutputLines(
- "ERROR: Makefile:123: Unknown Makefile line format: \"VARNAME.#=\\tvalue\".")
-}
-
-// Ensures that pkglint parses escaped # characters in the same way as bmake.
-//
-// To check that bmake parses them the same, set a breakpoint after the t.NewMkLines
-// and look in t.tmpdir for the location of the file. Then run bmake with that file.
-func (s *Suite) Test_MkLineParser_Parse__escaped_hash_in_value(c *check.C) {
- t := s.Init(c)
-
- mklines := t.SetUpFileMkLines("Makefile",
- "VAR0=\tvalue#",
- "VAR1=\tvalue\\#",
- "VAR2=\tvalue\\\\#",
- "VAR3=\tvalue\\\\\\#",
- "VAR4=\tvalue\\\\\\\\#",
- "",
- "all:",
- ".for var in VAR0 VAR1 VAR2 VAR3 VAR4",
- "\t@printf '%s\\n' ${${var}}''",
- ".endfor")
- parsed := mklines.mklines
-
- t.CheckEquals(parsed[0].Value(), "value")
- t.CheckEquals(parsed[1].Value(), "value#")
- t.CheckEquals(parsed[2].Value(), "value\\\\")
- t.CheckEquals(parsed[3].Value(), "value\\\\#")
- t.CheckEquals(parsed[4].Value(), "value\\\\\\\\")
-
- t.CheckOutputLines(
- "WARN: ~/Makefile:1: The # character starts a Makefile comment.",
- "WARN: ~/Makefile:3: The # character starts a Makefile comment.",
- "WARN: ~/Makefile:5: The # character starts a Makefile comment.")
-}
-
func (s *Suite) Test_MkLine_Varparam(c *check.C) {
t := s.Init(c)
@@ -293,26 +28,45 @@ func (s *Suite) Test_MkLine_ValueAlign__commented(c *check.C) {
func (s *Suite) Test_MkLine_FirstLineContainsValue(c *check.C) {
t := s.Init(c)
- mklines := t.NewMkLines("filename.mk",
- MkCvsID,
- "VAR=\tvalue",
- "VAR= value \\",
- "\tstarts in first line",
- "VAR= \\",
- "\tvalue starts in second line",
- "#VAR= value \\",
- "\tstarts in first line",
- "#VAR= \\",
- "\tvalue starts in second line")
+ lines := func(texts ...string) []string { return texts }
+ test := func(lines []string, expected bool) {
+ mklines := t.NewMkLines("filename.mk", lines...)
+ actual := mklines.mklines[0].FirstLineContainsValue()
+ t.CheckEquals(actual, expected)
+ }
+ testAssert := func(lines ...string) {
+ mklines := t.NewMkLines("filename.mk", lines...)
+ t.ExpectAssert(func() { mklines.mklines[0].FirstLineContainsValue() })
+ }
+
+ // Not a variable assignment.
+ testAssert(MkCvsID)
- t.ExpectAssert(func() { mklines.mklines[0].FirstLineContainsValue() })
+ // Not a multiline variable assignment.
+ testAssert("VAR=\tvalue")
- t.ExpectAssert(func() { mklines.mklines[1].FirstLineContainsValue() })
+ test(
+ lines(
+ "VAR= value \\",
+ "\tstarts in first line"),
+ true)
+
+ test(
+ lines(
+ "VAR= \\",
+ "\tvalue starts in second line"),
+ false)
+
+ test(
+ lines(
+ "#VAR= value \\",
+ "\tstarts in first line"),
+ true)
- t.CheckEquals(mklines.mklines[2].FirstLineContainsValue(), true)
- t.CheckEquals(mklines.mklines[3].FirstLineContainsValue(), false)
- t.CheckEquals(mklines.mklines[4].FirstLineContainsValue(), true)
- t.CheckEquals(mklines.mklines[5].FirstLineContainsValue(), false)
+ test(lines(
+ "#VAR= \\",
+ "\tvalue starts in second line"),
+ false)
}
// Up to July 2019, there was a method MkLine.IsMultiAligned, which has
@@ -510,86 +264,6 @@ func (s *Suite) Test_VarUseContext_String(c *check.C) {
t.CheckEquals(vuc.String(), "(Pkgname (package-settable) time:unknown quoting:backt wordpart:false)")
}
-// In variable assignments, a plain '#' introduces a line comment, unless
-// it is escaped by a backslash. In shell commands, on the other hand, it
-// is interpreted literally.
-func (s *Suite) Test_MkLineParser_Parse__number_sign(c *check.C) {
- t := s.Init(c)
-
- mklineVarassignEscaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,\\#,hash,g'")
-
- t.CheckEquals(mklineVarassignEscaped.Varname(), "SED_CMD")
- t.CheckEquals(mklineVarassignEscaped.Value(), "'s,#,hash,g'")
-
- mklineCommandEscaped := t.NewMkLine("filename.mk", 1, "\tsed -e 's,\\#,hash,g'")
-
- t.CheckEquals(mklineCommandEscaped.ShellCommand(), "sed -e 's,\\#,hash,g'")
-
- // From shells/zsh/Makefile.common, rev. 1.78
- mklineCommandUnescaped := t.NewMkLine("filename.mk", 1, "\t# $ sha1 patches/patch-ac")
-
- t.CheckEquals(mklineCommandUnescaped.ShellCommand(), "# $ sha1 patches/patch-ac")
- t.CheckOutputEmpty() // No warning about parsing the lonely dollar sign.
-
- mklineVarassignUnescaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,#,hash,'")
-
- t.CheckEquals(mklineVarassignUnescaped.Value(), "'s,")
- t.CheckOutputLines(
- "WARN: filename.mk:1: The # character starts a Makefile comment.")
-}
-
-func (s *Suite) Test_MkLineParser_Parse__varassign_leading_space(c *check.C) {
- t := s.Init(c)
-
- _ = t.NewMkLine("rubyversion.mk", 427, " _RUBYVER=\t2.15")
- _ = t.NewMkLine("bsd.buildlink3.mk", 132, " ok:=yes")
-
- // In mk/buildlink3/bsd.buildlink3.mk, the leading space is really helpful,
- // therefore no warnings for that file.
- t.CheckOutputLines(
- "WARN: rubyversion.mk:427: Makefile lines should not start with space characters.")
-}
-
-// Exotic code examples from the pkgsrc infrastructure.
-// Hopefully, pkgsrc packages don't need such complicated code.
-// Still, pkglint needs to parse them correctly, or it would not
-// be able to parse and check the infrastructure files as well.
-//
-// See Pkgsrc.loadUntypedVars.
-func (s *Suite) Test_MkLineParser_Parse__infrastructure(c *check.C) {
- t := s.Init(c)
-
- mklines := t.NewMkLines("infra.mk",
- MkCvsID,
- " USE_BUILTIN.${_pkg_:S/^-//}:=no",
- ".error \"Something went wrong\"",
- ".export WRKDIR",
- ".export",
- ".unexport-env WRKDIR",
- "",
- ".ifmake target1", // Luckily, this is not used in the wild.
- ".elifnmake target2", // Neither is this.
- ".endif")
-
- t.CheckEquals(mklines.mklines[1].Varcanon(), "USE_BUILTIN.*")
- t.CheckEquals(mklines.mklines[2].Directive(), "error")
- t.CheckEquals(mklines.mklines[3].Directive(), "export")
-
- t.CheckOutputLines(
- "WARN: infra.mk:2: Makefile lines should not start with space characters.",
- "ERROR: infra.mk:8: Unknown Makefile line format: \".ifmake target1\".",
- "ERROR: infra.mk:9: Unknown Makefile line format: \".elifnmake target2\".")
-
- mklines.Check()
-
- t.CheckOutputLines(
- "WARN: infra.mk:2: USE_BUILTIN.${_pkg_:S/^-//} is defined but not used.",
- "WARN: infra.mk:2: _pkg_ is used but not defined.",
- "ERROR: infra.mk:5: \".export\" requires arguments.",
- "NOTE: infra.mk:2: This variable value should be aligned to column 41.",
- "ERROR: infra.mk:10: Unmatched .endif.")
-}
-
func (s *Suite) Test_MkLine_VariableNeedsQuoting__unknown_rhs(c *check.C) {
t := s.Init(c)
@@ -1068,9 +742,7 @@ func (s *Suite) Test_MkLine_VariableNeedsQuoting__only_D_modifier(c *check.C) {
// If any, the variable PKGSRCDIR should be quoted, but that is a safe
// variable since it is a pkgsrc-specific directory and it appears as
// part of a word, therefore it cannot result in an empty string.
- // FIXME: Don't warn in this situation.
- t.CheckOutputLines(
- "WARN: ~/Makefile:6: The variable BATCH should be quoted as part of a shell word.")
+ t.CheckOutputEmpty()
}
// As of October 2018, these examples from real pkgsrc end up in the
@@ -1458,12 +1130,11 @@ func (s *Suite) Test_MkLine_ValueFields__compared_to_splitIntoShellTokens(c *che
func (s *Suite) Test_MkLine_ValueTokens(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
+ text := b.TextToken
+ varUseText := b.VaruseTextToken
+ tokens := b.Tokens
- text := func(text string) *MkToken { return &MkToken{text, nil} }
- varUseText := func(text string, varname string, modifiers ...string) *MkToken {
- return &MkToken{text, NewMkVarUse(varname, modifiers...)}
- }
- tokens := func(tokens ...*MkToken) []*MkToken { return tokens }
test := func(value string, expected []*MkToken, diagnostics ...string) {
mkline := t.NewMkLine("Makefile", 1, "PATH=\t"+value)
actualTokens, _ := mkline.ValueTokens()
@@ -1516,16 +1187,15 @@ func (s *Suite) Test_MkLine_ValueTokens__parse_error(c *check.C) {
func (s *Suite) Test_MkLine_ValueTokens__caching(c *check.C) {
t := s.Init(c)
-
- tokens := func(tokens ...*MkToken) []*MkToken { return tokens }
+ b := NewMkTokenBuilder()
mkline := t.NewMkLine("Makefile", 1, "PATH=\tvalue ${UNFINISHED")
valueTokens, rest := mkline.ValueTokens()
t.CheckDeepEquals(valueTokens,
- tokens(
- &MkToken{"value ", nil},
- &MkToken{"${UNFINISHED", NewMkVarUse("UNFINISHED")}))
+ b.Tokens(
+ b.TextToken("value "),
+ b.VaruseTextToken("${UNFINISHED", "UNFINISHED")))
t.CheckEquals(rest, "")
t.CheckOutputLines(
"WARN: Makefile:1: Missing closing \"}\" for \"UNFINISHED\".")
@@ -1539,16 +1209,12 @@ func (s *Suite) Test_MkLine_ValueTokens__caching(c *check.C) {
func (s *Suite) Test_MkLine_ValueTokens__caching_parse_error(c *check.C) {
t := s.Init(c)
-
- tokens := func(tokens ...*MkToken) []*MkToken { return tokens }
- varuseText := func(text, varname string, modifiers ...string) *MkToken {
- return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
- }
+ b := NewMkTokenBuilder()
mkline := t.NewMkLine("Makefile", 1, "PATH=\t${UNFINISHED")
valueTokens, rest := mkline.ValueTokens()
- t.CheckDeepEquals(valueTokens, tokens(varuseText("${UNFINISHED", "UNFINISHED")))
+ t.CheckDeepEquals(valueTokens, b.Tokens(b.VaruseTextToken("${UNFINISHED", "UNFINISHED")))
t.CheckEquals(rest, "")
t.CheckOutputLines(
"WARN: Makefile:1: Missing closing \"}\" for \"UNFINISHED\".")
@@ -1650,209 +1316,6 @@ func (s *Suite) Test_MkLine_ResolveVarsInRelativePath__without_tracing(c *check.
"WARN: ~/buildlink3.mk:2: PKGPATH.multimedia/totem is used but not defined.")
}
-func (s *Suite) Test_MkLineParser_MatchVarassign(c *check.C) {
- t := s.Init(c)
-
- testLine := func(line *Line, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string, diagnostics ...string) {
- text := line.Text
-
- m, actual := MkLineParser{}.MatchVarassign(line, text)
-
- assert(m)
- expected := mkLineAssign{
- commented: commented,
- varname: varname,
- varcanon: varnameCanon(varname),
- varparam: varnameParam(varname),
- spaceAfterVarname: spaceAfterVarname,
- op: NewMkOperator(op),
- valueAlign: align,
- value: value,
- valueMk: nil,
- valueMkRest: "",
- fields: nil,
- spaceAfterValue: spaceAfterValue,
- comment: comment,
- }
- t.CheckDeepEquals(*actual, expected)
- t.CheckOutput(diagnostics)
- }
-
- test := func(text string, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string, diagnostics ...string) {
- line := t.NewLine("filename.mk", 123, text)
- testLine(line, commented, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment, diagnostics...)
- }
-
- testInvalid := func(text string, diagnostics ...string) {
- line := t.NewLine("filename.mk", 123, text)
- m, _ := MkLineParser{}.MatchVarassign(line, text)
- if m {
- c.Errorf("Text %q matches variable assignment but shouldn't.", text)
- }
- t.CheckOutput(diagnostics)
- }
-
- lines := func(text ...string) *Line {
- mklines := t.NewMkLines("filename.mk",
- text...)
- return mklines.mklines[0].Line
- }
-
- test("C++=c11", false, "C+", "", "+=", "C++=", "c11", "", "")
- test("V=v", false, "V", "", "=", "V=", "v", "", "")
- test("VAR=#comment", false, "VAR", "", "=", "VAR=", "", "", "#comment")
- test("VAR=\\#comment", false, "VAR", "", "=", "VAR=", "#comment", "", "")
- test("VAR=\\\\\\##comment", false, "VAR", "", "=", "VAR=", "\\\\#", "", "#comment")
- test("VAR=\\", false, "VAR", "", "=", "VAR=", "\\", "", "")
- test("VAR += value", false, "VAR", " ", "+=", "VAR += ", "value", "", "")
- test(" VAR=value", false, "VAR", "", "=", " VAR=", "value", "", "")
- test("VAR=value #comment", false, "VAR", "", "=", "VAR=", "value", " ", "#comment")
- test("NFILES=${FILES:[#]}", false, "NFILES", "", "=", "NFILES=", "${FILES:[#]}", "", "")
-
- // To humans, the base variable name seems to be SITES_, being parameterized
- // with distfile-1.0.tar.gz. For pkglint though, the base variable name is
- // SITES_distfile-1.
- test("SITES_distfile-1.0.tar.gz=https://example.org/",
- false,
- "SITES_distfile-1.0.tar.gz",
- "",
- "=",
- "SITES_distfile-1.0.tar.gz=",
- "https://example.org/",
- "",
- "")
-
- test("SITES_${distfile}=https://example.org/",
- false,
- "SITES_${distfile}",
- "",
- "=",
- "SITES_${distfile}=",
- "https://example.org/",
- "",
- "")
-
- t.ExpectAssert(func() { testInvalid("\tVAR=value") })
- testInvalid("?=value")
- testInvalid("<=value")
- testInvalid("#")
- testInvalid("VAR.$$=value")
-
- // A commented variable assignment must start immediately after the comment character.
- // There must be no additional whitespace before the variable name.
- test("#VAR=value", true, "VAR", "", "=", "#VAR=", "value", "", "")
-
- // A single space is typically used for writing documentation, not for commenting out code.
- // Therefore this line doesn't count as commented variable assignment.
- testInvalid("# VAR=value")
-
- // Ensure that the alignment for the variable value is correct.
- test("BUILD_DIRS=\tdir1 dir2",
- false,
- "BUILD_DIRS",
- "",
- "=",
- "BUILD_DIRS=\t",
- "dir1 dir2",
- "",
- "")
-
- // Ensure that the alignment for the variable value is correct,
- // even if the whole line is commented.
- test("#BUILD_DIRS=\tdir1 dir2",
- true,
- "BUILD_DIRS",
- "",
- "=",
- "#BUILD_DIRS=\t",
- "dir1 dir2",
- "",
- "")
-
- test("MASTER_SITES=\t#none",
- false,
- "MASTER_SITES",
- "",
- "=",
- "MASTER_SITES=\t",
- "",
- "",
- "#none")
-
- test("MASTER_SITES=\t# none",
- false,
- "MASTER_SITES",
- "",
- "=",
- "MASTER_SITES=\t",
- "",
- "",
- "# none")
-
- test("EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
-
- false,
- "EGDIRS",
- "",
- "=",
- "EGDIRS=\t",
- "${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
- "",
- "")
-
- test("VAR:=\t${VAR:M-*:[\\#]}",
- false,
- "VAR",
- "",
- ":=",
- "VAR:=\t",
- "${VAR:M-*:[#]}",
- "",
- "")
-
- test("#VAR=value",
- true, "VAR", "", "=", "#VAR=", "value", "", "")
-
- testInvalid("# VAR=value")
- testInvalid("#\tVAR=value")
- testInvalid(MkCvsID)
-
- testLine(
- lines(
- "VAR=\t\t\t\\",
- "\tvalue"),
- false,
- "VAR",
- "",
- "=",
- "VAR=\t\t\t",
- "value",
- "",
- "")
-
- testLine(
- lines(
- "#VAR=\t\t\t\\",
- "#\tvalue"),
- true,
- "VAR",
- "",
- "=",
- "#VAR=\t\t\t",
- "value",
- "",
- "")
-}
-
-func (s *Suite) Test_MkLineParser_getRawValueAlign__assertion(c *check.C) {
- t := s.Init(c)
-
- var p MkLineParser
-
- // This is unrealistic; just for code coverage of the assertion.
- t.ExpectAssert(func() { p.getRawValueAlign("a", "b") })
-}
-
func (s *Suite) Test_NewMkOperator(c *check.C) {
t := s.Init(c)
@@ -2147,531 +1610,15 @@ func (s *Suite) Test_MkLine_UnquoteShell(c *check.C) {
"may lead to unintended file globbing.")
}
-func (s *Suite) Test_MkLineParser_unescapeComment(c *check.C) {
- t := s.Init(c)
-
- test := func(text string, main, comment string) {
- aMain, aComment := MkLineParser{}.unescapeComment(text)
- t.CheckDeepEquals(
- []interface{}{text, aMain, aComment},
- []interface{}{text, main, comment})
- }
-
- test("",
- "",
- "")
- test("text",
- "text",
- "")
-
- // The leading space from the comment is preserved to make parsing as exact
- // as possible.
- //
- // The difference between "#defined" and "# defined" is relevant in a few
- // cases, such as the API documentation of the infrastructure files.
- test("# comment",
- "",
- "# comment")
- test("#\tcomment",
- "",
- "#\tcomment")
- test("# comment",
- "",
- "# comment")
-
- // Other than in the shell, # also starts a comment in the middle of a word.
- test("COMMENT=\tThe C# compiler",
- "COMMENT=\tThe C",
- "# compiler")
- test("COMMENT=\tThe C\\# compiler",
- "COMMENT=\tThe C# compiler",
- "")
-
- test("${TARGET}: ${SOURCES} # comment",
- "${TARGET}: ${SOURCES} ",
- "# comment")
-
- // A # starts a comment, except if it immediately follows a [.
- // This is done so that the length modifier :[#] can be written without
- // escaping the #.
- test("VAR=\t${OTHER:[#]} # comment",
- "VAR=\t${OTHER:[#]} ",
- "# comment")
-
- // The # in the :[#] modifier may be escaped or not. Both forms are equivalent.
- test("VAR:=\t${VAR:M-*:[\\#]}",
- "VAR:=\t${VAR:M-*:[#]}",
- "")
-
- // The character [ prevents the following # from starting a comment, even
- // outside of variable modifiers.
- test("COMMENT=\t[#] $$\\# $$# comment",
- "COMMENT=\t[#] $$# $$",
- "# comment")
-
- // A backslash always escapes the next character, be it a # for a comment
- // or something else. This makes it difficult to write a literal \# in a
- // Makefile, but that's an edge case anyway.
- test("VAR0=\t#comment",
- "VAR0=\t",
- "#comment")
- test("VAR1=\t\\#no-comment",
- "VAR1=\t#no-comment",
- "")
- test("VAR2=\t\\\\#comment",
- "VAR2=\t\\\\",
- "#comment")
-
- // The backslash is only removed when it escapes a comment.
- // In particular, it cannot be used to escape a dollar that starts a
- // variable use.
- test("VAR0=\t$T",
- "VAR0=\t$T",
- "")
- test("VAR1=\t\\$T",
- "VAR1=\t\\$T",
- "")
- test("VAR2=\t\\\\$T",
- "VAR2=\t\\\\$T",
- "")
-
- // To escape a dollar, write it twice.
- test("$$shellvar $${shellvar} \\${MKVAR} [] \\x",
- "$$shellvar $${shellvar} \\${MKVAR} [] \\x",
- "")
-
- // Parse errors are recorded in the rest return value.
- test("${UNCLOSED",
- "${UNCLOSED",
- "")
-
- // In this early phase of parsing, unfinished variable uses are not
- // interpreted and do not influence the detection of the comment start.
- test("text before ${UNCLOSED # comment",
- "text before ${UNCLOSED ",
- "# comment")
-
- // The dollar-space refers to a normal Make variable named " ".
- // The lonely dollar at the very end refers to the variable named "",
- // which is specially protected in bmake to always contain the empty string.
- // It is heavily used in .for loops in the form ${:Uvalue}.
- test("Lonely $ character $",
- "Lonely $ character $",
- "")
-
- // An even number of backslashes does not escape the #.
- // Therefore it starts a comment here.
- test("VAR2=\t\\\\#comment",
- "VAR2=\t\\\\",
- "#comment")
-}
-
-func (s *Suite) Test_MkLineParser_split(c *check.C) {
- t := s.Init(c)
-
- varuse := func(varname string, modifiers ...string) *MkToken {
- var text strings.Builder
- text.WriteString("${")
- text.WriteString(varname)
- for _, modifier := range modifiers {
- text.WriteString(":")
- text.WriteString(modifier)
- }
- text.WriteString("}")
- return &MkToken{Text: text.String(), Varuse: NewMkVarUse(varname, modifiers...)}
- }
- varuseText := func(text, varname string, modifiers ...string) *MkToken {
- return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
- }
- text := func(text string) *MkToken {
- return &MkToken{text, nil}
- }
- tokens := func(tokens ...*MkToken) []*MkToken {
- return tokens
- }
-
- test := func(text string, data mkLineSplitResult, diagnostics ...string) {
- line := t.NewLine("filename.mk", 123, text)
- actualData := MkLineParser{}.split(line, text)
-
- t.CheckOutput(diagnostics)
- t.CheckDeepEquals([]interface{}{text, actualData}, []interface{}{text, data})
- }
-
- t.Use(text, varuse, varuseText, tokens)
-
- test(
- "",
- mkLineSplitResult{})
-
- test(
- "text",
- mkLineSplitResult{
- main: "text",
- tokens: tokens(text("text")),
- })
-
- // Leading space is always kept.
- test(
- " text",
- mkLineSplitResult{
- main: " text",
- tokens: tokens(text(" text")),
- })
-
- // Trailing space does not end up in the tokens since it is usually
- // ignored.
- test(
- "text\t",
- mkLineSplitResult{
- main: "text",
- tokens: tokens(text("text")),
- spaceBeforeComment: "\t",
- })
-
- test(
- "text\t# intended comment",
- mkLineSplitResult{
- main: "text",
- tokens: tokens(text("text")),
- spaceBeforeComment: "\t",
- hasComment: true,
- comment: " intended comment",
- })
-
- // Trailing space is saved in a separate field to detect accidental
- // unescaped # in the middle of a word, like the URL fragment in this
- // example.
- test(
- "url#fragment",
- mkLineSplitResult{
- main: "url",
- tokens: tokens(text("url")),
- hasComment: true,
- comment: "fragment",
- })
-
- // The leading space from the comment is preserved to make parsing as exact
- // as possible.
- //
- // The difference between "#defined" and "# defined" is relevant in a few
- // cases, such as the API documentation of the infrastructure files.
- test("# comment",
- mkLineSplitResult{
- hasComment: true,
- comment: " comment",
- })
-
- test("#\tcomment",
- mkLineSplitResult{
- hasComment: true,
- comment: "\tcomment",
- })
-
- test("# comment",
- mkLineSplitResult{
- hasComment: true,
- comment: " comment",
- })
-
- // Other than in the shell, # also starts a comment in the middle of a word.
- test("COMMENT=\tThe C# compiler",
- mkLineSplitResult{
- main: "COMMENT=\tThe C",
- tokens: tokens(text("COMMENT=\tThe C")),
- hasComment: true,
- comment: " compiler",
- })
-
- test("COMMENT=\tThe C\\# compiler",
- mkLineSplitResult{
- main: "COMMENT=\tThe C# compiler",
- tokens: tokens(text("COMMENT=\tThe C# compiler")),
- hasComment: false,
- comment: "",
- })
-
- test("${TARGET}: ${SOURCES} # comment",
- mkLineSplitResult{
- main: "${TARGET}: ${SOURCES}",
- tokens: tokens(varuse("TARGET"), text(": "), varuse("SOURCES")),
- spaceBeforeComment: " ",
- hasComment: true,
- comment: " comment",
- })
-
- // A # starts a comment, except if it immediately follows a [.
- // This is done so that the length modifier :[#] can be written without
- // escaping the #.
- test("VAR=\t${OTHER:[#]} # comment",
- mkLineSplitResult{
- main: "VAR=\t${OTHER:[#]}",
- tokens: tokens(text("VAR=\t"), varuse("OTHER", "[#]")),
- spaceBeforeComment: " ",
- hasComment: true,
- comment: " comment",
- })
-
- // The # in the :[#] modifier may be escaped or not. Both forms are equivalent.
- test("VAR:=\t${VAR:M-*:[\\#]}",
- mkLineSplitResult{
- main: "VAR:=\t${VAR:M-*:[#]}",
- tokens: tokens(text("VAR:=\t"), varuse("VAR", "M-*", "[#]")),
- })
-
- // A backslash always escapes the next character, be it a # for a comment
- // or something else. This makes it difficult to write a literal \# in a
- // Makefile, but that's an edge case anyway.
- test("VAR0=\t#comment",
- mkLineSplitResult{
- main: "VAR0=",
- tokens: tokens(text("VAR0=")),
- // Later, when converting this result into a proper variable assignment,
- // this "space before comment" is reclassified as "space before the value",
- // in order to align the "#comment" with the other variable values.
- spaceBeforeComment: "\t",
- hasComment: true,
- comment: "comment",
- })
-
- test("VAR1=\t\\#no-comment",
- mkLineSplitResult{
- main: "VAR1=\t#no-comment",
- tokens: tokens(text("VAR1=\t#no-comment")),
- })
-
- test("VAR2=\t\\\\#comment",
- mkLineSplitResult{
- main: "VAR2=\t\\\\",
- tokens: tokens(text("VAR2=\t\\\\")),
- hasComment: true,
- comment: "comment",
- })
-
- // The backslash is only removed when it escapes a comment.
- // In particular, it cannot be used to escape a dollar that starts a
- // variable use.
- test("VAR0=\t$T",
- mkLineSplitResult{
- main: "VAR0=\t$T",
- tokens: tokens(text("VAR0=\t"), varuseText("$T", "T")),
- },
- "WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")
-
- test("VAR1=\t\\$T",
- mkLineSplitResult{
- main: "VAR1=\t\\$T",
- tokens: tokens(text("VAR1=\t\\"), varuseText("$T", "T")),
- },
- "WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")
-
- test("VAR2=\t\\\\$T",
- mkLineSplitResult{
- main: "VAR2=\t\\\\$T",
- tokens: tokens(text("VAR2=\t\\\\"), varuseText("$T", "T")),
- },
- "WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")
-
- // To escape a dollar, write it twice.
- test("$$shellvar $${shellvar} \\${MKVAR} [] \\x",
- mkLineSplitResult{
- main: "$$shellvar $${shellvar} \\${MKVAR} [] \\x",
- tokens: tokens(text("$$shellvar $${shellvar} \\"), varuse("MKVAR"), text(" [] \\x")),
- })
-
- // Parse errors are recorded in the rest return value.
- test("${UNCLOSED",
- mkLineSplitResult{
- main: "${UNCLOSED",
- tokens: tokens(varuseText("${UNCLOSED", "UNCLOSED")),
- },
- "WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED\".")
-
- // Even if there is a parse error in the main part,
- // the comment is extracted.
- test("text before ${UNCLOSED# comment",
- mkLineSplitResult{
- main: "text before ${UNCLOSED",
- tokens: tokens(
- text("text before "),
- varuseText("${UNCLOSED", "UNCLOSED")),
- hasComment: true,
- comment: " comment",
- },
- "WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED\".")
-
- // Even in case of parse errors, the space before the comment is parsed
- // correctly.
- test("text before ${UNCLOSED # comment",
- mkLineSplitResult{
- main: "text before ${UNCLOSED",
- tokens: tokens(
- text("text before "),
- // It's a bit inconsistent that the varname includes the space
- // but the text doesn't; anyway, it's an edge case.
- varuseText("${UNCLOSED", "UNCLOSED ")),
- spaceBeforeComment: " ",
- hasComment: true,
- comment: " comment",
- },
- "WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED \".",
- "WARN: filename.mk:123: Invalid part \" \" after variable name \"UNCLOSED\".")
-
- // The dollar-space refers to a normal Make variable named " ".
- // The lonely dollar at the very end refers to the variable named "",
- // which is specially protected in bmake to always contain the empty string.
- // It is heavily used in .for loops in the form ${:Uvalue}.
- //
- // TODO: The rest of pkglint assumes that the empty string is not a valid
- // variable name, mainly because the empty variable name is not visible
- // outside of the bmake debugging mode.
- test("Lonely $ character $",
- mkLineSplitResult{
- main: "Lonely $ character $",
- tokens: tokens(
- text("Lonely "),
- varuseText("$ " /* instead of "${ }" */, " "),
- text("character "),
- text("$")),
- })
-
- // The character [ prevents the following # from starting a comment, even
- // outside of variable modifiers.
- test("COMMENT=\t[#] $$\\# $$# comment",
- mkLineSplitResult{
- main: "COMMENT=\t[#] $$# $$",
- tokens: tokens(text("COMMENT=\t[#] $$# $$")),
- hasComment: true,
- comment: " comment",
- })
-
- test("VAR2=\t\\\\#comment",
- mkLineSplitResult{
- main: "VAR2=\t\\\\",
- tokens: tokens(text("VAR2=\t\\\\")),
- hasComment: true,
- comment: "comment",
- })
-
- // At this stage, MkLine.split doesn't know that empty(...) takes
- // a variable use. Instead it just sees ordinary characters and
- // other uses of variables.
- test(".if empty(${VAR.${tool}}:C/\\:.*$//:M${pattern})",
- mkLineSplitResult{
- main: ".if empty(${VAR.${tool}}:C/\\:.*$//:M${pattern})",
- tokens: tokens(
- text(".if empty("),
- varuse("VAR.${tool}"),
- text(":C/\\:.*"),
- text("$"),
- text("//:M"),
- varuse("pattern"),
- text(")")),
- })
-
- test(" # comment after spaces",
- mkLineSplitResult{
- spaceBeforeComment: " ",
- hasComment: true,
- comment: " comment after spaces",
- })
-
- // FIXME: This theoretical edge case is interpreted differently
- // between bmake and pkglint. Pkglint treats the # as a comment,
- // while bmake interprets it as a regular character.
- test("\\[#",
- mkLineSplitResult{
- main: "\\[",
- tokens: tokens(text("\\[")),
- hasComment: true,
- })
-
- test("\\\\[#",
- mkLineSplitResult{
- main: "\\\\[#",
- tokens: tokens(text("\\\\[#")),
- })
-}
-
-func (s *Suite) Test_MkLineParser_split__unclosed_varuse(c *check.C) {
- t := s.Init(c)
-
- test := func(text string, expected mkLineSplitResult, diagnostics ...string) {
- line := t.NewLine("filename.mk", 123, text)
-
- data := MkLineParser{}.split(line, text)
-
- t.CheckDeepEquals(data, expected)
- t.CheckOutput(diagnostics)
- }
-
- test(
- "EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
-
- mkLineSplitResult{
- "EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
- []*MkToken{
- {"EGDIRS=\t", nil},
- {"${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
- NewMkVarUse("EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d")}},
- "",
- false,
- "",
- },
-
- "WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/pam.d\".",
- "WARN: filename.mk:123: Invalid part \"/pam.d\" after variable name \"EGDIR\".",
- "WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/dbus-1/system.d ${EGDIR/pam.d\".",
- "WARN: filename.mk:123: Invalid part \"/dbus-1/system.d ${EGDIR/pam.d\" after variable name \"EGDIR\".",
- "WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\".",
- "WARN: filename.mk:123: Invalid part \"/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\" after variable name \"EGDIR\".")
-}
-
-func (s *Suite) Test_MkLineParser_parseDirective(c *check.C) {
- t := s.Init(c)
-
- test := func(input, expectedIndent, expectedDirective, expectedArgs, expectedComment string, diagnostics ...string) {
- line := t.NewLine("filename.mk", 123, input)
- data := MkLineParser{}.split(line, input)
- mkline := MkLineParser{}.parseDirective(line, data)
- if !c.Check(mkline, check.NotNil) {
- return
- }
-
- t.CheckDeepEquals(
- []interface{}{mkline.Indent(), mkline.Directive(), mkline.Args(), mkline.DirectiveComment()},
- []interface{}{expectedIndent, expectedDirective, expectedArgs, expectedComment})
- t.CheckOutput(diagnostics)
- }
-
- test(".if ${VAR} == value",
- "", "if", "${VAR} == value", "")
-
- test(".\tendif # comment",
- "\t", "endif", "", "comment")
-
- test(".if ${VAR} == \"#\"",
- "", "if", "${VAR} == \"", "\"")
-
- test(".if ${VAR:[#]}",
- "", "if", "${VAR:[#]}", "")
-
- test(".if ${VAR} == \\",
- "", "if", "${VAR} == \\", "")
-
- test(".if ${VAR",
- "", "if", "${VAR", "",
- "WARN: filename.mk:123: Missing closing \"}\" for \"VAR\".")
-}
-
func (s *Suite) Test_MatchMkInclude(c *check.C) {
t := s.Init(c)
- test := func(input, expectedIndent, expectedDirective, expectedFilename string) {
- m, indent, directive, args := MatchMkInclude(input)
+ test := func(input, expectedIndent, expectedDirective, expectedFilename, expectedComment string) {
+ splitResult := NewMkLineParser().split(nil, input, true)
+ m, indent, directive, args := MatchMkInclude(splitResult.main)
t.CheckDeepEquals(
- []interface{}{m, indent, directive, args},
- []interface{}{true, expectedIndent, expectedDirective, expectedFilename})
+ []interface{}{m, indent, directive, args, condStr(splitResult.hasComment, "#", "") + splitResult.comment},
+ []interface{}{true, expectedIndent, expectedDirective, expectedFilename, expectedComment})
}
testFail := func(input string) {
@@ -2689,16 +1636,16 @@ func (s *Suite) Test_MatchMkInclude(c *check.C) {
testFail(".include \"other.mk\" \"")
test(".include \"other.mk\"",
- "", "include", "other.mk")
+ "", "include", "other.mk", "")
test(".include \"other.mk\"\t",
- "", "include", "other.mk")
+ "", "include", "other.mk", "")
test(".include \"other.mk\"\t#",
- "", "include", "other.mk")
+ "", "include", "other.mk", "#")
test(".include \"other.mk\"\t# comment",
- "", "include", "other.mk")
+ "", "include", "other.mk", "# comment")
t.CheckOutputEmpty()
}
diff --git a/pkgtools/pkglint/files/mklinechecker.go b/pkgtools/pkglint/files/mklinechecker.go
index 3f050c7c1f2..28958f2c4fe 100644
--- a/pkgtools/pkglint/files/mklinechecker.go
+++ b/pkgtools/pkglint/files/mklinechecker.go
@@ -131,7 +131,8 @@ func (ck MkLineChecker) checkInclude() {
mkline.Warnf("Please write \"USE_TOOLS+= intltool\" instead of this line.")
case hasSuffix(includedFile, "/builtin.mk"):
- if mkline.Basename != "hacks.mk" {
+ // TODO: mkline.HasRationale
+ if mkline.Basename != "hacks.mk" && !mkline.HasComment() {
fix := mkline.Autofix()
fix.Errorf("%s must not be included directly. Include \"%s/buildlink3.mk\" instead.", includedFile, path.Dir(includedFile))
fix.Replace("builtin.mk", "buildlink3.mk")
@@ -272,8 +273,8 @@ func (ck MkLineChecker) checkDirectiveIndentation(expectedDepth int) {
func (ck MkLineChecker) checkDependencyRule(allowedTargets map[string]bool) {
mkline := ck.MkLine
- targets := ck.MkLine.ValueFields(mkline.Targets())
- sources := ck.MkLine.ValueFields(mkline.Sources())
+ targets := mkline.ValueFields(mkline.Targets())
+ sources := mkline.ValueFields(mkline.Sources())
for _, source := range sources {
if source == ".PHONY" {
@@ -282,32 +283,38 @@ func (ck MkLineChecker) checkDependencyRule(allowedTargets map[string]bool) {
}
}
}
-
for _, target := range targets {
if target == ".PHONY" {
- for _, dep := range sources {
- allowedTargets[dep] = true
+ for _, source := range sources {
+ allowedTargets[source] = true
}
+ }
+ }
- } else if target == ".ORDER" {
- // TODO: Check for spelling mistakes.
-
- } else if hasPrefix(target, "${.CURDIR}/") {
- // This is deliberate, see the explanation below.
+ for _, target := range targets {
+ ck.checkDependencyTarget(target, allowedTargets)
+ }
+}
- } else if !allowedTargets[target] {
- mkline.Warnf("Undeclared target %q.", target)
- mkline.Explain(
- "To define a custom target in a package, declare it like this:",
- "",
- "\t.PHONY: my-target",
- "",
- "To define a custom target that creates a file (should be rarely needed),",
- "declare it like this:",
- "",
- "\t${.CURDIR}/my-file:")
- }
+func (ck MkLineChecker) checkDependencyTarget(target string, allowedTargets map[string]bool) {
+ if target == ".PHONY" ||
+ target == ".ORDER" ||
+ NewMkParser(nil, target).VarUse() != nil ||
+ allowedTargets[target] {
+ return
}
+
+ mkline := ck.MkLine
+ mkline.Warnf("Undeclared target %q.", target)
+ mkline.Explain(
+ "To define a custom target in a package, declare it like this:",
+ "",
+ "\t.PHONY: my-target",
+ "",
+ "To define a custom target that creates a file (should be rarely needed),",
+ "declare it like this:",
+ "",
+ "\t${.CURDIR}/my-file:")
}
// checkVarassignLeftPermissions checks the permissions for the left-hand side
@@ -450,7 +457,7 @@ func (ck MkLineChecker) checkVarassignLeftRationale() {
return
}
- if mkline.VarassignComment() != "" {
+ if mkline.HasComment() {
return
}
@@ -1048,7 +1055,7 @@ func (ck MkLineChecker) checkVarassignOpShell() {
case mkline.Op() != opAssignShell:
return
- case mkline.VarassignComment() != "":
+ case mkline.HasComment():
return
case mkline.Basename == "builtin.mk":
@@ -1094,7 +1101,7 @@ func (ck MkLineChecker) checkVarassignRight() {
varname := mkline.Varname()
op := mkline.Op()
value := mkline.Value()
- comment := mkline.VarassignComment()
+ comment := condStr(mkline.HasComment(), "#", "") + mkline.Comment()
if trace.Tracing {
defer trace.Call(varname, op, value)()
@@ -1254,7 +1261,7 @@ func (ck MkLineChecker) checkVarassignMisc() {
ck.checkVarassignDecreasingVersions()
}
- if mkline.VarassignComment() == "# defined" && !hasSuffix(varname, "_MK") && !hasSuffix(varname, "_COMMON") {
+ if mkline.Comment() == " defined" && !hasSuffix(varname, "_MK") && !hasSuffix(varname, "_COMMON") {
mkline.Notef("Please use \"# empty\", \"# none\" or \"# yes\" instead of \"# defined\".")
mkline.Explain(
"The value #defined says something about the state of the variable,",
@@ -1378,7 +1385,7 @@ func (ck MkLineChecker) checkVarassignLeftUserSettable() bool {
}
switch {
- case mkline.VarassignComment() != "":
+ case mkline.HasComment():
// Assume that the comment contains a rationale for disabling
// this particular check.
@@ -1407,6 +1414,7 @@ func (ck MkLineChecker) checkVarassignLeftUserSettable() bool {
return true
}
+// comment is an empty string for no comment, or "#" + the actual comment otherwise.
func (ck MkLineChecker) checkVartype(varname string, op MkOperator, value, comment string) {
if trace.Tracing {
defer trace.Call(varname, op, value, comment)()
diff --git a/pkgtools/pkglint/files/mklinechecker_test.go b/pkgtools/pkglint/files/mklinechecker_test.go
index 9694e02e3ef..3d78b54a721 100644
--- a/pkgtools/pkglint/files/mklinechecker_test.go
+++ b/pkgtools/pkglint/files/mklinechecker_test.go
@@ -395,6 +395,24 @@ func (s *Suite) Test_MkLineChecker_checkInclude__hacks(c *check.C) {
"Relative path \"../../category/package/nonexistent.mk\" does not exist.")
}
+func (s *Suite) Test_MkLineChecker_checkInclude__builtin_mk(c *check.C) {
+ t := s.Init(c)
+
+ t.SetUpPackage("category/package",
+ ".include \"../../category/package/builtin.mk\"",
+ ".include \"../../category/package/builtin.mk\" # ok")
+ t.CreateFileLines("category/package/builtin.mk",
+ MkCvsID)
+ t.FinishSetUp()
+
+ G.checkdirPackage(t.File("category/package"))
+
+ t.CheckOutputLines(
+ "ERROR: ~/category/package/Makefile:20: " +
+ "../../category/package/builtin.mk must not be included directly. " +
+ "Include \"../../category/package/buildlink3.mk\" instead.")
+}
+
func (s *Suite) Test_MkLineChecker__permissions_in_hacks_mk(c *check.C) {
t := s.Init(c)
@@ -606,7 +624,8 @@ func (s *Suite) Test_MkLineChecker_checkDependencyRule(c *check.C) {
".ORDER: target-1 target-2",
"target-1:",
"target-2:",
- "target-3:")
+ "target-3:",
+ "${_COOKIE.test}:")
mklines.Check()
@@ -2107,6 +2126,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparing_PKGSRC_COMPILER
func (s *Suite) Test_MkLineChecker_checkDirectiveCondCompareVarStr__no_tracing(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
t.SetUpVartypes()
mklines := t.NewMkLines("filename.mk",
@@ -2114,7 +2134,7 @@ func (s *Suite) Test_MkLineChecker_checkDirectiveCondCompareVarStr__no_tracing(c
t.DisableTracing()
ck := MkLineChecker{mklines, mklines.mklines[0]}
- varUse := NewMkVarUse("DISTFILES", "Mpattern", "O", "u")
+ varUse := b.VarUse("DISTFILES", "Mpattern", "O", "u")
ck.checkDirectiveCondCompareVarStr(varUse, "==", "distfile-1.0.tar.gz")
t.CheckOutputEmpty()
@@ -2416,6 +2436,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__for(c *check.C) {
// check that the variable names match exactly.
func (s *Suite) Test_MkLineChecker_CheckVaruse__varcanon(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
t.SetUpPkgsrc()
t.CreateFileLines("mk/sys-vars.mk",
@@ -2429,7 +2450,7 @@ func (s *Suite) Test_MkLineChecker_CheckVaruse__varcanon(c *check.C) {
ck := MkLineChecker{mklines, mklines.mklines[1]}
- ck.CheckVaruse(NewMkVarUse("CPPPATH.SunOS"), &VarUseContext{
+ ck.CheckVaruse(b.VarUse("CPPPATH.SunOS"), &VarUseContext{
vartype: &Vartype{
basicType: BtPathname,
options: Guessed,
diff --git a/pkgtools/pkglint/files/mklineparser.go b/pkgtools/pkglint/files/mklineparser.go
new file mode 100644
index 00000000000..f1db601aca9
--- /dev/null
+++ b/pkgtools/pkglint/files/mklineparser.go
@@ -0,0 +1,450 @@
+package pkglint
+
+import (
+ "netbsd.org/pkglint/textproc"
+ "strings"
+)
+
+type MkLineParser struct{}
+
+func NewMkLineParser() MkLineParser { return MkLineParser{} }
+
+// Parse parses the text of a Makefile line to see what kind of line
+// it is: variable assignment, include, comment, etc.
+//
+// See devel/bmake/parse.c:/^Parse_File/
+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.
+ if hasPrefix(text, " ") && line.Basename != "bsd.buildlink3.mk" {
+ line.Warnf("Makefile lines should not start with space characters.")
+ line.Explain(
+ "If this line should be a shell command connected to a target, use a tab character for indentation.",
+ "Otherwise remove the leading whitespace.")
+ }
+
+ // Check for shell commands first because these cannot have comments
+ // at the end of the line.
+ if hasPrefix(text, "\t") {
+ lex := textproc.NewLexer(text)
+ for lex.SkipByte('\t') {
+ }
+
+ splitResult := p.split(line, lex.Rest(), false)
+ return p.parseShellcmd(line, splitResult)
+ }
+
+ splitResult := p.split(line, text, true)
+
+ if mkline := p.parseVarassign(line, text, splitResult); mkline != nil {
+ return mkline
+ }
+ if mkline := p.parseCommentOrEmpty(line, splitResult); mkline != nil {
+ return mkline
+ }
+ if mkline := p.parseDirective(line, splitResult); mkline != nil {
+ return mkline
+ }
+ if mkline := p.parseInclude(line, splitResult); mkline != nil {
+ return mkline
+ }
+ if mkline := p.parseSysinclude(line, splitResult); mkline != nil {
+ return mkline
+ }
+ if mkline := p.parseDependency(line, splitResult); mkline != nil {
+ return mkline
+ }
+ if mkline := p.parseMergeConflict(line, splitResult); mkline != nil {
+ return mkline
+ }
+
+ // The %q is deliberate here since it shows possible strange characters.
+ line.Errorf("Unknown Makefile line format: %q.", text)
+ return &MkLine{line, splitResult, nil}
+}
+
+func (p MkLineParser) parseVarassign(line *Line, text string, splitResult mkLineSplitResult) *MkLine {
+ m, a := p.MatchVarassign(line, text, &splitResult)
+ if !m {
+ return nil
+ }
+
+ if a.spaceAfterVarname != "" {
+ varname := a.varname
+ op := a.op
+ switch {
+ case hasSuffix(varname, "+") && (op == opAssign || op == opAssignAppend):
+ break
+ case matches(varname, `^[a-z]`) && op == opAssignEval:
+ break
+ default:
+ fix := line.Autofix()
+ fix.Notef("Unnecessary space after variable name %q.", varname)
+ fix.Replace(varname+a.spaceAfterVarname+op.String(), varname+op.String())
+ fix.Apply()
+ }
+ }
+
+ if splitResult.hasComment && a.value != "" && splitResult.spaceBeforeComment == "" {
+ line.Warnf("The # character starts a Makefile comment.")
+ line.Explain(
+ "In a variable assignment, an unescaped # starts a comment that",
+ "continues until the end of the line.",
+ "To escape the #, write \\#.",
+ "",
+ "If this # character intentionally starts a comment,",
+ "it should be preceded by a space in order to make it more visible.")
+ }
+
+ return &MkLine{line, splitResult, a}
+}
+
+func (p MkLineParser) MatchVarassign(line *Line, text string, splitResult *mkLineSplitResult) (bool, *mkLineAssign) {
+
+ // A commented variable assignment does not have leading whitespace.
+ // Otherwise line 1 of almost every Makefile fragment would need to
+ // be scanned for a variable assignment even though it only contains
+ // the $NetBSD CVS Id.
+ commented := splitResult.main == "" && splitResult.hasComment
+ if commented {
+ clex := textproc.NewLexer(splitResult.comment)
+ if clex.SkipHspace() || clex.EOF() {
+ return false, nil
+ }
+ *splitResult = p.split(nil, text[1:], true)
+ }
+
+ lexer := NewMkTokensLexer(splitResult.tokens)
+ mainStart := lexer.Mark()
+
+ for !commented && lexer.SkipByte(' ') {
+ }
+
+ varnameStart := lexer.Mark()
+ // TODO: duplicated code in MkParser.Varname
+ for lexer.NextBytesSet(VarbaseBytes) != "" || lexer.NextVarUse() != nil {
+ }
+ if lexer.SkipByte('.') || hasPrefix(splitResult.main, "SITES_") {
+ for lexer.NextBytesSet(VarparamBytes) != "" || lexer.NextVarUse() != nil {
+ }
+ }
+
+ varname := lexer.Since(varnameStart)
+
+ if varname == "" {
+ return false, nil
+ }
+
+ spaceAfterVarname := lexer.NextHspace()
+
+ opStart := lexer.Mark()
+ switch lexer.PeekByte() {
+ case '!', '+', ':', '?':
+ lexer.Skip(1)
+ }
+ if !lexer.SkipByte('=') {
+ return false, nil
+ }
+ op := NewMkOperator(lexer.Since(opStart))
+
+ if hasSuffix(varname, "+") && op == opAssign && spaceAfterVarname == "" {
+ varname = varname[:len(varname)-1]
+ op = opAssignAppend
+ }
+
+ lexer.SkipHspace()
+
+ value := trimHspace(lexer.Rest())
+ parsedValueAlign := condStr(commented, "#", "") + lexer.Since(mainStart)
+ valueAlign := p.getRawValueAlign(line.raw[0].orignl, parsedValueAlign)
+ if value == "" {
+ valueAlign += splitResult.spaceBeforeComment
+ splitResult.spaceBeforeComment = ""
+ }
+
+ return true, &mkLineAssign{
+ commented: commented,
+ varname: varname,
+ varcanon: varnameCanon(varname),
+ varparam: varnameParam(varname),
+ spaceAfterVarname: spaceAfterVarname,
+ op: op,
+ valueAlign: valueAlign,
+ value: value,
+ valueMk: nil, // filled in lazily
+ valueMkRest: "", // filled in lazily
+ fields: nil, // filled in lazily
+ }
+}
+
+func (p MkLineParser) parseShellcmd(line *Line, splitResult mkLineSplitResult) *MkLine {
+ return &MkLine{line, splitResult, mkLineShell{line.Text[1:]}}
+}
+
+func (p MkLineParser) parseCommentOrEmpty(line *Line, splitResult mkLineSplitResult) *MkLine {
+ trimmedText := trimHspace(line.Text)
+
+ if strings.HasPrefix(trimmedText, "#") {
+ return &MkLine{line, splitResult, mkLineComment{}}
+ }
+
+ if trimmedText == "" {
+ return &MkLine{line, splitResult, mkLineEmpty{}}
+ }
+
+ return nil
+}
+
+func (p MkLineParser) parseDirective(line *Line, splitResult mkLineSplitResult) *MkLine {
+ text := line.Text
+ if !hasPrefix(text, ".") { // TODO: use lexer instead
+ return nil
+ }
+
+ lexer := textproc.NewLexer(splitResult.main[1:])
+
+ indent := lexer.NextHspace()
+ directive := lexer.NextBytesSet(LowerDash)
+ switch directive {
+ case "if", "else", "elif", "endif",
+ "ifdef", "ifndef",
+ "for", "endfor", "undef",
+ "error", "warning", "info",
+ "export", "export-env", "unexport", "unexport-env":
+ break
+ default:
+ // Intentionally not supported are: ifmake ifnmake elifdef elifndef elifmake elifnmake.
+ return nil
+ }
+
+ lexer.SkipHspace()
+
+ args := lexer.Rest()
+
+ // In .if and .endif lines the space surrounding the comment is irrelevant.
+ // Especially for checking that the .endif comment matches the .if condition,
+ // it must be trimmed.
+ trimmedComment := trimHspace(splitResult.comment)
+
+ return &MkLine{line, splitResult, &mkLineDirective{indent, directive, args, trimmedComment, nil, nil, nil}}
+}
+
+func (p MkLineParser) parseInclude(line *Line, splitResult mkLineSplitResult) *MkLine {
+ m, indent, directive, includedFile := MatchMkInclude(splitResult.main)
+ if !m {
+ return nil
+ }
+
+ return &MkLine{line, splitResult, &mkLineInclude{directive == "include", false, indent, includedFile, nil}}
+}
+
+func (p MkLineParser) parseSysinclude(line *Line, splitResult mkLineSplitResult) *MkLine {
+ m, indent, directive, includedFile := match3(splitResult.main, `^\.([\t ]*)(s?include)[\t ]+<([^>]+)>$`)
+ if !m {
+ return nil
+ }
+
+ return &MkLine{line, splitResult, &mkLineInclude{directive == "include", true, indent, includedFile, nil}}
+}
+
+func (p MkLineParser) parseDependency(line *Line, splitResult mkLineSplitResult) *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 ]*#.*)?$`)
+ if !m {
+ return nil
+ }
+
+ if whitespace != "" {
+ line.Notef("Space before colon in dependency line.")
+ }
+ return &MkLine{line, splitResult, mkLineDependency{targets, sources}}
+}
+
+func (p MkLineParser) parseMergeConflict(line *Line, splitResult mkLineSplitResult) *MkLine {
+ if !matches(line.Text, `^(<<<<<<<|=======|>>>>>>>)`) {
+ return nil
+ }
+
+ return &MkLine{line, splitResult, nil}
+}
+
+// split parses a logical line from a Makefile (that is, after joining
+// the lines that end in a backslash) into two parts: the main part and the
+// comment.
+//
+// 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.
+//
+// If line is given, it is used for logging parse errors and warnings
+// about round parentheses instead of curly braces, as well as ambiguous
+// variables of the form $v instead of ${v}.
+//
+// If trimComment is true, the main task of this method is to split the
+// text into tokens. The remaining space is placed into spaceBeforeComment,
+// but hasComment will always be false, and comment will always be empty.
+// This behavior is useful for shell commands (which are indented with a
+// single tab).
+func (MkLineParser) split(line *Line, text string, trimComment bool) mkLineSplitResult {
+ assert(!hasPrefix(text, "\t"))
+
+ var mainWithSpaces, comment string
+ if trimComment {
+ mainWithSpaces, comment = NewMkLineParser().unescapeComment(text)
+ } else {
+ mainWithSpaces = text
+ }
+
+ parser := NewMkParser(line, mainWithSpaces)
+ lexer := parser.lexer
+
+ parseOther := func() string {
+ var sb strings.Builder
+
+ for !lexer.EOF() {
+ if lexer.SkipString("$$") {
+ sb.WriteString("$$")
+ continue
+ }
+
+ other := lexer.NextBytesFunc(func(b byte) bool { return b != '$' })
+ if other == "" {
+ break
+ }
+
+ sb.WriteString(other)
+ }
+
+ return sb.String()
+ }
+
+ var tokens []*MkToken
+ for !lexer.EOF() {
+ mark := lexer.Mark()
+
+ if varUse := parser.VarUse(); varUse != nil {
+ tokens = append(tokens, &MkToken{lexer.Since(mark), varUse})
+
+ } else if other := parseOther(); other != "" {
+ tokens = append(tokens, &MkToken{other, nil})
+
+ } else {
+ assert(lexer.SkipByte('$'))
+ tokens = append(tokens, &MkToken{"$", nil})
+ }
+ }
+
+ hasComment := comment != ""
+ if hasComment {
+ comment = comment[1:]
+ }
+
+ mainTrimmed := rtrimHspace(mainWithSpaces)
+ spaceBeforeComment := mainWithSpaces[len(mainTrimmed):]
+ if spaceBeforeComment != "" {
+ tokenText := &tokens[len(tokens)-1].Text
+ *tokenText = rtrimHspace(*tokenText)
+ if *tokenText == "" {
+ if len(tokens) == 1 {
+ tokens = nil
+ } else {
+ tokens = tokens[:len(tokens)-1]
+ }
+ }
+ }
+
+ return mkLineSplitResult{mainTrimmed, tokens, spaceBeforeComment, hasComment, comment}
+}
+
+// unescapeComment takes a Makefile line, as written in a file, and splits
+// it into the main part and the comment.
+//
+// The comment starts at the first #. Except if it is preceded by an odd number
+// of backslashes. Or by an opening bracket.
+//
+// The main text is returned including leading and trailing whitespace.
+// Any escaped # is unescaped, that is, \# becomes #.
+//
+// The comment is returned including the leading "#", if any.
+// If the line has no comment, it is an empty string.
+func (MkLineParser) unescapeComment(text string) (main, comment string) {
+ var sb strings.Builder
+
+ lexer := textproc.NewLexer(text)
+
+again:
+ if plain := lexer.NextBytesSet(unescapeMkCommentSafeChars); plain != "" {
+ sb.WriteString(plain)
+ goto again
+ }
+
+ switch {
+ case lexer.SkipString("\\#"):
+ sb.WriteByte('#')
+
+ case lexer.PeekByte() == '\\' && len(lexer.Rest()) >= 2:
+ sb.WriteString(lexer.Rest()[:2])
+ lexer.Skip(2)
+
+ case lexer.SkipByte('\\'):
+ sb.WriteByte('\\')
+
+ case lexer.SkipString("[#"):
+ // See devel/bmake/files/parse.c:/as in modifier/
+ sb.WriteString("[#")
+
+ case lexer.SkipByte('['):
+ sb.WriteByte('[')
+
+ default:
+ main = sb.String()
+ if lexer.PeekByte() == '#' {
+ return main, lexer.Rest()
+ }
+
+ assert(lexer.EOF())
+ return main, ""
+ }
+
+ goto again
+}
+
+func (MkLineParser) getRawValueAlign(raw, parsed string) string {
+ r := textproc.NewLexer(raw)
+ p := textproc.NewLexer(parsed)
+ mark := r.Mark()
+
+ for !p.EOF() {
+ pch := p.PeekByte()
+ rch := r.PeekByte()
+
+ switch {
+ case pch == rch:
+ p.Skip(1)
+ r.Skip(1)
+
+ case pch == ' ', pch == '\t':
+ p.SkipHspace()
+ r.SkipHspace()
+
+ default:
+ assert(pch == '#')
+ assert(r.SkipString("\\#"))
+ p.Skip(1)
+ }
+ }
+
+ return r.Since(mark)
+}
+
+type mkLineSplitResult struct {
+ // The text of the line, without the comment at the end of the line,
+ // and with # signs unescaped.
+ main string
+ tokens []*MkToken
+ spaceBeforeComment string
+ hasComment bool
+ comment string
+}
diff --git a/pkgtools/pkglint/files/mklineparser_test.go b/pkgtools/pkglint/files/mklineparser_test.go
new file mode 100644
index 00000000000..731d318416d
--- /dev/null
+++ b/pkgtools/pkglint/files/mklineparser_test.go
@@ -0,0 +1,1147 @@
+package pkglint
+
+import "gopkg.in/check.v1"
+
+// Exotic code examples from the pkgsrc infrastructure.
+// Hopefully, pkgsrc packages don't need such complicated code.
+// Still, pkglint needs to parse them correctly, or it would not
+// be able to parse and check the infrastructure files as well.
+//
+// See Pkgsrc.loadUntypedVars.
+func (s *Suite) Test_MkLineParser_Parse__infrastructure(c *check.C) {
+ t := s.Init(c)
+
+ mklines := t.NewMkLines("infra.mk",
+ MkCvsID,
+ " USE_BUILTIN.${_pkg_:S/^-//}:=no",
+ ".error \"Something went wrong\"",
+ ".export WRKDIR",
+ ".export",
+ ".unexport-env WRKDIR",
+ "",
+ ".ifmake target1", // Luckily, this is not used in the wild.
+ ".elifnmake target2", // Neither is this.
+ ".endif")
+
+ t.CheckEquals(mklines.mklines[1].Varcanon(), "USE_BUILTIN.*")
+ t.CheckEquals(mklines.mklines[2].Directive(), "error")
+ t.CheckEquals(mklines.mklines[3].Directive(), "export")
+
+ t.CheckOutputLines(
+ "WARN: infra.mk:2: Makefile lines should not start with space characters.",
+ "ERROR: infra.mk:8: Unknown Makefile line format: \".ifmake target1\".",
+ "ERROR: infra.mk:9: Unknown Makefile line format: \".elifnmake target2\".")
+
+ mklines.Check()
+
+ t.CheckOutputLines(
+ "WARN: infra.mk:2: USE_BUILTIN.${_pkg_:S/^-//} is defined but not used.",
+ "WARN: infra.mk:2: _pkg_ is used but not defined.",
+ "ERROR: infra.mk:5: \".export\" requires arguments.",
+ "NOTE: infra.mk:2: This variable value should be aligned to column 41.",
+ "ERROR: infra.mk:10: Unmatched .endif.")
+}
+
+// In variable assignments, a plain '#' introduces a line comment, unless
+// it is escaped by a backslash. In shell commands, on the other hand, it
+// is interpreted literally.
+func (s *Suite) Test_MkLineParser_Parse__comment_or_not(c *check.C) {
+ t := s.Init(c)
+
+ mklineVarassignEscaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,\\#,hash,g'")
+
+ t.CheckEquals(mklineVarassignEscaped.Varname(), "SED_CMD")
+ t.CheckEquals(mklineVarassignEscaped.Value(), "'s,#,hash,g'")
+
+ mklineCommandEscaped := t.NewMkLine("filename.mk", 1, "\tsed -e 's,\\#,hash,g'")
+
+ t.CheckEquals(mklineCommandEscaped.ShellCommand(), "sed -e 's,\\#,hash,g'")
+
+ // From shells/zsh/Makefile.common, rev. 1.78
+ mklineCommandUnescaped := t.NewMkLine("filename.mk", 1, "\t# $ sha1 patches/patch-ac")
+
+ t.CheckEquals(mklineCommandUnescaped.ShellCommand(), "# $ sha1 patches/patch-ac")
+ t.CheckOutputEmpty() // No warning about parsing the lonely dollar sign.
+
+ mklineVarassignUnescaped := t.NewMkLine("filename.mk", 1, "SED_CMD=\t's,#,hash,'")
+
+ t.CheckEquals(mklineVarassignUnescaped.Value(), "'s,")
+ t.CheckOutputLines(
+ "WARN: filename.mk:1: The # character starts a Makefile comment.")
+}
+
+func (s *Suite) Test_MkLineParser_Parse__commented_lines(c *check.C) {
+ t := s.Init(c)
+
+ test := func(text string) {
+ mkline := t.NewMkLines("filename.mk", text).mklines[0]
+ t.CheckEquals(mkline.HasComment(), true)
+ t.CheckEquals(mkline.Comment(), " the comment")
+ }
+
+ test("VAR=value # the comment")
+ test("# the comment")
+ test(".if 0 # the comment")
+ test(".include \"other.mk\" # the comment")
+ test(".include <other.mk> # the comment")
+ test("target: source # the comment")
+}
+
+func (s *Suite) Test_MkLineParser_parseVarassign(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ "VARNAME.param?=value # varassign comment")
+
+ t.CheckEquals(mkline.IsVarassign(), true)
+ t.CheckEquals(mkline.Varname(), "VARNAME.param")
+ t.CheckEquals(mkline.Varcanon(), "VARNAME.*")
+ t.CheckEquals(mkline.Varparam(), "param")
+ t.CheckEquals(mkline.Op(), opAssignDefault)
+ t.CheckEquals(mkline.Value(), "value")
+ t.CheckEquals(mkline.Comment(), " varassign comment")
+}
+
+func (s *Suite) Test_MkLineParser_parseVarassign__empty_multiline(c *check.C) {
+ t := s.Init(c)
+
+ mklines := t.NewMkLines("test.mk",
+ "VAR=\t\\",
+ "\t\\",
+ "\t\\",
+ "\t# nothing",
+ "",
+ "VAR=\t1\\",
+ "\t\\",
+ "\t\\",
+ "\t# a single letter")
+
+ // Bmake and pkglint agree that the variable value is an empty string.
+ // They don't agree on the exact whitespace in the line, though,
+ // but this doesn't matter in practice. To see the difference, run:
+ // bmake -dA 2>&1 | grep 'ParseReadLine.*VAR'
+ // See devel/bmake/files/parse.c:/non-comment, non-blank line/
+ t.CheckEquals(mklines.mklines[0].Text, "VAR= # nothing")
+ t.CheckEquals(mklines.mklines[2].Text, "VAR=\t1 # a single letter")
+
+ mkline := mklines.mklines[0]
+ t.CheckEquals(mkline.IsVarassign(), true)
+ t.CheckEquals(mkline.Varname(), "VAR")
+ t.CheckEquals(mkline.Op(), opAssign)
+ t.CheckEquals(mkline.Value(), "")
+ t.CheckEquals(mkline.Comment(), " nothing")
+}
+
+func (s *Suite) Test_MkLineParser_parseVarassign__leading_space(c *check.C) {
+ t := s.Init(c)
+
+ _ = t.NewMkLine("rubyversion.mk", 427, " _RUBYVER=\t2.15")
+ _ = t.NewMkLine("bsd.buildlink3.mk", 132, " ok:=yes")
+
+ // In mk/buildlink3/bsd.buildlink3.mk, the leading space is really helpful,
+ // therefore no warnings for that file.
+ t.CheckOutputLines(
+ "WARN: rubyversion.mk:427: Makefile lines should not start with space characters.")
+}
+
+func (s *Suite) Test_MkLineParser_parseVarassign__space_around_operator(c *check.C) {
+ t := s.Init(c)
+
+ t.SetUpCommandLine("--show-autofix", "--source")
+ t.NewMkLine("test.mk", 101,
+ "pkgbase = package")
+
+ t.CheckOutputLines(
+ "NOTE: test.mk:101: Unnecessary space after variable name \"pkgbase\".",
+ "AUTOFIX: test.mk:101: Replacing \"pkgbase =\" with \"pkgbase=\".",
+ "-\tpkgbase = package",
+ "+\tpkgbase= package")
+}
+
+func (s *Suite) Test_MkLineParser_parseVarassign__autofix_space_after_varname(c *check.C) {
+ t := s.Init(c)
+
+ t.SetUpCommandLine("-Wspace")
+ filename := t.CreateFileLines("Makefile",
+ MkCvsID,
+ "VARNAME +=\t${VARNAME}",
+ "VARNAME+ =\t${VARNAME+}",
+ "VARNAME+ +=\t${VARNAME+}",
+ "VARNAME+ ?=\t${VARNAME}",
+ "pkgbase := pkglint")
+
+ CheckFileMk(filename)
+
+ t.CheckOutputLines(
+ "NOTE: ~/Makefile:2: Unnecessary space after variable name \"VARNAME\".",
+
+ // The assignment operators other than = and += cannot lead to ambiguities.
+ "NOTE: ~/Makefile:5: Unnecessary space after variable name \"VARNAME+\".")
+
+ t.SetUpCommandLine("-Wspace", "--autofix")
+
+ CheckFileMk(filename)
+
+ t.CheckOutputLines(
+ "AUTOFIX: ~/Makefile:2: Replacing \"VARNAME +=\" with \"VARNAME+=\".",
+ "AUTOFIX: ~/Makefile:5: Replacing \"VARNAME+ ?=\" with \"VARNAME+?=\".")
+ t.CheckFileLines("Makefile",
+ MkCvsID+"",
+ "VARNAME+=\t${VARNAME}",
+ "VARNAME+ =\t${VARNAME+}",
+ "VARNAME+ +=\t${VARNAME+}",
+ "VARNAME+?=\t${VARNAME}",
+ "pkgbase := pkglint")
+}
+
+func (s *Suite) Test_MkLineParser_parseVarassign__append(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ "VARNAME+=value")
+
+ t.CheckEquals(mkline.IsVarassign(), true)
+ t.CheckEquals(mkline.Varname(), "VARNAME")
+ t.CheckEquals(mkline.Varcanon(), "VARNAME")
+ t.CheckEquals(mkline.Varparam(), "")
+}
+
+func (s *Suite) Test_MkLineParser_parseVarassign__varname_with_hash(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("Makefile", 123, "VARNAME.#=\tvalue")
+
+ // Parse error because the # starts a comment.
+ t.CheckEquals(mkline.IsVarassign(), false)
+
+ mkline2 := t.NewMkLine("Makefile", 124, "VARNAME.\\#=\tvalue")
+
+ t.CheckEquals(mkline2.IsVarassign(), true)
+ t.CheckEquals(mkline2.Varname(), "VARNAME.#")
+
+ t.CheckOutputLines(
+ "ERROR: Makefile:123: Unknown Makefile line format: \"VARNAME.#=\\tvalue\".")
+}
+
+// Ensures that pkglint parses escaped # characters in the same way as bmake.
+//
+// To check that bmake parses them the same, set a breakpoint after the t.NewMkLines
+// and look in t.tmpdir for the location of the file. Then run bmake with that file.
+func (s *Suite) Test_MkLineParser_parseVarassign__escaped_hash_in_value(c *check.C) {
+ t := s.Init(c)
+
+ mklines := t.SetUpFileMkLines("Makefile",
+ "VAR0=\tvalue#",
+ "VAR1=\tvalue\\#",
+ "VAR2=\tvalue\\\\#",
+ "VAR3=\tvalue\\\\\\#",
+ "VAR4=\tvalue\\\\\\\\#",
+ "",
+ "all:",
+ ".for var in VAR0 VAR1 VAR2 VAR3 VAR4",
+ "\t@printf '%s\\n' ${${var}}''",
+ ".endfor")
+ parsed := mklines.mklines
+
+ t.CheckEquals(parsed[0].Value(), "value")
+ t.CheckEquals(parsed[1].Value(), "value#")
+ t.CheckEquals(parsed[2].Value(), "value\\\\")
+ t.CheckEquals(parsed[3].Value(), "value\\\\#")
+ t.CheckEquals(parsed[4].Value(), "value\\\\\\\\")
+
+ t.CheckOutputLines(
+ "WARN: ~/Makefile:1: The # character starts a Makefile comment.",
+ "WARN: ~/Makefile:3: The # character starts a Makefile comment.",
+ "WARN: ~/Makefile:5: The # character starts a Makefile comment.")
+}
+
+func (s *Suite) Test_MkLineParser_MatchVarassign(c *check.C) {
+ t := s.Init(c)
+
+ testLine := func(line *Line, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string, diagnostics ...string) {
+ text := line.Text
+
+ parser := NewMkLineParser()
+ splitResult := parser.split(nil, text, true)
+ m, actual := parser.MatchVarassign(line, text, &splitResult)
+
+ assert(m)
+ expected := mkLineAssign{
+ commented: commented,
+ varname: varname,
+ varcanon: varnameCanon(varname),
+ varparam: varnameParam(varname),
+ spaceAfterVarname: spaceAfterVarname,
+ op: NewMkOperator(op),
+ valueAlign: align,
+ value: value,
+ valueMk: nil,
+ valueMkRest: "",
+ fields: nil,
+ }
+ t.CheckDeepEquals(*actual, expected)
+ t.CheckEquals(splitResult.spaceBeforeComment, spaceAfterValue)
+ t.CheckEquals(splitResult.hasComment, comment != "")
+ t.CheckEquals(condStr(splitResult.hasComment, "#", "")+splitResult.comment, comment)
+ t.CheckOutput(diagnostics)
+ }
+
+ test := func(text string, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string, diagnostics ...string) {
+ line := t.NewLine("filename.mk", 123, text)
+ testLine(line, commented, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment, diagnostics...)
+ }
+
+ testInvalid := func(text string, diagnostics ...string) {
+ line := t.NewLine("filename.mk", 123, text)
+ parser := NewMkLineParser()
+ splitResult := parser.split(nil, text, true)
+ m, _ := parser.MatchVarassign(line, text, &splitResult)
+ if m {
+ c.Errorf("Text %q matches variable assignment but shouldn't.", text)
+ }
+ t.CheckOutput(diagnostics)
+ }
+
+ lines := func(text ...string) *Line {
+ mklines := t.NewMkLines("filename.mk",
+ text...)
+ return mklines.mklines[0].Line
+ }
+
+ test("C++=c11", false, "C+", "", "+=", "C++=", "c11", "", "")
+ test("V=v", false, "V", "", "=", "V=", "v", "", "")
+ test("VAR=#comment", false, "VAR", "", "=", "VAR=", "", "", "#comment")
+ test("VAR=\\#comment", false, "VAR", "", "=", "VAR=", "#comment", "", "")
+ test("VAR=\\\\\\##comment", false, "VAR", "", "=", "VAR=", "\\\\#", "", "#comment")
+ test("VAR=\\", false, "VAR", "", "=", "VAR=", "\\", "", "")
+ test("VAR += value", false, "VAR", " ", "+=", "VAR += ", "value", "", "")
+ test(" VAR=value", false, "VAR", "", "=", " VAR=", "value", "", "")
+ test("VAR=value #comment", false, "VAR", "", "=", "VAR=", "value", " ", "#comment")
+ test("NFILES=${FILES:[#]}", false, "NFILES", "", "=", "NFILES=", "${FILES:[#]}", "", "")
+
+ // To humans, the base variable name seems to be SITES_, being parameterized
+ // with distfile-1.0.tar.gz. For pkglint though, the base variable name is
+ // SITES_distfile-1.
+ test("SITES_distfile-1.0.tar.gz=https://example.org/",
+ false,
+ "SITES_distfile-1.0.tar.gz",
+ "",
+ "=",
+ "SITES_distfile-1.0.tar.gz=",
+ "https://example.org/",
+ "",
+ "")
+
+ test("SITES_${distfile}=https://example.org/",
+ false,
+ "SITES_${distfile}",
+ "",
+ "=",
+ "SITES_${distfile}=",
+ "https://example.org/",
+ "",
+ "")
+
+ t.ExpectAssert(func() { testInvalid("\tVAR=value") })
+ testInvalid("?=value")
+ testInvalid("<=value")
+ testInvalid("#")
+ testInvalid("VAR.$$=value")
+
+ // A commented variable assignment must start immediately after the comment character.
+ // There must be no additional whitespace before the variable name.
+ test("#VAR=value", true, "VAR", "", "=", "#VAR=", "value", "", "")
+
+ // A single space is typically used for writing documentation, not for commenting out code.
+ // Therefore this line doesn't count as commented variable assignment.
+ testInvalid("# VAR=value")
+
+ // Ensure that the alignment for the variable value is correct.
+ test("BUILD_DIRS=\tdir1 dir2",
+ false,
+ "BUILD_DIRS",
+ "",
+ "=",
+ "BUILD_DIRS=\t",
+ "dir1 dir2",
+ "",
+ "")
+
+ // Ensure that the alignment for the variable value is correct,
+ // even if the whole line is commented.
+ test("#BUILD_DIRS=\tdir1 dir2",
+ true,
+ "BUILD_DIRS",
+ "",
+ "=",
+ "#BUILD_DIRS=\t",
+ "dir1 dir2",
+ "",
+ "")
+
+ test("MASTER_SITES=\t#none",
+ false,
+ "MASTER_SITES",
+ "",
+ "=",
+ "MASTER_SITES=\t",
+ "",
+ "",
+ "#none")
+
+ test("MASTER_SITES=\t# none",
+ false,
+ "MASTER_SITES",
+ "",
+ "=",
+ "MASTER_SITES=\t",
+ "",
+ "",
+ "# none")
+
+ test("EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
+
+ false,
+ "EGDIRS",
+ "",
+ "=",
+ "EGDIRS=\t",
+ "${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
+ "",
+ "")
+
+ test("VAR:=\t${VAR:M-*:[\\#]}",
+ false,
+ "VAR",
+ "",
+ ":=",
+ "VAR:=\t",
+ "${VAR:M-*:[#]}",
+ "",
+ "")
+
+ test("#VAR=value",
+ true, "VAR", "", "=", "#VAR=", "value", "", "")
+
+ testInvalid("# VAR=value")
+ testInvalid("#\tVAR=value")
+ testInvalid(MkCvsID)
+
+ testLine(
+ lines(
+ "VAR=\t\t\t\\",
+ "\tvalue"),
+ false,
+ "VAR",
+ "",
+ "=",
+ "VAR=\t\t\t",
+ "value",
+ "",
+ "")
+
+ testLine(
+ lines(
+ "#VAR=\t\t\t\\",
+ "#\tvalue"),
+ true,
+ "VAR",
+ "",
+ "=",
+ "#VAR=\t\t\t",
+ "value",
+ "",
+ "")
+}
+
+func (s *Suite) Test_MkLineParser_parseShellcmd(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ "\tshell command # shell comment")
+
+ t.CheckEquals(mkline.IsShellCommand(), true)
+ t.CheckEquals(mkline.ShellCommand(), "shell command # shell comment")
+}
+
+func (s *Suite) Test_MkLineParser_parseCommentOrEmpty__comment(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ "# whole line comment")
+
+ t.CheckEquals(mkline.IsComment(), true)
+}
+
+func (s *Suite) Test_MkLineParser_parseCommentOrEmpty__empty(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101, "")
+
+ t.CheckEquals(mkline.IsEmpty(), true)
+}
+
+func (s *Suite) Test_MkLineParser_parseDirective(c *check.C) {
+ t := s.Init(c)
+
+ test := func(input, expectedIndent, expectedDirective, expectedArgs, expectedComment string, diagnostics ...string) {
+ line := t.NewLine("filename.mk", 123, input)
+ parser := NewMkLineParser()
+ splitResult := parser.split(line, input, true)
+ mkline := parser.parseDirective(line, splitResult)
+ if !c.Check(mkline, check.NotNil) {
+ return
+ }
+
+ t.CheckDeepEquals(
+ []interface{}{mkline.Indent(), mkline.Directive(), mkline.Args(), mkline.DirectiveComment()},
+ []interface{}{expectedIndent, expectedDirective, expectedArgs, expectedComment})
+ t.CheckOutput(diagnostics)
+ }
+
+ test(".if ${VAR} == value",
+ "", "if", "${VAR} == value", "")
+
+ test(".\tendif # comment",
+ "\t", "endif", "", "comment")
+
+ test(".if ${VAR} == \"#\"",
+ "", "if", "${VAR} == \"", "\"")
+
+ test(".if ${VAR:[#]}",
+ "", "if", "${VAR:[#]}", "")
+
+ test(".if ${VAR} == \\",
+ "", "if", "${VAR} == \\", "")
+
+ test(".if ${VAR",
+ "", "if", "${VAR", "",
+ "WARN: filename.mk:123: Missing closing \"}\" for \"VAR\".")
+}
+
+func (s *Suite) Test_MkLineParser_parseDirective__escaped_hash(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ ". if !empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[\\#]} == 1 # directive comment")
+
+ t.CheckEquals(mkline.IsDirective(), true)
+ t.CheckEquals(mkline.Indent(), " ")
+ t.CheckEquals(mkline.Directive(), "if")
+ t.CheckEquals(mkline.Args(), "!empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[#]} == 1")
+ t.CheckEquals(mkline.DirectiveComment(), "directive comment")
+}
+
+func (s *Suite) Test_MkLineParser_parseInclude(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ ". include \"../../mk/bsd.prefs.mk\" # include comment")
+
+ t.CheckEquals(mkline.IsInclude(), true)
+ t.CheckEquals(mkline.Indent(), " ")
+ t.CheckEquals(mkline.MustExist(), true)
+ t.CheckEquals(mkline.IncludedFile(), "../../mk/bsd.prefs.mk")
+
+ t.CheckEquals(mkline.IsSysinclude(), false)
+}
+
+func (s *Suite) Test_MkLineParser_parseSysinclude(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ ". include <subdir.mk> # sysinclude comment")
+
+ t.CheckEquals(mkline.IsSysinclude(), true)
+ t.CheckEquals(mkline.Indent(), " ")
+ t.CheckEquals(mkline.MustExist(), true)
+ t.CheckEquals(mkline.IncludedFile(), "subdir.mk")
+
+ t.CheckEquals(mkline.IsInclude(), false)
+}
+
+func (s *Suite) Test_MkLineParser_parseDependency(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ "target1 target2: source1 source2")
+
+ t.CheckEquals(mkline.IsDependency(), true)
+ t.CheckEquals(mkline.Targets(), "target1 target2")
+ t.CheckEquals(mkline.Sources(), "source1 source2")
+}
+
+func (s *Suite) Test_MkLineParser_parseDependency__space(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ "target : source")
+
+ t.CheckEquals(mkline.Targets(), "target")
+ t.CheckEquals(mkline.Sources(), "source")
+ t.CheckOutputLines(
+ "NOTE: test.mk:101: Space before colon in dependency line.")
+}
+
+func (s *Suite) Test_MkLineParser_parseMergeConflict(c *check.C) {
+ t := s.Init(c)
+
+ mkline := t.NewMkLine("test.mk", 101,
+ "<<<<<<<<<<<<<<<<<")
+
+ // Merge conflicts are of neither type.
+ t.CheckEquals(mkline.IsVarassign(), false)
+ t.CheckEquals(mkline.IsDirective(), false)
+ t.CheckEquals(mkline.IsInclude(), false)
+ t.CheckEquals(mkline.IsEmpty(), false)
+ t.CheckEquals(mkline.IsComment(), false)
+ t.CheckEquals(mkline.IsDependency(), false)
+ t.CheckEquals(mkline.IsShellCommand(), false)
+ t.CheckEquals(mkline.IsSysinclude(), false)
+}
+
+func (s *Suite) Test_MkLineParser_split(c *check.C) {
+ t := s.Init(c)
+ b := NewMkTokenBuilder()
+
+ varuse := b.VaruseToken
+ varuseText := b.VaruseTextToken
+ text := b.TextToken
+ tokens := b.Tokens
+
+ test := func(text string, expected mkLineSplitResult, diagnostics ...string) {
+ line := t.NewLine("filename.mk", 123, text)
+ actual := NewMkLineParser().split(line, text, true)
+
+ t.CheckOutput(diagnostics)
+ t.CheckDeepEquals([]interface{}{text, actual}, []interface{}{text, expected})
+ }
+
+ t.Use(text, varuse, varuseText, tokens)
+
+ test(
+ "",
+ mkLineSplitResult{})
+
+ test(
+ "text",
+ mkLineSplitResult{
+ main: "text",
+ tokens: tokens(text("text")),
+ })
+
+ // Leading space is always kept.
+ test(
+ " text",
+ mkLineSplitResult{
+ main: " text",
+ tokens: tokens(text(" text")),
+ })
+
+ // Trailing space does not end up in the tokens since it is usually
+ // ignored.
+ test(
+ "text\t",
+ mkLineSplitResult{
+ main: "text",
+ tokens: tokens(text("text")),
+ spaceBeforeComment: "\t",
+ })
+
+ test(
+ "text\t# intended comment",
+ mkLineSplitResult{
+ main: "text",
+ tokens: tokens(text("text")),
+ spaceBeforeComment: "\t",
+ hasComment: true,
+ comment: " intended comment",
+ })
+
+ // Trailing space is saved in a separate field to detect accidental
+ // unescaped # in the middle of a word, like the URL fragment in this
+ // example.
+ test(
+ "url#fragment",
+ mkLineSplitResult{
+ main: "url",
+ tokens: tokens(text("url")),
+ hasComment: true,
+ comment: "fragment",
+ })
+
+ // The leading space from the comment is preserved to make parsing as exact
+ // as possible.
+ //
+ // The difference between "#defined" and "# defined" is relevant in a few
+ // cases, such as the API documentation of the infrastructure files.
+ test("# comment",
+ mkLineSplitResult{
+ hasComment: true,
+ comment: " comment",
+ })
+
+ test("#\tcomment",
+ mkLineSplitResult{
+ hasComment: true,
+ comment: "\tcomment",
+ })
+
+ test("# comment",
+ mkLineSplitResult{
+ hasComment: true,
+ comment: " comment",
+ })
+
+ test(
+ "#VAR=#value",
+ mkLineSplitResult{
+ hasComment: true,
+ comment: "VAR=#value"})
+
+ // When parsing a commented variable assignment, the code assumes that
+ // the whole comment is left uninterpreted.
+ test(
+ "#VAR=\\#value",
+ mkLineSplitResult{
+ hasComment: true,
+ comment: "VAR=\\#value"})
+
+ // Other than in the shell, # also starts a comment in the middle of a word.
+ test("COMMENT=\tThe C# compiler",
+ mkLineSplitResult{
+ main: "COMMENT=\tThe C",
+ tokens: tokens(text("COMMENT=\tThe C")),
+ hasComment: true,
+ comment: " compiler",
+ })
+
+ test("COMMENT=\tThe C\\# compiler",
+ mkLineSplitResult{
+ main: "COMMENT=\tThe C# compiler",
+ tokens: tokens(text("COMMENT=\tThe C# compiler")),
+ hasComment: false,
+ comment: "",
+ })
+
+ test("${TARGET}: ${SOURCES} # comment",
+ mkLineSplitResult{
+ main: "${TARGET}: ${SOURCES}",
+ tokens: tokens(varuse("TARGET"), text(": "), varuse("SOURCES")),
+ spaceBeforeComment: " ",
+ hasComment: true,
+ comment: " comment",
+ })
+
+ // A # starts a comment, except if it immediately follows a [.
+ // This is done so that the length modifier :[#] can be written without
+ // escaping the #.
+ test("VAR=\t${OTHER:[#]} # comment",
+ mkLineSplitResult{
+ main: "VAR=\t${OTHER:[#]}",
+ tokens: tokens(text("VAR=\t"), varuse("OTHER", "[#]")),
+ spaceBeforeComment: " ",
+ hasComment: true,
+ comment: " comment",
+ })
+
+ // The # in the :[#] modifier may be escaped or not. Both forms are equivalent.
+ test("VAR:=\t${VAR:M-*:[\\#]}",
+ mkLineSplitResult{
+ main: "VAR:=\t${VAR:M-*:[#]}",
+ tokens: tokens(text("VAR:=\t"), varuse("VAR", "M-*", "[#]")),
+ })
+
+ // A backslash always escapes the next character, be it a # for a comment
+ // or something else. This makes it difficult to write a literal \# in a
+ // Makefile, but that's an edge case anyway.
+ test("VAR0=\t#comment",
+ mkLineSplitResult{
+ main: "VAR0=",
+ tokens: tokens(text("VAR0=")),
+ // Later, when converting this result into a proper variable assignment,
+ // this "space before comment" is reclassified as "space before the value",
+ // in order to align the "#comment" with the other variable values.
+ spaceBeforeComment: "\t",
+ hasComment: true,
+ comment: "comment",
+ })
+
+ test("VAR1=\t\\#no-comment",
+ mkLineSplitResult{
+ main: "VAR1=\t#no-comment",
+ tokens: tokens(text("VAR1=\t#no-comment")),
+ })
+
+ test("VAR2=\t\\\\#comment",
+ mkLineSplitResult{
+ main: "VAR2=\t\\\\",
+ tokens: tokens(text("VAR2=\t\\\\")),
+ hasComment: true,
+ comment: "comment",
+ })
+
+ // The backslash is only removed when it escapes a comment.
+ // In particular, it cannot be used to escape a dollar that starts a
+ // variable use.
+ test("VAR0=\t$T",
+ mkLineSplitResult{
+ main: "VAR0=\t$T",
+ tokens: tokens(text("VAR0=\t"), varuseText("$T", "T")),
+ },
+ "WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")
+
+ test("VAR1=\t\\$T",
+ mkLineSplitResult{
+ main: "VAR1=\t\\$T",
+ tokens: tokens(text("VAR1=\t\\"), varuseText("$T", "T")),
+ },
+ "WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")
+
+ test("VAR2=\t\\\\$T",
+ mkLineSplitResult{
+ main: "VAR2=\t\\\\$T",
+ tokens: tokens(text("VAR2=\t\\\\"), varuseText("$T", "T")),
+ },
+ "WARN: filename.mk:123: $T is ambiguous. Use ${T} if you mean a Make variable or $$T if you mean a shell variable.")
+
+ // To escape a dollar, write it twice.
+ test("$$shellvar $${shellvar} \\${MKVAR} [] \\x",
+ mkLineSplitResult{
+ main: "$$shellvar $${shellvar} \\${MKVAR} [] \\x",
+ tokens: tokens(text("$$shellvar $${shellvar} \\"), varuse("MKVAR"), text(" [] \\x")),
+ })
+
+ // Parse errors are recorded in the rest return value.
+ test("${UNCLOSED",
+ mkLineSplitResult{
+ main: "${UNCLOSED",
+ tokens: tokens(varuseText("${UNCLOSED", "UNCLOSED")),
+ },
+ "WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED\".")
+
+ // Even if there is a parse error in the main part,
+ // the comment is extracted.
+ test("text before ${UNCLOSED# comment",
+ mkLineSplitResult{
+ main: "text before ${UNCLOSED",
+ tokens: tokens(
+ text("text before "),
+ varuseText("${UNCLOSED", "UNCLOSED")),
+ hasComment: true,
+ comment: " comment",
+ },
+ "WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED\".")
+
+ // Even in case of parse errors, the space before the comment is parsed
+ // correctly.
+ test("text before ${UNCLOSED # comment",
+ mkLineSplitResult{
+ main: "text before ${UNCLOSED",
+ tokens: tokens(
+ text("text before "),
+ // It's a bit inconsistent that the varname includes the space
+ // but the text doesn't; anyway, it's an edge case.
+ varuseText("${UNCLOSED", "UNCLOSED ")),
+ spaceBeforeComment: " ",
+ hasComment: true,
+ comment: " comment",
+ },
+ "WARN: filename.mk:123: Missing closing \"}\" for \"UNCLOSED \".",
+ "WARN: filename.mk:123: Invalid part \" \" after variable name \"UNCLOSED\".")
+
+ // The dollar-space refers to a normal Make variable named " ".
+ // The lonely dollar at the very end refers to the variable named "",
+ // which is specially protected in bmake to always contain the empty string.
+ // It is heavily used in .for loops in the form ${:Uvalue}.
+ //
+ // TODO: The rest of pkglint assumes that the empty string is not a valid
+ // variable name, mainly because the empty variable name is not visible
+ // outside of the bmake debugging mode.
+ test("Lonely $ character $",
+ mkLineSplitResult{
+ main: "Lonely $ character $",
+ tokens: tokens(
+ text("Lonely "),
+ varuseText("$ " /* instead of "${ }" */, " "),
+ text("character "),
+ text("$")),
+ })
+
+ // The character [ prevents the following # from starting a comment, even
+ // outside of variable modifiers.
+ test("COMMENT=\t[#] $$\\# $$# comment",
+ mkLineSplitResult{
+ main: "COMMENT=\t[#] $$# $$",
+ tokens: tokens(text("COMMENT=\t[#] $$# $$")),
+ hasComment: true,
+ comment: " comment",
+ })
+
+ test("VAR2=\t\\\\#comment",
+ mkLineSplitResult{
+ main: "VAR2=\t\\\\",
+ tokens: tokens(text("VAR2=\t\\\\")),
+ hasComment: true,
+ comment: "comment",
+ })
+
+ // At this stage, MkLine.split doesn't know that empty(...) takes
+ // a variable use. Instead it just sees ordinary characters and
+ // other uses of variables.
+ test(".if empty(${VAR.${tool}}:C/\\:.*$//:M${pattern})",
+ mkLineSplitResult{
+ main: ".if empty(${VAR.${tool}}:C/\\:.*$//:M${pattern})",
+ tokens: tokens(
+ text(".if empty("),
+ varuse("VAR.${tool}"),
+ text(":C/\\:.*"),
+ text("$"),
+ text("//:M"),
+ varuse("pattern"),
+ text(")")),
+ })
+
+ test(" # comment after spaces",
+ mkLineSplitResult{
+ spaceBeforeComment: " ",
+ hasComment: true,
+ comment: " comment after spaces",
+ })
+
+ // FIXME: This theoretical edge case is interpreted differently
+ // between bmake and pkglint. Pkglint treats the # as a comment,
+ // while bmake interprets it as a regular character.
+ test("\\[#",
+ mkLineSplitResult{
+ main: "\\[",
+ tokens: tokens(text("\\[")),
+ hasComment: true,
+ })
+
+ test("\\\\[#",
+ mkLineSplitResult{
+ main: "\\\\[#",
+ tokens: tokens(text("\\\\[#")),
+ })
+}
+
+func (s *Suite) Test_MkLineParser_split__preserve_comment(c *check.C) {
+ t := s.Init(c)
+ b := NewMkTokenBuilder()
+
+ tokens := b.Tokens
+ text := b.TextToken
+ varUse := b.VaruseToken
+
+ test := func(text string, expected mkLineSplitResult, diagnostics ...string) {
+ line := t.NewLine("filename.mk", 123, text)
+ actual := NewMkLineParser().split(line, text, false)
+
+ t.CheckDeepEquals(actual, expected)
+ t.CheckOutput(diagnostics)
+ }
+
+ test(
+ "text\t# no comment",
+ mkLineSplitResult{
+ main: "text\t# no comment",
+ tokens: tokens(text("text\t# no comment"))})
+
+ test(
+ "url#fragment",
+ mkLineSplitResult{
+ main: "url#fragment",
+ tokens: tokens(text("url#fragment"))})
+
+ test("# no comment",
+ mkLineSplitResult{
+ main: "# no comment",
+ tokens: tokens(text("# no comment"))})
+
+ // Other than in the shell, # also starts a comment in the middle of a word.
+ test("The C# compiler",
+ mkLineSplitResult{
+ main: "The C# compiler",
+ tokens: tokens(text("The C# compiler"))})
+
+ test("The C\\# compiler",
+ mkLineSplitResult{
+ main: "The C\\# compiler",
+ tokens: tokens(text("The C\\# compiler"))})
+
+ test("# ${VAR}",
+ mkLineSplitResult{
+ main: "# ${VAR}",
+ tokens: tokens(text("# "), varUse("VAR"))})
+
+ test("# ",
+ mkLineSplitResult{
+ main: "#",
+ tokens: tokens(text("#")),
+ spaceBeforeComment: " "})
+}
+
+func (s *Suite) Test_MkLineParser_split__unclosed_varuse(c *check.C) {
+ t := s.Init(c)
+ b := NewMkTokenBuilder()
+
+ test := func(text string, expected mkLineSplitResult, diagnostics ...string) {
+ line := t.NewLine("filename.mk", 123, text)
+
+ splitResult := NewMkLineParser().split(line, text, true)
+
+ t.CheckDeepEquals(splitResult, expected)
+ t.CheckOutput(diagnostics)
+ }
+
+ test(
+ "EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
+
+ mkLineSplitResult{
+ "EGDIRS=\t${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
+ b.Tokens(
+ b.TextToken("EGDIRS=\t"),
+ b.VaruseTextToken(
+ "${EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d",
+ "EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d")),
+ "",
+ false,
+ "",
+ },
+
+ "WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/pam.d\".",
+ "WARN: filename.mk:123: Invalid part \"/pam.d\" after variable name \"EGDIR\".",
+ "WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/dbus-1/system.d ${EGDIR/pam.d\".",
+ "WARN: filename.mk:123: Invalid part \"/dbus-1/system.d ${EGDIR/pam.d\" after variable name \"EGDIR\".",
+ "WARN: filename.mk:123: Missing closing \"}\" for \"EGDIR/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\".",
+ "WARN: filename.mk:123: Invalid part \"/apparmor.d ${EGDIR/dbus-1/system.d ${EGDIR/pam.d\" after variable name \"EGDIR\".")
+}
+
+func (s *Suite) Test_MkLineParser_unescapeComment(c *check.C) {
+ t := s.Init(c)
+
+ test := func(text string, main, comment string) {
+ aMain, aComment := NewMkLineParser().unescapeComment(text)
+ t.CheckDeepEquals(
+ []interface{}{text, aMain, aComment},
+ []interface{}{text, main, comment})
+ }
+
+ test("",
+ "",
+ "")
+ test("text",
+ "text",
+ "")
+
+ // The leading space from the comment is preserved to make parsing as exact
+ // as possible.
+ //
+ // The difference between "#defined" and "# defined" is relevant in a few
+ // cases, such as the API documentation of the infrastructure files.
+ test("# comment",
+ "",
+ "# comment")
+ test("#\tcomment",
+ "",
+ "#\tcomment")
+ test("# comment",
+ "",
+ "# comment")
+
+ // Other than in the shell, # also starts a comment in the middle of a word.
+ test("COMMENT=\tThe C# compiler",
+ "COMMENT=\tThe C",
+ "# compiler")
+ test("COMMENT=\tThe C\\# compiler",
+ "COMMENT=\tThe C# compiler",
+ "")
+
+ test("${TARGET}: ${SOURCES} # comment",
+ "${TARGET}: ${SOURCES} ",
+ "# comment")
+
+ // A # starts a comment, except if it immediately follows a [.
+ // This is done so that the length modifier :[#] can be written without
+ // escaping the #.
+ test("VAR=\t${OTHER:[#]} # comment",
+ "VAR=\t${OTHER:[#]} ",
+ "# comment")
+
+ // The # in the :[#] modifier may be escaped or not. Both forms are equivalent.
+ test("VAR:=\t${VAR:M-*:[\\#]}",
+ "VAR:=\t${VAR:M-*:[#]}",
+ "")
+
+ // The character [ prevents the following # from starting a comment, even
+ // outside of variable modifiers.
+ test("COMMENT=\t[#] $$\\# $$# comment",
+ "COMMENT=\t[#] $$# $$",
+ "# comment")
+
+ // A backslash always escapes the next character, be it a # for a comment
+ // or something else. This makes it difficult to write a literal \# in a
+ // Makefile, but that's an edge case anyway.
+ test("VAR0=\t#comment",
+ "VAR0=\t",
+ "#comment")
+ test("VAR1=\t\\#no-comment",
+ "VAR1=\t#no-comment",
+ "")
+ test("VAR2=\t\\\\#comment",
+ "VAR2=\t\\\\",
+ "#comment")
+
+ // The backslash is only removed when it escapes a comment.
+ // In particular, it cannot be used to escape a dollar that starts a
+ // variable use.
+ test("VAR0=\t$T",
+ "VAR0=\t$T",
+ "")
+ test("VAR1=\t\\$T",
+ "VAR1=\t\\$T",
+ "")
+ test("VAR2=\t\\\\$T",
+ "VAR2=\t\\\\$T",
+ "")
+
+ // To escape a dollar, write it twice.
+ test("$$shellvar $${shellvar} \\${MKVAR} [] \\x",
+ "$$shellvar $${shellvar} \\${MKVAR} [] \\x",
+ "")
+
+ // Parse errors are recorded in the rest return value.
+ test("${UNCLOSED",
+ "${UNCLOSED",
+ "")
+
+ // In this early phase of parsing, unfinished variable uses are not
+ // interpreted and do not influence the detection of the comment start.
+ test("text before ${UNCLOSED # comment",
+ "text before ${UNCLOSED ",
+ "# comment")
+
+ // The dollar-space refers to a normal Make variable named " ".
+ // The lonely dollar at the very end refers to the variable named "",
+ // which is specially protected in bmake to always contain the empty string.
+ // It is heavily used in .for loops in the form ${:Uvalue}.
+ test("Lonely $ character $",
+ "Lonely $ character $",
+ "")
+
+ // An even number of backslashes does not escape the #.
+ // Therefore it starts a comment here.
+ test("VAR2=\t\\\\#comment",
+ "VAR2=\t\\\\",
+ "#comment")
+}
+
+func (s *Suite) Test_MkLineParser_getRawValueAlign__assertion(c *check.C) {
+ t := s.Init(c)
+
+ var p MkLineParser
+
+ // This is unrealistic; just for code coverage of the assertion.
+ t.ExpectAssert(func() { p.getRawValueAlign("a", "b") })
+}
diff --git a/pkgtools/pkglint/files/mklines.go b/pkgtools/pkglint/files/mklines.go
index 0bbb14ed785..4741c8860cc 100644
--- a/pkgtools/pkglint/files/mklines.go
+++ b/pkgtools/pkglint/files/mklines.go
@@ -26,7 +26,7 @@ type MkLines struct {
func NewMkLines(lines *Lines) *MkLines {
mklines := make([]*MkLine, lines.Len())
for i, line := range lines.Lines {
- mklines[i] = MkLineParser{}.Parse(line)
+ mklines[i] = NewMkLineParser().Parse(line)
}
tools := NewTools()
@@ -578,5 +578,5 @@ func (mklines *MkLines) SaveAutofixChanges() {
}
func (mklines *MkLines) EOFLine() *MkLine {
- return MkLineParser{}.Parse(mklines.lines.EOFLine())
+ return NewMkLineParser().Parse(mklines.lines.EOFLine())
}
diff --git a/pkgtools/pkglint/files/mkparser_test.go b/pkgtools/pkglint/files/mkparser_test.go
index ee6d8087c1a..635b7ae1e6a 100644
--- a/pkgtools/pkglint/files/mkparser_test.go
+++ b/pkgtools/pkglint/files/mkparser_test.go
@@ -7,6 +7,7 @@ import (
func (s *Suite) Test_MkParser_MkTokens(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
testRest := func(input string, expectedTokens []*MkToken, expectedRest string) {
line := t.NewLines("Test_MkParser_MkTokens.mk", input).Lines[0]
@@ -22,19 +23,10 @@ func (s *Suite) Test_MkParser_MkTokens(c *check.C) {
t.CheckEquals(p.Rest(), expectedRest)
}
test := func(input string, expectedToken *MkToken) {
- testRest(input, []*MkToken{expectedToken}, "")
- }
- literal := func(text string) *MkToken {
- return &MkToken{Text: text}
- }
- varuse := func(varname string, modifiers ...string) *MkToken {
- text := "${" + varname
- for _, modifier := range modifiers {
- text += ":" + modifier
- }
- text += "}"
- return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
+ testRest(input, b.Tokens(expectedToken), "")
}
+ literal := b.TextToken
+ varuse := b.VaruseToken
// Everything except VarUses is passed through unmodified.
@@ -53,38 +45,42 @@ func (s *Suite) Test_MkParser_MkTokens(c *check.C) {
test("$$var1 $$var2 $$? $$",
literal("$$var1 $$var2 $$? $$"))
- testRest("hello, ${W:L:tl}orld", []*MkToken{
- literal("hello, "),
- varuse("W", "L", "tl"),
- literal("orld")},
+ testRest("hello, ${W:L:tl}orld",
+ b.Tokens(
+ literal("hello, "),
+ varuse("W", "L", "tl"),
+ literal("orld")),
"")
- testRest("ftp://${PKGNAME}/ ${MASTER_SITES:=subdir/}", []*MkToken{
- literal("ftp://"),
- varuse("PKGNAME"),
- literal("/ "),
- varuse("MASTER_SITES", "=subdir/")},
+ testRest("ftp://${PKGNAME}/ ${MASTER_SITES:=subdir/}",
+ b.Tokens(
+ literal("ftp://"),
+ varuse("PKGNAME"),
+ literal("/ "),
+ varuse("MASTER_SITES", "=subdir/")),
"")
testRest("${VAR:S,a,b,c,d,e,f}",
- []*MkToken{{
- Text: "${VAR:S,a,b,c,d,e,f}",
- Varuse: NewMkVarUse("VAR", "S,a,b,")}},
+ b.Tokens(b.VaruseTextToken("${VAR:S,a,b,c,d,e,f}", "VAR", "S,a,b,")),
"")
t.CheckOutputLines(
"WARN: Test_MkParser_MkTokens.mk:1: Invalid variable modifier \"c,d,e,f\" for \"VAR\".")
- testRest("Text${VAR:Mmodifier}${VAR2}more text${VAR3}", []*MkToken{
- literal("Text"),
- varuse("VAR", "Mmodifier"),
- varuse("VAR2"),
- literal("more text"),
- varuse("VAR3")},
+ testRest("Text${VAR:Mmodifier}${VAR2}more text${VAR3}",
+ b.Tokens(
+ literal("Text"),
+ varuse("VAR", "Mmodifier"),
+ varuse("VAR2"),
+ literal("more text"),
+ varuse("VAR3")),
"")
}
func (s *Suite) Test_MkParser_VarUse(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
+ varuse := b.VaruseToken
+ varuseText := b.VaruseTextToken
testRest := func(input string, expectedTokens []*MkToken, expectedRest string, diagnostics ...string) {
line := t.NewLines("Test_MkParser_VarUse.mk", input).Lines[0]
@@ -102,23 +98,11 @@ func (s *Suite) Test_MkParser_VarUse(c *check.C) {
t.CheckEquals(p.Rest(), expectedRest)
t.CheckOutput(diagnostics)
}
- tokens := func(tokens ...*MkToken) []*MkToken { return tokens }
test := func(input string, expectedToken *MkToken, diagnostics ...string) {
- testRest(input, []*MkToken{expectedToken}, "", diagnostics...)
- }
- varuse := func(varname string, modifiers ...string) *MkToken {
- text := "${" + varname
- for _, modifier := range modifiers {
- text += ":" + modifier
- }
- text += "}"
- return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
- }
- varuseText := func(text, varname string, modifiers ...string) *MkToken {
- return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
+ testRest(input, b.Tokens(expectedToken), "", diagnostics...)
}
- t.Use(testRest, tokens, test, varuse, varuseText)
+ t.Use(testRest, test, varuse, varuseText)
test("${VARIABLE}",
varuse("VARIABLE"))
@@ -446,6 +430,7 @@ func (s *Suite) Test_MkParser_varUseModifier__square_bracket(c *check.C) {
func (s *Suite) Test_MkParser_varUseModifier__condition_without_colon(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
line := t.NewLine("filename.mk", 123, "${${VAR}:?yes:no}${${VAR}:?yes}")
p := NewMkParser(line, line.Text)
@@ -453,8 +438,8 @@ func (s *Suite) Test_MkParser_varUseModifier__condition_without_colon(c *check.C
varUse1 := p.VarUse()
varUse2 := p.VarUse()
- t.CheckDeepEquals(varUse1, NewMkVarUse("${VAR}", "?yes:no"))
- t.CheckDeepEquals(varUse2, NewMkVarUse("${VAR}"))
+ t.CheckDeepEquals(varUse1, b.VarUse("${VAR}", "?yes:no"))
+ t.CheckDeepEquals(varUse2, b.VarUse("${VAR}"))
t.CheckEquals(p.Rest(), "")
t.CheckOutputLines(
@@ -463,13 +448,14 @@ func (s *Suite) Test_MkParser_varUseModifier__condition_without_colon(c *check.C
func (s *Suite) Test_MkParser_varUseModifier__malformed_in_parentheses(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
line := t.NewLine("filename.mk", 123, "$(${VAR}:?yes)")
p := NewMkParser(line, line.Text)
varUse := p.VarUse()
- t.CheckDeepEquals(varUse, NewMkVarUse("${VAR}"))
+ t.CheckDeepEquals(varUse, b.VarUse("${VAR}"))
t.CheckEquals(p.Rest(), "")
t.CheckOutputLines(
@@ -479,13 +465,14 @@ func (s *Suite) Test_MkParser_varUseModifier__malformed_in_parentheses(c *check.
func (s *Suite) Test_MkParser_varUseModifier__varuse_in_malformed_modifier(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
line := t.NewLine("filename.mk", 123, "${${VAR}:?yes${INNER}}")
p := NewMkParser(line, line.Text)
varUse := p.VarUse()
- t.CheckDeepEquals(varUse, NewMkVarUse("${VAR}"))
+ t.CheckDeepEquals(varUse, b.VarUse("${VAR}"))
t.CheckEquals(p.Rest(), "")
t.CheckOutputLines(
@@ -494,13 +481,14 @@ func (s *Suite) Test_MkParser_varUseModifier__varuse_in_malformed_modifier(c *ch
func (s *Suite) Test_MkParser_varUseModifierAt__missing_at_after_variable_name(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
line := t.NewLine("filename.mk", 123, "${VAR:@varname}")
p := NewMkParser(line, line.Text)
varUse := p.VarUse()
- t.CheckDeepEquals(varUse, NewMkVarUse("VAR"))
+ t.CheckDeepEquals(varUse, b.VarUse("VAR"))
t.CheckEquals(p.Rest(), "")
t.CheckOutputLines(
"WARN: filename.mk:123: Invalid variable modifier \"@varname\" for \"VAR\".")
@@ -508,31 +496,34 @@ func (s *Suite) Test_MkParser_varUseModifierAt__missing_at_after_variable_name(c
func (s *Suite) Test_MkParser_varUseModifierAt__dollar(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
line := t.NewLine("filename.mk", 123, "${VAR:@var@$$var@}")
p := NewMkParser(line, line.Text)
varUse := p.VarUse()
- t.CheckDeepEquals(varUse, NewMkVarUse("VAR", "@var@$$var@"))
+ t.CheckDeepEquals(varUse, b.VarUse("VAR", "@var@$$var@"))
t.CheckEquals(p.Rest(), "")
t.CheckOutputEmpty()
}
func (s *Suite) Test_MkParser_varUseModifierAt__incomplete_without_warning(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
p := NewMkParser(nil, "${VAR:@var@$$var}rest")
varUse := p.VarUse()
- t.CheckDeepEquals(varUse, NewMkVarUse("VAR", "@var@$$var}rest"))
+ t.CheckDeepEquals(varUse, b.VarUse("VAR", "@var@$$var}rest"))
t.CheckEquals(p.Rest(), "")
t.CheckOutputEmpty()
}
func (s *Suite) Test_MkParser_VarUse__ambiguous(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
t.SetUpCommandLine("--explain")
@@ -540,10 +531,10 @@ func (s *Suite) Test_MkParser_VarUse__ambiguous(c *check.C) {
p := NewMkParser(line, line.Text[1:])
tokens := p.MkTokens()
- t.CheckDeepEquals(tokens, []*MkToken{
- {"$V", NewMkVarUse("V")},
- {"arname ", nil},
- {"$X", NewMkVarUse("X")}})
+ t.CheckDeepEquals(tokens, b.Tokens(
+ b.VaruseTextToken("$V", "V"),
+ b.TextToken("arname "),
+ b.VaruseTextToken("$X", "X")))
t.CheckOutputLines(
"ERROR: module.mk:123: $Varname is ambiguous. Use ${Varname} if you mean a Make variable or $$Varname if you mean a shell variable.",
@@ -563,6 +554,8 @@ func (s *Suite) Test_MkParser_VarUse__ambiguous(c *check.C) {
func (s *Suite) Test_MkParser_MkCond(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
+ varUse := b.VarUse
testRest := func(input string, expectedTree *MkCond, expectedRest string) {
// As of July 2019 p.MkCond does not emit warnings;
@@ -576,57 +569,57 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) {
test := func(input string, expectedTree *MkCond) {
testRest(input, expectedTree, "")
}
- varUse := func(name string, modifiers ...string) MkCondTerm {
- return MkCondTerm{Var: NewMkVarUse(name, modifiers...)}
+ varTerm := func(name string, modifiers ...string) MkCondTerm {
+ return MkCondTerm{Var: varUse(name, modifiers...)}
}
str := func(s string) MkCondTerm { return MkCondTerm{Str: s} }
num := func(s string) MkCondTerm { return MkCondTerm{Num: s} }
- t.Use(testRest, test, varUse)
+ t.Use(testRest, test, varTerm)
test("${OPSYS:MNetBSD}",
- &MkCond{Term: &MkCondTerm{Var: NewMkVarUse("OPSYS", "MNetBSD")}})
+ &MkCond{Term: &MkCondTerm{Var: varUse("OPSYS", "MNetBSD")}})
test("defined(VARNAME)",
&MkCond{Defined: "VARNAME"})
test("empty(VARNAME)",
- &MkCond{Empty: NewMkVarUse("VARNAME")})
+ &MkCond{Empty: varUse("VARNAME")})
test("!empty(VARNAME)",
- &MkCond{Not: &MkCond{Empty: NewMkVarUse("VARNAME")}})
+ &MkCond{Not: &MkCond{Empty: varUse("VARNAME")}})
test("!empty(VARNAME:M[yY][eE][sS])",
- &MkCond{Not: &MkCond{Empty: NewMkVarUse("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: NewMkVarUse("USE_TOOLS", "Mautoconf:run")}})
+ &MkCond{Not: &MkCond{Empty: varUse("USE_TOOLS", "Mautoconf:run")}})
test("${VARNAME} != \"Value\"",
- &MkCond{Compare: &MkCondCompare{varUse("VARNAME"), "!=", str("Value")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("VARNAME"), "!=", str("Value")}})
test("${VARNAME:Mi386} != \"Value\"",
- &MkCond{Compare: &MkCondCompare{varUse("VARNAME", "Mi386"), "!=", str("Value")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("VARNAME", "Mi386"), "!=", str("Value")}})
test("${VARNAME} != Value",
- &MkCond{Compare: &MkCondCompare{varUse("VARNAME"), "!=", str("Value")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("VARNAME"), "!=", str("Value")}})
test("\"${VARNAME}\" != Value",
- &MkCond{Compare: &MkCondCompare{varUse("VARNAME"), "!=", str("Value")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("VARNAME"), "!=", str("Value")}})
test("${pkg} == \"${name}\"",
- &MkCond{Compare: &MkCondCompare{varUse("pkg"), "==", varUse("name")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("pkg"), "==", varTerm("name")}})
test("\"${pkg}\" == \"${name}\"",
- &MkCond{Compare: &MkCondCompare{varUse("pkg"), "==", varUse("name")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("pkg"), "==", varTerm("name")}})
// The right-hand side is not analyzed further to keep the data types simple.
test("${ABC} == \"${A}B${C}\"",
- &MkCond{Compare: &MkCondCompare{varUse("ABC"), "==", str("${A}B${C}")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("ABC"), "==", str("${A}B${C}")}})
test("${ABC} == \"${A}\\\"${B}\\\\${C}$${shellvar}${D}\"",
- &MkCond{Compare: &MkCondCompare{varUse("ABC"), "==", str("${A}\"${B}\\${C}$${shellvar}${D}")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("ABC"), "==", str("${A}\"${B}\\${C}$${shellvar}${D}")}})
test("exists(/etc/hosts)",
&MkCond{Call: &MkCondCall{"exists", "/etc/hosts"}})
@@ -636,13 +629,13 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) {
test("${OPSYS} == \"NetBSD\" || ${OPSYS} == \"OpenBSD\"",
&MkCond{Or: []*MkCond{
- {Compare: &MkCondCompare{varUse("OPSYS"), "==", str("NetBSD")}},
- {Compare: &MkCondCompare{varUse("OPSYS"), "==", str("OpenBSD")}}}})
+ {Compare: &MkCondCompare{varTerm("OPSYS"), "==", str("NetBSD")}},
+ {Compare: &MkCondCompare{varTerm("OPSYS"), "==", str("OpenBSD")}}}})
test("${OPSYS} == \"NetBSD\" && ${MACHINE_ARCH} == \"i386\"",
&MkCond{And: []*MkCond{
- {Compare: &MkCondCompare{varUse("OPSYS"), "==", str("NetBSD")}},
- {Compare: &MkCondCompare{varUse("MACHINE_ARCH"), "==", str("i386")}}}})
+ {Compare: &MkCondCompare{varTerm("OPSYS"), "==", str("NetBSD")}},
+ {Compare: &MkCondCompare{varTerm("MACHINE_ARCH"), "==", str("i386")}}}})
test("defined(A) && defined(B) || defined(C) && defined(D)",
&MkCond{Or: []*MkCond{
@@ -655,11 +648,11 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) {
test("${MACHINE_ARCH:Mi386} || ${MACHINE_OPSYS:MNetBSD}",
&MkCond{Or: []*MkCond{
- {Term: &MkCondTerm{Var: NewMkVarUse("MACHINE_ARCH", "Mi386")}},
- {Term: &MkCondTerm{Var: NewMkVarUse("MACHINE_OPSYS", "MNetBSD")}}}})
+ {Term: &MkCondTerm{Var: varUse("MACHINE_ARCH", "Mi386")}},
+ {Term: &MkCondTerm{Var: varUse("MACHINE_OPSYS", "MNetBSD")}}}})
test("${VAR} == \"${VAR}suffix\"",
- &MkCond{Compare: &MkCondCompare{varUse("VAR"), "==", str("${VAR}suffix")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("VAR"), "==", str("${VAR}suffix")}})
// Exotic cases
@@ -673,7 +666,7 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) {
test("${VAR} == 0xCAFEBABE",
&MkCond{
Compare: &MkCondCompare{
- varUse("VAR"),
+ varTerm("VAR"),
"==",
num("0xCAFEBABE")}})
@@ -681,26 +674,26 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) {
&MkCond{Not: &MkCond{
And: []*MkCond{
{Defined: "A"},
- {Empty: NewMkVarUse("VARNAME")}}}})
+ {Empty: varUse("VARNAME")}}}})
test("${REQD_MAJOR} > ${MAJOR}",
- &MkCond{Compare: &MkCondCompare{varUse("REQD_MAJOR"), ">", varUse("MAJOR")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("REQD_MAJOR"), ">", varTerm("MAJOR")}})
test("${OS_VERSION} >= 6.5",
- &MkCond{Compare: &MkCondCompare{varUse("OS_VERSION"), ">=", num("6.5")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("OS_VERSION"), ">=", num("6.5")}})
test("${OS_VERSION} == 5.3",
- &MkCond{Compare: &MkCondCompare{varUse("OS_VERSION"), "==", num("5.3")}})
+ &MkCond{Compare: &MkCondCompare{varTerm("OS_VERSION"), "==", num("5.3")}})
test("!empty(${OS_VARIANT:MIllumos})", // Probably not intended
- &MkCond{Not: &MkCond{Empty: NewMkVarUse("${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"})
test("${\"${PKG_OPTIONS:Moption}\":?--enable-option:--disable-option}",
- &MkCond{Term: &MkCondTerm{Var: NewMkVarUse("\"${PKG_OPTIONS:Moption}\"", "?--enable-option:--disable-option")}})
+ &MkCond{Term: &MkCondTerm{Var: varUse("\"${PKG_OPTIONS:Moption}\"", "?--enable-option:--disable-option")}})
// Contrary to most other programming languages, the == operator binds
// more tightly that the ! operator.
@@ -708,7 +701,7 @@ 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{Compare: &MkCondCompare{varUse("VAR"), "==", str("value")}}})
+ &MkCond{Not: &MkCond{Compare: &MkCondCompare{varTerm("VAR"), "==", str("value")}}})
// The left-hand side of the comparison can be a quoted string.
test("\"${VAR}suffix\" == value",
@@ -744,11 +737,11 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) {
"exists(/unfinished")
testRest("!empty(PKG_OPTIONS:Msndfile) || defined(PKG_OPTIONS:Msamplerate)",
- &MkCond{Not: &MkCond{Empty: NewMkVarUse("PKG_OPTIONS", "Msndfile")}},
+ &MkCond{Not: &MkCond{Empty: varUse("PKG_OPTIONS", "Msndfile")}},
"|| defined(PKG_OPTIONS:Msamplerate)")
testRest("${LEFT} &&",
- &MkCond{Term: &MkCondTerm{Var: NewMkVarUse("LEFT")}},
+ &MkCond{Term: &MkCondTerm{Var: varUse("LEFT")}},
"&&")
testRest("\"unfinished string literal",
@@ -785,7 +778,7 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) {
// Too many closing parentheses are a syntax error.
testRest("(${VAR}))",
- &MkCond{Term: &MkCondTerm{Var: NewMkVarUse("VAR")}},
+ &MkCond{Term: &MkCondTerm{Var: varUse("VAR")}},
")")
// The left-hand side of the comparison cannot be an unquoted string literal.
@@ -806,6 +799,7 @@ func (s *Suite) Test_MkParser_MkCond(c *check.C) {
func (s *Suite) Test_MkParser_mkCondCompare(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
mkline := t.NewMkLine("Makefile", 123, ".if ${PKGPATH} == category/pack.age-3+")
p := NewMkParser(mkline.Line, mkline.Args())
@@ -816,7 +810,7 @@ func (s *Suite) Test_MkParser_mkCondCompare(c *check.C) {
cond,
&MkCond{
Compare: &MkCondCompare{
- Left: MkCondTerm{Var: NewMkVarUse("PKGPATH")},
+ Left: MkCondTerm{Var: b.VarUse("PKGPATH")},
Op: "==",
Right: MkCondTerm{Str: "category/pack.age-3+"}}})
@@ -886,7 +880,7 @@ func (s *Suite) Test_MkParser_VarUse__parentheses_autofix(c *check.C) {
func (s *Suite) Test_MkParser_VarUseModifiers(c *check.C) {
t := s.Init(c)
- varUse := NewMkVarUse
+ varUse := NewMkTokenBuilder().VarUse
test := func(text string, varUse *MkVarUse, diagnostics ...string) {
line := t.NewLine("Makefile", 20, "\t"+text)
p := NewMkParser(line, text)
@@ -924,7 +918,7 @@ func (s *Suite) Test_MkParser_VarUseModifiers(c *check.C) {
func (s *Suite) Test_MkParser_varUseModifierSubst(c *check.C) {
t := s.Init(c)
- varUse := NewMkVarUse
+ varUse := NewMkTokenBuilder().VarUse
test := func(text string, varUse *MkVarUse, rest string, diagnostics ...string) {
line := t.NewLine("Makefile", 20, "\t"+text)
p := NewMkParser(line, text)
@@ -982,7 +976,7 @@ func (s *Suite) Test_MkParser_varUseModifierSubst(c *check.C) {
func (s *Suite) Test_MkParser_varUseModifierAt(c *check.C) {
t := s.Init(c)
- varUse := NewMkVarUse
+ varUse := NewMkTokenBuilder().VarUse
test := func(text string, varUse *MkVarUse, rest string, diagnostics ...string) {
line := t.NewLine("Makefile", 20, "\t"+text)
p := NewMkParser(line, text)
diff --git a/pkgtools/pkglint/files/mktokenslexer_test.go b/pkgtools/pkglint/files/mktokenslexer_test.go
index a185e085880..c74a4ee8674 100644
--- a/pkgtools/pkglint/files/mktokenslexer_test.go
+++ b/pkgtools/pkglint/files/mktokenslexer_test.go
@@ -16,8 +16,9 @@ func (s *Suite) Test_MkTokensLexer__empty_slice_returns_EOF(c *check.C) {
// A slice of a single token behaves like textproc.Lexer.
func (s *Suite) Test_MkTokensLexer__single_plain_text_token(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- lexer := NewMkTokensLexer([]*MkToken{{"\\# $$ [#] $V", nil}})
+ lexer := NewMkTokensLexer(b.Tokens(b.TextToken("\\# $$ [#] $V")))
t.CheckEquals(lexer.SkipByte('\\'), true)
t.CheckEquals(lexer.Rest(), "# $$ [#] $V")
@@ -36,8 +37,9 @@ func (s *Suite) Test_MkTokensLexer__single_plain_text_token(c *check.C) {
// text.
func (s *Suite) Test_MkTokensLexer__single_varuse_token(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- tokens := []*MkToken{{"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")}}
+ tokens := b.Tokens(b.VaruseToken("VAR", "Mpattern"))
lexer := NewMkTokensLexer(tokens)
t.CheckEquals(lexer.EOF(), false)
@@ -47,10 +49,11 @@ func (s *Suite) Test_MkTokensLexer__single_varuse_token(c *check.C) {
func (s *Suite) Test_MkTokensLexer__plain_then_varuse(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- tokens := []*MkToken{
- {"plain text", nil},
- {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")}}
+ tokens := b.Tokens(
+ b.TextToken("plain text"),
+ b.VaruseToken("VAR", "Mpattern"))
lexer := NewMkTokensLexer(tokens)
t.CheckEquals(lexer.NextBytesSet(textproc.Digit.Inverse()), "plain text")
@@ -60,11 +63,12 @@ func (s *Suite) Test_MkTokensLexer__plain_then_varuse(c *check.C) {
func (s *Suite) Test_MkTokensLexer__varuse_varuse_varuse(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- tokens := []*MkToken{
- {"${dirs:O:u}", NewMkVarUse("dirs", "O", "u")},
- {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
- {"${.TARGET}", NewMkVarUse(".TARGET")}}
+ tokens := b.Tokens(
+ b.VaruseToken("dirs", "O", "u"),
+ b.VaruseToken("VAR", "Mpattern"),
+ b.VaruseToken(".TARGET"))
lexer := NewMkTokensLexer(tokens)
t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
@@ -75,11 +79,12 @@ func (s *Suite) Test_MkTokensLexer__varuse_varuse_varuse(c *check.C) {
func (s *Suite) Test_MkTokensLexer__mark_reset_since_in_initial_state(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- tokens := []*MkToken{
- {"${dirs:O:u}", NewMkVarUse("dirs", "O", "u")},
- {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
- {"${.TARGET}", NewMkVarUse(".TARGET")}}
+ tokens := b.Tokens(
+ b.VaruseToken("dirs", "O", "u"),
+ b.VaruseToken("VAR", "Mpattern"),
+ b.VaruseToken(".TARGET"))
lexer := NewMkTokensLexer(tokens)
start := lexer.Mark()
@@ -94,11 +99,12 @@ func (s *Suite) Test_MkTokensLexer__mark_reset_since_in_initial_state(c *check.C
func (s *Suite) Test_MkTokensLexer__mark_reset_since_inside_plain_text(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- lexer := NewMkTokensLexer([]*MkToken{
- {"plain text", nil},
- {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
- {"rest", nil}})
+ lexer := NewMkTokensLexer(b.Tokens(
+ b.TextToken("plain text"),
+ b.VaruseToken("VAR", "Mpattern"),
+ b.TextToken("rest")))
start := lexer.Mark()
t.CheckEquals(lexer.NextBytesSet(textproc.Alpha), "plain")
@@ -112,11 +118,12 @@ func (s *Suite) Test_MkTokensLexer__mark_reset_since_inside_plain_text(c *check.
func (s *Suite) Test_MkTokensLexer__mark_reset_since_after_plain_text(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- lexer := NewMkTokensLexer([]*MkToken{
- {"plain text", nil},
- {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
- {"rest", nil}})
+ lexer := NewMkTokensLexer(b.Tokens(
+ b.TextToken("plain text"),
+ b.VaruseToken("VAR", "Mpattern"),
+ b.TextToken("rest")))
start := lexer.Mark()
t.CheckEquals(lexer.SkipString("plain text"), true)
@@ -130,10 +137,11 @@ func (s *Suite) Test_MkTokensLexer__mark_reset_since_after_plain_text(c *check.C
func (s *Suite) Test_MkTokensLexer__mark_reset_since_after_varuse(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- tokens := []*MkToken{
- {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
- {"rest", nil}}
+ tokens := b.Tokens(
+ b.VaruseToken("VAR", "Mpattern"),
+ b.TextToken("rest"))
lexer := NewMkTokensLexer(tokens)
start := lexer.Mark()
@@ -148,11 +156,12 @@ func (s *Suite) Test_MkTokensLexer__mark_reset_since_after_varuse(c *check.C) {
func (s *Suite) Test_MkTokensLexer__multiple_marks_in_same_plain_text(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- lexer := NewMkTokensLexer([]*MkToken{
- {"plain text", nil},
- {"${VAR:Mpattern}", NewMkVarUse("VAR", "Mpattern")},
- {"rest", nil}})
+ lexer := NewMkTokensLexer(b.Tokens(
+ b.TextToken("plain text"),
+ b.VaruseToken("VAR", "Mpattern"),
+ b.TextToken("rest")))
start := lexer.Mark()
t.CheckEquals(lexer.NextString("plain "), "plain ")
@@ -170,11 +179,12 @@ func (s *Suite) Test_MkTokensLexer__multiple_marks_in_same_plain_text(c *check.C
func (s *Suite) Test_MkTokensLexer__multiple_marks_in_varuse(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- tokens := []*MkToken{
- {"${VAR1}", NewMkVarUse("VAR1")},
- {"${VAR2}", NewMkVarUse("VAR2")},
- {"${VAR3}", NewMkVarUse("VAR3")}}
+ tokens := b.Tokens(
+ b.VaruseToken("VAR1"),
+ b.VaruseToken("VAR2"),
+ b.VaruseToken("VAR3"))
lexer := NewMkTokensLexer(tokens)
start := lexer.Mark()
@@ -197,16 +207,18 @@ func (s *Suite) Test_MkTokensLexer__multiple_marks_in_varuse(c *check.C) {
func (s *Suite) Test_MkTokensLexer__EOF_before_plain_text(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- lexer := NewMkTokensLexer([]*MkToken{{"rest", nil}})
+ lexer := NewMkTokensLexer(b.Tokens(b.TextToken("rest")))
t.CheckEquals(lexer.EOF(), false)
}
func (s *Suite) Test_MkTokensLexer__EOF_before_varuse(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- lexer := NewMkTokensLexer([]*MkToken{{"${VAR}", NewMkVarUse("VAR")}})
+ lexer := NewMkTokensLexer(b.Tokens(b.VaruseToken("VAR")))
t.CheckEquals(lexer.EOF(), false)
}
@@ -223,25 +235,27 @@ func (s *Suite) Test_MkTokensLexer__EOF_before_varuse(c *check.C) {
// bother to make this unnecessary copy and works on the shared slice.
func (s *Suite) Test_MkTokensLexer__constructor_uses_shared_array(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- tokens := []*MkToken{{"${VAR}", NewMkVarUse("VAR")}}
+ tokens := b.Tokens(b.VaruseToken("VAR"))
lexer := NewMkTokensLexer(tokens)
t.CheckEquals(lexer.Rest(), "${VAR}")
tokens[0].Text = "modified text"
- tokens[0].Varuse = NewMkVarUse("MODIFIED", "Mpattern")
+ tokens[0].Varuse = b.VarUse("MODIFIED", "Mpattern")
t.CheckEquals(lexer.Rest(), "modified text")
}
func (s *Suite) Test_MkTokensLexer__peek_after_varuse(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- tokens := []*MkToken{
- {"${VAR}", NewMkVarUse("VAR")},
- {"${VAR}", NewMkVarUse("VAR")},
- {"text", nil}}
+ tokens := b.Tokens(
+ b.VaruseToken("VAR"),
+ b.VaruseToken("VAR"),
+ b.TextToken("text"))
lexer := NewMkTokensLexer(tokens)
t.CheckDeepEquals(lexer.NextVarUse(), tokens[0])
@@ -253,8 +267,9 @@ func (s *Suite) Test_MkTokensLexer__peek_after_varuse(c *check.C) {
func (s *Suite) Test_MkTokensLexer__varuse_when_plain_text(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- lexer := NewMkTokensLexer([]*MkToken{{"text", nil}})
+ lexer := NewMkTokensLexer(b.Tokens(b.TextToken("text")))
t.Check(lexer.NextVarUse(), check.IsNil)
t.CheckEquals(lexer.NextString("te"), "te")
@@ -269,10 +284,11 @@ func (s *Suite) Test_MkTokensLexer__varuse_when_plain_text(c *check.C) {
// the beginning.
func (s *Suite) Test_MkTokensLexer__adjacent_plain_text(c *check.C) {
t := s.Init(c)
+ b := NewMkTokenBuilder()
- lexer := NewMkTokensLexer([]*MkToken{
- {"text1", nil},
- {"text2", nil}})
+ lexer := NewMkTokensLexer(b.Tokens(
+ b.TextToken("text1"),
+ b.TextToken("text2")))
// Returns false since the string is distributed over two separate tokens.
t.CheckEquals(lexer.SkipString("text1text2"), false)
diff --git a/pkgtools/pkglint/files/mktypes_test.go b/pkgtools/pkglint/files/mktypes_test.go
index 96576729b7f..6c18f15907a 100644
--- a/pkgtools/pkglint/files/mktypes_test.go
+++ b/pkgtools/pkglint/files/mktypes_test.go
@@ -2,9 +2,36 @@ package pkglint
import (
"gopkg.in/check.v1"
+ "strings"
)
-func NewMkVarUse(varname string, modifiers ...string) *MkVarUse {
+type MkTokenBuilder struct{}
+
+func NewMkTokenBuilder() MkTokenBuilder { return MkTokenBuilder{} }
+
+func (b MkTokenBuilder) VaruseToken(varname string, modifiers ...string) *MkToken {
+ var text strings.Builder
+ text.WriteString("${")
+ text.WriteString(varname)
+ for _, modifier := range modifiers {
+ text.WriteString(":")
+ text.WriteString(modifier)
+ }
+ text.WriteString("}")
+ return &MkToken{Text: text.String(), Varuse: b.VarUse(varname, modifiers...)}
+}
+
+func (b MkTokenBuilder) VaruseTextToken(text, varname string, modifiers ...string) *MkToken {
+ return &MkToken{Text: text, Varuse: b.VarUse(varname, modifiers...)}
+}
+
+func (MkTokenBuilder) TextToken(text string) *MkToken {
+ return &MkToken{text, nil}
+}
+
+func (MkTokenBuilder) Tokens(tokens ...*MkToken) []*MkToken { return tokens }
+
+func (MkTokenBuilder) VarUse(varname string, modifiers ...string) *MkVarUse {
var mods []MkVarUseModifier
for _, modifier := range modifiers {
mods = append(mods, MkVarUseModifier{modifier})
diff --git a/pkgtools/pkglint/files/package.go b/pkgtools/pkglint/files/package.go
index 452ef1e26b7..5f1e75dc7c3 100644
--- a/pkgtools/pkglint/files/package.go
+++ b/pkgtools/pkglint/files/package.go
@@ -777,7 +777,7 @@ func (pkg *Package) checkGnuConfigureUseLanguages() {
continue
}
- if matches(mkline.VarassignComment(), `(?-i)\b(?:c|empty|none)\b`) {
+ if matches(mkline.Comment(), `(?-i)\b(?:c|empty|none)\b`) {
// Don't emit a warning since the comment probably contains a
// statement that C is really not needed.
return
@@ -836,7 +836,7 @@ func (pkg *Package) determineEffectivePkgVars() {
}
if pkgnameLine != nil && (pkgname == distname || pkgname == "${DISTNAME}") {
- if pkgnameLine.VarassignComment() == "" {
+ if !pkgnameLine.HasComment() {
pkgnameLine.Notef("This assignment is probably redundant " +
"since PKGNAME is ${DISTNAME} by default.")
pkgnameLine.Explain(
@@ -1294,6 +1294,16 @@ func (pkg *Package) checkIncludeConditionally(mkline *MkLine, indentation *Inden
"%q is included unconditionally here "+
"and conditionally in %s (depending on %s).",
cleanpath(mkline.IncludedFile()), mkline.RefTo(other), strings.Join(other.ConditionalVars(), ", "))
+
+ if mkline.Basename == "buildlink3.mk" && containsStr(other.ConditionalVars(), "PKG_OPTIONS") {
+ mkline.Explain(
+ "When including a dependent file, the conditions in the",
+ "buildlink3.mk file should be the same as in options.mk",
+ "or the Makefile.",
+ "",
+ "To find out the PKG_OPTIONS of this package at build time,",
+ "have a look at mk/pkg-build-options.mk.")
+ }
}
}
diff --git a/pkgtools/pkglint/files/shtokenizer_test.go b/pkgtools/pkglint/files/shtokenizer_test.go
index fcaa27b95b5..024b42a4e37 100644
--- a/pkgtools/pkglint/files/shtokenizer_test.go
+++ b/pkgtools/pkglint/files/shtokenizer_test.go
@@ -47,7 +47,7 @@ func (s *Suite) Test_ShTokenizer_ShAtom(c *check.C) {
text += ":" + strings.Replace(strings.Replace(modifier, "\\", "\\\\", -1), ":", "\\:", -1)
}
text += "}"
- varuse := NewMkVarUse(varname, modifiers...)
+ varuse := NewMkTokenBuilder().VarUse(varname, modifiers...)
return &ShAtom{shtVaruse, text, shqPlain, varuse}
}
shvar := func(text, varname string) *ShAtom { return &ShAtom{shtShVarUse, text, shqPlain, varname} }
diff --git a/pkgtools/pkglint/files/substcontext.go b/pkgtools/pkglint/files/substcontext.go
index 1b243c6ed7d..d20a6a3791b 100644
--- a/pkgtools/pkglint/files/substcontext.go
+++ b/pkgtools/pkglint/files/substcontext.go
@@ -267,7 +267,7 @@ func (ctx *SubstContext) suggestSubstVars(mkline *MkLine) {
fix.Explain(
"Replacing @VAR@ with ${VAR} is such a typical pattern that pkgsrc has built-in support for it,",
"requiring only the variable name instead of the full sed command.")
- if mkline.VarassignComment() == "" && len(tokens) == 2 && tokens[0] == "-e" {
+ if !mkline.HasComment() && len(tokens) == 2 && tokens[0] == "-e" {
fix.Replace(mkline.Text, alignWith(varop, mkline.ValueAlign())+varname)
}
fix.Anyway()
diff --git a/pkgtools/pkglint/files/toplevel.go b/pkgtools/pkglint/files/toplevel.go
index bb9f26f24fd..71af8aa6c87 100644
--- a/pkgtools/pkglint/files/toplevel.go
+++ b/pkgtools/pkglint/files/toplevel.go
@@ -39,8 +39,7 @@ func (ctx *Toplevel) checkSubdir(mkline *MkLine) {
subdir := mkline.Value()
if mkline.IsCommentedVarassign() {
- comment := mkline.VarassignComment()
- if comment == "" || comment == "#" {
+ if !mkline.HasComment() || mkline.Comment() == "" {
mkline.Warnf("%q commented out without giving a reason.", subdir)
}
}
diff --git a/pkgtools/pkglint/files/util.go b/pkgtools/pkglint/files/util.go
index 37a6186f0b4..4d7a3712630 100644
--- a/pkgtools/pkglint/files/util.go
+++ b/pkgtools/pkglint/files/util.go
@@ -73,6 +73,15 @@ func replaceAllFunc(s string, re regex.Pattern, repl func(string) string) string
return G.res.Compile(re).ReplaceAllStringFunc(s, repl)
}
+func containsStr(slice []string, s string) bool {
+ for _, str := range slice {
+ if s == str {
+ return true
+ }
+ }
+ return false
+}
+
// intern returns an independent copy of the given string.
//
// It should be called when only a small substring of a large string
diff --git a/pkgtools/pkglint/files/varalignblock.go b/pkgtools/pkglint/files/varalignblock.go
index ce020b30253..78c5c2a4c0e 100644
--- a/pkgtools/pkglint/files/varalignblock.go
+++ b/pkgtools/pkglint/files/varalignblock.go
@@ -118,7 +118,7 @@ func (va *VaralignBlock) processVarassign(mkline *MkLine) {
// .include "../../mk/pkg-build-options.mk"
return
- case mkline.Value() == "" && mkline.VarassignComment() == "":
+ case mkline.Value() == "" && !mkline.HasComment():
// Multiple-inclusion guards usually appear in a block of
// their own and therefore do not need alignment.
//
diff --git a/pkgtools/pkglint/files/vargroups_test.go b/pkgtools/pkglint/files/vargroups_test.go
index f04c965e719..c0aeb894fee 100644
--- a/pkgtools/pkglint/files/vargroups_test.go
+++ b/pkgtools/pkglint/files/vargroups_test.go
@@ -222,6 +222,7 @@ func (s *Suite) Test_VargroupsChecker__ignore(c *check.C) {
"_VARGROUPS+=\t\tgroup",
"_IGN_VARS.group=\tPREFER_*",
"_IGN_VARS.group+=\t[",
+ "_IGN_VARS.group=\t.CURDIR",
"_UNDERSCORE=\t\t_", // This is not an isVargroups name.
"",
".if ${PREFER_PKGSRC:U} || ${WRKOBJDIR:U}",
@@ -231,11 +232,11 @@ func (s *Suite) Test_VargroupsChecker__ignore(c *check.C) {
t.CheckOutputLines(
"WARN: Makefile:5: \"[\" is not a valid variable name pattern.",
- "WARN: Makefile:6: Variable names starting with an underscore (_UNDERSCORE) "+
+ "WARN: Makefile:7: Variable names starting with an underscore (_UNDERSCORE) "+
"are reserved for internal pkgsrc use.",
- "WARN: Makefile:6: _UNDERSCORE is defined but not used.",
- "WARN: Makefile:6: Variable _UNDERSCORE is defined but not mentioned in the _VARGROUPS section.",
- "WARN: Makefile:8: Variable WRKOBJDIR is used but not mentioned in the _VARGROUPS section.")
+ "WARN: Makefile:7: _UNDERSCORE is defined but not used.",
+ "WARN: Makefile:7: Variable _UNDERSCORE is defined but not mentioned in the _VARGROUPS section.",
+ "WARN: Makefile:9: Variable WRKOBJDIR is used but not mentioned in the _VARGROUPS section.")
}
func (s *Suite) Test_VargroupsChecker__private_before_public(c *check.C) {
diff --git a/pkgtools/pkglint/files/vartypecheck.go b/pkgtools/pkglint/files/vartypecheck.go
index 2277473afc8..d76414007fd 100644
--- a/pkgtools/pkglint/files/vartypecheck.go
+++ b/pkgtools/pkglint/files/vartypecheck.go
@@ -614,7 +614,7 @@ func (cv *VartypeCheck) FetchURL() {
hasSuffix(fetchURL, "="),
hasSuffix(fetchURL, ":"),
hasPrefix(fetchURL, "-"),
- len(tokens) == 0 || tokens[len(tokens)-1].Varuse != nil:
+ tokens[len(tokens)-1].Varuse != nil:
break
default:
@@ -1401,7 +1401,7 @@ func (cv *VartypeCheck) VariableNamePattern() {
}
// TODO: sync with MkParser.Varname
- if matches(cv.Value, `^[*A-Z_][*0-9A-Z_]*(?:[.].*)?$`) {
+ if matches(cv.Value, `^[*A-Z_.][*0-9A-Z_]*(?:[.].*)?$`) {
return
}
diff --git a/pkgtools/pkglint/files/vartypecheck_test.go b/pkgtools/pkglint/files/vartypecheck_test.go
index a10f2b538fa..fed4d208ae8 100644
--- a/pkgtools/pkglint/files/vartypecheck_test.go
+++ b/pkgtools/pkglint/files/vartypecheck_test.go
@@ -1841,10 +1841,9 @@ func (vt *VartypeCheckTester) Values(values ...string) {
test := func(mklines *MkLines, mkline *MkLine, value string) {
varname := vt.varname
- comment := ""
+ comment := condStr(mkline.HasComment(), "#", "") + mkline.Comment()
if mkline.IsVarassign() {
- mkline.Tokenize(value, true) // Produce some warnings as side-effects.
- comment = mkline.VarassignComment()
+ _ = mkline.Tokenize(value, true) // Produce some warnings as side-effects.
}
effectiveValue := value