react-doctor 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -15
- package/dist/cli.js +104 -28
- package/dist/eslint-plugin.js +1 -1
- package/dist/index.d.ts +17 -0
- package/dist/index.js +92 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,21 +160,26 @@ When a suppression isn't working, `--explain <file:line>` reports what the scann
|
|
|
160
160
|
|
|
161
161
|
### Config keys
|
|
162
162
|
|
|
163
|
-
| Key
|
|
164
|
-
|
|
|
165
|
-
| `ignore.rules`
|
|
166
|
-
| `ignore.files`
|
|
167
|
-
| `ignore.overrides`
|
|
168
|
-
| `lint`
|
|
169
|
-
| `deadCode`
|
|
170
|
-
| `verbose`
|
|
171
|
-
| `diff`
|
|
172
|
-
| `failOn`
|
|
173
|
-
| `customRulesOnly`
|
|
174
|
-
| `share`
|
|
175
|
-
| `textComponents`
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
163
|
+
| Key | Type | Default |
|
|
164
|
+
| -------------------------- | -------------------------------- | -------- |
|
|
165
|
+
| `ignore.rules` | `string[]` | `[]` |
|
|
166
|
+
| `ignore.files` | `string[]` | `[]` |
|
|
167
|
+
| `ignore.overrides` | `{ files, rules? }[]` | `[]` |
|
|
168
|
+
| `lint` | `boolean` | `true` |
|
|
169
|
+
| `deadCode` | `boolean` | `true` |
|
|
170
|
+
| `verbose` | `boolean` | `false` |
|
|
171
|
+
| `diff` | `boolean \| string` | |
|
|
172
|
+
| `failOn` | `"error" \| "warning" \| "none"` | `"none"` |
|
|
173
|
+
| `customRulesOnly` | `boolean` | `false` |
|
|
174
|
+
| `share` | `boolean` | `true` |
|
|
175
|
+
| `textComponents` | `string[]` | `[]` |
|
|
176
|
+
| `rawTextWrapperComponents` | `string[]` | `[]` |
|
|
177
|
+
| `respectInlineDisables` | `boolean` | `true` |
|
|
178
|
+
| `adoptExistingLintConfig` | `boolean` | `true` |
|
|
179
|
+
|
|
180
|
+
`textComponents` is the broad escape hatch for `rn-no-raw-text` — list components that themselves behave like React Native's `<Text>` (custom `Typography`, `NativeTabs.Trigger.Label`, etc.) and the rule will treat them as text containers regardless of what their children look like.
|
|
181
|
+
|
|
182
|
+
`rawTextWrapperComponents` is the narrower option for components that are not text elements but safely route string-only children through an internal `<Text>` (e.g. `heroui-native`'s `Button`, which stringifies its children and renders them through a `ButtonLabel`). Listed wrappers suppress `rn-no-raw-text` only when their children are entirely stringifiable. A wrapper with mixed children — e.g. `<Button>Save<Icon /></Button>` — still reports because the wrapper can't safely route raw text alongside a sibling JSX element.
|
|
178
183
|
|
|
179
184
|
## Node.js API
|
|
180
185
|
|
package/dist/cli.js
CHANGED
|
@@ -212,7 +212,10 @@ const finalize = (method, originalText, displayText) => {
|
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
214
|
sharedInstance.stop();
|
|
215
|
-
ora(
|
|
215
|
+
ora({
|
|
216
|
+
text: displayText,
|
|
217
|
+
indent: 2
|
|
218
|
+
}).start()[method](displayText);
|
|
216
219
|
const [remainingText] = pendingTexts;
|
|
217
220
|
if (remainingText) sharedInstance.text = remainingText;
|
|
218
221
|
sharedInstance.start();
|
|
@@ -221,7 +224,10 @@ const spinner = (text) => ({ start() {
|
|
|
221
224
|
if (isSilent) return noopHandle;
|
|
222
225
|
activeCount++;
|
|
223
226
|
pendingTexts.add(text);
|
|
224
|
-
if (!sharedInstance) sharedInstance = ora({
|
|
227
|
+
if (!sharedInstance) sharedInstance = ora({
|
|
228
|
+
text,
|
|
229
|
+
indent: 2
|
|
230
|
+
}).start();
|
|
225
231
|
else sharedInstance.text = text;
|
|
226
232
|
const handle = {
|
|
227
233
|
succeed: (displayText) => {
|
|
@@ -906,6 +912,8 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
|
906
912
|
//#endregion
|
|
907
913
|
//#region src/utils/filter-diagnostics.ts
|
|
908
914
|
const OPENING_TAG_PATTERN = /<([A-Z][\w.]*)/;
|
|
915
|
+
const JSX_CHILD_OPEN_PATTERN = /<[A-Za-z]/;
|
|
916
|
+
const escapeRegExpSpecials = (rawText) => rawText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
909
917
|
const resolveCandidateReadPath = (rootDirectory, filePath) => {
|
|
910
918
|
const normalizedFile = filePath.replace(/\\/g, "/");
|
|
911
919
|
if (normalizedFile.startsWith("/") || /^[a-zA-Z]:\//.test(normalizedFile) || /^[a-zA-Z]:\\/.test(filePath)) return filePath;
|
|
@@ -931,21 +939,93 @@ const isInsideTextComponent = (lines, diagnosticLine, textComponentNames) => {
|
|
|
931
939
|
}
|
|
932
940
|
return false;
|
|
933
941
|
};
|
|
942
|
+
const findOpenerAtOrAbove = (lines, upperBoundLineIndex) => {
|
|
943
|
+
for (let lineIndex = upperBoundLineIndex; lineIndex >= 0; lineIndex--) {
|
|
944
|
+
const match = lines[lineIndex].match(OPENING_TAG_PATTERN);
|
|
945
|
+
if (!match) continue;
|
|
946
|
+
const fullName = match[1];
|
|
947
|
+
return {
|
|
948
|
+
fullName,
|
|
949
|
+
leafName: fullName.includes(".") ? fullName.split(".").at(-1) ?? fullName : fullName,
|
|
950
|
+
lineIndex
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
return null;
|
|
954
|
+
};
|
|
955
|
+
const resolveJsxRange = (lines, opener) => {
|
|
956
|
+
const closingPattern = new RegExp(`</(?:${escapeRegExpSpecials(opener.fullName)}|${escapeRegExpSpecials(opener.leafName)})\\s*>`);
|
|
957
|
+
let closerLineIndex = -1;
|
|
958
|
+
let closerColumn = -1;
|
|
959
|
+
for (let lineIndex = opener.lineIndex; lineIndex < lines.length; lineIndex++) {
|
|
960
|
+
const match = closingPattern.exec(lines[lineIndex]);
|
|
961
|
+
if (!match) continue;
|
|
962
|
+
closerLineIndex = lineIndex;
|
|
963
|
+
closerColumn = match.index;
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
if (closerLineIndex < 0) return null;
|
|
967
|
+
const openerLine = lines[opener.lineIndex];
|
|
968
|
+
const tagStartIndex = openerLine.indexOf(`<${opener.fullName}`);
|
|
969
|
+
if (tagStartIndex < 0) return null;
|
|
970
|
+
const openerEndIndex = openerLine.indexOf(">", tagStartIndex);
|
|
971
|
+
let bodyText;
|
|
972
|
+
if (opener.lineIndex === closerLineIndex) {
|
|
973
|
+
if (openerEndIndex < 0 || openerEndIndex >= closerColumn) return null;
|
|
974
|
+
bodyText = openerLine.slice(openerEndIndex + 1, closerColumn);
|
|
975
|
+
} else {
|
|
976
|
+
const segments = [];
|
|
977
|
+
if (openerEndIndex >= 0) segments.push(openerLine.slice(openerEndIndex + 1));
|
|
978
|
+
for (let lineIndex = opener.lineIndex + 1; lineIndex < closerLineIndex; lineIndex++) segments.push(lines[lineIndex]);
|
|
979
|
+
segments.push(lines[closerLineIndex].slice(0, closerColumn));
|
|
980
|
+
bodyText = segments.join("\n");
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
closerLineIndex,
|
|
984
|
+
closerColumn,
|
|
985
|
+
bodyText
|
|
986
|
+
};
|
|
987
|
+
};
|
|
988
|
+
const isInsideStringOnlyWrapper = (lines, diagnosticLine, diagnosticColumn, wrapperNames) => {
|
|
989
|
+
const diagnosticLineIndex = diagnosticLine - 1;
|
|
990
|
+
const diagnosticColumnIndex = Math.max(0, diagnosticColumn - 1);
|
|
991
|
+
let upperBoundLineIndex = diagnosticLineIndex;
|
|
992
|
+
while (upperBoundLineIndex >= 0) {
|
|
993
|
+
const opener = findOpenerAtOrAbove(lines, upperBoundLineIndex);
|
|
994
|
+
if (!opener) return false;
|
|
995
|
+
const range = resolveJsxRange(lines, opener);
|
|
996
|
+
if (range === null) {
|
|
997
|
+
upperBoundLineIndex = opener.lineIndex - 1;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (range.closerLineIndex < diagnosticLineIndex || range.closerLineIndex === diagnosticLineIndex && range.closerColumn <= diagnosticColumnIndex) {
|
|
1001
|
+
upperBoundLineIndex = opener.lineIndex - 1;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (!wrapperNames.has(opener.fullName) && !wrapperNames.has(opener.leafName)) return false;
|
|
1005
|
+
return !JSX_CHILD_OPEN_PATTERN.test(range.bodyText);
|
|
1006
|
+
}
|
|
1007
|
+
return false;
|
|
1008
|
+
};
|
|
934
1009
|
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
|
|
935
1010
|
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
|
|
936
1011
|
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
937
1012
|
const compiledOverrides = compileIgnoreOverrides(config);
|
|
938
1013
|
const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents.filter((name) => typeof name === "string") : []);
|
|
939
1014
|
const hasTextComponents = textComponentNames.size > 0;
|
|
1015
|
+
const rawTextWrapperComponentNames = new Set(Array.isArray(config.rawTextWrapperComponents) ? config.rawTextWrapperComponents.filter((name) => typeof name === "string") : []);
|
|
1016
|
+
const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
|
|
940
1017
|
const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
|
|
941
1018
|
return diagnostics.filter((diagnostic) => {
|
|
942
1019
|
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
943
1020
|
if (ignoredRules.has(ruleIdentifier)) return false;
|
|
944
1021
|
if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
|
|
945
1022
|
if (isDiagnosticIgnoredByOverrides(diagnostic, rootDirectory, compiledOverrides)) return false;
|
|
946
|
-
if (hasTextComponents && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
|
|
1023
|
+
if ((hasTextComponents || hasRawTextWrappers) && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
|
|
947
1024
|
const lines = getFileLines(diagnostic.filePath);
|
|
948
|
-
if (lines
|
|
1025
|
+
if (lines) {
|
|
1026
|
+
if (hasTextComponents && isInsideTextComponent(lines, diagnostic.line, textComponentNames)) return false;
|
|
1027
|
+
if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return false;
|
|
1028
|
+
}
|
|
949
1029
|
}
|
|
950
1030
|
return true;
|
|
951
1031
|
});
|
|
@@ -2177,22 +2257,22 @@ const TANSTACK_START_RULES = {
|
|
|
2177
2257
|
"react-doctor/tanstack-start-loader-parallel-fetch": "warn"
|
|
2178
2258
|
};
|
|
2179
2259
|
const REACT_COMPILER_RULES = {
|
|
2180
|
-
"react-hooks-js/set-state-in-render": "
|
|
2181
|
-
"react-hooks-js/immutability": "
|
|
2182
|
-
"react-hooks-js/refs": "
|
|
2183
|
-
"react-hooks-js/purity": "
|
|
2184
|
-
"react-hooks-js/hooks": "
|
|
2185
|
-
"react-hooks-js/set-state-in-effect": "
|
|
2186
|
-
"react-hooks-js/globals": "
|
|
2187
|
-
"react-hooks-js/error-boundaries": "
|
|
2188
|
-
"react-hooks-js/preserve-manual-memoization": "
|
|
2189
|
-
"react-hooks-js/unsupported-syntax": "
|
|
2190
|
-
"react-hooks-js/component-hook-factories": "
|
|
2191
|
-
"react-hooks-js/static-components": "
|
|
2192
|
-
"react-hooks-js/use-memo": "
|
|
2193
|
-
"react-hooks-js/void-use-memo": "
|
|
2194
|
-
"react-hooks-js/incompatible-library": "
|
|
2195
|
-
"react-hooks-js/todo": "
|
|
2260
|
+
"react-hooks-js/set-state-in-render": "error",
|
|
2261
|
+
"react-hooks-js/immutability": "error",
|
|
2262
|
+
"react-hooks-js/refs": "error",
|
|
2263
|
+
"react-hooks-js/purity": "error",
|
|
2264
|
+
"react-hooks-js/hooks": "error",
|
|
2265
|
+
"react-hooks-js/set-state-in-effect": "error",
|
|
2266
|
+
"react-hooks-js/globals": "error",
|
|
2267
|
+
"react-hooks-js/error-boundaries": "error",
|
|
2268
|
+
"react-hooks-js/preserve-manual-memoization": "error",
|
|
2269
|
+
"react-hooks-js/unsupported-syntax": "error",
|
|
2270
|
+
"react-hooks-js/component-hook-factories": "error",
|
|
2271
|
+
"react-hooks-js/static-components": "error",
|
|
2272
|
+
"react-hooks-js/use-memo": "error",
|
|
2273
|
+
"react-hooks-js/void-use-memo": "error",
|
|
2274
|
+
"react-hooks-js/incompatible-library": "error",
|
|
2275
|
+
"react-hooks-js/todo": "error"
|
|
2196
2276
|
};
|
|
2197
2277
|
const readPluginRuleNames = (pluginSpecifier) => {
|
|
2198
2278
|
try {
|
|
@@ -3251,18 +3331,13 @@ const printDiagnostics = (diagnostics, isVerbose, rootDirectory) => {
|
|
|
3251
3331
|
const visibleRuleGroups = isVerbose ? sortedRuleGroups : sortedRuleGroups.slice(0, 5);
|
|
3252
3332
|
const hiddenRuleGroups = isVerbose ? [] : sortedRuleGroups.slice(5);
|
|
3253
3333
|
const ruleNameColumnWidth = computeRuleNameColumnWidth(visibleRuleGroups.map(([ruleKey]) => ruleKey));
|
|
3254
|
-
visibleRuleGroups.forEach(([ruleKey, ruleDiagnostics]
|
|
3334
|
+
visibleRuleGroups.forEach(([ruleKey, ruleDiagnostics]) => {
|
|
3255
3335
|
if (isVerbose) {
|
|
3256
3336
|
printVerboseRuleGroup(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
|
|
3257
3337
|
return;
|
|
3258
3338
|
}
|
|
3259
|
-
|
|
3260
|
-
printDetailedRuleGroup(ruleKey, ruleDiagnostics, rootDirectory, ruleNameColumnWidth);
|
|
3261
|
-
return;
|
|
3262
|
-
}
|
|
3263
|
-
printCompactRuleGroupLine(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
|
|
3339
|
+
printDetailedRuleGroup(ruleKey, ruleDiagnostics, rootDirectory, ruleNameColumnWidth);
|
|
3264
3340
|
});
|
|
3265
|
-
if (visibleRuleGroups.length > 1 && !isVerbose) logger.break();
|
|
3266
3341
|
if (hiddenRuleGroups.length > 0) printHiddenDiagnosticsSummary(hiddenRuleGroups);
|
|
3267
3342
|
};
|
|
3268
3343
|
const printHiddenDiagnosticsSummary = (hiddenRuleGroups) => {
|
|
@@ -3608,6 +3683,7 @@ const runScan = async (directory, options, userConfig, startTime) => {
|
|
|
3608
3683
|
else printNoScoreHeader(noScoreMessage);
|
|
3609
3684
|
return buildResult();
|
|
3610
3685
|
}
|
|
3686
|
+
logger.break();
|
|
3611
3687
|
printDiagnostics(diagnostics, options.verbose, directory);
|
|
3612
3688
|
const displayedSourceFileCount = isDiffMode ? includePaths.length : lintSourceFileCount;
|
|
3613
3689
|
const shouldShowShareLink = !options.offline && options.share;
|
|
@@ -4026,7 +4102,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
4026
4102
|
};
|
|
4027
4103
|
//#endregion
|
|
4028
4104
|
//#region src/cli.ts
|
|
4029
|
-
const VERSION = "0.1.
|
|
4105
|
+
const VERSION = "0.1.3";
|
|
4030
4106
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
4031
4107
|
"error",
|
|
4032
4108
|
"warning",
|
package/dist/eslint-plugin.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -76,6 +76,23 @@ interface ReactDoctorConfig {
|
|
|
76
76
|
customRulesOnly?: boolean;
|
|
77
77
|
share?: boolean;
|
|
78
78
|
textComponents?: string[];
|
|
79
|
+
/**
|
|
80
|
+
* Names of components that safely route string-only children through a
|
|
81
|
+
* React Native `<Text>` internally (e.g. `heroui-native`'s `Button`,
|
|
82
|
+
* which stringifies its children and renders them through a
|
|
83
|
+
* `ButtonLabel` → `Text`). For listed components, `rn-no-raw-text`
|
|
84
|
+
* is suppressed ONLY when the wrapper's children are entirely
|
|
85
|
+
* stringifiable (no nested JSX elements). A wrapper with mixed
|
|
86
|
+
* children — e.g. `<Button>Save<Icon /></Button>` — still reports,
|
|
87
|
+
* because the wrapper can't safely route raw text alongside a
|
|
88
|
+
* sibling JSX element.
|
|
89
|
+
*
|
|
90
|
+
* Use this instead of `textComponents` when the component is not
|
|
91
|
+
* itself a text element but is known to wrap its string children
|
|
92
|
+
* in one. `textComponents` is the broader escape hatch and
|
|
93
|
+
* suppresses regardless of sibling content.
|
|
94
|
+
*/
|
|
95
|
+
rawTextWrapperComponents?: string[];
|
|
79
96
|
/**
|
|
80
97
|
* Whether to respect inline `// eslint-disable*`, `// oxlint-disable*`,
|
|
81
98
|
* and `// react-doctor-disable*` comments in source files. Default: `true`.
|
package/dist/index.js
CHANGED
|
@@ -1349,6 +1349,8 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
|
1349
1349
|
//#endregion
|
|
1350
1350
|
//#region src/utils/filter-diagnostics.ts
|
|
1351
1351
|
const OPENING_TAG_PATTERN = /<([A-Z][\w.]*)/;
|
|
1352
|
+
const JSX_CHILD_OPEN_PATTERN = /<[A-Za-z]/;
|
|
1353
|
+
const escapeRegExpSpecials = (rawText) => rawText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1352
1354
|
const resolveCandidateReadPath = (rootDirectory, filePath) => {
|
|
1353
1355
|
const normalizedFile = filePath.replace(/\\/g, "/");
|
|
1354
1356
|
if (normalizedFile.startsWith("/") || /^[a-zA-Z]:\//.test(normalizedFile) || /^[a-zA-Z]:\\/.test(filePath)) return filePath;
|
|
@@ -1374,21 +1376,93 @@ const isInsideTextComponent = (lines, diagnosticLine, textComponentNames) => {
|
|
|
1374
1376
|
}
|
|
1375
1377
|
return false;
|
|
1376
1378
|
};
|
|
1379
|
+
const findOpenerAtOrAbove = (lines, upperBoundLineIndex) => {
|
|
1380
|
+
for (let lineIndex = upperBoundLineIndex; lineIndex >= 0; lineIndex--) {
|
|
1381
|
+
const match = lines[lineIndex].match(OPENING_TAG_PATTERN);
|
|
1382
|
+
if (!match) continue;
|
|
1383
|
+
const fullName = match[1];
|
|
1384
|
+
return {
|
|
1385
|
+
fullName,
|
|
1386
|
+
leafName: fullName.includes(".") ? fullName.split(".").at(-1) ?? fullName : fullName,
|
|
1387
|
+
lineIndex
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
return null;
|
|
1391
|
+
};
|
|
1392
|
+
const resolveJsxRange = (lines, opener) => {
|
|
1393
|
+
const closingPattern = new RegExp(`</(?:${escapeRegExpSpecials(opener.fullName)}|${escapeRegExpSpecials(opener.leafName)})\\s*>`);
|
|
1394
|
+
let closerLineIndex = -1;
|
|
1395
|
+
let closerColumn = -1;
|
|
1396
|
+
for (let lineIndex = opener.lineIndex; lineIndex < lines.length; lineIndex++) {
|
|
1397
|
+
const match = closingPattern.exec(lines[lineIndex]);
|
|
1398
|
+
if (!match) continue;
|
|
1399
|
+
closerLineIndex = lineIndex;
|
|
1400
|
+
closerColumn = match.index;
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
if (closerLineIndex < 0) return null;
|
|
1404
|
+
const openerLine = lines[opener.lineIndex];
|
|
1405
|
+
const tagStartIndex = openerLine.indexOf(`<${opener.fullName}`);
|
|
1406
|
+
if (tagStartIndex < 0) return null;
|
|
1407
|
+
const openerEndIndex = openerLine.indexOf(">", tagStartIndex);
|
|
1408
|
+
let bodyText;
|
|
1409
|
+
if (opener.lineIndex === closerLineIndex) {
|
|
1410
|
+
if (openerEndIndex < 0 || openerEndIndex >= closerColumn) return null;
|
|
1411
|
+
bodyText = openerLine.slice(openerEndIndex + 1, closerColumn);
|
|
1412
|
+
} else {
|
|
1413
|
+
const segments = [];
|
|
1414
|
+
if (openerEndIndex >= 0) segments.push(openerLine.slice(openerEndIndex + 1));
|
|
1415
|
+
for (let lineIndex = opener.lineIndex + 1; lineIndex < closerLineIndex; lineIndex++) segments.push(lines[lineIndex]);
|
|
1416
|
+
segments.push(lines[closerLineIndex].slice(0, closerColumn));
|
|
1417
|
+
bodyText = segments.join("\n");
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
closerLineIndex,
|
|
1421
|
+
closerColumn,
|
|
1422
|
+
bodyText
|
|
1423
|
+
};
|
|
1424
|
+
};
|
|
1425
|
+
const isInsideStringOnlyWrapper = (lines, diagnosticLine, diagnosticColumn, wrapperNames) => {
|
|
1426
|
+
const diagnosticLineIndex = diagnosticLine - 1;
|
|
1427
|
+
const diagnosticColumnIndex = Math.max(0, diagnosticColumn - 1);
|
|
1428
|
+
let upperBoundLineIndex = diagnosticLineIndex;
|
|
1429
|
+
while (upperBoundLineIndex >= 0) {
|
|
1430
|
+
const opener = findOpenerAtOrAbove(lines, upperBoundLineIndex);
|
|
1431
|
+
if (!opener) return false;
|
|
1432
|
+
const range = resolveJsxRange(lines, opener);
|
|
1433
|
+
if (range === null) {
|
|
1434
|
+
upperBoundLineIndex = opener.lineIndex - 1;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
if (range.closerLineIndex < diagnosticLineIndex || range.closerLineIndex === diagnosticLineIndex && range.closerColumn <= diagnosticColumnIndex) {
|
|
1438
|
+
upperBoundLineIndex = opener.lineIndex - 1;
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (!wrapperNames.has(opener.fullName) && !wrapperNames.has(opener.leafName)) return false;
|
|
1442
|
+
return !JSX_CHILD_OPEN_PATTERN.test(range.bodyText);
|
|
1443
|
+
}
|
|
1444
|
+
return false;
|
|
1445
|
+
};
|
|
1377
1446
|
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
|
|
1378
1447
|
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
|
|
1379
1448
|
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
1380
1449
|
const compiledOverrides = compileIgnoreOverrides(config);
|
|
1381
1450
|
const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents.filter((name) => typeof name === "string") : []);
|
|
1382
1451
|
const hasTextComponents = textComponentNames.size > 0;
|
|
1452
|
+
const rawTextWrapperComponentNames = new Set(Array.isArray(config.rawTextWrapperComponents) ? config.rawTextWrapperComponents.filter((name) => typeof name === "string") : []);
|
|
1453
|
+
const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
|
|
1383
1454
|
const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
|
|
1384
1455
|
return diagnostics.filter((diagnostic) => {
|
|
1385
1456
|
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
1386
1457
|
if (ignoredRules.has(ruleIdentifier)) return false;
|
|
1387
1458
|
if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
|
|
1388
1459
|
if (isDiagnosticIgnoredByOverrides(diagnostic, rootDirectory, compiledOverrides)) return false;
|
|
1389
|
-
if (hasTextComponents && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
|
|
1460
|
+
if ((hasTextComponents || hasRawTextWrappers) && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
|
|
1390
1461
|
const lines = getFileLines(diagnostic.filePath);
|
|
1391
|
-
if (lines
|
|
1462
|
+
if (lines) {
|
|
1463
|
+
if (hasTextComponents && isInsideTextComponent(lines, diagnostic.line, textComponentNames)) return false;
|
|
1464
|
+
if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return false;
|
|
1465
|
+
}
|
|
1392
1466
|
}
|
|
1393
1467
|
return true;
|
|
1394
1468
|
});
|
|
@@ -1850,22 +1924,22 @@ const TANSTACK_START_RULES = {
|
|
|
1850
1924
|
"react-doctor/tanstack-start-loader-parallel-fetch": "warn"
|
|
1851
1925
|
};
|
|
1852
1926
|
const REACT_COMPILER_RULES = {
|
|
1853
|
-
"react-hooks-js/set-state-in-render": "
|
|
1854
|
-
"react-hooks-js/immutability": "
|
|
1855
|
-
"react-hooks-js/refs": "
|
|
1856
|
-
"react-hooks-js/purity": "
|
|
1857
|
-
"react-hooks-js/hooks": "
|
|
1858
|
-
"react-hooks-js/set-state-in-effect": "
|
|
1859
|
-
"react-hooks-js/globals": "
|
|
1860
|
-
"react-hooks-js/error-boundaries": "
|
|
1861
|
-
"react-hooks-js/preserve-manual-memoization": "
|
|
1862
|
-
"react-hooks-js/unsupported-syntax": "
|
|
1863
|
-
"react-hooks-js/component-hook-factories": "
|
|
1864
|
-
"react-hooks-js/static-components": "
|
|
1865
|
-
"react-hooks-js/use-memo": "
|
|
1866
|
-
"react-hooks-js/void-use-memo": "
|
|
1867
|
-
"react-hooks-js/incompatible-library": "
|
|
1868
|
-
"react-hooks-js/todo": "
|
|
1927
|
+
"react-hooks-js/set-state-in-render": "error",
|
|
1928
|
+
"react-hooks-js/immutability": "error",
|
|
1929
|
+
"react-hooks-js/refs": "error",
|
|
1930
|
+
"react-hooks-js/purity": "error",
|
|
1931
|
+
"react-hooks-js/hooks": "error",
|
|
1932
|
+
"react-hooks-js/set-state-in-effect": "error",
|
|
1933
|
+
"react-hooks-js/globals": "error",
|
|
1934
|
+
"react-hooks-js/error-boundaries": "error",
|
|
1935
|
+
"react-hooks-js/preserve-manual-memoization": "error",
|
|
1936
|
+
"react-hooks-js/unsupported-syntax": "error",
|
|
1937
|
+
"react-hooks-js/component-hook-factories": "error",
|
|
1938
|
+
"react-hooks-js/static-components": "error",
|
|
1939
|
+
"react-hooks-js/use-memo": "error",
|
|
1940
|
+
"react-hooks-js/void-use-memo": "error",
|
|
1941
|
+
"react-hooks-js/incompatible-library": "error",
|
|
1942
|
+
"react-hooks-js/todo": "error"
|
|
1869
1943
|
};
|
|
1870
1944
|
const readPluginRuleNames = (pluginSpecifier) => {
|
|
1871
1945
|
try {
|
package/package.json
CHANGED