Skip to content

Commit ab5ce46

Browse files
refactor: code review fixes per AGENTS.md rules
- DRY: Extract shared createFileLinesCache in filter-diagnostics.ts - DRY: Extract resolveFailOnLevel in cli.ts (was duplicated in staged and normal paths) - DRY: Reuse exported getCalleeName from helpers.ts instead of duplicating in state-and-effects.ts and performance.ts - Magic number: Move maxBuffer 10MB to GIT_SHOW_MAX_BUFFER_BYTES constant - Variable naming: prevLine → previousLine, tagName → fullTagName, leafName → leafTagName, configFiles → projectConfigFilenames - Remove .pop()! non-null assertion in favor of .at(-1) with fallback Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
1 parent 1f930e6 commit ab5ce46

File tree

7 files changed

+70
-95
lines changed

7 files changed

+70
-95
lines changed

packages/react-doctor/src/cli.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ const shouldFailForDiagnostics = (diagnostics: Diagnostic[], failOnLevel: FailOn
4141
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
4242
};
4343

44+
const resolveFailOnLevel = (
45+
programInstance: Command,
46+
flags: CliFlags,
47+
userConfig: ReactDoctorConfig | null,
48+
): FailOnLevel => {
49+
const resolvedFailOn =
50+
programInstance.getOptionValueSource("failOn") === "cli"
51+
? flags.failOn
52+
: (userConfig?.failOn ?? flags.failOn);
53+
return isValidFailOnLevel(resolvedFailOn) ? resolvedFailOn : "none";
54+
};
55+
4456
const printAnnotations = (diagnostics: Diagnostic[]): void => {
4557
for (const diagnostic of diagnostics) {
4658
const level = diagnostic.severity === "error" ? "error" : "warning";
@@ -199,15 +211,12 @@ const program = new Command()
199211
printAnnotations(remappedDiagnostics);
200212
}
201213

202-
const resolvedFailOn =
203-
program.getOptionValueSource("failOn") === "cli"
204-
? flags.failOn
205-
: (userConfig?.failOn ?? flags.failOn);
206-
const effectiveFailOn: FailOnLevel = isValidFailOnLevel(resolvedFailOn)
207-
? resolvedFailOn
208-
: "none";
209-
210-
if (shouldFailForDiagnostics(remappedDiagnostics, effectiveFailOn)) {
214+
if (
215+
shouldFailForDiagnostics(
216+
remappedDiagnostics,
217+
resolveFailOnLevel(program, flags, userConfig),
218+
)
219+
) {
211220
process.exitCode = 1;
212221
}
213222
} finally {
@@ -268,19 +277,13 @@ const program = new Command()
268277
}
269278
}
270279

271-
const resolvedFailOn =
272-
program.getOptionValueSource("failOn") === "cli"
273-
? flags.failOn
274-
: (userConfig?.failOn ?? flags.failOn);
275-
const effectiveFailOn: FailOnLevel = isValidFailOnLevel(resolvedFailOn)
276-
? resolvedFailOn
277-
: "none";
278-
279280
if (flags.annotations) {
280281
printAnnotations(allDiagnostics);
281282
}
282283

