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 true100 }101102 for _, a := range x.Assigns {103 if a.Name.Value == varname {104 ret = false105 return false106 }107 }108 case *syntax.SglQuoted:109 if x.Dollar && x.Value == varname {110 ret = false111 return false112 }113 case *syntax.ParamExp:114 if x.Param.Value == varname {115 ret = false116 return false117 }118 }119120 return true121 })122123 return ret124}