feat(react-doctor): multi-line JSX & stacked suppressions, per-file rule overrides, near-miss hints#165
Merged
Conversation
…verrides, near-miss hints Refactors inline-suppression matching into small composable utilities and extends its semantics to cover three reported gaps: - #158 multi-line JSX: `react-doctor-disable-next-line` immediately above a multi-line JSX opening tag now suppresses diagnostics on attribute lines inside that opener (matching the ESLint convention). - #159 stacked comments: walking the chain of consecutive disable-next-line comments above the diagnostic now applies all of them, not just the immediately-adjacent one. - #160 per-file rule overrides: `ignore.overrides: [{ files, rules }]` scopes specific rules to specific globs without forcing whole-file ignores. - #161 near-miss hints + `why` subcommand: when a disable comment exists nearby but didn't apply, an explanatory hint is attached to the diagnostic (verbose / JSON / `react-doctor why <file:line>`). Each utility lives in its own file: find-jsx-opener-span, find-enclosing-jsx-opener, find-stacked-disable-comments, is-rule-suppressed-at, classify-suppression-near-miss, apply-ignore-overrides, to-relative-path, is-rule-listed-in-comment, parse-file-line-argument. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…near-miss Adds: - find-jsx-opener-span tests: single-line, multi-line, brace/string awareness, =>/>= operator skipping, lookahead cap, closing tag rejection. - inline-suppressions regressions: 5 multi-line JSX cases (#158), 3 stacked comment cases (#159), and the JSX-comment-inside-opener case (#144 form preserved). - classify-suppression-near-miss tests: wrong-rule, gap-code, JSX-anchor near-miss, and active-suppression silence. - filter-diagnostics overrides tests: scoped per-rule, no-rule-list, multi-entry. - parse-file-line-argument tests: standard parse, Windows drive letters, validation errors. Also: README docs for #158 (multi-line JSX), #159 (comma form prominence), #160 (ignore.overrides), and #161 (react-doctor why subcommand and suppressionHint diagnostic field). And restored the previous logger-silent state in runWhy via the existing scan() silent option instead of toggling the module-level flag manually. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Removes function-level / interface-level explanation comments from the new suppression utilities. Function names, type signatures, and the dedicated test files document the behavior; narrative prose violates 'never comment unless absolutely necessary'. HACK comments on heuristic caps in constants.ts are kept and trimmed to two lines each (matching surrounding style). Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
ESLint's idiom for diagnostic introspection is a flag with a required path argument (`--print-config <file>`, `--inspect-config`, `--debug`); it has no subcommand for this kind of question. The original issue (#161) also wrote it as `--why <file:line>`. Subcommand-form `react-doctor why` was divergent on both axes — package-manager idiom (pnpm/npm/yarn `why`) bled into a lint tool where it doesn't belong, and `react-doctor diagnose` would have collided with the existing `diagnose()` API export anyway. Move `--why <file:line>` to a top-level flag, mode-exclusive with `--json`, `--score`, `--annotations`, and `--staged` (consistent with the existing mutual-exclusion checks for output modes). The targeted report output is unchanged. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…s hidden alias
`rustc --explain <error-code>` is the closest CLI precedent for this
operation: a flag with a target argument that prints explanatory info
about why the tool reported what it did. `--explain` reads more clearly
than `--why` in --help output ("explain this site" vs. "why this site")
and matches the verb-shape ESLint already uses for diagnostic flags.
`--why` is preserved as a hidden alias (via `Option.hideHelp()`) so the
issue's vocabulary still works for anyone who already typed it. Passing
both flags at once is rejected with a clear message.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
cursor Bot
pushed a commit
that referenced
this pull request
May 8, 2026
…idation, --explain monorepo, JSX generics, line-comment skip, single-pass evaluator (#166) Follow-up to #165. Addresses everything flagged during the post-merge audit. 1. Audit-mode asymmetry. `respectInlineDisables: false` was documented as neutralizing every inline suppression, but `mergeAndFilterDiagnostics` ran `filterInlineSuppressions` unconditionally, so `// react-doctor-disable*` comments still hid diagnostics in audit mode. Thread the option through `combineDiagnostics → mergeAndFilterDiagnostics` and skip the inline filter when false. Config-level `ignore.rules`, `ignore.files`, and `ignore.overrides` stay honored, matching the existing `.oxlintignore` carve-out. 2. `ignore.overrides` config-shape footgun. Malformed entries (e.g. `rules: "react/no-danger"` instead of an array) silently became "ignore everything for these files". Validate each entry, emit stderr warnings (mirroring `validate-config-types`), preserve `files` while dropping malformed `rules`. 3. `--explain <file:line>` monorepo + ergonomics. Previously crashed in monorepos and silently ignored `--project` / `--no-lint` / `--no-dead-code`. New `findOwningProjectDirectory` walks workspace packages and picks the longest-prefix match for the requested file; `--project` is honored when set; resolved `ScanOptions` propagate. Output is severity-aware (warnings yellow, errors red) and shows the diagnostic's `category`. 4. TypeScript generic JSX components. `<List<Item>` followed by a multi-line attribute list is now correctly recognized via an `innerAngleDepth` counter — each `<` followed by an alpha char at brace depth 0 increments, each matching `>` decrements, the OUTER `>` closes the opener. Nested generic constraints (`<Form<Schema<Item>>>`) work too. 5. Line-comment skipping in the JSX opener scanner. `isOpenerMatchInsideLineComment` walks the line up to the regex match position (string-aware) and rejects matches that come after `//`, eliminating a false-positive class where a `disable-next-line` above a `// docs example: <Foo>` line could extend coverage to a real diagnostic below. Block-comment edge case remains a documented limitation. 6. Single-pass suppression evaluator. `filterInlineSuppressions` used to call both `isRuleSuppressedAt` and `classifySuppressionNearMiss` for every surviving diagnostic, each independently looking up `findEnclosingMultilineJsxOpenerStart` and `findStackedDisableCommentsAbove`. New `evaluate-suppression.ts` consolidates both into one pass; the original utility files remain as thin one-line wrappers so external test imports keep working. 7. README polish. `--explain` section calls out monorepo auto-resolution, the `--project` override, and steers users to `--verbose` when they want hints across many sites. `respectInlineDisables` documentation now lists `// react-doctor-disable*` alongside the eslint / oxlint forms in both prose and the config table. Cleanup. `findOpenerTagOnLine` now uses `String.prototype.matchAll` instead of `exec()` against a stateful module-scope `g`-flag regex. `findChainSuppressor` was using `.find()` to retrieve an entry but the caller only checked existence — renamed to `hasChainSuppressor` and switched to `.some()`. Tests: 17 new (527 total) - find-jsx-opener-span: 7 added (line-comment containing fake `<Tag`, single-generic component, nested generic constraints, generic + self-close, generic + immediate close, string-attribute angle, code-after-//-on-same-line) - inline-suppressions regressions: 2 added (line-comment fake-opener doesn't cause a false-positive suppression; generic-typed JSX component attribute-line suppression works end-to-end) - merge-and-filter-diagnostics: 3 (default respects, audit bypasses, audit still honors config-level ignores) - filter-diagnostics: 1 (stderr warning when overrides[i].rules is a string) - find-owning-project: 4 (single-project, monorepo dispatch, orphan fallback, relative-path resolution) Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
This was referenced May 10, 2026
aidenybai
added a commit
that referenced
this pull request
May 10, 2026
…ext (#159) (#196) Two small but related fixes for inline-suppression robustness: 1. The disable-line / disable-next-line regexes used a narrow rule-list character class ([\\w/\\-.,\\s]) that excluded common comment punctuation (`;` `:` `(` `'` `?` `!` …). Whenever a user appended an explanatory tail like // react-doctor-disable-next-line react-doctor/foo -- searchQuery initial; user can type the regex stopped matching entirely and the suppression was silently ignored. Broaden the capture to `[^\\r\\n]*?` (anything on the same line) and split the rule section vs. description in a follow-up step, so any prose after the rule ids is fine. 2. `isRuleListedInComment` now strips text after ` -- ` before tokenizing the rule list, mirroring ESLint's convention that anything after ` -- ` on a disable comment is a description rather than a rule id. Without this, descriptive words could be tokenized alongside the rule list, and any trailing punctuation kept polluting equality checks. Stacked single-rule comments (#159) already worked end-to-end after #165 landed multi-line / chain handling — the failing case in the report was purely the regex rejecting comments with descriptive tails. Adds two regression tests using the issue's exact reproduction (`useState(searchQuery)` with stacked descriptive disables, plus the equivalent comma-separated form), and documents both forms (and the chain-breaks-on-code rule) in the README so the comma-separated syntax stops being undocumented. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
aidenybai
added a commit
that referenced
this pull request
May 10, 2026
) * docs(config): clarify ignore.overrides covers per-file rule ignore (#160) The `ignore.overrides` config field already supports per-file rule ignores with the same shape requested in #160 (`{ files, rules }[]`) landing in #165 alongside multi-line JSX suppressions. This rewrites the configuration section so the three nested keys' relative scope is unambiguous and the example covers two of the scenarios from the issue (a glob with multiple rules, a specific file with a single-rule override). No code change needed — the feature is fully implemented and unit-tested in tests/filter-diagnostics.test.ts. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> * chore(docs): apply formatter to README example Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #158, #159, #160, #161.
What this PR does
Refactors inline-suppression matching into small composable utilities and extends its semantics to address the four open issues filed against
react-doctor0.0.47. Architecture-wise, the previous monolithicfilter-diagnostics.tsis now an orchestrator over single-purpose utilities — one per file, in line with the project's "one utility per file" convention.#158 — multi-line JSX
disable-next-lineA
react-doctor-disable-next-lineimmediately above a multi-line JSX opening tag now suppresses diagnostics on attribute lines inside that opener, matching the ESLint convention users expect:Coverage extends through the closing
>of the opening tag, not into children — so the suppression doesn't silently mask unrelated rules in element bodies. The detection is brace- and string-aware (>inside{...}expressions or string attributes is correctly ignored), skips=>and>=operators, and is bounded byJSX_OPENER_SCAN_MAX_LINESto keep worst-case work bounded.#159 — stacked single-rule comments
Walking the chain of consecutive
disable-next-linecomments above the diagnostic now applies all of them, not just the immediately-adjacent one:The README's "Inline suppressions" section is also reworked to surface the comma-separated multi-rule form (already supported but easy to miss in docs):
// react-doctor-disable-next-line react-doctor/no-derived-state-effect, react-doctor/no-fetch-in-effect#160 — per-file rule overrides
New
ignore.overridesconfig field scopes specific rules to specific globs without forcing whole-file ignores:{ "ignore": { "overrides": [ { "files": ["components/diff/**"], "rules": ["react-doctor/no-array-index-as-key"] }, { "files": ["components/search/HighlightedSnippet.tsx"], "rules": ["react/no-danger"] } ] } }A diagnostic is dropped when its file matches an entry's globs and its
plugin/ruleid is listed. Omitrules(or pass[]) to suppress every rule for the matched files.#161 — near-miss hints +
--explain <file:line>When a diagnostic survives the suppression filter but a nearby
disable-next-linelooks intentional, an explanatory hint is attached to the diagnostic. Two near-miss shapes get a hint:The hint is exposed via:
--verboseoutput: appended under each affected file:line.--jsonoutput: as the new optionalDiagnostic.suppressionHintfield.--explain <file:line>flag (with--whyas a hidden alias) for single-site investigation, mirroring therustc --explain <error-code>shape:--explainis mode-exclusive with--json,--score,--annotations, and--staged(consistent with existing CLI mutual-exclusion checks). Passing both--explainand--whyis rejected with a clear message.File layout
New utilities (one per file):
find-jsx-opener-span.ts— heuristic, brace/string-aware finder for the line that closes a JSX opener.find-enclosing-jsx-opener.ts— uses the span finder to locate any multi-line opener that contains a diagnostic.find-stacked-disable-comments.ts— single-pass walk that returns alldisable-next-linecomments above an anchor, tagged withisInChain.is-rule-listed-in-comment.ts— atomic helper: does this comment's rule list cover this rule?is-rule-suppressed-at.ts— orchestrates same-line + chain-above + opener-above into the boolean answer.classify-suppression-near-miss.ts— builds the diagnostic hint string when a near-miss is detected.apply-ignore-overrides.ts— compilesignore.overridesonce and matches each diagnostic against it.to-relative-path.ts— extracted fromis-ignored-file.tsso the override matcher and ignore-file matcher share the same path normalization.parse-file-line-argument.ts— last-colon-wins parsing so Windows drive letters round-trip.Type changes:
Diagnostic.suppressionHint?: string(additive, backward-compatible).ReactDoctorIgnoreOverride { files: string[]; rules?: string[] }andReactDoctorIgnoreConfig.overrides?: ReactDoctorIgnoreOverride[].Constants:
JSX_OPENER_SCAN_MAX_LINES = 32andSUPPRESSION_NEAR_MISS_MAX_LINES = 10inconstants.ts.Test coverage
30 new tests across 4 files (510 total, all passing):
find-jsx-opener-span.test.ts(8): single-line, multi-line, brace/string awareness,=>/>=skipping, lookahead cap, closing-tag rejection.inline-suppressions.test.ts(8 added): 5 multi-line JSX cases (// react-doctor-disable-next-linedoesn't suppress when the rule reports an attribute on a later line of a multi-line JSX element #158), 3 stacked-comment cases (Stacked// react-doctor-disable-next-linecomments shadow each other; comma-syntax works but is undocumented #159), preserves the existing JSX-block-comment form (Support block comments with react-doctor-disable-next-line #144).classify-suppression-near-miss.test.ts(5): wrong-rule, gap-code, JSX-anchor near-miss, active-suppression silence.filter-diagnostics.test.ts(3 added): scoped per-rule override, no-rule-list override, multi-entry override.parse-file-line-argument.test.ts(6): standard parse, Windows drive letters, validation errors.Backward compatibility
inline-suppressions.test.ts+respect-lint-ignores.test.tssuites). Boundary tests (suppression doesn't leak toN+2, doesn't apply to other rules, etc.) all still pass.ignore.rulesandignore.filessemantics unchanged.Diagnostic.suppressionHintandignore.overridesare both additive optional fields — existing configs continue to work without changes.Risk notes
<List<Item>) used as a JSX component is rare enough I haven't worked around it; the JSX-block-comment workaround{/* … */}immediately above the offending attribute still works in that case.