283-
if (shouldFailForDiagnostics(allDiagnostics, effectiveFailOn)) {
284+
if (
285+
shouldFailForDiagnostics(allDiagnostics, resolveFailOnLevel(program, flags, userConfig))
286+
) {
284287
process.exitCode = 1;
285288
}
286289
} catch (error) {

packages/react-doctor/src/constants.ts

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

4949
export const OXLINT_RECOMMENDED_NODE_MAJOR = 24;
5050

51+
export const GIT_SHOW_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
52+
5153
export const IGNORED_DIRECTORIES = new Set(["node_modules", "dist", "build", "coverage"]);

packages/react-doctor/src/plugin/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export const isComponentAssignment = (node: EsTreeNode): boolean =>
9898
Boolean(node.init) &&
9999
(node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression");
100100

101-
const getCalleeName = (node: EsTreeNode): string | null => {
101+
export const getCalleeName = (node: EsTreeNode): string | null => {
102102
if (node.callee?.type === "Identifier") return node.callee.name;
103103
if (node.callee?.type === "MemberExpression" && node.callee.property?.type === "Identifier") {
104104
return node.callee.property.name;

packages/react-doctor/src/plugin/rules/performance.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SETTER_PATTERN,
1010
} from "../constants.js";
1111
import {
12+
getCalleeName,
1213
getEffectCallback,
1314
isComponentAssignment,
1415
isHookCall,
@@ -412,16 +413,12 @@ export const renderingHydrationNoFlicker: Rule = {
412413
if (!bodyStatements || bodyStatements.length !== 1) return;
413414

414415
const soleStatement = bodyStatements[0];
415-
const soleCallee = soleStatement?.expression?.callee;
416416
const soleCalleeName =
417-
soleCallee?.type === "Identifier"
418-
? soleCallee.name
419-
: soleCallee?.type === "MemberExpression" && soleCallee.property?.type === "Identifier"
420-
? soleCallee.property.name
421-
: null;
417+
soleStatement?.expression?.type === "CallExpression"
418+
? getCalleeName(soleStatement.expression)
419+
: null;
422420
if (
423421
soleStatement?.type === "ExpressionStatement" &&
424-
soleStatement.expression?.type === "CallExpression" &&
425422
soleCalleeName &&
426423
SETTER_PATTERN.test(soleCalleeName)
427424
) {

packages/react-doctor/src/plugin/rules/state-and-effects.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
countSetStateCalls,
1111
extractDestructuredPropNames,
1212
getCallbackStatements,
13+
getCalleeName,
1314
getEffectCallback,
1415
isComponentAssignment,
1516
isHookCall,
@@ -40,21 +41,10 @@ export const noDerivedStateEffect: Rule = {
4041
const statements = getCallbackStatements(callback);
4142
if (statements.length === 0) return;
4243

43-
const getSetterName = (expression: EsTreeNode): string | null => {
44-
if (expression.callee?.type === "Identifier") return expression.callee.name;
45-
if (
46-
expression.callee?.type === "MemberExpression" &&
47-
expression.callee.property?.type === "Identifier"
48-
) {
49-
return expression.callee.property.name;
50-
}
51-
return null;
52-
};
53-
5444
const containsOnlySetStateCalls = statements.every((statement: EsTreeNode) => {
5545
if (statement.type !== "ExpressionStatement") return false;
5646
if (statement.expression?.type !== "CallExpression") return false;
57-
const name = getSetterName(statement.expression);
47+
const name = getCalleeName(statement.expression);
5848
return name !== null && isSetterIdentifier(name);
5949
});
6050
if (!containsOnlySetStateCalls) return;
@@ -258,15 +248,7 @@ export const rerenderLazyStateInit: Rule = {
258248
export const rerenderFunctionalSetstate: Rule = {
259249
create: (context: RuleContext) => ({
260250
CallExpression(node: EsTreeNode) {
261-
let calleeName: string | null = null;
262-
if (node.callee?.type === "Identifier") {
263-
calleeName = node.callee.name;
264-
} else if (
265-
node.callee?.type === "MemberExpression" &&
266-
node.callee.property?.type === "Identifier"
267-
) {
268-
calleeName = node.callee.property.name;
269-
}
251+
const calleeName = getCalleeName(node);
270252
if (!calleeName || !isSetterIdentifier(calleeName)) return;
271253
if (!node.arguments?.length) return;
272254

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

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ import type { Diagnostic, ReactDoctorConfig } from "../types.js";
44
import { compileIgnoredFilePatterns, isFileIgnoredByPatterns } from "./is-ignored-file.js";
55

66
const OPENING_TAG_PATTERN = /<([A-Z][\w.]*)/;
7+
const DISABLE_NEXT_LINE_PATTERN = /\/\/\s*react-doctor-disable-next-line\b(?:\s+(.+))?/;
8+
const DISABLE_LINE_PATTERN = /\/\/\s*react-doctor-disable-line\b(?:\s+(.+))?/;
9+
10+
const createFileLinesCache = (rootDirectory: string) => {
11+
const cache = new Map<string, string[] | null>();
12+
13+
return (filePath: string): string[] | null => {
14+
const cached = cache.get(filePath);
15+
if (cached !== undefined) return cached;
16+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
17+
try {
18+
const lines = fs.readFileSync(absolutePath, "utf-8").split("\n");
19+
cache.set(filePath, lines);
20+
return lines;
21+
} catch {
22+
cache.set(filePath, null);
23+
return null;
24+
}
25+
};
26+
};
727

828
const isInsideTextComponent = (
929
lines: string[],
@@ -13,13 +33,20 @@ const isInsideTextComponent = (
1333
for (let lineIndex = diagnosticLine - 1; lineIndex >= 0; lineIndex--) {
1434
const match = lines[lineIndex].match(OPENING_TAG_PATTERN);
1535
if (!match) continue;
16-
const tagName = match[1];
17-
const leafName = tagName.includes(".") ? tagName.split(".").pop()! : tagName;
18-
return textComponentNames.has(tagName) || textComponentNames.has(leafName);
36+
const fullTagName = match[1];
37+
const leafTagName = fullTagName.includes(".")
38+
? (fullTagName.split(".").at(-1) ?? fullTagName)
39+
: fullTagName;
40+
return textComponentNames.has(fullTagName) || textComponentNames.has(leafTagName);
1941
}
2042
return false;
2143
};
2244

45+
const isRuleSuppressed = (commentRules: string | undefined, ruleId: string): boolean => {
46+
if (!commentRules?.trim()) return true;
47+
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
48+
};
49+
2350
export const filterIgnoredDiagnostics = (
2451
diagnostics: Diagnostic[],
2552
config: ReactDoctorConfig,
@@ -31,21 +58,7 @@ export const filterIgnoredDiagnostics = (
3158
Array.isArray(config.textComponents) ? config.textComponents : [],
3259
);
3360
const hasTextComponents = textComponentNames.size > 0;
34-
35-
const fileLineCache = new Map<string, string[] | null>();
36-
const getFileLines = (filePath: string): string[] | null => {
37-
const cached = fileLineCache.get(filePath);
38-
if (cached !== undefined) return cached;
39-
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
40-
try {
41-
const lines = fs.readFileSync(absolutePath, "utf-8").split("\n");
42-
fileLineCache.set(filePath, lines);
43-
return lines;
44-
} catch {
45-
fileLineCache.set(filePath, null);
46-
return null;
47-
}
48-
};
61+
const getFileLines = createFileLinesCache(rootDirectory);
4962

5063
return diagnostics.filter((diagnostic) => {
5164
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
@@ -68,33 +81,11 @@ export const filterIgnoredDiagnostics = (
6881
});
6982
};
7083

71-
const DISABLE_NEXT_LINE_PATTERN = /\/\/\s*react-doctor-disable-next-line\b(?:\s+(.+))?/;
72-
const DISABLE_LINE_PATTERN = /\/\/\s*react-doctor-disable-line\b(?:\s+(.+))?/;
73-
74-
const isRuleSuppressed = (commentRules: string | undefined, ruleId: string): boolean => {
75-
if (!commentRules?.trim()) return true;
76-
return commentRules.split(/[,\s]+/).some((rule) => rule.trim() === ruleId);
77-
};
78-
7984
export const filterInlineSuppressions = (
8085
diagnostics: Diagnostic[],
8186
rootDirectory: string,
8287
): Diagnostic[] => {
83-
const fileLineCache = new Map<string, string[] | null>();
84-
85-
const getFileLines = (filePath: string): string[] | null => {
86-
const cached = fileLineCache.get(filePath);
87-
if (cached !== undefined) return cached;
88-
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
89-
try {
90-
const lines = fs.readFileSync(absolutePath, "utf-8").split("\n");
91-
fileLineCache.set(filePath, lines);
92-
return lines;
93-
} catch {
94-
fileLineCache.set(filePath, null);
95-
return null;
96-
}
97-
};
88+
const getFileLines = createFileLinesCache(rootDirectory);
9889

9990
return diagnostics.filter((diagnostic) => {
10091
if (diagnostic.line <= 0) return true;
@@ -111,9 +102,9 @@ export const filterInlineSuppressions = (
111102
}
112103

113104
if (diagnostic.line >= 2) {
114-
const prevLine = lines[diagnostic.line - 2];
115-
if (prevLine) {
116-
const nextLineMatch = prevLine.match(DISABLE_NEXT_LINE_PATTERN);
105+
const previousLine = lines[diagnostic.line - 2];
106+
if (previousLine) {
107+
const nextLineMatch = previousLine.match(DISABLE_NEXT_LINE_PATTERN);
117108
if (nextLineMatch && isRuleSuppressed(nextLineMatch[1], ruleId)) return false;
118109
}
119110
}

packages/react-doctor/src/utils/get-staged-files.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { execSync } from "node:child_process";
22
import fs from "node:fs";
33
import path from "node:path";
4-
import { SOURCE_FILE_PATTERN } from "../constants.js";
4+
import { GIT_SHOW_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from "../constants.js";
55

66
const getStagedFilePaths = (directory: string): string[] => {
77
try {
@@ -23,7 +23,7 @@ const readStagedContent = (directory: string, relativePath: string): string | nu
2323
return execSync(`git show ":${relativePath}"`, {
2424
cwd: directory,
2525
stdio: "pipe",
26-
maxBuffer: 10 * 1024 * 1024,
26+
maxBuffer: GIT_SHOW_MAX_BUFFER_BYTES,
2727
}).toString();
2828
} catch {
2929
return null;
@@ -56,8 +56,8 @@ export const materializeStagedFiles = (
5656
materializedFiles.push(relativePath);
5757
}
5858

59-
const configFiles = ["tsconfig.json", "package.json"];
60-
for (const configFile of configFiles) {
59+
const projectConfigFilenames = ["tsconfig.json", "package.json"];
60+
for (const configFile of projectConfigFilenames) {
6161
const sourcePath = path.join(directory, configFile);
6262
const targetPath = path.join(tempDirectory, configFile);
6363
if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) {

0 commit comments

Comments
 (0)