From 79b724e272af844696c5ad3cd6b8514ab417463c Mon Sep 17 00:00:00 2001 From: Ken'ichiro Oyama Date: Mon, 8 Sep 2025 19:31:33 +0900 Subject: [PATCH] feat: add configurable acceptable warning threshold --- README.md | 56 +++++++++++++++++++++---------------- cmd/lint.go | 8 ++++-- config/config.go | 5 ++++ tailor/helper_test.go | 64 ++++++++++++++++++++++--------------------- tailor/lint.go | 32 +++++++++++----------- tailor/lint_test.go | 42 ++++++++++++++-------------- 6 files changed, 113 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 6c0263f..57fddcd 100644 --- a/README.md +++ b/README.md @@ -87,31 +87,41 @@ Patterner uses a `.patterner.yml` file for configuration. The configuration incl ```yaml workspaceID: xxxxxxxXXxxxxxxxxxxxxx lint: - pipeline: - deprecatedFeature: - enabled: true - allowCELScript: false - allowDraft: false - allowStateFlow: false - insecureAuthorization: - enabled: false - stepLength: - enabled: true - max: 30 - multipleMutations: - enabled: true - queryBeforeMutation: - enabled: true - tailordb: - deprecatedFeature: - enabled: true - allowDraft: false - allowCELHooks: false - stateflow: - deprecatedFeature: - enabled: true + acceptable: 5 + rules: + pipeline: + deprecatedFeature: + enabled: true + allowCELScript: false + allowDraft: false + allowStateFlow: false + insecureAuthorization: + enabled: false + stepLength: + enabled: true + max: 30 + multipleMutations: + enabled: true + queryBeforeMutation: + enabled: true + tailordb: + deprecatedFeature: + enabled: true + allowDraft: false + allowCELHooks: false + stateflow: + deprecatedFeature: + enabled: true ``` +### Lint Configuration + +#### Acceptable Warnings +- **acceptable** - Set the maximum number of acceptable lint warnings (default: 0) + - When the number of warnings exceeds this value, the lint command will exit with a failure status + - This allows you to gradually improve code quality by setting a reasonable warning threshold + - Example: `acceptable: 5` allows up to 5 warnings before failing + ### Lint Rules #### Pipeline Rules diff --git a/cmd/lint.go b/cmd/lint.go index 73341d3..7147cf1 100644 --- a/cmd/lint.go +++ b/cmd/lint.go @@ -22,7 +22,6 @@ THE SOFTWARE. package cmd import ( - "errors" "fmt" "github.com/spf13/cobra" @@ -58,8 +57,11 @@ var lintCmd = &cobra.Command{ for _, w := range warns { fmt.Printf("[%s] %s: %s\n", w.Type, w.Name, w.Message) } - if len(warns) > 0 { - return errors.New("lint warnings found") + if len(warns) > cfg.Lint.Acceptable { + if cfg.Lint.Acceptable == 0 { + return fmt.Errorf("%d warnings found", len(warns)) + } + return fmt.Errorf("%d warnings found, which exceeds the acceptable number of %d", len(warns), cfg.Lint.Acceptable) } return nil }, diff --git a/config/config.go b/config/config.go index 20998c3..ba5ccf0 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,11 @@ type Config struct { } type Lint struct { + Acceptable int `default:"0" yaml:"acceptable,omitempty"` + Rules Rules `yaml:"rules,omitempty,omitzero"` +} + +type Rules struct { Pipeline Pipeline `yaml:"pipeline,omitempty,omitzero"` TailorDB TailorDB `yaml:"tailordb,omitempty,omitzero"` StateFlow StateFlow `yaml:"stateflow,omitempty,omitzero"` diff --git a/tailor/helper_test.go b/tailor/helper_test.go index 3cdf0eb..69095c5 100644 --- a/tailor/helper_test.go +++ b/tailor/helper_test.go @@ -12,39 +12,41 @@ func createTestConfig(t *testing.T) *config.Config { return &config.Config{ WorkspaceID: "test-workspace-id", Lint: config.Lint{ - TailorDB: config.TailorDB{ - DeprecatedFeature: config.TailorDBDeprecatedFeature{ - Enabled: true, - AllowDraft: false, - AllowCELHooks: false, - AllowTypePermission: false, - AllowRecordPermission: false, + Rules: config.Rules{ + TailorDB: config.TailorDB{ + DeprecatedFeature: config.TailorDBDeprecatedFeature{ + Enabled: true, + AllowDraft: false, + AllowCELHooks: false, + AllowTypePermission: false, + AllowRecordPermission: false, + }, }, - }, - Pipeline: config.Pipeline{ - InsecureAuthorization: config.InsecureAuthorization{ - Enabled: true, - }, - StepLength: config.StepLength{ - Enabled: true, - Max: 10, - }, - DeprecatedFeature: config.PipelineDeprecatedFeature{ - Enabled: true, - AllowStateFlow: false, - AllowDraft: false, - AllowCELScript: false, + Pipeline: config.Pipeline{ + InsecureAuthorization: config.InsecureAuthorization{ + Enabled: true, + }, + StepLength: config.StepLength{ + Enabled: true, + Max: 10, + }, + DeprecatedFeature: config.PipelineDeprecatedFeature{ + Enabled: true, + AllowStateFlow: false, + AllowDraft: false, + AllowCELScript: false, + }, + MultipleMutations: config.MultipleMutations{ + Enabled: true, + }, + QueryBeforeMutation: config.QueryBeforeMutation{ + Enabled: true, + }, }, - MultipleMutations: config.MultipleMutations{ - Enabled: true, - }, - QueryBeforeMutation: config.QueryBeforeMutation{ - Enabled: true, - }, - }, - StateFlow: config.StateFlow{ - DeprecatedFeature: config.StateFlowDeprecatedFeature{ - Enabled: true, + StateFlow: config.StateFlow{ + DeprecatedFeature: config.StateFlowDeprecatedFeature{ + Enabled: true, + }, }, }, }, diff --git a/tailor/lint.go b/tailor/lint.go index e293e3c..d9b009f 100644 --- a/tailor/lint.go +++ b/tailor/lint.go @@ -36,22 +36,22 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { for _, t := range db.Types { typeNames = append(typeNames, t.Name) - if c.cfg.Lint.TailorDB.DeprecatedFeature.Enabled { - if !c.cfg.Lint.TailorDB.DeprecatedFeature.AllowDraft && t.Draft { + if c.cfg.Lint.Rules.TailorDB.DeprecatedFeature.Enabled { + if !c.cfg.Lint.Rules.TailorDB.DeprecatedFeature.AllowDraft && t.Draft { warns = append(warns, &LintWarn{ Type: LintTargetTypeTailorDB, Name: fmt.Sprintf("%s/%s", db.NamespaceName, t.Name), Message: "Draft feature is deprecated", }) } - if !c.cfg.Lint.TailorDB.DeprecatedFeature.AllowTypePermission && t.TypePermission != nil { + if !c.cfg.Lint.Rules.TailorDB.DeprecatedFeature.AllowTypePermission && t.TypePermission != nil { warns = append(warns, &LintWarn{ Type: LintTargetTypeTailorDB, Name: fmt.Sprintf("%s/%s", db.NamespaceName, t.Name), Message: "Type-level permission is deprecated. Use `Permission` or `GQLPermission` instead", }) } - if !c.cfg.Lint.TailorDB.DeprecatedFeature.AllowRecordPermission && t.RecordPermission != nil { + if !c.cfg.Lint.Rules.TailorDB.DeprecatedFeature.AllowRecordPermission && t.RecordPermission != nil { warns = append(warns, &LintWarn{ Type: LintTargetTypeTailorDB, Name: fmt.Sprintf("%s/%s", db.NamespaceName, t.Name), @@ -60,7 +60,7 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { } } - if c.cfg.Lint.TailorDB.DeprecatedFeature.Enabled && !c.cfg.Lint.TailorDB.DeprecatedFeature.AllowCELHooks { + if c.cfg.Lint.Rules.TailorDB.DeprecatedFeature.Enabled && !c.cfg.Lint.Rules.TailorDB.DeprecatedFeature.AllowCELHooks { for _, f := range t.Fields { if f.Hooks.CreateExpr != "" || f.Hooks.UpdateExpr != "" { warns = append(warns, &LintWarn{ @@ -78,7 +78,7 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { for _, p := range resources.Pipelines { for _, r := range p.Resolvers { // Pipeline/InsecureAuthorization - if c.cfg.Lint.Pipeline.InsecureAuthorization.Enabled && (r.Authorization == "true" || r.Authorization == "true==true") { + if c.cfg.Lint.Rules.Pipeline.InsecureAuthorization.Enabled && (r.Authorization == "true" || r.Authorization == "true==true") { warns = append(warns, &LintWarn{ Type: LintTargetTypePipeline, Name: fmt.Sprintf("%s/%s", p.NamespaceName, r.Name), @@ -88,11 +88,11 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { stepLength := len(r.Steps) // Pipeline/StepLength - if c.cfg.Lint.Pipeline.StepLength.Enabled && stepLength > c.cfg.Lint.Pipeline.StepLength.Max { + if c.cfg.Lint.Rules.Pipeline.StepLength.Enabled && stepLength > c.cfg.Lint.Rules.Pipeline.StepLength.Max { warns = append(warns, &LintWarn{ Type: LintTargetTypePipeline, Name: fmt.Sprintf("%s/%s", p.NamespaceName, r.Name), - Message: fmt.Sprintf("resolver has too many steps (%d > %d)", stepLength, c.cfg.Lint.Pipeline.StepLength.Max), + Message: fmt.Sprintf("resolver has too many steps (%d > %d)", stepLength, c.cfg.Lint.Rules.Pipeline.StepLength.Max), }) } @@ -110,9 +110,9 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { for _, selection := range op.SelectionSet { switch sel := selection.(type) { case *ast.Field: - if c.cfg.Lint.Pipeline.DeprecatedFeature.Enabled { + if c.cfg.Lint.Rules.Pipeline.DeprecatedFeature.Enabled { // StateFlow - if !c.cfg.Lint.Pipeline.DeprecatedFeature.AllowStateFlow { + if !c.cfg.Lint.Rules.Pipeline.DeprecatedFeature.AllowStateFlow { if slices.Contains(stateFlowMutations, sel.Name) { warns = append(warns, &LintWarn{ Type: LintTargetTypePipeline, @@ -123,7 +123,7 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { } // Draft - if !c.cfg.Lint.Pipeline.DeprecatedFeature.AllowDraft { + if !c.cfg.Lint.Rules.Pipeline.DeprecatedFeature.AllowDraft { if replaced := draftMutationPrefixRe.ReplaceAllString(sel.Name, ""); replaced != sel.Name { if slices.Contains(typeNames, replaced) { warns = append(warns, &LintWarn{ @@ -139,7 +139,7 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { } } } - if c.cfg.Lint.Pipeline.DeprecatedFeature.Enabled && !c.cfg.Lint.Pipeline.DeprecatedFeature.AllowCELScript { + if c.cfg.Lint.Rules.Pipeline.DeprecatedFeature.Enabled && !c.cfg.Lint.Rules.Pipeline.DeprecatedFeature.AllowCELScript { if s.PreValidation != "" { warns = append(warns, &LintWarn{ Type: LintTargetTypePipeline, @@ -170,7 +170,7 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { } } } - if c.cfg.Lint.Pipeline.MultipleMutations.Enabled { + if c.cfg.Lint.Rules.Pipeline.MultipleMutations.Enabled { var count int for _, op := range operations { if op == "mutation" { @@ -185,7 +185,7 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { }) } } - if c.cfg.Lint.Pipeline.QueryBeforeMutation.Enabled { + if c.cfg.Lint.Rules.Pipeline.QueryBeforeMutation.Enabled { if slices.Contains(operations, "mutation") && slices.Contains(operations, "query") && slices.Index(operations, "mutation") > slices.Index(operations, "query") { warns = append(warns, &LintWarn{ Type: LintTargetTypePipeline, @@ -198,9 +198,9 @@ func (c *Client) Lint(resources *Resources) ([]*LintWarn, error) { } // StateFlow Linting - if c.cfg.Lint.StateFlow.DeprecatedFeature.Enabled { + if c.cfg.Lint.Rules.StateFlow.DeprecatedFeature.Enabled { for _, sf := range resources.StateFlows { - if !c.cfg.Lint.StateFlow.DeprecatedFeature.Enabled { + if !c.cfg.Lint.Rules.StateFlow.DeprecatedFeature.Enabled { continue } warns = append(warns, &LintWarn{ diff --git a/tailor/lint_test.go b/tailor/lint_test.go index 13899b2..cdbbe7f 100644 --- a/tailor/lint_test.go +++ b/tailor/lint_test.go @@ -105,8 +105,8 @@ func TestClient_Lint_TailorDB(t *testing.T) { { name: "draft feature warning", configMod: func(c *config.Config) { - c.Lint.TailorDB.DeprecatedFeature.Enabled = true - c.Lint.TailorDB.DeprecatedFeature.AllowDraft = false + c.Lint.Rules.TailorDB.DeprecatedFeature.Enabled = true + c.Lint.Rules.TailorDB.DeprecatedFeature.AllowDraft = false }, resources: &Resources{ TailorDBs: []*TailorDB{ @@ -126,8 +126,8 @@ func TestClient_Lint_TailorDB(t *testing.T) { { name: "legacy type permission warning", configMod: func(c *config.Config) { - c.Lint.TailorDB.DeprecatedFeature.Enabled = true - c.Lint.TailorDB.DeprecatedFeature.AllowTypePermission = false + c.Lint.Rules.TailorDB.DeprecatedFeature.Enabled = true + c.Lint.Rules.TailorDB.DeprecatedFeature.AllowTypePermission = false }, resources: &Resources{ TailorDBs: []*TailorDB{ @@ -147,8 +147,8 @@ func TestClient_Lint_TailorDB(t *testing.T) { { name: "legacy record permission warning", configMod: func(c *config.Config) { - c.Lint.TailorDB.DeprecatedFeature.Enabled = true - c.Lint.TailorDB.DeprecatedFeature.AllowRecordPermission = false + c.Lint.Rules.TailorDB.DeprecatedFeature.Enabled = true + c.Lint.Rules.TailorDB.DeprecatedFeature.AllowRecordPermission = false }, resources: &Resources{ TailorDBs: []*TailorDB{ @@ -168,8 +168,8 @@ func TestClient_Lint_TailorDB(t *testing.T) { { name: "CEL hooks deprecated warning", configMod: func(c *config.Config) { - c.Lint.TailorDB.DeprecatedFeature.Enabled = true - c.Lint.TailorDB.DeprecatedFeature.AllowCELHooks = false + c.Lint.Rules.TailorDB.DeprecatedFeature.Enabled = true + c.Lint.Rules.TailorDB.DeprecatedFeature.AllowCELHooks = false }, resources: &Resources{ TailorDBs: []*TailorDB{ @@ -240,7 +240,7 @@ func TestClient_Lint_Pipeline(t *testing.T) { { name: "insecure authorization warning", configMod: func(c *config.Config) { - c.Lint.Pipeline.InsecureAuthorization.Enabled = true + c.Lint.Rules.Pipeline.InsecureAuthorization.Enabled = true }, resources: &Resources{ Pipelines: []*Pipeline{ @@ -260,8 +260,8 @@ func TestClient_Lint_Pipeline(t *testing.T) { { name: "step length warning", configMod: func(c *config.Config) { - c.Lint.Pipeline.StepLength.Enabled = true - c.Lint.Pipeline.StepLength.Max = 1 + c.Lint.Rules.Pipeline.StepLength.Enabled = true + c.Lint.Rules.Pipeline.StepLength.Max = 1 }, resources: &Resources{ Pipelines: []*Pipeline{ @@ -284,8 +284,8 @@ func TestClient_Lint_Pipeline(t *testing.T) { { name: "legacy script warnings", configMod: func(c *config.Config) { - c.Lint.Pipeline.DeprecatedFeature.Enabled = true - c.Lint.Pipeline.DeprecatedFeature.AllowCELScript = false + c.Lint.Rules.Pipeline.DeprecatedFeature.Enabled = true + c.Lint.Rules.Pipeline.DeprecatedFeature.AllowCELScript = false }, resources: &Resources{ Pipelines: []*Pipeline{ @@ -318,7 +318,7 @@ func TestClient_Lint_Pipeline(t *testing.T) { { name: "invalid GraphQL syntax error", configMod: func(c *config.Config) { - c.Lint.Pipeline.DeprecatedFeature.Enabled = true + c.Lint.Rules.Pipeline.DeprecatedFeature.Enabled = true }, resources: &Resources{ Pipelines: []*Pipeline{ @@ -414,8 +414,8 @@ func TestClient_Lint_GraphQLParsing(t *testing.T) { name: "StateFlow mutation warning", graphqlQuery: "mutation { newState(input: {status: \"active\"}) { id } }", configMod: func(c *config.Config) { - c.Lint.Pipeline.DeprecatedFeature.Enabled = true - c.Lint.Pipeline.DeprecatedFeature.AllowStateFlow = false + c.Lint.Rules.Pipeline.DeprecatedFeature.Enabled = true + c.Lint.Rules.Pipeline.DeprecatedFeature.AllowStateFlow = false }, expectedMsgs: []string{"StateFlow feature is deprecated (found usage of newState)"}, }, @@ -424,8 +424,8 @@ func TestClient_Lint_GraphQLParsing(t *testing.T) { graphqlQuery: "mutation { appendDraftUser(input: {name: \"test\"}) { id } }", typeNames: []string{"User"}, configMod: func(c *config.Config) { - c.Lint.Pipeline.DeprecatedFeature.Enabled = true - c.Lint.Pipeline.DeprecatedFeature.AllowDraft = false + c.Lint.Rules.Pipeline.DeprecatedFeature.Enabled = true + c.Lint.Rules.Pipeline.DeprecatedFeature.AllowDraft = false }, expectedMsgs: []string{"Draft feature is deprecated (found usage of appendDraftUser)"}, }, @@ -509,7 +509,7 @@ func TestClient_Lint_GraphQLParsing(t *testing.T) { func TestClient_Lint_StateFlow(t *testing.T) { cfg := createTestConfig(t) - cfg.Lint.StateFlow.DeprecatedFeature.Enabled = true + cfg.Lint.Rules.StateFlow.DeprecatedFeature.Enabled = true resources := &Resources{ StateFlows: []*StateFlow{ @@ -545,7 +545,7 @@ func TestClient_Lint_StateFlow(t *testing.T) { func TestClient_Lint_MultipleMutations(t *testing.T) { cfg := createTestConfig(t) - cfg.Lint.Pipeline.MultipleMutations.Enabled = true + cfg.Lint.Rules.Pipeline.MultipleMutations.Enabled = true resources := &Resources{ Pipelines: []*Pipeline{ @@ -599,7 +599,7 @@ func TestClient_Lint_MultipleMutations(t *testing.T) { func TestClient_Lint_QueryBeforeMutation(t *testing.T) { cfg := createTestConfig(t) - cfg.Lint.Pipeline.QueryBeforeMutation.Enabled = true + cfg.Lint.Rules.Pipeline.QueryBeforeMutation.Enabled = true resources := &Resources{ Pipelines: []*Pipeline{