abuild-lint

A linting utility for Alpine Linux APKBUILDs

git clone https://git.8pit.net/abuild-lint.git

  1package main
  2
  3import (
  4	"fmt"
  5	"io"
  6	"mvdan.cc/sh/syntax"
  7	"net/mail"
  8	"strings"
  9)
 10
 11const (
 12	// Prefix used to indicate that a comment specifies the package
 13	// maintainer.
 14	maintainerPrefix = " Maintainer:"
 15
 16	// Prefix used to indicate that a comment specifies a package
 17	// contributor.
 18	contributorPrefix = " Contributor:"
 19)
 20
 21// metaPos describes the position of a metadata variable in an APKBUILD.
 22type metaPos int
 23
 24const (
 25	// Metadata variable must be declared before the first function
 26	// declaration.
 27	beforeFuncs metaPos = iota
 28
 29	// Metadata variable must be declared after the last function
 30	// declaration.
 31	afterFuncs
 32)
 33
 34// metadata represents an APKBUILD metadata variable.
 35type metadata struct {
 36	p metaPos // Position of the metadata variable
 37	r bool    // Whether the metadata variable is required
 38}
 39
 40// Array containing all variables which are directly used by
 41// abuild and should thus be declared globally.
 42var metadataVariables = map[string]metadata{
 43	"pkgname":           {beforeFuncs, true},
 44	"pkgver":            {beforeFuncs, true},
 45	"pkgrel":            {beforeFuncs, true},
 46	"pkgdesc":           {beforeFuncs, true},
 47	"url":               {beforeFuncs, true},
 48	"arch":              {beforeFuncs, true},
 49	"license":           {beforeFuncs, true},
 50	"depends":           {beforeFuncs, false},
 51	"depends_doc":       {beforeFuncs, false},
 52	"depends_dev":       {beforeFuncs, false},
 53	"makedepends":       {beforeFuncs, false},
 54	"makedepends_build": {beforeFuncs, false},
 55	"makedepends_host":  {beforeFuncs, false},
 56	"checkdepends":      {beforeFuncs, false},
 57	"install":           {beforeFuncs, false},
 58	"triggers":          {beforeFuncs, false},
 59	"subpackages":       {beforeFuncs, false},
 60	"source":            {beforeFuncs, false},
 61	"disturl":           {beforeFuncs, false},
 62	"svnurl":            {beforeFuncs, false},
 63	"giturl":            {beforeFuncs, false},
 64	"options":           {beforeFuncs, false},
 65	"sonameprefix":      {beforeFuncs, false},
 66	"somask":            {beforeFuncs, false},
 67	"replaces":          {beforeFuncs, false},
 68	"conflicts":         {beforeFuncs, false},
 69	"provides":          {beforeFuncs, false},
 70	"provider_priority": {beforeFuncs, false},
 71	"install_if":        {beforeFuncs, false},
 72	"pkgusers":          {beforeFuncs, false},
 73	"pkggroups":         {beforeFuncs, false},
 74	"langdir":           {beforeFuncs, false},
 75	"builddir":          {beforeFuncs, false},
 76	"ldpath":            {beforeFuncs, false},
 77	"patch_args":        {beforeFuncs, false},
 78	"md5sums":           {afterFuncs, false},
 79	"sha256sums":        {afterFuncs, false},
 80	"sha512sums":        {afterFuncs, false},
 81}
 82
 83// Array containing all functions which can be declared by an APKBUILD
 84// and are then called from abuild(1). The elements of the array should
 85// be sorted by invocation time.
 86var packageFunctions = []string{
 87	"sanitycheck",
 88	"snapshot",
 89	"fetch",
 90	"unpack",
 91	"prepare",
 92	"build",
 93	"check",
 94	"package",
 95}
 96
 97// addressComment represents a comment which prefixed with a certain
 98// string and contains an RFC 5322 address.
 99type addressComment struct {
100	c syntax.Comment // Comment containing the address
101	a *mail.Address  // RFC 5322 address contained in the comment
102}
103
104// Linter lints Alpine Linux APKBUILDs.
105type Linter struct {
106	v bool      // Whether a style violation was found
107	w io.Writer // Writer to use for reporting violations
108	f *APKBUILD // APKBUILD which should be checked
109}
110
111// Lint performs all linter checks and reports whether it found any
112// style violations.
113func (l *Linter) Lint() bool {
114	l.lintComments()
115	l.lintMaintainerAndContributors()
116	l.lintGlobalVariables()
117	l.lintGlobalCmdSubsts()
118	l.lintLocalVariables()
119	l.lintUnusedVariables()
120	l.lintParamExpression()
121	l.lintMetadataPlacement()
122	l.lintRequiredMetadata()
123	l.lintFunctionOrder()
124	l.lintBashisms()
125	return l.v
126}
127
128// lintComments checks that all comments start with a space. Shebangs
129// are no exception to this rule since they shouldn't appear in an
130// APKBUILD at all.
131func (l *Linter) lintComments() {
132	l.f.Walk(func(node syntax.Node) bool {
133		c, ok := node.(*syntax.Comment)
134		if ok && c.Text != "" && !strings.HasPrefix(c.Text, " ") {
135			l.error(node.Pos(), badCommentPrefix)
136		}
137
138		return true
139	})
140}
141
142// lintMaintainerAndContributors checks the APKBUILD maintainer and
143// contributor comments. It complains if there is not exactly one
144// maintainer comment, if the address specified in a maintainer or
145// contributors comment doesn't conform to RFC 5322.
146//
147// Besides it checks that contributor comments are declared before
148// maintainer comments and that contributor comments aren't declared
149// twice. Regarding the order of the comments it also checks that the
150// maintainer comment is declared before the first variable assignment.
151func (l *Linter) lintMaintainerAndContributors() {
152	var maintainer *addressComment
153	n, m := l.lintAddressComments(maintainerPrefix)
154	if n == 0 {
155		l.error(syntax.Pos{}, missingMaintainer)
156	} else if n > 1 {
157		l.error(m[len(m)-1].c.Pos(), tooManyMaintainers)
158	}
159
160	if len(m) >= 1 {
161		maintainer = &m[0]
162	}
163
164	if maintainer != nil && len(l.f.Assignments) > 0 &&
165		maintainer.c.Pos().After(l.f.Assignments[0].Pos()) {
166		l.error(maintainer.c.Pos(), maintainerAfterAssign)
167	}
168
169	addrMap := make(map[string]bool)
170	_, contributors := l.lintAddressComments(contributorPrefix)
171	for _, c := range contributors {
172		pos := c.c.Pos()
173		if maintainer != nil && pos.After(maintainer.c.Pos()) {
174			l.error(pos, wrongAddrCommentOrder)
175		}
176
177		_, ok := addrMap[c.a.String()]
178		if ok {
179			l.error(pos, repeatedAddrComment)
180		} else {
181			addrMap[c.a.String()] = true
182		}
183	}
184
185	// TODO: check for same address in contributor and maintainer?
186}
187
188// lintGlobalVariables checks that all declared globally declared
189// variables which are not prefixed with an underscore are metadata
190// variables actually picked up by abuild(1).
191func (l *Linter) lintGlobalVariables() {
192	for _, a := range l.f.Assignments {
193		v := a.Name.Value
194		if !IsMetaVar(v) && !IsPrefixVar(v) {
195			l.errorf(a.Pos(), invalidGlobalVar, v)
196			continue
197		}
198	}
199}
200
201// lintUnusedVariables checks if all globally and locally declared
202// non-metadata variable are actually used somewhere in the APKBUILD.
203func (l *Linter) lintUnusedVariables() {
204	l.f.Walk(func(node syntax.Node) bool {
205		switch x := node.(type) {
206		case *syntax.CallExpr:
207			if len(x.Args) > 0 {
208				return false // FOO=bar ./foo
209			}
210		case *syntax.Assign:
211			v := x.Name.Value
212			if !IsMetaVar(v) && l.f.IsUnusedVar(v) {
213				l.errorf(x.Pos(), variableUnused, v)
214			}
215		}
216
217		return true
218	})
219}
220
221// lintGlobalCmdSubsts check that all global shell statements don't use
222// any kind of command substitutions.
223func (l *Linter) lintGlobalCmdSubsts() {
224	l.f.Walk(func(node syntax.Node) bool {
225		switch node.(type) {
226		case *syntax.CmdSubst:
227			l.error(node.Pos(), cmdSubstInGlobalVar)
228		case *syntax.FuncDecl:
229			return false
230		}
231
232		return true
233	})
234}
235
236// lintLocalVariables checks that all variables declared inside a
237// function are declared using the local keyword.
238func (l *Linter) lintLocalVariables() {
239	vars := make(map[string][]string)
240	for n, f := range l.f.Functions {
241		fn := func(node syntax.Node) bool {
242			switch x := node.(type) {
243			case *syntax.CallExpr:
244				if len(x.Args) > 0 {
245					return false // FOO=bar ./foo
246				}
247			case *syntax.DeclClause:
248				variant := x.Variant.Value
249				if variant != "local" && variant != "export" {
250					return true
251				}
252
253				for _, a := range x.Assigns {
254					vars[n] = append(vars[n], a.Name.Value)
255				}
256			case *syntax.Assign:
257				if !l.isValidVarScope(vars[n], x.Name) {
258					l.errorf(x.Pos(), nonLocalVariable, x.Name.Value)
259				}
260			case *syntax.WordIter:
261				if !l.isValidVarScope(vars[n], x.Name) {
262					l.errorf(x.Pos(), nonLocalVariable, x.Name.Value)
263				}
264			}
265
266			return true
267		}
268
269		syntax.Walk(&f, fn)
270	}
271}
272
273// lintParamExpression checks for long parameter expansion of the form
274// ${…} and checks if they can be replaced by a semantically equivalent
275// short parameter expansion of the $… form.
276func (l *Linter) lintParamExpression() {
277	var words []*syntax.Word
278	l.f.Walk(func(node syntax.Node) bool {
279		word, ok := node.(*syntax.Word)
280		if ok {
281			words = append(words, word)
282			return false
283		}
284
285		return true
286	})
287
288	for _, word := range words {
289		nparts := len(word.Parts)
290		for n, p := range word.Parts {
291			paramExp, ok := p.(*syntax.ParamExp)
292			if !ok {
293				continue
294			} else if paramExp.Short {
295				continue
296			} else if IsParamExp(paramExp) {
297				continue
298			}
299
300			if n < nparts-1 {
301				next := word.Parts[n+1]
302				lit, ok := next.(*syntax.Lit)
303				if !ok || IsNamePart(lit.Value) {
304					continue
305				}
306			}
307
308			l.errorf(paramExp.Pos(), trivialLongParamExp,
309				paramExp.Param.Value, paramExp.Param.Value)
310		}
311	}
312
313}
314
315// lintMetadataPlacement checks the placement of metadata variables.
316// Some need to be declared before the first function declaration,
317// others need to be declared after the last function declaration.
318func (l *Linter) lintMetadataPlacement() {
319	var firstFn, lastFn *syntax.FuncDecl
320	for _, fn := range l.f.Functions {
321		if firstFn == nil || !fn.Pos().After(firstFn.Pos()) {
322			firstFn = &fn
323		}
324		if lastFn == nil || fn.Pos().After(lastFn.Pos()) {
325			lastFn = &fn
326		}
327	}
328
329	for _, v := range l.f.Assignments {
330		name := v.Name.Value
331		mpos, ok := metadataVariables[name]
332		if !ok {
333			continue
334		}
335
336		vpos := v.Pos()
337		switch mpos.p {
338		case beforeFuncs:
339			if firstFn != nil && vpos.After(firstFn.Pos()) {
340				l.errorf(vpos, metadataBeforeFunc, name)
341			}
342		case afterFuncs:
343			if lastFn != nil && !vpos.After(lastFn.Pos()) {
344				l.errorf(vpos, metadataAfterFunc, name)
345			}
346		}
347	}
348}
349
350// lintRequiredMetadata checks that all required metadata variables are
351// defined in the APKBUILD.
352func (l *Linter) lintRequiredMetadata() {
353	for n, m := range metadataVariables {
354		if !m.r {
355			continue
356		}
357
358		if !l.f.IsGlobalVar(n) {
359			l.errorf(syntax.Pos{}, missingMetadata, n)
360		}
361	}
362}
363
364// lintFunctionOrder checks that all package functions are declared in
365// the order they are called by abuild(1).
366func (l *Linter) lintFunctionOrder() {
367	var seen []*syntax.FuncDecl
368	for _, fn := range packageFunctions {
369		decl, ok := l.f.Functions[fn]
370		if !ok {
371			continue
372		}
373
374		for _, s := range seen {
375			if !decl.Pos().After(s.Pos()) {
376				l.errorf(decl.Pos(), wrongFuncOrder,
377					decl.Name.Value, s.Name.Value)
378			}
379		}
380		seen = append(seen, &decl)
381	}
382
383	// TODO: check subpackage functions
384}
385
386// lintBashisms checks for bash language features that are not allowed
387// to be used in an APKBUILD.
388func (l *Linter) lintBashisms() {
389	l.f.Walk(func(n syntax.Node) bool {
390		switch x := n.(type) {
391		case *syntax.TestClause:
392			l.errorf(x.Pos(), forbiddenBashism, "test clause")
393		case *syntax.ExtGlob:
394			l.errorf(x.Pos(), forbiddenBashism, "extended globbing expression")
395		case *syntax.ProcSubst:
396			l.errorf(x.Pos(), forbiddenBashism, "process substitution")
397		case *syntax.LetClause:
398			l.errorf(x.Pos(), forbiddenBashism, "let clause")
399		case *syntax.DeclClause:
400			v := x.Variant.Value
401			if v != "local" && v != "export" {
402				l.errorf(x.Variant.Pos(), forbiddenBashism, v)
403			}
404		case *syntax.ParamExp:
405			if x.Excl || x.Length || x.Width || x.Index != nil {
406				l.errorf(x.Pos(), forbiddenBashism, "advanced parameter expression")
407			}
408		case *syntax.ForClause:
409			if x.Select {
410				l.errorf(x.Pos(), forbiddenBashism, "select clause")
411			}
412		case *syntax.FuncDecl:
413			if x.RsrvWord {
414				l.errorf(x.Pos(), forbiddenBashism, "non-POSIX function declaration")
415			}
416		}
417
418		return true
419	})
420}
421
422// lintAddressComments checks all global comments which start with given
423// prefix followed by an ascii space character and makes sure that they
424// contain a valid RFC 5322 mail address. It returns the amount of
425// comment that started with the given prefix.
426func (l *Linter) lintAddressComments(prefix string) (int, []addressComment) {
427	var amount int
428	var comments []addressComment
429
430	for _, c := range l.f.Comments {
431		if !strings.HasPrefix(c.Text, prefix) {
432			continue
433		}
434
435		amount++
436		if len(strings.TrimFunc(c.Text, IsSpace)) ==
437			len(strings.TrimFunc(prefix, IsSpace)) {
438			l.error(c.Pos(), missingAddress)
439			continue
440		}
441
442		idx := len(prefix)
443		if c.Text[idx] != ' ' {
444			l.error(c.Pos(), noAddressSeparator)
445			continue
446		}
447
448		a, err := mail.ParseAddress(c.Text[idx+1:])
449		if err != nil {
450			l.error(c.Pos(), invalidAddress)
451			continue
452		}
453
454		comments = append(comments, addressComment{c, a})
455	}
456
457	return amount, comments
458}
459
460// isValidVarScope reports whether the given literal is included in the
461// given string slice or if it is a global or metadata variable.
462func (l *Linter) isValidVarScope(vars []string, v *syntax.Lit) bool {
463	if IsIncluded(vars, v.Value) {
464		return true
465	}
466
467	if !(l.f.IsGlobalVar(v.Value) || IsMetaVar(v.Value)) {
468		return false
469	}
470
471	return true
472}
473
474// errof formats a style violation at the given position according to
475// format and writes it to the writer associated with the linter.
476func (l *Linter) errorf(pos syntax.Pos, format string,
477	argv ...interface{}) {
478	l.v = true // Linter found a style violation
479
480	prefix := l.f.Name()
481	if pos.IsValid() {
482		prefix += ":" + pos.String()
483	}
484
485	fmt.Fprintf(l.w, "%s: %s\n", prefix,
486		fmt.Sprintf(format, argv...))
487}
488
489// error formats a style violation at the given position using the
490// default formats and writes it to the writer associated with the
491// linter.
492func (l *Linter) error(pos syntax.Pos, str string) {
493	l.errorf(pos, "%s", str)
494}