Skip to content
Open
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
6 changes: 6 additions & 0 deletions packages/react-doctor/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,14 @@ export interface ReactDoctorIgnoreConfig {
files?: string[];
}

export interface ReactDoctorOverride {
files: string[];
ignore: { rules: string[] };
}

export interface ReactDoctorConfig {
ignore?: ReactDoctorIgnoreConfig;
overrides?: ReactDoctorOverride[];
lint?: boolean;
deadCode?: boolean;
verbose?: boolean;
Expand Down
35 changes: 34 additions & 1 deletion packages/react-doctor/src/utils/filter-diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import type { Diagnostic, ReactDoctorConfig } from "../types.js";
import { compileGlobPattern } from "./match-glob-pattern.js";

interface CompiledOverride {
filePatterns: RegExp[];
ignoredRules: Set<string>;
}

const compileOverrides = (config: ReactDoctorConfig): CompiledOverride[] => {
if (!Array.isArray(config.overrides)) return [];

return config.overrides.map((override) => ({
filePatterns: override.files.map(compileGlobPattern),
ignoredRules: new Set(override.ignore.rules),
Comment on lines +13 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
filePatterns: override.files.map(compileGlobPattern),
ignoredRules: new Set(override.ignore.rules),
filePatterns: Array.isArray(override.files) ? override.files.map(compileGlobPattern) : [],
ignoredRules: new Set(Array.isArray(override.ignore?.rules) ? override.ignore.rules : []),

The compileOverrides function crashes with "Cannot read properties of undefined" when processing malformed override configs missing ignore or files properties.

Fix on Vercel

}));
};

const isRuleIgnoredByOverride = (
normalizedPath: string,
ruleIdentifier: string,
compiledOverrides: CompiledOverride[],
): boolean =>
compiledOverrides.some(
(override) =>
override.ignoredRules.has(ruleIdentifier) &&
override.filePatterns.some((pattern) => pattern.test(normalizedPath)),
);

export const filterIgnoredDiagnostics = (
diagnostics: Diagnostic[],
config: ReactDoctorConfig,
Expand All @@ -9,8 +34,12 @@ export const filterIgnoredDiagnostics = (
const ignoredFilePatterns = Array.isArray(config.ignore?.files)
? config.ignore.files.map(compileGlobPattern)
: [];
const compiledOverrides = compileOverrides(config);

if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) {
const hasNoFilters =
ignoredRules.size === 0 && ignoredFilePatterns.length === 0 && compiledOverrides.length === 0;

if (hasNoFilters) {
return diagnostics;
}

Expand All @@ -25,6 +54,10 @@ export const filterIgnoredDiagnostics = (
return false;
}

if (isRuleIgnoredByOverride(normalizedPath, ruleIdentifier, compiledOverrides)) {
return false;
}

return true;
});
};
149 changes: 149 additions & 0 deletions packages/react-doctor/tests/filter-diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,153 @@ describe("filterIgnoredDiagnostics", () => {
expect(filtered).toHaveLength(1);
expect(filtered[0].rule).toBe("files");
});

describe("overrides", () => {
it("ignores a specific rule only for matching files", () => {
const diagnostics = [
createDiagnostic({
plugin: "react-doctor",
rule: "no-giant-component",
filePath: "src/legacy/Dashboard.tsx",
}),
createDiagnostic({
plugin: "react-doctor",
rule: "no-giant-component",
filePath: "src/components/App.tsx",
}),
];
const config: ReactDoctorConfig = {
overrides: [
{
files: ["src/legacy/**"],
ignore: { rules: ["react-doctor/no-giant-component"] },
},
],
};

const filtered = filterIgnoredDiagnostics(diagnostics, config);
expect(filtered).toHaveLength(1);
expect(filtered[0].filePath).toBe("src/components/App.tsx");
});

it("applies multiple overrides to different file patterns", () => {
const diagnostics = [
createDiagnostic({
plugin: "react",
rule: "no-danger",
filePath: "src/legacy/Old.tsx",
}),
createDiagnostic({
plugin: "knip",
rule: "exports",
filePath: "src/generated/api.tsx",
}),
createDiagnostic({
plugin: "react-doctor",
rule: "no-giant-component",
filePath: "src/components/App.tsx",
}),
];
const config: ReactDoctorConfig = {
overrides: [
{
files: ["src/legacy/**"],
ignore: { rules: ["react/no-danger"] },
},
{
files: ["src/generated/**"],
ignore: { rules: ["knip/exports"] },
},
],
};

const filtered = filterIgnoredDiagnostics(diagnostics, config);
expect(filtered).toHaveLength(1);
expect(filtered[0].filePath).toBe("src/components/App.tsx");
});

it("combines overrides with global ignore.rules and ignore.files", () => {
const diagnostics = [
createDiagnostic({
plugin: "react",
rule: "no-danger",
filePath: "src/app.tsx",
}),
createDiagnostic({
plugin: "knip",
rule: "exports",
filePath: "src/generated/api.tsx",
}),
createDiagnostic({
plugin: "react-doctor",
rule: "no-giant-component",
filePath: "src/legacy/Dashboard.tsx",
}),
createDiagnostic({
plugin: "jsx-a11y",
rule: "no-autofocus",
filePath: "src/components/Search.tsx",
}),
];
const config: ReactDoctorConfig = {
ignore: {
rules: ["react/no-danger"],
files: ["src/generated/**"],
},
overrides: [
{
files: ["src/legacy/**"],
ignore: { rules: ["react-doctor/no-giant-component"] },
},
],
};

const filtered = filterIgnoredDiagnostics(diagnostics, config);
expect(filtered).toHaveLength(1);
expect(filtered[0].rule).toBe("no-autofocus");
});

it("has no effect when overrides array is empty", () => {
const diagnostics = [
createDiagnostic({ plugin: "react", rule: "no-danger" }),
createDiagnostic({ plugin: "knip", rule: "exports" }),
];
const config: ReactDoctorConfig = { overrides: [] };

const filtered = filterIgnoredDiagnostics(diagnostics, config);
expect(filtered).toHaveLength(2);
});

it("supports multiple file globs in a single override", () => {
const diagnostics = [
createDiagnostic({
plugin: "react",
rule: "no-danger",
filePath: "src/legacy/Old.tsx",
}),
createDiagnostic({
plugin: "react",
rule: "no-danger",
filePath: "src/deprecated/Stale.tsx",
}),
createDiagnostic({
plugin: "react",
rule: "no-danger",
filePath: "src/components/App.tsx",
}),
];
const config: ReactDoctorConfig = {
overrides: [
{
files: ["src/legacy/**", "src/deprecated/**"],
ignore: { rules: ["react/no-danger"] },
},
],
};

const filtered = filterIgnoredDiagnostics(diagnostics, config);
expect(filtered).toHaveLength(1);
expect(filtered[0].filePath).toBe("src/components/App.tsx");
});
});
});