project-graph-mcp 1.5.0 → 2.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.
Files changed (125) hide show
  1. package/README.md +171 -31
  2. package/docs/img/explorer-compact.jpg +0 -0
  3. package/docs/img/explorer-expanded.jpg +0 -0
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -1
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/.project-graph-cache.json +1 -0
  23. package/src/compact/ai-context.js +7 -0
  24. package/src/compact/compact-migrate.js +17 -0
  25. package/src/compact/compact.js +18 -0
  26. package/src/compact/compress.js +14 -0
  27. package/src/compact/ctx-to-jsdoc.js +29 -0
  28. package/src/compact/doc-dialect.js +30 -0
  29. package/src/compact/expand.js +37 -0
  30. package/src/compact/framework-references.js +5 -0
  31. package/src/compact/instructions.js +3 -0
  32. package/src/compact/mode-config.js +8 -0
  33. package/src/compact/validate-pipeline.js +9 -0
  34. package/src/core/event-bus.js +9 -0
  35. package/src/core/filters.js +14 -0
  36. package/src/core/graph-builder.js +12 -0
  37. package/src/core/parser.js +31 -0
  38. package/src/core/workspace.js +8 -0
  39. package/src/lang/lang-go.js +17 -0
  40. package/src/lang/lang-python.js +12 -0
  41. package/src/lang/lang-sql.js +23 -0
  42. package/src/lang/lang-typescript.js +9 -0
  43. package/src/lang/lang-utils.js +4 -0
  44. package/src/mcp/mcp-server.js +17 -0
  45. package/src/mcp/tool-defs.js +3 -0
  46. package/src/mcp/tools.js +25 -0
  47. package/src/network/backend-lifecycle.js +19 -0
  48. package/src/network/backend.js +5 -0
  49. package/src/network/local-gateway.js +23 -0
  50. package/src/network/mdns.js +13 -0
  51. package/src/network/server.js +10 -0
  52. package/src/network/web-server.js +34 -0
  53. package/web/.project-graph-cache.json +1 -0
  54. package/web/app.js +17 -0
  55. package/web/components/code-block.js +3 -0
  56. package/web/components/quick-open.js +5 -0
  57. package/web/dashboard-state.js +3 -0
  58. package/web/dashboard.html +27 -0
  59. package/web/dashboard.js +8 -0
  60. package/web/highlight.js +13 -0
  61. package/web/index.html +35 -0
  62. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  63. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  64. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  65. package/web/panels/EventItem/EventItem.css.js +1 -0
  66. package/web/panels/EventItem/EventItem.js +4 -0
  67. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  69. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  70. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  72. package/web/panels/ProjectList/ProjectList.js +4 -0
  73. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  74. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  77. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  78. package/web/panels/code-viewer.js +5 -0
  79. package/web/panels/ctx-panel.js +4 -0
  80. package/web/panels/dep-graph.js +6 -0
  81. package/web/panels/file-tree.js +188 -0
  82. package/web/panels/health-panel.js +3 -0
  83. package/web/panels/live-monitor.js +3 -0
  84. package/web/state.js +17 -0
  85. package/web/style.css +157 -0
  86. package/references/symbiote-3x.md +0 -834
  87. package/src/ai-context.js +0 -113
  88. package/src/analysis-cache.js +0 -155
  89. package/src/cli-handlers.js +0 -271
  90. package/src/cli.js +0 -95
  91. package/src/compact.js +0 -207
  92. package/src/complexity.js +0 -237
  93. package/src/compress.js +0 -319
  94. package/src/ctx-to-jsdoc.js +0 -514
  95. package/src/custom-rules.js +0 -584
  96. package/src/db-analysis.js +0 -194
  97. package/src/dead-code.js +0 -468
  98. package/src/doc-dialect.js +0 -716
  99. package/src/filters.js +0 -227
  100. package/src/framework-references.js +0 -177
  101. package/src/full-analysis.js +0 -470
  102. package/src/graph-builder.js +0 -299
  103. package/src/instructions.js +0 -73
  104. package/src/jsdoc-checker.js +0 -351
  105. package/src/jsdoc-generator.js +0 -203
  106. package/src/lang-go.js +0 -285
  107. package/src/lang-python.js +0 -197
  108. package/src/lang-sql.js +0 -309
  109. package/src/lang-typescript.js +0 -190
  110. package/src/lang-utils.js +0 -124
  111. package/src/large-files.js +0 -163
  112. package/src/mcp-server.js +0 -675
  113. package/src/mode-config.js +0 -127
  114. package/src/outdated-patterns.js +0 -296
  115. package/src/parser.js +0 -662
  116. package/src/server.js +0 -28
  117. package/src/similar-functions.js +0 -279
  118. package/src/test-annotations.js +0 -323
  119. package/src/tool-defs.js +0 -793
  120. package/src/tools.js +0 -470
  121. package/src/type-checker.js +0 -188
  122. package/src/undocumented.js +0 -259
  123. package/src/workspace.js +0 -70
  124. /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
  125. /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
