universal-ast-mapper 0.5.2
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/BLUEPRINT.md +230 -0
- package/README.md +465 -0
- package/dist/analysis.js +134 -0
- package/dist/callgraph.js +238 -0
- package/dist/cli.js +617 -0
- package/dist/config.js +53 -0
- package/dist/extractors/common.js +54 -0
- package/dist/extractors/go.js +212 -0
- package/dist/extractors/python.js +142 -0
- package/dist/extractors/typescript.js +320 -0
- package/dist/graph-analysis.js +243 -0
- package/dist/graph.js +118 -0
- package/dist/html.js +325 -0
- package/dist/index.js +762 -0
- package/dist/parser.js +84 -0
- package/dist/registry.js +40 -0
- package/dist/resolver.js +131 -0
- package/dist/search.js +68 -0
- package/dist/skeleton.js +106 -0
- package/dist/types.js +5 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# AST-MCP — Universal Code Skeleton & Dependency Graph
|
|
2
|
+
|
|
3
|
+
An **MCP server + CLI tool** that turns source code into structured, machine-readable skeletons and symbol-level dependency graphs — so AI agents can reason about large codebases without reading every file.
|
|
4
|
+
|
|
5
|
+
Built on [tree-sitter](https://tree-sitter.github.io/) WASM grammars. Zero regex guessing — real AST parsing.
|
|
6
|
+
|
|
7
|
+
**Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install && npm run build
|
|
15
|
+
|
|
16
|
+
# CLI
|
|
17
|
+
ast-map --help
|
|
18
|
+
ast-map langs
|
|
19
|
+
ast-map dead src/
|
|
20
|
+
ast-map validate src/
|
|
21
|
+
|
|
22
|
+
# Or without installing globally
|
|
23
|
+
node dist/cli.js dead src/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Two Ways to Use
|
|
29
|
+
|
|
30
|
+
| Mode | Entry Point | When to Use |
|
|
31
|
+
|------|-------------|-------------|
|
|
32
|
+
| **CLI** (`ast-map`) | `dist/cli.js` | Terminal, CI scripts, quick checks |
|
|
33
|
+
| **MCP Server** | `dist/index.js` | AI agents (Claude, Cursor, etc.) |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## MCP Setup — Claude Desktop
|
|
38
|
+
|
|
39
|
+
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
40
|
+
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"ast-mapper": {
|
|
46
|
+
"command": "node",
|
|
47
|
+
"args": ["C:\\path\\to\\AST-MCP\\dist\\index.js"],
|
|
48
|
+
"env": {
|
|
49
|
+
"AST_MAP_ROOT": "C:\\path\\to\\your\\project"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> `AST_MAP_ROOT` is the security boundary — the server only reads files inside this path.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## CLI Reference
|
|
61
|
+
|
|
62
|
+
All commands default to `cwd` as root. Override with `AST_MAP_ROOT=/path/to/project ast-map <cmd>`.
|
|
63
|
+
Add `--json` to any command for machine-readable output.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
ast-map langs
|
|
67
|
+
ast-map skeleton <path> [-d outline|full] [--html] [--combine] [-o dir]
|
|
68
|
+
ast-map symbol <file> <name> [-k kind] [--related]
|
|
69
|
+
ast-map imports <file>
|
|
70
|
+
ast-map graph <dir> [-o graph.json]
|
|
71
|
+
ast-map validate <path> [--max-lines N] [--max-imports N] [--max-exports N]
|
|
72
|
+
ast-map dead <dir>
|
|
73
|
+
ast-map cycles <dir>
|
|
74
|
+
ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
|
|
75
|
+
ast-map deps <file> [--scan <dir>]
|
|
76
|
+
ast-map top <dir> [-n 10]
|
|
77
|
+
ast-map impact <file> <symbol> [--scan <dir>]
|
|
78
|
+
ast-map calls <file> <fn> [--scan <dir>]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Examples
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# What does this file export?
|
|
85
|
+
ast-map skeleton src/lib/auth.ts
|
|
86
|
+
|
|
87
|
+
# Show source of validateSession + related types
|
|
88
|
+
ast-map symbol src/lib/auth.ts validateSession --related
|
|
89
|
+
|
|
90
|
+
# Find unused exports
|
|
91
|
+
ast-map dead src/
|
|
92
|
+
|
|
93
|
+
# Detect circular imports
|
|
94
|
+
ast-map cycles src/
|
|
95
|
+
|
|
96
|
+
# Check architecture + structural health
|
|
97
|
+
ast-map validate src/
|
|
98
|
+
ast-map validate src/ --max-lines 300 --max-imports 20
|
|
99
|
+
|
|
100
|
+
# Find all symbols named like "handler" across the project
|
|
101
|
+
ast-map search handler src/ --exported
|
|
102
|
+
|
|
103
|
+
# What does this file import / what imports it?
|
|
104
|
+
ast-map deps src/lib/auth.ts --scan src/
|
|
105
|
+
|
|
106
|
+
# Top 10 most-imported symbols (God Node detector)
|
|
107
|
+
ast-map top src/
|
|
108
|
+
|
|
109
|
+
# Blast radius of changing sanitize()
|
|
110
|
+
ast-map impact src/utils.ts sanitize --scan src/
|
|
111
|
+
|
|
112
|
+
# Full call graph for a function
|
|
113
|
+
ast-map calls src/graph.ts buildSymbolGraph --scan src/
|
|
114
|
+
|
|
115
|
+
# Build symbol graph, write to file (large projects)
|
|
116
|
+
ast-map graph src/ -o graph.json
|
|
117
|
+
|
|
118
|
+
# Machine-readable output
|
|
119
|
+
ast-map dead src/ --json | jq '.deadExports[] | select(.kind == "function")'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## MCP Tools Reference
|
|
125
|
+
|
|
126
|
+
### `list_supported_languages`
|
|
127
|
+
Returns all supported languages and their file extensions.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### `get_skeleton_json`
|
|
132
|
+
Parse a single source file → return normalized JSON skeleton (no HTML written).
|
|
133
|
+
Use when the AI needs file structure only.
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"schemaVersion": "1.1",
|
|
138
|
+
"file": "src/lib/auth.ts",
|
|
139
|
+
"language": "typescript",
|
|
140
|
+
"directives": ["use server"],
|
|
141
|
+
"imports": [
|
|
142
|
+
{ "symbol": "prisma", "from": "./prisma", "isDefault": true }
|
|
143
|
+
],
|
|
144
|
+
"symbols": [
|
|
145
|
+
{ "name": "validateSession", "kind": "function", "exported": true,
|
|
146
|
+
"range": { "startLine": 12, "endLine": 34 } }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Params:** `path`, `detail` (`"outline"` | `"full"`)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### `generate_skeleton`
|
|
156
|
+
Map a file **or directory** → compact JSON + self-contained HTML views.
|
|
157
|
+
|
|
158
|
+
**Params:** `path`, `detail`, `emitHtml` (default `true`), `combineHtml` (single `index.html` with sidebar + search), `outputDir`
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### `get_symbol_context`
|
|
163
|
+
Extract exact source lines of a named symbol. Token-efficient: a 300-line file → ~40 lines.
|
|
164
|
+
Use `includeRelated: true` to also pull related types referenced in the signature.
|
|
165
|
+
|
|
166
|
+
**Params:** `path`, `symbol`, `kind` (optional), `includeRelated`
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `resolve_imports`
|
|
171
|
+
Resolve every import in a file to its target symbol with kind, signature, and params.
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"resolved": [
|
|
176
|
+
{
|
|
177
|
+
"symbol": "validateSession", "from": "../../lib/auth",
|
|
178
|
+
"resolvedRel": "src/lib/auth.ts", "kind": "function",
|
|
179
|
+
"signature": "async function validateSession(token: string): Promise<Session>",
|
|
180
|
+
"params": "(token: string)",
|
|
181
|
+
"found": true, "importKind": "relative"
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Params:** `path`
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
### `build_symbol_graph`
|
|
192
|
+
Scan a directory → build a two-layer dependency graph.
|
|
193
|
+
|
|
194
|
+
- **Nodes:** `"file"` (one per source file) and `"symbol"` (one per function/class/type/const)
|
|
195
|
+
- **Edges:** `"contains"` (structural hierarchy) and `"imports"` (cross-file dependency)
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"stats": { "fileCount": 42, "symbolNodeCount": 380, "edgeCount": 712 },
|
|
200
|
+
"edges": [
|
|
201
|
+
{ "from": "src/app/route.ts", "to": "src/lib/auth.ts::validateSession", "edgeType": "imports" }
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Use `outputFile` to write the graph to disk for large projects.
|
|
207
|
+
|
|
208
|
+
**Params:** `path`, `detail`, `outputFile`
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### `find_dead_code`
|
|
213
|
+
Scan a directory → find exported symbols with zero incoming import edges.
|
|
214
|
+
|
|
215
|
+
Returns two confidence tiers:
|
|
216
|
+
- `"high"` — functions, classes, consts (very likely unused)
|
|
217
|
+
- `"low"` — interfaces, types, enums (may be used as type annotations only)
|
|
218
|
+
|
|
219
|
+
> Note: framework entry-points (Next.js pages, route handlers) are technically "dead" inside the graph — review before deleting.
|
|
220
|
+
|
|
221
|
+
**Params:** `path`, `detail`
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### `find_circular_deps`
|
|
226
|
+
Detect circular import chains (A → B → C → A) using DFS.
|
|
227
|
+
Each cycle is canonicalised to avoid duplicates.
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"cycles": [
|
|
232
|
+
{ "cycle": ["src/a.ts", "src/b.ts", "src/c.ts", "src/a.ts"], "length": 3 }
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Params:** `path`
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### `get_change_impact`
|
|
242
|
+
Given a file + symbol, reverse-traverse the import graph to compute **blast radius**.
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"targetNodeId": "src/lib/auth.ts::validateSession",
|
|
247
|
+
"direct": [{ "file": "src/app/login/page.tsx", "symbol": "validateSession" }],
|
|
248
|
+
"transitive": [{ "file": "src/middleware.ts" }],
|
|
249
|
+
"totalFiles": 5
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Params:** `path`, `symbol`, `scanDir`
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### `get_call_graph`
|
|
258
|
+
Parse a function body → extract every call expression, resolve callees via the import map, find reverse importers.
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"calls": [
|
|
263
|
+
{ "callee": "prisma.session.findUnique", "line": 15, "calleeFileRel": "src/lib/prisma.ts" },
|
|
264
|
+
{ "callee": "jwt.verify", "line": 20, "isExternal": true, "calleeFileRel": "jsonwebtoken" }
|
|
265
|
+
],
|
|
266
|
+
"calledBy": [
|
|
267
|
+
{ "file": "src/app/api/auth/route.ts" }
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Supports TypeScript, JavaScript, Python, Go.
|
|
273
|
+
Handles destructured aliases: `const { sign } = jwt` → `sign` correctly resolves to `jsonwebtoken`.
|
|
274
|
+
|
|
275
|
+
**Params:** `path`, `function`, `scanDir`
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
### `search_symbol`
|
|
280
|
+
Find symbols by name across all source files in a directory.
|
|
281
|
+
|
|
282
|
+
**Params:** `path`, `name`, `matchType` (`"contains"` | `"exact"` | `"regex"`), `kind`, `exportedOnly`
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
### `get_file_deps`
|
|
287
|
+
For a single file, show what it imports and what imports it (with symbol names).
|
|
288
|
+
More focused than `build_symbol_graph` — use for quick dependency lookup.
|
|
289
|
+
|
|
290
|
+
**Params:** `path`, `scanDir`
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### `validate_architecture`
|
|
295
|
+
Scan for architecture violations across two rule sets.
|
|
296
|
+
|
|
297
|
+
**Next.js App Router rules:**
|
|
298
|
+
- `client-server-boundary` — `"use client"` file importing a server-only module *(error)*
|
|
299
|
+
- `api-missing-try-catch` — API route handler with no try/catch *(warning)*
|
|
300
|
+
|
|
301
|
+
**General structural rules (any project):**
|
|
302
|
+
- `large-file` — file exceeds `maxLines` (default 500) *(warning)*
|
|
303
|
+
- `too-many-imports` — file has more than `maxImports` imports (default 15) *(warning)*
|
|
304
|
+
- `god-export` — file exports more than `maxExports` symbols (default 10) *(warning)*
|
|
305
|
+
|
|
306
|
+
Thresholds can be set per-call or in `.ast-map.config.json`.
|
|
307
|
+
|
|
308
|
+
**Params:** `path`, `maxLines`, `maxImports`, `maxExports`
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### `get_top_symbols`
|
|
313
|
+
Return the N most-imported symbols — your codebase's "God Nodes" where a breaking change has maximum blast radius.
|
|
314
|
+
|
|
315
|
+
**Params:** `path`, `limit` (default 10)
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Project Config — `.ast-map.config.json`
|
|
320
|
+
|
|
321
|
+
Place in your project root. All fields optional.
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"ignore": ["dist", "coverage", ".turbo"],
|
|
326
|
+
"maxFileBytes": 500000,
|
|
327
|
+
"outputDir": ".ast-map",
|
|
328
|
+
"rules": {
|
|
329
|
+
"large-file": { "maxLines": 400 },
|
|
330
|
+
"too-many-imports": { "maxImports": 20 },
|
|
331
|
+
"god-export": { "maxExports": 15 }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The config is read live — changes take effect on the next call without restarting the MCP server.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Power Prompts
|
|
341
|
+
|
|
342
|
+
### Full Architecture Audit
|
|
343
|
+
```
|
|
344
|
+
Use build_symbol_graph on src/, then:
|
|
345
|
+
1. get_top_symbols — find the 5 God Nodes
|
|
346
|
+
2. find_circular_deps — any cycles?
|
|
347
|
+
3. validate_architecture — architecture violations + structural warnings
|
|
348
|
+
4. For the worst issue, show source with get_symbol_context
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Safe Refactor Checklist
|
|
352
|
+
```
|
|
353
|
+
Before refactoring [functionName] in [file]:
|
|
354
|
+
1. get_change_impact — who depends on it?
|
|
355
|
+
2. get_call_graph — what does it call?
|
|
356
|
+
3. get_symbol_context with includeRelated=true — show me the full signature + types
|
|
357
|
+
4. Summarise what needs to change alongside the refactor
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Dead Code Cleanup
|
|
361
|
+
```
|
|
362
|
+
Run find_dead_code on src/.
|
|
363
|
+
Show high-confidence results grouped by file.
|
|
364
|
+
For each candidate, use get_change_impact to confirm it's truly unreachable,
|
|
365
|
+
then show the source with get_symbol_context so I can verify before deleting.
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Adding a Language
|
|
371
|
+
|
|
372
|
+
1. Pick a grammar from `tree-sitter-wasms` (~36 bundled grammars).
|
|
373
|
+
2. Write `src/extractors/<lang>.ts` — export `extract()` and `extractImports()`.
|
|
374
|
+
3. Add one entry to `src/registry.ts`.
|
|
375
|
+
|
|
376
|
+
No changes to the core pipeline or any MCP tool.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Schema Reference
|
|
381
|
+
|
|
382
|
+
### `SymbolNode`
|
|
383
|
+
```typescript
|
|
384
|
+
interface SymbolNode {
|
|
385
|
+
name: string
|
|
386
|
+
kind: "class" | "interface" | "struct" | "function" | "method"
|
|
387
|
+
| "type" | "enum" | "const" | "var" | "field"
|
|
388
|
+
visibility: "public" | "private"
|
|
389
|
+
exported?: boolean
|
|
390
|
+
signature?: string // full detail only
|
|
391
|
+
doc?: string // full detail only
|
|
392
|
+
range: { startLine: number; endLine: number }
|
|
393
|
+
children: SymbolNode[]
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### `ImportRef`
|
|
398
|
+
```typescript
|
|
399
|
+
interface ImportRef {
|
|
400
|
+
symbol: string // imported name, or "*"
|
|
401
|
+
from: string // module specifier as written in source
|
|
402
|
+
alias?: string // import { Foo as Bar } → "Bar"
|
|
403
|
+
isTypeOnly?: boolean
|
|
404
|
+
isNamespaceImport?: boolean
|
|
405
|
+
isDefault?: boolean
|
|
406
|
+
isSideEffect?: boolean
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### `SkeletonFile` (schema v1.1)
|
|
411
|
+
```typescript
|
|
412
|
+
interface SkeletonFile {
|
|
413
|
+
schemaVersion: "1.1"
|
|
414
|
+
file: string // relative path, forward-slashed
|
|
415
|
+
language: string
|
|
416
|
+
generatedAt: string
|
|
417
|
+
parser: { engine: "tree-sitter"; grammar: string }
|
|
418
|
+
symbolCount: number
|
|
419
|
+
directives?: string[] // e.g. ["use client"]
|
|
420
|
+
imports?: ImportRef[]
|
|
421
|
+
symbols: SymbolNode[]
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Project Layout
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
src/
|
|
431
|
+
├── index.ts — MCP server + all 14 tool registrations
|
|
432
|
+
├── cli.ts — ast-map CLI (13 commands)
|
|
433
|
+
├── types.ts — SkeletonFile, SymbolNode, ImportRef
|
|
434
|
+
├── config.ts — SkeletonOptions, resolveOptions(), loadProjectConfig()
|
|
435
|
+
├── registry.ts — language detection + extractor registry
|
|
436
|
+
├── parser.ts — tree-sitter WASM loader + AST node helpers
|
|
437
|
+
├── skeleton.ts — buildSkeleton(), collectSourceFiles() + parse cache
|
|
438
|
+
├── resolver.ts — resolveImportPath(), resolveFileImports()
|
|
439
|
+
├── graph.ts — buildSymbolGraph()
|
|
440
|
+
├── graph-analysis.ts — findDeadExports(), findCircularDeps(), getChangeImpact(),
|
|
441
|
+
│ getFileDeps(), getTopSymbols()
|
|
442
|
+
├── callgraph.ts — buildCallGraph() — AST-level call extraction
|
|
443
|
+
├── analysis.ts — findSymbol(), validate helpers, checkGeneralRules()
|
|
444
|
+
├── html.ts — renderHtml(), renderCombinedHtml()
|
|
445
|
+
├── search.ts — searchSymbols()
|
|
446
|
+
└── extractors/
|
|
447
|
+
├── common.ts — makeSymbol(), toOutline()
|
|
448
|
+
├── typescript.ts — TS/JS/TSX: symbols + imports + re-exports
|
|
449
|
+
├── python.ts — Python: symbols + relative import resolution
|
|
450
|
+
└── go.ts — Go: symbols + imports
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Changelog
|
|
456
|
+
|
|
457
|
+
| Version | What changed |
|
|
458
|
+
|---------|--------------|
|
|
459
|
+
| **0.5.2** | Iterative DFS in `findCircularDeps` (eliminates stack overflow on large codebases) · `build_symbol_graph` inline size guard (>2000 nodes → stats + warning) · integration test suite (`test/analysis.mjs`) |
|
|
460
|
+
| **0.5.1** | Re-export tracking (`export { X } from './foo'`, barrel files) · `export const` surfaced as symbols · `const X = class {}` support · Python relative import fix · parser instance cache |
|
|
461
|
+
| **0.5.0** | Call graph destructuring aliases · in-process parse cache · `.ast-map.config.json` · general validation rules (large-file, too-many-imports, god-export) |
|
|
462
|
+
| **0.4.0** | `search_symbol` · `get_file_deps` · `get_top_symbols` · dead code confidence tiers · 3 new CLI commands |
|
|
463
|
+
| **0.3.0** | `ast-map` CLI · `find_dead_code` · `find_circular_deps` · `get_change_impact` · `get_call_graph` |
|
|
464
|
+
| **0.2.0** | Import extraction · `resolve_imports` · `build_symbol_graph` |
|
|
465
|
+
| **0.1.0** | `get_skeleton_json` · `generate_skeleton` · `get_symbol_context` · `validate_architecture` |
|
package/dist/analysis.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
// ─── Symbol lookup ────────────────────────────────────────────────────────────
|
|
3
|
+
/** Recursively search for a symbol by name and optional kind. */
|
|
4
|
+
export function findSymbol(symbols, name, kind) {
|
|
5
|
+
for (const sym of symbols) {
|
|
6
|
+
if (sym.name === name && (!kind || sym.kind === kind))
|
|
7
|
+
return sym;
|
|
8
|
+
const found = findSymbol(sym.children, name, kind);
|
|
9
|
+
if (found)
|
|
10
|
+
return found;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Given a target symbol with a signature, find related type/interface/enum
|
|
16
|
+
* symbols referenced in that signature and return their source code blocks.
|
|
17
|
+
*/
|
|
18
|
+
export function findRelatedSymbols(symbols, target, sourceLines) {
|
|
19
|
+
if (!target.signature)
|
|
20
|
+
return [];
|
|
21
|
+
const seen = new Set([target.name]);
|
|
22
|
+
// PascalCase identifiers in the signature are likely type references
|
|
23
|
+
const typeRefs = [...target.signature.matchAll(/\b([A-Z][a-zA-Z0-9_]*)\b/g)]
|
|
24
|
+
.map((m) => m[1])
|
|
25
|
+
.filter((v) => !seen.has(v) && (seen.add(v), true));
|
|
26
|
+
const related = [];
|
|
27
|
+
for (const typeName of typeRefs) {
|
|
28
|
+
const sym = findSymbol(symbols, typeName);
|
|
29
|
+
if (sym && (sym.kind === "interface" || sym.kind === "type" || sym.kind === "enum")) {
|
|
30
|
+
const code = sourceLines.slice(sym.range.startLine - 1, sym.range.endLine).join("\n");
|
|
31
|
+
related.push({ name: sym.name, kind: sym.kind, range: sym.range, code });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return related;
|
|
35
|
+
}
|
|
36
|
+
// ─── Architecture validation ──────────────────────────────────────────────────
|
|
37
|
+
/** True if the first 500 chars of source contain the given directive literal. */
|
|
38
|
+
export function hasDirective(source, directive) {
|
|
39
|
+
const head = source.slice(0, 500);
|
|
40
|
+
return head.includes(`"${directive}"`) || head.includes(`'${directive}'`);
|
|
41
|
+
}
|
|
42
|
+
/** Patterns that flag server-only imports in a "use client" file. */
|
|
43
|
+
const SERVER_IMPORT_PATTERNS = [
|
|
44
|
+
{ pattern: /from\s+['"]server-only['"]/, label: "server-only" },
|
|
45
|
+
{ pattern: /from\s+['"][^'"]*\/prisma['"]/, label: "prisma client" },
|
|
46
|
+
{ pattern: /from\s+['"][^'"]*lib\/prisma['"]/, label: "lib/prisma" },
|
|
47
|
+
{ pattern: /from\s+['"]next\/headers['"]/, label: "next/headers" },
|
|
48
|
+
{ pattern: /from\s+['"]next\/cookies['"]/, label: "next/cookies" },
|
|
49
|
+
{ pattern: /from\s+['"][^'"]*lib\/auth['"]/, label: "lib/auth" },
|
|
50
|
+
{ pattern: /from\s+['"][^'"]*lib\/auditLog['"]/, label: "lib/auditLog" },
|
|
51
|
+
{ pattern: /from\s+['"][^'"]*lib\/apiAuth['"]/, label: "lib/apiAuth" },
|
|
52
|
+
];
|
|
53
|
+
/** Scan source lines for server-only imports (called on "use client" files). */
|
|
54
|
+
export function findServerImports(source) {
|
|
55
|
+
const lines = source.split("\n");
|
|
56
|
+
const violations = [];
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const line = lines[i];
|
|
59
|
+
for (const { pattern, label } of SERVER_IMPORT_PATTERNS) {
|
|
60
|
+
if (pattern.test(line)) {
|
|
61
|
+
const match = line.match(/from\s+['"]([^'"]+)['"]/);
|
|
62
|
+
violations.push({ module: match ? match[1] : line.trim(), label, line: i + 1 });
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return violations;
|
|
68
|
+
}
|
|
69
|
+
const HTTP_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
70
|
+
/** True if the relative path looks like a Next.js App Router API route file. */
|
|
71
|
+
export function isApiRoute(relPath) {
|
|
72
|
+
const norm = relPath.split(path.sep).join("/");
|
|
73
|
+
return /app\/api\/.+\/route\.(ts|js|tsx|jsx)$/.test(norm);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Find exported HTTP handler functions that have no try/catch.
|
|
77
|
+
* A simple but effective heuristic: look for the `try {` keyword in the body.
|
|
78
|
+
*/
|
|
79
|
+
export function findMissingTryCatch(symbols, sourceLines) {
|
|
80
|
+
const missing = [];
|
|
81
|
+
for (const sym of symbols) {
|
|
82
|
+
if (!HTTP_METHODS.has(sym.name) || sym.exported === false)
|
|
83
|
+
continue;
|
|
84
|
+
const bodyText = sourceLines.slice(sym.range.startLine - 1, sym.range.endLine).join("\n");
|
|
85
|
+
if (!/\btry\s*\{/.test(bodyText))
|
|
86
|
+
missing.push(sym);
|
|
87
|
+
}
|
|
88
|
+
return missing;
|
|
89
|
+
}
|
|
90
|
+
export const GENERAL_RULE_DEFAULTS = {
|
|
91
|
+
largeFileLines: 500,
|
|
92
|
+
tooManyImports: 15,
|
|
93
|
+
godExportCount: 10,
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Run general-purpose structural rules against a source file.
|
|
97
|
+
* Returns violations for any threshold exceeded.
|
|
98
|
+
*/
|
|
99
|
+
export function checkGeneralRules(fileRel, source, symbols, importCount, thresholds = GENERAL_RULE_DEFAULTS) {
|
|
100
|
+
const violations = [];
|
|
101
|
+
const lineCount = source.split("\n").length;
|
|
102
|
+
if (lineCount > thresholds.largeFileLines) {
|
|
103
|
+
violations.push({
|
|
104
|
+
file: fileRel,
|
|
105
|
+
rule: "large-file",
|
|
106
|
+
severity: "warning",
|
|
107
|
+
message: `File has ${lineCount} lines (threshold: ${thresholds.largeFileLines}) — consider splitting`,
|
|
108
|
+
value: lineCount,
|
|
109
|
+
threshold: thresholds.largeFileLines,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (importCount > thresholds.tooManyImports) {
|
|
113
|
+
violations.push({
|
|
114
|
+
file: fileRel,
|
|
115
|
+
rule: "too-many-imports",
|
|
116
|
+
severity: "warning",
|
|
117
|
+
message: `File has ${importCount} imports (threshold: ${thresholds.tooManyImports}) — high coupling`,
|
|
118
|
+
value: importCount,
|
|
119
|
+
threshold: thresholds.tooManyImports,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const exportedCount = symbols.filter((s) => s.exported).length;
|
|
123
|
+
if (exportedCount > thresholds.godExportCount) {
|
|
124
|
+
violations.push({
|
|
125
|
+
file: fileRel,
|
|
126
|
+
rule: "god-export",
|
|
127
|
+
severity: "warning",
|
|
128
|
+
message: `File exports ${exportedCount} symbols (threshold: ${thresholds.godExportCount}) — potential God File`,
|
|
129
|
+
value: exportedCount,
|
|
130
|
+
threshold: thresholds.godExportCount,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return violations;
|
|
134
|
+
}
|