diff --git a/CLAUDE.md b/CLAUDE.md index bfed098..88fe1ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ The linter follows a pipeline architecture: - `golangci-lint` - Wrapper for golangci-lint tool - `simplify-slice-range` - Detects unnecessary slice length in range loops - `unnecessary-type-conversion` - Finds redundant type conversions -- `cycle-detection` - Detects cyclic dependencies +- `cycle-detection` - Detects cyclic dependencies (opt-in: enable via `.tlin.yaml`) - `emit-format` - Checks gno emit statement formatting - `useless-break` - Finds unnecessary break statements - `early-return-opportunity` - Suggests early returns to reduce nesting diff --git a/internal/engine_test.go b/internal/engine_test.go index b84b003..ef49869 100644 --- a/internal/engine_test.go +++ b/internal/engine_test.go @@ -292,6 +292,8 @@ func BenchmarkRun(b *testing.B) { // - high-cyclomatic-complexity: not wired into the default Engine // (uses the lint.ProcessCyclomaticComplexity special path; covered // by TestDetectHighCyclomaticComplexity). +// - cycle-detection: opt-in (DefaultSeverity is SeverityOff); +// covered by TestRulesFireOnTestdata_OptIn. func TestRulesFireOnTestdata(t *testing.T) { t.Parallel() _, currentFile, _, ok := runtime.Caller(0) @@ -304,7 +306,6 @@ func TestRulesFireOnTestdata(t *testing.T) { }{ {"simplify-slice-range", "slice0.gno"}, {"unnecessary-type-conversion", "coversion/conv0.gno"}, - {"cycle-detection", "cycle0.gno"}, {"emit-format", "emit/emit1.gno"}, {"useless-break", "break/break1.gno"}, {"early-return-opportunity", "early_return/a0.gno"}, @@ -339,6 +340,46 @@ func TestRulesFireOnTestdata(t *testing.T) { } } +// TestRulesFireOnTestdata_OptIn is the opt-in counterpart to +// TestRulesFireOnTestdata for rules whose DefaultSeverity is SeverityOff. +func TestRulesFireOnTestdata_OptIn(t *testing.T) { + t.Parallel() + _, currentFile, _, ok := runtime.Caller(0) + require.True(t, ok) + testDataDir := filepath.Join(filepath.Dir(currentFile), "../testdata") + + cases := []struct { + rule string + file string // relative to testDataDir + }{ + {"cycle-detection", "cycle0.gno"}, + } + + for _, tc := range cases { + t.Run(tc.rule, func(t *testing.T) { + t.Parallel() + engine, err := NewEngine(map[string]types.ConfigRule{ + tc.rule: {Severity: types.SeverityWarning}, + }) + require.NoError(t, err) + + path := filepath.Join(testDataDir, tc.file) + issues, err := engine.Run(path) + require.NoError(t, err) + + rules := make([]string, 0, len(issues)) + for _, issue := range issues { + if issue.Rule == tc.rule { + return + } + rules = append(rules, issue.Rule) + } + t.Fatalf("rule %q did not fire on %s when enabled via config; rules that fired: %v", + tc.rule, tc.file, rules) + }) + } +} + // TestConfigSeverityReachesIssues is the integration anchor for the // severity-resolution chain: // diff --git a/internal/lints/detect_cycles.go b/internal/lints/detect_cycles.go index 0fc1593..0f8fa07 100644 --- a/internal/lints/detect_cycles.go +++ b/internal/lints/detect_cycles.go @@ -15,8 +15,10 @@ func init() { type cycleDetectionRule struct{} -func (cycleDetectionRule) Name() string { return "cycle-detection" } -func (cycleDetectionRule) DefaultSeverity() tt.Severity { return tt.SeverityError } +func (cycleDetectionRule) Name() string { return "cycle-detection" } + +// SeverityOff: AST-only call-graph flags bounded mutual recursion as a cycle; opt in via .tlin.yaml. +func (cycleDetectionRule) DefaultSeverity() tt.Severity { return tt.SeverityOff } func (cycleDetectionRule) Check(ctx *rule.AnalysisContext) ([]tt.Issue, error) { return DetectCycle(ctx)