Skip to content

Commit eaafdf3

Browse files
authored
feat: add verify command (#4527)
1 parent 6709c97 commit eaafdf3

File tree

12 files changed

+518
-57
lines changed

12 files changed

+518
-57
lines changed

.github/workflows/pr.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ jobs:
166166

167167
- run: ./golangci-lint config
168168
- run: ./golangci-lint config path
169+
- run: ./golangci-lint config verify --schema jsonschema/golangci.jsonschema.json
169170

170171
- run: ./golangci-lint help
171172
- run: ./golangci-lint help linters

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ issues:
163163
text: "SA1019: c.cfg.Run.ShowStats is deprecated: use Output.ShowStats instead."
164164
- path: pkg/golinters/govet.go
165165
text: "SA1019: cfg.CheckShadowing is deprecated: the linter should be enabled inside `Enable`."
166+
- path: pkg/commands/config.go
167+
text: "SA1019: cfg.Run.UseDefaultSkipDirs is deprecated: use Issues.UseDefaultExcludeDirs instead."
166168

167169
- path: pkg/golinters
168170
linters:

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ go.mod: FORCE
8989
go.sum: go.mod
9090

9191
website_copy_jsonschema:
92-
cp -r ./jsonschema ./docs/static
92+
go run ./scripts/website/copy_jsonschema/
9393
.PHONY: website_copy_jsonschema
9494

9595
website_expand_templates:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ require (
8080
github.com/nishanths/exhaustive v0.12.0
8181
github.com/nishanths/predeclared v0.2.2
8282
github.com/nunnatsa/ginkgolinter v0.16.1
83+
github.com/pelletier/go-toml/v2 v2.1.1
8384
github.com/polyfloyd/go-errorlint v1.4.8
8485
github.com/quasilyte/go-ruleguard/dsl v0.3.22
8586
github.com/ryancurrah/gomodguard v1.3.1
@@ -161,7 +162,6 @@ require (
161162
github.com/mitchellh/mapstructure v1.5.0 // indirect
162163
github.com/olekukonko/tablewriter v0.0.5 // indirect
163164
github.com/pelletier/go-toml v1.9.5 // indirect
164-
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
165165
github.com/pmezard/go-difflib v1.0.0 // indirect
166166
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
167167
github.com/prometheus/client_golang v1.12.1 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/commands/config.go

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66

7+
"github.com/fatih/color"
78
"github.com/spf13/cobra"
89
"github.com/spf13/viper"
910

@@ -17,13 +18,19 @@ type configCommand struct {
1718
viper *viper.Viper
1819
cmd *cobra.Command
1920

21+
opts config.LoaderOptions
22+
verifyOpts verifyOptions
23+
24+
buildInfo BuildInfo
25+
2026
log logutils.Log
2127
}
2228

23-
func newConfigCommand(log logutils.Log) *configCommand {
29+
func newConfigCommand(log logutils.Log, info BuildInfo) *configCommand {
2430
c := &configCommand{
25-
viper: viper.New(),
26-
log: log,
31+
viper: viper.New(),
32+
log: log,
33+
buildInfo: info,
2734
}
2835

2936
configCmd := &cobra.Command{
@@ -33,6 +40,15 @@ func newConfigCommand(log logutils.Log) *configCommand {
3340
RunE: func(cmd *cobra.Command, _ []string) error {
3441
return cmd.Help()
3542
},
43+
PersistentPreRunE: c.preRunE,
44+
}
45+
46+
verifyCommand := &cobra.Command{
47+
Use: "verify",
48+
Short: "Verify configuration against JSON schema",
49+
Args: cobra.NoArgs,
50+
ValidArgsFunction: cobra.NoFileCompletions,
51+
RunE: c.executeVerify,
3652
}
3753

3854
configCmd.AddCommand(
@@ -41,11 +57,21 @@ func newConfigCommand(log logutils.Log) *configCommand {
4157
Short: "Print used config path",
4258
Args: cobra.NoArgs,
4359
ValidArgsFunction: cobra.NoFileCompletions,
44-
Run: c.execute,
45-
PreRunE: c.preRunE,
60+
Run: c.executePath,
4661
},
62+
verifyCommand,
4763
)
4864

65+
flagSet := configCmd.PersistentFlags()
66+
flagSet.SortFlags = false // sort them as they are defined here
67+
68+
setupConfigFileFlagSet(flagSet, &c.opts)
69+
70+
// ex: --schema jsonschema/golangci.next.jsonschema.json
71+
verifyFlagSet := verifyCommand.Flags()
72+
verifyFlagSet.StringVar(&c.verifyOpts.schemaURL, "schema", "", color.GreenString("JSON schema URL"))
73+
_ = verifyFlagSet.MarkHidden("schema")
74+
4975
c.cmd = configCmd
5076

5177
return c
@@ -54,7 +80,16 @@ func newConfigCommand(log logutils.Log) *configCommand {
5480
func (c *configCommand) preRunE(cmd *cobra.Command, _ []string) error {
5581
// The command doesn't depend on the real configuration.
5682
// It only needs to know the path of the configuration file.
57-
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), config.LoaderOptions{}, config.NewDefault())
83+
cfg := config.NewDefault()
84+
85+
// Hack to hide deprecation messages related to `--skip-dirs-use-default`:
86+
// Flags are not bound then the default values, defined only through flags, are not applied.
87+
// In this command, file path and file information are the only requirements, i.e. it don't need flag values.
88+
//
89+
// TODO(ldez) add an option (check deprecation) to `Loader.Load()` but this require a dedicated PR.
90+
cfg.Run.UseDefaultSkipDirs = true
91+
92+
loader := config.NewLoader(c.log.Child(logutils.DebugKeyConfigReader), c.viper, cmd.Flags(), c.opts, cfg)
5893

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

66-
func (c *configCommand) execute(_ *cobra.Command, _ []string) {
101+
func (c *configCommand) executePath(cmd *cobra.Command, _ []string) {
67102
usedConfigFile := c.getUsedConfig()
68103
if usedConfigFile == "" {
69104
c.log.Warnf("No config file detected")
70105
os.Exit(exitcodes.NoConfigFileDetected)
71106
}
72107

73-
fmt.Println(usedConfigFile)
108+
cmd.Println(usedConfigFile)
74109
}
75110

76111
// getUsedConfig returns the resolved path to the golangci config file,

pkg/commands/config_verify.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package commands
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
hcversion "github.com/hashicorp/go-version"
11+
"github.com/pelletier/go-toml/v2"
12+
"github.com/santhosh-tekuri/jsonschema/v5"
13+
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
14+
"github.com/spf13/cobra"
15+
"github.com/spf13/pflag"
16+
"gopkg.in/yaml.v3"
17+
18+
"github.com/golangci/golangci-lint/pkg/exitcodes"
19+
)
20+
21+
type verifyOptions struct {
22+
schemaURL string // For debugging purpose only (Flag only).
23+
}
24+
25+
func (c *configCommand) executeVerify(cmd *cobra.Command, _ []string) error {
26+
usedConfigFile := c.getUsedConfig()
27+
if usedConfigFile == "" {
28+
c.log.Warnf("No config file detected")
29+
os.Exit(exitcodes.NoConfigFileDetected)
30+
}
31+
32+
schemaURL, err := createSchemaURL(cmd.Flags(), c.buildInfo)
33+
if err != nil {
34+
return fmt.Errorf("get JSON schema: %w", err)
35+
}
36+
37+
err = validateConfiguration(schemaURL, usedConfigFile)
38+
if err != nil {
39+
var v *jsonschema.ValidationError
40+
if !errors.As(err, &v) {
41+
return fmt.Errorf("[%s] validate: %w", usedConfigFile, err)
42+
}
43+
44+
detail := v.DetailedOutput()
45+
46+
printValidationDetail(cmd, &detail)
47+
48+
return fmt.Errorf("the configuration contains invalid elements")
49+
}
50+
51+
return nil
52+
}
53+
54+
func createSchemaURL(flags *pflag.FlagSet, buildInfo BuildInfo) (string, error) {
55+
schemaURL, err := flags.GetString("schema")
56+
if err != nil {
57+
return "", fmt.Errorf("get schema flag: %w", err)
58+
}
59+
60+
if schemaURL != "" {
61+
return schemaURL, nil
62+
}
63+
64+
switch {
65+
case buildInfo.Version != "" && buildInfo.Version != "(devel)":
66+
version, err := hcversion.NewVersion(buildInfo.Version)
67+
if err != nil {
68+
return "", fmt.Errorf("parse version: %w", err)
69+
}
70+
71+
schemaURL = fmt.Sprintf("https://golangci-lint.run/jsonschema/golangci.v%d.%d.jsonschema.json",
72+
version.Segments()[0], version.Segments()[1])
73+
74+
case buildInfo.Commit != "" && buildInfo.Commit != "?":
75+
if buildInfo.Commit == "unknown" {
76+
return "", errors.New("unknown commit information")
77+
}
78+
79+
commit := buildInfo.Commit
80+
81+
if strings.HasPrefix(commit, "(") {
82+
c, _, ok := strings.Cut(strings.TrimPrefix(commit, "("), ",")
83+
if !ok {
84+
return "", errors.New("commit information not found")
85+
}
86+
87+
commit = c
88+
}
89+
90+
schemaURL = fmt.Sprintf("https://raw.githubusercontent.com/golangci/golangci-lint/%s/jsonschema/golangci.next.jsonschema.json",
91+
commit)
92+
93+
default:
94+
return "", errors.New("version not found")
95+
}
96+
97+
return schemaURL, nil
98+
}
99+
100+
func validateConfiguration(schemaPath, targetFile string) error {
101+
compiler := jsonschema.NewCompiler()
102+
compiler.Draft = jsonschema.Draft7
103+
104+
schema, err := compiler.Compile(schemaPath)
105+
if err != nil {
106+
return fmt.Errorf("compile schema: %w", err)
107+
}
108+
109+
var m any
110+
111+
switch strings.ToLower(filepath.Ext(targetFile)) {
112+
case ".yaml", ".yml", ".json":
113+
m, err = decodeYamlFile(targetFile)
114+
if err != nil {
115+
return err
116+
}
117+
118+
case ".toml":
119+
m, err = decodeTomlFile(targetFile)
120+
if err != nil {
121+
return err
122+
}
123+
124+
default:
125+
// unsupported
126+
return errors.New("unsupported configuration format")
127+
}
128+
129+
return schema.Validate(m)
130+
}
131+
132+
func printValidationDetail(cmd *cobra.Command, detail *jsonschema.Detailed) {
133+
if detail.Error != "" {
134+
cmd.PrintErrf("jsonschema: %q does not validate with %q: %s\n",
135+
strings.ReplaceAll(strings.TrimPrefix(detail.InstanceLocation, "/"), "/", "."), detail.KeywordLocation, detail.Error)
136+
}
137+
138+
for _, d := range detail.Errors {
139+
d := d
140+
printValidationDetail(cmd, &d)
141+
}
142+
}
143+
144+
func decodeYamlFile(filename string) (any, error) {
145+
file, err := os.Open(filename)
146+
if err != nil {
147+
return nil, fmt.Errorf("[%s] file open: %w", filename, err)
148+
}
149+
150+
defer func() { _ = file.Close() }()
151+
152+
var m any
153+
err = yaml.NewDecoder(file).Decode(&m)
154+
if err != nil {
155+
return nil, fmt.Errorf("[%s] YAML decode: %w", filename, err)
156+
}
157+
158+
return m, nil
159+
}
160+
161+
func decodeTomlFile(filename string) (any, error) {
162+
file, err := os.Open(filename)
163+
if err != nil {
164+
return nil, fmt.Errorf("[%s] file open: %w", filename, err)
165+
}
166+
167+
defer func() { _ = file.Close() }()
168+
169+
var m any
170+
err = toml.NewDecoder(file).Decode(&m)
171+
if err != nil {
172+
return nil, fmt.Errorf("[%s] TOML decode: %w", filename, err)
173+
}
174+
175+
return m, nil
176+
}

0 commit comments

Comments
 (0)