abuild-lint

A linting utility for Alpine Linux APKBUILDs

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

  1package main
  2
  3import (
  4	"io"
  5	"mvdan.cc/sh/syntax"
  6)
  7
  8const (
  9	// Shell variant to use. Even though APKBUILDs are mostly
 10	// written in POSIX shell we still use LangBash here to check
 11	// for the `local` variable declaration keyword and other
 12	// bashims permitted in APKBUILDs.
 13	lang = syntax.LangBash
 14)
 15
 16// APKBUILD represents an Alpine Linux APKBUILD.
 17type APKBUILD struct {
 18	// Root node of the AST.
 19	prog *syntax.File
 20
 21	// Globally declared comments.
 22	Comments []syntax.Comment
 23
 24	// Global variable assignments excluding environment variables.
 25	Assignments []syntax.Assign
 26
 27	// Declared functions.
 28	Functions map[string]syntax.FuncDecl
 29}
 30
 31// Parse reads and parses an Alpine Linux APKBUILD. The name will be
 32// used in error messages emitted for this APKBUILD.
 33func Parse(r io.Reader, name string) (*APKBUILD, error) {
 34	parser := syntax.NewParser(syntax.KeepComments,
 35		syntax.Variant(lang))
 36
 37	prog, err := parser.Parse(r, name)
 38	if err != nil {
 39		return nil, err
 40	}
 41
 42	apkbuild := APKBUILD{prog: prog}
 43	apkbuild.Functions = make(map[string]syntax.FuncDecl)
 44	apkbuild.Walk(apkbuild.visit)
 45
 46	return &apkbuild, nil
 47}
 48
 49// Name returns the name supplied to the parse function.
 50func (a *APKBUILD) Name() string {
 51	return a.prog.Name
 52}
 53
 54// Walk traverses the underlying AST of the APKBUILD in depth-first
 55// order. It's just a wrapper function around syntax.Walk.
 56func (a *APKBUILD) Walk(f func(syntax.Node) bool) {
 57	syntax.Walk(a.prog, f)
 58}
 59
 60func (a *APKBUILD) visit(node syntax.Node) bool {
 61	switch x := node.(type) {
 62	case *syntax.DeclClause:
 63		return x.Variant.Value != "export"
 64	case *syntax.FuncDecl:
 65		a.Functions[x.Name.Value] = *x
 66		return false // All nodes after this have local scope
 67	case *syntax.Assign:
 68		a.Assignments = append(a.Assignments, *x)
 69		return true
 70	case *syntax.Comment:
 71		a.Comments = append(a.Comments, *x)
 72		return true
 73	default:
 74		return true
 75	}
 76}
 77
 78// IsGlobalVar checks if the supplied name responds to a global
 79// variable declaration.
 80func (a *APKBUILD) IsGlobalVar(varname string) bool {
 81	for _, assignment := range a.Assignments {
 82		if assignment.Name.Value == varname {
 83			return true
 84		}
 85	}
 86
 87	return false
 88}
 89
 90// IsUnusedVar checks if the variable with the supplied name is unused
 91// in the APKBUILD. It also returns true if the given variable is an
 92// environment variable.
 93func (a *APKBUILD) IsUnusedVar(varname string) bool {
 94	ret := true
 95	a.Walk(func(node syntax.Node) bool {
 96		switch x := node.(type) {
 97		case *syntax.DeclClause:
 98			if x.Variant.Value != "export" {
 99				return true
100			}
101
102			for _, a := range x.Assigns {
103				if a.Name.Value == varname {
104					ret = false
105					return false
106				}
107			}
108		case *syntax.SglQuoted:
109			if x.Dollar && x.Value == varname {
110				ret = false
111				return false
112			}
113		case *syntax.ParamExp:
114			if x.Param.Value == varname {
115				ret = false
116				return false
117			}
118		}
119
120		return true
121	})
122
123	return ret
124}