Skip to content

Commit 720f421

Browse files
authored
fix: apply ignore.files as pre-filter to skip linting ignored files (#110)
1 parent ec4a41f commit 720f421

File tree

11 files changed

+159
-29
lines changed

11 files changed

+159
-29
lines changed

packages/react-doctor/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export const OXLINT_NODE_REQUIREMENT = "^20.19.0 || >=22.12.0";
5353

5454
export const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
5555

56+
export const IGNORED_DIRECTORIES = new Set(["node_modules", "dist", "build", "coverage"]);
57+
5658
export const AMI_WEBSITE_URL = "https://ami.dev";
5759

5860
export const AMI_INSTALL_URL = `${AMI_WEBSITE_URL}/install.sh`;

packages/react-doctor/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { calculateScore } from "./utils/calculate-score.js";
55
import { combineDiagnostics, computeJsxIncludePaths } from "./utils/combine-diagnostics.js";
66
import { discoverProject } from "./utils/discover-project.js";
77
import { loadConfig } from "./utils/load-config.js";
8+
import { resolveLintIncludePaths } from "./utils/resolve-lint-include-paths.js";
89
import { runKnip } from "./utils/run-knip.js";
910
import { runOxlint } from "./utils/run-oxlint.js";
1011

@@ -44,6 +45,8 @@ export const diagnose = async (
4445
}
4546

4647
const jsxIncludePaths = computeJsxIncludePaths(includePaths);
48+
const lintIncludePaths =
49+
jsxIncludePaths ?? resolveLintIncludePaths(resolvedDirectory, userConfig);
4750

4851
const emptyDiagnostics: Diagnostic[] = [];
4952

@@ -53,7 +56,7 @@ export const diagnose = async (
5356
projectInfo.hasTypeScript,
5457
projectInfo.framework,
5558
projectInfo.hasReactCompiler,
56-
jsxIncludePaths,
59+
lintIncludePaths,
5760
).catch((error: unknown) => {
5861
console.error("Lint failed:", error);
5962
return emptyDiagnostics;

packages/react-doctor/src/scan.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
isNvmInstalled,
3939
resolveNodeForOxlint,
4040
} from "./utils/resolve-compatible-node.js";
41+
import { resolveLintIncludePaths } from "./utils/resolve-lint-include-paths.js";
4142
import { runKnip } from "./utils/run-knip.js";
4243
import { runOxlint } from "./utils/run-oxlint.js";
4344
import { spinner } from "./utils/spinner.js";
@@ -425,6 +426,7 @@ const printProjectDetection = (
425426
userConfig: ReactDoctorConfig | null,
426427
isDiffMode: boolean,
427428
includePaths: string[],
429+
lintSourceFileCount?: number,
428430
): void => {
429431
const frameworkLabel = formatFrameworkName(projectInfo.framework);
430432
const languageLabel = projectInfo.hasTypeScript ? "TypeScript" : "JavaScript";
@@ -445,7 +447,9 @@ const printProjectDetection = (
445447
if (isDiffMode) {
446448
completeStep(`Scanning ${highlighter.info(`${includePaths.length}`)} changed source files.`);
447449
} else {
448-
completeStep(`Found ${highlighter.info(`${projectInfo.sourceFileCount}`)} source files.`);
450+
completeStep(
451+
`Found ${highlighter.info(`${lintSourceFileCount ?? projectInfo.sourceFileCount}`)} source files.`,
452+
);
449453
}
450454

451455
if (userConfig) {
@@ -470,12 +474,14 @@ export const scan = async (
470474
throw new Error("No React dependency found in package.json");
471475
}
472476

477+
const jsxIncludePaths = computeJsxIncludePaths(includePaths);
478+
const lintIncludePaths = jsxIncludePaths ?? resolveLintIncludePaths(directory, userConfig);
479+
const lintSourceFileCount = lintIncludePaths?.length ?? projectInfo.sourceFileCount;
480+
473481
if (!options.scoreOnly) {
474-
printProjectDetection(projectInfo, userConfig, isDiffMode, includePaths);
482+
printProjectDetection(projectInfo, userConfig, isDiffMode, includePaths, lintSourceFileCount);
475483
}
476484

477-
const jsxIncludePaths = computeJsxIncludePaths(includePaths);
478-
479485
let didLintFail = false;
480486
let didDeadCodeFail = false;
481487

@@ -491,7 +497,7 @@ export const scan = async (
491497
projectInfo.hasTypeScript,
492498
projectInfo.framework,
493499
projectInfo.hasReactCompiler,
494-
jsxIncludePaths,
500+
lintIncludePaths,
495501
resolvedNodeBinaryPath,
496502
);
497503
lintSpinner?.succeed("Running lint checks.");
@@ -594,7 +600,7 @@ export const scan = async (
594600

595601
printDiagnostics(diagnostics, options.verbose);
596602

597-
const displayedSourceFileCount = isDiffMode ? includePaths.length : projectInfo.sourceFileCount;
603+
const displayedSourceFileCount = isDiffMode ? includePaths.length : lintSourceFileCount;
598604

599605
printSummary(
600606
diagnostics,

packages/react-doctor/src/utils/combine-diagnostics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ export const combineDiagnostics = (
2020
...deadCodeDiagnostics,
2121
...(isDiffMode ? [] : checkReducedMotion(directory)),
2222
];
23-
const filtered = userConfig ? filterIgnoredDiagnostics(merged, userConfig) : merged;
23+
const filtered = userConfig ? filterIgnoredDiagnostics(merged, userConfig, directory) : merged;
2424
return filterInlineSuppressions(filtered, directory);
2525
};

packages/react-doctor/src/utils/discover-project.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { spawnSync } from "node:child_process";
4-
import { GIT_LS_FILES_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from "../constants.js";
4+
import {
5+
GIT_LS_FILES_MAX_BUFFER_BYTES,
6+
IGNORED_DIRECTORIES,
7+
SOURCE_FILE_PATTERN,
8+
} from "../constants.js";
59
import type {
610
DependencyInfo,
711
Framework,
@@ -71,8 +75,6 @@ const FRAMEWORK_DISPLAY_NAMES: Record<Framework, string> = {
7175
export const formatFrameworkName = (framework: Framework): string =>
7276
FRAMEWORK_DISPLAY_NAMES[framework];
7377

74-
const IGNORED_DIRECTORIES = new Set(["node_modules", "dist", "build", "coverage"]);
75-
7678
const countSourceFilesViaFilesystem = (rootDirectory: string): number => {
7779
let count = 0;
7880
const stack = [rootDirectory];

packages/react-doctor/src/utils/filter-diagnostics.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import type { Diagnostic, ReactDoctorConfig } from "../types.js";
4-
import { compileGlobPattern } from "./match-glob-pattern.js";
4+
import { compileIgnoredFilePatterns, isFileIgnoredByPatterns } from "./is-ignored-file.js";
55

66
export const filterIgnoredDiagnostics = (
77
diagnostics: Diagnostic[],
88
config: ReactDoctorConfig,
9+
rootDirectory: string,
910
): Diagnostic[] => {
1011
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
11-
const ignoredFilePatterns = Array.isArray(config.ignore?.files)
12-
? config.ignore.files.map(compileGlobPattern)
13-
: [];
12+
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
1413

1514
if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) {
1615
return diagnostics;
@@ -22,8 +21,7 @@ export const filterIgnoredDiagnostics = (
2221
return false;
2322
}
2423

25-
const normalizedPath = diagnostic.filePath.replace(/\\/g, "/").replace(/^\.\//, "");
26-
if (ignoredFilePatterns.some((pattern) => pattern.test(normalizedPath))) {
24+
if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) {
2725
return false;
2826
}
2927

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { ReactDoctorConfig } from "../types.js";
2+
import { compileGlobPattern } from "./match-glob-pattern.js";
3+
4+
const toRelativePath = (filePath: string, rootDirectory: string): string => {
5+
const normalizedFilePath = filePath.replace(/\\/g, "/");
6+
const normalizedRoot = rootDirectory.replace(/\\/g, "/").replace(/\/$/, "") + "/";
7+
8+
if (normalizedFilePath.startsWith(normalizedRoot)) {
9+
return normalizedFilePath.slice(normalizedRoot.length);
10+
}
11+
12+
return normalizedFilePath.replace(/^\.\//, "");
13+
};
14+
15+
export const compileIgnoredFilePatterns = (userConfig: ReactDoctorConfig | null): RegExp[] =>
16+
Array.isArray(userConfig?.ignore?.files) ? userConfig.ignore.files.map(compileGlobPattern) : [];
17+
18+
export const isFileIgnoredByPatterns = (
19+
filePath: string,
20+
rootDirectory: string,
21+
patterns: RegExp[],
22+
): boolean => {
23+
if (patterns.length === 0) {
24+
return false;
25+
}
26+
27+
const relativePath = toRelativePath(filePath, rootDirectory);
28+
return patterns.some((pattern) => pattern.test(relativePath));
29+
};

packages/react-doctor/src/utils/neutralize-disable-directives.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@ import fs from "node:fs";
33
import path from "node:path";
44
import { GIT_LS_FILES_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from "../constants.js";
55

6-
const findFilesWithDisableDirectives = (rootDirectory: string): string[] => {
7-
const result = spawnSync("git", ["grep", "-l", "--untracked", "-E", "(eslint|oxlint)-disable"], {
6+
const findFilesWithDisableDirectives = (
7+
rootDirectory: string,
8+
includePaths?: string[],
9+
): string[] => {
10+
const grepArgs = ["grep", "-l", "--untracked", "-E", "(eslint|oxlint)-disable"];
11+
if (includePaths && includePaths.length > 0) {
12+
grepArgs.push("--", ...includePaths);
13+
}
14+
15+
const result = spawnSync("git", grepArgs, {
816
cwd: rootDirectory,
917
encoding: "utf-8",
1018
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
1119
});
1220

1321
if (result.error || result.status === null) return [];
22+
if (result.status !== 0 && result.stdout.trim().length === 0) return [];
1423

1524
return result.stdout
1625
.split("\n")
@@ -22,8 +31,11 @@ const neutralizeContent = (content: string): string =>
2231
.replaceAll("eslint-disable", "eslint_disable")
2332
.replaceAll("oxlint-disable", "oxlint_disable");
2433

25-
export const neutralizeDisableDirectives = (rootDirectory: string): (() => void) => {
26-
const filePaths = findFilesWithDisableDirectives(rootDirectory);
34+
export const neutralizeDisableDirectives = (
35+
rootDirectory: string,
36+
includePaths?: string[],
37+
): (() => void) => {
38+
const filePaths = findFilesWithDisableDirectives(rootDirectory, includePaths);
2739
const originalContents = new Map<string, string>();
2840

2941
for (const relativePath of filePaths) {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { spawnSync } from "node:child_process";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import {
5+
GIT_LS_FILES_MAX_BUFFER_BYTES,
6+
IGNORED_DIRECTORIES,
7+
JSX_FILE_PATTERN,
8+
SOURCE_FILE_PATTERN,
9+
} from "../constants.js";
10+
import type { ReactDoctorConfig } from "../types.js";
11+
import { compileIgnoredFilePatterns, isFileIgnoredByPatterns } from "./is-ignored-file.js";
12+
13+
const listSourceFilesViaGit = (rootDirectory: string): string[] | null => {
14+
const result = spawnSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], {
15+
cwd: rootDirectory,
16+
encoding: "utf-8",
17+
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,
18+
});
19+
20+
if (result.error || result.status !== 0) {
21+
return null;
22+
}
23+
24+
return result.stdout
25+
.split("\n")
26+
.filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath));
27+
};
28+
29+
const listSourceFilesViaFilesystem = (rootDirectory: string): string[] => {
30+
const filePaths: string[] = [];
31+
const stack = [rootDirectory];
32+
33+
while (stack.length > 0) {
34+
const currentDirectory = stack.pop()!;
35+
const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
36+
37+
for (const entry of entries) {
38+
const absolutePath = path.join(currentDirectory, entry.name);
39+
40+
if (entry.isDirectory()) {
41+
if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) {
42+
stack.push(absolutePath);
43+
}
44+
continue;
45+
}
46+
47+
if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) {
48+
filePaths.push(path.relative(rootDirectory, absolutePath).replace(/\\/g, "/"));
49+
}
50+
}
51+
}
52+
53+
return filePaths;
54+
};
55+
56+
const listSourceFiles = (rootDirectory: string): string[] =>
57+
listSourceFilesViaGit(rootDirectory) ?? listSourceFilesViaFilesystem(rootDirectory);
58+
59+
export const resolveLintIncludePaths = (
60+
rootDirectory: string,
61+
userConfig: ReactDoctorConfig | null,
62+
): string[] | undefined => {
63+
if (!Array.isArray(userConfig?.ignore?.files) || userConfig.ignore.files.length === 0) {
64+
return undefined;
65+
}
66+
67+
const ignoredPatterns = compileIgnoredFilePatterns(userConfig);
68+
69+
const includedPaths = listSourceFiles(rootDirectory).filter((filePath) => {
70+
if (!JSX_FILE_PATTERN.test(filePath)) {
71+
return false;
72+
}
73+
74+
return !isFileIgnoredByPatterns(filePath, rootDirectory, ignoredPatterns);
75+
});
76+
77+
return includedPaths;
78+
};

packages/react-doctor/src/utils/run-oxlint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ export const runOxlint = async (
396396
const configPath = path.join(os.tmpdir(), `react-doctor-oxlintrc-${process.pid}.json`);
397397
const pluginPath = resolvePluginPath();
398398
const config = createOxlintConfig({ pluginPath, framework, hasReactCompiler });
399-
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory);
399+
const restoreDisableDirectives = neutralizeDisableDirectives(rootDirectory, includePaths);
400400

401401
try {
402402
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));

0 commit comments

Comments
 (0)