1/* Copyright 2018 The Bazel Authors. All rights reserved. 2 3Licensed under the Apache License, Version 2.0 (the "License"); 4you may not use this file except in compliance with the License. 5You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9Unless required by applicable law or agreed to in writing, software 10distributed under the License is distributed on an "AS IS" BASIS, 11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12See the License for the specific language governing permissions and 13limitations under the License. 14*/ 15 16// Generates the nogo binary to analyze Go source code at build time. 17 18package main 19 20import ( 21 "encoding/json" 22 "errors" 23 "flag" 24 "fmt" 25 "io/ioutil" 26 "math" 27 "os" 28 "regexp" 29 "strconv" 30 "text/template" 31) 32 33const nogoMainTpl = ` 34package main 35 36 37import ( 38{{- if .NeedRegexp }} 39 "regexp" 40{{- end}} 41{{- range $import := .Imports}} 42 {{$import.Name}} "{{$import.Path}}" 43{{- end}} 44 "golang.org/x/tools/go/analysis" 45) 46 47var analyzers = []*analysis.Analyzer{ 48{{- range $import := .Imports}} 49 {{$import.Name}}.Analyzer, 50{{- end}} 51} 52 53// configs maps analysis names to configurations. 54var configs = map[string]config{ 55{{- range $name, $config := .Configs}} 56 {{printf "%q" $name}}: config{ 57 {{- if $config.AnalyzerFlags }} 58 analyzerFlags: map[string]string { 59 {{- range $flagKey, $flagValue := $config.AnalyzerFlags}} 60 {{printf "%q: %q" $flagKey $flagValue}}, 61 {{- end}} 62 }, 63 {{- end -}} 64 {{- if $config.OnlyFiles}} 65 onlyFiles: []*regexp.Regexp{ 66 {{- range $path, $comment := $config.OnlyFiles}} 67 {{- if $comment}} 68 // {{$comment}} 69 {{end -}} 70 {{printf "regexp.MustCompile(%q)" $path}}, 71 {{- end}} 72 }, 73 {{- end -}} 74 {{- if $config.ExcludeFiles}} 75 excludeFiles: []*regexp.Regexp{ 76 {{- range $path, $comment := $config.ExcludeFiles}} 77 {{- if $comment}} 78 // {{$comment}} 79 {{end -}} 80 {{printf "regexp.MustCompile(%q)" $path}}, 81 {{- end}} 82 }, 83 {{- end}} 84 }, 85{{- end}} 86} 87` 88 89func genNogoMain(args []string) error { 90 analyzerImportPaths := multiFlag{} 91 flags := flag.NewFlagSet("generate_nogo_main", flag.ExitOnError) 92 out := flags.String("output", "", "output file to write (defaults to stdout)") 93 flags.Var(&analyzerImportPaths, "analyzer_importpath", "import path of an analyzer library") 94 configFile := flags.String("config", "", "nogo config file") 95 if err := flags.Parse(args); err != nil { 96 return err 97 } 98 if *out == "" { 99 return errors.New("must provide output file") 100 } 101 102 outFile := os.Stdout 103 var cErr error 104 outFile, err := os.Create(*out) 105 if err != nil { 106 return fmt.Errorf("os.Create(%q): %v", *out, err) 107 } 108 defer func() { 109 if err := outFile.Close(); err != nil { 110 cErr = fmt.Errorf("error closing %s: %v", outFile.Name(), err) 111 } 112 }() 113 114 config, err := buildConfig(*configFile) 115 if err != nil { 116 return err 117 } 118 119 type Import struct { 120 Path, Name string 121 } 122 // Create unique name for each imported analyzer. 123 suffix := 1 124 imports := make([]Import, 0, len(analyzerImportPaths)) 125 for _, path := range analyzerImportPaths { 126 imports = append(imports, Import{ 127 Path: path, 128 Name: "analyzer" + strconv.Itoa(suffix)}) 129 if suffix == math.MaxInt32 { 130 return fmt.Errorf("cannot generate more than %d analyzers", suffix) 131 } 132 suffix++ 133 } 134 data := struct { 135 Imports []Import 136 Configs Configs 137 NeedRegexp bool 138 }{ 139 Imports: imports, 140 Configs: config, 141 } 142 for _, c := range config { 143 if len(c.OnlyFiles) > 0 || len(c.ExcludeFiles) > 0 { 144 data.NeedRegexp = true 145 break 146 } 147 } 148 149 tpl := template.Must(template.New("source").Parse(nogoMainTpl)) 150 if err := tpl.Execute(outFile, data); err != nil { 151 return fmt.Errorf("template.Execute failed: %v", err) 152 } 153 return cErr 154} 155 156func buildConfig(path string) (Configs, error) { 157 if path == "" { 158 return Configs{}, nil 159 } 160 b, err := ioutil.ReadFile(path) 161 if err != nil { 162 return Configs{}, fmt.Errorf("failed to read config file: %v", err) 163 } 164 configs := make(Configs) 165 if err = json.Unmarshal(b, &configs); err != nil { 166 return Configs{}, fmt.Errorf("failed to unmarshal config file: %v", err) 167 } 168 for name, config := range configs { 169 for pattern := range config.OnlyFiles { 170 if _, err := regexp.Compile(pattern); err != nil { 171 return Configs{}, fmt.Errorf("invalid pattern for analysis %q: %v", name, err) 172 } 173 } 174 for pattern := range config.ExcludeFiles { 175 if _, err := regexp.Compile(pattern); err != nil { 176 return Configs{}, fmt.Errorf("invalid pattern for analysis %q: %v", name, err) 177 } 178 } 179 configs[name] = Config{ 180 // Description is currently unused. 181 OnlyFiles: config.OnlyFiles, 182 ExcludeFiles: config.ExcludeFiles, 183 AnalyzerFlags: config.AnalyzerFlags, 184 } 185 } 186 return configs, nil 187} 188 189type Configs map[string]Config 190 191type Config struct { 192 Description string 193 OnlyFiles map[string]string `json:"only_files"` 194 ExcludeFiles map[string]string `json:"exclude_files"` 195 AnalyzerFlags map[string]string `json:"analyzer_flags"` 196} 197