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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 42 additions & 1 deletion internal/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"},
Expand Down Expand Up @@ -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:
//
Expand Down
6 changes: 4 additions & 2 deletions internal/lints/detect_cycles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading