1212// [--sarif inspect-report/inspect.sarif]
1313// [--skip-scan]
1414// [--staged]
15+ // [--exclude-rule <id> ...]
1516//
1617// --skip-scan reuses the SARIF from a prior `audit:resharper` run, useful in
1718// CI where the full scan and the gate are split into separate steps so the
2021// --staged filters findings to staged C# lines (`git diff --cached`) instead of
2122// PR-diff lines. Pair with --skip-scan for fast iterative pre-commit checks
2223// against an existing SARIF report.
24+ //
25+ // --exclude-rule adds a ReSharper rule id to skip (repeatable). The default
26+ // list covers rules known to misfire on ASP.NET Core / EF DTO patterns where
27+ // public surface looks unused to static analysis but is wired up at runtime
28+ // (JSON serialization, MVC model binding, Razor views, EF projections).
2329
2430const fs = require ( "node:fs" )
2531const path = require ( "node:path" )
@@ -34,8 +40,30 @@ const MAX_BUFFER_BYTES = 268_435_456
3440// Cap how many findings we print per rule before summarising the rest.
3541const MAX_FINDINGS_PER_RULE = 5
3642
43+ // Rules excluded by default because they fire false positives on the kinds of
44+ // public surface ASP.NET Core / EF wires up at runtime (DTO/binding/EF
45+ // projection types) or where ReSharper's NRT contract analysis disagrees
46+ // with Roslyn's flow analysis (EF nav-property dereferences after `?.`).
47+ const DEFAULT_EXCLUDED_RULES = new Set ( [
48+ "UnusedAutoPropertyAccessor.Global" ,
49+ "UnusedAutoPropertyAccessor.Local" ,
50+ "NotAccessedPositionalProperty.Local" ,
51+ "S3260" , // SonarLint sealed-record rule, low actionable value here
52+ // ReSharper trusts the NRT annotation on EF nav properties (`Rotation` is
53+ // declared non-null with `null!` default), but Roslyn rightly insists on
54+ // `?.` because the runtime can produce null when Include() is missing.
55+ // Keep the runtime-safe `?.Nav?.Member` style and silence the ReSharper rule.
56+ "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract" ,
57+ ] )
58+
3759function parseArgs ( argv ) {
38- const args = { base : "origin/main" , sarif : DEFAULT_SARIF , skipScan : false , staged : false }
60+ const args = {
61+ base : "origin/main" ,
62+ sarif : DEFAULT_SARIF ,
63+ skipScan : false ,
64+ staged : false ,
65+ excludedRules : new Set ( DEFAULT_EXCLUDED_RULES ) ,
66+ }
3967 const remaining = [ ...argv ]
4068 while ( remaining . length > 0 ) {
4169 const flag = remaining . shift ( )
@@ -47,6 +75,8 @@ function parseArgs(argv) {
4775 args . skipScan = true
4876 } else if ( flag === "--staged" ) {
4977 args . staged = true
78+ } else if ( flag === "--exclude-rule" ) {
79+ args . excludedRules . add ( remaining . shift ( ) )
5080 } else {
5181 console . error ( `Unknown arg: ${ flag } ` )
5282 process . exit ( 2 )
@@ -130,7 +160,7 @@ function normalizeUri(uri) {
130160 return s
131161}
132162
133- function findRegressions ( sarifPath , changedLines ) {
163+ function findRegressions ( sarifPath , changedLines , excludedRules ) {
134164 const sarif = JSON . parse ( fs . readFileSync ( sarifPath , "utf8" ) )
135165 const results = sarif . runs ?. [ 0 ] ?. results ?? [ ]
136166
@@ -142,6 +172,9 @@ function findRegressions(sarifPath, changedLines) {
142172 const regressions = [ ]
143173 for ( const r of results ) {
144174 const ruleId = r . ruleId ?? "?"
175+ if ( excludedRules . has ( ruleId ) ) {
176+ continue
177+ }
145178 for ( const loc of r . locations ?? [ ] ) {
146179 const uri = loc . physicalLocation ?. artifactLocation ?. uri
147180 const line = loc . physicalLocation ?. region ?. startLine
@@ -186,7 +219,11 @@ if (changed.size === 0) {
186219 process . exit ( 0 )
187220}
188221
189- const regressions = findRegressions ( args . sarif , changed )
222+ if ( args . excludedRules . size > 0 ) {
223+ const sortedRules = [ ...args . excludedRules ] . toSorted ( )
224+ console . log ( `Excluding ${ sortedRules . length } rule(s) from gate: ${ sortedRules . join ( ", " ) } ` )
225+ }
226+ const regressions = findRegressions ( args . sarif , changed , args . excludedRules )
190227if ( regressions . length === 0 ) {
191228 console . log ( `✅ No new ReSharper warnings at ${ touchedLabel } lines.` )
192229 process . exit ( 0 )
0 commit comments