Skip to content

feat: migration command #5506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
973f628
chore: neutral configuration loader
ldez Feb 26, 2025
e24f69c
chore: copy v1 configuration structures
ldez Feb 26, 2025
519820a
chore: clean configuration v2
ldez Mar 1, 2025
450752d
chore: configuration v2 cloner
ldez Mar 1, 2025
1a74a2a
feat: migration command
ldez Mar 1, 2025
4ef8f4c
tests: configuration flles
ldez Mar 1, 2025
9bea57a
review: add comment on generated files
ldez Mar 5, 2025
101ac2a
review: add comment about v1 configuration files
ldez Mar 5, 2025
b7ca75f
review: rephrase
ldez Mar 5, 2025
e102282
review: log level
ldez Mar 5, 2025
ca11e3e
review: remove a switch
ldez Mar 5, 2025
d7d4986
fix: add a missing linter names migration case
ldez Mar 5, 2025
6e989eb
docs: improve v1 configuration files
ldez Mar 6, 2025
c95ec05
review: rename configuration packages
ldez Mar 6, 2025
8a3fbff
review
ldez Mar 6, 2025
064f43f
review: add log about run.timeout
ldez Mar 6, 2025
5ec43d0
review
ldez Mar 7, 2025
345e674
review
ldez Mar 7, 2025
1ab0066
review
ldez Mar 7, 2025
7df6432
review
ldez Mar 7, 2025
fe69114
feat: missing case
ldez Mar 7, 2025
a7e11f8
review
ldez Mar 8, 2025
fdf27fc
review
ldez Mar 8, 2025
e809f59
review
ldez Mar 8, 2025
7646196
chore: reduce log verbosity
ldez Mar 9, 2025
d66ff11
chore: rewrite the configuration loading
ldez Mar 9, 2025
0a90047
fix: toml multiline string
ldez Mar 9, 2025
8883832
chore: split test files by extensions
ldez Mar 10, 2025
d35e0d4
fix: exclude-use-default is true by default
ldez Mar 10, 2025
6cbdf61
review
ldez Mar 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
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
Loading