@@ -1,194 +0,0 @@
1
- /**
2
- * Database Analysis Tools
3
- *
4
- * Provides MCP tools for understanding code-database interactions:
5
- * - getDBSchema: Extract table/column structure from .sql files
6
- * - getTableUsage: Map functions to the tables they read/write
7
- * - getDBDeadTables: Find schema-defined tables/columns not referenced in code
8
- */
9
-
10
- import { parseProject } from './parser.js';
11
- import { buildGraph } from './graph-builder.js';
12
-
13
- /**
14
- * Get database schema from SQL files in the project.
15
- * Scans for .sql files and extracts CREATE TABLE definitions.
16
- * @param {string} dir - Directory to scan
17
- * @returns {Promise<Object>}
18
- */
19
- export async function getDBSchema(dir) {
20
- const parsed = await parseProject(dir);
21
- const tables = parsed.tables || [];
22
-
23
- return {
24
- tables: tables.map(t => ({
25
- name: t.name,
26
- columns: t.columns,
27
- file: t.file,
28
- line: t.line,
29
- })),
30
- totalTables: tables.length,
31
- totalColumns: tables.reduce((sum, t) => sum + t.columns.length, 0),
32
- };
33
- }
34
-
35
- /**
36
- * Show which functions read/write database tables.
37
- * Traces SQL queries in code to table references.
38
- * @param {string} dir - Directory to scan
39
- * @param {string} [tableName] - Optional: filter to specific table
40
- * @returns {Promise<Object>}
41
- */
42
- export async function getTableUsage(dir, tableName) {
43
- const parsed = await parseProject(dir);
44
- const graph = buildGraph(parsed);
45
-
46
- // Collect all table references from edges
47
- const tableMap = {};
48
-
49
- for (const [from, type, to] of graph.edges) {
50
- if (type !== 'R→' && type !== 'W→') continue;
51
-
52
- const table = to;
53
- if (tableName && table !== tableName) continue;
54
-
55
- if (!tableMap[table]) {
56
- tableMap[table] = { readers: [], writers: [] };
57
- }
58
-
59
- // Resolve the function/class name
60
- const fullName = graph.reverseLegend[from] || from;
61
- const node = graph.nodes[from];
62
- const entry = {
63
- name: fullName,
64
- file: node?.f || '?',
65
- };
66
-
67
- if (type === 'R→') {
68
- if (!tableMap[table].readers.some(r => r.name === fullName)) {
69
- tableMap[table].readers.push(entry);
70
- }
71
- } else {
72
- if (!tableMap[table].writers.some(w => w.name === fullName)) {
73
- tableMap[table].writers.push(entry);
74
- }
75
- }
76
- }
77
-
78
- // Format output
79
- const tables = Object.entries(tableMap)
80
- .map(([name, usage]) => ({
81
- table: name,
82
- readers: usage.readers,
83
- writers: usage.writers,
84
- totalReaders: usage.readers.length,
85
- totalWriters: usage.writers.length,
86
- }))
87
- .sort((a, b) => (b.totalReaders + b.totalWriters) - (a.totalReaders + a.totalWriters));
88
-
89
- return {
90
- tables,
91
- totalTables: tables.length,
92
- totalQueries: tables.reduce((sum, t) => sum + t.totalReaders + t.totalWriters, 0),
93
- };
94
- }
95
-
96
- /**
97
- * Find tables and columns defined in schema but never referenced in code.
98
- * @param {string} dir - Directory to scan
99
- * @returns {Promise<Object>}
100
- */
101
- export async function getDBDeadTables(dir) {
102
- const parsed = await parseProject(dir);
103
- const graph = buildGraph(parsed);
104
- const schemaTables = parsed.tables || [];
105
-
106
- // Collect all tables referenced in code (from R→/W→ edges)
107
- const referencedTables = new Set();
108
- for (const [, type, to] of graph.edges) {
109
- if (type === 'R→' || type === 'W→') {
110
- referencedTables.add(to);
111
- }
112
- }
113
-
114
- // Find dead tables (in schema but not in code)
115
- const deadTables = schemaTables
116
- .filter(t => !referencedTables.has(t.name))
117
- .map(t => ({
118
- name: t.name,
119
- file: t.file,
120
- line: t.line,
121
- columnCount: t.columns.length,
122
- }));
123
-
124
- // Collect all column names referenced in SQL strings (best-effort)
125
- // We extract column names from SELECT/WHERE clauses heuristically
126
- const referencedColumns = collectReferencedColumns(parsed);
127
-
128
- // Find dead columns (in schema but not referenced)
129
- const deadColumns = [];
130
- for (const table of schemaTables) {
131
- if (!referencedTables.has(table.name)) continue; // skip dead tables entirely
132
- for (const col of table.columns) {
133
- if (!referencedColumns.has(col.name)) {
134
- deadColumns.push({
135
- table: table.name,
136
- column: col.name,
137
- type: col.type,
138
- });
139
- }
140
- }
141
- }
142
-
143
- return {
144
- deadTables,
145
- deadColumns,
146
- stats: {
147
- totalSchemaTables: schemaTables.length,
148
- totalSchemaColumns: schemaTables.reduce((sum, t) => sum + t.columns.length, 0),
149
- deadTableCount: deadTables.length,
150
- deadColumnCount: deadColumns.length,
151
- },
152
- };
153
- }
154
-
155
- /**
156
- * Collect column names referenced in code SQL strings (best-effort).
157
- * Scans all string literals for column-like identifiers after SQL keywords.
158
- * @param {Object} parsed - ParseResult
159
- * @returns {Set<string>}
160
- */
161
- function collectReferencedColumns(parsed) {
162
- const columns = new Set();
163
-
164
- // Gather all dbReads/dbWrites context isn't enough for columns.
165
- // We need to scan the actual SQL strings.
166
- // For simplicity, we collect all identifiers that appear near SQL contexts
167
- // from functions/classes that have any DB interaction.
168
- for (const func of parsed.functions || []) {
169
- if (func.dbReads?.length || func.dbWrites?.length) {
170
- // Mark all reasonable identifiers from this function's SQL as "referenced"
171
- // This is a heuristic - we accept false negatives for safety
172
- for (const table of [...(func.dbReads || []), ...(func.dbWrites || [])]) {
173
- columns.add(table); // table name itself
174
- }
175
- }
176
- }
177
-
178
- for (const cls of parsed.classes || []) {
179
- if (cls.dbReads?.length || cls.dbWrites?.length) {
180
- for (const table of [...(cls.dbReads || []), ...(cls.dbWrites || [])]) {
181
- columns.add(table);
182
- }
183
- }
184
- }
185
-
186
- // Add common column names that are almost always used
187
- // (prevents noisy false-positive "dead columns")
188
- columns.add('id');
189
- columns.add('uuid');
190
- columns.add('created_at');
191
- columns.add('updated_at');
192
-
193
- return columns;
194
- }
package/src/dead-code.js DELETED
@@ -1,468 +0,0 @@
1
- /**
2
- * Dead Code Detector
3
- * Finds unused functions, classes, exports, variables, and imports
4
- */
5
-
6
- import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
7
- import { join, relative, resolve, dirname } from 'path';
8
- import { parse } from '../vendor/acorn.mjs';
9
- import * as walk from '../vendor/walk.mjs';
10
- import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
11
-
12
- /**
13
- * @typedef {Object} DeadCodeItem
14
- * @property {string} name
15
- * @property {string} type - 'function' | 'class' | 'export' | 'variable' | 'import'
16
- * @property {string} file
17
- * @property {number} line
18
- * @property {string} reason
19
- */
20
-
21
- /**
22
- * Find all JS files
23
- * @param {string} dir
24
- * @param {string} rootDir
25
- * @returns {string[]}
26
- */
27
- function findJSFiles(dir, rootDir = dir) {
28
- if (dir === rootDir) parseGitignore(rootDir);
29
- const files = [];
30
-
31
- try {
32
- for (const entry of readdirSync(dir)) {
33
- const fullPath = join(dir, entry);
34
- const relativePath = relative(rootDir, fullPath);
35
- const stat = statSync(fullPath);
36
-
37
- if (stat.isDirectory()) {
38
- if (!shouldExcludeDir(entry, relativePath)) {
39
- files.push(...findJSFiles(fullPath, rootDir));
40
- }
41
- } else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
42
- if (!shouldExcludeFile(entry, relativePath)) {
43
- files.push(fullPath);
44
- }
45
- } else if (entry.endsWith('.css.js') || entry.endsWith('.tpl.js')) {
46
- if (!shouldExcludeFile(entry, relativePath)) {
47
- files.push(fullPath);
48
- }
49
- }
50
- }
51
- } catch (e) { }
52
-
53
- return files;
54
- }
55
-
56
- /**
57
- * Find project root by walking up from dir to find package.json
58
- * @param {string} dir
59
- * @returns {string}
60
- */
61
- function findProjectRoot(dir) {
62
- let current = resolve(dir);
63
- while (current !== dirname(current)) {
64
- if (existsSync(join(current, 'package.json'))) return current;
65
- current = dirname(current);
66
- }
67
- return resolve(dir);
68
- }
69
-
70
- /**
71
- * @typedef {Object} ImportInfo
72
- * @property {string} name - imported name
73
- * @property {string} source - import source path
74
- */
75
-
76
- /**
77
- * @typedef {Object} ExportInfo
78
- * @property {string} name - exported name
79
- * @property {number} line - line number
80
- */
81
-
82
- /**
83
- * Parse file and extract definitions, calls, exports, and imports
84
- * @param {string} code
85
- * @returns {{definitions: Set<string>, calls: Set<string>, exports: Set<string>, imports: ImportInfo[], namedExports: ExportInfo[]}}
86
- */
87
- function analyzeFile(code) {
88
- const definitions = new Set();
89
- const calls = new Set();
90
- const exports = new Set();
91
- /** @type {ImportInfo[]} */
92
- const imports = [];
93
- /** @type {ExportInfo[]} */
94
- const namedExports = [];
95
-
96
- let ast;
97
- try {
98
- ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
99
- } catch (e) {
100
- return { definitions, calls, exports, imports, namedExports };
101
- }
102
-
103
- walk.simple(ast, {
104
- FunctionDeclaration(node) {
105
- if (node.id) {
106
- definitions.add(node.id.name);
107
- }
108
- },
109
-
110
- ClassDeclaration(node) {
111
- if (node.id) {
112
- definitions.add(node.id.name);
113
- }
114
- },
115
-
116
- CallExpression(node) {
117
- if (node.callee.type === 'Identifier') {
118
- calls.add(node.callee.name);
119
- } else if (node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier') {
120
- calls.add(node.callee.object.name);
121
- }
122
- // Track function references passed as arguments: .map(funcName)
123
- for (const arg of node.arguments) {
124
- if (arg.type === 'Identifier') {
125
- calls.add(arg.name);
126
- }
127
- }
128
- },
129
-
130
- NewExpression(node) {
131
- if (node.callee.type === 'Identifier') {
132
- calls.add(node.callee.name);
133
- }
134
- },
135
-
136
- ImportDeclaration(node) {
137
- const source = node.source.value;
138
- for (const spec of node.specifiers) {
139
- if (spec.type === 'ImportSpecifier') {
140
- imports.push({ name: spec.imported.name, source });
141
- } else if (spec.type === 'ImportDefaultSpecifier') {
142
- imports.push({ name: 'default', source });
143
- }
144
- }
145
- },
146
-
147
- ExportNamedDeclaration(node) {
148
- if (node.declaration) {
149
- if (node.declaration.id) {
150
- const name = node.declaration.id.name;
151
- exports.add(name);
152
- namedExports.push({ name, line: node.loc.start.line });
153
- } else if (node.declaration.declarations) {
154
- for (const decl of node.declaration.declarations) {
155
- if (decl.id.type === 'Identifier') {
156
- const name = decl.id.name;
157
- exports.add(name);
158
- namedExports.push({ name, line: node.loc.start.line });
159
- }
160
- }
161
- }
162
- }
163
- if (node.specifiers) {
164
- for (const spec of node.specifiers) {
165
- const name = spec.exported.name;
166
- exports.add(name);
167
- namedExports.push({ name, line: node.loc.start.line });
168
- }
169
- }
170
- },
171
-
172
- ExportDefaultDeclaration(node) {
173
- if (node.declaration?.id) {
174
- exports.add(node.declaration.id.name);
175
- }
176
- },
177
- });
178
-
179
- return { definitions, calls, exports, imports, namedExports };
180
- }
181
-
182
- /**
183
- * Analyze file for unused local variables and imports
184
- * @param {string} code
185
- * @returns {{unusedVars: Array<{name: string, line: number}>, unusedImports: Array<{name: string, local: string, source: string, line: number}>}}
186
- */
187
- function analyzeFileLocals(code) {
188
- const unusedVars = [];
189
- const unusedImports = [];
190
-
191
- let ast;
192
- try {
193
- ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
194
- } catch (e) {
195
- return { unusedVars, unusedImports };
196
- }
197
-
198
- // Collect all identifier references (non-declaration sites)
199
- const refs = new Set();
200
- // Collect variable declarations: { name, line, isExported }
201
- const varDecls = [];
202
- // Collect import specifiers: { name, local, source, line }
203
- const importDecls = [];
204
- // Track names at declaration sites to exclude
205
- const declSites = new Set();
206
-
207
- // First pass: collect declarations
208
- walk.simple(ast, {
209
- VariableDeclaration(node) {
210
- const isExported = node.parent?.type === 'ExportNamedDeclaration';
211
- for (const decl of node.declarations) {
212
- if (decl.id.type === 'Identifier') {
213
- varDecls.push({ name: decl.id.name, line: decl.loc.start.line, isExported });
214
- declSites.add(decl.id);
215
- }
216
- }
217
- },
218
- ImportDeclaration(node) {
219
- for (const spec of node.specifiers) {
220
- const local = spec.local.name;
221
- const imported = spec.type === 'ImportSpecifier'
222
- ? spec.imported.name
223
- : spec.type === 'ImportDefaultSpecifier'
224
- ? 'default' : '*';
225
- importDecls.push({
226
- name: imported,
227
- local,
228
- source: node.source.value,
229
- line: node.loc.start.line,
230
- });
231
- declSites.add(spec.local);
232
- }
233
- },
234
- });
235
-
236
- // Walk AST to find variable declaration sites more precisely
237
- // (the walk.simple above doesn't set parents, so we need ancestor walk)
238
- // Instead, use top-level statement analysis
239
- const topLevelExportedNames = new Set();
240
- for (const node of ast.body) {
241
- if (node.type === 'ExportNamedDeclaration' && node.declaration) {
242
- if (node.declaration.declarations) {
243
- for (const decl of node.declaration.declarations) {
244
- if (decl.id.type === 'Identifier') topLevelExportedNames.add(decl.id.name);
245
- }
246
- }
247
- if (node.declaration.id) topLevelExportedNames.add(node.declaration.id.name);
248
- }
249
- if (node.type === 'ExportNamedDeclaration' && node.specifiers) {
250
- for (const spec of node.specifiers) {
251
- topLevelExportedNames.add(spec.local.name);
252
- }
253
- }
254
- }
255
-
256
- // Collect all identifier references across the AST
257
- walk.simple(ast, {
258
- Identifier(node) {
259
- refs.add(node.name);
260
- },
261
- });
262
-
263
- // Find unused variables (declared but never referenced beyond declaration)
264
- // We count total references: if name appears only in declaration, it's unused
265
- for (const v of varDecls) {
266
- // Skip exported variables (handled by orphan exports)
267
- if (topLevelExportedNames.has(v.name)) continue;
268
- // Skip destructuring names and common patterns
269
- if (v.name.startsWith('_')) continue;
270
- // Check if name is referenced anywhere in the code beyond its declaration
271
- // Simple heuristic: count occurrences in the source
272
- const regex = new RegExp(`\\b${v.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
273
- const matches = code.match(regex);
274
- // If only 1 occurrence, it's the declaration itself
275
- if (matches && matches.length <= 1) {
276
- unusedVars.push({ name: v.name, line: v.line });
277
- }
278
- }
279
-
280
- // Find unused imports
281
- for (const imp of importDecls) {
282
- // Skip namespace imports
283
- if (imp.name === '*') continue;
284
- // Check if local name is referenced beyond the import declaration
285
- const regex = new RegExp(`\\b${imp.local.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
286
- const matches = code.match(regex);
287
- // If only 1 occurrence, it's the import declaration itself
288
- if (matches && matches.length <= 1) {
289
- unusedImports.push(imp);
290
- }
291
- }
292
-
293
- return { unusedVars, unusedImports };
294
- }
295
-
296
- /**
297
- * Get dead code items
298
- * @param {string} dir
299
- * @returns {Promise<{total: number, byType: Object, items: DeadCodeItem[]}>}
300
- */
301
- export async function getDeadCode(dir) {
302
- const resolvedDir = resolve(dir);
303
- const files = findJSFiles(dir);
304
- const items = [];
305
-
306
- // Collect all calls and exports across target directory
307
- const allCalls = new Set();
308
- const allExports = new Set();
309
- const fileData = [];
310
-
311
- // Track imports project-wide: key = "importedName@resolvedSourcePath", value = consumer files
312
- /** @type {Map<string, Set<string>>} */
313
- const importConsumers = new Map();
314
-
315
- // Scan entire project for import consumers (not just target dir)
316
- const projectRoot = findProjectRoot(dir);
317
- const projectFiles = findJSFiles(projectRoot);
318
-
319
- for (const file of projectFiles) {
320
- let code;
321
- try { code = readFileSync(file, 'utf-8'); } catch { continue; }
322
- const relPath = relative(resolvedDir, file);
323
- const { imports } = analyzeFile(code);
324
-
325
- for (const imp of imports) {
326
- if (!imp.source.startsWith('.')) continue;
327
- const fileDir = dirname(file);
328
- let resolvedSource = resolve(fileDir, imp.source);
329
- if (!resolvedSource.endsWith('.js')) resolvedSource += '.js';
330
- const relSource = relative(resolvedDir, resolvedSource);
331
- const key = `${imp.name}@${relSource}`;
332
- if (!importConsumers.has(key)) importConsumers.set(key, new Set());
333
- importConsumers.get(key).add(relPath);
334
- }
335
- }
336
-
337
- // Analyze target directory files for definitions, calls, exports
338
- for (const file of files) {
339
- const code = readFileSync(file, 'utf-8');
340
- const relPath = relative(resolvedDir, file);
341
- const { definitions, calls, exports, namedExports } = analyzeFile(code);
342
-
343
- for (const call of calls) allCalls.add(call);
344
- for (const exp of exports) allExports.add(exp);
345
-
346
- fileData.push({ file: relPath, code, definitions, calls, exports, namedExports });
347
- }
348
-
349
- // Find dead code (functions/classes)
350
- for (const { file, code, definitions, exports } of fileData) {
351
- // Skip test files and presentation files
352
- if (file.includes('.test.') || file.includes('/tests/')) continue;
353
- if (file.endsWith('.css.js') || file.endsWith('.tpl.js')) continue;
354
-
355
- let ast;
356
- try {
357
- ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
358
- } catch (e) {
359
- continue;
360
- }
361
-
362
- walk.simple(ast, {
363
- FunctionDeclaration(node) {
364
- if (!node.id) return;
365
- const name = node.id.name;
366
-
367
- // Skip if exported
368
- if (exports.has(name) || allExports.has(name)) return;
369
-
370
- // Skip if called anywhere
371
- if (allCalls.has(name)) return;
372
-
373
- // Skip private functions
374
- if (name.startsWith('_')) return;
375
-
376
- items.push({
377
- name,
378
- type: 'function',
379
- file,
380
- line: node.loc.start.line,
381
- reason: 'Never called',
382
- });
383
- },
384
-
385
- ClassDeclaration(node) {
386
- if (!node.id) return;
387
- const name = node.id.name;
388
-
389
- // Skip if exported
390
- if (exports.has(name) || allExports.has(name)) return;
391
-
392
- // Skip if used (new Class() or extended)
393
- if (allCalls.has(name)) return;
394
-
395
- items.push({
396
- name,
397
- type: 'class',
398
- file,
399
- line: node.loc.start.line,
400
- reason: 'Never instantiated',
401
- });
402
- },
403
- });
404
- }
405
-
406
- // Find orphan exports (named exports not imported by any other file)
407
- for (const { file, calls, namedExports } of fileData) {
408
- if (file.includes('.test.') || file.includes('/tests/')) continue;
409
-
410
- for (const exp of namedExports) {
411
- // Skip exports used as call targets within the same file (e.g. ClassName.reg())
412
- if (calls.has(exp.name)) continue;
413
- const key = `${exp.name}@${file}`;
414
- const consumers = importConsumers.get(key);
415
- if (!consumers || consumers.size === 0) {
416
- items.push({
417
- name: exp.name,
418
- type: 'export',
419
- file,
420
- line: exp.line,
421
- reason: 'Exported but never imported',
422
- });
423
- }
424
- }
425
- }
426
-
427
- // Find unused variables and imports per file
428
- for (const { file, code } of fileData) {
429
- if (file.includes('.test.') || file.includes('/tests/')) continue;
430
- if (file.endsWith('.css.js') || file.endsWith('.tpl.js')) continue;
431
-
432
- const { unusedVars, unusedImports } = analyzeFileLocals(code);
433
-
434
- for (const v of unusedVars) {
435
- items.push({
436
- name: v.name,
437
- type: 'variable',
438
- file,
439
- line: v.line,
440
- reason: 'Declared but never used',
441
- });
442
- }
443
-
444
- for (const imp of unusedImports) {
445
- items.push({
446
- name: imp.local,
447
- type: 'import',
448
- file,
449
- line: imp.line,
450
- reason: `Imported from '${imp.source}' but never used`,
451
- });
452
- }
453
- }
454
-
455
- const byType = {
456
- function: items.filter(i => i.type === 'function').length,
457
- class: items.filter(i => i.type === 'class').length,
458
- export: items.filter(i => i.type === 'export').length,
459
- variable: items.filter(i => i.type === 'variable').length,
460
- import: items.filter(i => i.type === 'import').length,
461
- };
462
-
463
- return {
464
- total: items.length,
465
- byType,
466
- items: items.slice(0, 50),
467
- };
468
- }