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  }