Skip to content

Commit 59ef754

Browse files
committed
Add --rules CLI option to run specific rules ignoring .editorconfig
Accepts comma-separated rule IDs (e.g., --rules CSLINT266,CSLINT268) or 'all' to enable every rule. Validates rule IDs and exits with error on unknown IDs. 🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent af207c9 commit 59ef754

File tree

3 files changed

+122
-2
lines changed

3 files changed

+122
-2
lines changed

src/CsLint.Cli/Program.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
Description = "Show a summary of diagnostics grouped by rule ID",
5151
};
5252

53+
var rulesOption = new Option<string>("--rules")
54+
{
55+
Description = "Comma-separated rule IDs to run (e.g., CSLINT266,CSLINT268), or 'all' to enable every rule. Ignores .editorconfig when set.",
56+
};
57+
5358
var versionOption = new Option<bool>("--version")
5459
{
5560
Description = "Show version information and exit",
@@ -65,6 +70,7 @@
6570
showConfigOption,
6671
semanticOption,
6772
summaryOption,
73+
rulesOption,
6874
versionOption,
6975
};
7076

@@ -131,9 +137,50 @@
131137
#endif
132138

133139
RuleRegistry registry = RuleRegistry.CreateDefault();
140+
string? rulesValue = parseResult.GetValue(rulesOption);
141+
142+
HashSet<string>? ruleFilter = null;
143+
bool skipEnabledCheck = false;
144+
145+
if (rulesValue is not null)
146+
{
147+
if (string.Equals(rulesValue, "all", StringComparison.OrdinalIgnoreCase))
148+
{
149+
skipEnabledCheck = true;
150+
}
151+
else
152+
{
153+
var knownIds = new HashSet<string>(registry.Rules.Select(r => r.RuleId), StringComparer.OrdinalIgnoreCase);
154+
string[] requested = rulesValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
155+
156+
if (requested.Length == 0)
157+
{
158+
Console.Error.WriteLine("--rules requires at least one rule ID or 'all'.");
159+
return 2;
160+
}
161+
162+
ruleFilter = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
163+
164+
foreach (string id in requested)
165+
{
166+
if (!knownIds.Contains(id))
167+
{
168+
Console.Error.WriteLine($"Unknown rule ID: {id}");
169+
return 2;
170+
}
171+
172+
ruleFilter.Add(id);
173+
}
174+
175+
skipEnabledCheck = true;
176+
}
177+
}
178+
134179
var configProvider = new EditorConfigProvider();
135180
var fileLinter = new FileLinter(registry, configProvider)
136181
{
182+
RuleFilter = ruleFilter,
183+
SkipEnabledCheck = skipEnabledCheck,
137184
#if SEMANTIC
138185
EnableSemantic = semantic,
139186
#endif

src/CsLint.Core/Engine/FileLinter.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ namespace Cslint.Core.Engine;
1313

1414
public sealed class FileLinter(RuleRegistry registry, IConfigProvider configProvider)
1515
{
16+
public HashSet<string>? RuleFilter { get; set; }
17+
18+
public bool SkipEnabledCheck { get; set; }
19+
1620
#if SEMANTIC
1721
public bool EnableSemantic { get; set; }
1822

@@ -135,7 +139,12 @@ public IReadOnlyList<LintDiagnostic> LintSource(
135139

136140
foreach (IRuleDefinition rule in registry.Rules)
137141
{
138-
if (!rule.IsEnabled(configuration))
142+
if (RuleFilter is not null && !RuleFilter.Contains(rule.RuleId))
143+
{
144+
continue;
145+
}
146+
147+
if (!SkipEnabledCheck && !rule.IsEnabled(configuration))
139148
{
140149
continue;
141150
}

test/CsLint.Core.Tests/Engine/FileLinterTests.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace Cslint.Core.Tests.Engine;
77

8-
public class FileLinterTests
8+
public sealed class FileLinterTests
99
{
1010
[Fact]
1111
public void LintSource_WithEnabledRule_ReturnsLintDiagnostics()
@@ -171,4 +171,68 @@ public void LintSource_TrueWithSeveritySuffix_RuleIsEnabled()
171171
Assert.NotEmpty(diagnostics);
172172
Assert.Contains(diagnostics, d => d.RuleId == "CSLINT001");
173173
}
174+
175+
[Fact]
176+
public void LintSource_RuleFilter_OnlyRunsSpecifiedRules()
177+
{
178+
// Enable two rules via config, but filter to only one
179+
var config = new LintConfiguration(
180+
new Dictionary<string, string>
181+
{
182+
["trim_trailing_whitespace"] = "true",
183+
["insert_final_newline"] = "true",
184+
});
185+
186+
RuleRegistry registry = RuleRegistry.CreateDefault();
187+
var mockProvider = new Mock<IConfigProvider>();
188+
var linter = new FileLinter(registry, mockProvider.Object)
189+
{
190+
RuleFilter = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "CSLINT001" },
191+
SkipEnabledCheck = true,
192+
};
193+
194+
// Source has trailing whitespace (CSLINT001) and no final newline (CSLINT002)
195+
IReadOnlyList<LintDiagnostic> diagnostics = linter.LintSource("test.cs", "class Foo { } ", config);
196+
197+
Assert.Contains(diagnostics, d => d.RuleId == "CSLINT001");
198+
Assert.DoesNotContain(diagnostics, d => d.RuleId == "CSLINT002");
199+
}
200+
201+
[Fact]
202+
public void LintSource_SkipEnabledCheck_RunsRuleEvenWhenConfigDisabled()
203+
{
204+
// Config does not enable the rule
205+
var config = new LintConfiguration(new Dictionary<string, string>());
206+
207+
RuleRegistry registry = RuleRegistry.CreateDefault();
208+
var mockProvider = new Mock<IConfigProvider>();
209+
var linter = new FileLinter(registry, mockProvider.Object)
210+
{
211+
RuleFilter = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "CSLINT001" },
212+
SkipEnabledCheck = true,
213+
};
214+
215+
IReadOnlyList<LintDiagnostic> diagnostics = linter.LintSource("test.cs", "class Foo { } \n", config);
216+
217+
Assert.Contains(diagnostics, d => d.RuleId == "CSLINT001");
218+
}
219+
220+
[Fact]
221+
public void LintSource_SkipEnabledCheckWithoutFilter_RunsAllRules()
222+
{
223+
// Empty config — normally no Tier1 rules would be enabled
224+
var config = new LintConfiguration(new Dictionary<string, string>());
225+
226+
RuleRegistry registry = RuleRegistry.CreateDefault();
227+
var mockProvider = new Mock<IConfigProvider>();
228+
var linter = new FileLinter(registry, mockProvider.Object)
229+
{
230+
SkipEnabledCheck = true,
231+
};
232+
233+
IReadOnlyList<LintDiagnostic> diagnostics = linter.LintSource("test.cs", "class Foo { } \n", config);
234+
235+
// With all rules force-enabled, we should get trailing whitespace at minimum
236+
Assert.Contains(diagnostics, d => d.RuleId == "CSLINT001");
237+
}
174238
}

0 commit comments

Comments
 (0)