Skip to content

Commit ccde16a

Browse files
committed
feat(tooling): add ReSharper staged-files gate, expand audit umbrella
- `npm run audit` now includes the ReSharper inspectcode pass and takes several minutes; use the per-tool scripts (`audit:fallow`, `audit:dupes`, `audit:resharper`) for fast iteration.
1 parent 32f9962 commit ccde16a

4 files changed

Lines changed: 48 additions & 13 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,24 @@ The project includes VS Code launch configurations for debugging both frontend a
179179
- `npm run lint:staged` - Lint only git-staged files
180180
- `npm run precommit` - Run full pre-commit checks manually (lint, test, build verify)
181181

182+
### Auditing (regression detection)
183+
184+
Heavier whole-project scanners that surface dead code, duplication, and C# inspection findings that the per-file linter doesn't catch. Outputs land in gitignored report directories (`.fallow/`, `jscpd-report/`, `inspect-report/`).
185+
186+
**Whole-project scans** — run locally for human review:
187+
188+
- `npm run audit` - Run all whole-project audits (fallow + jscpd + ReSharper); takes several minutes due to the ReSharper scan
189+
- `npm run audit:fallow` - Vue/TS dead code, unused exports, complexity, duplication (`VueApp/`)
190+
- `npm run audit:dupes` - jscpd duplicate-code report across `VueApp/src/` and `web/Areas/`
191+
- `npm run audit:resharper` - ReSharper `inspectcode` whole-solution scan (writes SARIF + HTML to `inspect-report/`); takes several minutes
192+
193+
**Diff-scoped gates** — fail only on *new* findings at touched lines, so pre-existing issues never block:
194+
195+
- `npm run audit:resharper:pr` - C# inspection gate vs `origin/main`. Same gate the `Code Quality` GitHub Actions workflow runs on PRs.
196+
- `npm run audit:resharper-staged` - C# inspection gate vs the git index (`git diff --cached`). Useful as a manual pre-commit check. For fast iteration after one full scan, append `-- --skip-scan` to reuse the existing SARIF: `npm run audit:resharper-staged -- --skip-scan`.
197+
198+
**CI integration**`.github/workflows/code-quality.yml` runs four gates on every PR to `main`: `fallow-regression` (Vue), `jscpd-regression-csharp`, `jscpd-regression-vue`, and `resharper-pr-gate` (C# inspections). All four are diff-scoped so existing technical debt doesn't block merges.
199+
182200
### Build Cache
183201

184202
The linter, test, and build verification scripts use caching to avoid redundant rebuilds. If you encounter stale cache issues (e.g., linter showing warnings for already-fixed code), clear the cache:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"audit:dupes": "node scripts/audit-jscpd.js",
2525
"audit:resharper": "node scripts/audit-resharper.js",
2626
"audit:resharper:pr": "node scripts/audit-resharper-regression.js",
27+
"audit:resharper-staged": "node scripts/audit-resharper-regression.js --staged",
2728
"mailpit:start": "node --env-file-if-exists=.env.local scripts/manage-mailpit.js start",
2829
"mailpit:status": "node --env-file-if-exists=.env.local scripts/manage-mailpit.js status",
2930
"mailpit:stop": "node --env-file-if-exists=.env.local scripts/manage-mailpit.js stop",

scripts/audit-resharper-regression.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@
1111
// node scripts/audit-resharper-regression.js [--base origin/main]
1212
// [--sarif inspect-report/inspect.sarif]
1313
// [--skip-scan]
14+
// [--staged]
1415
//
1516
// --skip-scan reuses the SARIF from a prior `audit:resharper` run, useful in
1617
// CI where the full scan and the gate are split into separate steps so the
1718
// expensive scan output can be uploaded as an artifact.
19+
//
20+
// --staged filters findings to staged C# lines (`git diff --cached`) instead of
21+
// PR-diff lines. Pair with --skip-scan for fast iterative pre-commit checks
22+
// against an existing SARIF report.
1823

1924
const fs = require("node:fs")
2025
const path = require("node:path")
@@ -30,7 +35,7 @@ const MAX_BUFFER_BYTES = 268_435_456
3035
const MAX_FINDINGS_PER_RULE = 5
3136

3237
function parseArgs(argv) {
33-
const args = { base: "origin/main", sarif: DEFAULT_SARIF, skipScan: false }
38+
const args = { base: "origin/main", sarif: DEFAULT_SARIF, skipScan: false, staged: false }
3439
const remaining = [...argv]
3540
while (remaining.length > 0) {
3641
const flag = remaining.shift()
@@ -40,6 +45,8 @@ function parseArgs(argv) {
4045
args.sarif = remaining.shift()
4146
} else if (flag === "--skip-scan") {
4247
args.skipScan = true
48+
} else if (flag === "--staged") {
49+
args.staged = true
4350
} else {
4451
console.error(`Unknown arg: ${flag}`)
4552
process.exit(2)
@@ -49,9 +56,10 @@ function parseArgs(argv) {
4956
}
5057

5158
// Returns Map<repoRelativePath, Set<lineNumber>> of lines added/modified in
52-
// the PR, derived from `git diff --unified=0 base...HEAD`.
53-
function getChangedLines(baseRef) {
54-
const result = spawnSync("git", ["diff", "--unified=0", `${baseRef}...HEAD`, "--", "*.cs"], {
59+
// the diff selection: PR-mode uses `git diff base...HEAD`; staged-mode uses
60+
// `git diff --cached` (staged-vs-HEAD).
61+
function getChangedLines(diffSelector) {
62+
const result = spawnSync("git", ["diff", "--unified=0", ...diffSelector, "--", "*.cs"], {
5563
cwd: PROJECT_ROOT,
5664
encoding: "utf8",
5765
windowsHide: true,
@@ -164,20 +172,27 @@ if (!fs.existsSync(args.sarif)) {
164172
process.exit(1)
165173
}
166174

167-
const changed = getChangedLines(args.base)
168-
console.log(`Comparing against base ${args.base}: ${changed.size} C# file(s) with added/modified lines`)
175+
const diffSelector = args.staged ? ["--cached"] : [`${args.base}...HEAD`]
176+
const diffLabel = args.staged ? "staged C# changes" : `base ${args.base}`
177+
const noChangesMsg = args.staged
178+
? "✅ No staged C# changes; nothing to gate."
179+
: "✅ No C# changes in this PR; nothing to gate."
180+
const touchedLabel = args.staged ? "staged" : "PR-touched"
181+
182+
const changed = getChangedLines(diffSelector)
183+
console.log(`Comparing against ${diffLabel}: ${changed.size} C# file(s) with added/modified lines`)
169184
if (changed.size === 0) {
170-
console.log("✅ No C# changes in this PR; nothing to gate.")
185+
console.log(noChangesMsg)
171186
process.exit(0)
172187
}
173188

174189
const regressions = findRegressions(args.sarif, changed)
175190
if (regressions.length === 0) {
176-
console.log("✅ No new ReSharper warnings at PR-touched lines.")
191+
console.log(`✅ No new ReSharper warnings at ${touchedLabel} lines.`)
177192
process.exit(0)
178193
}
179194

180-
console.error(`\n❌ ${regressions.length} new ReSharper warning(s) at PR-touched lines:\n`)
195+
console.error(`\n❌ ${regressions.length} new ReSharper warning(s) at ${touchedLabel} lines:\n`)
181196
const byRule = new Map()
182197
for (const r of regressions) {
183198
if (!byRule.has(r.ruleId)) {

scripts/audit.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

3-
// `npm run audit` — run both fallow and jscpd whole-project audits.
3+
// `npm run audit` — run all whole-project audits (fallow, jscpd, ReSharper).
4+
// Note: ReSharper inspectcode adds several minutes to the run.
45

56
const path = require("node:path")
67
const { spawnSync } = require("node:child_process")
@@ -16,9 +17,9 @@ function runNode(script) {
1617
return result.status ?? 1
1718
}
1819

19-
// Run both audits unconditionally so a failure in the first doesn't hide the
20-
// second's findings; aggregate exit codes at the end.
21-
const codes = ["audit-fallow.js", "audit-jscpd.js"].map(runNode)
20+
// Run all audits unconditionally so a failure in one doesn't hide the
21+
// others' findings; aggregate exit codes at the end.
22+
const codes = ["audit-fallow.js", "audit-jscpd.js", "audit-resharper.js"].map(runNode)
2223
const max = Math.max(0, ...codes)
2324
if (max !== 0) {
2425
process.exit(max)

0 commit comments

Comments
 (0)