project-graph-mcp 1.2.4 → 1.3.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/AGENT_ROLE.md +19 -0
- package/package.json +1 -1
- package/src/db-analysis.js +194 -0
- package/src/full-analysis.js +16 -1
- package/src/graph-builder.js +32 -2
- package/src/lang-sql.js +309 -0
- package/src/mcp-server.js +28 -0
- package/src/parser.js +143 -23
- package/src/tool-defs.js +48 -0
- package/src/.project-graph-cache.json +0 -1
package/AGENT_ROLE.md
CHANGED
|
@@ -52,6 +52,25 @@ After code changes, you MUST verify UI with this flow:
|
|
|
52
52
|
async onDeletePersona() { ... }
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## 🗄️ Database Analysis
|
|
56
|
+
| Tool | Purpose |
|
|
57
|
+
|------|---------|
|
|
58
|
+
| `get_db_schema` | Extract tables, columns, types from .sql files |
|
|
59
|
+
| `get_table_usage` | Show which functions read/write each table |
|
|
60
|
+
| `get_db_dead_tables` | Find schema tables/columns never referenced in code |
|
|
61
|
+
|
|
62
|
+
### How It Works
|
|
63
|
+
The graph automatically detects SQL queries in your code:
|
|
64
|
+
- **Tagged templates**: `` sql`SELECT * FROM users` ``
|
|
65
|
+
- **DB client calls**: `.query()`, `.execute()`, `.raw()`, `.exec()`, `.queryFile()`, `.one()`, `.none()`, `.many()`, `.any()`, `.oneOrNone()`, `.manyOrNone()`, `.result()`
|
|
66
|
+
- **String literals**: SQL-anchored strings (`SELECT`, `INSERT`, `UPDATE`, `DELETE`, `WITH`)
|
|
67
|
+
- **Schema files**: `CREATE TABLE` statements from `.sql` files
|
|
68
|
+
|
|
69
|
+
### Limitations
|
|
70
|
+
- Regex-based (~80% accuracy). Dynamic SQL (string concatenation) may be missed.
|
|
71
|
+
- Column-level dead code detection is best-effort.
|
|
72
|
+
- ORM-specific patterns (Prisma, Sequelize, Knex query builder) are not yet supported.
|
|
73
|
+
|
|
55
74
|
## 🔍 Code Quality Analysis
|
|
56
75
|
| Tool | Purpose |
|
|
57
76
|
|------|---------|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-graph-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for AI agents — multi-language project graph (JS, TS, Python, Go), code quality analysis, and framework-specific lint rules. Zero dependencies.",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -0,0 +1,194 @@
|
|
|
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/full-analysis.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getSimilarFunctions } from './similar-functions.js';
|
|
|
9
9
|
import { getComplexity } from './complexity.js';
|
|
10
10
|
import { getLargeFiles } from './large-files.js';
|
|
11
11
|
import { getOutdatedPatterns } from './outdated-patterns.js';
|
|
12
|
+
import { getTableUsage } from './db-analysis.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @typedef {Object} AnalysisResult
|
|
@@ -102,13 +103,14 @@ export async function getFullAnalysis(dir, options = {}) {
|
|
|
102
103
|
const includeItems = options.includeItems || false;
|
|
103
104
|
|
|
104
105
|
// Run all analyses in parallel
|
|
105
|
-
const [deadCode, undocumented, similar, complexity, largeFiles, outdated] = await Promise.all([
|
|
106
|
+
const [deadCode, undocumented, similar, complexity, largeFiles, outdated, dbUsage] = await Promise.all([
|
|
106
107
|
getDeadCode(dir),
|
|
107
108
|
getUndocumentedSummary(dir, 'tests'),
|
|
108
109
|
getSimilarFunctions(dir, { threshold: 70 }),
|
|
109
110
|
getComplexity(dir, { minComplexity: 5 }),
|
|
110
111
|
getLargeFiles(dir),
|
|
111
112
|
getOutdatedPatterns(dir),
|
|
113
|
+
getTableUsage(dir).catch(() => ({ tables: [], totalTables: 0, totalQueries: 0 })),
|
|
112
114
|
]);
|
|
113
115
|
|
|
114
116
|
// Calculate overall health
|
|
@@ -155,5 +157,18 @@ export async function getFullAnalysis(dir, options = {}) {
|
|
|
155
157
|
overall,
|
|
156
158
|
};
|
|
157
159
|
|
|
160
|
+
// Add DB metrics if any SQL interactions found (non-scoring)
|
|
161
|
+
if (dbUsage.totalTables > 0) {
|
|
162
|
+
result.database = {
|
|
163
|
+
tablesUsed: dbUsage.totalTables,
|
|
164
|
+
totalQueries: dbUsage.totalQueries,
|
|
165
|
+
tables: dbUsage.tables.map(t => ({
|
|
166
|
+
name: t.table,
|
|
167
|
+
readers: t.totalReaders,
|
|
168
|
+
writers: t.totalWriters,
|
|
169
|
+
})),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
158
173
|
return result;
|
|
159
174
|
}
|
package/src/graph-builder.js
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* @property {number} v - version
|
|
21
21
|
* @property {Object<string, string>} legend - minified name → full name
|
|
22
22
|
* @property {Object<string, string>} reverseLegend - full name → minified
|
|
23
|
-
* @property {Object} stats - { files, classes, functions }
|
|
23
|
+
* @property {Object} stats - { files, classes, functions, tables }
|
|
24
24
|
* @property {Object<string, GraphNode>} nodes
|
|
25
|
-
* @property {Array<[string, string, string]>} edges - [from, type, to]
|
|
25
|
+
* @property {Array<[string, string, string]>} edges - [from, type, to] where type is →, R→, or W→
|
|
26
26
|
* @property {string[]} orphans
|
|
27
27
|
* @property {Object<string, string[]>} duplicates
|
|
28
28
|
* @property {string[]} files - list of parsed file paths
|
|
@@ -106,6 +106,7 @@ export function buildGraph(parsed) {
|
|
|
106
106
|
files: (parsed.files || []).length,
|
|
107
107
|
classes: classes.length,
|
|
108
108
|
functions: functions.length,
|
|
109
|
+
tables: (parsed.tables || []).length,
|
|
109
110
|
},
|
|
110
111
|
nodes: {},
|
|
111
112
|
edges: [],
|
|
@@ -153,6 +154,34 @@ export function buildGraph(parsed) {
|
|
|
153
154
|
e: func.exported,
|
|
154
155
|
f: func.file || undefined,
|
|
155
156
|
};
|
|
157
|
+
|
|
158
|
+
// Build DB edges from function SQL reads/writes
|
|
159
|
+
for (const table of func.dbReads || []) {
|
|
160
|
+
graph.edges.push([shortName, 'R→', table]);
|
|
161
|
+
}
|
|
162
|
+
for (const table of func.dbWrites || []) {
|
|
163
|
+
graph.edges.push([shortName, 'W→', table]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build DB edges from class SQL reads/writes
|
|
168
|
+
for (const cls of classes) {
|
|
169
|
+
const shortName = legend[cls.name];
|
|
170
|
+
for (const table of cls.dbReads || []) {
|
|
171
|
+
graph.edges.push([shortName, 'R→', table]);
|
|
172
|
+
}
|
|
173
|
+
for (const table of cls.dbWrites || []) {
|
|
174
|
+
graph.edges.push([shortName, 'W→', table]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Build table nodes from parsed SQL files
|
|
179
|
+
for (const table of parsed.tables || []) {
|
|
180
|
+
graph.nodes[table.name] = {
|
|
181
|
+
t: 'T',
|
|
182
|
+
cols: table.columns.map(c => c.name),
|
|
183
|
+
f: table.file || undefined,
|
|
184
|
+
};
|
|
156
185
|
}
|
|
157
186
|
|
|
158
187
|
// Detect orphans (nodes with no incoming edges)
|
|
@@ -215,6 +244,7 @@ export function createSkeleton(graph) {
|
|
|
215
244
|
if (node.f) entry.f = node.f;
|
|
216
245
|
nodes[short] = entry;
|
|
217
246
|
}
|
|
247
|
+
// Skip Table nodes (T) — they only appear in dedicated DB tools
|
|
218
248
|
}
|
|
219
249
|
|
|
220
250
|
// Build exported functions grouped by file: { "file.js": ["shortName1", ...] }
|
package/src/lang-sql.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Language Support for Project Graph
|
|
3
|
+
*
|
|
4
|
+
* Parses .sql files (schema dumps) and extracts SQL queries from code strings.
|
|
5
|
+
* Provides table/column extraction via regex (zero-dependency, ~80% accuracy).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} TableInfo
|
|
10
|
+
* @property {string} name - Table name
|
|
11
|
+
* @property {{name: string, type: string}[]} columns - Column definitions
|
|
12
|
+
* @property {string} file - Source file
|
|
13
|
+
* @property {number} line - Line number
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} SQLExtraction
|
|
18
|
+
* @property {string[]} reads - Tables read (SELECT/FROM/JOIN)
|
|
19
|
+
* @property {string[]} writes - Tables written (INSERT/UPDATE/DELETE)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* SQL keywords that should NOT be treated as table names.
|
|
24
|
+
* Used to filter false positives from regex extraction.
|
|
25
|
+
*/
|
|
26
|
+
const SQL_KEYWORDS = new Set([
|
|
27
|
+
'select', 'from', 'where', 'and', 'or', 'not', 'in', 'on',
|
|
28
|
+
'as', 'join', 'left', 'right', 'inner', 'outer', 'cross', 'full',
|
|
29
|
+
'group', 'order', 'by', 'having', 'limit', 'offset', 'union',
|
|
30
|
+
'all', 'distinct', 'case', 'when', 'then', 'else', 'end',
|
|
31
|
+
'null', 'true', 'false', 'is', 'between', 'like', 'ilike',
|
|
32
|
+
'exists', 'any', 'some', 'set', 'values', 'into', 'table',
|
|
33
|
+
'create', 'alter', 'drop', 'index', 'primary', 'key', 'foreign',
|
|
34
|
+
'references', 'constraint', 'default', 'check', 'unique',
|
|
35
|
+
'if', 'begin', 'commit', 'rollback', 'transaction',
|
|
36
|
+
'returning', 'conflict', 'nothing', 'do', 'update',
|
|
37
|
+
'cascade', 'restrict', 'lateral', 'each', 'row',
|
|
38
|
+
'with', 'recursive', 'only',
|
|
39
|
+
// PostgreSQL data types (prevent false positives)
|
|
40
|
+
'integer', 'int', 'bigint', 'smallint', 'serial', 'bigserial',
|
|
41
|
+
'text', 'varchar', 'char', 'character', 'boolean', 'bool',
|
|
42
|
+
'timestamp', 'timestamptz', 'date', 'time', 'timetz', 'interval',
|
|
43
|
+
'numeric', 'decimal', 'real', 'float', 'double',
|
|
44
|
+
'json', 'jsonb', 'uuid', 'bytea', 'inet', 'cidr', 'macaddr',
|
|
45
|
+
'array', 'point', 'line', 'box', 'circle', 'polygon', 'path',
|
|
46
|
+
// Common false positives
|
|
47
|
+
'count', 'sum', 'avg', 'min', 'max', 'coalesce', 'cast',
|
|
48
|
+
'extract', 'now', 'current_timestamp', 'current_date',
|
|
49
|
+
'generate_series', 'unnest', 'string_agg', 'array_agg',
|
|
50
|
+
'row_number', 'rank', 'dense_rank', 'over', 'partition',
|
|
51
|
+
'asc', 'desc', 'nulls', 'first', 'last', 'filter',
|
|
52
|
+
// PostgreSQL system/meta identifiers
|
|
53
|
+
'columns', 'rows', 'tables', 'schema', 'schemas',
|
|
54
|
+
'information_schema', 'pg_catalog', 'pg_tables', 'pg_class',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a string looks like a SQL query.
|
|
59
|
+
* Requires SQL keyword anchor at the start to minimize false positives.
|
|
60
|
+
* @param {string} str
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function isSQLString(str) {
|
|
64
|
+
if (!str || typeof str !== 'string') return false;
|
|
65
|
+
return /^\s*(SELECT|INSERT|UPDATE|DELETE|WITH|CREATE\s+TABLE)\b/i.test(str);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if identifier is a valid table name (not a SQL keyword or too short).
|
|
70
|
+
* @param {string} name
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
function isValidTableName(name) {
|
|
74
|
+
if (!name || name.length < 2) return false;
|
|
75
|
+
if (SQL_KEYWORDS.has(name.toLowerCase())) return false;
|
|
76
|
+
// Must look like a valid identifier
|
|
77
|
+
if (!/^[a-zA-Z_]\w*$/.test(name)) return false;
|
|
78
|
+
// Reject ALL-UPPERCASE or PascalCase (real PG tables are snake_case/lowercase)
|
|
79
|
+
if (/^[A-Z][A-Z_]*$/.test(name)) return false; // SKIP, API, etc.
|
|
80
|
+
if (/^[A-Z][a-z]/.test(name)) return false; // Job, Organization, etc.
|
|
81
|
+
// Reject PostgreSQL built-in functions/types
|
|
82
|
+
if (/^(pg_|jsonb_|array_|string_|regexp_)/.test(name)) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract table names from a SQL string.
|
|
88
|
+
* Returns lists of tables being read and written.
|
|
89
|
+
* @param {string} sql
|
|
90
|
+
* @returns {SQLExtraction}
|
|
91
|
+
*/
|
|
92
|
+
export function extractSQLFromString(sql) {
|
|
93
|
+
if (!sql || typeof sql !== 'string') {
|
|
94
|
+
return { reads: [], writes: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const reads = new Set();
|
|
98
|
+
const writes = new Set();
|
|
99
|
+
|
|
100
|
+
// Normalize: collapse whitespace, strip comments
|
|
101
|
+
const normalized = sql
|
|
102
|
+
.replace(/--[^\n]*/g, '') // single-line SQL comments
|
|
103
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // multi-line SQL comments
|
|
104
|
+
.replace(/\s+/g, ' ')
|
|
105
|
+
.trim();
|
|
106
|
+
|
|
107
|
+
// READ patterns
|
|
108
|
+
// FROM table [alias] — skip function calls: FROM func(...)
|
|
109
|
+
const fromRegex = /\bFROM\s+([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)?)/gi;
|
|
110
|
+
let match;
|
|
111
|
+
while ((match = fromRegex.exec(normalized)) !== null) {
|
|
112
|
+
// Skip if followed by ( — it's a function call, not a table
|
|
113
|
+
const afterMatch = normalized.slice(match.index + match[0].length).trimStart();
|
|
114
|
+
if (afterMatch.startsWith('(')) continue;
|
|
115
|
+
const name = match[1].split('.').pop(); // handle schema.table
|
|
116
|
+
if (isValidTableName(name)) reads.add(name);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// JOIN table [alias] — same check for safety
|
|
120
|
+
const joinRegex = /\bJOIN\s+([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)?)/gi;
|
|
121
|
+
while ((match = joinRegex.exec(normalized)) !== null) {
|
|
122
|
+
const afterMatch = normalized.slice(match.index + match[0].length).trimStart();
|
|
123
|
+
if (afterMatch.startsWith('(')) continue;
|
|
124
|
+
const name = match[1].split('.').pop();
|
|
125
|
+
if (isValidTableName(name)) reads.add(name);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// WRITE patterns
|
|
129
|
+
// INSERT INTO table
|
|
130
|
+
const insertRegex = /\bINSERT\s+INTO\s+([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)?)/gi;
|
|
131
|
+
while ((match = insertRegex.exec(normalized)) !== null) {
|
|
132
|
+
const name = match[1].split('.').pop();
|
|
133
|
+
if (isValidTableName(name)) writes.add(name);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// UPDATE table
|
|
137
|
+
const updateRegex = /\bUPDATE\s+([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)?)/gi;
|
|
138
|
+
while ((match = updateRegex.exec(normalized)) !== null) {
|
|
139
|
+
const name = match[1].split('.').pop();
|
|
140
|
+
if (isValidTableName(name)) writes.add(name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// DELETE FROM table
|
|
144
|
+
const deleteRegex = /\bDELETE\s+FROM\s+([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)?)/gi;
|
|
145
|
+
while ((match = deleteRegex.exec(normalized)) !== null) {
|
|
146
|
+
const name = match[1].split('.').pop();
|
|
147
|
+
if (isValidTableName(name)) writes.add(name);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Per peer review: remove DELETE primary target from reads to avoid double-counting.
|
|
151
|
+
// Only the primary mutation target is removed — subquery reads are preserved.
|
|
152
|
+
for (const w of writes) {
|
|
153
|
+
if (/\bDELETE\s+FROM\s+/i.test(normalized)) {
|
|
154
|
+
const deleteTargetMatch = normalized.match(/\bDELETE\s+FROM\s+([a-zA-Z_]\w*)/i);
|
|
155
|
+
if (deleteTargetMatch) {
|
|
156
|
+
const primaryTarget = deleteTargetMatch[1].split('.').pop();
|
|
157
|
+
if (w === primaryTarget) reads.delete(primaryTarget);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
reads: [...reads],
|
|
164
|
+
writes: [...writes],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Parse a .sql file (schema dump / migration).
|
|
170
|
+
* Extracts CREATE TABLE statements with column definitions.
|
|
171
|
+
* @param {string} code - SQL file content
|
|
172
|
+
* @param {string} filename - File path
|
|
173
|
+
* @returns {Object} ParseResult-compatible + tables[]
|
|
174
|
+
*/
|
|
175
|
+
export function parseSQL(code = '', filename = '') {
|
|
176
|
+
const result = {
|
|
177
|
+
file: filename,
|
|
178
|
+
classes: [],
|
|
179
|
+
functions: [],
|
|
180
|
+
imports: [],
|
|
181
|
+
exports: [],
|
|
182
|
+
tables: [],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (!code) return result;
|
|
186
|
+
|
|
187
|
+
// Match CREATE TABLE statements
|
|
188
|
+
// Handles: CREATE TABLE [IF NOT EXISTS] [schema.]name (columns...)
|
|
189
|
+
const createTableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:[a-zA-Z_]\w*\.)?([a-zA-Z_]\w*)\s*\(([\s\S]*?)\);/gi;
|
|
190
|
+
let match;
|
|
191
|
+
|
|
192
|
+
while ((match = createTableRegex.exec(code)) !== null) {
|
|
193
|
+
const tableName = match[1];
|
|
194
|
+
const columnsBlock = match[2];
|
|
195
|
+
const line = code.substring(0, match.index).split('\n').length;
|
|
196
|
+
|
|
197
|
+
const columns = parseColumns(columnsBlock);
|
|
198
|
+
|
|
199
|
+
result.tables.push({
|
|
200
|
+
name: tableName,
|
|
201
|
+
columns,
|
|
202
|
+
file: filename,
|
|
203
|
+
line,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Parse column definitions from CREATE TABLE body.
|
|
212
|
+
* @param {string} block - The content between parentheses
|
|
213
|
+
* @returns {{name: string, type: string}[]}
|
|
214
|
+
*/
|
|
215
|
+
function parseColumns(block) {
|
|
216
|
+
const columns = [];
|
|
217
|
+
// Split by commas, but respect parentheses (for types like NUMERIC(10,2))
|
|
218
|
+
const parts = splitByTopLevelComma(block);
|
|
219
|
+
|
|
220
|
+
for (const part of parts) {
|
|
221
|
+
const trimmed = part.trim();
|
|
222
|
+
// Skip constraints (PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK, CONSTRAINT)
|
|
223
|
+
if (/^\s*(PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT|EXCLUDE)\b/i.test(trimmed)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Match: column_name TYPE [constraints...]
|
|
228
|
+
const colMatch = trimmed.match(/^([a-zA-Z_]\w*)\s+([A-Za-z]\w*(?:\s*\([^)]*\))?(?:\s*\[\])?)/);
|
|
229
|
+
if (colMatch) {
|
|
230
|
+
const name = colMatch[1];
|
|
231
|
+
const type = colMatch[2].trim();
|
|
232
|
+
// Skip if "name" is a SQL keyword (misparse)
|
|
233
|
+
if (!SQL_KEYWORDS.has(name.toLowerCase())) {
|
|
234
|
+
columns.push({ name, type });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return columns;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Split string by commas, respecting parentheses depth.
|
|
244
|
+
* @param {string} str
|
|
245
|
+
* @returns {string[]}
|
|
246
|
+
*/
|
|
247
|
+
function splitByTopLevelComma(str) {
|
|
248
|
+
const parts = [];
|
|
249
|
+
let current = '';
|
|
250
|
+
let depth = 0;
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < str.length; i++) {
|
|
253
|
+
const ch = str[i];
|
|
254
|
+
if (ch === '(') depth++;
|
|
255
|
+
else if (ch === ')') depth--;
|
|
256
|
+
else if (ch === ',' && depth === 0) {
|
|
257
|
+
parts.push(current);
|
|
258
|
+
current = '';
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
current += ch;
|
|
262
|
+
}
|
|
263
|
+
if (current.trim()) parts.push(current);
|
|
264
|
+
|
|
265
|
+
return parts;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract SQL queries from raw source code (Python/Go pre-pass).
|
|
270
|
+
* Scans string literals and finds SQL patterns before stripStringsAndComments
|
|
271
|
+
* destroys the string contents.
|
|
272
|
+
*
|
|
273
|
+
* Returns aggregated reads/writes for the entire file.
|
|
274
|
+
* @param {string} code - Raw source code
|
|
275
|
+
* @returns {SQLExtraction}
|
|
276
|
+
*/
|
|
277
|
+
export function extractSQLFromCode(code) {
|
|
278
|
+
const allReads = new Set();
|
|
279
|
+
const allWrites = new Set();
|
|
280
|
+
|
|
281
|
+
if (!code) return { reads: [], writes: [] };
|
|
282
|
+
|
|
283
|
+
// Find all string literals that look like SQL
|
|
284
|
+
// Match: "...", '...', `...`, """...""", '''...'''
|
|
285
|
+
const stringPatterns = [
|
|
286
|
+
/"""([\s\S]*?)"""/g, // Python triple double-quote
|
|
287
|
+
/'''([\s\S]*?)'''/g, // Python triple single-quote
|
|
288
|
+
/`([\s\S]*?)`/g, // Go/JS backtick
|
|
289
|
+
/"((?:[^"\\]|\\.)*)"/g, // Double-quoted
|
|
290
|
+
/'((?:[^'\\]|\\.)*)'/g, // Single-quoted
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
for (const pattern of stringPatterns) {
|
|
294
|
+
let match;
|
|
295
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
296
|
+
const content = match[1];
|
|
297
|
+
if (isSQLString(content)) {
|
|
298
|
+
const extraction = extractSQLFromString(content);
|
|
299
|
+
extraction.reads.forEach(t => allReads.add(t));
|
|
300
|
+
extraction.writes.forEach(t => allWrites.add(t));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
reads: [...allReads],
|
|
307
|
+
writes: [...allWrites],
|
|
308
|
+
};
|
|
309
|
+
}
|
package/src/mcp-server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { getFullAnalysis } from './full-analysis.js';
|
|
|
25
25
|
import { getCustomRules, setCustomRule, checkCustomRules } from './custom-rules.js';
|
|
26
26
|
import { getFrameworkReference } from './framework-references.js';
|
|
27
27
|
import { setRoots, resolvePath } from './workspace.js';
|
|
28
|
+
import { getDBSchema, getTableUsage, getDBDeadTables } from './db-analysis.js';
|
|
28
29
|
|
|
29
30
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
|
|
@@ -108,6 +109,11 @@ const TOOL_HANDLERS = {
|
|
|
108
109
|
framework: args.framework,
|
|
109
110
|
path: args.path ? resolvePath(args.path) : undefined,
|
|
110
111
|
}),
|
|
112
|
+
|
|
113
|
+
// Database Analysis
|
|
114
|
+
get_db_schema: (args) => getDBSchema(resolvePath(args.path)),
|
|
115
|
+
get_table_usage: (args) => getTableUsage(resolvePath(args.path), args.table),
|
|
116
|
+
get_db_dead_tables: (args) => getDBDeadTables(resolvePath(args.path)),
|
|
111
117
|
};
|
|
112
118
|
|
|
113
119
|
/**
|
|
@@ -177,6 +183,28 @@ const RESPONSE_HINTS = {
|
|
|
177
183
|
get_pending_tests: () => [
|
|
178
184
|
'💡 Use mark_test_passed(testId) or mark_test_failed(testId, reason) to track progress.',
|
|
179
185
|
],
|
|
186
|
+
|
|
187
|
+
get_db_schema: (result) => {
|
|
188
|
+
const hints = [];
|
|
189
|
+
if (result.totalTables > 0) {
|
|
190
|
+
hints.push(`💡 Found ${result.totalTables} tables. Use get_table_usage() to see which code reads/writes them.`);
|
|
191
|
+
} else {
|
|
192
|
+
hints.push('💡 No .sql schema files found. Add schema.sql or migrations/*.sql to your project.');
|
|
193
|
+
}
|
|
194
|
+
return hints;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
get_table_usage: (result) => {
|
|
198
|
+
const hints = ['💡 Use get_db_dead_tables() to find tables defined in schema but never queried.'];
|
|
199
|
+
if (result.totalTables === 0) {
|
|
200
|
+
hints.push('💡 No SQL queries detected. This tool finds SQL in .query(), .execute(), sql`...` patterns.');
|
|
201
|
+
}
|
|
202
|
+
return hints;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
get_db_dead_tables: () => [
|
|
206
|
+
'💡 Dead columns detection is best-effort — verify before removing.',
|
|
207
|
+
],
|
|
180
208
|
};
|
|
181
209
|
|
|
182
210
|
/**
|
package/src/parser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AST Parser for JavaScript files using Acorn
|
|
3
|
-
* Extracts classes, functions, methods, properties, imports, and
|
|
3
|
+
* Extracts classes, functions, methods, properties, imports, calls, and SQL queries
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
@@ -11,9 +11,10 @@ import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.j
|
|
|
11
11
|
import { parseTypeScript } from './lang-typescript.js';
|
|
12
12
|
import { parsePython } from './lang-python.js';
|
|
13
13
|
import { parseGo } from './lang-go.js';
|
|
14
|
+
import { parseSQL, extractSQLFromString, isSQLString } from './lang-sql.js';
|
|
14
15
|
|
|
15
16
|
/** Supported source file extensions */
|
|
16
|
-
const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go'];
|
|
17
|
+
const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go', '.sql'];
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* @typedef {Object} ClassInfo
|
|
@@ -22,6 +23,8 @@ const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go'];
|
|
|
22
23
|
* @property {string[]} methods
|
|
23
24
|
* @property {string[]} properties
|
|
24
25
|
* @property {string[]} calls
|
|
26
|
+
* @property {string[]} [dbReads] - Tables read by SQL queries
|
|
27
|
+
* @property {string[]} [dbWrites] - Tables written by SQL queries
|
|
25
28
|
* @property {string} file
|
|
26
29
|
* @property {number} line
|
|
27
30
|
*/
|
|
@@ -31,6 +34,8 @@ const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go'];
|
|
|
31
34
|
* @property {string} name
|
|
32
35
|
* @property {boolean} exported
|
|
33
36
|
* @property {string[]} calls
|
|
37
|
+
* @property {string[]} [dbReads] - Tables read by SQL queries
|
|
38
|
+
* @property {string[]} [dbWrites] - Tables written by SQL queries
|
|
34
39
|
* @property {string} file
|
|
35
40
|
* @property {number} line
|
|
36
41
|
*/
|
|
@@ -120,6 +125,8 @@ export async function parseFile(code, filename) {
|
|
|
120
125
|
methods: [],
|
|
121
126
|
properties: [],
|
|
122
127
|
calls: [],
|
|
128
|
+
dbReads: [],
|
|
129
|
+
dbWrites: [],
|
|
123
130
|
file: filename,
|
|
124
131
|
line: node.loc.start.line,
|
|
125
132
|
};
|
|
@@ -129,8 +136,8 @@ export async function parseFile(code, filename) {
|
|
|
129
136
|
if (element.type === 'MethodDefinition' && element.key.name !== 'constructor') {
|
|
130
137
|
classInfo.methods.push(element.key.name);
|
|
131
138
|
|
|
132
|
-
// Extract calls from method body
|
|
133
|
-
|
|
139
|
+
// Extract calls and SQL from method body
|
|
140
|
+
extractCallsAndSQL(element.value.body, classInfo.calls, classInfo.dbReads, classInfo.dbWrites);
|
|
134
141
|
} else if (element.type === 'PropertyDefinition') {
|
|
135
142
|
const propName = element.key.name;
|
|
136
143
|
|
|
@@ -155,11 +162,13 @@ export async function parseFile(code, filename) {
|
|
|
155
162
|
name: node.id.name,
|
|
156
163
|
exported: false, // Will be updated later
|
|
157
164
|
calls: [],
|
|
165
|
+
dbReads: [],
|
|
166
|
+
dbWrites: [],
|
|
158
167
|
file: filename,
|
|
159
168
|
line: node.loc.start.line,
|
|
160
169
|
};
|
|
161
170
|
|
|
162
|
-
|
|
171
|
+
extractCallsAndSQL(node.body, funcInfo.calls, funcInfo.dbReads, funcInfo.dbWrites);
|
|
163
172
|
result.functions.push(funcInfo);
|
|
164
173
|
}
|
|
165
174
|
},
|
|
@@ -176,55 +185,159 @@ export async function parseFile(code, filename) {
|
|
|
176
185
|
return result;
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
/** DB client method names that accept SQL as first argument */
|
|
189
|
+
const DB_METHODS = new Set(['query', 'execute', 'raw', 'exec', 'queryFile', 'none', 'one', 'many', 'any', 'oneOrNone', 'manyOrNone', 'result']);
|
|
190
|
+
|
|
179
191
|
/**
|
|
180
|
-
* Extract method calls from AST node
|
|
192
|
+
* Extract method calls AND SQL queries from AST node in a single walk.
|
|
193
|
+
* Combines what was previously two separate walk.simple() calls.
|
|
181
194
|
* @param {Object} node
|
|
182
195
|
* @param {string[]} calls
|
|
196
|
+
* @param {string[]} [dbReads]
|
|
197
|
+
* @param {string[]} [dbWrites]
|
|
183
198
|
*/
|
|
184
|
-
function
|
|
199
|
+
function extractCallsAndSQL(node, calls, dbReads, dbWrites) {
|
|
185
200
|
if (!node) return;
|
|
186
201
|
|
|
187
202
|
walk.simple(node, {
|
|
188
203
|
CallExpression(callNode) {
|
|
189
204
|
const callee = callNode.callee;
|
|
190
205
|
|
|
206
|
+
// === Call extraction ===
|
|
191
207
|
if (callee.type === 'MemberExpression') {
|
|
192
|
-
// obj.method() or this.method()
|
|
193
208
|
const object = callee.object;
|
|
194
209
|
const property = callee.property;
|
|
195
210
|
|
|
196
211
|
if (property.type === 'Identifier') {
|
|
197
212
|
if (object.type === 'Identifier') {
|
|
198
|
-
// Class.method() or obj.method()
|
|
199
213
|
const call = `${object.name}.${property.name}`;
|
|
200
|
-
if (!calls.includes(call))
|
|
201
|
-
calls.push(call);
|
|
202
|
-
}
|
|
214
|
+
if (!calls.includes(call)) calls.push(call);
|
|
203
215
|
} else if (object.type === 'MemberExpression' && object.property.type === 'Identifier') {
|
|
204
|
-
// this.obj.method()
|
|
205
216
|
const call = `${object.property.name}.${property.name}`;
|
|
206
|
-
if (!calls.includes(call))
|
|
207
|
-
calls.push(call);
|
|
208
|
-
}
|
|
217
|
+
if (!calls.includes(call)) calls.push(call);
|
|
209
218
|
} else if (object.type === 'ThisExpression') {
|
|
210
|
-
// this.method() - internal call
|
|
211
219
|
const call = property.name;
|
|
212
|
-
if (!calls.includes(call))
|
|
213
|
-
calls.push(call);
|
|
214
|
-
}
|
|
220
|
+
if (!calls.includes(call)) calls.push(call);
|
|
215
221
|
}
|
|
216
222
|
}
|
|
217
223
|
} else if (callee.type === 'Identifier') {
|
|
218
|
-
// Direct function call: funcName()
|
|
219
224
|
const call = callee.name;
|
|
220
|
-
if (!calls.includes(call))
|
|
221
|
-
|
|
225
|
+
if (!calls.includes(call)) calls.push(call);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// === SQL extraction from DB client calls ===
|
|
229
|
+
if (dbReads && dbWrites) {
|
|
230
|
+
const methodName = getCallMethodName(callNode);
|
|
231
|
+
if (methodName && DB_METHODS.has(methodName) && callNode.arguments.length > 0) {
|
|
232
|
+
const sqlStr = extractStringValue(callNode.arguments[0]);
|
|
233
|
+
if (sqlStr && isSQLString(sqlStr)) {
|
|
234
|
+
const ext = extractSQLFromString(sqlStr);
|
|
235
|
+
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
236
|
+
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
// === SQL: Tagged templates ===
|
|
243
|
+
TaggedTemplateExpression(tagNode) {
|
|
244
|
+
if (!dbReads || !dbWrites) return;
|
|
245
|
+
const tagName = getTagName(tagNode.tag);
|
|
246
|
+
if (tagName && /sql/i.test(tagName)) {
|
|
247
|
+
const sqlStr = templateToString(tagNode.quasi);
|
|
248
|
+
if (sqlStr) {
|
|
249
|
+
const ext = extractSQLFromString(sqlStr);
|
|
250
|
+
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
251
|
+
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
222
252
|
}
|
|
223
253
|
}
|
|
224
254
|
},
|
|
255
|
+
|
|
256
|
+
// === SQL: Standalone template literals ===
|
|
257
|
+
TemplateLiteral(tplNode) {
|
|
258
|
+
if (!dbReads || !dbWrites) return;
|
|
259
|
+
const sqlStr = templateToString(tplNode);
|
|
260
|
+
if (sqlStr && isSQLString(sqlStr)) {
|
|
261
|
+
const ext = extractSQLFromString(sqlStr);
|
|
262
|
+
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
263
|
+
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// === SQL: String literals ===
|
|
268
|
+
Literal(litNode) {
|
|
269
|
+
if (!dbReads || !dbWrites) return;
|
|
270
|
+
if (typeof litNode.value === 'string' && isSQLString(litNode.value)) {
|
|
271
|
+
const ext = extractSQLFromString(litNode.value);
|
|
272
|
+
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
273
|
+
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
274
|
+
}
|
|
275
|
+
},
|
|
225
276
|
});
|
|
226
277
|
}
|
|
227
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Get tag name from tagged template expression.
|
|
281
|
+
* Handles: sql`...`, Prisma.sql`...`, db.sql`...`
|
|
282
|
+
* @param {Object} tag - AST node
|
|
283
|
+
* @returns {string|null}
|
|
284
|
+
*/
|
|
285
|
+
function getTagName(tag) {
|
|
286
|
+
if (tag.type === 'Identifier') return tag.name;
|
|
287
|
+
if (tag.type === 'MemberExpression' && tag.property.type === 'Identifier') {
|
|
288
|
+
return tag.property.name;
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get method name from a CallExpression callee.
|
|
295
|
+
* @param {Object} callNode
|
|
296
|
+
* @returns {string|null}
|
|
297
|
+
*/
|
|
298
|
+
function getCallMethodName(callNode) {
|
|
299
|
+
const callee = callNode.callee;
|
|
300
|
+
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
301
|
+
return callee.property.name;
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Extract string value from AST node (Literal or TemplateLiteral).
|
|
308
|
+
* For templates with expressions, substitutes $N placeholders.
|
|
309
|
+
* @param {Object} node
|
|
310
|
+
* @returns {string|null}
|
|
311
|
+
*/
|
|
312
|
+
function extractStringValue(node) {
|
|
313
|
+
if (!node) return null;
|
|
314
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
315
|
+
return node.value;
|
|
316
|
+
}
|
|
317
|
+
if (node.type === 'TemplateLiteral') {
|
|
318
|
+
return templateToString(node);
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Convert TemplateLiteral AST node to string.
|
|
325
|
+
* Expressions are replaced with $N placeholders.
|
|
326
|
+
* @param {Object} tplNode
|
|
327
|
+
* @returns {string}
|
|
328
|
+
*/
|
|
329
|
+
function templateToString(tplNode) {
|
|
330
|
+
if (!tplNode || !tplNode.quasis) return '';
|
|
331
|
+
let result = '';
|
|
332
|
+
for (let i = 0; i < tplNode.quasis.length; i++) {
|
|
333
|
+
result += tplNode.quasis[i].value.cooked || tplNode.quasis[i].value.raw || '';
|
|
334
|
+
if (i < tplNode.expressions?.length) {
|
|
335
|
+
result += '$' + (i + 1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
228
341
|
/**
|
|
229
342
|
* Parse all JS files in a directory
|
|
230
343
|
* @param {string} dir
|
|
@@ -237,6 +350,7 @@ export async function parseProject(dir) {
|
|
|
237
350
|
functions: [],
|
|
238
351
|
imports: [],
|
|
239
352
|
exports: [],
|
|
353
|
+
tables: [],
|
|
240
354
|
};
|
|
241
355
|
|
|
242
356
|
const resolvedDir = resolve(dir);
|
|
@@ -252,6 +366,9 @@ export async function parseProject(dir) {
|
|
|
252
366
|
result.functions.push(...parsed.functions);
|
|
253
367
|
result.imports.push(...parsed.imports);
|
|
254
368
|
result.exports.push(...parsed.exports);
|
|
369
|
+
if (parsed.tables?.length) {
|
|
370
|
+
result.tables.push(...parsed.tables);
|
|
371
|
+
}
|
|
255
372
|
}
|
|
256
373
|
|
|
257
374
|
// Dedupe imports/exports
|
|
@@ -268,6 +385,9 @@ export async function parseProject(dir) {
|
|
|
268
385
|
* @returns {Promise<ParseResult>}
|
|
269
386
|
*/
|
|
270
387
|
async function parseFileByExtension(code, filename) {
|
|
388
|
+
if (filename.endsWith('.sql')) {
|
|
389
|
+
return parseSQL(code, filename);
|
|
390
|
+
}
|
|
271
391
|
if (filename.endsWith('.py')) {
|
|
272
392
|
return parsePython(code, filename);
|
|
273
393
|
}
|
package/src/tool-defs.js
CHANGED
|
@@ -474,4 +474,52 @@ export const TOOLS = [
|
|
|
474
474
|
},
|
|
475
475
|
},
|
|
476
476
|
},
|
|
477
|
+
|
|
478
|
+
// Database Analysis
|
|
479
|
+
{
|
|
480
|
+
name: 'get_db_schema',
|
|
481
|
+
description: 'Get database schema from SQL files. Extracts tables, columns, and types from schema.sql or migration files in the project.',
|
|
482
|
+
inputSchema: {
|
|
483
|
+
type: 'object',
|
|
484
|
+
properties: {
|
|
485
|
+
path: {
|
|
486
|
+
type: 'string',
|
|
487
|
+
description: 'Path to scan (e.g., "." or "database/")',
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
required: ['path'],
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: 'get_table_usage',
|
|
495
|
+
description: 'Show which functions read/write database tables. Traces SQL queries in JS/TS/Python/Go code to table references.',
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: 'object',
|
|
498
|
+
properties: {
|
|
499
|
+
path: {
|
|
500
|
+
type: 'string',
|
|
501
|
+
description: 'Path to scan (e.g., "src/")',
|
|
502
|
+
},
|
|
503
|
+
table: {
|
|
504
|
+
type: 'string',
|
|
505
|
+
description: 'Optional: filter to a specific table name',
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
required: ['path'],
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: 'get_db_dead_tables',
|
|
513
|
+
description: 'Find tables and columns defined in schema but never referenced in code queries. Requires .sql schema files in the project.',
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
path: {
|
|
518
|
+
type: 'string',
|
|
519
|
+
description: 'Path to scan (e.g., ".")',
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
required: ['path'],
|
|
523
|
+
},
|
|
524
|
+
},
|
|
477
525
|
];
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":1,"path":"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src","mtimes":{"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/cli-handlers.js":1770656908396.6091,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/cli.js":1770402560025.795,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/complexity.js":1770659410318.8738,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/custom-rules.js":1772229894400.4248,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/dead-code.js":1771039661152.3376,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/filters.js":1773680641811.5923,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/framework-references.js":1772230204706.0527,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/full-analysis.js":1770398073907.7559,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/graph-builder.js":1773699507591.4272,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/instructions.js":1770399269998.5479,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/jsdoc-generator.js":1770659484514.0476,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/lang-go.js":1773698053155.2214,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/lang-python.js":1773699559544.998,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/lang-typescript.js":1773698053134.5566,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/lang-utils.js":1773697910333.1853,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/large-files.js":1770659415051.0068,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/mcp-server.js":1773842134375.5247,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/outdated-patterns.js":1770659431382.597,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/parser.js":1773683193338.3542,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/server.js":1774797876245.2288,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/similar-functions.js":1770659421339.757,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/test-annotations.js":1772223332771.1304,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/tool-defs.js":1773841810070.0337,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/tools.js":1773699507598.5554,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/undocumented.js":1770659436429.1372,"/Users/v.matiyasevich/Documents/GitHub/project-graph-mcp/src/workspace.js":1773699575195.6013},"graph":{"v":1,"legend":{"getArg":"gA","getPath":"gP","printHelp":"pH","runCLI":"CLI","findJSFiles":"JSF","calculateComplexity":"cC","getRating":"gR","analyzeFile":"aF","getComplexity":"gC","parseGraphignore":"pG","isGraphignored":"iG","loadRuleSets":"RS","saveRuleSet":"RS1","findFiles":"fF","isExcluded":"iE","isInStringOrComment":"ISO","isWithinContext":"WC","checkFileAgainstRule":"FAR","getCustomRules":"CR","setCustomRule":"CR1","deleteCustomRule":"CR2","detectProjectRuleSets":"PRS","checkCustomRules":"CR3","findProjectRoot":"PR","analyzeFileLocals":"FL","getDeadCode":"DC","getFilters":"gF","setFilters":"sF","addExcludes":"aE","removeExcludes":"rE","resetFilters":"rF","parseGitignore":"pG1","shouldExcludeDir":"ED","shouldExcludeFile":"EF","matchWildcard":"mW","matchGitignorePattern":"GP","fetchReference":"fR","listAvailable":"lA","getFrameworkReference":"FR","calculateHealthScore":"HS","getFullAnalysis":"FA","minifyLegend":"mL","createShortName":"SN","buildGraph":"bG","createSkeleton":"cS","getInstructions":"gI","generateJSDoc":"JSD","buildJSDoc":"JSD1","extractParamName":"PN","inferParamType":"PT","generateJSDocFor":"JSD2","parseGo":"pG2","extractImports":"eI","getBody":"gB","extractCalls":"eC","parsePython":"pP","parseTypeScript":"TS","extractParams":"eP","stripStringsAndComments":"SAC","getLargeFiles":"LF","createServer":"cS1","startStdioServer":"SS","analyzeFilePatterns":"FP","analyzePackageJson":"PJ","getOutdatedPatterns":"OP","parseFile":"pF","parseProject":"pP1","parseFileByExtension":"FBE","isSourceFile":"SF","extractSignatures":"eS","buildSignature":"bS","hashBodyStructure":"BS","calculateSimilarity":"cS2","getSimilarFunctions":"SF1","parseAnnotations":"pA","getAllFeatures":"AF","getPendingTests":"PT1","markTestPassed":"TP","markTestFailed":"TF","getTestSummary":"TS1","resetTestState":"TS2","generateMarkdown":"gM","saveDiskCache":"DC1","loadDiskCache":"DC2","getGraph":"gG","detectChanges":"dC","snapshotMtimes":"sM","getSkeleton":"gS","getFocusZone":"FZ","expand":"ex","deps":"de","usages":"us","extractMethod":"eM","getCallChain":"CC","invalidateCache":"iC","extractComments":"eC1","findJSDocBefore":"JSD3","checkMissing":"cM","getUndocumented":"gU","getUndocumentedSummary":"US","setRoots":"sR","getWorkspaceRoot":"WR","resolvePath":"rP"},"reverseLegend":{"gA":"getArg","gP":"getPath","pH":"printHelp","CLI":"runCLI","JSF":"findJSFiles","cC":"calculateComplexity","gR":"getRating","aF":"analyzeFile","gC":"getComplexity","pG":"parseGraphignore","iG":"isGraphignored","RS":"loadRuleSets","RS1":"saveRuleSet","fF":"findFiles","iE":"isExcluded","ISO":"isInStringOrComment","WC":"isWithinContext","FAR":"checkFileAgainstRule","CR":"getCustomRules","CR1":"setCustomRule","CR2":"deleteCustomRule","PRS":"detectProjectRuleSets","CR3":"checkCustomRules","PR":"findProjectRoot","FL":"analyzeFileLocals","DC":"getDeadCode","gF":"getFilters","sF":"setFilters","aE":"addExcludes","rE":"removeExcludes","rF":"resetFilters","pG1":"parseGitignore","ED":"shouldExcludeDir","EF":"shouldExcludeFile","mW":"matchWildcard","GP":"matchGitignorePattern","fR":"fetchReference","lA":"listAvailable","FR":"getFrameworkReference","HS":"calculateHealthScore","FA":"getFullAnalysis","mL":"minifyLegend","SN":"createShortName","bG":"buildGraph","cS":"createSkeleton","gI":"getInstructions","JSD":"generateJSDoc","JSD1":"buildJSDoc","PN":"extractParamName","PT":"inferParamType","JSD2":"generateJSDocFor","pG2":"parseGo","eI":"extractImports","gB":"getBody","eC":"extractCalls","pP":"parsePython","TS":"parseTypeScript","eP":"extractParams","SAC":"stripStringsAndComments","LF":"getLargeFiles","cS1":"createServer","SS":"startStdioServer","FP":"analyzeFilePatterns","PJ":"analyzePackageJson","OP":"getOutdatedPatterns","pF":"parseFile","pP1":"parseProject","FBE":"parseFileByExtension","SF":"isSourceFile","eS":"extractSignatures","bS":"buildSignature","BS":"hashBodyStructure","cS2":"calculateSimilarity","SF1":"getSimilarFunctions","pA":"parseAnnotations","AF":"getAllFeatures","PT1":"getPendingTests","TP":"markTestPassed","TF":"markTestFailed","TS1":"getTestSummary","TS2":"resetTestState","gM":"generateMarkdown","DC1":"saveDiskCache","DC2":"loadDiskCache","gG":"getGraph","dC":"detectChanges","sM":"snapshotMtimes","gS":"getSkeleton","FZ":"getFocusZone","ex":"expand","de":"deps","us":"usages","eM":"extractMethod","CC":"getCallChain","iC":"invalidateCache","eC1":"extractComments","JSD3":"findJSDocBefore","cM":"checkMissing","gU":"getUndocumented","US":"getUndocumentedSummary","sR":"setRoots","WR":"getWorkspaceRoot","rP":"resolvePath"},"stats":{"files":26,"classes":0,"functions":115},"nodes":{"gA":{"t":"F","e":false,"f":"cli-handlers.js"},"gP":{"t":"F","e":false,"f":"cli-handlers.js"},"pH":{"t":"F","e":true,"f":"cli.js"},"CLI":{"t":"F","e":true,"f":"cli.js"},"JSF":{"t":"F","e":false,"f":"undocumented.js"},"cC":{"t":"F","e":false,"f":"complexity.js"},"gR":{"t":"F","e":false,"f":"complexity.js"},"aF":{"t":"F","e":false,"f":"large-files.js"},"gC":{"t":"F","e":true,"f":"complexity.js"},"pG":{"t":"F","e":false,"f":"custom-rules.js"},"iG":{"t":"F","e":false,"f":"custom-rules.js"},"RS":{"t":"F","e":false,"f":"custom-rules.js"},"RS1":{"t":"F","e":false,"f":"custom-rules.js"},"fF":{"t":"F","e":false,"f":"custom-rules.js"},"iE":{"t":"F","e":false,"f":"custom-rules.js"},"ISO":{"t":"F","e":false,"f":"custom-rules.js"},"WC":{"t":"F","e":false,"f":"custom-rules.js"},"FAR":{"t":"F","e":false,"f":"custom-rules.js"},"CR":{"t":"F","e":true,"f":"custom-rules.js"},"CR1":{"t":"F","e":true,"f":"custom-rules.js"},"CR2":{"t":"F","e":true,"f":"custom-rules.js"},"PRS":{"t":"F","e":true,"f":"custom-rules.js"},"CR3":{"t":"F","e":true,"f":"custom-rules.js"},"PR":{"t":"F","e":false,"f":"dead-code.js"},"FL":{"t":"F","e":false,"f":"dead-code.js"},"DC":{"t":"F","e":true,"f":"dead-code.js"},"gF":{"t":"F","e":true,"f":"filters.js"},"sF":{"t":"F","e":true,"f":"filters.js"},"aE":{"t":"F","e":true,"f":"filters.js"},"rE":{"t":"F","e":true,"f":"filters.js"},"rF":{"t":"F","e":true,"f":"filters.js"},"pG1":{"t":"F","e":true,"f":"filters.js"},"ED":{"t":"F","e":true,"f":"filters.js"},"EF":{"t":"F","e":true,"f":"filters.js"},"mW":{"t":"F","e":false,"f":"filters.js"},"GP":{"t":"F","e":false,"f":"filters.js"},"fR":{"t":"F","e":false,"f":"framework-references.js"},"lA":{"t":"F","e":false,"f":"framework-references.js"},"FR":{"t":"F","e":true,"f":"framework-references.js"},"HS":{"t":"F","e":false,"f":"full-analysis.js"},"FA":{"t":"F","e":true,"f":"full-analysis.js"},"mL":{"t":"F","e":true,"f":"graph-builder.js"},"SN":{"t":"F","e":false,"f":"graph-builder.js"},"bG":{"t":"F","e":true,"f":"graph-builder.js"},"cS":{"t":"F","e":true,"f":"graph-builder.js"},"gI":{"t":"F","e":true,"f":"instructions.js"},"JSD":{"t":"F","e":true,"f":"jsdoc-generator.js"},"JSD1":{"t":"F","e":false,"f":"jsdoc-generator.js"},"PN":{"t":"F","e":false,"f":"similar-functions.js"},"PT":{"t":"F","e":false,"f":"jsdoc-generator.js"},"JSD2":{"t":"F","e":true,"f":"jsdoc-generator.js"},"pG2":{"t":"F","e":true,"f":"lang-go.js"},"eI":{"t":"F","e":false,"f":"lang-go.js"},"gB":{"t":"F","e":false,"f":"lang-go.js"},"eC":{"t":"F","e":false,"f":"parser.js"},"pP":{"t":"F","e":true,"f":"lang-python.js"},"TS":{"t":"F","e":true,"f":"lang-typescript.js"},"eP":{"t":"F","e":false,"f":"lang-typescript.js"},"SAC":{"t":"F","e":true,"f":"lang-utils.js"},"LF":{"t":"F","e":true,"f":"large-files.js"},"cS1":{"t":"F","e":true,"f":"mcp-server.js"},"SS":{"t":"F","e":true,"f":"mcp-server.js"},"FP":{"t":"F","e":false,"f":"outdated-patterns.js"},"PJ":{"t":"F","e":false,"f":"outdated-patterns.js"},"OP":{"t":"F","e":true,"f":"outdated-patterns.js"},"pF":{"t":"F","e":false,"f":"undocumented.js"},"pP1":{"t":"F","e":true,"f":"parser.js"},"FBE":{"t":"F","e":false,"f":"parser.js"},"SF":{"t":"F","e":false,"f":"parser.js"},"eS":{"t":"F","e":false,"f":"similar-functions.js"},"bS":{"t":"F","e":false,"f":"similar-functions.js"},"BS":{"t":"F","e":false,"f":"similar-functions.js"},"cS2":{"t":"F","e":false,"f":"similar-functions.js"},"SF1":{"t":"F","e":true,"f":"similar-functions.js"},"pA":{"t":"F","e":true,"f":"test-annotations.js"},"AF":{"t":"F","e":true,"f":"test-annotations.js"},"PT1":{"t":"F","e":true,"f":"test-annotations.js"},"TP":{"t":"F","e":true,"f":"test-annotations.js"},"TF":{"t":"F","e":true,"f":"test-annotations.js"},"TS1":{"t":"F","e":true,"f":"test-annotations.js"},"TS2":{"t":"F","e":true,"f":"test-annotations.js"},"gM":{"t":"F","e":true,"f":"test-annotations.js"},"DC1":{"t":"F","e":false,"f":"tools.js"},"DC2":{"t":"F","e":false,"f":"tools.js"},"gG":{"t":"F","e":false,"f":"tools.js"},"dC":{"t":"F","e":false,"f":"tools.js"},"sM":{"t":"F","e":false,"f":"tools.js"},"gS":{"t":"F","e":true,"f":"tools.js"},"FZ":{"t":"F","e":true,"f":"tools.js"},"ex":{"t":"F","e":true,"f":"tools.js"},"de":{"t":"F","e":true,"f":"tools.js"},"us":{"t":"F","e":true,"f":"tools.js"},"eM":{"t":"F","e":false,"f":"tools.js"},"CC":{"t":"F","e":true,"f":"tools.js"},"iC":{"t":"F","e":true,"f":"tools.js"},"eC1":{"t":"F","e":false,"f":"undocumented.js"},"JSD3":{"t":"F","e":false,"f":"undocumented.js"},"cM":{"t":"F","e":false,"f":"undocumented.js"},"gU":{"t":"F","e":true,"f":"undocumented.js"},"US":{"t":"F","e":true,"f":"undocumented.js"},"sR":{"t":"F","e":true,"f":"workspace.js"},"WR":{"t":"F","e":true,"f":"workspace.js"},"rP":{"t":"F","e":true,"f":"workspace.js"}},"edges":[],"orphans":["getArg","getPath","findJSFiles","calculateComplexity","getRating","analyzeFile","parseGraphignore","isGraphignored","loadRuleSets","saveRuleSet","findFiles","isExcluded","isInStringOrComment","isWithinContext","checkFileAgainstRule","findProjectRoot","analyzeFileLocals","matchWildcard","matchGitignorePattern","fetchReference","listAvailable","calculateHealthScore","createShortName","buildJSDoc","extractParamName","inferParamType","extractImports","getBody","extractCalls","extractParams","analyzeFilePatterns","analyzePackageJson","parseFile","parseFileByExtension","isSourceFile","extractSignatures","buildSignature","hashBodyStructure","calculateSimilarity","saveDiskCache","loadDiskCache","getGraph","detectChanges","snapshotMtimes","extractMethod","extractComments","findJSDocBefore","checkMissing"],"duplicates":{},"files":["cli-handlers.js","cli.js","complexity.js","custom-rules.js","dead-code.js","filters.js","framework-references.js","full-analysis.js","graph-builder.js","instructions.js","jsdoc-generator.js","lang-go.js","lang-python.js","lang-typescript.js","lang-utils.js","large-files.js","mcp-server.js","outdated-patterns.js","parser.js","server.js","similar-functions.js","test-annotations.js","tool-defs.js","tools.js","undocumented.js","workspace.js"]}}
|