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.
- package/README.md +171 -31
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/package.json +12 -8
- package/src/.project-graph-cache.json +1 -1
- package/src/analysis/analysis-cache.js +7 -0
- package/src/analysis/complexity.js +14 -0
- package/src/analysis/custom-rules.js +36 -0
- package/src/analysis/db-analysis.js +9 -0
- package/src/analysis/dead-code.js +19 -0
- package/src/analysis/full-analysis.js +18 -0
- package/src/analysis/jsdoc-checker.js +24 -0
- package/src/analysis/jsdoc-generator.js +10 -0
- package/src/analysis/large-files.js +11 -0
- package/src/analysis/outdated-patterns.js +12 -0
- package/src/analysis/similar-functions.js +16 -0
- package/src/analysis/test-annotations.js +21 -0
- package/src/analysis/type-checker.js +8 -0
- package/src/analysis/undocumented.js +14 -0
- package/src/cli/cli-handlers.js +4 -0
- package/src/cli/cli.js +5 -0
- package/src/compact/.project-graph-cache.json +1 -0
- package/src/compact/ai-context.js +7 -0
- package/src/compact/compact-migrate.js +17 -0
- package/src/compact/compact.js +18 -0
- package/src/compact/compress.js +14 -0
- package/src/compact/ctx-to-jsdoc.js +29 -0
- package/src/compact/doc-dialect.js +30 -0
- package/src/compact/expand.js +37 -0
- package/src/compact/framework-references.js +5 -0
- package/src/compact/instructions.js +3 -0
- package/src/compact/mode-config.js +8 -0
- package/src/compact/validate-pipeline.js +9 -0
- package/src/core/event-bus.js +9 -0
- package/src/core/filters.js +14 -0
- package/src/core/graph-builder.js +12 -0
- package/src/core/parser.js +31 -0
- package/src/core/workspace.js +8 -0
- package/src/lang/lang-go.js +17 -0
- package/src/lang/lang-python.js +12 -0
- package/src/lang/lang-sql.js +23 -0
- package/src/lang/lang-typescript.js +9 -0
- package/src/lang/lang-utils.js +4 -0
- package/src/mcp/mcp-server.js +17 -0
- package/src/mcp/tool-defs.js +3 -0
- package/src/mcp/tools.js +25 -0
- package/src/network/backend-lifecycle.js +19 -0
- package/src/network/backend.js +5 -0
- package/src/network/local-gateway.js +23 -0
- package/src/network/mdns.js +13 -0
- package/src/network/server.js +10 -0
- package/src/network/web-server.js +34 -0
- package/web/.project-graph-cache.json +1 -0
- package/web/app.js +17 -0
- package/web/components/code-block.js +3 -0
- package/web/components/quick-open.js +5 -0
- package/web/dashboard-state.js +3 -0
- package/web/dashboard.html +27 -0
- package/web/dashboard.js +8 -0
- package/web/highlight.js +13 -0
- package/web/index.html +35 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +4 -0
- package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
- package/web/panels/EventItem/EventItem.css.js +1 -0
- package/web/panels/EventItem/EventItem.js +4 -0
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.js +5 -0
- package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +4 -0
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +5 -0
- package/web/panels/ctx-panel.js +4 -0
- package/web/panels/dep-graph.js +6 -0
- package/web/panels/file-tree.js +188 -0
- package/web/panels/health-panel.js +3 -0
- package/web/panels/live-monitor.js +3 -0
- package/web/state.js +17 -0
- package/web/style.css +157 -0
- package/references/symbiote-3x.md +0 -834
- package/src/ai-context.js +0 -113
- package/src/analysis-cache.js +0 -155
- package/src/cli-handlers.js +0 -271
- package/src/cli.js +0 -95
- package/src/compact.js +0 -207
- package/src/complexity.js +0 -237
- package/src/compress.js +0 -319
- package/src/ctx-to-jsdoc.js +0 -514
- package/src/custom-rules.js +0 -584
- package/src/db-analysis.js +0 -194
- package/src/dead-code.js +0 -468
- package/src/doc-dialect.js +0 -716
- package/src/filters.js +0 -227
- package/src/framework-references.js +0 -177
- package/src/full-analysis.js +0 -470
- package/src/graph-builder.js +0 -299
- package/src/instructions.js +0 -73
- package/src/jsdoc-checker.js +0 -351
- package/src/jsdoc-generator.js +0 -203
- package/src/lang-go.js +0 -285
- package/src/lang-python.js +0 -197
- package/src/lang-sql.js +0 -309
- package/src/lang-typescript.js +0 -190
- package/src/lang-utils.js +0 -124
- package/src/large-files.js +0 -163
- package/src/mcp-server.js +0 -675
- package/src/mode-config.js +0 -127
- package/src/outdated-patterns.js +0 -296
- package/src/parser.js +0 -662
- package/src/server.js +0 -28
- package/src/similar-functions.js +0 -279
- package/src/test-annotations.js +0 -323
- package/src/tool-defs.js +0 -793
- package/src/tools.js +0 -470
- package/src/type-checker.js +0 -188
- package/src/undocumented.js +0 -259
- package/src/workspace.js +0 -70
- /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
- /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
package/src/db-analysis.js
DELETED
|
@@ -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
|
-
}
|