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 address101 a *mail.Address // RFC 5322 address contained in the comment102}103104// Linter lints Alpine Linux APKBUILDs.105type Linter struct {106 v bool // Whether a style violation was found107 w io.Writer // Writer to use for reporting violations108 f *APKBUILD // APKBUILD which should be checked109}110111// Lint performs all linter checks and reports whether it found any112// 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.v126}127128// lintComments checks that all comments start with a space. Shebangs129// are no exception to this rule since they shouldn't appear in an130// 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 }137138 return true139 })140}141142// lintMaintainerAndContributors checks the APKBUILD maintainer and143// contributor comments. It complains if there is not exactly one144// maintainer comment, if the address specified in a maintainer or145// contributors comment doesn't conform to RFC 5322.146//147// Besides it checks that contributor comments are declared before148// maintainer comments and that contributor comments aren't declared149// twice. Regarding the order of the comments it also checks that the150// maintainer comment is declared before the first variable assignment.151func (l *Linter) lintMaintainerAndContributors() {152 var maintainer *addressComment153 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 }159160 if len(m) >= 1 {161 maintainer = &m[0]162 }163164 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 }168169 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 }176177 _, ok := addrMap[c.a.String()]178 if ok {179 l.error(pos, repeatedAddrComment)180 } else {181 addrMap[c.a.String()] = true182 }183 }184185 // TODO: check for same address in contributor and maintainer?186}187188// lintGlobalVariables checks that all declared globally declared189// variables which are not prefixed with an underscore are metadata190// variables actually picked up by abuild(1).191func (l *Linter) lintGlobalVariables() {192 for _, a := range l.f.Assignments {193 v := a.Name.Value194 if !IsMetaVar(v) && !IsPrefixVar(v) {195 l.errorf(a.Pos(), invalidGlobalVar, v)196 continue197 }198 }199}200201// lintUnusedVariables checks if all globally and locally declared202// 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 ./foo209 }210 case *syntax.Assign:211 v := x.Name.Value212 if !IsMetaVar(v) && l.f.IsUnusedVar(v) {213 l.errorf(x.Pos(), variableUnused, v)214 }215 }216217 return true218 })219}220221// lintGlobalCmdSubsts check that all global shell statements don't use222// 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 false230 }231232 return true233 })234}235236// lintLocalVariables checks that all variables declared inside a237// 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 ./foo246 }247 case *syntax.DeclClause:248 variant := x.Variant.Value249 if variant != "local" && variant != "export" {250 return true251 }252253 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 }265266 return true267 }268269 syntax.Walk(&f, fn)270 }271}272273// lintParamExpression checks for long parameter expansion of the form274// ${…} and checks if they can be replaced by a semantically equivalent275// short parameter expansion of the $… form.276func (l *Linter) lintParamExpression() {277 var words []*syntax.Word278 l.f.Walk(func(node syntax.Node) bool {279 word, ok := node.(*syntax.Word)280 if ok {281 words = append(words, word)282 return false283 }284285 return true286 })287288 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 continue294 } else if paramExp.Short {295 continue296 } else if IsParamExp(paramExp) {297 continue298 }299300 if n < nparts-1 {301 next := word.Parts[n+1]302 lit, ok := next.(*syntax.Lit)303 if !ok || IsNamePart(lit.Value) {304 continue305 }306 }307308 l.errorf(paramExp.Pos(), trivialLongParamExp,309 paramExp.Param.Value, paramExp.Param.Value)310 }311 }312313}314315// 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.FuncDecl320 for _, fn := range l.f.Functions {321 if firstFn == nil || !fn.Pos().After(firstFn.Pos()) {322 firstFn = &fn323 }324 if lastFn == nil || fn.Pos().After(lastFn.Pos()) {325 lastFn = &fn326 }327 }328329 for _, v := range l.f.Assignments {330 name := v.Name.Value331 mpos, ok := metadataVariables[name]332 if !ok {333 continue334 }335336 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}349350// lintRequiredMetadata checks that all required metadata variables are351// defined in the APKBUILD.352func (l *Linter) lintRequiredMetadata() {353 for n, m := range metadataVariables {354 if !m.r {355 continue356 }357358 if !l.f.IsGlobalVar(n) {359 l.errorf(syntax.Pos{}, missingMetadata, n)360 }361 }362}363364// lintFunctionOrder checks that all package functions are declared in365// the order they are called by abuild(1).366func (l *Linter) lintFunctionOrder() {367 var seen []*syntax.FuncDecl368 for _, fn := range packageFunctions {369 decl, ok := l.f.Functions[fn]370 if !ok {371 continue372 }373374 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 }382383 // TODO: check subpackage functions384}385386// lintBashisms checks for bash language features that are not allowed387// 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.Value401 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 }417418 return true419 })420}421422// lintAddressComments checks all global comments which start with given423// prefix followed by an ascii space character and makes sure that they424// contain a valid RFC 5322 mail address. It returns the amount of425// comment that started with the given prefix.426func (l *Linter) lintAddressComments(prefix string) (int, []addressComment) {427 var amount int428 var comments []addressComment429430 for _, c := range l.f.Comments {431 if !strings.HasPrefix(c.Text, prefix) {432 continue433 }434435 amount++436 if len(strings.TrimFunc(c.Text, IsSpace)) ==437 len(strings.TrimFunc(prefix, IsSpace)) {438 l.error(c.Pos(), missingAddress)439 continue440 }441442 idx := len(prefix)443 if c.Text[idx] != ' ' {444 l.error(c.Pos(), noAddressSeparator)445 continue446 }447448 a, err := mail.ParseAddress(c.Text[idx+1:])449 if err != nil {450 l.error(c.Pos(), invalidAddress)451 continue452 }453454 comments = append(comments, addressComment{c, a})455 }456457 return amount, comments458}459460// isValidVarScope reports whether the given literal is included in the461// 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 true465 }466467 if !(l.f.IsGlobalVar(v.Value) || IsMetaVar(v.Value)) {468 return false469 }470471 return true472}473474// errof formats a style violation at the given position according to475// 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 violation479480 prefix := l.f.Name()481 if pos.IsValid() {482 prefix += ":" + pos.String()483 }484485 fmt.Fprintf(l.w, "%s: %s\n", prefix,486 fmt.Sprintf(format, argv...))487}488489// error formats a style violation at the given position using the490// default formats and writes it to the writer associated with the491// linter.492func (l *Linter) error(pos syntax.Pos, str string) {493 l.errorf(pos, "%s", str)494}