Skip to content

Commit 2ddb8ca

Browse files
fix: recognize custom text components in rn-no-raw-text rule
Fixes #93: The rn-no-raw-text rule now recognizes a much wider set of text-handling components: 1. Expanded built-in names: Typography, Paragraph, Span, H1-H6 2. Keyword matching instead of suffix-only: any component whose name contains Text, Title, Label, Heading, Caption, Subtitle, Typography, Paragraph, Description, or Body is treated as text-handling (e.g. StyledText, BodyText, CustomLabel, MyTypography) 3. New textComponents config option: users can specify arbitrary component names in react-doctor.config.json that should be treated as text-handling: { "textComponents": ["MyCustomComp", "NativeWindText"] } Diagnostics inside these components are filtered out post-lint. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
1 parent 037d38b commit 2ddb8ca

File tree

4 files changed

+63
-7
lines changed

4 files changed

+63
-7
lines changed

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,15 +262,31 @@ export const MOTION_LIBRARY_PACKAGES = new Set(["framer-motion", "motion"]);
262262

263263
export const RAW_TEXT_PREVIEW_MAX_CHARS = 30;
264264

265-
export const REACT_NATIVE_TEXT_COMPONENTS = new Set(["Text", "TextInput"]);
265+
export const REACT_NATIVE_TEXT_COMPONENTS = new Set([
266+
"Text",
267+
"TextInput",
268+
"Typography",
269+
"Paragraph",
270+
"Span",
271+
"H1",
272+
"H2",
273+
"H3",
274+
"H4",
275+
"H5",
276+
"H6",
277+
]);
266278

267-
export const REACT_NATIVE_TEXT_COMPONENT_SUFFIXES = new Set([
279+
export const REACT_NATIVE_TEXT_COMPONENT_KEYWORDS = new Set([
268280
"Text",
269281
"Title",
270282
"Label",
271283
"Heading",
272284
"Caption",
273285
"Subtitle",
286+
"Typography",
287+
"Paragraph",
288+
"Description",
289+
"Body",
274290
]);
275291

276292
export const DEPRECATED_RN_MODULE_REPLACEMENTS: Record<string, string> = {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
RAW_TEXT_PREVIEW_MAX_CHARS,
66
REACT_NATIVE_LIST_COMPONENTS,
77
REACT_NATIVE_TEXT_COMPONENTS,
8-
REACT_NATIVE_TEXT_COMPONENT_SUFFIXES,
8+
REACT_NATIVE_TEXT_COMPONENT_KEYWORDS,
99
} from "../constants.js";
1010
import { hasDirective, isMemberProperty } from "../helpers.js";
1111
import type { EsTreeNode, Rule, RuleContext } from "../types.js";
@@ -56,7 +56,7 @@ const getRawTextDescription = (child: EsTreeNode): string => {
5656

5757
const isTextHandlingComponent = (elementName: string): boolean => {
5858
if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
59-
return [...REACT_NATIVE_TEXT_COMPONENT_SUFFIXES].some((suffix) => elementName.endsWith(suffix));
59+
return [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS].some((keyword) => elementName.includes(keyword));
6060
};
6161

6262
export const rnNoRawText: Rule = {

packages/react-doctor/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,5 @@ export interface ReactDoctorConfig {
169169
failOn?: FailOnLevel;
170170
customRulesOnly?: boolean;
171171
share?: boolean;
172+
textComponents?: string[];
172173
}

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,49 @@ import path from "node:path";
33
import type { Diagnostic, ReactDoctorConfig } from "../types.js";
44
import { compileIgnoredFilePatterns, isFileIgnoredByPatterns } from "./is-ignored-file.js";
55

6+
const OPENING_TAG_PATTERN = /<([A-Z][\w.]*)/;
7+
8+
const isInsideTextComponent = (
9+
lines: string[],
10+
diagnosticLine: number,
11+
textComponentNames: Set<string>,
12+
): boolean => {
13+
for (let lineIndex = diagnosticLine - 1; lineIndex >= 0; lineIndex--) {
14+
const match = lines[lineIndex].match(OPENING_TAG_PATTERN);
15+
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);
19+
}
20+
return false;
21+
};
22+
623
export const filterIgnoredDiagnostics = (
724
diagnostics: Diagnostic[],
825
config: ReactDoctorConfig,
926
rootDirectory: string,
1027
): Diagnostic[] => {
1128
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules : []);
1229
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
30+
const textComponentNames = new Set(
31+
Array.isArray(config.textComponents) ? config.textComponents : [],
32+
);
33+
const hasTextComponents = textComponentNames.size > 0;
1334

14-
if (ignoredRules.size === 0 && ignoredFilePatterns.length === 0) {
15-
return diagnostics;
16-
}
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+
};
1749

1850
return diagnostics.filter((diagnostic) => {
1951
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
@@ -25,6 +57,13 @@ export const filterIgnoredDiagnostics = (
2557
return false;
2658
}
2759

60+
if (hasTextComponents && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
61+
const lines = getFileLines(diagnostic.filePath);
62+
if (lines && isInsideTextComponent(lines, diagnostic.line, textComponentNames)) {
63+
return false;
64+
}
65+
}
66+
2867
return true;
2968
});
3069
};

0 commit comments

Comments
 (0)