Skip to content
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

Add "enable-new" command that enables any linters not explicitly disabled #1754

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ require (
golang.org/x/text v0.3.4 // indirect
golang.org/x/tools v0.1.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
Comment on lines 79 to +80
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets use either v2 or v3

honnef.co/go/tools v0.0.1-2020.1.6
mvdan.cc/gofumpt v0.1.0
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed
Expand Down
2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 11 additions & 3 deletions pkg/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ func (e *Executor) initConfig() {
}
e.initRunConfiguration(pathCmd) // allow --config
cmd.AddCommand(pathCmd)

enableNewCmd := &cobra.Command{
Use: "enable-new",
Short: "Enable all linters not explicitly disabled in the active config file",
Run: e.executeEnableNewCmd,
}
e.initRunConfiguration(enableNewCmd) // allow --config
cmd.AddCommand(enableNewCmd)
}

func (e *Executor) getUsedConfig() string {
Expand All @@ -55,12 +63,12 @@ func (e *Executor) executePathCmd(_ *cobra.Command, args []string) {
e.log.Fatalf("Usage: golangci-lint config path")
}

usedConfigFile := e.getUsedConfig()
if usedConfigFile == "" {
usedConfigFilePath := e.getUsedConfig()
if usedConfigFilePath == "" {
e.log.Warnf("No config file detected")
os.Exit(exitcodes.NoConfigFileDetected)
}

fmt.Println(usedConfigFile)
fmt.Println(usedConfigFilePath)
os.Exit(0)
}
72 changes: 72 additions & 0 deletions pkg/commands/enable_new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package commands

import (
"os"

"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/lint/linter"
)

func (e *Executor) executeEnableNewCmd(_ *cobra.Command, args []string) {
if len(args) != 0 {
e.log.Fatalf("Usage: golangci-lint config enable-new")
}

if e.cfg.Linters.EnableAll {
e.log.Fatalf("enable-new is not compatible with the enable-all linters setting")
}

unmentionedLinters, err := e.getUnmentionedLinters()
if err != nil {
e.log.Fatalf("Failed to determine unmentioned linters: %s", err)
}

var newLinterNames []string
for _, l := range unmentionedLinters {
newLinterNames = append(newLinterNames, l.Name())
}

configFilePath := e.getUsedConfig()
if configFilePath == "" {
e.log.Fatalf("No config file detected")
}

color.Yellow("\nEnabling the following new linters in %q:\n", configFilePath)
printLinterConfigs(unmentionedLinters)

if err = config.UpdateConfigFileWithNewLinters(configFilePath, newLinterNames); err != nil {
e.log.Fatalf("failed to update config file: %s", err)
}

os.Exit(0)
}

func (e *Executor) getUnmentionedLinters() ([]*linter.Config, error) {
enabledLinters, err := e.EnabledLintersSet.GetEnabledLintersMap()
if err != nil {
return nil, errors.Wrap(err, "could not determine enabled linters")
}

var newLinters []*linter.Config

NextLinterConfig:
for _, lc := range e.DBManager.GetAllSupportedLinterConfigs() {
for _, name := range lc.AllNames() {
if enabledLinters[name] != nil {
continue NextLinterConfig
}
for _, e := range e.cfg.Linters.Disable {
if e == name {
continue NextLinterConfig
}
}
}
newLinters = append(newLinters, lc)
}

return newLinters, nil
}
81 changes: 81 additions & 0 deletions pkg/config/update_config_with_new_linters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package config

import (
"io/ioutil"
"os"

"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)

// UpdateConfigFileWithNewLinters adds new linters to the "linters" config in the file at the provided path
func UpdateConfigFileWithNewLinters(configFilePath string, newLinters []string) error {
configData, err := ioutil.ReadFile(configFilePath)
if err != nil {
return errors.Wrap(err, "could not read config file")
}

var docNode yaml.Node
if err = yaml.Unmarshal(configData, &docNode); err != nil {
return errors.Wrapf(err, "failed to unmarshal config file %q", configFilePath)
}

var configNode *yaml.Node
if len(docNode.Content) > 0 {
configNode = docNode.Content[0]
} else {
configNode = &yaml.Node{Kind: yaml.MappingNode}
docNode.Content = append(docNode.Content, configNode)
}

// guess the indent level by looking at the column of second level nodes
indentSpaces := 2
GuessSpaces:
for _, n := range configNode.Content {
for _, nn := range n.Content {
indentSpaces = nn.Column - 1
break GuessSpaces
}
}

lintersNode := findOrInsertKeyedValue(configNode, "linters", &yaml.Node{Kind: yaml.MappingNode})

// find the "linters" -> "enable" node (or create it)
enableNode := findOrInsertKeyedValue(lintersNode, "enable", &yaml.Node{Kind: yaml.SequenceNode})

for _, l := range newLinters {
node := &yaml.Node{}
node.SetString(l)
enableNode.Content = append(enableNode.Content, node)
}

configFile, err := os.OpenFile(configFilePath, os.O_WRONLY|os.O_TRUNC, 0)
if err != nil {
return errors.Wrapf(err, "failed to open file %q for writing", configFilePath)
}

encoder := yaml.NewEncoder(configFile)
encoder.SetIndent(indentSpaces)
err = encoder.Encode(docNode.Content[0])
if err == nil {
err = encoder.Close()
}
if err != nil {
err = configFile.Close()
}
return errors.Wrapf(err, "failed to update config file %q", configFilePath)
}

func findOrInsertKeyedValue(node *yaml.Node, key string, value *yaml.Node) *yaml.Node {
for i, n := range node.Content {
var childKey string
err := n.Decode(&childKey)
if err == nil && key == childKey {
return node.Content[i+1]
}
}
keyNode := &yaml.Node{}
keyNode.SetString(key)
node.Content = append(node.Content, keyNode, value)
return value
}
86 changes: 86 additions & 0 deletions pkg/config/update_config_with_new_linters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package config

import (
"io"
"io/ioutil"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/golangci/golangci-lint/pkg/logutils"
)

func TestUpdateConfigWithNewLinters(t *testing.T) {
writeConfig := func(t *testing.T, cfg string) string {
tmpFile, err := ioutil.TempFile("", "golangci-lint-test-config-*.yml")
require.NoError(t, err)

_, err = io.WriteString(tmpFile, cfg)
require.NoError(t, err)

err = tmpFile.Close()
require.NoError(t, err)

t.Cleanup(func() {
os.Remove(tmpFile.Name())
})
return tmpFile.Name()
}

readConfig := func(t *testing.T, filePath string) Config {
var cfg Config
cmdLineCfg := Config{Run: Run{Config: filePath}}
r := NewFileReader(&cfg, &cmdLineCfg, logutils.NewStderrLog("testing"))
err := r.Read()
require.NoError(t, err)
return cfg
}

t.Run(`when the "linters" -> "enable" node exists, we add to it, matching the indent size`, func(t *testing.T) {
cfgFilePath := writeConfig(t, `
linters:
enable:
- other-linter
`)
err := UpdateConfigFileWithNewLinters(cfgFilePath, []string{"new-linter"})
require.NoError(t, err)
cfg := readConfig(t, cfgFilePath)
require.Contains(t, cfg.Linters.Enable, "new-linter")

data, err := ioutil.ReadFile(cfgFilePath)
require.NoError(t, err)
assert.Contains(t, string(data), "\n"+strings.Repeat(" ", 6)+"- new-linter",
"indent size does not match")
})

t.Run(`when there is no "enable" node, we create one`, func(t *testing.T) {
cfgFilePath := writeConfig(t, `
linters: {}
`)
err := UpdateConfigFileWithNewLinters(cfgFilePath, []string{"new-linter"})
require.NoError(t, err)
cfg := readConfig(t, cfgFilePath)
assert.Contains(t, cfg.Linters.Enable, "new-linter")
})

t.Run(`when the file is empty, we create values from scratch`, func(t *testing.T) {
cfgFilePath := writeConfig(t, `
{}
`)
err := UpdateConfigFileWithNewLinters(cfgFilePath, []string{"new-linter"})
require.NoError(t, err)
cfg := readConfig(t, cfgFilePath)
assert.Contains(t, cfg.Linters.Enable, "new-linter")
})

t.Run(`when there is no "linters" node, we create one`, func(t *testing.T) {
cfgFilePath := writeConfig(t, "")
err := UpdateConfigFileWithNewLinters(cfgFilePath, []string{"new-linter"})
require.NoError(t, err)
cfg := readConfig(t, cfgFilePath)
assert.Contains(t, cfg.Linters.Enable, "new-linter")
})
}