github.com/chenfeining/golangci-lint@v1.0.2-0.20230730162517-14c6c67868df/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" 11 "log" 12 "net/http" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "reflect" 17 "sort" 18 "strings" 19 "unicode" 20 "unicode/utf8" 21 22 "gopkg.in/yaml.v3" 23 24 "github.com/chenfeining/golangci-lint/internal/renameio" 25 "github.com/chenfeining/golangci-lint/pkg/config" 26 "github.com/chenfeining/golangci-lint/pkg/lint/linter" 27 "github.com/chenfeining/golangci-lint/pkg/lint/lintersdb" 28 ) 29 30 const listItemPrefix = "list-item-" 31 32 var stateFilePath = filepath.Join("docs", "template_data.state") 33 34 func main() { 35 var onlyWriteState bool 36 flag.BoolVar(&onlyWriteState, "only-state", false, fmt.Sprintf("Only write hash of state to %s and exit", stateFilePath)) 37 flag.Parse() 38 39 replacements, err := buildTemplateContext() 40 if err != nil { 41 log.Fatalf("Failed to build template context: %s", err) 42 } 43 44 if err = updateStateFile(replacements); err != nil { 45 log.Fatalf("Failed to update state file: %s", err) 46 } 47 48 if onlyWriteState { 49 return 50 } 51 52 if err := rewriteDocs(replacements); err != nil { 53 log.Fatalf("Failed to rewrite docs: %s", err) 54 } 55 log.Print("Successfully expanded templates") 56 } 57 58 func updateStateFile(replacements map[string]string) error { 59 replBytes, err := json.Marshal(replacements) 60 if err != nil { 61 return fmt.Errorf("failed to json marshal replacements: %w", err) 62 } 63 64 h := sha256.New() 65 if _, err := h.Write(replBytes); err != nil { 66 return err 67 } 68 69 contentBuf := bytes.NewBufferString("This file stores hash of website templates to trigger " + 70 "Netlify rebuild when something changes, e.g. new linter is added.\n") 71 contentBuf.WriteString(hex.EncodeToString(h.Sum(nil))) 72 73 return renameio.WriteFile(stateFilePath, contentBuf.Bytes(), os.ModePerm) 74 } 75 76 func rewriteDocs(replacements map[string]string) error { 77 madeReplacements := map[string]bool{} 78 err := filepath.Walk(filepath.Join("docs", "src", "docs"), 79 func(path string, info os.FileInfo, err error) error { 80 if err != nil { 81 return err 82 } 83 if info.IsDir() { 84 return nil 85 } 86 return processDoc(path, replacements, madeReplacements) 87 }) 88 if err != nil { 89 return fmt.Errorf("failed to walk dir: %w", err) 90 } 91 92 if len(madeReplacements) != len(replacements) { 93 for key := range replacements { 94 if !madeReplacements[key] { 95 log.Printf("Replacement %q wasn't performed", key) 96 } 97 } 98 return fmt.Errorf("%d replacements weren't performed", len(replacements)-len(madeReplacements)) 99 } 100 return nil 101 } 102 103 func processDoc(path string, replacements map[string]string, madeReplacements map[string]bool) error { 104 contentBytes, err := os.ReadFile(path) 105 if err != nil { 106 return fmt.Errorf("failed to read %s: %w", path, err) 107 } 108 109 content := string(contentBytes) 110 hasReplacements := false 111 for key, replacement := range replacements { 112 nextContent := content 113 nextContent = strings.ReplaceAll(nextContent, fmt.Sprintf("{.%s}", key), replacement) 114 115 // Yaml formatter in mdx code section makes extra spaces, need to match them too. 116 nextContent = strings.ReplaceAll(nextContent, fmt.Sprintf("{ .%s }", key), replacement) 117 118 if nextContent != content { 119 hasReplacements = true 120 madeReplacements[key] = true 121 content = nextContent 122 } 123 } 124 if !hasReplacements { 125 return nil 126 } 127 128 log.Printf("Expanded template in %s, saving it", path) 129 if err = renameio.WriteFile(path, []byte(content), os.ModePerm); err != nil { 130 return fmt.Errorf("failed to write changes to file %s: %w", path, err) 131 } 132 133 return nil 134 } 135 136 type latestRelease struct { 137 TagName string `json:"tag_name"` 138 } 139 140 func getLatestVersion() (string, error) { 141 req, err := http.NewRequest( //nolint:noctx 142 http.MethodGet, 143 "https://api.github.com/repos/chenfeining/golangci-lint/releases/latest", 144 http.NoBody, 145 ) 146 if err != nil { 147 return "", fmt.Errorf("failed to prepare a http request: %w", err) 148 } 149 req.Header.Add("Accept", "application/vnd.github.v3+json") 150 resp, err := http.DefaultClient.Do(req) 151 if err != nil { 152 return "", fmt.Errorf("failed to get http response for the latest tag: %w", err) 153 } 154 defer resp.Body.Close() 155 body, err := io.ReadAll(resp.Body) 156 if err != nil { 157 return "", fmt.Errorf("failed to read a body for the latest tag: %w", err) 158 } 159 release := latestRelease{} 160 err = json.Unmarshal(body, &release) 161 if err != nil { 162 return "", fmt.Errorf("failed to unmarshal the body for the latest tag: %w", err) 163 } 164 return release.TagName, nil 165 } 166 167 func buildTemplateContext() (map[string]string, error) { 168 golangciYamlExample, err := os.ReadFile(".golangci.reference.yml") 169 if err != nil { 170 return nil, fmt.Errorf("can't read .golangci.reference.yml: %w", err) 171 } 172 173 snippets, err := extractExampleSnippets(golangciYamlExample) 174 if err != nil { 175 return nil, fmt.Errorf("can't read .golangci.reference.yml: %w", err) 176 } 177 178 if err = exec.Command("make", "build").Run(); err != nil { 179 return nil, fmt.Errorf("can't run go install: %w", err) 180 } 181 182 lintersOut, err := exec.Command("./golangci-lint", "help", "linters").Output() 183 if err != nil { 184 return nil, fmt.Errorf("can't run linters cmd: %w", err) 185 } 186 187 lintersOutParts := bytes.Split(lintersOut, []byte("\n\n")) 188 189 helpCmd := exec.Command("./golangci-lint", "run", "-h") 190 helpCmd.Env = append(helpCmd.Env, os.Environ()...) 191 helpCmd.Env = append(helpCmd.Env, "HELP_RUN=1") // make default concurrency stable: don't depend on machine CPU number 192 help, err := helpCmd.Output() 193 if err != nil { 194 return nil, fmt.Errorf("can't run help cmd: %w", err) 195 } 196 197 helpLines := bytes.Split(help, []byte("\n")) 198 shortHelp := bytes.Join(helpLines[2:], []byte("\n")) 199 changeLog, err := os.ReadFile("CHANGELOG.md") 200 if err != nil { 201 return nil, err 202 } 203 204 latestVersion, err := getLatestVersion() 205 if err != nil { 206 return nil, fmt.Errorf("failed to get the latest version: %w", err) 207 } 208 209 return map[string]string{ 210 "LintersExample": snippets.LintersSettings, 211 "ConfigurationExample": snippets.ConfigurationFile, 212 "LintersCommandOutputEnabledOnly": string(lintersOutParts[0]), 213 "LintersCommandOutputDisabledOnly": string(lintersOutParts[1]), 214 "EnabledByDefaultLinters": getLintersListMarkdown(true), 215 "DisabledByDefaultLinters": getLintersListMarkdown(false), 216 "DefaultExclusions": getDefaultExclusions(), 217 "ThanksList": getThanksList(), 218 "RunHelpText": string(shortHelp), 219 "ChangeLog": string(changeLog), 220 "LatestVersion": latestVersion, 221 }, nil 222 } 223 224 func getDefaultExclusions() string { 225 bufferString := bytes.NewBufferString("") 226 227 for _, pattern := range config.DefaultExcludePatterns { 228 _, _ = fmt.Fprintln(bufferString) 229 _, _ = fmt.Fprintf(bufferString, "### %s\n", pattern.ID) 230 _, _ = fmt.Fprintln(bufferString) 231 _, _ = fmt.Fprintf(bufferString, "- linter: `%s`\n", pattern.Linter) 232 _, _ = fmt.Fprintf(bufferString, "- pattern: `%s`\n", strings.ReplaceAll(pattern.Pattern, "`", "`")) 233 _, _ = fmt.Fprintf(bufferString, "- why: %s\n", pattern.Why) 234 } 235 236 return bufferString.String() 237 } 238 239 func getLintersListMarkdown(enabled bool) string { 240 var neededLcs []*linter.Config 241 lcs := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs() 242 for _, lc := range lcs { 243 if lc.Internal { 244 continue 245 } 246 247 if lc.EnabledByDefault == enabled { 248 neededLcs = append(neededLcs, lc) 249 } 250 } 251 252 sort.Slice(neededLcs, func(i, j int) bool { 253 return neededLcs[i].Name() < neededLcs[j].Name() 254 }) 255 256 lines := []string{ 257 "|Name|Description|Presets|AutoFix|Since|", 258 "|---|---|---|---|---|---|", 259 } 260 261 for _, lc := range neededLcs { 262 line := fmt.Sprintf("|%s|%s|%s|%v|%s|", 263 getName(lc), 264 getDesc(lc), 265 strings.Join(lc.InPresets, ", "), 266 check(lc.CanAutoFix, "Auto fix supported"), 267 lc.Since, 268 ) 269 lines = append(lines, line) 270 } 271 272 return strings.Join(lines, "\n") 273 } 274 275 func getName(lc *linter.Config) string { 276 name := lc.Name() 277 278 if lc.OriginalURL != "" { 279 name = fmt.Sprintf("[%s](%s)", name, lc.OriginalURL) 280 } 281 282 if hasSettings(lc.Name()) { 283 name = fmt.Sprintf("%s [%s](#%s)", name, spanWithID(listItemPrefix+lc.Name(), "Configuration", "⚙️"), lc.Name()) 284 } 285 286 if !lc.IsDeprecated() { 287 return name 288 } 289 290 title := "deprecated" 291 if lc.Deprecation.Replacement != "" { 292 title += fmt.Sprintf(" since %s", lc.Deprecation.Since) 293 } 294 295 return name + " " + span(title, "⚠") 296 } 297 298 func getDesc(lc *linter.Config) string { 299 desc := lc.Linter.Desc() 300 if lc.IsDeprecated() { 301 desc = lc.Deprecation.Message 302 if lc.Deprecation.Replacement != "" { 303 desc += fmt.Sprintf(" Replaced by %s.", lc.Deprecation.Replacement) 304 } 305 } 306 307 return formatDesc(desc) 308 } 309 310 func formatDesc(desc string) string { 311 runes := []rune(desc) 312 313 r, _ := utf8.DecodeRuneInString(desc) 314 runes[0] = unicode.ToUpper(r) 315 316 if runes[len(runes)-1] != '.' { 317 runes = append(runes, '.') 318 } 319 320 return strings.ReplaceAll(string(runes), "\n", "<br/>") 321 } 322 323 func check(b bool, title string) string { 324 if b { 325 return span(title, "✔") 326 } 327 return "" 328 } 329 330 func hasSettings(name string) bool { 331 tp := reflect.TypeOf(config.LintersSettings{}) 332 333 for i := 0; i < tp.NumField(); i++ { 334 if strings.EqualFold(name, tp.Field(i).Name) { 335 return true 336 } 337 } 338 339 return false 340 } 341 342 func span(title, icon string) string { 343 return fmt.Sprintf(`<span title=%q>%s</span>`, title, icon) 344 } 345 346 func spanWithID(id, title, icon string) string { 347 return fmt.Sprintf(`<span id=%q title=%q>%s</span>`, id, title, icon) 348 } 349 350 type authorDetails struct { 351 Linters []string 352 Profile string 353 Avatar string 354 } 355 356 func getThanksList() string { 357 addedAuthors := map[string]*authorDetails{} 358 359 for _, lc := range lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs() { 360 if lc.Internal { 361 continue 362 } 363 364 if lc.OriginalURL == "" { 365 continue 366 } 367 368 linterURL := lc.OriginalURL 369 if lc.Name() == "staticcheck" { 370 linterURL = "https://github.com/dominikh/go-tools" 371 } 372 373 if author := extractAuthor(linterURL, "https://github.com/"); author != "" && author != "golangci" { 374 if _, ok := addedAuthors[author]; ok { 375 addedAuthors[author].Linters = append(addedAuthors[author].Linters, lc.Name()) 376 } else { 377 addedAuthors[author] = &authorDetails{ 378 Linters: []string{lc.Name()}, 379 Profile: fmt.Sprintf("[%[1]s](https://github.com/sponsors/%[1]s)", author), 380 Avatar: fmt.Sprintf(`<img src="https://github.com/%[1]s.png" alt="%[1]s" style="max-width: 100%%;" width="20px;" />`, author), 381 } 382 } 383 } else if author := extractAuthor(linterURL, "https://gitlab.com/"); author != "" { 384 if _, ok := addedAuthors[author]; ok { 385 addedAuthors[author].Linters = append(addedAuthors[author].Linters, lc.Name()) 386 } else { 387 addedAuthors[author] = &authorDetails{ 388 Linters: []string{lc.Name()}, 389 Profile: fmt.Sprintf("[%[1]s](https://gitlab.com/%[1]s)", author), 390 } 391 } 392 } else { 393 continue 394 } 395 } 396 397 var authors []string 398 for author := range addedAuthors { 399 authors = append(authors, author) 400 } 401 402 sort.Slice(authors, func(i, j int) bool { 403 return strings.ToLower(authors[i]) < strings.ToLower(authors[j]) 404 }) 405 406 lines := []string{ 407 "|Author|Linter(s)|", 408 "|---|---|", 409 } 410 411 for _, author := range authors { 412 lines = append(lines, fmt.Sprintf("|%s %s|%s|", 413 addedAuthors[author].Avatar, addedAuthors[author].Profile, strings.Join(addedAuthors[author].Linters, ", "))) 414 } 415 416 return strings.Join(lines, "\n") 417 } 418 419 func extractAuthor(originalURL, prefix string) string { 420 if !strings.HasPrefix(originalURL, prefix) { 421 return "" 422 } 423 424 return strings.SplitN(strings.TrimPrefix(originalURL, prefix), "/", 2)[0] 425 } 426 427 type SettingSnippets struct { 428 ConfigurationFile string 429 LintersSettings string 430 } 431 432 func extractExampleSnippets(example []byte) (*SettingSnippets, error) { 433 var data yaml.Node 434 err := yaml.Unmarshal(example, &data) 435 if err != nil { 436 return nil, err 437 } 438 439 root := data.Content[0] 440 441 globalNode := &yaml.Node{ 442 Kind: root.Kind, 443 Style: root.Style, 444 Tag: root.Tag, 445 Value: root.Value, 446 Anchor: root.Anchor, 447 Alias: root.Alias, 448 HeadComment: root.HeadComment, 449 LineComment: root.LineComment, 450 FootComment: root.FootComment, 451 Line: root.Line, 452 Column: root.Column, 453 } 454 455 snippets := SettingSnippets{} 456 457 builder := strings.Builder{} 458 459 for j, node := range root.Content { 460 switch node.Value { 461 case "run", "output", "linters", "linters-settings", "issues", "severity": 462 default: 463 continue 464 } 465 466 nextNode := root.Content[j+1] 467 468 newNode := &yaml.Node{ 469 Kind: nextNode.Kind, 470 Content: []*yaml.Node{ 471 { 472 HeadComment: fmt.Sprintf("See the dedicated %q documentation section.", node.Value), 473 Kind: node.Kind, 474 Style: node.Style, 475 Tag: node.Tag, 476 Value: "option", 477 }, 478 { 479 Kind: node.Kind, 480 Style: node.Style, 481 Tag: node.Tag, 482 Value: "value", 483 }, 484 }, 485 } 486 487 globalNode.Content = append(globalNode.Content, node, newNode) 488 489 if node.Value == "linters-settings" { 490 snippets.LintersSettings, err = getLintersSettingSections(node, nextNode) 491 if err != nil { 492 return nil, err 493 } 494 495 _, _ = builder.WriteString( 496 fmt.Sprintf( 497 "### `%s` configuration\n\nSee the dedicated [linters-settings](/usage/linters) documentation section.\n\n", 498 node.Value, 499 ), 500 ) 501 continue 502 } 503 504 nodeSection := &yaml.Node{ 505 Kind: root.Kind, 506 Style: root.Style, 507 Tag: root.Tag, 508 Value: root.Value, 509 Content: []*yaml.Node{node, nextNode}, 510 } 511 512 snippet, errSnip := marshallSnippet(nodeSection) 513 if errSnip != nil { 514 return nil, errSnip 515 } 516 517 _, _ = builder.WriteString(fmt.Sprintf("### `%s` configuration\n\n%s", node.Value, snippet)) 518 } 519 520 overview, err := marshallSnippet(globalNode) 521 if err != nil { 522 return nil, err 523 } 524 525 snippets.ConfigurationFile = overview + builder.String() 526 527 return &snippets, nil 528 } 529 530 func getLintersSettingSections(node, nextNode *yaml.Node) (string, error) { 531 lcs := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs() 532 533 var lintersDesc = make(map[string]string) 534 for _, lc := range lcs { 535 if lc.Internal { 536 continue 537 } 538 539 // it's important to use lc.Name() nor name because name can be alias 540 lintersDesc[lc.Name()] = getDesc(lc) 541 } 542 543 builder := &strings.Builder{} 544 545 for i := 0; i < len(nextNode.Content); i += 2 { 546 r := &yaml.Node{ 547 Kind: nextNode.Kind, 548 Style: nextNode.Style, 549 Tag: nextNode.Tag, 550 Value: node.Value, 551 Content: []*yaml.Node{ 552 { 553 Kind: node.Kind, 554 Value: node.Value, 555 }, 556 { 557 Kind: nextNode.Kind, 558 Content: []*yaml.Node{nextNode.Content[i], nextNode.Content[i+1]}, 559 }, 560 }, 561 } 562 563 _, _ = fmt.Fprintf(builder, "### %s\n\n", nextNode.Content[i].Value) 564 _, _ = fmt.Fprintf(builder, "%s\n\n", lintersDesc[nextNode.Content[i].Value]) 565 _, _ = fmt.Fprintln(builder, "```yaml") 566 567 encoder := yaml.NewEncoder(builder) 568 encoder.SetIndent(2) 569 570 err := encoder.Encode(r) 571 if err != nil { 572 return "", err 573 } 574 575 _, _ = fmt.Fprintln(builder, "```") 576 _, _ = fmt.Fprintln(builder) 577 _, _ = fmt.Fprintf(builder, "[%s](#%s)\n\n", span("Back to the top", "🔼"), listItemPrefix+nextNode.Content[i].Value) 578 _, _ = fmt.Fprintln(builder) 579 } 580 581 return builder.String(), nil 582 } 583 584 func marshallSnippet(node *yaml.Node) (string, error) { 585 builder := &strings.Builder{} 586 587 if node.Value != "" { 588 _, _ = fmt.Fprintf(builder, "### %s\n\n", node.Value) 589 } 590 _, _ = fmt.Fprintln(builder, "```yaml") 591 592 encoder := yaml.NewEncoder(builder) 593 encoder.SetIndent(2) 594 595 err := encoder.Encode(node) 596 if err != nil { 597 return "", err 598 } 599 600 _, _ = fmt.Fprintln(builder, "```") 601 _, _ = fmt.Fprintln(builder) 602 603 return builder.String(), nil 604 }