github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/scripts/expand_website_templates/main.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "encoding/hex" 7 "encoding/json" 8 "flag" 9 "fmt" 10 "io/ioutil" 11 "log" 12 "net/http" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "sort" 17 "strings" 18 19 "gopkg.in/yaml.v3" 20 21 "github.com/golangci/golangci-lint/internal/renameio" 22 "github.com/golangci/golangci-lint/pkg/lint/linter" 23 "github.com/golangci/golangci-lint/pkg/lint/lintersdb" 24 ) 25 26 var stateFilePath = filepath.Join("docs", "template_data.state") 27 28 func main() { 29 var onlyWriteState bool 30 flag.BoolVar(&onlyWriteState, "only-state", false, fmt.Sprintf("Only write hash of state to %s and exit", stateFilePath)) 31 flag.Parse() 32 33 replacements, err := buildTemplateContext() 34 if err != nil { 35 log.Fatalf("Failed to build template context: %s", err) 36 } 37 38 if err = updateStateFile(replacements); err != nil { 39 log.Fatalf("Failed to update state file: %s", err) 40 } 41 42 if onlyWriteState { 43 return 44 } 45 46 if err := rewriteDocs(replacements); err != nil { 47 log.Fatalf("Failed to rewrite docs: %s", err) 48 } 49 log.Printf("Successfully expanded templates") 50 } 51 52 func updateStateFile(replacements map[string]string) error { 53 replBytes, err := json.Marshal(replacements) 54 if err != nil { 55 return fmt.Errorf("failed to json marshal replacements: %w", err) 56 } 57 58 h := sha256.New() 59 if _, err := h.Write(replBytes); err != nil { 60 return err 61 } 62 63 var contentBuf bytes.Buffer 64 contentBuf.WriteString("This file stores hash of website templates to trigger " + 65 "Netlify rebuild when something changes, e.g. new linter is added.\n") 66 contentBuf.WriteString(hex.EncodeToString(h.Sum(nil))) 67 68 return renameio.WriteFile(stateFilePath, contentBuf.Bytes(), os.ModePerm) 69 } 70 71 func rewriteDocs(replacements map[string]string) error { 72 madeReplacements := map[string]bool{} 73 err := filepath.Walk(filepath.Join("docs", "src", "docs"), 74 func(path string, info os.FileInfo, err error) error { 75 if err != nil { 76 return err 77 } 78 if info.IsDir() { 79 return nil 80 } 81 return processDoc(path, replacements, madeReplacements) 82 }) 83 if err != nil { 84 return fmt.Errorf("failed to walk dir: %w", err) 85 } 86 87 if len(madeReplacements) != len(replacements) { 88 for key := range replacements { 89 if !madeReplacements[key] { 90 log.Printf("Replacement %q wasn't performed", key) 91 } 92 } 93 return fmt.Errorf("%d replacements weren't performed", len(replacements)-len(madeReplacements)) 94 } 95 return nil 96 } 97 98 func processDoc(path string, replacements map[string]string, madeReplacements map[string]bool) error { 99 contentBytes, err := ioutil.ReadFile(path) 100 if err != nil { 101 return fmt.Errorf("failed to read %s: %w", path, err) 102 } 103 104 content := string(contentBytes) 105 hasReplacements := false 106 for key, replacement := range replacements { 107 nextContent := content 108 nextContent = strings.ReplaceAll(nextContent, fmt.Sprintf("{.%s}", key), replacement) 109 110 // Yaml formatter in mdx code section makes extra spaces, need to match them too. 111 nextContent = strings.ReplaceAll(nextContent, fmt.Sprintf("{ .%s }", key), replacement) 112 113 if nextContent != content { 114 hasReplacements = true 115 madeReplacements[key] = true 116 content = nextContent 117 } 118 } 119 if !hasReplacements { 120 return nil 121 } 122 123 log.Printf("Expanded template in %s, saving it", path) 124 if err = renameio.WriteFile(path, []byte(content), os.ModePerm); err != nil { 125 return fmt.Errorf("failed to write changes to file %s: %w", path, err) 126 } 127 128 return nil 129 } 130 131 type latestRelease struct { 132 TagName string `json:"tag_name"` 133 } 134 135 func getLatestVersion() (string, error) { 136 req, err := http.NewRequest( // nolint:noctx 137 http.MethodGet, 138 "https://api.github.com/repos/golangci/golangci-lint/releases/latest", 139 nil, 140 ) 141 if err != nil { 142 return "", fmt.Errorf("failed to prepare a http request: %s", err) 143 } 144 req.Header.Add("Accept", "application/vnd.github.v3+json") 145 resp, err := http.DefaultClient.Do(req) 146 if err != nil { 147 return "", fmt.Errorf("failed to get http response for the latest tag: %s", err) 148 } 149 defer resp.Body.Close() 150 body, err := ioutil.ReadAll(resp.Body) 151 if err != nil { 152 return "", fmt.Errorf("failed to read a body for the latest tag: %s", err) 153 } 154 release := latestRelease{} 155 err = json.Unmarshal(body, &release) 156 if err != nil { 157 return "", fmt.Errorf("failed to unmarshal the body for the latest tag: %s", err) 158 } 159 return release.TagName, nil 160 } 161 162 func buildTemplateContext() (map[string]string, error) { 163 golangciYamlExample, err := ioutil.ReadFile(".golangci.example.yml") 164 if err != nil { 165 return nil, fmt.Errorf("can't read .golangci.example.yml: %s", err) 166 } 167 168 lintersCfg, err := getLintersConfiguration(golangciYamlExample) 169 if err != nil { 170 return nil, fmt.Errorf("can't read .golangci.example.yml: %s", err) 171 } 172 173 if err = exec.Command("make", "build").Run(); err != nil { 174 return nil, fmt.Errorf("can't run go install: %s", err) 175 } 176 177 lintersOut, err := exec.Command("./golangci-lint", "help", "linters").Output() 178 if err != nil { 179 return nil, fmt.Errorf("can't run linters cmd: %s", err) 180 } 181 182 lintersOutParts := bytes.Split(lintersOut, []byte("\n\n")) 183 184 helpCmd := exec.Command("./golangci-lint", "run", "-h") 185 helpCmd.Env = append(helpCmd.Env, os.Environ()...) 186 helpCmd.Env = append(helpCmd.Env, "HELP_RUN=1") // make default concurrency stable: don't depend on machine CPU number 187 help, err := helpCmd.Output() 188 if err != nil { 189 return nil, fmt.Errorf("can't run help cmd: %s", err) 190 } 191 192 helpLines := bytes.Split(help, []byte("\n")) 193 shortHelp := bytes.Join(helpLines[2:], []byte("\n")) 194 changeLog, err := ioutil.ReadFile("CHANGELOG.md") 195 if err != nil { 196 return nil, err 197 } 198 199 latestVersion, err := getLatestVersion() 200 if err != nil { 201 return nil, fmt.Errorf("failed to get latest version: %s", err) 202 } 203 204 return map[string]string{ 205 "LintersExample": lintersCfg, 206 "GolangciYamlExample": strings.TrimSpace(string(golangciYamlExample)), 207 "LintersCommandOutputEnabledOnly": string(lintersOutParts[0]), 208 "LintersCommandOutputDisabledOnly": string(lintersOutParts[1]), 209 "EnabledByDefaultLinters": getLintersListMarkdown(true), 210 "DisabledByDefaultLinters": getLintersListMarkdown(false), 211 "ThanksList": getThanksList(), 212 "RunHelpText": string(shortHelp), 213 "ChangeLog": string(changeLog), 214 "LatestVersion": latestVersion, 215 }, nil 216 } 217 218 func getLintersListMarkdown(enabled bool) string { 219 var neededLcs []*linter.Config 220 lcs := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs() 221 for _, lc := range lcs { 222 if lc.EnabledByDefault == enabled { 223 neededLcs = append(neededLcs, lc) 224 } 225 } 226 227 sort.Slice(neededLcs, func(i, j int) bool { 228 return neededLcs[i].Name() < neededLcs[j].Name() 229 }) 230 231 lines := []string{ 232 "|Name|Description|Presets|AutoFix|Since|", 233 "|---|---|---|---|---|---|", 234 } 235 236 for _, lc := range neededLcs { 237 line := fmt.Sprintf("|%s|%s|%s|%v|%s|", 238 getName(lc), 239 getDesc(lc), 240 strings.Join(lc.InPresets, ", "), 241 check(lc.CanAutoFix, "Auto fix supported"), 242 lc.Since, 243 ) 244 lines = append(lines, line) 245 } 246 247 return strings.Join(lines, "\n") 248 } 249 250 func getName(lc *linter.Config) string { 251 name := lc.Name() 252 253 if lc.OriginalURL != "" { 254 name = fmt.Sprintf("[%s](%s)", lc.Name(), lc.OriginalURL) 255 } 256 257 if !lc.IsDeprecated() { 258 return name 259 } 260 261 title := "deprecated" 262 if lc.Deprecation.Replacement != "" { 263 title += fmt.Sprintf(" since %s", lc.Deprecation.Since) 264 } 265 266 return name + " " + span(title, "⚠") 267 } 268 269 func getDesc(lc *linter.Config) string { 270 desc := lc.Linter.Desc() 271 if lc.IsDeprecated() { 272 desc = lc.Deprecation.Message 273 if lc.Deprecation.Replacement != "" { 274 desc += fmt.Sprintf(" Replaced by %s.", lc.Deprecation.Replacement) 275 } 276 } 277 278 return strings.ReplaceAll(desc, "\n", "<br/>") 279 } 280 281 func check(b bool, title string) string { 282 if b { 283 return span(title, "✔") 284 } 285 return "" 286 } 287 288 func span(title, icon string) string { 289 return fmt.Sprintf(`<span title="%s">%s</span>`, title, icon) 290 } 291 292 func getThanksList() string { 293 var lines []string 294 addedAuthors := map[string]bool{} 295 for _, lc := range lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs() { 296 if lc.OriginalURL == "" { 297 continue 298 } 299 300 const githubPrefix = "https://github.com/" 301 if !strings.HasPrefix(lc.OriginalURL, githubPrefix) { 302 continue 303 } 304 305 githubSuffix := strings.TrimPrefix(lc.OriginalURL, githubPrefix) 306 githubAuthor := strings.Split(githubSuffix, "/")[0] 307 if addedAuthors[githubAuthor] { 308 continue 309 } 310 addedAuthors[githubAuthor] = true 311 312 line := fmt.Sprintf("- [%s](https://github.com/%s)", 313 githubAuthor, githubAuthor) 314 lines = append(lines, line) 315 } 316 317 return strings.Join(lines, "\n") 318 } 319 320 func getLintersConfiguration(example []byte) (string, error) { 321 builder := &strings.Builder{} 322 323 var data yaml.Node 324 err := yaml.Unmarshal(example, &data) 325 if err != nil { 326 return "", err 327 } 328 329 root := data.Content[0] 330 331 for j, node := range root.Content { 332 if node.Value != "linters-settings" { 333 continue 334 } 335 336 nodes := root.Content[j+1] 337 338 for i := 0; i < len(nodes.Content); i += 2 { 339 r := &yaml.Node{ 340 Kind: nodes.Kind, 341 Style: nodes.Style, 342 Tag: nodes.Tag, 343 Value: node.Value, 344 Content: []*yaml.Node{ 345 { 346 Kind: root.Content[j].Kind, 347 Value: root.Content[j].Value, 348 }, 349 { 350 Kind: nodes.Kind, 351 Content: []*yaml.Node{nodes.Content[i], nodes.Content[i+1]}, 352 }, 353 }, 354 } 355 356 _, _ = fmt.Fprintf(builder, "### %s\n\n", nodes.Content[i].Value) 357 _, _ = fmt.Fprintln(builder, "```yaml") 358 359 const ident = 2 360 encoder := yaml.NewEncoder(builder) 361 encoder.SetIndent(ident) 362 363 err = encoder.Encode(r) 364 if err != nil { 365 return "", err 366 } 367 368 _, _ = fmt.Fprintln(builder, "```") 369 _, _ = fmt.Fprintln(builder) 370 } 371 } 372 373 return builder.String(), nil 374 }