react-doctor 0.1.2 → 0.1.4
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 +185 -85
- package/dist/eslint-plugin.js +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.js +93 -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
|
@@ -214,7 +214,7 @@ const finalize = (method, originalText, displayText) => {
|
|
|
214
214
|
sharedInstance.stop();
|
|
215
215
|
ora({
|
|
216
216
|
text: displayText,
|
|
217
|
-
indent:
|
|
217
|
+
indent: 0
|
|
218
218
|
}).start()[method](displayText);
|
|
219
219
|
const [remainingText] = pendingTexts;
|
|
220
220
|
if (remainingText) sharedInstance.text = remainingText;
|
|
@@ -226,7 +226,7 @@ const spinner = (text) => ({ start() {
|
|
|
226
226
|
pendingTexts.add(text);
|
|
227
227
|
if (!sharedInstance) sharedInstance = ora({
|
|
228
228
|
text,
|
|
229
|
-
indent:
|
|
229
|
+
indent: 0
|
|
230
230
|
}).start();
|
|
231
231
|
else sharedInstance.text = text;
|
|
232
232
|
const handle = {
|
|
@@ -304,28 +304,6 @@ const runInstallSkill = async (options = {}) => {
|
|
|
304
304
|
}
|
|
305
305
|
};
|
|
306
306
|
//#endregion
|
|
307
|
-
//#region src/utils/build-category-breakdown.ts
|
|
308
|
-
const buildCategoryBreakdown = (diagnostics) => {
|
|
309
|
-
const entriesByCategory = /* @__PURE__ */ new Map();
|
|
310
|
-
for (const diagnostic of diagnostics) {
|
|
311
|
-
const existingEntry = entriesByCategory.get(diagnostic.category) ?? {
|
|
312
|
-
category: diagnostic.category,
|
|
313
|
-
totalCount: 0,
|
|
314
|
-
errorCount: 0,
|
|
315
|
-
warningCount: 0
|
|
316
|
-
};
|
|
317
|
-
existingEntry.totalCount += 1;
|
|
318
|
-
if (diagnostic.severity === "error") existingEntry.errorCount += 1;
|
|
319
|
-
else existingEntry.warningCount += 1;
|
|
320
|
-
entriesByCategory.set(diagnostic.category, existingEntry);
|
|
321
|
-
}
|
|
322
|
-
return [...entriesByCategory.values()].sort((entryA, entryB) => {
|
|
323
|
-
if (entryA.errorCount !== entryB.errorCount) return entryB.errorCount - entryA.errorCount;
|
|
324
|
-
if (entryA.totalCount !== entryB.totalCount) return entryB.totalCount - entryA.totalCount;
|
|
325
|
-
return entryA.category.localeCompare(entryB.category);
|
|
326
|
-
});
|
|
327
|
-
};
|
|
328
|
-
//#endregion
|
|
329
307
|
//#region src/utils/build-hidden-diagnostics-summary.ts
|
|
330
308
|
const buildHiddenDiagnosticsSummary = (hiddenDiagnostics) => {
|
|
331
309
|
const errorCount = hiddenDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
@@ -912,6 +890,8 @@ const isFileIgnoredByPatterns = (filePath, rootDirectory, patterns) => {
|
|
|
912
890
|
//#endregion
|
|
913
891
|
//#region src/utils/filter-diagnostics.ts
|
|
914
892
|
const OPENING_TAG_PATTERN = /<([A-Z][\w.]*)/;
|
|
893
|
+
const JSX_CHILD_OPEN_PATTERN = /<[A-Za-z]/;
|
|
894
|
+
const escapeRegExpSpecials = (rawText) => rawText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
915
895
|
const resolveCandidateReadPath = (rootDirectory, filePath) => {
|
|
916
896
|
const normalizedFile = filePath.replace(/\\/g, "/");
|
|
917
897
|
if (normalizedFile.startsWith("/") || /^[a-zA-Z]:\//.test(normalizedFile) || /^[a-zA-Z]:\\/.test(filePath)) return filePath;
|
|
@@ -937,21 +917,93 @@ const isInsideTextComponent = (lines, diagnosticLine, textComponentNames) => {
|
|
|
937
917
|
}
|
|
938
918
|
return false;
|
|
939
919
|
};
|
|
920
|
+
const findOpenerAtOrAbove = (lines, upperBoundLineIndex) => {
|
|
921
|
+
for (let lineIndex = upperBoundLineIndex; lineIndex >= 0; lineIndex--) {
|
|
922
|
+
const match = lines[lineIndex].match(OPENING_TAG_PATTERN);
|
|
923
|
+
if (!match) continue;
|
|
924
|
+
const fullName = match[1];
|
|
925
|
+
return {
|
|
926
|
+
fullName,
|
|
927
|
+
leafName: fullName.includes(".") ? fullName.split(".").at(-1) ?? fullName : fullName,
|
|
928
|
+
lineIndex
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
};
|
|
933
|
+
const resolveJsxRange = (lines, opener) => {
|
|
934
|
+
const closingPattern = new RegExp(`</(?:${escapeRegExpSpecials(opener.fullName)}|${escapeRegExpSpecials(opener.leafName)})\\s*>`);
|
|
935
|
+
let closerLineIndex = -1;
|
|
936
|
+
let closerColumn = -1;
|
|
937
|
+
for (let lineIndex = opener.lineIndex; lineIndex < lines.length; lineIndex++) {
|
|
938
|
+
const match = closingPattern.exec(lines[lineIndex]);
|
|
939
|
+
if (!match) continue;
|
|
940
|
+
closerLineIndex = lineIndex;
|
|
941
|
+
closerColumn = match.index;
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
if (closerLineIndex < 0) return null;
|
|
945
|
+
const openerLine = lines[opener.lineIndex];
|
|
946
|
+
const tagStartIndex = openerLine.indexOf(`<${opener.fullName}`);
|
|
947
|
+
if (tagStartIndex < 0) return null;
|
|
948
|
+
const openerEndIndex = openerLine.indexOf(">", tagStartIndex);
|
|
949
|
+
let bodyText;
|
|
950
|
+
if (opener.lineIndex === closerLineIndex) {
|
|
951
|
+
if (openerEndIndex < 0 || openerEndIndex >= closerColumn) return null;
|
|
952
|
+
bodyText = openerLine.slice(openerEndIndex + 1, closerColumn);
|
|
953
|
+
} else {
|
|
954
|
+
const segments = [];
|
|
955
|
+
if (openerEndIndex >= 0) segments.push(openerLine.slice(openerEndIndex + 1));
|
|
956
|
+
for (let lineIndex = opener.lineIndex + 1; lineIndex < closerLineIndex; lineIndex++) segments.push(lines[lineIndex]);
|
|
957
|
+
segments.push(lines[closerLineIndex].slice(0, closerColumn));
|
|
958
|
+
bodyText = segments.join("\n");
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
closerLineIndex,
|
|
962
|
+
closerColumn,
|
|
963
|
+
bodyText
|
|
964
|
+
};
|
|
965
|
+
};
|
|
966
|
+
const isInsideStringOnlyWrapper = (lines, diagnosticLine, diagnosticColumn, wrapperNames) => {
|
|
967
|
+
const diagnosticLineIndex = diagnosticLine - 1;
|
|
968
|
+
const diagnosticColumnIndex = Math.max(0, diagnosticColumn - 1);
|
|
969
|
+
let upperBoundLineIndex = diagnosticLineIndex;
|
|
970
|
+
while (upperBoundLineIndex >= 0) {
|
|
971
|
+
const opener = findOpenerAtOrAbove(lines, upperBoundLineIndex);
|
|
972
|
+
if (!opener) return false;
|
|
973
|
+
const range = resolveJsxRange(lines, opener);
|
|
974
|
+
if (range === null) {
|
|
975
|
+
upperBoundLineIndex = opener.lineIndex - 1;
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
if (range.closerLineIndex < diagnosticLineIndex || range.closerLineIndex === diagnosticLineIndex && range.closerColumn <= diagnosticColumnIndex) {
|
|
979
|
+
upperBoundLineIndex = opener.lineIndex - 1;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (!wrapperNames.has(opener.fullName) && !wrapperNames.has(opener.leafName)) return false;
|
|
983
|
+
return !JSX_CHILD_OPEN_PATTERN.test(range.bodyText);
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
};
|
|
940
987
|
const filterIgnoredDiagnostics = (diagnostics, config, rootDirectory, readFileLinesSync) => {
|
|
941
988
|
const ignoredRules = new Set(Array.isArray(config.ignore?.rules) ? config.ignore.rules.filter((rule) => typeof rule === "string") : []);
|
|
942
989
|
const ignoredFilePatterns = compileIgnoredFilePatterns(config);
|
|
943
990
|
const compiledOverrides = compileIgnoreOverrides(config);
|
|
944
991
|
const textComponentNames = new Set(Array.isArray(config.textComponents) ? config.textComponents.filter((name) => typeof name === "string") : []);
|
|
945
992
|
const hasTextComponents = textComponentNames.size > 0;
|
|
993
|
+
const rawTextWrapperComponentNames = new Set(Array.isArray(config.rawTextWrapperComponents) ? config.rawTextWrapperComponents.filter((name) => typeof name === "string") : []);
|
|
994
|
+
const hasRawTextWrappers = rawTextWrapperComponentNames.size > 0;
|
|
946
995
|
const getFileLines = createFileLinesCache(rootDirectory, readFileLinesSync);
|
|
947
996
|
return diagnostics.filter((diagnostic) => {
|
|
948
997
|
const ruleIdentifier = `${diagnostic.plugin}/${diagnostic.rule}`;
|
|
949
998
|
if (ignoredRules.has(ruleIdentifier)) return false;
|
|
950
999
|
if (isFileIgnoredByPatterns(diagnostic.filePath, rootDirectory, ignoredFilePatterns)) return false;
|
|
951
1000
|
if (isDiagnosticIgnoredByOverrides(diagnostic, rootDirectory, compiledOverrides)) return false;
|
|
952
|
-
if (hasTextComponents && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
|
|
1001
|
+
if ((hasTextComponents || hasRawTextWrappers) && diagnostic.rule === "rn-no-raw-text" && diagnostic.line > 0) {
|
|
953
1002
|
const lines = getFileLines(diagnostic.filePath);
|
|
954
|
-
if (lines
|
|
1003
|
+
if (lines) {
|
|
1004
|
+
if (hasTextComponents && isInsideTextComponent(lines, diagnostic.line, textComponentNames)) return false;
|
|
1005
|
+
if (hasRawTextWrappers && isInsideStringOnlyWrapper(lines, diagnostic.line, diagnostic.column, rawTextWrapperComponentNames)) return false;
|
|
1006
|
+
}
|
|
955
1007
|
}
|
|
956
1008
|
return true;
|
|
957
1009
|
});
|
|
@@ -1601,6 +1653,32 @@ const loadConfig = (rootDirectory) => {
|
|
|
1601
1653
|
return null;
|
|
1602
1654
|
};
|
|
1603
1655
|
//#endregion
|
|
1656
|
+
//#region src/utils/wrap-indented-text.ts
|
|
1657
|
+
const wrapLine = (lineText, contentWidth) => {
|
|
1658
|
+
if (lineText.length <= contentWidth) return [lineText];
|
|
1659
|
+
const wrappedLines = [];
|
|
1660
|
+
let remainingText = lineText.trim();
|
|
1661
|
+
while (remainingText.length > contentWidth) {
|
|
1662
|
+
const candidateText = remainingText.slice(0, contentWidth);
|
|
1663
|
+
const breakIndex = candidateText.lastIndexOf(" ");
|
|
1664
|
+
if (breakIndex <= 0) {
|
|
1665
|
+
wrappedLines.push(candidateText);
|
|
1666
|
+
remainingText = remainingText.slice(contentWidth).trimStart();
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
wrappedLines.push(remainingText.slice(0, breakIndex));
|
|
1670
|
+
remainingText = remainingText.slice(breakIndex + 1).trimStart();
|
|
1671
|
+
}
|
|
1672
|
+
if (remainingText.length > 0) wrappedLines.push(remainingText);
|
|
1673
|
+
return wrappedLines;
|
|
1674
|
+
};
|
|
1675
|
+
const wrapIndentedText = (text, linePrefix, width) => {
|
|
1676
|
+
const contentWidth = width - linePrefix.length;
|
|
1677
|
+
if (contentWidth <= 0) return indentOnly(text, linePrefix);
|
|
1678
|
+
return text.split("\n").flatMap((lineText) => wrapLine(lineText, contentWidth)).map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
1679
|
+
};
|
|
1680
|
+
const indentOnly = (text, linePrefix) => text.split("\n").map((lineText) => `${linePrefix}${lineText}`).join("\n");
|
|
1681
|
+
//#endregion
|
|
1604
1682
|
//#region src/utils/resolve-compatible-node.ts
|
|
1605
1683
|
const parseNodeVersion = (versionString) => {
|
|
1606
1684
|
const [major = 0, minor = 0, patch = 0] = versionString.replace(/^v/, "").trim().split(".").map(Number);
|
|
@@ -2183,22 +2261,22 @@ const TANSTACK_START_RULES = {
|
|
|
2183
2261
|
"react-doctor/tanstack-start-loader-parallel-fetch": "warn"
|
|
2184
2262
|
};
|
|
2185
2263
|
const REACT_COMPILER_RULES = {
|
|
2186
|
-
"react-hooks-js/set-state-in-render": "
|
|
2187
|
-
"react-hooks-js/immutability": "
|
|
2188
|
-
"react-hooks-js/refs": "
|
|
2189
|
-
"react-hooks-js/purity": "
|
|
2190
|
-
"react-hooks-js/hooks": "
|
|
2191
|
-
"react-hooks-js/set-state-in-effect": "
|
|
2192
|
-
"react-hooks-js/globals": "
|
|
2193
|
-
"react-hooks-js/error-boundaries": "
|
|
2194
|
-
"react-hooks-js/preserve-manual-memoization": "
|
|
2195
|
-
"react-hooks-js/unsupported-syntax": "
|
|
2196
|
-
"react-hooks-js/component-hook-factories": "
|
|
2197
|
-
"react-hooks-js/static-components": "
|
|
2198
|
-
"react-hooks-js/use-memo": "
|
|
2199
|
-
"react-hooks-js/void-use-memo": "
|
|
2200
|
-
"react-hooks-js/incompatible-library": "
|
|
2201
|
-
"react-hooks-js/todo": "
|
|
2264
|
+
"react-hooks-js/set-state-in-render": "error",
|
|
2265
|
+
"react-hooks-js/immutability": "error",
|
|
2266
|
+
"react-hooks-js/refs": "error",
|
|
2267
|
+
"react-hooks-js/purity": "error",
|
|
2268
|
+
"react-hooks-js/hooks": "error",
|
|
2269
|
+
"react-hooks-js/set-state-in-effect": "error",
|
|
2270
|
+
"react-hooks-js/globals": "error",
|
|
2271
|
+
"react-hooks-js/error-boundaries": "error",
|
|
2272
|
+
"react-hooks-js/preserve-manual-memoization": "error",
|
|
2273
|
+
"react-hooks-js/unsupported-syntax": "error",
|
|
2274
|
+
"react-hooks-js/component-hook-factories": "error",
|
|
2275
|
+
"react-hooks-js/static-components": "error",
|
|
2276
|
+
"react-hooks-js/use-memo": "error",
|
|
2277
|
+
"react-hooks-js/void-use-memo": "error",
|
|
2278
|
+
"react-hooks-js/incompatible-library": "error",
|
|
2279
|
+
"react-hooks-js/todo": "error"
|
|
2202
2280
|
};
|
|
2203
2281
|
const readPluginRuleNames = (pluginSpecifier) => {
|
|
2204
2282
|
try {
|
|
@@ -3080,6 +3158,7 @@ const parseOxlintOutput = (stdout) => {
|
|
|
3080
3158
|
severity: diagnostic.severity,
|
|
3081
3159
|
message: cleaned.message,
|
|
3082
3160
|
help: cleaned.help,
|
|
3161
|
+
url: diagnostic.url,
|
|
3083
3162
|
line: primaryLabel?.span.line ?? 0,
|
|
3084
3163
|
column: primaryLabel?.span.column ?? 0,
|
|
3085
3164
|
category: resolveDiagnosticCategory(plugin, rule)
|
|
@@ -3211,6 +3290,11 @@ const buildVerboseSiteMap = (diagnostics) => {
|
|
|
3211
3290
|
return fileSites;
|
|
3212
3291
|
};
|
|
3213
3292
|
const formatSiteCountBadge = (count) => count > 1 ? `×${count}` : "";
|
|
3293
|
+
const formatIssueCount = (count) => `${count} ${count === 1 ? "issue" : "issues"}`;
|
|
3294
|
+
const toRuleTitle = (ruleName) => {
|
|
3295
|
+
const readableRuleName = ruleName.replace(/^(no|prefer|require|use)-/, "").replace(/^(nextjs|tanstack-start)-/, "").replaceAll("-", " ");
|
|
3296
|
+
return (readableRuleName.charAt(0).toUpperCase() + readableRuleName.slice(1)).replace(/\b(css|html|url|svg|jsx|api|ua)\b/gi, (match) => match.toUpperCase());
|
|
3297
|
+
};
|
|
3214
3298
|
const computeRuleNameColumnWidth = (ruleKeys) => {
|
|
3215
3299
|
const longestRuleNameLength = ruleKeys.reduce((longest, ruleKey) => Math.max(longest, ruleKey.length), 0);
|
|
3216
3300
|
return Math.max(36, longestRuleNameLength);
|
|
@@ -3222,6 +3306,9 @@ const padRuleNameToColumn = (ruleName, columnWidth) => {
|
|
|
3222
3306
|
const grayLine = (text) => {
|
|
3223
3307
|
logger.log(highlighter.gray(text));
|
|
3224
3308
|
};
|
|
3309
|
+
const grayWrappedLine = (text, linePrefix) => {
|
|
3310
|
+
grayLine(wrapIndentedText(text, linePrefix, 88));
|
|
3311
|
+
};
|
|
3225
3312
|
const printCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
|
|
3226
3313
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
3227
3314
|
const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
|
|
@@ -3230,13 +3317,38 @@ const printCompactRuleGroupLine = (ruleKey, ruleDiagnostics, ruleNameColumnWidth
|
|
|
3230
3317
|
const trailingBadge = siteCountBadge.length > 0 ? ` ${highlighter.gray(siteCountBadge)}` : "";
|
|
3231
3318
|
logger.log(` ${icon} ${ruleNameRendering}${trailingBadge}`);
|
|
3232
3319
|
};
|
|
3233
|
-
const
|
|
3234
|
-
|
|
3320
|
+
const getWorstSeverity = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "error" : "warning";
|
|
3321
|
+
const buildCategoryDiagnosticGroups = (diagnostics) => {
|
|
3322
|
+
return [...groupBy(diagnostics, (diagnostic) => diagnostic.category).entries()].map(([category, categoryDiagnostics]) => {
|
|
3323
|
+
return {
|
|
3324
|
+
category,
|
|
3325
|
+
diagnostics: categoryDiagnostics,
|
|
3326
|
+
ruleGroups: sortByImportance([...groupBy(categoryDiagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()])
|
|
3327
|
+
};
|
|
3328
|
+
}).toSorted((categoryGroupA, categoryGroupB) => {
|
|
3329
|
+
const severityDelta = SEVERITY_ORDER[getWorstSeverity(categoryGroupA.diagnostics)] - SEVERITY_ORDER[getWorstSeverity(categoryGroupB.diagnostics)];
|
|
3330
|
+
if (severityDelta !== 0) return severityDelta;
|
|
3331
|
+
if (categoryGroupA.diagnostics.length !== categoryGroupB.diagnostics.length) return categoryGroupB.diagnostics.length - categoryGroupA.diagnostics.length;
|
|
3332
|
+
return categoryGroupA.category.localeCompare(categoryGroupB.category);
|
|
3333
|
+
});
|
|
3334
|
+
};
|
|
3335
|
+
const printDefaultRuleGroup = (ruleKey, ruleDiagnostics, rootDirectory) => {
|
|
3235
3336
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
3236
|
-
|
|
3237
|
-
|
|
3337
|
+
const ruleTitle = toRuleTitle(firstDiagnostic.rule);
|
|
3338
|
+
const icon = colorizeBySeverity(firstDiagnostic.severity === "error" ? "✗" : "⚠", firstDiagnostic.severity);
|
|
3339
|
+
const siteCountBadge = formatSiteCountBadge(ruleDiagnostics.length);
|
|
3340
|
+
const trailingBadge = siteCountBadge.length > 0 ? ` ${highlighter.gray(siteCountBadge)}` : "";
|
|
3341
|
+
logger.log(` ${icon} ${ruleTitle}${trailingBadge}`);
|
|
3342
|
+
grayWrappedLine(firstDiagnostic.message, " ");
|
|
3343
|
+
if (firstDiagnostic.help) grayWrappedLine(firstDiagnostic.help, " ");
|
|
3344
|
+
if (firstDiagnostic.url) grayLine(` ${firstDiagnostic.url}`);
|
|
3238
3345
|
const firstLocation = ruleDiagnostics.find((diagnostic) => diagnostic.line > 0);
|
|
3239
|
-
if (firstLocation) grayLine(`
|
|
3346
|
+
if (firstLocation) grayLine(` ${toRelativePath(firstLocation.filePath, rootDirectory)}:${firstLocation.line}`);
|
|
3347
|
+
};
|
|
3348
|
+
const printDefaultCategoryGroup = (categoryGroup, visibleRuleGroups, rootDirectory) => {
|
|
3349
|
+
const issueCount = formatIssueCount(categoryGroup.diagnostics.length);
|
|
3350
|
+
logger.log(`${highlighter.bold(categoryGroup.category)} ${highlighter.dim(issueCount)}`);
|
|
3351
|
+
for (const [ruleKey, ruleDiagnostics] of visibleRuleGroups) printDefaultRuleGroup(ruleKey, ruleDiagnostics, rootDirectory);
|
|
3240
3352
|
logger.break();
|
|
3241
3353
|
};
|
|
3242
3354
|
const printVerboseRuleGroup = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) => {
|
|
@@ -3252,22 +3364,36 @@ const printVerboseRuleGroup = (ruleKey, ruleDiagnostics, ruleNameColumnWidth) =>
|
|
|
3252
3364
|
else grayLine(` ${filePath}`);
|
|
3253
3365
|
logger.break();
|
|
3254
3366
|
};
|
|
3367
|
+
const printDefaultDiagnostics = (diagnostics, rootDirectory) => {
|
|
3368
|
+
const categoryGroups = buildCategoryDiagnosticGroups(diagnostics);
|
|
3369
|
+
const hiddenRuleGroups = [];
|
|
3370
|
+
const visibleCategoryGroups = categoryGroups.slice(0, 5);
|
|
3371
|
+
const hiddenCategoryGroups = categoryGroups.slice(5);
|
|
3372
|
+
for (const categoryGroup of visibleCategoryGroups) {
|
|
3373
|
+
const visibleRuleGroups = categoryGroup.ruleGroups.slice(0, 3);
|
|
3374
|
+
const remainingRuleGroups = categoryGroup.ruleGroups.slice(3);
|
|
3375
|
+
printDefaultCategoryGroup(categoryGroup, visibleRuleGroups, rootDirectory);
|
|
3376
|
+
hiddenRuleGroups.push(...remainingRuleGroups);
|
|
3377
|
+
}
|
|
3378
|
+
hiddenRuleGroups.push(...hiddenCategoryGroups.flatMap((categoryGroup) => categoryGroup.ruleGroups));
|
|
3379
|
+
if (hiddenRuleGroups.length > 0) printHiddenDiagnosticsSummary(hiddenRuleGroups);
|
|
3380
|
+
};
|
|
3255
3381
|
const printDiagnostics = (diagnostics, isVerbose, rootDirectory) => {
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3382
|
+
if (!isVerbose) {
|
|
3383
|
+
printDefaultDiagnostics(diagnostics, rootDirectory);
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
const visibleRuleGroups = sortByImportance([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
|
|
3259
3387
|
const ruleNameColumnWidth = computeRuleNameColumnWidth(visibleRuleGroups.map(([ruleKey]) => ruleKey));
|
|
3260
3388
|
visibleRuleGroups.forEach(([ruleKey, ruleDiagnostics]) => {
|
|
3261
|
-
|
|
3262
|
-
printVerboseRuleGroup(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
|
|
3263
|
-
return;
|
|
3264
|
-
}
|
|
3265
|
-
printDetailedRuleGroup(ruleKey, ruleDiagnostics, rootDirectory, ruleNameColumnWidth);
|
|
3389
|
+
printVerboseRuleGroup(ruleKey, ruleDiagnostics, ruleNameColumnWidth);
|
|
3266
3390
|
});
|
|
3267
|
-
if (hiddenRuleGroups.length > 0) printHiddenDiagnosticsSummary(hiddenRuleGroups);
|
|
3268
3391
|
};
|
|
3269
3392
|
const printHiddenDiagnosticsSummary = (hiddenRuleGroups) => {
|
|
3270
|
-
const renderedParts = buildHiddenDiagnosticsSummary(hiddenRuleGroups.flatMap(([, ruleDiagnostics]) => ruleDiagnostics)).map((part) =>
|
|
3393
|
+
const renderedParts = buildHiddenDiagnosticsSummary(hiddenRuleGroups.flatMap(([, ruleDiagnostics]) => ruleDiagnostics)).map((part) => {
|
|
3394
|
+
const [icon, ...labelParts] = part.text.split(" ");
|
|
3395
|
+
return `${colorizeBySeverity(icon, part.severity)} ${highlighter.dim(labelParts.join(" "))}`;
|
|
3396
|
+
});
|
|
3271
3397
|
logger.log(` ${renderedParts.join(" ")}`);
|
|
3272
3398
|
grayLine(" Run `npx react-doctor@latest . --verbose` to get all details");
|
|
3273
3399
|
logger.break();
|
|
@@ -3287,6 +3413,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
|
|
|
3287
3413
|
firstDiagnostic.message
|
|
3288
3414
|
];
|
|
3289
3415
|
if (firstDiagnostic.help) sections.push("", `Suggestion: ${firstDiagnostic.help}`);
|
|
3416
|
+
if (firstDiagnostic.url) sections.push("", `Docs: ${firstDiagnostic.url}`);
|
|
3290
3417
|
sections.push("", "Files:");
|
|
3291
3418
|
const fileSites = buildVerboseSiteMap(ruleDiagnostics);
|
|
3292
3419
|
for (const [filePath, sites] of fileSites) if (sites.length > 0) for (const site of sites) {
|
|
@@ -3358,32 +3485,6 @@ const printNoScoreHeader = (noScoreMessage) => {
|
|
|
3358
3485
|
logger.log(` ${highlighter.gray(noScoreMessage)}`);
|
|
3359
3486
|
logger.break();
|
|
3360
3487
|
};
|
|
3361
|
-
const buildCategoryBar = (count, maximumCount, useErrorColor) => {
|
|
3362
|
-
if (maximumCount === 0) return highlighter.dim("░".repeat(16));
|
|
3363
|
-
const filledCount = Math.max(1, Math.round(count / maximumCount * 16));
|
|
3364
|
-
const cappedFilledCount = Math.min(filledCount, 16);
|
|
3365
|
-
const emptyCount = 16 - cappedFilledCount;
|
|
3366
|
-
const filledSegment = "█".repeat(cappedFilledCount);
|
|
3367
|
-
const emptySegment = "░".repeat(emptyCount);
|
|
3368
|
-
return `${useErrorColor ? highlighter.error(filledSegment) : highlighter.warn(filledSegment)}${highlighter.dim(emptySegment)}`;
|
|
3369
|
-
};
|
|
3370
|
-
const padCategoryLabel = (categoryLabel) => {
|
|
3371
|
-
if (categoryLabel.length >= 18) return categoryLabel;
|
|
3372
|
-
return categoryLabel + " ".repeat(18 - categoryLabel.length);
|
|
3373
|
-
};
|
|
3374
|
-
const printCategoryBreakdown = (entries) => {
|
|
3375
|
-
if (entries.length === 0) return;
|
|
3376
|
-
const maximumCount = Math.max(...entries.map((entry) => entry.totalCount));
|
|
3377
|
-
logger.dim(" By category");
|
|
3378
|
-
for (const entry of entries) {
|
|
3379
|
-
const paddedLabel = padCategoryLabel(entry.category);
|
|
3380
|
-
const categoryBar = buildCategoryBar(entry.totalCount, maximumCount, entry.errorCount > 0);
|
|
3381
|
-
const totalCountDisplay = String(entry.totalCount);
|
|
3382
|
-
const errorBadge = entry.errorCount > 0 ? ` ${highlighter.error(`${entry.errorCount}×`)}` : "";
|
|
3383
|
-
logger.log(` ${paddedLabel}${categoryBar} ${totalCountDisplay}${errorBadge}`);
|
|
3384
|
-
}
|
|
3385
|
-
logger.break();
|
|
3386
|
-
};
|
|
3387
3488
|
const buildShareUrl = (diagnostics, scoreResult, projectName) => {
|
|
3388
3489
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
3389
3490
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
@@ -3409,7 +3510,6 @@ const printCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMillis
|
|
|
3409
3510
|
logger.log(` ${issueCountColor(issueCountText)} ${highlighter.dim(`${fileCountText} ${elapsedTimeText}`)}`);
|
|
3410
3511
|
};
|
|
3411
3512
|
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, projectName, totalSourceFileCount, noScoreMessage, isOffline) => {
|
|
3412
|
-
printCategoryBreakdown(buildCategoryBreakdown(diagnostics));
|
|
3413
3513
|
if (scoreResult) printScoreHeader(scoreResult);
|
|
3414
3514
|
else printNoScoreHeader(noScoreMessage);
|
|
3415
3515
|
printCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds);
|
|
@@ -4028,7 +4128,7 @@ const promptProjectSelection = async (workspacePackages, rootDirectory) => {
|
|
|
4028
4128
|
};
|
|
4029
4129
|
//#endregion
|
|
4030
4130
|
//#region src/cli.ts
|
|
4031
|
-
const VERSION = "0.1.
|
|
4131
|
+
const VERSION = "0.1.4";
|
|
4032
4132
|
const VALID_FAIL_ON_LEVELS = new Set([
|
|
4033
4133
|
"error",
|
|
4034
4134
|
"warning",
|
package/dist/eslint-plugin.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ interface Diagnostic {
|
|
|
18
18
|
severity: "error" | "warning";
|
|
19
19
|
message: string;
|
|
20
20
|
help: string;
|
|
21
|
+
url?: string;
|
|
21
22
|
line: number;
|
|
22
23
|
column: number;
|
|
23
24
|
category: string;
|
|
@@ -76,6 +77,23 @@ interface ReactDoctorConfig {
|
|
|
76
77
|
customRulesOnly?: boolean;
|
|
77
78
|
share?: boolean;
|
|
78
79
|
textComponents?: string[];
|
|
80
|
+
/**
|
|
81
|
+
* Names of components that safely route string-only children through a
|
|
82
|
+
* React Native `<Text>` internally (e.g. `heroui-native`'s `Button`,
|
|
83
|
+
* which stringifies its children and renders them through a
|
|
84
|
+
* `ButtonLabel` → `Text`). For listed components, `rn-no-raw-text`
|
|
85
|
+
* is suppressed ONLY when the wrapper's children are entirely
|
|
86
|
+
* stringifiable (no nested JSX elements). A wrapper with mixed
|
|
87
|
+
* children — e.g. `<Button>Save<Icon /></Button>` — still reports,
|
|
88
|
+
* because the wrapper can't safely route raw text alongside a
|
|
89
|
+
* sibling JSX element.
|
|
90
|
+
*
|
|
91
|
+
* Use this instead of `textComponents` when the component is not
|
|
92
|
+
* itself a text element but is known to wrap its string children
|
|
93
|
+
* in one. `textComponents` is the broader escape hatch and
|
|
94
|
+
* suppresses regardless of sibling content.
|
|
95
|
+
*/
|
|
96
|
+
rawTextWrapperComponents?: string[];
|
|
79
97
|
/**
|
|
80
98
|
* Whether to respect inline `// eslint-disable*`, `// oxlint-disable*`,
|
|
81
99
|
* 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 {
|
|
@@ -2747,6 +2821,7 @@ const parseOxlintOutput = (stdout) => {
|
|
|
2747
2821
|
severity: diagnostic.severity,
|
|
2748
2822
|
message: cleaned.message,
|
|
2749
2823
|
help: cleaned.help,
|
|
2824
|
+
url: diagnostic.url,
|
|
2750
2825
|
line: primaryLabel?.span.line ?? 0,
|
|
2751
2826
|
column: primaryLabel?.span.column ?? 0,
|
|
2752
2827
|
category: resolveDiagnosticCategory(plugin, rule)
|
package/package.json
CHANGED