Skip to content

Commit

Permalink
feat: migration command (#5506)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez authored Mar 10, 2025
1 parent c3a7802 commit 6a37088
Show file tree
Hide file tree
Showing 369 changed files with 13,490 additions and 263 deletions.
11 changes: 11 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ linters:
linters: [gosec]
text: "G306: Expect WriteFile permissions to be 0600 or less"

# Related to migration command.
- path: pkg/commands/internal/migrate/two/
linters:
- lll

# Related to migration command.
- path: pkg/commands/internal/migrate/
linters:
- gocritic
text: "hugeParam:"

formatters:
enable:
- gofmt
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ fast_check_generated:
git checkout -- go.mod go.sum # can differ between go1.16 and go1.17
git diff --exit-code # check no changes

# Migration

clone_config:
go run ./pkg/commands/internal/migrate/cloner/

# Benchmark

# Benchmark with a local version
Expand Down
195 changes: 195 additions & 0 deletions pkg/commands/internal/migrate/cloner/cloner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package main

import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"strings"

"golang.org/x/tools/imports"
)

const newPkgName = "versiontwo"

const (
srcDir = "./pkg/config"
dstDir = "./pkg/commands/internal/migrate/versiontwo"
)

func main() {
stat, err := os.Stat(srcDir)
if err != nil {
log.Fatal(err)
}

if !stat.IsDir() {
log.Fatalf("%s is not a directory", srcDir)
}

_ = os.RemoveAll(dstDir)

err = processPackage(srcDir, dstDir)
if err != nil {
log.Fatalf("Processing package error: %v", err)
}
}

func processPackage(srcDir, dstDir string) error {
return filepath.Walk(srcDir, func(srcPath string, _ os.FileInfo, err error) error {
if err != nil {
return err
}

if skipFile(srcPath) {
return nil
}

fset := token.NewFileSet()

file, err := parser.ParseFile(fset, srcPath, nil, parser.AllErrors)
if err != nil {
return fmt.Errorf("parsing %s: %w", srcPath, err)
}

processFile(file)

return writeNewFile(fset, file, srcPath, dstDir)
})
}

func skipFile(path string) bool {
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return true
}

switch filepath.Base(path) {
case "base_loader.go", "loader.go":
return true
default:
return false
}
}

func processFile(file *ast.File) {
file.Name.Name = newPkgName

var newDecls []ast.Decl
for _, decl := range file.Decls {
d, ok := decl.(*ast.GenDecl)
if !ok {
continue
}

switch d.Tok {
case token.CONST, token.VAR:
continue
case token.TYPE:
for _, spec := range d.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}

structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}

processStructFields(structType)
}
default:
// noop
}

newDecls = append(newDecls, decl)
}

file.Decls = newDecls
}

func processStructFields(structType *ast.StructType) {
var newFields []*ast.Field

for _, field := range structType.Fields.List {
if len(field.Names) > 0 && !field.Names[0].IsExported() {
continue
}

if field.Tag == nil {
continue
}

field.Type = convertType(field.Type)
field.Tag.Value = convertStructTag(field.Tag.Value)

newFields = append(newFields, field)
}

structType.Fields.List = newFields
}

func convertType(expr ast.Expr) ast.Expr {
ident, ok := expr.(*ast.Ident)
if !ok {
return expr
}

switch ident.Name {
case "bool", "string", "int", "int8", "int16", "int32", "int64", "float32", "float64":
return &ast.StarExpr{X: ident}

default:
return expr
}
}

func convertStructTag(value string) string {
structTag := reflect.StructTag(strings.Trim(value, "`"))

key := structTag.Get("mapstructure")

if key == ",squash" {
return wrapStructTag(`yaml:",inline"`)
}

return wrapStructTag(fmt.Sprintf(`yaml:"%[1]s,omitempty" toml:"%[1]s,multiline,omitempty"`, key))
}

func wrapStructTag(s string) string {
return "`" + s + "`"
}

func writeNewFile(fset *token.FileSet, file *ast.File, srcPath, dstDir string) error {
var buf bytes.Buffer

buf.WriteString("// Code generated by pkg/commands/internal/migrate/cloner/cloner.go. DO NOT EDIT.\n\n")

err := printer.Fprint(&buf, fset, file)
if err != nil {
return fmt.Errorf("printing %s: %w", srcPath, err)
}

dstPath := filepath.Join(dstDir, filepath.Base(srcPath))

_ = os.MkdirAll(filepath.Dir(dstPath), os.ModePerm)

formatted, err := imports.Process(dstPath, buf.Bytes(), nil)
if err != nil {
return fmt.Errorf("formatting %s: %w", dstPath, err)
}

//nolint:gosec,mnd // The permission is right.
err = os.WriteFile(dstPath, formatted, 0o644)
if err != nil {
return fmt.Errorf("writing file %s: %w", dstPath, err)
}

return nil
}
22 changes: 22 additions & 0 deletions pkg/commands/internal/migrate/fakeloader/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fakeloader

// Config implements [config.BaseConfig].
// This only the stub for the real file loader.
type Config struct {
Version string `mapstructure:"version"`

cfgDir string // Path to the directory containing golangci-lint config file.
}

func NewConfig() *Config {
return &Config{}
}

// SetConfigDir sets the path to directory that contains golangci-lint config file.
func (c *Config) SetConfigDir(dir string) {
c.cfgDir = dir
}

func (*Config) IsInternalTest() bool {
return false
}
48 changes: 48 additions & 0 deletions pkg/commands/internal/migrate/fakeloader/fakeloader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package fakeloader

import (
"fmt"
"os"

"github.com/go-viper/mapstructure/v2"

"github.com/golangci/golangci-lint/pkg/commands/internal/migrate/parser"
"github.com/golangci/golangci-lint/pkg/config"
)

// Load is used to keep case of configuration.
// Viper serialize raw map keys in lowercase, this is a problem with the configuration of some linters.
func Load(srcPath string, old any) error {
file, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("open file: %w", err)
}

defer func() { _ = file.Close() }()

raw := map[string]any{}

err = parser.Decode(file, raw)
if err != nil {
return err
}

// NOTE: this is inspired by viper internals.
cc := &mapstructure.DecoderConfig{
Result: old,
WeaklyTypedInput: true,
DecodeHook: config.DecodeHookFunc(),
}

decoder, err := mapstructure.NewDecoder(cc)
if err != nil {
return fmt.Errorf("constructing mapstructure decoder: %w", err)
}

err = decoder.Decode(raw)
if err != nil {
return fmt.Errorf("decoding configuration file: %w", err)
}

return nil
}
19 changes: 19 additions & 0 deletions pkg/commands/internal/migrate/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package migrate

import (
"github.com/golangci/golangci-lint/pkg/commands/internal/migrate/ptr"
"github.com/golangci/golangci-lint/pkg/commands/internal/migrate/versionone"
"github.com/golangci/golangci-lint/pkg/commands/internal/migrate/versiontwo"
)

func ToConfig(old *versionone.Config) *versiontwo.Config {
return &versiontwo.Config{
Version: ptr.Pointer("2"),
Linters: toLinters(old),
Formatters: toFormatters(old),
Issues: toIssues(old),
Output: toOutput(old),
Severity: toSeverity(old),
Run: toRun(old),
}
}
Loading

0 comments on commit 6a37088

Please # to comment.