Skip to content
Merged
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 .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ jobs:

- run: ./golangci-lint config
- run: ./golangci-lint config path
- run: ./golangci-lint config verify --schema jsonschema/golangci.jsonschema.json

- run: ./golangci-lint help
- run: ./golangci-lint help linters
Expand Down
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ issues:
text: "SA1019: c.cfg.Run.ShowStats is deprecated: use Output.ShowStats instead."
- path: pkg/golinters/govet.go
text: "SA1019: cfg.CheckShadowing is deprecated: the linter should be enabled inside `Enable`."
- path: pkg/commands/config.go
text: "SA1019: cfg.Run.UseDefaultSkipDirs is deprecated: use Issues.UseDefaultExcludeDirs instead."

- path: pkg/golinters
linters:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ go.mod: FORCE
go.sum: go.mod

website_copy_jsonschema:
cp -r ./jsonschema ./docs/static
go run ./scripts/website/copy_jsonschema/
.PHONY: website_copy_jsonschema

website_expand_templates:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ require (
github.com/nishanths/exhaustive v0.12.0
github.com/nishanths/predeclared v0.2.2
github.com/nunnatsa/ginkgolinter v0.16.1
github.com/pelletier/go-toml/v2 v2.1.1
github.com/polyfloyd/go-errorlint v1.4.8
github.com/quasilyte/go-ruleguard/dsl v0.3.22
github.com/ryancurrah/gomodguard v1.3.1
Expand Down Expand Up @@ -161,7 +162,6 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum

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

51 changes: 43 additions & 8 deletions pkg/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"

"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"

Expand All @@ -17,13 +18,19 @@ type configCommand struct {
viper *viper.Viper
cmd *cobra.Command

opts config.LoaderOptions
verifyOpts verifyOptions

buildInfo BuildInfo

log logutils.Log
}

func newConfigCommand(log logutils.Log) *configCommand {
func newConfigCommand(log logutils.Log, info BuildInfo) *configCommand {
c := &configCommand{
viper: viper.New(),
log: log,
viper: viper.New(),
log: log,
buildInfo: info,
}

configCmd := &cobra.Command{
Expand All @@ -33,6 +40,15 @@ func newConfigCommand(log logutils.Log) *configCommand {
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
PersistentPreRunE: c.preRunE,
}

verifyCommand := &cobra.Command{
Use: "verify",
Short: "Verify configuration against JSON schema",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: c.executeVerify,
}

configCmd.AddCommand(
Expand All @@ -41,11 +57,21 @@ func newConfigCommand(log logutils.Log) *configCommand {
Short: "Print used config path",
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
Run: c.execute,
PreRunE: c.preRunE,
Run: c.executePath,
},
verifyCommand,
)

flagSet := configCmd.PersistentFlags()
flagSet.SortFlags = false // sort them as they are defined here

setupConfigFileFlagSet(flagSet, &c.opts)

// ex: --schema jsonschema/golangci.next.jsonschema.json
verifyFlagSet := verifyCommand.Flags()
verifyFlagSet.StringVar(&c.verifyOpts.schemaURL, "schema", "", color.GreenString("JSON schema URL"))
_ = verifyFlagSet.MarkHidden("schema")

c.cmd = configCmd

return c
Expand All @@ -54,7 +80,16 @@ func newConfigCommand(log logutils.Log) *configCommand {
func (c *configCommand) preRunE(cmd *cobra.Command, _ []string) error {
// The command doesn't depend on the real configuration.
// It only needs to know the path of the configuration file.
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), config.LoaderOptions{}, config.NewDefault())
cfg := config.NewDefault()

// Hack to hide deprecation messages related to `--skip-dirs-use-default`:
// Flags are not bound then the default values, defined only through flags, are not applied.
// In this command, file path and file information are the only requirements, i.e. it don't need flag values.
//
// TODO(ldez) add an option (check deprecation) to `Loader.Load()` but this require a dedicated PR.
cfg.Run.UseDefaultSkipDirs = true

loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts, cfg)

if err := loader.Load(); err != nil {
return fmt.Errorf("can't load config: %w", err)
Expand All @@ -63,14 +98,14 @@ func (c *configCommand) preRunE(cmd *cobra.Command, _ []string) error {
return nil
}

func (c *configCommand) execute(_ *cobra.Command, _ []string) {
func (c *configCommand) executePath(cmd *cobra.Command, _ []string) {
usedConfigFile := c.getUsedConfig()
if usedConfigFile == "" {
c.log.Warnf("No config file detected")
os.Exit(exitcodes.NoConfigFileDetected)
}

fmt.Println(usedConfigFile)
cmd.Println(usedConfigFile)
}

// getUsedConfig returns the resolved path to the golangci config file,
Expand Down
176 changes: 176 additions & 0 deletions pkg/commands/config_verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package commands

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

hcversion "github.com/hashicorp/go-version"
"github.com/pelletier/go-toml/v2"
"github.com/santhosh-tekuri/jsonschema/v5"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"gopkg.in/yaml.v3"

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

type verifyOptions struct {
schemaURL string // For debugging purpose only (Flag only).
}

func (c *configCommand) executeVerify(cmd *cobra.Command, _ []string) error {
usedConfigFile := c.getUsedConfig()
if usedConfigFile == "" {
c.log.Warnf("No config file detected")
os.Exit(exitcodes.NoConfigFileDetected)
}

schemaURL, err := createSchemaURL(cmd.Flags(), c.buildInfo)
if err != nil {
return fmt.Errorf("get JSON schema: %w", err)
}

err = validateConfiguration(schemaURL, usedConfigFile)
if err != nil {
var v *jsonschema.ValidationError
if !errors.As(err, &v) {
return fmt.Errorf("[%s] validate: %w", usedConfigFile, err)
}

detail := v.DetailedOutput()

printValidationDetail(cmd, &detail)

return fmt.Errorf("the configuration contains invalid elements")
}

return nil
}

func createSchemaURL(flags *pflag.FlagSet, buildInfo BuildInfo) (string, error) {
schemaURL, err := flags.GetString("schema")
if err != nil {
return "", fmt.Errorf("get schema flag: %w", err)
}

if schemaURL != "" {
return schemaURL, nil
}

switch {
case buildInfo.Version != "" && buildInfo.Version != "(devel)":
version, err := hcversion.NewVersion(buildInfo.Version)
if err != nil {
return "", fmt.Errorf("parse version: %w", err)
}

schemaURL = fmt.Sprintf("https://golangci-lint.run/jsonschema/golangci.v%d.%d.jsonschema.json",
version.Segments()[0], version.Segments()[1])

case buildInfo.Commit != "" && buildInfo.Commit != "?":
if buildInfo.Commit == "unknown" {
return "", errors.New("unknown commit information")
}

commit := buildInfo.Commit

if strings.HasPrefix(commit, "(") {
c, _, ok := strings.Cut(strings.TrimPrefix(commit, "("), ",")
if !ok {
return "", errors.New("commit information not found")
}

commit = c
}

schemaURL = fmt.Sprintf("https://raw.githubusercontent.com/golangci/golangci-lint/%s/jsonschema/golangci.next.jsonschema.json",
commit)

default:
return "", errors.New("version not found")
}

return schemaURL, nil
}

func validateConfiguration(schemaPath, targetFile string) error {
compiler := jsonschema.NewCompiler()
compiler.Draft = jsonschema.Draft7

schema, err := compiler.Compile(schemaPath)
if err != nil {
return fmt.Errorf("compile schema: %w", err)
}

var m any

switch strings.ToLower(filepath.Ext(targetFile)) {
case ".yaml", ".yml", ".json":
m, err = decodeYamlFile(targetFile)
if err != nil {
return err
}

case ".toml":
m, err = decodeTomlFile(targetFile)
if err != nil {
return err
}

default:
// unsupported
return errors.New("unsupported configuration format")
}

return schema.Validate(m)
}

func printValidationDetail(cmd *cobra.Command, detail *jsonschema.Detailed) {
if detail.Error != "" {
cmd.PrintErrf("jsonschema: %q does not validate with %q: %s\n",
strings.ReplaceAll(strings.TrimPrefix(detail.InstanceLocation, "/"), "/", "."), detail.KeywordLocation, detail.Error)
}

for _, d := range detail.Errors {
d := d
printValidationDetail(cmd, &d)
}
}

func decodeYamlFile(filename string) (any, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("[%s] file open: %w", filename, err)
}

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

var m any
err = yaml.NewDecoder(file).Decode(&m)
if err != nil {
return nil, fmt.Errorf("[%s] YAML decode: %w", filename, err)
}

return m, nil
}

func decodeTomlFile(filename string) (any, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("[%s] file open: %w", filename, err)
}

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

var m any
err = toml.NewDecoder(file).Decode(&m)
if err != nil {
return nil, fmt.Errorf("[%s] TOML decode: %w", filename, err)
}

return m, nil
}
Loading