deep-slop 1.4.1
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/.deep-slop/.deep-slop-ignore +13 -0
- package/LICENSE +21 -0
- package/README.md +1170 -0
- package/dist/arch-constraints-C7s1E_bc.js +450 -0
- package/dist/arch-rules-DI1SYPqu.js +358 -0
- package/dist/ast-slop-BGdr58wZ.js +1839 -0
- package/dist/config-lint-ph3vMUbg.js +371 -0
- package/dist/dead-flow-DHRkyxZT.js +1422 -0
- package/dist/deep-slop-bundled.js +33140 -0
- package/dist/discover-B_S_Fy2S.js +164 -0
- package/dist/dup-detect-DKRXM04q.js +709 -0
- package/dist/file-utils-B_HFXhCs.js +93 -0
- package/dist/format-lint-DeElllNm.js +445 -0
- package/dist/framework-lint-CqdlF9hX.js +782 -0
- package/dist/i18n-lint-CPzx7V8Q.js +605 -0
- package/dist/import-intelligence-SK4F7XpL.js +966 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +1030 -0
- package/dist/knip-CgxnnTBZ.js +93 -0
- package/dist/lint-external-ZbW3jGvB.js +326 -0
- package/dist/markup-lint-DKVEDz9M.js +805 -0
- package/dist/mcp.js +35939 -0
- package/dist/meta-quality-Dai1W5iC.js +224 -0
- package/dist/perf-hints-BnWFMFff.js +500 -0
- package/dist/security-deep-DJRINs10.js +1198 -0
- package/dist/syntax-deep-ZQYMutky.js +624 -0
- package/dist/tree-sitter-CM-cP0nl.js +661 -0
- package/dist/type-safety-Dboj2C1t.js +519 -0
- package/package.json +92 -0
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import { _ as parseFile, d as initParser, n as extractImportFromNode, o as findNodesOfTypes, y as walkAST } from "./tree-sitter-CM-cP0nl.js";
|
|
2
|
+
import { t as collectFiles } from "./discover-B_S_Fy2S.js";
|
|
3
|
+
import { i as toLines, n as extractImports, r as readFileContent } from "./file-utils-B_HFXhCs.js";
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { stat } from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
//#region src/engines/import-intelligence/index.ts
|
|
8
|
+
/** Packages known to support deep / tree-shakeable imports */
|
|
9
|
+
const TREE_SHAKEABLE_PACKAGES = {
|
|
10
|
+
lodash: "lodash/{symbol}",
|
|
11
|
+
"lodash-es": "lodash-es/{symbol}",
|
|
12
|
+
ramda: "ramda/src/{symbol}",
|
|
13
|
+
underscore: "underscore/cjs/{symbol}",
|
|
14
|
+
rxjs: "rxjs/{symbol}",
|
|
15
|
+
d3: "d3-{symbol}",
|
|
16
|
+
"date-fns": "date-fns/{symbol}"
|
|
17
|
+
};
|
|
18
|
+
/** Side-effect-only import pattern (no bindings) */
|
|
19
|
+
const SIDE_EFFECT_RE = /^import\s+['"][^'"]+['"];?\s*$/;
|
|
20
|
+
/** Named-import extraction: `import { A, B as C, ... } from ...` */
|
|
21
|
+
const NAMED_IMPORTS_RE = /import\s+(?:type\s+)?\{([^}]+)\}/;
|
|
22
|
+
/** Default-import extraction: `import X from ...` (no braces) */
|
|
23
|
+
const DEFAULT_IMPORT_RE = /^import\s+(?:type\s+)?(\w+)\s+from\s+['"]/;
|
|
24
|
+
/** Namespace import: `import * as X from ...` */
|
|
25
|
+
const NAMESPACE_IMPORT_RE = /^import\s+\*\s+as\s+(\w+)\s+from\s+['"]/;
|
|
26
|
+
/** React version threshold for automatic JSX runtime */
|
|
27
|
+
const REACT_AUTOMATIC_JSX_VERSION = 17;
|
|
28
|
+
/** Drizzle ORM symbols that are commonly flagged as unused (false positives) */
|
|
29
|
+
const DRIZZLE_FP_SYMBOLS = new Set([
|
|
30
|
+
"sql",
|
|
31
|
+
"count",
|
|
32
|
+
"desc",
|
|
33
|
+
"and",
|
|
34
|
+
"inArray",
|
|
35
|
+
"eq",
|
|
36
|
+
"ne",
|
|
37
|
+
"gt",
|
|
38
|
+
"gte",
|
|
39
|
+
"lt",
|
|
40
|
+
"lte",
|
|
41
|
+
"like",
|
|
42
|
+
"ilike",
|
|
43
|
+
"not",
|
|
44
|
+
"or",
|
|
45
|
+
"between",
|
|
46
|
+
"exists",
|
|
47
|
+
"notInArray",
|
|
48
|
+
"isNull",
|
|
49
|
+
"isNotNull"
|
|
50
|
+
]);
|
|
51
|
+
/** Packages that are Drizzle ORM */
|
|
52
|
+
const DRIZZLE_PACKAGES = new Set([
|
|
53
|
+
"drizzle-orm",
|
|
54
|
+
"drizzle-orm/sqlite-core",
|
|
55
|
+
"drizzle-orm/pg-core",
|
|
56
|
+
"drizzle-orm/mysql-core",
|
|
57
|
+
"drizzle-orm/sqlite-singlestore-core"
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* Parse imports from a file using tree-sitter AST.
|
|
61
|
+
* Returns ParsedImport[] with viaAST=true, or null if tree-sitter unavailable.
|
|
62
|
+
*/
|
|
63
|
+
async function parseImportsAST(content, filePath) {
|
|
64
|
+
if (!await initParser()) return null;
|
|
65
|
+
const ast = await parseFile(content, filePath.endsWith(".tsx") || filePath.endsWith(".jsx"));
|
|
66
|
+
if (!ast) return null;
|
|
67
|
+
const importNodes = findNodesOfTypes(ast, ["import_statement", "import_declaration"]);
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const node of importNodes) {
|
|
70
|
+
const extracted = extractImportFromNode(node);
|
|
71
|
+
if (!extracted) continue;
|
|
72
|
+
const raw = node.text.trim();
|
|
73
|
+
const isSideEffect = SIDE_EFFECT_RE.test(raw);
|
|
74
|
+
const isNamespace = NAMESPACE_IMPORT_RE.test(raw);
|
|
75
|
+
const nsMatch = raw.match(NAMESPACE_IMPORT_RE);
|
|
76
|
+
const namespaceAlias = nsMatch ? nsMatch[1] : "";
|
|
77
|
+
const isDefault = !!(raw.match(DEFAULT_IMPORT_RE) && !raw.includes("{"));
|
|
78
|
+
results.push({
|
|
79
|
+
line: extracted.line,
|
|
80
|
+
source: extracted.source,
|
|
81
|
+
raw,
|
|
82
|
+
isTypeOnly: extracted.isTypeOnly,
|
|
83
|
+
isDefault,
|
|
84
|
+
isDynamic: false,
|
|
85
|
+
symbols: extracted.symbols,
|
|
86
|
+
isSideEffect,
|
|
87
|
+
isNamespace,
|
|
88
|
+
namespaceAlias,
|
|
89
|
+
viaAST: true
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const lazyImports = findLazyImportsAST(ast);
|
|
93
|
+
for (const lazy of lazyImports) results.push({
|
|
94
|
+
line: lazy.line,
|
|
95
|
+
source: lazy.source,
|
|
96
|
+
raw: `import('${lazy.source}')`,
|
|
97
|
+
isTypeOnly: false,
|
|
98
|
+
isDefault: false,
|
|
99
|
+
isDynamic: true,
|
|
100
|
+
symbols: [],
|
|
101
|
+
isSideEffect: false,
|
|
102
|
+
isNamespace: false,
|
|
103
|
+
namespaceAlias: "",
|
|
104
|
+
viaAST: true
|
|
105
|
+
});
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Walk AST to find dynamic import() calls inside function bodies.
|
|
110
|
+
* These are "lazy imports" that create hidden circular dependencies.
|
|
111
|
+
*/
|
|
112
|
+
function findLazyImportsAST(ast) {
|
|
113
|
+
const lazyImports = [];
|
|
114
|
+
walkAST(ast, (node) => {
|
|
115
|
+
if (node.type === "call_expression") {
|
|
116
|
+
const func = node.children[0];
|
|
117
|
+
if (func && func.type === "import") {
|
|
118
|
+
const args = node.children.find((c) => c.type === "arguments");
|
|
119
|
+
if (args) {
|
|
120
|
+
const stringArg = args.children.find((c) => c.type === "string" || c.type === "template_string");
|
|
121
|
+
if (stringArg) {
|
|
122
|
+
const source = stringArg.text.replace(/^['"]|['"]$/g, "");
|
|
123
|
+
lazyImports.push({
|
|
124
|
+
source,
|
|
125
|
+
line: node.startRow + 1,
|
|
126
|
+
insideFunction: true,
|
|
127
|
+
isDynamic: true
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return lazyImports;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* AST-enhanced barrel file detection.
|
|
139
|
+
* Uses tree-sitter to find all export nodes including type-only exports
|
|
140
|
+
* that the regex-based detector misses.
|
|
141
|
+
*/
|
|
142
|
+
async function detectBarrelFileAST(content, filePath) {
|
|
143
|
+
if (!await initParser()) return null;
|
|
144
|
+
const ast = await parseFile(content, filePath.endsWith(".tsx") || filePath.endsWith(".jsx"));
|
|
145
|
+
if (!ast) return null;
|
|
146
|
+
const reExports = [];
|
|
147
|
+
let hasNonExportCode = false;
|
|
148
|
+
findNodesOfTypes(ast, ["export_statement", "export_declaration"]);
|
|
149
|
+
const topLevelTypes = /* @__PURE__ */ new Set();
|
|
150
|
+
walkAST(ast, (node) => {
|
|
151
|
+
if (node.type === "program") {
|
|
152
|
+
for (const child of node.children) {
|
|
153
|
+
const t = child.type;
|
|
154
|
+
if (t !== "comment" && t !== "//" && t !== "/*") topLevelTypes.add(t);
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const programNode = ast.type === "program" ? ast : null;
|
|
160
|
+
if (programNode) for (const child of programNode.children) {
|
|
161
|
+
const t = child.type;
|
|
162
|
+
if (!child.text.trim() || t === "comment" || t === "//" || t === "/*") continue;
|
|
163
|
+
if (t === "export_statement" || t === "export_declaration") {
|
|
164
|
+
const parsed = parseExportNodeAST(child);
|
|
165
|
+
if (parsed) {
|
|
166
|
+
reExports.push(parsed);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
hasNonExportCode = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (reExports.length > 0 && !hasNonExportCode) return {
|
|
174
|
+
filePath: "",
|
|
175
|
+
reExports
|
|
176
|
+
};
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Parse an export AST node into a re-export descriptor.
|
|
181
|
+
*/
|
|
182
|
+
function parseExportNodeAST(node) {
|
|
183
|
+
const text = node.text.trim();
|
|
184
|
+
if (/\bexport\s+\*\s+from\s+/.test(text)) {
|
|
185
|
+
const sourceMatch = text.match(/from\s+['"]([^'"]+)['"]/);
|
|
186
|
+
if (sourceMatch) return {
|
|
187
|
+
source: sourceMatch[1],
|
|
188
|
+
symbols: [],
|
|
189
|
+
isWildcard: true
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const isTypeOnly = /\bexport\s+type\s+\{/.test(text);
|
|
193
|
+
const namedExport = text.match(/\bexport\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
|
|
194
|
+
if (namedExport) {
|
|
195
|
+
const symbols = namedExport[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
196
|
+
return {
|
|
197
|
+
source: namedExport[2],
|
|
198
|
+
symbols,
|
|
199
|
+
isWildcard: false,
|
|
200
|
+
isTypeOnly
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const localExport = text.match(/\bexport\s+(?:type\s+)?\{([^}]+)\}/);
|
|
204
|
+
if (localExport && !text.includes("from")) return {
|
|
205
|
+
source: ".",
|
|
206
|
+
symbols: localExport[1].split(",").map((s) => s.trim()).filter(Boolean),
|
|
207
|
+
isWildcard: false,
|
|
208
|
+
isTypeOnly
|
|
209
|
+
};
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* AST-enhanced unused symbol detection.
|
|
214
|
+
* Walks the AST to find actual identifier usage in value and type positions,
|
|
215
|
+
* producing more accurate results than the regex word-boundary check.
|
|
216
|
+
*/
|
|
217
|
+
function findUsedSymbolsAST(ast, symbolNames) {
|
|
218
|
+
const usedSymbols = /* @__PURE__ */ new Set();
|
|
219
|
+
const symbolSet = new Set(symbolNames);
|
|
220
|
+
walkAST(ast, (node) => {
|
|
221
|
+
if (node.type === "identifier" || node.type === "type_identifier") {
|
|
222
|
+
const name = node.text;
|
|
223
|
+
if (symbolSet.has(name)) {
|
|
224
|
+
const parent = node.parent;
|
|
225
|
+
if (parent && (parent.type === "import_statement" || parent.type === "import_declaration" || parent.type === "named_imports" || parent.type === "import_clause" || parent.type === "import_specifier")) return;
|
|
226
|
+
usedSymbols.add(name);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (node.type === "property_identifier" || node.type === "member_expression") {
|
|
230
|
+
const text = node.text;
|
|
231
|
+
for (const sym of symbolNames) if (text.startsWith(sym + ".") || text === sym) usedSymbols.add(sym);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return usedSymbols;
|
|
235
|
+
}
|
|
236
|
+
/** Make a Diagnostic with sensible defaults */
|
|
237
|
+
function diag(filePath, rule, severity, message, line, help, opts = {}) {
|
|
238
|
+
return {
|
|
239
|
+
filePath,
|
|
240
|
+
engine: "import-intelligence",
|
|
241
|
+
rule,
|
|
242
|
+
severity,
|
|
243
|
+
message,
|
|
244
|
+
help,
|
|
245
|
+
line,
|
|
246
|
+
column: opts.column ?? 1,
|
|
247
|
+
category: "imports",
|
|
248
|
+
fixable: opts.fixable ?? false,
|
|
249
|
+
suggestion: opts.suggestion,
|
|
250
|
+
detail: opts.detail
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/** Parse an import line into richer ParsedImport (regex fallback) */
|
|
254
|
+
function parseImport(imp) {
|
|
255
|
+
const raw = imp.raw.trim();
|
|
256
|
+
const isSideEffect = SIDE_EFFECT_RE.test(raw);
|
|
257
|
+
let symbols = [];
|
|
258
|
+
const namedMatch = raw.match(NAMED_IMPORTS_RE);
|
|
259
|
+
if (namedMatch) symbols = namedMatch[1].split(",").map((s) => {
|
|
260
|
+
const trimmed = s.trim();
|
|
261
|
+
const asMatch = trimmed.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
262
|
+
return asMatch ? asMatch[2] : trimmed;
|
|
263
|
+
}).filter(Boolean);
|
|
264
|
+
const defaultMatch = raw.match(DEFAULT_IMPORT_RE);
|
|
265
|
+
if (defaultMatch && !raw.includes("{")) symbols.push(defaultMatch[1]);
|
|
266
|
+
let isNamespace = false;
|
|
267
|
+
let namespaceAlias = "";
|
|
268
|
+
const nsMatch = raw.match(NAMESPACE_IMPORT_RE);
|
|
269
|
+
if (nsMatch) {
|
|
270
|
+
isNamespace = true;
|
|
271
|
+
namespaceAlias = nsMatch[1];
|
|
272
|
+
symbols.push(nsMatch[1]);
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
...imp,
|
|
276
|
+
symbols,
|
|
277
|
+
isSideEffect,
|
|
278
|
+
isNamespace,
|
|
279
|
+
namespaceAlias,
|
|
280
|
+
viaAST: false
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** Check if a symbol is used in the file body (text after the import block) */
|
|
284
|
+
function isSymbolUsed(symbol, bodyAfterImports) {
|
|
285
|
+
return new RegExp(`\\b${escapeRegex(symbol)}\\b`).test(bodyAfterImports);
|
|
286
|
+
}
|
|
287
|
+
/** Escape special regex characters */
|
|
288
|
+
function escapeRegex(s) {
|
|
289
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
290
|
+
}
|
|
291
|
+
/** Read and parse package.json */
|
|
292
|
+
async function readPackageJson(rootDir) {
|
|
293
|
+
try {
|
|
294
|
+
const content = await readFileContent(join(rootDir, "package.json"));
|
|
295
|
+
const pkg = JSON.parse(content);
|
|
296
|
+
return {
|
|
297
|
+
...pkg.dependencies,
|
|
298
|
+
...pkg.devDependencies,
|
|
299
|
+
...pkg.peerDependencies
|
|
300
|
+
};
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** Read and parse tsconfig.json for paths and compilerOptions */
|
|
306
|
+
async function readTsConfig(rootDir) {
|
|
307
|
+
try {
|
|
308
|
+
const content = await readFileContent(join(rootDir, "tsconfig.json"));
|
|
309
|
+
const co = JSON.parse(content).compilerOptions ?? {};
|
|
310
|
+
return {
|
|
311
|
+
paths: co.paths,
|
|
312
|
+
baseUrl: co.baseUrl,
|
|
313
|
+
jsx: co.jsx,
|
|
314
|
+
jsxImportSource: co.jsxImportSource
|
|
315
|
+
};
|
|
316
|
+
} catch {
|
|
317
|
+
return {};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Check whether a file path exists */
|
|
321
|
+
async function fileExists(p) {
|
|
322
|
+
try {
|
|
323
|
+
return (await stat(p)).isFile();
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Resolve a tsconfig path alias to a real path */
|
|
329
|
+
function resolveAliasPath(source, paths, baseUrl, rootDir) {
|
|
330
|
+
const sortedAliases = Object.keys(paths).sort((a, b) => b.length - a.length);
|
|
331
|
+
for (const alias of sortedAliases) {
|
|
332
|
+
const aliasRegexStr = "^" + escapeRegex(alias).replace(/\\\*/g, "(.*)") + "$";
|
|
333
|
+
const match = source.match(new RegExp(aliasRegexStr));
|
|
334
|
+
if (match) {
|
|
335
|
+
const resolved = paths[alias][0].replace(/\*/g, match[1]);
|
|
336
|
+
return {
|
|
337
|
+
alias,
|
|
338
|
+
resolvedPattern: resolve(rootDir, baseUrl ?? ".", resolved)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Merge regex-parsed and AST-parsed imports, preferring AST-confirmed.
|
|
346
|
+
* Key: (source, line) — same import from same line should be counted once.
|
|
347
|
+
*/
|
|
348
|
+
function mergeImportSources(regexImports, astImports) {
|
|
349
|
+
if (!astImports) return regexImports;
|
|
350
|
+
const seen = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const imp of regexImports) {
|
|
352
|
+
const key = `${imp.source}:${imp.line}`;
|
|
353
|
+
seen.set(key, imp);
|
|
354
|
+
}
|
|
355
|
+
for (const imp of astImports) {
|
|
356
|
+
const key = `${imp.source}:${imp.line}`;
|
|
357
|
+
const existing = seen.get(key);
|
|
358
|
+
if (existing) seen.set(key, {
|
|
359
|
+
...existing,
|
|
360
|
+
symbols: imp.symbols.length > 0 ? imp.symbols : existing.symbols,
|
|
361
|
+
isTypeOnly: imp.isTypeOnly ?? existing.isTypeOnly,
|
|
362
|
+
viaAST: true
|
|
363
|
+
});
|
|
364
|
+
else seen.set(key, imp);
|
|
365
|
+
}
|
|
366
|
+
return [...seen.values()];
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Deduplicate diagnostics: when the same (filePath, rule, line) appears
|
|
370
|
+
* from both regex and AST paths, prefer the AST-confirmed one.
|
|
371
|
+
*/
|
|
372
|
+
function deduplicateDiagnostics(diagnostics) {
|
|
373
|
+
const seen = /* @__PURE__ */ new Map();
|
|
374
|
+
for (const d of diagnostics) {
|
|
375
|
+
const key = `${d.filePath}:${d.rule}:${d.line}`;
|
|
376
|
+
const existing = seen.get(key);
|
|
377
|
+
if (!existing) {
|
|
378
|
+
seen.set(key, d);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const existingAST = existing.detail?.astConfirmed === true;
|
|
382
|
+
const currentAST = d.detail?.astConfirmed === true;
|
|
383
|
+
if (currentAST && !existingAST) seen.set(key, d);
|
|
384
|
+
else if (currentAST && existingAST) {
|
|
385
|
+
if ((d.suggestion?.confidence ?? 0) > (existing.suggestion?.confidence ?? 0)) seen.set(key, d);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return [...seen.values()];
|
|
389
|
+
}
|
|
390
|
+
function checkAlternativeImports(parsed, filePath, dependencies, frameworks, isReactAutoJsx) {
|
|
391
|
+
const diagnostics = [];
|
|
392
|
+
const pkgName = parsed.source.startsWith("@") ? parsed.source.split("/").slice(0, 2).join("/") : parsed.source.split("/")[0];
|
|
393
|
+
const template = TREE_SHAKEABLE_PACKAGES[pkgName];
|
|
394
|
+
if (template && parsed.symbols.length > 0 && !parsed.isNamespace) {
|
|
395
|
+
const alternatives = parsed.symbols.map((sym) => {
|
|
396
|
+
return `import ${sym} from '${template.replace("{symbol}", sym)}'`;
|
|
397
|
+
});
|
|
398
|
+
const baseConfidence = parsed.viaAST ? .9 : .85;
|
|
399
|
+
diagnostics.push(diag(filePath, "import-intelligence/tree-shakeable", "suggestion", `Tree-shakeable alternative available for '${pkgName}' import`, parsed.line, `Replace with deep imports so bundlers can eliminate unused code.`, {
|
|
400
|
+
fixable: true,
|
|
401
|
+
suggestion: {
|
|
402
|
+
type: "replace",
|
|
403
|
+
text: alternatives.join("\n"),
|
|
404
|
+
range: {
|
|
405
|
+
startLine: parsed.line,
|
|
406
|
+
startCol: 1,
|
|
407
|
+
endLine: parsed.line,
|
|
408
|
+
endCol: parsed.raw.length + 1
|
|
409
|
+
},
|
|
410
|
+
confidence: baseConfidence,
|
|
411
|
+
reason: `Deep imports from '${pkgName}' allow bundlers to tree-shake unused modules, reducing bundle size significantly. Named imports from the barrel pull in the entire package.`
|
|
412
|
+
},
|
|
413
|
+
detail: { astConfirmed: parsed.viaAST }
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
if (isReactAutoJsx && parsed.source === "react") {
|
|
417
|
+
if (parsed.isDefault && parsed.symbols.length === 1 && parsed.symbols[0] === "React") diagnostics.push(diag(filePath, "import-intelligence/react-auto-jsx", "suggestion", `Default React import is unnecessary with automatic JSX runtime`, parsed.line, `Remove the default React import or switch to named imports (hooks, etc.) if you use them.`, {
|
|
418
|
+
fixable: true,
|
|
419
|
+
suggestion: {
|
|
420
|
+
type: "delete",
|
|
421
|
+
text: "",
|
|
422
|
+
range: {
|
|
423
|
+
startLine: parsed.line,
|
|
424
|
+
startCol: 1,
|
|
425
|
+
endLine: parsed.line,
|
|
426
|
+
endCol: parsed.raw.length + 1
|
|
427
|
+
},
|
|
428
|
+
confidence: parsed.viaAST ? .85 : .8,
|
|
429
|
+
reason: `With React ${REACT_AUTOMATIC_JSX_VERSION}+ automatic JSX runtime (jsx: 'react-jsx' in tsconfig), 'import React' is not needed for JSX transforms. Removing it reduces unused imports.`
|
|
430
|
+
},
|
|
431
|
+
detail: { astConfirmed: parsed.viaAST }
|
|
432
|
+
}));
|
|
433
|
+
if (parsed.isDefault && parsed.symbols.length > 1 && parsed.symbols.includes("React")) {
|
|
434
|
+
const replacement = `import { ${parsed.symbols.filter((s) => s !== "React").join(", ")} } from 'react'`;
|
|
435
|
+
diagnostics.push(diag(filePath, "import-intelligence/react-auto-jsx-named", "suggestion", `Default React import is unnecessary with automatic JSX runtime; keep named imports only`, parsed.line, `Remove the default React import and keep only the named hooks/utilities.`, {
|
|
436
|
+
fixable: true,
|
|
437
|
+
suggestion: {
|
|
438
|
+
type: "replace",
|
|
439
|
+
text: replacement,
|
|
440
|
+
range: {
|
|
441
|
+
startLine: parsed.line,
|
|
442
|
+
startCol: 1,
|
|
443
|
+
endLine: parsed.line,
|
|
444
|
+
endCol: parsed.raw.length + 1
|
|
445
|
+
},
|
|
446
|
+
confidence: parsed.viaAST ? .85 : .8,
|
|
447
|
+
reason: `With automatic JSX runtime, the default 'React' import is unused. Keeping only named imports is cleaner and avoids pulling in the full React object.`
|
|
448
|
+
},
|
|
449
|
+
detail: { astConfirmed: parsed.viaAST }
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return diagnostics;
|
|
454
|
+
}
|
|
455
|
+
/** Detect barrel files (index.ts that only re-export) — regex fallback */
|
|
456
|
+
function detectBarrelFile(content) {
|
|
457
|
+
const lines = toLines(content);
|
|
458
|
+
const reExports = [];
|
|
459
|
+
let hasNonExportCode = false;
|
|
460
|
+
for (const { text } of lines) {
|
|
461
|
+
const trimmed = text.trim();
|
|
462
|
+
if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("/*")) continue;
|
|
463
|
+
const namedExport = trimmed.match(/^export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"];?/);
|
|
464
|
+
if (namedExport) {
|
|
465
|
+
const symbols = namedExport[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
466
|
+
reExports.push({
|
|
467
|
+
source: namedExport[2],
|
|
468
|
+
symbols,
|
|
469
|
+
isWildcard: false
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const wildcardExport = trimmed.match(/^export\s+\*\s+from\s+['"]([^'"]+)['"];?/);
|
|
474
|
+
if (wildcardExport) {
|
|
475
|
+
reExports.push({
|
|
476
|
+
source: wildcardExport[1],
|
|
477
|
+
symbols: [],
|
|
478
|
+
isWildcard: true
|
|
479
|
+
});
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const typeExport = trimmed.match(/^export\s+type\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"];?/);
|
|
483
|
+
if (typeExport) {
|
|
484
|
+
const symbols = typeExport[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
485
|
+
reExports.push({
|
|
486
|
+
source: typeExport[2],
|
|
487
|
+
symbols,
|
|
488
|
+
isWildcard: false,
|
|
489
|
+
isTypeOnly: true
|
|
490
|
+
});
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const localExport = trimmed.match(/^export\s+\{([^}]+)\};?/);
|
|
494
|
+
if (localExport) {
|
|
495
|
+
const symbols = localExport[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
496
|
+
reExports.push({
|
|
497
|
+
source: ".",
|
|
498
|
+
symbols,
|
|
499
|
+
isWildcard: false
|
|
500
|
+
});
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
hasNonExportCode = true;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
if (reExports.length > 0 && !hasNonExportCode) return {
|
|
507
|
+
filePath: "",
|
|
508
|
+
reExports
|
|
509
|
+
};
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
function checkBarrelOptimization(parsed, filePath, barrelCache, rootDir) {
|
|
513
|
+
const diagnostics = [];
|
|
514
|
+
if (!parsed.source.startsWith(".")) return diagnostics;
|
|
515
|
+
const barrelKey = parsed.source;
|
|
516
|
+
const barrel = barrelCache.get(barrelKey);
|
|
517
|
+
if (!barrel) return diagnostics;
|
|
518
|
+
const symbolToSource = /* @__PURE__ */ new Map();
|
|
519
|
+
for (const reExport of barrel.reExports) {
|
|
520
|
+
if (reExport.isWildcard) continue;
|
|
521
|
+
for (const sym of reExport.symbols) {
|
|
522
|
+
const localName = sym.includes(" as ") ? sym.split(" as ")[1].trim() : sym;
|
|
523
|
+
symbolToSource.set(localName, reExport.source);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const sourceGroups = /* @__PURE__ */ new Map();
|
|
527
|
+
for (const sym of parsed.symbols) {
|
|
528
|
+
const source = symbolToSource.get(sym);
|
|
529
|
+
if (source && source !== ".") {
|
|
530
|
+
if (!sourceGroups.has(source)) sourceGroups.set(source, []);
|
|
531
|
+
sourceGroups.get(source).push(sym);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (sourceGroups.size > 0) {
|
|
535
|
+
const replacementLines = [];
|
|
536
|
+
for (const [source, symbols] of sourceGroups) replacementLines.push(`import { ${symbols.join(", ")} } from '${source}'`);
|
|
537
|
+
const hasTypeOnlyReExports = barrel.reExports.some((r) => r.isTypeOnly);
|
|
538
|
+
const baseConfidence = parsed.viaAST || hasTypeOnlyReExports ? .78 : .7;
|
|
539
|
+
diagnostics.push(diag(filePath, "import-intelligence/barrel-bypass", "suggestion", `Import directly from source instead of barrel file '${parsed.source}'`, parsed.line, `Direct imports avoid the barrel file indirection, improving tree-shaking and build speed.`, {
|
|
540
|
+
fixable: true,
|
|
541
|
+
suggestion: {
|
|
542
|
+
type: "replace",
|
|
543
|
+
text: replacementLines.join("\n"),
|
|
544
|
+
range: {
|
|
545
|
+
startLine: parsed.line,
|
|
546
|
+
startCol: 1,
|
|
547
|
+
endLine: parsed.line,
|
|
548
|
+
endCol: parsed.raw.length + 1
|
|
549
|
+
},
|
|
550
|
+
confidence: baseConfidence,
|
|
551
|
+
reason: `Barrel files (index.ts re-exporting from sub-modules) prevent bundlers from tree-shaking effectively. Importing from the source module directly allows the bundler to only include what you actually use, and reduces module resolution overhead during builds.`
|
|
552
|
+
},
|
|
553
|
+
detail: {
|
|
554
|
+
astConfirmed: parsed.viaAST,
|
|
555
|
+
hasTypeOnlyReExports
|
|
556
|
+
}
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
return diagnostics;
|
|
560
|
+
}
|
|
561
|
+
async function checkAliasResolution(parsed, filePath, paths, baseUrl, rootDir) {
|
|
562
|
+
const diagnostics = [];
|
|
563
|
+
if (!paths || Object.keys(paths).length === 0) return diagnostics;
|
|
564
|
+
const aliasResult = resolveAliasPath(parsed.source, paths, baseUrl ?? ".", rootDir);
|
|
565
|
+
if (!aliasResult) return diagnostics;
|
|
566
|
+
const resolvedPath = aliasResult.resolvedPattern;
|
|
567
|
+
let found = false;
|
|
568
|
+
for (const ext of [
|
|
569
|
+
"",
|
|
570
|
+
".ts",
|
|
571
|
+
".tsx",
|
|
572
|
+
".js",
|
|
573
|
+
".jsx",
|
|
574
|
+
"/index.ts",
|
|
575
|
+
"/index.tsx",
|
|
576
|
+
"/index.js"
|
|
577
|
+
]) if (await fileExists(resolvedPath + ext)) {
|
|
578
|
+
found = true;
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
if (!found) diagnostics.push(diag(filePath, "import-intelligence/broken-alias", "error", `Alias '${aliasResult.alias}' resolves to '${resolvedPath}' which does not exist`, parsed.line, `Fix the tsconfig paths mapping or the import path.`, {
|
|
582
|
+
fixable: false,
|
|
583
|
+
suggestion: {
|
|
584
|
+
type: "replace",
|
|
585
|
+
text: `/* TODO: fix alias — ${parsed.source} resolves to non-existent ${resolvedPath} */`,
|
|
586
|
+
confidence: parsed.viaAST ? .95 : .9,
|
|
587
|
+
reason: `The tsconfig paths alias '${aliasResult.alias}' maps to '${resolvedPath}', but no file exists at that location. This will cause a TypeScript compilation error or runtime module-not-found.`
|
|
588
|
+
},
|
|
589
|
+
detail: {
|
|
590
|
+
alias: aliasResult.alias,
|
|
591
|
+
resolvedPath,
|
|
592
|
+
originalImport: parsed.source,
|
|
593
|
+
astConfirmed: parsed.viaAST
|
|
594
|
+
}
|
|
595
|
+
}));
|
|
596
|
+
else {
|
|
597
|
+
const relPath = relative(dirname(filePath), resolvedPath);
|
|
598
|
+
const canonicalPath = relPath.startsWith(".") ? relPath : "./" + relPath;
|
|
599
|
+
if (parsed.source !== canonicalPath && !parsed.source.startsWith(".")) diagnostics.push(diag(filePath, "import-intelligence/alias-canonical", "info", `Alias '${parsed.source}' resolves to '${canonicalPath}'`, parsed.line, `Consider using the relative path for explicitness in small projects, or keep the alias for large monorepos.`, {
|
|
600
|
+
suggestion: {
|
|
601
|
+
type: "replace",
|
|
602
|
+
text: `from '${canonicalPath}'`,
|
|
603
|
+
confidence: parsed.viaAST ? .6 : .5,
|
|
604
|
+
reason: `Using the resolved relative path makes the dependency graph explicit without relying on tsconfig alias resolution, but aliases are often preferred for readability in large codebases.`
|
|
605
|
+
},
|
|
606
|
+
detail: { astConfirmed: parsed.viaAST }
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
return diagnostics;
|
|
610
|
+
}
|
|
611
|
+
function buildImportGraph(fileImports, rootDir) {
|
|
612
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
613
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
614
|
+
for (const [filePath, imports] of fileImports) {
|
|
615
|
+
const deps = /* @__PURE__ */ new Set();
|
|
616
|
+
for (const imp of imports) if (imp.source.startsWith(".")) {
|
|
617
|
+
const resolved = resolve(dirname(filePath), imp.source);
|
|
618
|
+
deps.add(resolved);
|
|
619
|
+
}
|
|
620
|
+
adjacency.set(filePath, deps);
|
|
621
|
+
for (const dep of deps) {
|
|
622
|
+
if (!reverse.has(dep)) reverse.set(dep, /* @__PURE__ */ new Set());
|
|
623
|
+
reverse.get(dep).add(filePath);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
adjacency,
|
|
628
|
+
reverse
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/** DFS-based cycle detection with path tracking */
|
|
632
|
+
function detectCycles(graph, maxDepth) {
|
|
633
|
+
const cycles = [];
|
|
634
|
+
const visited = /* @__PURE__ */ new Set();
|
|
635
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
636
|
+
const stack = [];
|
|
637
|
+
function dfs(node) {
|
|
638
|
+
if (inStack.has(node)) {
|
|
639
|
+
const cycleStart = stack.indexOf(node);
|
|
640
|
+
if (cycleStart !== -1) {
|
|
641
|
+
const cyclePath = stack.slice(cycleStart);
|
|
642
|
+
cyclePath.push(node);
|
|
643
|
+
cycles.push({
|
|
644
|
+
cycle: cyclePath,
|
|
645
|
+
depth: cyclePath.length - 1
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (visited.has(node)) return;
|
|
651
|
+
visited.add(node);
|
|
652
|
+
inStack.add(node);
|
|
653
|
+
stack.push(node);
|
|
654
|
+
if (stack.length <= maxDepth) {
|
|
655
|
+
const neighbors = graph.adjacency.get(node) ?? /* @__PURE__ */ new Set();
|
|
656
|
+
for (const neighbor of neighbors) dfs(neighbor);
|
|
657
|
+
}
|
|
658
|
+
stack.pop();
|
|
659
|
+
inStack.delete(node);
|
|
660
|
+
}
|
|
661
|
+
for (const node of graph.adjacency.keys()) dfs(node);
|
|
662
|
+
const seen = /* @__PURE__ */ new Set();
|
|
663
|
+
const unique = [];
|
|
664
|
+
for (const c of cycles) {
|
|
665
|
+
const key = [...c.cycle.slice(0, -1)].sort().join("→");
|
|
666
|
+
if (!seen.has(key)) {
|
|
667
|
+
seen.add(key);
|
|
668
|
+
unique.push(c);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return unique;
|
|
672
|
+
}
|
|
673
|
+
function reportCycles(cycles, rootDir, hasASTImports) {
|
|
674
|
+
const diagnostics = [];
|
|
675
|
+
for (const { cycle, depth } of cycles) {
|
|
676
|
+
const chain = cycle.map((p) => relative(rootDir, p) || p).join(" → ");
|
|
677
|
+
const involvedFiles = cycle.slice(0, -1);
|
|
678
|
+
const firstFile = involvedFiles[0] ?? "";
|
|
679
|
+
const relFirst = relative(rootDir, firstFile) || firstFile;
|
|
680
|
+
diagnostics.push(diag(relFirst, "import-intelligence/circular-dependency", "warning", `Circular dependency detected: ${chain}`, 1, `Break the cycle by extracting shared logic into a separate module that both files can import without creating a loop.`, {
|
|
681
|
+
fixable: false,
|
|
682
|
+
detail: {
|
|
683
|
+
cycle: involvedFiles.map((p) => relative(rootDir, p)),
|
|
684
|
+
depth,
|
|
685
|
+
astConfirmed: hasASTImports
|
|
686
|
+
},
|
|
687
|
+
suggestion: {
|
|
688
|
+
type: "refactor",
|
|
689
|
+
text: `/* Circular: ${chain} — extract shared code to break the cycle */`,
|
|
690
|
+
confidence: hasASTImports ? .97 : .95,
|
|
691
|
+
reason: `Circular dependencies create fragile coupling, can cause initialization order bugs, and make the module graph harder to reason about. Extracting the shared dependency into a third module breaks the cycle cleanly.${hasASTImports ? " AST analysis also checked lazy imports inside function bodies." : ""}`
|
|
692
|
+
}
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
return diagnostics;
|
|
696
|
+
}
|
|
697
|
+
function checkImportClassification(parsed, filePath, fileContent, astUsedSymbols) {
|
|
698
|
+
const diagnostics = [];
|
|
699
|
+
if (parsed.isSideEffect) {
|
|
700
|
+
diagnostics.push(diag(filePath, "import-intelligence/side-effect-import", "info", `Side-effect import: '${parsed.source}' (no bindings imported)`, parsed.line, `Ensure this import is needed for its side effects (polyfills, CSS, etc.). Remove if unnecessary.`, {
|
|
701
|
+
suggestion: {
|
|
702
|
+
type: "delete",
|
|
703
|
+
text: "",
|
|
704
|
+
confidence: .3,
|
|
705
|
+
reason: `Side-effect imports ('import foo') load a module for its side effects only. This is correct for polyfills, CSS, and global setup, but can be an accidental leftover if the module was expected to provide bindings.`
|
|
706
|
+
},
|
|
707
|
+
detail: { astConfirmed: parsed.viaAST }
|
|
708
|
+
}));
|
|
709
|
+
return diagnostics;
|
|
710
|
+
}
|
|
711
|
+
if (parsed.isDynamic) return diagnostics;
|
|
712
|
+
if (!parsed.isTypeOnly && parsed.symbols.length > 0) {
|
|
713
|
+
let allTypeUsage;
|
|
714
|
+
if (astUsedSymbols !== void 0 && parsed.viaAST) allTypeUsage = parsed.symbols.every((sym) => {
|
|
715
|
+
if (!astUsedSymbols.has(sym)) return true;
|
|
716
|
+
return isTypeOnlyUsage(sym, getBodyAfterImports(fileContent, parsed.line));
|
|
717
|
+
});
|
|
718
|
+
else {
|
|
719
|
+
const bodyAfterImports = getBodyAfterImports(fileContent, parsed.line);
|
|
720
|
+
allTypeUsage = parsed.symbols.every((sym) => {
|
|
721
|
+
return isTypeOnlyUsage(sym, bodyAfterImports);
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
if (allTypeUsage) {
|
|
725
|
+
const replacement = parsed.raw.replace(/^import\s+/, "import type ").replace(/^import\s+type\s+type\s+/, "import type ");
|
|
726
|
+
diagnostics.push(diag(filePath, "import-intelligence/type-only-import", "suggestion", `All imported symbols from '${parsed.source}' are used only as types — use 'import type'`, parsed.line, `Switch to 'import type' for better tree-shaking and to make intent explicit.`, {
|
|
727
|
+
fixable: true,
|
|
728
|
+
suggestion: {
|
|
729
|
+
type: "replace",
|
|
730
|
+
text: replacement,
|
|
731
|
+
range: {
|
|
732
|
+
startLine: parsed.line,
|
|
733
|
+
startCol: 1,
|
|
734
|
+
endLine: parsed.line,
|
|
735
|
+
endCol: parsed.raw.length + 1
|
|
736
|
+
},
|
|
737
|
+
confidence: parsed.viaAST ? .85 : .75,
|
|
738
|
+
reason: `Using 'import type' makes it explicit that the import is type-only, allowing TypeScript compilers and bundlers to erase it at build time. This reduces runtime bundle size and clarifies the module's role.`
|
|
739
|
+
},
|
|
740
|
+
detail: { astConfirmed: parsed.viaAST }
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return diagnostics;
|
|
745
|
+
}
|
|
746
|
+
/** Get the file content after the last import line */
|
|
747
|
+
function getBodyAfterImports(content, lastImportLine) {
|
|
748
|
+
return content.split("\n").slice(lastImportLine).join("\n");
|
|
749
|
+
}
|
|
750
|
+
/** Heuristic: check if a symbol is used only in type positions */
|
|
751
|
+
function isTypeOnlyUsage(symbol, body) {
|
|
752
|
+
let stripped = body;
|
|
753
|
+
stripped = stripped.replace(/:\s*[A-Z]\w*(?:\s*[&|]\s*[A-Z]\w*)*/g, "");
|
|
754
|
+
stripped = stripped.replace(/<[^>]*>/g, "");
|
|
755
|
+
stripped = stripped.replace(/\bas\s+[A-Z]\w*/g, "");
|
|
756
|
+
stripped = stripped.replace(/\b(?:extends|implements)\s+[A-Z]\w*/g, "");
|
|
757
|
+
stripped = stripped.replace(/\btype\s+\w+\s*=\s*[A-Z]\w*/g, "");
|
|
758
|
+
stripped = stripped.replace(/\binterface\s+\w+[^{]*/g, "");
|
|
759
|
+
return !new RegExp(`\\b${escapeRegex(symbol)}\\b`).test(stripped);
|
|
760
|
+
}
|
|
761
|
+
function checkUnusedImports(parsed, filePath, fileContent, astUsedSymbols) {
|
|
762
|
+
const diagnostics = [];
|
|
763
|
+
if (parsed.isSideEffect || parsed.isNamespace || parsed.isDynamic) return diagnostics;
|
|
764
|
+
const bodyAfterImports = getBodyAfterImports(fileContent, parsed.line);
|
|
765
|
+
const unusedSymbols = [];
|
|
766
|
+
for (const sym of parsed.symbols) {
|
|
767
|
+
if (DRIZZLE_FP_SYMBOLS.has(sym) && DRIZZLE_PACKAGES.has(parsed.source.split("/").slice(0, 2).join("/"))) continue;
|
|
768
|
+
if (astUsedSymbols !== void 0 && parsed.viaAST) {
|
|
769
|
+
if (!astUsedSymbols.has(sym) && !isSymbolUsed(sym, bodyAfterImports)) unusedSymbols.push(sym);
|
|
770
|
+
} else if (!isSymbolUsed(sym, bodyAfterImports)) unusedSymbols.push(sym);
|
|
771
|
+
}
|
|
772
|
+
if (unusedSymbols.length > 0 && unusedSymbols.length < parsed.symbols.length) {
|
|
773
|
+
const usedSymbols = parsed.symbols.filter((s) => !unusedSymbols.includes(s));
|
|
774
|
+
const replacement = parsed.raw.replace(/\{[^}]+\}/, `{ ${usedSymbols.join(", ")} }`);
|
|
775
|
+
diagnostics.push(diag(filePath, "import-intelligence/unused-symbol", "warning", `Unused imported symbols: ${unusedSymbols.join(", ")}`, parsed.line, `Remove unused symbols from the import to keep the codebase clean.`, {
|
|
776
|
+
fixable: true,
|
|
777
|
+
suggestion: {
|
|
778
|
+
type: "replace",
|
|
779
|
+
text: replacement,
|
|
780
|
+
range: {
|
|
781
|
+
startLine: parsed.line,
|
|
782
|
+
startCol: 1,
|
|
783
|
+
endLine: parsed.line,
|
|
784
|
+
endCol: parsed.raw.length + 1
|
|
785
|
+
},
|
|
786
|
+
confidence: parsed.viaAST ? .95 : .9,
|
|
787
|
+
reason: `Unused imported symbols add noise and may cause bundlers to include dead code. Removing them clarifies what the module actually depends on.`
|
|
788
|
+
},
|
|
789
|
+
detail: { astConfirmed: parsed.viaAST }
|
|
790
|
+
}));
|
|
791
|
+
} else if (unusedSymbols.length === parsed.symbols.length) {
|
|
792
|
+
const isDrizzle = DRIZZLE_PACKAGES.has(parsed.source.split("/").slice(0, 2).join("/"));
|
|
793
|
+
const allDrizzleFPs = parsed.symbols.every((s) => DRIZZLE_FP_SYMBOLS.has(s));
|
|
794
|
+
if (isDrizzle && allDrizzleFPs) return diagnostics;
|
|
795
|
+
diagnostics.push(diag(filePath, "import-intelligence/unused-import", "warning", `Entire import from '${parsed.source}' is unused`, parsed.line, `Remove the unused import line entirely.`, {
|
|
796
|
+
fixable: true,
|
|
797
|
+
suggestion: {
|
|
798
|
+
type: "delete",
|
|
799
|
+
text: "",
|
|
800
|
+
range: {
|
|
801
|
+
startLine: parsed.line,
|
|
802
|
+
startCol: 1,
|
|
803
|
+
endLine: parsed.line,
|
|
804
|
+
endCol: parsed.raw.length + 1
|
|
805
|
+
},
|
|
806
|
+
confidence: parsed.viaAST ? .92 : .85,
|
|
807
|
+
reason: `This import is never used in the file. Removing it reduces bundle size and avoids misleading readers about the module's dependencies.`
|
|
808
|
+
},
|
|
809
|
+
detail: { astConfirmed: parsed.viaAST }
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
return diagnostics;
|
|
813
|
+
}
|
|
814
|
+
function checkDuplicateImports(allImports, filePath) {
|
|
815
|
+
const diagnostics = [];
|
|
816
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
817
|
+
for (const imp of allImports) {
|
|
818
|
+
const key = imp.source;
|
|
819
|
+
if (!bySource.has(key)) bySource.set(key, []);
|
|
820
|
+
bySource.get(key).push(imp);
|
|
821
|
+
}
|
|
822
|
+
for (const [source, imports] of bySource) {
|
|
823
|
+
if (imports.length < 2) continue;
|
|
824
|
+
const allSymbols = [];
|
|
825
|
+
const hasDefault = imports.some((imp) => imp.isDefault);
|
|
826
|
+
const defaultImport = hasDefault ? imports.find((imp) => imp.isDefault) : void 0;
|
|
827
|
+
defaultImport && defaultImport.symbols.find((s) => !imports.find((imp2) => imp2 !== defaultImport && imp2.symbols.includes(s) && !imp2.isDefault));
|
|
828
|
+
for (const imp of imports) for (const sym of imp.symbols) if (!allSymbols.includes(sym)) allSymbols.push(sym);
|
|
829
|
+
let merged;
|
|
830
|
+
const namedSymbols = allSymbols.filter((s) => {
|
|
831
|
+
if (hasDefault) {
|
|
832
|
+
const defImp = imports.find((imp) => imp.isDefault);
|
|
833
|
+
if (defImp && defImp.symbols[0] === s && s !== "React") return false;
|
|
834
|
+
}
|
|
835
|
+
return true;
|
|
836
|
+
});
|
|
837
|
+
if (hasDefault && namedSymbols.length > 0) merged = `import ${imports.find((imp) => imp.isDefault).symbols[0]}, { ${namedSymbols.join(", ")} } from '${source}'`;
|
|
838
|
+
else if (hasDefault) merged = `import ${imports.find((imp) => imp.isDefault).symbols[0]} from '${source}'`;
|
|
839
|
+
else merged = `import { ${allSymbols.join(", ")} } from '${source}'`;
|
|
840
|
+
const firstLine = Math.min(...imports.map((imp) => imp.line));
|
|
841
|
+
const anyAST = imports.some((imp) => imp.viaAST);
|
|
842
|
+
diagnostics.push(diag(filePath, "import-intelligence/duplicate-import", "suggestion", `Multiple import statements from '${source}' — merge into one`, firstLine, `Combine imports from the same module into a single statement.`, {
|
|
843
|
+
fixable: true,
|
|
844
|
+
suggestion: {
|
|
845
|
+
type: "replace",
|
|
846
|
+
text: merged,
|
|
847
|
+
range: {
|
|
848
|
+
startLine: Math.min(...imports.map((imp) => imp.line)),
|
|
849
|
+
startCol: 1,
|
|
850
|
+
endLine: Math.max(...imports.map((imp) => imp.line)),
|
|
851
|
+
endCol: imports.reduce((max, imp) => imp.line === Math.max(...imports.map((i) => i.line)) ? Math.max(max, imp.raw.length + 1) : max, 0)
|
|
852
|
+
},
|
|
853
|
+
confidence: anyAST ? .93 : .9,
|
|
854
|
+
reason: `Multiple import statements from the same module are redundant and add visual noise. Merging them into a single line makes the dependency on that module clearer and is the conventional style.`
|
|
855
|
+
},
|
|
856
|
+
detail: {
|
|
857
|
+
duplicateLines: imports.map((imp) => imp.line),
|
|
858
|
+
mergedImport: merged,
|
|
859
|
+
astConfirmed: anyAST
|
|
860
|
+
}
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
863
|
+
return diagnostics;
|
|
864
|
+
}
|
|
865
|
+
async function scanBarrelFiles(rootDir, files) {
|
|
866
|
+
const barrels = /* @__PURE__ */ new Map();
|
|
867
|
+
const indexFiles = files.filter((f) => {
|
|
868
|
+
const base = basename(f);
|
|
869
|
+
return base === "index.ts" || base === "index.tsx" || base === "index.js";
|
|
870
|
+
});
|
|
871
|
+
for (const idxFile of indexFiles) try {
|
|
872
|
+
const content = await readFileContent(idxFile);
|
|
873
|
+
let barrel = await detectBarrelFileAST(content, idxFile);
|
|
874
|
+
if (!barrel) barrel = detectBarrelFile(content);
|
|
875
|
+
if (barrel) {
|
|
876
|
+
const relDir = relative(rootDir, dirname(idxFile));
|
|
877
|
+
barrel.filePath = idxFile;
|
|
878
|
+
barrels.set(relDir, barrel);
|
|
879
|
+
barrels.set("./" + relDir, barrel);
|
|
880
|
+
barrels.set(".//" + relDir, barrel);
|
|
881
|
+
const parentRel = relative(rootDir, dirname(idxFile));
|
|
882
|
+
barrels.set(parentRel, barrel);
|
|
883
|
+
}
|
|
884
|
+
} catch {}
|
|
885
|
+
return barrels;
|
|
886
|
+
}
|
|
887
|
+
const importIntelligenceEngine = {
|
|
888
|
+
name: "import-intelligence",
|
|
889
|
+
description: "Deep import analysis: tree-shakeable alternatives, barrel optimization, alias validation, circular dependency detection, import classification, unused detection (with Drizzle FP suppression), and duplicate merging. AST-enhanced via tree-sitter for more accurate results.",
|
|
890
|
+
supportedLanguages: ["typescript", "javascript"],
|
|
891
|
+
async run(context) {
|
|
892
|
+
const startTime = Date.now();
|
|
893
|
+
const diagnostics = [];
|
|
894
|
+
const { rootDirectory, frameworks, config } = context;
|
|
895
|
+
const cfg = config.imports;
|
|
896
|
+
const files = await collectFiles(rootDirectory, ["typescript", "javascript"], config.exclude, context.files);
|
|
897
|
+
if (files.length === 0) return {
|
|
898
|
+
engine: "import-intelligence",
|
|
899
|
+
diagnostics: [],
|
|
900
|
+
elapsed: Date.now() - startTime,
|
|
901
|
+
skipped: true,
|
|
902
|
+
skipReason: "No TypeScript or JavaScript files found to scan."
|
|
903
|
+
};
|
|
904
|
+
const astAvailable = await initParser();
|
|
905
|
+
const dependencies = await readPackageJson(rootDirectory);
|
|
906
|
+
const tsconfig = await readTsConfig(rootDirectory);
|
|
907
|
+
const isReactAutoJsx = frameworks.includes("react") || frameworks.includes("next.js") ? (() => {
|
|
908
|
+
const reactVersion = dependencies?.["react"] ?? dependencies?.["react-dom"] ?? "0";
|
|
909
|
+
return (parseInt(reactVersion.replace(/[^0-9]/g, ""), 10) || 0) >= REACT_AUTOMATIC_JSX_VERSION || tsconfig.jsx === "react-jsx" || tsconfig.jsx === "react-jsxdev";
|
|
910
|
+
})() : false;
|
|
911
|
+
let barrelCache = /* @__PURE__ */ new Map();
|
|
912
|
+
if (cfg.optimizeBarrels) barrelCache = await scanBarrelFiles(rootDirectory, files);
|
|
913
|
+
const fileImportsMap = /* @__PURE__ */ new Map();
|
|
914
|
+
let anyASTUsed = false;
|
|
915
|
+
for (const filePath of files) {
|
|
916
|
+
const relPath = relative(rootDirectory, filePath);
|
|
917
|
+
let content;
|
|
918
|
+
try {
|
|
919
|
+
content = await readFileContent(filePath);
|
|
920
|
+
} catch {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
const regexParsed = extractImports(content, "typescript").map(parseImport);
|
|
924
|
+
let astParsed = null;
|
|
925
|
+
let astUsedSymbols;
|
|
926
|
+
let astRoot = null;
|
|
927
|
+
if (astAvailable) {
|
|
928
|
+
astParsed = await parseImportsAST(content, filePath);
|
|
929
|
+
const isTsx = filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
|
|
930
|
+
astRoot = await parseFile(content, isTsx);
|
|
931
|
+
if (astRoot) {
|
|
932
|
+
const allSymbolNames = [...regexParsed.flatMap((p) => p.symbols), ...astParsed?.flatMap((p) => p.symbols) ?? []];
|
|
933
|
+
const uniqueNames = [...new Set(allSymbolNames)];
|
|
934
|
+
astUsedSymbols = findUsedSymbolsAST(astRoot, uniqueNames);
|
|
935
|
+
anyASTUsed = true;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const parsedImports = mergeImportSources(regexParsed, astParsed);
|
|
939
|
+
fileImportsMap.set(filePath, parsedImports);
|
|
940
|
+
for (const parsed of parsedImports) {
|
|
941
|
+
if (cfg.suggestAlternatives) diagnostics.push(...checkAlternativeImports(parsed, relPath, dependencies, frameworks, isReactAutoJsx));
|
|
942
|
+
if (cfg.optimizeBarrels) diagnostics.push(...checkBarrelOptimization(parsed, relPath, barrelCache, rootDirectory));
|
|
943
|
+
if (cfg.validateAliases && tsconfig.paths) {
|
|
944
|
+
const aliasDiagnostics = await checkAliasResolution(parsed, relPath, tsconfig.paths, tsconfig.baseUrl, rootDirectory);
|
|
945
|
+
diagnostics.push(...aliasDiagnostics);
|
|
946
|
+
}
|
|
947
|
+
diagnostics.push(...checkImportClassification(parsed, relPath, content, astUsedSymbols));
|
|
948
|
+
diagnostics.push(...checkUnusedImports(parsed, relPath, content, astUsedSymbols));
|
|
949
|
+
}
|
|
950
|
+
diagnostics.push(...checkDuplicateImports(parsedImports, relPath));
|
|
951
|
+
}
|
|
952
|
+
if (cfg.buildGraph) {
|
|
953
|
+
const cycleDiagnostics = reportCycles(detectCycles(buildImportGraph(fileImportsMap, rootDirectory), cfg.maxCircularDepth), rootDirectory, anyASTUsed);
|
|
954
|
+
diagnostics.push(...cycleDiagnostics);
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
engine: "import-intelligence",
|
|
958
|
+
diagnostics: deduplicateDiagnostics(diagnostics),
|
|
959
|
+
elapsed: Date.now() - startTime,
|
|
960
|
+
skipped: false
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
//#endregion
|
|
966
|
+
export { importIntelligenceEngine };
|