Skip to content

Commit

Permalink
feat: add verify command (#4527)
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez committed Mar 19, 2024
1 parent 6709c97 commit eaafdf3
Show file tree
Hide file tree
Showing 12 changed files with 518 additions and 57 deletions.
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

0 comments on commit eaafdf3

Please sign in to comment.