debtlens 0.1.0
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/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +153 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +10 -0
- package/dist/cli/init.js +18 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/parseList.d.ts +2 -0
- package/dist/cli/parseList.js +29 -0
- package/dist/cli/parseList.js.map +1 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +40 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loadConfig.d.ts +2 -0
- package/dist/config/loadConfig.js +23 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/mergeConfig.d.ts +2 -0
- package/dist/config/mergeConfig.js +26 -0
- package/dist/config/mergeConfig.js.map +1 -0
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +63 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/template.d.ts +10 -0
- package/dist/config/template.js +32 -0
- package/dist/config/template.js.map +1 -0
- package/dist/core/baseline.d.ts +23 -0
- package/dist/core/baseline.js +118 -0
- package/dist/core/baseline.js.map +1 -0
- package/dist/core/scan.d.ts +2 -0
- package/dist/core/scan.js +129 -0
- package/dist/core/scan.js.map +1 -0
- package/dist/core/severity.d.ts +7 -0
- package/dist/core/severity.js +26 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/core/types.d.ts +96 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/detectors/deadAbstraction.d.ts +2 -0
- package/dist/detectors/deadAbstraction.js +115 -0
- package/dist/detectors/deadAbstraction.js.map +1 -0
- package/dist/detectors/duplicateLogic.d.ts +2 -0
- package/dist/detectors/duplicateLogic.js +81 -0
- package/dist/detectors/duplicateLogic.js.map +1 -0
- package/dist/detectors/effectComplexity.d.ts +2 -0
- package/dist/detectors/effectComplexity.js +64 -0
- package/dist/detectors/effectComplexity.js.map +1 -0
- package/dist/detectors/index.d.ts +3 -0
- package/dist/detectors/index.js +20 -0
- package/dist/detectors/index.js.map +1 -0
- package/dist/detectors/largeComponent.d.ts +2 -0
- package/dist/detectors/largeComponent.js +46 -0
- package/dist/detectors/largeComponent.js.map +1 -0
- package/dist/detectors/namingDrift.d.ts +10 -0
- package/dist/detectors/namingDrift.js +82 -0
- package/dist/detectors/namingDrift.js.map +1 -0
- package/dist/detectors/propDrilling.d.ts +2 -0
- package/dist/detectors/propDrilling.js +97 -0
- package/dist/detectors/propDrilling.js.map +1 -0
- package/dist/detectors/stateSprawl.d.ts +2 -0
- package/dist/detectors/stateSprawl.js +47 -0
- package/dist/detectors/stateSprawl.js.map +1 -0
- package/dist/detectors/todoComment.d.ts +2 -0
- package/dist/detectors/todoComment.js +45 -0
- package/dist/detectors/todoComment.js.map +1 -0
- package/dist/reporters/index.d.ts +4 -0
- package/dist/reporters/index.js +14 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/jsonReporter.d.ts +2 -0
- package/dist/reporters/jsonReporter.js +4 -0
- package/dist/reporters/jsonReporter.js.map +1 -0
- package/dist/reporters/markdownReporter.d.ts +2 -0
- package/dist/reporters/markdownReporter.js +52 -0
- package/dist/reporters/markdownReporter.js.map +1 -0
- package/dist/reporters/sarifReporter.d.ts +7 -0
- package/dist/reporters/sarifReporter.js +77 -0
- package/dist/reporters/sarifReporter.js.map +1 -0
- package/dist/reporters/terminalReporter.d.ts +4 -0
- package/dist/reporters/terminalReporter.js +39 -0
- package/dist/reporters/terminalReporter.js.map +1 -0
- package/dist/utils/ast.d.ts +23 -0
- package/dist/utils/ast.js +132 -0
- package/dist/utils/ast.js.map +1 -0
- package/dist/utils/color.d.ts +8 -0
- package/dist/utils/color.js +27 -0
- package/dist/utils/color.js.map +1 -0
- package/dist/utils/createIssue.d.ts +13 -0
- package/dist/utils/createIssue.js +28 -0
- package/dist/utils/createIssue.js.map +1 -0
- package/dist/utils/git.d.ts +15 -0
- package/dist/utils/git.js +68 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/hostComponents.d.ts +12 -0
- package/dist/utils/hostComponents.js +57 -0
- package/dist/utils/hostComponents.js.map +1 -0
- package/dist/utils/identifiers.d.ts +3 -0
- package/dist/utils/identifiers.js +20 -0
- package/dist/utils/identifiers.js.map +1 -0
- package/dist/utils/lines.d.ts +8 -0
- package/dist/utils/lines.js +19 -0
- package/dist/utils/lines.js.map +1 -0
- package/dist/utils/similarity.d.ts +8 -0
- package/dist/utils/similarity.js +63 -0
- package/dist/utils/similarity.js.map +1 -0
- package/docs/architecture.md +68 -0
- package/docs/example-report.md +141 -0
- package/docs/good-first-issues.md +63 -0
- package/docs/rules.md +139 -0
- package/docs/showcase-expensify-app.md +119 -0
- package/package.json +60 -0
- package/schema/debtlens.config.schema.json +116 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
2
|
+
import { collectFunctionLikes, getFunctionBody } from "../utils/ast.js";
|
|
3
|
+
import { createIssue } from "../utils/createIssue.js";
|
|
4
|
+
import { isHookName } from "../utils/identifiers.js";
|
|
5
|
+
import { nodeLineSpan } from "../utils/lines.js";
|
|
6
|
+
export const deadAbstractionDetector = {
|
|
7
|
+
id: "dead-abstraction",
|
|
8
|
+
name: "Dead abstraction",
|
|
9
|
+
description: "Flags wrappers that add naming but very little behavior.",
|
|
10
|
+
defaultSeverity: "low",
|
|
11
|
+
tags: ["abstraction", "cleanup", "maintainability"],
|
|
12
|
+
detect(context) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
const maxWrapperLines = context.getThreshold("dead-abstraction.maxWrapperLines", 8);
|
|
15
|
+
for (const file of context.files) {
|
|
16
|
+
// Route modules (Expo Router / Next.js app & pages dirs) are thin wrappers by
|
|
17
|
+
// convention — a route that renders its screen is not a dead abstraction.
|
|
18
|
+
if (isRouteFile(file.relativePath))
|
|
19
|
+
continue;
|
|
20
|
+
for (const fn of collectFunctionLikes(file)) {
|
|
21
|
+
const body = getFunctionBody(fn.node);
|
|
22
|
+
if (!body)
|
|
23
|
+
continue;
|
|
24
|
+
const span = nodeLineSpan(body);
|
|
25
|
+
if (span.lines > maxWrapperLines)
|
|
26
|
+
continue;
|
|
27
|
+
const wrapper = describeWrapper(body, extractParamNames(fn.node));
|
|
28
|
+
if (!wrapper)
|
|
29
|
+
continue;
|
|
30
|
+
issues.push(createIssue({
|
|
31
|
+
detector: deadAbstractionDetector,
|
|
32
|
+
severity: fn.classification === "hook" ? "medium" : "low",
|
|
33
|
+
confidence: wrapper.confidence,
|
|
34
|
+
file: file.relativePath,
|
|
35
|
+
location: { startLine: span.startLine, endLine: span.endLine },
|
|
36
|
+
message: `${fn.name} looks like a thin wrapper: ${wrapper.description}.`,
|
|
37
|
+
evidence: [body.getText().replace(/\s+/g, " ").slice(0, 180)],
|
|
38
|
+
suggestion: "Keep the wrapper only if it creates a stable domain boundary. Otherwise inline it or add the missing behavior where this abstraction belongs.",
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return issues;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
function describeWrapper(body, paramNames) {
|
|
46
|
+
if (Node.isBlock(body)) {
|
|
47
|
+
const statements = body.getStatements();
|
|
48
|
+
if (statements.length !== 1)
|
|
49
|
+
return undefined;
|
|
50
|
+
const only = statements[0];
|
|
51
|
+
if (!only)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (Node.isReturnStatement(only)) {
|
|
54
|
+
const expression = only.getExpression();
|
|
55
|
+
if (!expression)
|
|
56
|
+
return undefined;
|
|
57
|
+
return describeExpression(expression, paramNames);
|
|
58
|
+
}
|
|
59
|
+
if (Node.isExpressionStatement(only)) {
|
|
60
|
+
return describeExpression(only.getExpression(), paramNames);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return describeExpression(body, paramNames);
|
|
64
|
+
}
|
|
65
|
+
function isRouteFile(relativePath) {
|
|
66
|
+
return /(^|\/)(app|pages)\//.test(relativePath);
|
|
67
|
+
}
|
|
68
|
+
/** Param identifier names in order, or null if any param is destructured/non-trivial. */
|
|
69
|
+
function extractParamNames(node) {
|
|
70
|
+
const names = [];
|
|
71
|
+
for (const param of node.getParameters()) {
|
|
72
|
+
const nameNode = param.getNameNode();
|
|
73
|
+
if (!Node.isIdentifier(nameNode))
|
|
74
|
+
return null;
|
|
75
|
+
names.push(nameNode.getText());
|
|
76
|
+
}
|
|
77
|
+
return names;
|
|
78
|
+
}
|
|
79
|
+
/** True if a call forwards exactly the wrapper's params, in order, unchanged. */
|
|
80
|
+
function isPassThrough(call, paramNames) {
|
|
81
|
+
const args = call.getArguments();
|
|
82
|
+
if (args.length !== paramNames.length)
|
|
83
|
+
return false;
|
|
84
|
+
return args.every((arg, index) => Node.isIdentifier(arg) && arg.getText() === paramNames[index]);
|
|
85
|
+
}
|
|
86
|
+
function describeExpression(expression, paramNames) {
|
|
87
|
+
if (Node.isCallExpression(expression)) {
|
|
88
|
+
const callee = expression.getExpression();
|
|
89
|
+
const calleeName = Node.isIdentifier(callee)
|
|
90
|
+
? callee.getText()
|
|
91
|
+
: Node.isPropertyAccessExpression(callee)
|
|
92
|
+
? callee.getName()
|
|
93
|
+
: "";
|
|
94
|
+
// A hook that just composes another hook (useCallback, a store selector, etc.) is
|
|
95
|
+
// idiomatic composition, not a dead abstraction.
|
|
96
|
+
if (isHookName(calleeName))
|
|
97
|
+
return undefined;
|
|
98
|
+
// Only flag a delegation when it adds nothing: a pure pass-through of the params.
|
|
99
|
+
// `f(a, b) => g(a, b)` is dead; `f(a) => g(transform(a))` or `g(a, CONST)` is not.
|
|
100
|
+
if (!paramNames || !isPassThrough(expression, paramNames))
|
|
101
|
+
return undefined;
|
|
102
|
+
return { description: `it only delegates to ${callee.getText()}(...)`, confidence: 0.8 };
|
|
103
|
+
}
|
|
104
|
+
if (Node.isIdentifier(expression)) {
|
|
105
|
+
return { description: `it only returns ${expression.getText()}`, confidence: 0.76 };
|
|
106
|
+
}
|
|
107
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
108
|
+
return { description: `it only returns ${expression.getText()}`, confidence: 0.72 };
|
|
109
|
+
}
|
|
110
|
+
if (expression.getKind() === SyntaxKind.JsxElement || expression.getKind() === SyntaxKind.JsxSelfClosingElement) {
|
|
111
|
+
return { description: "it only forwards to a single JSX element", confidence: 0.68 };
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=deadAbstraction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deadAbstraction.js","sourceRoot":"","sources":["../../src/detectors/deadAbstraction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { collectFunctionLikes, getFunctionBody, structuralFingerprint } from "../utils/ast.js";
|
|
2
|
+
import { createIssue } from "../utils/createIssue.js";
|
|
3
|
+
import { nodeLineSpan } from "../utils/lines.js";
|
|
4
|
+
import { cosineSimilarity, jaccard, normalizeSnippet, shingle } from "../utils/similarity.js";
|
|
5
|
+
export const duplicateLogicDetector = {
|
|
6
|
+
id: "duplicate-logic",
|
|
7
|
+
name: "Duplicate logic",
|
|
8
|
+
description: "Finds near-duplicate functions/components after comments, identifiers, strings, and literals are normalized.",
|
|
9
|
+
defaultSeverity: "medium",
|
|
10
|
+
tags: ["duplication", "maintainability", "review"],
|
|
11
|
+
detect(context) {
|
|
12
|
+
const issues = [];
|
|
13
|
+
const minSimilarity = context.getThreshold("duplicate-logic.minSimilarity", 0.86);
|
|
14
|
+
const minStructural = context.getThreshold("duplicate-logic.minStructuralSimilarity", 0.6);
|
|
15
|
+
const minLines = context.getThreshold("duplicate-logic.minLines", 8);
|
|
16
|
+
const maxSnippets = context.getThreshold("duplicate-logic.maxSnippets", 450);
|
|
17
|
+
const snippets = [];
|
|
18
|
+
for (const file of context.files) {
|
|
19
|
+
for (const fn of collectFunctionLikes(file)) {
|
|
20
|
+
const body = getFunctionBody(fn.node) ?? fn.node;
|
|
21
|
+
const span = nodeLineSpan(body);
|
|
22
|
+
if (span.lines < minLines || span.lines > 220)
|
|
23
|
+
continue;
|
|
24
|
+
const text = body.getText();
|
|
25
|
+
const normalized = normalizeSnippet(text);
|
|
26
|
+
if (normalized.length < 80)
|
|
27
|
+
continue;
|
|
28
|
+
snippets.push({
|
|
29
|
+
name: fn.name,
|
|
30
|
+
file: file.relativePath,
|
|
31
|
+
startLine: span.startLine,
|
|
32
|
+
endLine: span.endLine,
|
|
33
|
+
lines: span.lines,
|
|
34
|
+
normalized,
|
|
35
|
+
shingles: shingle(normalized),
|
|
36
|
+
fingerprint: structuralFingerprint(body),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const limited = snippets.slice(0, maxSnippets);
|
|
41
|
+
const seenPairs = new Set();
|
|
42
|
+
for (let i = 0; i < limited.length; i += 1) {
|
|
43
|
+
for (let j = i + 1; j < limited.length; j += 1) {
|
|
44
|
+
const a = limited[i];
|
|
45
|
+
const b = limited[j];
|
|
46
|
+
if (!a || !b)
|
|
47
|
+
continue;
|
|
48
|
+
if (a.file === b.file && Math.abs(a.startLine - b.startLine) < 4)
|
|
49
|
+
continue;
|
|
50
|
+
// Cheap structural pre-filter: reject pairs whose control-flow/call shape
|
|
51
|
+
// differs before doing the more expensive text-shingle comparison.
|
|
52
|
+
if (cosineSimilarity(a.fingerprint, b.fingerprint) < minStructural)
|
|
53
|
+
continue;
|
|
54
|
+
const similarity = jaccard(a.shingles, b.shingles);
|
|
55
|
+
if (similarity < minSimilarity)
|
|
56
|
+
continue;
|
|
57
|
+
const key = [a.file, a.startLine, b.file, b.startLine].sort().join("|");
|
|
58
|
+
if (seenPairs.has(key))
|
|
59
|
+
continue;
|
|
60
|
+
seenPairs.add(key);
|
|
61
|
+
issues.push(createIssue({
|
|
62
|
+
detector: duplicateLogicDetector,
|
|
63
|
+
severity: similarity > 0.93 ? "high" : "medium",
|
|
64
|
+
confidence: similarity,
|
|
65
|
+
file: a.file,
|
|
66
|
+
location: { startLine: a.startLine, endLine: a.endLine },
|
|
67
|
+
message: `${a.name} is ${Math.round(similarity * 100)}% structurally similar to ${b.name}.`,
|
|
68
|
+
evidence: [
|
|
69
|
+
`${a.file}:${a.startLine}-${a.endLine} (${a.lines} lines)`,
|
|
70
|
+
`${b.file}:${b.startLine}-${b.endLine} (${b.lines} lines)`,
|
|
71
|
+
],
|
|
72
|
+
suggestion: "Compare the two implementations. Extract shared behavior only if the variation is intentional and stable; otherwise delete the weaker duplicate.",
|
|
73
|
+
}));
|
|
74
|
+
if (issues.length >= 50)
|
|
75
|
+
return issues;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return issues;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=duplicateLogic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duplicateLogic.js","sourceRoot":"","sources":["../../src/detectors/duplicateLogic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
2
|
+
import { createIssue } from "../utils/createIssue.js";
|
|
3
|
+
import { nodeLineSpan } from "../utils/lines.js";
|
|
4
|
+
export const effectComplexityDetector = {
|
|
5
|
+
id: "effect-complexity",
|
|
6
|
+
name: "Effect complexity",
|
|
7
|
+
description: "Flags useEffect blocks that are long, branchy, or overloaded with dependencies.",
|
|
8
|
+
defaultSeverity: "medium",
|
|
9
|
+
tags: ["react", "effects", "complexity"],
|
|
10
|
+
detect(context) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const maxLines = context.getThreshold("effect-complexity.maxLines", 30);
|
|
13
|
+
const maxDependencies = context.getThreshold("effect-complexity.maxDependencies", 8);
|
|
14
|
+
for (const file of context.files) {
|
|
15
|
+
for (const call of file.sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
16
|
+
const expression = call.getExpression();
|
|
17
|
+
const expressionName = Node.isIdentifier(expression)
|
|
18
|
+
? expression.getText()
|
|
19
|
+
: Node.isPropertyAccessExpression(expression)
|
|
20
|
+
? expression.getName()
|
|
21
|
+
: undefined;
|
|
22
|
+
if (expressionName !== "useEffect")
|
|
23
|
+
continue;
|
|
24
|
+
const [callback, dependencies] = call.getArguments();
|
|
25
|
+
if (!callback)
|
|
26
|
+
continue;
|
|
27
|
+
const span = nodeLineSpan(callback);
|
|
28
|
+
const branchCount = callback.getDescendants().filter((node) => {
|
|
29
|
+
const kind = node.getKind();
|
|
30
|
+
return kind === SyntaxKind.IfStatement
|
|
31
|
+
|| kind === SyntaxKind.ConditionalExpression
|
|
32
|
+
|| kind === SyntaxKind.ForStatement
|
|
33
|
+
|| kind === SyntaxKind.ForOfStatement
|
|
34
|
+
|| kind === SyntaxKind.WhileStatement
|
|
35
|
+
|| kind === SyntaxKind.SwitchStatement;
|
|
36
|
+
}).length;
|
|
37
|
+
const dependencyCount = dependencies && Node.isArrayLiteralExpression(dependencies)
|
|
38
|
+
? dependencies.getElements().length
|
|
39
|
+
: 0;
|
|
40
|
+
const hasAsyncWork = callback.getDescendantsOfKind(SyntaxKind.AwaitExpression).length > 0;
|
|
41
|
+
const callCount = callback.getDescendantsOfKind(SyntaxKind.CallExpression).length;
|
|
42
|
+
if (span.lines < maxLines && dependencyCount < maxDependencies && branchCount < 4 && callCount < 8)
|
|
43
|
+
continue;
|
|
44
|
+
issues.push(createIssue({
|
|
45
|
+
detector: effectComplexityDetector,
|
|
46
|
+
severity: span.lines >= maxLines * 1.5 || branchCount >= 6 ? "high" : "medium",
|
|
47
|
+
confidence: 0.8,
|
|
48
|
+
file: file.relativePath,
|
|
49
|
+
location: { startLine: span.startLine, endLine: span.endLine },
|
|
50
|
+
message: `This useEffect spans ${span.lines} lines, has ${dependencyCount} dependencies, ${branchCount} branches, and ${callCount} nested calls.`,
|
|
51
|
+
evidence: [
|
|
52
|
+
`Lines: ${span.lines} / ${maxLines}`,
|
|
53
|
+
`Dependencies: ${dependencyCount} / ${maxDependencies}`,
|
|
54
|
+
`Branches: ${branchCount}`,
|
|
55
|
+
hasAsyncWork ? "Contains async work" : "No await detected",
|
|
56
|
+
],
|
|
57
|
+
suggestion: "Split unrelated effects, move imperative workflows into named functions, or replace derived state effects with memoized values.",
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return issues;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=effectComplexity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effectComplexity.js","sourceRoot":"","sources":["../../src/detectors/effectComplexity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,CAAC,MAAM,wBAAwB,GAAa;IAChD,EAAE,EAAE,mBAAmB;IACvB,IAAI,EAAE,mBAAmB;IACzB,WAAW,EAAE,iFAAiF;IAC9F,eAAe,EAAE,QAAQ;IACzB,IAAI,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,YAAY,CAAC;IACxC,MAAM,CAAC,OAAwB;QAC7B,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,4BAA4B,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,mCAAmC,EAAE,CAAC,CAAC,CAAC;QAErF,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,oBAAoB,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;gBACnF,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;gBACxC,MAAM,cAAc,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC;oBAClD,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE;oBACtB,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC;wBAC3C,CAAC,CAAC,UAAU,CAAC,OAAO,EAAE;wBACtB,CAAC,CAAC,SAAS,CAAC;gBAEhB,IAAI,cAAc,KAAK,WAAW;oBAAE,SAAS;gBAE7C,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;gBACrD,IAAI,CAAC,QAAQ;oBAAE,SAAS;gBAExB,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;gBACpC,MAAM,WAAW,GAAG,QAAQ,CAAC,cAAc,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;oBAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;oBAC5B,OAAO,IAAI,KAAK,UAAU,CAAC,WAAW;2BACjC,IAAI,KAAK,UAAU,CAAC,qBAAqB;2BACzC,IAAI,KAAK,UAAU,CAAC,YAAY;2BAChC,IAAI,KAAK,UAAU,CAAC,cAAc;2BAClC,IAAI,KAAK,UAAU,CAAC,cAAc;2BAClC,IAAI,KAAK,UAAU,CAAC,eAAe,CAAC;gBAC3C,CAAC,CAAC,CAAC,MAAM,CAAC;gBAEV,MAAM,eAAe,GAAG,YAAY,IAAI,IAAI,CAAC,wBAAwB,CAAC,YAAY,CAAC;oBACjF,CAAC,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,MAAM;oBACnC,CAAC,CAAC,CAAC,CAAC;gBAEN,MAAM,YAAY,GAAG,QAAQ,CAAC,oBAAoB,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;gBAC1F,MAAM,SAAS,GAAG,QAAQ,CAAC,oBAAoB,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC;gBAElF,IAAI,IAAI,CAAC,KAAK,GAAG,QAAQ,IAAI,eAAe,GAAG,eAAe,IAAI,WAAW,GAAG,CAAC,IAAI,SAAS,GAAG,CAAC;oBAAE,SAAS;gBAE7G,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;oBACtB,QAAQ,EAAE,wBAAwB;oBAClC,QAAQ,EAAE,IAAI,CAAC,KAAK,IAAI,QAAQ,GAAG,GAAG,IAAI,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;oBAC9E,UAAU,EAAE,GAAG;oBACf,IAAI,EAAE,IAAI,CAAC,YAAY;oBACvB,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;oBAC9D,OAAO,EAAE,wBAAwB,IAAI,CAAC,KAAK,eAAe,eAAe,kBAAkB,WAAW,kBAAkB,SAAS,gBAAgB;oBACjJ,QAAQ,EAAE;wBACR,UAAU,IAAI,CAAC,KAAK,MAAM,QAAQ,EAAE;wBACpC,iBAAiB,eAAe,MAAM,eAAe,EAAE;wBACvD,aAAa,WAAW,EAAE;wBAC1B,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,mBAAmB;qBAC3D;oBACD,UAAU,EAAE,iIAAiI;iBAC9I,CAAC,CAAC,CAAC;YACN,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { deadAbstractionDetector } from "./deadAbstraction.js";
|
|
2
|
+
import { duplicateLogicDetector } from "./duplicateLogic.js";
|
|
3
|
+
import { effectComplexityDetector } from "./effectComplexity.js";
|
|
4
|
+
import { largeComponentDetector } from "./largeComponent.js";
|
|
5
|
+
import { namingDriftDetector } from "./namingDrift.js";
|
|
6
|
+
import { propDrillingDetector } from "./propDrilling.js";
|
|
7
|
+
import { stateSprawlDetector } from "./stateSprawl.js";
|
|
8
|
+
import { todoCommentDetector } from "./todoComment.js";
|
|
9
|
+
export const allDetectors = [
|
|
10
|
+
largeComponentDetector,
|
|
11
|
+
stateSprawlDetector,
|
|
12
|
+
effectComplexityDetector,
|
|
13
|
+
duplicateLogicDetector,
|
|
14
|
+
deadAbstractionDetector,
|
|
15
|
+
propDrillingDetector,
|
|
16
|
+
todoCommentDetector,
|
|
17
|
+
namingDriftDetector,
|
|
18
|
+
];
|
|
19
|
+
export const detectorIds = allDetectors.map((detector) => detector.id);
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,MAAM,CAAC,MAAM,YAAY,GAAe;IACtC,sBAAsB;IACtB,mBAAmB;IACnB,wBAAwB;IACxB,sBAAsB;IACtB,uBAAuB;IACvB,oBAAoB;IACpB,mBAAmB;IACnB,mBAAmB;CACpB,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { collectFunctionLikes, countBranches, countHookCalls, getFunctionBody } from "../utils/ast.js";
|
|
2
|
+
import { createIssue } from "../utils/createIssue.js";
|
|
3
|
+
import { nodeLineSpan } from "../utils/lines.js";
|
|
4
|
+
export const largeComponentDetector = {
|
|
5
|
+
id: "large-component",
|
|
6
|
+
name: "Large component",
|
|
7
|
+
description: "Flags React-style components that have grown large enough to hide unrelated responsibilities.",
|
|
8
|
+
defaultSeverity: "medium",
|
|
9
|
+
tags: ["react", "maintainability", "component-design"],
|
|
10
|
+
detect(context) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const maxLines = context.getThreshold("large-component.maxLines", 250);
|
|
13
|
+
const maxBranches = context.getThreshold("large-component.maxBranches", 16);
|
|
14
|
+
const maxHooks = context.getThreshold("large-component.maxHooks", 10);
|
|
15
|
+
for (const file of context.files) {
|
|
16
|
+
for (const fn of collectFunctionLikes(file)) {
|
|
17
|
+
if (fn.classification !== "component")
|
|
18
|
+
continue;
|
|
19
|
+
const body = getFunctionBody(fn.node) ?? fn.node;
|
|
20
|
+
const span = nodeLineSpan(body);
|
|
21
|
+
const branchCount = countBranches(body);
|
|
22
|
+
const hookCount = countHookCalls(body);
|
|
23
|
+
const isOverLineBudget = span.lines >= maxLines;
|
|
24
|
+
const isOverComplexityBudget = branchCount >= maxBranches || hookCount >= maxHooks;
|
|
25
|
+
if (!isOverLineBudget && !isOverComplexityBudget)
|
|
26
|
+
continue;
|
|
27
|
+
issues.push(createIssue({
|
|
28
|
+
detector: largeComponentDetector,
|
|
29
|
+
severity: isOverLineBudget && isOverComplexityBudget ? "high" : "medium",
|
|
30
|
+
confidence: isOverLineBudget ? 0.86 : 0.74,
|
|
31
|
+
file: file.relativePath,
|
|
32
|
+
location: { startLine: span.startLine, endLine: span.endLine },
|
|
33
|
+
message: `${fn.name} appears to own too many responsibilities. It spans ${span.lines} lines, uses ${hookCount} hooks, and contains ${branchCount} branch points.`,
|
|
34
|
+
evidence: [
|
|
35
|
+
`Lines: ${span.lines} / ${maxLines}`,
|
|
36
|
+
`Hook calls: ${hookCount} / ${maxHooks}`,
|
|
37
|
+
`Branch points: ${branchCount} / ${maxBranches}`,
|
|
38
|
+
],
|
|
39
|
+
suggestion: "Extract data loading, derived state, or rendering sections into focused hooks/components before adding more behavior.",
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return issues;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=largeComponent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"largeComponent.js","sourceRoot":"","sources":["../../src/detectors/largeComponent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,CAAC,MAAM,sBAAsB,GAAa;IAC9C,EAAE,EAAE,iBAAiB;IACrB,IAAI,EAAE,iBAAiB;IACvB,WAAW,EAAE,+FAA+F;IAC5G,eAAe,EAAE,QAAQ;IACzB,IAAI,EAAE,CAAC,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,CAAC;IACtD,MAAM,CAAC,OAAwB;QAC7B,MAAM,MAAM,GAAgB,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAC;QACvE,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,6BAA6B,EAAE,EAAE,CAAC,CAAC;QAC5E,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAC;QAEtE,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,EAAE,IAAI,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC5C,IAAI,EAAE,CAAC,cAAc,KAAK,WAAW;oBAAE,SAAS;gBAChD,MAAM,IAAI,GAAG,eAAe,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC;gBACjD,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;gBAChC,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;gBACxC,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;gBACvC,MAAM,gBAAgB,GAAG,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC;gBAChD,MAAM,sBAAsB,GAAG,WAAW,IAAI,WAAW,IAAI,SAAS,IAAI,QAAQ,CAAC;gBAEnF,IAAI,CAAC,gBAAgB,IAAI,CAAC,sBAAsB;oBAAE,SAAS;gBAE3D,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;oBACtB,QAAQ,EAAE,sBAAsB;oBAChC,QAAQ,EAAE,gBAAgB,IAAI,sBAAsB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;oBACxE,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;oBAC1C,IAAI,EAAE,IAAI,CAAC,YAAY;oBACvB,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;oBAC9D,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,uDAAuD,IAAI,CAAC,KAAK,gBAAgB,SAAS,wBAAwB,WAAW,iBAAiB;oBACjK,QAAQ,EAAE;wBACR,UAAU,IAAI,CAAC,KAAK,MAAM,QAAQ,EAAE;wBACpC,eAAe,SAAS,MAAM,QAAQ,EAAE;wBACxC,kBAAkB,WAAW,MAAM,WAAW,EAAE;qBACjD;oBACD,UAAU,EAAE,uHAAuH;iBACpI,CAAC,CAAC,CAAC;YACN,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Detector } from "../core/types.js";
|
|
2
|
+
interface ConceptGroup {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
variants: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Merge the built-in pack with user-supplied vocabulary; config groups win on id. */
|
|
8
|
+
export declare function resolveConceptGroups(vocabulary?: Record<string, string[]>): ConceptGroup[];
|
|
9
|
+
export declare const namingDriftDetector: Detector;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
import { createIssue } from "../utils/createIssue.js";
|
|
3
|
+
import { splitIdentifier } from "../utils/identifiers.js";
|
|
4
|
+
// Built-in "media/release" vocabulary pack. Projects in other domains can add their
|
|
5
|
+
// own concept groups via the `vocabulary` config (config groups override built-ins
|
|
6
|
+
// that share an id).
|
|
7
|
+
const defaultConceptGroups = [
|
|
8
|
+
{
|
|
9
|
+
id: "media-entity",
|
|
10
|
+
label: "media entity",
|
|
11
|
+
variants: ["movie", "film", "show", "series", "title", "release", "media", "content", "game", "episode"],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "time-of-release",
|
|
15
|
+
label: "release timing",
|
|
16
|
+
variants: ["date", "air", "premiere", "launch", "drop", "release", "available", "calendar", "upcoming"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "saved-user-item",
|
|
20
|
+
label: "saved user item",
|
|
21
|
+
variants: ["saved", "favorite", "favourite", "watchlist", "tracked", "bookmark", "following", "library"],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "data-source",
|
|
25
|
+
label: "data source",
|
|
26
|
+
variants: ["api", "client", "service", "provider", "adapter", "source", "repository", "store"],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
function humanizeId(id) {
|
|
30
|
+
return id.replace(/[-_]+/g, " ").trim();
|
|
31
|
+
}
|
|
32
|
+
/** Merge the built-in pack with user-supplied vocabulary; config groups win on id. */
|
|
33
|
+
export function resolveConceptGroups(vocabulary) {
|
|
34
|
+
const groups = new Map();
|
|
35
|
+
for (const group of defaultConceptGroups)
|
|
36
|
+
groups.set(group.id, group);
|
|
37
|
+
if (vocabulary) {
|
|
38
|
+
for (const [id, variants] of Object.entries(vocabulary)) {
|
|
39
|
+
groups.set(id, {
|
|
40
|
+
id,
|
|
41
|
+
label: humanizeId(id),
|
|
42
|
+
variants: variants.map((variant) => variant.toLowerCase()),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...groups.values()];
|
|
47
|
+
}
|
|
48
|
+
export const namingDriftDetector = {
|
|
49
|
+
id: "naming-drift",
|
|
50
|
+
name: "Naming drift",
|
|
51
|
+
description: "Finds files where similar concepts are represented by many competing names.",
|
|
52
|
+
defaultSeverity: "info",
|
|
53
|
+
tags: ["naming", "architecture", "domain-modeling"],
|
|
54
|
+
detect(context) {
|
|
55
|
+
const issues = [];
|
|
56
|
+
const minVariants = context.getThreshold("naming-drift.minVariants", 4);
|
|
57
|
+
const conceptGroups = resolveConceptGroups(context.options.vocabulary);
|
|
58
|
+
for (const file of context.files) {
|
|
59
|
+
const identifiers = file.sourceFile
|
|
60
|
+
.getDescendantsOfKind(SyntaxKind.Identifier)
|
|
61
|
+
.flatMap((identifier) => splitIdentifier(identifier.getText()));
|
|
62
|
+
const tokenSet = new Set(identifiers);
|
|
63
|
+
for (const group of conceptGroups) {
|
|
64
|
+
const found = group.variants.filter((variant) => tokenSet.has(variant));
|
|
65
|
+
if (found.length < minVariants)
|
|
66
|
+
continue;
|
|
67
|
+
issues.push(createIssue({
|
|
68
|
+
detector: namingDriftDetector,
|
|
69
|
+
severity: found.length >= minVariants + 2 ? "low" : "info",
|
|
70
|
+
confidence: 0.62,
|
|
71
|
+
file: file.relativePath,
|
|
72
|
+
location: { startLine: 1 },
|
|
73
|
+
message: `This file uses ${found.length} competing terms for ${group.label}: ${found.join(", ")}.`,
|
|
74
|
+
evidence: [`Concept group: ${group.id}`, `Variants found: ${found.join(", ")}`],
|
|
75
|
+
suggestion: "Pick a canonical domain name and rename adapters at system boundaries instead of mixing boundary names throughout the feature.",
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return issues;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=namingDrift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namingDrift.js","sourceRoot":"","sources":["../../src/detectors/namingDrift.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
2
|
+
import { collectFunctionLikes, getFunctionBody } from "../utils/ast.js";
|
|
3
|
+
import { createIssue } from "../utils/createIssue.js";
|
|
4
|
+
import { isHostComponent } from "../utils/hostComponents.js";
|
|
5
|
+
import { nodeLineSpan } from "../utils/lines.js";
|
|
6
|
+
export const propDrillingDetector = {
|
|
7
|
+
id: "prop-drilling",
|
|
8
|
+
name: "Prop drilling",
|
|
9
|
+
description: "Flags components that mostly forward props to child components.",
|
|
10
|
+
defaultSeverity: "medium",
|
|
11
|
+
tags: ["react", "props", "component-design"],
|
|
12
|
+
detect(context) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
const maxForwardedProps = context.getThreshold("prop-drilling.maxForwardedProps", 4);
|
|
15
|
+
for (const file of context.files) {
|
|
16
|
+
for (const fn of collectFunctionLikes(file)) {
|
|
17
|
+
if (fn.classification !== "component")
|
|
18
|
+
continue;
|
|
19
|
+
const propNames = inferPropNames(fn.node);
|
|
20
|
+
if (propNames.size === 0)
|
|
21
|
+
continue;
|
|
22
|
+
const body = getFunctionBody(fn.node) ?? fn.node;
|
|
23
|
+
const forwarded = new Map();
|
|
24
|
+
const jsxNodes = [
|
|
25
|
+
...body.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
|
|
26
|
+
...body.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
|
|
27
|
+
];
|
|
28
|
+
for (const jsx of jsxNodes) {
|
|
29
|
+
const tagName = jsx.getTagNameNode().getText();
|
|
30
|
+
// Skip host elements (lowercase DOM tags) and UI primitives (RN/icon
|
|
31
|
+
// components are PascalCase but aren't user-defined children).
|
|
32
|
+
if (/^[a-z]/.test(tagName) || isHostComponent(tagName))
|
|
33
|
+
continue;
|
|
34
|
+
const childForwarded = new Set();
|
|
35
|
+
for (const attribute of jsx.getAttributes()) {
|
|
36
|
+
if (!Node.isJsxAttribute(attribute))
|
|
37
|
+
continue;
|
|
38
|
+
const initializer = attribute.getInitializer()?.getText() ?? "";
|
|
39
|
+
for (const propName of propNames) {
|
|
40
|
+
if (new RegExp(`\\b${escapeRegExp(propName)}\\b`).test(initializer)) {
|
|
41
|
+
childForwarded.add(propName);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (childForwarded.size > 0) {
|
|
46
|
+
forwarded.set(tagName, childForwarded);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const uniqueForwarded = new Set([...forwarded.values()].flatMap((value) => [...value]));
|
|
50
|
+
if (uniqueForwarded.size < maxForwardedProps && forwarded.size < 3)
|
|
51
|
+
continue;
|
|
52
|
+
const span = nodeLineSpan(body);
|
|
53
|
+
issues.push(createIssue({
|
|
54
|
+
detector: propDrillingDetector,
|
|
55
|
+
severity: uniqueForwarded.size >= maxForwardedProps + 3 ? "high" : "medium",
|
|
56
|
+
confidence: 0.73,
|
|
57
|
+
file: file.relativePath,
|
|
58
|
+
location: { startLine: span.startLine, endLine: span.endLine },
|
|
59
|
+
message: `${fn.name} forwards ${uniqueForwarded.size} props across ${forwarded.size} child components.`,
|
|
60
|
+
evidence: [...forwarded.entries()].slice(0, 5).map(([child, props]) => `${child}: ${[...props].join(", ")}`),
|
|
61
|
+
suggestion: "Consider colocating the data owner closer to consumers, using a composition slot, or extracting a focused context for stable cross-cutting values.",
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return issues;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
function inferPropNames(node) {
|
|
69
|
+
const names = new Set();
|
|
70
|
+
const [firstParam] = node.getParameters();
|
|
71
|
+
if (!firstParam)
|
|
72
|
+
return names;
|
|
73
|
+
const text = firstParam.getText();
|
|
74
|
+
if (text.startsWith("{")) {
|
|
75
|
+
const withoutType = text.split(":").slice(0, -1).join(":") || text;
|
|
76
|
+
for (const match of withoutType.matchAll(/\b([A-Za-z_$][\w$]*)\b/g)) {
|
|
77
|
+
const name = match[1];
|
|
78
|
+
if (name && !["readonly", "string", "number", "boolean", "React", "Props"].includes(name)) {
|
|
79
|
+
names.add(name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return names;
|
|
83
|
+
}
|
|
84
|
+
const paramName = text.split(/[\s:=>,]/)[0];
|
|
85
|
+
if (!paramName)
|
|
86
|
+
return names;
|
|
87
|
+
if (paramName === "props") {
|
|
88
|
+
names.add("props");
|
|
89
|
+
return names;
|
|
90
|
+
}
|
|
91
|
+
names.add(paramName);
|
|
92
|
+
return names;
|
|
93
|
+
}
|
|
94
|
+
function escapeRegExp(value) {
|
|
95
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=propDrilling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"propDrilling.js","sourceRoot":"","sources":["../../src/detectors/propDrilling.ts"],"names":[],"mappings":"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"}
|