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&nbsp;[%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 + "&nbsp;" + 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  }