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 1 commit
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
55 changes: 33 additions & 22 deletions pkg/commands/internal/migrate/migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,27 @@ import (
"strings"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"

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

type fakeFile struct {
bytes.Buffer
name string
}

func newFakeFile(name string) *fakeFile {
return &fakeFile{name: name}
}

func (f *fakeFile) Name() string {
return f.name
}

func TestToConfig(t *testing.T) {
var testFiles []string

Expand All @@ -30,7 +41,7 @@ func TestToConfig(t *testing.T) {
return nil
}

if strings.HasSuffix(path, ".golden.yml") {
if strings.Contains(path, ".golden.") {
return nil
}

Expand All @@ -46,7 +57,7 @@ func TestToConfig(t *testing.T) {
t.Parallel()

ext := filepath.Ext(fileIn)
fileGolden := strings.TrimSuffix(fileIn, ext) + ".golden.yml"
fileGolden := strings.TrimSuffix(fileIn, ext) + ".golden" + ext

testFile(t, fileIn, fileGolden, false)
})
Expand All @@ -58,30 +69,33 @@ func testFile(t *testing.T, in, golden string, update bool) {

old := versionone.NewConfig()

options := config.LoaderOptions{Config: in}

// Fake load of the configuration.
// IMPORTANT: The default values from flags are not set.
loader := config.NewBaseLoader(logutils.NewStderrLog("skip"), viper.New(), options, old, nil)

err := loader.Load()
err := fakeloader.Load(in, old)
require.NoError(t, err)

if update {
updateGolden(t, golden, old)
}

expected, err := os.ReadFile(golden)
require.NoError(t, err)
buf := newFakeFile("test" + filepath.Ext(golden))

var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
encoder.SetIndent(2)
err = parser.Encode(ToConfig(old), buf)
require.NoError(t, err)

err = encoder.Encode(ToConfig(old))
expected, err := os.ReadFile(golden)
require.NoError(t, err)

assert.YAMLEq(t, string(expected), buf.String())
switch filepath.Ext(golden) {
case ".yml":
assert.YAMLEq(t, string(expected), buf.String())
case ".json":
assert.JSONEq(t, string(expected), buf.String())
case ".toml":
assert.Equal(t, string(expected), buf.String())
default:
require.Failf(t, "unsupported extension: %s", golden)
}
}

func updateGolden(t *testing.T, golden string, old *versionone.Config) {
Expand All @@ -94,9 +108,6 @@ func updateGolden(t *testing.T, golden string, old *versionone.Config) {
_ = fileOut.Close()
}()

encoder := yaml.NewEncoder(fileOut)
encoder.SetIndent(2)

err = encoder.Encode(ToConfig(old))
err = parser.Encode(ToConfig(old), fileOut)
require.NoError(t, err)
}
87 changes: 87 additions & 0 deletions pkg/commands/internal/migrate/parser/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package parser

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"path/filepath"
"strings"

"github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v3"
)

type File interface {
io.ReadWriter
Name() string
}

// Decode decodes a file into data.
// The choice of the decoder is based on the file extension.
func Decode(file File, data any) error {
ext := filepath.Ext(file.Name())

switch strings.ToLower(ext) {
case ".yaml", ".yml", ".json":
err := yaml.NewDecoder(file).Decode(data)
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("YAML decode file %s: %w", file.Name(), err)
}

case ".toml":
err := toml.NewDecoder(file).Decode(&data)
if err != nil {
return fmt.Errorf("TOML decode file %s: %w", file.Name(), err)
}

default:
return fmt.Errorf("unsupported file type: %s", ext)
}

return nil
}

// Encode encodes data into a file.
// The choice of the encoder is based on the file extension.
func Encode(data any, dstFile File) error {
ext := filepath.Ext(dstFile.Name())

switch strings.ToLower(ext) {
case ".yml", ".yaml":
encoder := yaml.NewEncoder(dstFile)
encoder.SetIndent(2)

return encoder.Encode(data)

case ".toml":
encoder := toml.NewEncoder(dstFile)

return encoder.Encode(data)

case ".json":
// The JSON encoder converts empty struct to `{}` instead of nothing (even with omitempty JSON struct tags).
// So we need to use the YAML encoder as bridge to create JSON file.

var buf bytes.Buffer
err := yaml.NewEncoder(&buf).Encode(data)
if err != nil {
return err
}

raw := map[string]any{}
err = yaml.NewDecoder(&buf).Decode(raw)
if err != nil {
return err
}

encoder := json.NewEncoder(dstFile)
encoder.SetIndent("", " ")

return encoder.Encode(raw)

default:
return fmt.Errorf("unsupported file type: %s", ext)
}
}
18 changes: 18 additions & 0 deletions pkg/commands/internal/migrate/testdata/empty.golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"formatters": {
"exclusions": {
"generated": "lax"
}
},
"linters": {
"exclusions": {
"generated": "lax",
"paths": [
"third_party$",
"builtin$",
"examples$"
]
}
},
"version": "2"
}
10 changes: 10 additions & 0 deletions pkg/commands/internal/migrate/testdata/empty.golden.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version = '2'

[linters]
[linters.exclusions]
generated = 'lax'
paths = ['third_party$', 'builtin$', 'examples$']

[formatters]
[formatters.exclusions]
generated = 'lax'
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -231,35 +231,35 @@ linters:
- experimental
- opinionated
settings:
captlocal:
paramsonly: false
commentedoutcode:
minlength: 50
captLocal:
paramsOnly: false
commentedOutCode:
minLength: 50
elseif:
skipbalanced: false
hugeparam:
sizethreshold: 70
ifelsechain:
minthreshold: 4
nestingreduce:
bodywidth: 4
rangeexprcopy:
sizethreshold: 516
skiptestfuncs: false
rangevalcopy:
sizethreshold: 32
skiptestfuncs: false
skipBalanced: false
hugeParam:
sizeThreshold: 70
ifElseChain:
minThreshold: 4
nestingReduce:
bodyWidth: 4
rangeExprCopy:
sizeThreshold: 516
skipTestFuncs: false
rangeValCopy:
sizeThreshold: 32
skipTestFuncs: false
ruleguard:
debug: emptyDecl
disable: myGroupName,#myTagName
enable: myGroupName,#myTagName
failon: dsl,import
failOn: dsl,import
rules: ${base-path}/ruleguard/rules-*.go,${base-path}/myrule1.go
toomanyresultschecker:
maxresults: 10
truncatecmp:
skiparchdependent: false
tooManyResultsChecker:
maxResults: 10
truncateCmp:
skipArchDependent: false
underef:
skiprecvderef: false
unnamedresult:
checkexported: true
skipRecvDeref: false
unnamedResult:
checkExported: true
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ linters:
goheader:
values:
const:
company: MY COMPANY
COMPANY: MY COMPANY
regexp:
author: .*@mycompany\.com
AUTHOR: .*@mycompany\.com
template: |-
Put here copyright header template for source code files
For example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,20 @@ linters:
severity: medium
confidence: medium
config:
g101:
G101:
entropy_threshold: "80.0"
ignore_entropy: false
pattern: (?i)example
per_char_threshold: "3.0"
truncate: "32"
g104:
G104:
fmt:
- Fscanf
g111:
G111:
pattern: custom\.Dir\(\)
g301: "0750"
g302: "0600"
g306: "0600"
G301: "0750"
G302: "0600"
G306: "0600"
global:
'#nosec': '#my-custom-nosec'
audit: true
Expand Down
Loading
Loading