├── LICENSE ├── README.md ├── tmplwalk └── walk.go └── lint.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Sourcegraph, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-template-lint 2 | 3 | `go-template-lint` is a linter for Go 4 | [text/template](http://golang.org/pkg/text/template/) (and 5 | html/template) template files. 6 | 7 | 8 | ## Checks 9 | 10 | * unused template functions (e.g., your `FuncMap` defines `f` but none 11 | of your templates call it) 12 | 13 | 14 | ## Usage 15 | 16 | ``` 17 | go get sourcegraph.com/sourcegraph/go-template-lint 18 | go-template-lint -f= -t= -td= 19 | ``` 20 | 21 | The `file-with-FuncMap.go` option should be a Go source file that 22 | contains a `FuncMap` literal, such as: 23 | 24 | ``` 25 | package foo 26 | 27 | import "text/template" // html/template and/or other import aliases are also detected 28 | 29 | // ... 30 | // can be nested in any block 31 | template.FuncMap{ 32 | "f": myFunc, 33 | "g": func(v string) string { /* ... */ }, 34 | } 35 | // ... 36 | ``` 37 | 38 | The `file-with-template-[][]string-list.go` option should be a Go 39 | source file that contains a list of top-level templates and other 40 | template files (relative to the Go file) to include, such as: 41 | 42 | ``` 43 | package foo 44 | 45 | // ... 46 | // can be nested in any block 47 | [][]string{ 48 | {"profile.html", "common.html", "layout.html"}, 49 | {"edit.html", "common.html", "layout.html"}, 50 | } 51 | // ... 52 | ``` 53 | 54 | The `base-template-dir` should be the directory that contains your Go 55 | templates and that the template filenames in your code are relative 56 | to. For example, if the template files above (profile,html, 57 | common.html, etc.) were stored in `app/templates`, we'd use 58 | `-td=app/templates`. 59 | -------------------------------------------------------------------------------- /tmplwalk/walk.go: -------------------------------------------------------------------------------- 1 | package tmplwalk 2 | 3 | import "text/template/parse" 4 | 5 | // A Visitor's Visit method is invoked for each node encountered by Walk. 6 | // If the result visitor w is not nil, Walk visits each of the children 7 | // of node with the visitor w, followed by a call of w.Visit(nil). 8 | type Visitor interface { 9 | Visit(node parse.Node) (w Visitor) 10 | } 11 | 12 | // Helper functions for common node types. 13 | 14 | func walkBranchNode(v Visitor, n *parse.BranchNode) { 15 | Walk(v, n.Pipe) 16 | Walk(v, n.List) 17 | if n.ElseList != nil { 18 | Walk(v, n.ElseList) 19 | } 20 | } 21 | 22 | // Walk traverses a template parse tree in depth-first order: It 23 | // starts by calling v.Visit(node); node must not be nil. If the 24 | // visitor w returned by v.Visit(node) is not nil, Walk is invoked 25 | // recursively with visitor w for each of the non-nil children of 26 | // node, followed by a call of w.Visit(nil). 27 | func Walk(v Visitor, node parse.Node) { 28 | if v = v.Visit(node); v == nil { 29 | return 30 | } 31 | 32 | // walk children 33 | switch n := node.(type) { 34 | 35 | case *parse.ActionNode: 36 | Walk(v, n.Pipe) 37 | 38 | case *parse.BoolNode: 39 | // nothing to do 40 | 41 | case *parse.ChainNode: 42 | Walk(v, n.Node) 43 | 44 | case *parse.CommandNode: 45 | for _, a := range n.Args { 46 | Walk(v, a) 47 | } 48 | 49 | case *parse.DotNode: 50 | // nothing to do 51 | 52 | case *parse.FieldNode: 53 | // nothing to do 54 | 55 | case *parse.IdentifierNode: 56 | // nothing to do 57 | 58 | case *parse.IfNode: 59 | walkBranchNode(v, &n.BranchNode) 60 | 61 | case *parse.ListNode: 62 | for _, n := range n.Nodes { 63 | Walk(v, n) 64 | } 65 | 66 | case *parse.NilNode: 67 | // nothing to do 68 | 69 | case *parse.NumberNode: 70 | // nothing to do 71 | 72 | case *parse.PipeNode: 73 | for _, d := range n.Decl { 74 | Walk(v, d) 75 | } 76 | for _, c := range n.Cmds { 77 | Walk(v, c) 78 | } 79 | 80 | case *parse.RangeNode: 81 | walkBranchNode(v, &n.BranchNode) 82 | 83 | case *parse.StringNode: 84 | // nothing to do 85 | 86 | case *parse.TemplateNode: 87 | if n.Pipe != nil { 88 | Walk(v, n.Pipe) 89 | } 90 | 91 | case *parse.TextNode: 92 | // nothing to do 93 | 94 | case *parse.VariableNode: 95 | // nothing to do 96 | 97 | case *parse.WithNode: 98 | walkBranchNode(v, &n.BranchNode) 99 | 100 | } 101 | 102 | v.Visit(nil) 103 | } 104 | 105 | type inspector func(parse.Node) bool 106 | 107 | func (f inspector) Visit(node parse.Node) Visitor { 108 | if f(node) { 109 | return f 110 | } 111 | return nil 112 | } 113 | 114 | // Inspect traverses a template parse tree in depth-first order: It 115 | // starts by calling f(node); node must not be nil. If f returns true, 116 | // Inspect invokes f for all the non-nil children of node, 117 | // recursively. 118 | func Inspect(node parse.Node, f func(parse.Node) bool) { 119 | Walk(inspector(f), node) 120 | } 121 | -------------------------------------------------------------------------------- /lint.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | "text/template" 16 | "text/template/parse" 17 | 18 | "sourcegraph.com/sourcegraph/go-template-lint/tmplwalk" 19 | ) 20 | 21 | var ( 22 | verbose = flag.Bool("v", false, "show verbose output") 23 | funcmapFile = flag.String("f", "", "Go source file with FuncMap literal (commas separate multiple files)") 24 | tmplSetsFile = flag.String("t", "", "Go source file containing template set (a [][]string literal)") 25 | tmplDir = flag.String("td", "", "base path of templates (prepended to template set filenames)") 26 | 27 | fset = token.NewFileSet() 28 | ) 29 | 30 | func main() { 31 | log.SetFlags(0) 32 | 33 | flag.Parse() 34 | if *funcmapFile == "" { 35 | log.Fatal("-f is required (run with -h for usage info)") 36 | } 37 | if *tmplSetsFile == "" { 38 | log.Fatal("-t is required (run with -h for usage info)") 39 | } 40 | 41 | var allDefinedFuncs []string 42 | for _, f := range strings.Split(*funcmapFile, ",") { 43 | definedFuncs, err := parseFuncDefs(f) 44 | if err != nil { 45 | log.Fatalf("Error parsing FuncMap names from %s: %s", f, err) 46 | } 47 | if len(definedFuncs) == 0 { 48 | log.Fatalf("No func definitions in a FuncMap found in %s.", f) 49 | } 50 | allDefinedFuncs = append(allDefinedFuncs, definedFuncs...) 51 | } 52 | if *verbose { 53 | log.Printf("# Found %d template functions:", len(allDefinedFuncs)) 54 | for _, name := range allDefinedFuncs { 55 | log.Printf("# - %s", name) 56 | } 57 | } 58 | 59 | tmplSets, err := parseTmplSet(*tmplSetsFile) 60 | if err != nil { 61 | log.Fatalf("Error parsing template sets (a [][]string literal) from %s: %s", *tmplSetsFile, err) 62 | } 63 | if len(tmplSets) == 0 { 64 | log.Fatal("No template files found.") 65 | } 66 | if *verbose { 67 | log.Printf("# Found %d template file sets:", len(tmplSets)) 68 | for _, ts := range tmplSets { 69 | log.Printf("# - %s", strings.Join(ts, " ")) 70 | } 71 | } 72 | 73 | invokedFuncs, err := findInvokedFuncs(tmplSets, allDefinedFuncs) 74 | if err != nil { 75 | log.Fatalf("Error parsing templates to find invoked template functions: %s", err) 76 | } 77 | if len(invokedFuncs) == 0 { 78 | log.Fatal("No func invocations in templates found.") 79 | } 80 | if *verbose { 81 | log.Printf("# Found %d functions invoked in templates:", len(invokedFuncs)) 82 | for _, name := range invokedFuncs { 83 | log.Printf("# - %s", name) 84 | } 85 | } 86 | 87 | definedFuncMap := sliceToMap(allDefinedFuncs) 88 | invokedFuncMap := sliceToMap(invokedFuncs) 89 | fail := false 90 | 91 | unusedFuncs := subtract(definedFuncMap, invokedFuncMap) 92 | for _, f := range unusedFuncs { 93 | fmt.Println("unused template func", f) 94 | fail = true 95 | } 96 | 97 | addPredefs(definedFuncMap) 98 | undefinedFuncs := subtract(invokedFuncMap, definedFuncMap) 99 | for _, f := range undefinedFuncs { 100 | fmt.Println("undefined template func", f) 101 | fail = true 102 | } 103 | 104 | if fail { 105 | os.Exit(1) 106 | } 107 | } 108 | 109 | // parseFuncDefs extracts and returns function names from a Go source 110 | // file containing a FuncMap literal. 111 | func parseFuncDefs(filename string) ([]string, error) { 112 | f, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | var funcNames []string 118 | ast.Walk(visitFn(func(n ast.Node) bool { 119 | switch n := n.(type) { 120 | case *ast.CompositeLit: 121 | if isFuncMap(n.Type) { 122 | for _, e := range n.Elts { 123 | kv := e.(*ast.KeyValueExpr) 124 | name, err := strconv.Unquote(kv.Key.(*ast.BasicLit).Value) 125 | if err != nil { 126 | log.Fatal(err) 127 | } 128 | funcNames = append(funcNames, name) 129 | } 130 | } 131 | } 132 | return true 133 | }), f) 134 | return funcNames, nil 135 | } 136 | 137 | func isFuncMap(x ast.Expr) bool { 138 | switch x := x.(type) { 139 | case *ast.Ident: 140 | return x.Name == "FuncMap" 141 | case *ast.SelectorExpr: 142 | return isFuncMap(x.Sel) 143 | } 144 | return false 145 | } 146 | 147 | func parseTmplSet(filename string) ([][]string, error) { 148 | f, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | var tmplSets [][]string 154 | ast.Walk(visitFn(func(n ast.Node) bool { 155 | switch n := n.(type) { 156 | case *ast.CompositeLit: 157 | switch { 158 | case isTmplSet(n.Type): 159 | for _, e := range n.Elts { 160 | tmplSets = append(tmplSets, astStringSlice(e.(*ast.CompositeLit))) 161 | } 162 | case isLayoutSet(n.Type): 163 | tmplSets = append(tmplSets, astStringSlice(n)) 164 | } 165 | } 166 | return true 167 | }), f) 168 | return tmplSets, nil 169 | } 170 | 171 | func isTmplSet(x ast.Expr) bool { 172 | if sx, ok := x.(*ast.ArrayType); ok && sx.Len == nil { 173 | if sx2, ok := sx.Elt.(*ast.ArrayType); ok && sx2.Len == nil { 174 | if t, ok := sx2.Elt.(*ast.Ident); ok && t.Name == "string" { 175 | return true 176 | } 177 | } 178 | } 179 | return false 180 | } 181 | 182 | func isLayoutSet(x ast.Expr) bool { 183 | if sx, ok := x.(*ast.ArrayType); ok && sx.Len == nil { 184 | if t, ok := sx.Elt.(*ast.Ident); ok && t.Name == "string" { 185 | return true 186 | } 187 | } 188 | return false 189 | } 190 | 191 | func astStringSlice(cl *ast.CompositeLit) []string { 192 | var ss []string 193 | for _, e := range cl.Elts { 194 | s, err := strconv.Unquote(e.(*ast.BasicLit).Value) 195 | if err != nil { 196 | log.Fatal(err) 197 | } 198 | ss = append(ss, s) 199 | } 200 | return ss 201 | } 202 | 203 | // visitFn is a wrapper for traversing nodes in the AST 204 | type visitFn func(node ast.Node) (descend bool) 205 | 206 | func (v visitFn) Visit(node ast.Node) ast.Visitor { 207 | descend := v(node) 208 | if descend { 209 | return v 210 | } 211 | return nil 212 | } 213 | 214 | // findInvokedFuncs returns a list of all functions (including 215 | // predefined functions) invoked in Go templates in templateDir 216 | // (recursively). 217 | func findInvokedFuncs(tmplSets [][]string, definedFuncs []string) ([]string, error) { 218 | definedFuncMap := template.FuncMap{} 219 | for _, f := range definedFuncs { 220 | definedFuncMap[f] = func() interface{} { return nil } 221 | } 222 | 223 | invoked := map[string]struct{}{} 224 | for _, tmplSet := range tmplSets { 225 | tt := template.New("") 226 | tt.Funcs(definedFuncMap) 227 | _, err := tt.ParseFiles(joinTemplateDir(*tmplDir, tmplSet)...) 228 | if err != nil { 229 | return nil, fmt.Errorf("template set %v: %s", tmplSet, err) 230 | } 231 | for _, t := range tt.Templates() { 232 | if t.Tree == nil { 233 | log.Printf("No template root for %v", t.Name()) 234 | continue 235 | } 236 | tmplwalk.Inspect(t.Tree.Root, func(n parse.Node) bool { 237 | switch n := n.(type) { 238 | case *parse.IdentifierNode: 239 | invoked[n.Ident] = struct{}{} 240 | } 241 | return true 242 | }) 243 | } 244 | } 245 | 246 | var invokedList []string 247 | for f := range invoked { 248 | invokedList = append(invokedList, f) 249 | } 250 | sort.Strings(invokedList) 251 | return invokedList, nil 252 | } 253 | 254 | func joinTemplateDir(base string, files []string) []string { 255 | result := make([]string, len(files)) 256 | for i := range files { 257 | result[i] = filepath.Join(base, files[i]) 258 | } 259 | return result 260 | } 261 | 262 | var predefFuncs = []string{"and", "call", "html", "index", "js", "len", "not", "or", "print", "printf", "println", "urlquery", "eq", "ne", "lt", "le", "gt", "ge"} 263 | 264 | // addPredefs adds predefined template functions to fm. 265 | func addPredefs(fm map[string]struct{}) { 266 | for _, f := range predefFuncs { 267 | fm[f] = struct{}{} 268 | } 269 | } 270 | 271 | // subtract returns a list of keys in a that are not in b. 272 | func subtract(a, b map[string]struct{}) []string { 273 | var d []string 274 | for k := range a { 275 | if _, inB := b[k]; !inB { 276 | d = append(d, k) 277 | } 278 | } 279 | sort.Strings(d) 280 | return d 281 | } 282 | 283 | func sliceToMap(ss []string) map[string]struct{} { 284 | m := make(map[string]struct{}, len(ss)) 285 | for _, s := range ss { 286 | m[s] = struct{}{} 287 | } 288 | return m 289 | } 290 | --------------------------------------------------------------------------------