ucn 3.1.7 → 3.2.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/.claude/skills/ucn/SKILL.md +113 -48
- package/README.md +154 -28
- package/cli/index.js +2 -94
- package/core/discovery.js +1 -2
- package/core/imports.js +5 -328
- package/core/project.js +129 -99
- package/package.json +1 -1
|
@@ -2,76 +2,141 @@
|
|
|
2
2
|
name: ucn
|
|
3
3
|
description: Universal Code Navigator - extracts specific functions and their relationships (callers, callees, dependencies) without reading entire files. Use when you need one function from a large file or need to understand what calls/is called by a function. Saves context in codebases 1000+ LOC. Skip for simple text search, tiny codebases, or unsupported languages (only JS/TS, Python, Go, Rust, Java).
|
|
4
4
|
allowed-tools: Bash(ucn *), Bash(npx ucn *)
|
|
5
|
-
argument-hint: "[command] [symbol-name]"
|
|
5
|
+
argument-hint: "[command] [symbol-name] [--flags]"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# UCN -
|
|
8
|
+
# UCN - Universal Code Navigator
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
UCN uses tree-sitter ASTs to understand code structure: functions, classes, callers, callees, imports, and dependencies. It works on JavaScript/TypeScript, Python, Go, Rust, and Java.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## When to Use vs Skip
|
|
13
13
|
|
|
14
|
-
**Use UCN when
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
14
|
+
**Use UCN when** the codebase is 1000+ LOC and you need:
|
|
15
|
+
- Who calls a function or what it calls
|
|
16
|
+
- What breaks if you change something
|
|
17
|
+
- One function from a large file (without reading the whole file)
|
|
18
|
+
- Unused code detection, dependency graphs
|
|
19
19
|
|
|
20
20
|
**Skip UCN when:**
|
|
21
|
-
- Simple text search (error messages
|
|
22
|
-
- Codebase
|
|
23
|
-
- Language not supported
|
|
21
|
+
- Simple text search (TODOs, error messages) — use grep
|
|
22
|
+
- Codebase < 500 LOC — just read the files
|
|
23
|
+
- Language not supported — use grep/read
|
|
24
|
+
- Finding files by name — use glob
|
|
24
25
|
|
|
25
|
-
##
|
|
26
|
+
## Command Format
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
ucn about <name> # Definition, callers, callees, tests, code
|
|
30
|
-
ucn context <name> # Callers + callees
|
|
31
|
-
ucn smart <name> # Function + dependencies inline
|
|
32
|
-
ucn impact <name> # All call sites by file
|
|
33
|
-
ucn trace <name> # Call tree
|
|
34
|
-
ucn example <name> # Best usage example with context
|
|
28
|
+
```
|
|
29
|
+
ucn [target] <command> [name] [--flags]
|
|
35
30
|
```
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
**Target** (optional, defaults to current directory):
|
|
33
|
+
- Omit or `.` — current project directory (most common)
|
|
34
|
+
- `path/to/file.js` — single file mode
|
|
35
|
+
- `path/to/dir` — specific project directory
|
|
36
|
+
- `"src/**/*.py"` — glob pattern (quote it)
|
|
37
|
+
|
|
38
|
+
**Examples of correct invocation:**
|
|
38
39
|
```bash
|
|
39
|
-
ucn
|
|
40
|
-
ucn
|
|
41
|
-
ucn toc
|
|
42
|
-
ucn
|
|
43
|
-
ucn
|
|
40
|
+
ucn about handleRequest
|
|
41
|
+
ucn fn parseConfig --file=utils
|
|
42
|
+
ucn toc
|
|
43
|
+
ucn src/api/routes.js fn handleRequest
|
|
44
|
+
ucn impact createUser --exclude=test
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
### Understand Code
|
|
50
|
+
| Command | Args | What it returns |
|
|
51
|
+
|---------|------|-----------------|
|
|
52
|
+
| `about <name>` | symbol name | Definition + callers + callees + tests + source code. **Start here.** |
|
|
53
|
+
| `context <name>` | symbol name | Callers and callees (numbered — use `expand N` to see code) |
|
|
54
|
+
| `smart <name>` | symbol name | Function source + all helper functions it calls, inline |
|
|
55
|
+
| `impact <name>` | symbol name | Every call site grouped by file. Use before modifying a function. |
|
|
56
|
+
| `trace <name>` | symbol name | Call tree (who calls who) at `--depth=N` (default 3) |
|
|
57
|
+
| `example <name>` | symbol name | Best real usage example with surrounding context |
|
|
58
|
+
|
|
59
|
+
### Find Code
|
|
60
|
+
| Command | Args | What it returns |
|
|
61
|
+
|---------|------|-----------------|
|
|
62
|
+
| `find <name>` | symbol name | Definitions ranked by usage count (top 5) |
|
|
63
|
+
| `usages <name>` | symbol name | All usages grouped: definitions, calls, imports, references |
|
|
64
|
+
| `toc` | none | Table of contents: all functions, classes, exports |
|
|
65
|
+
| `tests <name>` | symbol name | Test files and test functions for the given symbol |
|
|
66
|
+
| `search <text>` | search term | Text search (grep-like, but respects project ignores) |
|
|
67
|
+
| `deadcode` | none | Lists all functions/classes with zero callers |
|
|
68
|
+
|
|
69
|
+
### Extract Code
|
|
70
|
+
| Command | Args | What it returns |
|
|
71
|
+
|---------|------|-----------------|
|
|
72
|
+
| `fn <name>` | function name | Full function source code |
|
|
73
|
+
| `class <name>` | class name | Full class source code |
|
|
74
|
+
| `lines <range>` | e.g. `50-100` | Lines from file. In project mode requires `--file=<path>` |
|
|
75
|
+
| `expand <N>` | number | Source code for numbered item from last `context` output |
|
|
76
|
+
|
|
77
|
+
### Dependencies
|
|
78
|
+
| Command | Args | What it returns |
|
|
79
|
+
|---------|------|-----------------|
|
|
80
|
+
| `imports <file>` | relative path | What the file imports (modules, symbols) |
|
|
81
|
+
| `exporters <file>` | relative path | Which files import this file |
|
|
82
|
+
| `file-exports <file>` | relative path | What the file exports |
|
|
83
|
+
| `graph <file>` | relative path | Dependency tree at `--depth=N` |
|
|
84
|
+
|
|
85
|
+
### Refactoring
|
|
86
|
+
| Command | Args | What it returns |
|
|
87
|
+
|---------|------|-----------------|
|
|
88
|
+
| `verify <name>` | function name | Checks all call sites match the function's signature |
|
|
89
|
+
| `plan <name>` | function name | Preview refactoring with `--rename-to`, `--add-param`, `--remove-param` |
|
|
90
|
+
| `related <name>` | symbol name | Functions in same file or sharing dependencies |
|
|
91
|
+
|
|
92
|
+
## Key Flags
|
|
93
|
+
|
|
94
|
+
| Flag | Works with | Effect |
|
|
95
|
+
|------|-----------|--------|
|
|
96
|
+
| `--file=<pattern>` | any symbol command | Filter by file path when name is ambiguous (e.g., `--file=routes`) |
|
|
97
|
+
| `--exclude=a,b` | any | Exclude files matching patterns (e.g., `--exclude=test,mock`) |
|
|
98
|
+
| `--in=<path>` | any | Only search within path (e.g., `--in=src/core`) |
|
|
99
|
+
| `--include-tests` | any | Include test files in results (excluded by default) |
|
|
100
|
+
| `--include-methods` | `context`, `smart` | Include `obj.method()` calls (only direct calls shown by default) |
|
|
101
|
+
| `--depth=N` | `trace`, `graph`, `about`, `find` | Tree/expansion depth (default 3) |
|
|
102
|
+
| `--context=N` | `usages`, `search` | Lines of context around each match |
|
|
103
|
+
| `--json` | any | Machine-readable JSON output |
|
|
104
|
+
| `--code-only` | `search` | Exclude matches in comments and strings |
|
|
105
|
+
| `--with-types` | `smart`, `about` | Include type definitions |
|
|
106
|
+
| `--top=N` / `--all` | `find`, `usages` | Limit results to top N, or show all |
|
|
107
|
+
| `--no-cache` | any | Skip cached index (use after file changes) |
|
|
108
|
+
| `--clear-cache` | any | Delete cached index before running |
|
|
109
|
+
|
|
110
|
+
## Common Patterns
|
|
111
|
+
|
|
112
|
+
**Investigate a function (first stop):**
|
|
47
113
|
```bash
|
|
48
|
-
ucn
|
|
49
|
-
ucn fn <name> --file routes # Disambiguate by path
|
|
50
|
-
ucn class <name> # Extract class
|
|
114
|
+
ucn about handleRequest
|
|
51
115
|
```
|
|
52
116
|
|
|
53
|
-
|
|
117
|
+
**Before modifying a function:**
|
|
54
118
|
```bash
|
|
55
|
-
ucn
|
|
56
|
-
ucn
|
|
57
|
-
ucn file-exports <file> # What this file exports
|
|
58
|
-
ucn graph <file> # Dependency tree
|
|
119
|
+
ucn impact handleRequest # See all callers
|
|
120
|
+
ucn smart handleRequest # See function + its helpers
|
|
59
121
|
```
|
|
60
122
|
|
|
61
|
-
|
|
123
|
+
**Extract one function from a large file:**
|
|
124
|
+
```bash
|
|
125
|
+
ucn fn handleRequest --file=api # Disambiguate by file path
|
|
126
|
+
```
|
|
62
127
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- `--code-only` - Filter out comments and strings
|
|
68
|
-
- `--include-methods` - Include obj.method() calls
|
|
69
|
-
- `--include-tests` - Include test files
|
|
70
|
-
- `--no-cache` - Disable caching
|
|
71
|
-
- `--clear-cache` - Clear cache before running
|
|
128
|
+
**Find unused code:**
|
|
129
|
+
```bash
|
|
130
|
+
ucn deadcode
|
|
131
|
+
```
|
|
72
132
|
|
|
73
|
-
|
|
133
|
+
**Understand a file's role:**
|
|
134
|
+
```bash
|
|
135
|
+
ucn imports core/project.js # What it depends on
|
|
136
|
+
ucn exporters core/project.js # Who depends on it
|
|
137
|
+
```
|
|
74
138
|
|
|
139
|
+
**Multiple queries (keeps index in memory):**
|
|
75
140
|
```bash
|
|
76
|
-
ucn --
|
|
141
|
+
ucn --interactive
|
|
77
142
|
```
|
package/README.md
CHANGED
|
@@ -1,38 +1,140 @@
|
|
|
1
1
|
# UCN - Universal Code Navigator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI agents working with large codebases often read entire files just to understand a single function. UCN uses tree-sitter ASTs to extract exactly what you need — functions, callers, callees, dependencies — without wasting context.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Examples
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Extract a function** from a large file without reading it:
|
|
8
|
+
```
|
|
9
|
+
$ ucn fn expandGlob
|
|
10
|
+
core/discovery.js:135
|
|
11
|
+
[ 135- 166] expandGlob(pattern, options = {})
|
|
12
|
+
────────────────────────────────────────────────────────────
|
|
13
|
+
function expandGlob(pattern, options = {}) {
|
|
14
|
+
const root = path.resolve(options.root || process.cwd());
|
|
15
|
+
const ignores = options.ignores || DEFAULT_IGNORES;
|
|
16
|
+
...
|
|
17
|
+
return files.sort(compareNames);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
**See who calls a function and what it calls:**
|
|
22
|
+
```
|
|
23
|
+
$ ucn context expandGlob
|
|
24
|
+
Context for expandGlob:
|
|
25
|
+
════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
CALLERS (7):
|
|
28
|
+
[1] cli/index.js:1847 [runGlobCommand]
|
|
29
|
+
const files = expandGlob(pattern);
|
|
30
|
+
[2] core/project.js:81
|
|
31
|
+
const files = expandGlob(pattern, {
|
|
32
|
+
[3] core/project.js:3434
|
|
33
|
+
const currentFiles = expandGlob(pattern, { root: this.root });
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
CALLEES (2):
|
|
37
|
+
[8] parseGlobPattern [utility] - core/discovery.js:171
|
|
38
|
+
[9] walkDir [utility] - core/discovery.js:227
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**See what breaks if you change a function:**
|
|
42
|
+
```
|
|
43
|
+
$ ucn impact shouldIgnore
|
|
44
|
+
Impact analysis for shouldIgnore
|
|
45
|
+
════════════════════════════════════════════════════════════
|
|
46
|
+
core/discovery.js:289
|
|
47
|
+
shouldIgnore (name, ignores, parentDir)
|
|
48
|
+
|
|
49
|
+
CALL SITES: 2
|
|
50
|
+
Files affected: 1
|
|
51
|
+
|
|
52
|
+
BY FILE:
|
|
53
|
+
|
|
54
|
+
core/discovery.js (2 calls)
|
|
55
|
+
:255 [walkDir]
|
|
56
|
+
if (shouldIgnore(entry.name, options.ignores, dir)) continue;
|
|
57
|
+
args: entry.name, options.ignores, dir
|
|
58
|
+
:373 [detectProjectPattern]
|
|
59
|
+
!shouldIgnore(entry.name, DEFAULT_IGNORES)) {
|
|
60
|
+
args: entry.name, DEFAULT_IGNORES
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Get a function with all its dependencies inline:**
|
|
64
|
+
```
|
|
65
|
+
$ ucn smart shouldIgnore
|
|
66
|
+
shouldIgnore (/Users/mihail/ucn/core/discovery.js:289)
|
|
67
|
+
════════════════════════════════════════════════════════════
|
|
68
|
+
function shouldIgnore(name, ignores, parentDir) {
|
|
69
|
+
for (const pattern of ignores) {
|
|
70
|
+
if (pattern.includes('*')) {
|
|
71
|
+
const regex = globToRegex(pattern);
|
|
72
|
+
...
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
...
|
|
18
76
|
}
|
|
19
77
|
|
|
20
|
-
|
|
21
|
-
CALLERS (3):
|
|
22
|
-
src/server.js:45 [startServer]
|
|
23
|
-
src/middleware.js:23 [authMiddleware]
|
|
24
|
-
test/api.test.js:67 [testHandler]
|
|
78
|
+
─── DEPENDENCIES ───
|
|
25
79
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
80
|
+
// globToRegex [utility] (core/discovery.js:208)
|
|
81
|
+
function globToRegex(glob) {
|
|
82
|
+
let regex = glob.replace(/[.+^$[\]\\]/g, '\\$&');
|
|
83
|
+
...
|
|
84
|
+
return new RegExp('^' + regex + '$');
|
|
85
|
+
}
|
|
30
86
|
```
|
|
31
87
|
|
|
32
|
-
|
|
88
|
+
**Trace the call tree:**
|
|
89
|
+
```
|
|
90
|
+
$ ucn trace expandGlob --depth=2
|
|
91
|
+
Call tree for expandGlob
|
|
92
|
+
════════════════════════════════════════════════════════════
|
|
93
|
+
|
|
94
|
+
expandGlob
|
|
95
|
+
├── parseGlobPattern (core/discovery.js:171) [utility] 1x
|
|
96
|
+
│ └── globToRegex (core/discovery.js:208) [utility] 1x
|
|
97
|
+
└── walkDir (core/discovery.js:227) [utility] 1x
|
|
98
|
+
└── shouldIgnore (core/discovery.js:289) [utility] 1x
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Find unused code:**
|
|
102
|
+
```
|
|
103
|
+
$ ucn deadcode
|
|
104
|
+
Dead code: 15 unused symbol(s)
|
|
105
|
+
|
|
106
|
+
cli/index.js
|
|
107
|
+
[1649-1654] extractFunctionNameFromContent (function)
|
|
108
|
+
core/project.js
|
|
109
|
+
[1664-1694] findReExportsOf (method)
|
|
110
|
+
[1998-2020] withCompleteness (method)
|
|
111
|
+
...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**See a file's dependencies:**
|
|
115
|
+
```
|
|
116
|
+
$ ucn imports core/project.js
|
|
117
|
+
Imports in core/project.js:
|
|
118
|
+
|
|
119
|
+
INTERNAL:
|
|
120
|
+
./discovery
|
|
121
|
+
-> core/discovery.js
|
|
122
|
+
expandGlob, findProjectRoot, detectProjectPattern, isTestFile
|
|
123
|
+
./imports
|
|
124
|
+
-> core/imports.js
|
|
125
|
+
extractImports, extractExports, resolveImport
|
|
126
|
+
./parser
|
|
127
|
+
-> core/parser.js
|
|
128
|
+
parseFile
|
|
129
|
+
../languages
|
|
130
|
+
-> languages/index.js
|
|
131
|
+
detectLanguage, getParser, getLanguageModule, PARSE_OPTIONS, safeParse
|
|
132
|
+
|
|
133
|
+
EXTERNAL:
|
|
134
|
+
fs, path, crypto
|
|
135
|
+
```
|
|
33
136
|
|
|
34
137
|
## Supported Languages
|
|
35
|
-
The supported languages can grow as tree-sitter supports many, but for my use cases I've added support for:
|
|
36
138
|
|
|
37
139
|
JavaScript, TypeScript, Python, Go, Rust, Java
|
|
38
140
|
|
|
@@ -75,49 +177,65 @@ cp -r ucn/.claude/skills/ucn ~/.agents/skills/
|
|
|
75
177
|
## Usage
|
|
76
178
|
|
|
77
179
|
```
|
|
180
|
+
UCN - Universal Code Navigator
|
|
181
|
+
|
|
182
|
+
Supported: JavaScript, TypeScript, Python, Go, Rust, Java
|
|
183
|
+
|
|
78
184
|
Usage:
|
|
79
185
|
ucn [command] [args] Project mode (current directory)
|
|
80
186
|
ucn <file> [command] [args] Single file mode
|
|
81
187
|
ucn <dir> [command] [args] Project mode (specific directory)
|
|
82
188
|
ucn "pattern" [command] [args] Glob pattern mode
|
|
83
189
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
190
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
191
|
+
UNDERSTAND CODE (UCN's strength - semantic analysis)
|
|
192
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
193
|
+
about <name> RECOMMENDED: Full picture (definition, callers, callees, tests, code)
|
|
194
|
+
context <name> Who calls this + what it calls (numbered for expand)
|
|
87
195
|
smart <name> Function + all dependencies inline
|
|
88
196
|
impact <name> What breaks if changed (call sites grouped by file)
|
|
89
197
|
trace <name> Call tree visualization (--depth=N)
|
|
90
198
|
|
|
199
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
91
200
|
FIND CODE
|
|
201
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
92
202
|
find <name> Find symbol definitions (top 5 by usage count)
|
|
93
203
|
usages <name> All usages grouped: definitions, calls, imports, references
|
|
94
204
|
toc Table of contents (functions, classes, state)
|
|
95
|
-
search <term> Text search
|
|
205
|
+
search <term> Text search (for simple patterns, consider grep instead)
|
|
96
206
|
tests <name> Find test files for a function
|
|
97
207
|
|
|
208
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
98
209
|
EXTRACT CODE
|
|
210
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
99
211
|
fn <name> Extract function (--file to disambiguate)
|
|
100
212
|
class <name> Extract class
|
|
101
213
|
lines <range> Extract line range (e.g., lines 50-100)
|
|
102
214
|
expand <N> Show code for item N from context output
|
|
103
215
|
|
|
216
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
104
217
|
FILE DEPENDENCIES
|
|
218
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
105
219
|
imports <file> What does file import
|
|
106
220
|
exporters <file> Who imports this file
|
|
107
221
|
file-exports <file> What does file export
|
|
108
222
|
graph <file> Full dependency tree (--depth=N)
|
|
109
223
|
|
|
224
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
110
225
|
REFACTORING HELPERS
|
|
226
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
111
227
|
plan <name> Preview refactoring (--add-param, --remove-param, --rename-to)
|
|
112
228
|
verify <name> Check all call sites match signature
|
|
113
229
|
deadcode Find unused functions/classes
|
|
114
230
|
related <name> Find similar functions (same file, shared deps)
|
|
115
231
|
|
|
232
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
116
233
|
OTHER
|
|
234
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
117
235
|
api Show exported/public symbols
|
|
118
236
|
typedef <name> Find type definitions
|
|
119
237
|
stats Project statistics
|
|
120
|
-
stacktrace <text> Parse stack trace, show code at each frame
|
|
238
|
+
stacktrace <text> Parse stack trace, show code at each frame (alias: stack)
|
|
121
239
|
example <name> Best usage example with context
|
|
122
240
|
|
|
123
241
|
Common Flags:
|
|
@@ -134,7 +252,15 @@ Common Flags:
|
|
|
134
252
|
--include-methods Include method calls (obj.fn) in caller/callee analysis
|
|
135
253
|
--no-cache Disable caching
|
|
136
254
|
--clear-cache Clear cache before running
|
|
255
|
+
--no-follow-symlinks Don't follow symbolic links
|
|
137
256
|
-i, --interactive Keep index in memory for multiple queries
|
|
257
|
+
|
|
258
|
+
Quick Start:
|
|
259
|
+
ucn toc # See project structure
|
|
260
|
+
ucn about handleRequest # Understand a function
|
|
261
|
+
ucn impact handleRequest # Before modifying
|
|
262
|
+
ucn fn handleRequest --file api # Extract specific function
|
|
263
|
+
ucn --interactive # Multiple queries
|
|
138
264
|
```
|
|
139
265
|
|
|
140
266
|
## License
|
package/cli/index.js
CHANGED
|
@@ -529,57 +529,11 @@ function usagesInFile(code, lines, name, filePath, result) {
|
|
|
529
529
|
return;
|
|
530
530
|
}
|
|
531
531
|
} catch (e) {
|
|
532
|
-
//
|
|
532
|
+
// AST parsing failed — usages will be empty, only definitions shown
|
|
533
533
|
}
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
-
//
|
|
537
|
-
const regex = new RegExp('\\b' + escapeRegExp(name) + '\\b');
|
|
538
|
-
lines.forEach((line, idx) => {
|
|
539
|
-
const lineNum = idx + 1;
|
|
540
|
-
|
|
541
|
-
// Skip definition lines
|
|
542
|
-
if (defs.some(d => d.startLine === lineNum)) {
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (regex.test(line)) {
|
|
547
|
-
if (flags.codeOnly && isCommentOrString(line)) {
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Skip if the match is inside a string literal
|
|
552
|
-
if (isInsideString(line, name)) {
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const usageType = classifyUsage(line, name);
|
|
557
|
-
const usage = {
|
|
558
|
-
file: filePath,
|
|
559
|
-
relativePath: filePath,
|
|
560
|
-
line: lineNum,
|
|
561
|
-
content: line,
|
|
562
|
-
usageType,
|
|
563
|
-
isDefinition: false
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
// Add context
|
|
567
|
-
if (flags.context > 0) {
|
|
568
|
-
const before = [];
|
|
569
|
-
const after = [];
|
|
570
|
-
for (let i = 1; i <= flags.context; i++) {
|
|
571
|
-
if (idx - i >= 0) before.unshift(lines[idx - i]);
|
|
572
|
-
if (idx + i < lines.length) after.push(lines[idx + i]);
|
|
573
|
-
}
|
|
574
|
-
usage.before = before;
|
|
575
|
-
usage.after = after;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
usages.push(usage);
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
// Add definitions to result
|
|
536
|
+
// Output definitions + any usages found via AST
|
|
583
537
|
const allUsages = [
|
|
584
538
|
...defs.map(d => ({
|
|
585
539
|
...d,
|
|
@@ -2137,27 +2091,6 @@ function escapeRegExp(text) {
|
|
|
2137
2091
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2138
2092
|
}
|
|
2139
2093
|
|
|
2140
|
-
function classifyUsage(line, name) {
|
|
2141
|
-
// Check if it's an import first
|
|
2142
|
-
if (/^\s*(import|from|require|use)\b/.test(line)) {
|
|
2143
|
-
return 'import';
|
|
2144
|
-
}
|
|
2145
|
-
// Check if it's a function call (but not a method call)
|
|
2146
|
-
if (new RegExp('\\b' + escapeRegExp(name) + '\\s*\\(').test(line)) {
|
|
2147
|
-
// Exclude method calls (obj.name, this.name, JSON.name, etc.)
|
|
2148
|
-
if (!isMethodCall(line, name)) {
|
|
2149
|
-
return 'call';
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
return 'reference';
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
function isMethodCall(line, name) {
|
|
2156
|
-
// Check if there's a dot or ] immediately before the name
|
|
2157
|
-
const methodPattern = new RegExp('[.\\]]\\s*' + escapeRegExp(name) + '\\s*\\(');
|
|
2158
|
-
return methodPattern.test(line);
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
2094
|
function isCommentOrString(line) {
|
|
2162
2095
|
const trimmed = line.trim();
|
|
2163
2096
|
return trimmed.startsWith('//') ||
|
|
@@ -2166,31 +2099,6 @@ function isCommentOrString(line) {
|
|
|
2166
2099
|
trimmed.startsWith('/*');
|
|
2167
2100
|
}
|
|
2168
2101
|
|
|
2169
|
-
function isInsideString(line, name) {
|
|
2170
|
-
// Simple heuristic: check if name appears inside quotes
|
|
2171
|
-
// Find all string regions in the line
|
|
2172
|
-
const stringRegex = /(['"`])(?:(?!\1|\\).|\\.)*\1/g;
|
|
2173
|
-
let match;
|
|
2174
|
-
|
|
2175
|
-
while ((match = stringRegex.exec(line)) !== null) {
|
|
2176
|
-
const stringContent = match[0];
|
|
2177
|
-
const stringStart = match.index;
|
|
2178
|
-
const stringEnd = stringStart + stringContent.length;
|
|
2179
|
-
|
|
2180
|
-
// Find where the name appears in the line
|
|
2181
|
-
const nameRegex = new RegExp('\\b' + escapeRegExp(name) + '\\b', 'g');
|
|
2182
|
-
let nameMatch;
|
|
2183
|
-
while ((nameMatch = nameRegex.exec(line)) !== null) {
|
|
2184
|
-
const nameStart = nameMatch.index;
|
|
2185
|
-
// Check if this name occurrence is inside the string
|
|
2186
|
-
if (nameStart > stringStart && nameStart < stringEnd) {
|
|
2187
|
-
return true;
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
return false;
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
2102
|
function printUsage() {
|
|
2195
2103
|
console.log(`UCN - Universal Code Navigator
|
|
2196
2104
|
|
package/core/discovery.js
CHANGED
package/core/imports.js
CHANGED
|
@@ -9,49 +9,6 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { getParser, getLanguageModule } = require('../languages');
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Import patterns by language
|
|
14
|
-
* @deprecated Use AST-based findImportsInCode() from language modules instead.
|
|
15
|
-
* Kept only as fallback for unsupported languages or when AST parsing fails.
|
|
16
|
-
*/
|
|
17
|
-
const IMPORT_PATTERNS = {
|
|
18
|
-
javascript: {
|
|
19
|
-
importDefault: /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
20
|
-
importNamed: /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g,
|
|
21
|
-
importNamespace: /import\s*\*\s*as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
22
|
-
require: /(?:const|let|var)\s+(?:\{[^}]+\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
23
|
-
exportNamed: /^\s*export\s+(?:async\s+)?(?:function|class|const|let|var|interface|type)\s+(\w+)/gm,
|
|
24
|
-
exportDefault: /^\s*export\s+default\s+(?:(?:async\s+)?(?:function|class)\s+)?(\w+)?/gm,
|
|
25
|
-
exportList: /^\s*export\s*\{([^}]+)\}/gm,
|
|
26
|
-
moduleExports: /^module\.exports\s*=\s*(?:\{([^}]+)\}|(\w+))/gm,
|
|
27
|
-
exportsNamed: /^exports\.(\w+)\s*=[^=]/gm,
|
|
28
|
-
importType: /import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g,
|
|
29
|
-
importSideEffect: /import\s+['"]([^'"]+)['"]/g,
|
|
30
|
-
importDynamic: /(?:await\s+)?import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
31
|
-
reExportNamed: /^\s*export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/gm,
|
|
32
|
-
reExportAll: /^\s*export\s*\*\s*from\s*['"]([^'"]+)['"]/gm
|
|
33
|
-
},
|
|
34
|
-
python: {
|
|
35
|
-
importModule: /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm,
|
|
36
|
-
fromImport: /^from\s+([.\w]+)\s+import\s+(.+)/gm,
|
|
37
|
-
exportAll: /__all__\s*=\s*\[([^\]]+)\]/g
|
|
38
|
-
},
|
|
39
|
-
go: {
|
|
40
|
-
importSingle: /import\s+"([^"]+)"/g,
|
|
41
|
-
importBlock: /import\s*\(\s*([\s\S]*?)\s*\)/g,
|
|
42
|
-
exportedFunc: /^func\s+(?:\([^)]+\)\s+)?([A-Z]\w*)\s*\(/gm,
|
|
43
|
-
exportedType: /^type\s+([A-Z]\w*)\s+/gm
|
|
44
|
-
},
|
|
45
|
-
java: {
|
|
46
|
-
importStatement: /import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/g,
|
|
47
|
-
exportedClass: /public\s+(?:abstract\s+)?(?:final\s+)?(?:class|interface|enum)\s+(\w+)/g
|
|
48
|
-
},
|
|
49
|
-
rust: {
|
|
50
|
-
useStatement: /^use\s+([^;]+);/gm,
|
|
51
|
-
modDecl: /^\s*mod\s+(\w+)\s*;/gm
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
12
|
/**
|
|
56
13
|
* Extract imports from file content using AST
|
|
57
14
|
*
|
|
@@ -63,7 +20,6 @@ function extractImports(content, language) {
|
|
|
63
20
|
// Normalize language name for parser
|
|
64
21
|
const normalizedLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
|
|
65
22
|
|
|
66
|
-
// Try AST-based extraction first
|
|
67
23
|
const langModule = getLanguageModule(normalizedLang);
|
|
68
24
|
if (langModule && typeof langModule.findImportsInCode === 'function') {
|
|
69
25
|
try {
|
|
@@ -73,176 +29,11 @@ function extractImports(content, language) {
|
|
|
73
29
|
return { imports };
|
|
74
30
|
}
|
|
75
31
|
} catch (e) {
|
|
76
|
-
//
|
|
32
|
+
// AST parsing failed
|
|
77
33
|
}
|
|
78
34
|
}
|
|
79
35
|
|
|
80
|
-
|
|
81
|
-
const imports = [];
|
|
82
|
-
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
83
|
-
extractJSImports(content, imports);
|
|
84
|
-
} else if (language === 'python') {
|
|
85
|
-
extractPythonImports(content, imports);
|
|
86
|
-
} else if (language === 'go') {
|
|
87
|
-
extractGoImports(content, imports);
|
|
88
|
-
} else if (language === 'java') {
|
|
89
|
-
extractJavaImports(content, imports);
|
|
90
|
-
} else if (language === 'rust') {
|
|
91
|
-
extractRustImports(content, imports);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { imports };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @deprecated Use AST-based findImportsInCode() from language modules.
|
|
99
|
-
*/
|
|
100
|
-
function extractJSImports(content, imports) {
|
|
101
|
-
const patterns = IMPORT_PATTERNS.javascript;
|
|
102
|
-
let match;
|
|
103
|
-
|
|
104
|
-
// Default imports
|
|
105
|
-
let regex = new RegExp(patterns.importDefault.source, 'g');
|
|
106
|
-
while ((match = regex.exec(content)) !== null) {
|
|
107
|
-
imports.push({ module: match[2], names: [match[1]], type: 'default' });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Named imports
|
|
111
|
-
regex = new RegExp(patterns.importNamed.source, 'g');
|
|
112
|
-
while ((match = regex.exec(content)) !== null) {
|
|
113
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
114
|
-
imports.push({ module: match[2], names, type: 'named' });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Namespace imports
|
|
118
|
-
regex = new RegExp(patterns.importNamespace.source, 'g');
|
|
119
|
-
while ((match = regex.exec(content)) !== null) {
|
|
120
|
-
imports.push({ module: match[2], names: [match[1]], type: 'namespace' });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Require
|
|
124
|
-
regex = new RegExp(patterns.require.source, 'g');
|
|
125
|
-
while ((match = regex.exec(content)) !== null) {
|
|
126
|
-
imports.push({ module: match[2], names: match[1] ? [match[1]] : [], type: 'require' });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Type imports
|
|
130
|
-
regex = new RegExp(patterns.importType.source, 'g');
|
|
131
|
-
while ((match = regex.exec(content)) !== null) {
|
|
132
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
133
|
-
imports.push({ module: match[2], names, type: 'type' });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Side-effect imports
|
|
137
|
-
regex = new RegExp(patterns.importSideEffect.source, 'g');
|
|
138
|
-
while ((match = regex.exec(content)) !== null) {
|
|
139
|
-
const module = match[1];
|
|
140
|
-
if (!imports.some(i => i.module === module)) {
|
|
141
|
-
imports.push({ module, names: [], type: 'side-effect' });
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Dynamic imports
|
|
146
|
-
regex = new RegExp(patterns.importDynamic.source, 'g');
|
|
147
|
-
while ((match = regex.exec(content)) !== null) {
|
|
148
|
-
const module = match[1];
|
|
149
|
-
if (!imports.some(i => i.module === module)) {
|
|
150
|
-
imports.push({ module, names: [], type: 'dynamic' });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Re-exports
|
|
155
|
-
regex = new RegExp(patterns.reExportNamed.source, 'gm');
|
|
156
|
-
while ((match = regex.exec(content)) !== null) {
|
|
157
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
158
|
-
imports.push({ module: match[2], names, type: 're-export' });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
regex = new RegExp(patterns.reExportAll.source, 'gm');
|
|
162
|
-
while ((match = regex.exec(content)) !== null) {
|
|
163
|
-
imports.push({ module: match[1], names: ['*'], type: 're-export-all' });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
168
|
-
function extractPythonImports(content, imports) {
|
|
169
|
-
const patterns = IMPORT_PATTERNS.python;
|
|
170
|
-
let match;
|
|
171
|
-
|
|
172
|
-
let regex = new RegExp(patterns.importModule.source, 'gm');
|
|
173
|
-
while ((match = regex.exec(content)) !== null) {
|
|
174
|
-
const moduleName = match[1];
|
|
175
|
-
const alias = match[2] || moduleName.split('.').pop();
|
|
176
|
-
imports.push({ module: moduleName, names: [alias], type: 'module' });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
regex = new RegExp(patterns.fromImport.source, 'gm');
|
|
180
|
-
while ((match = regex.exec(content)) !== null) {
|
|
181
|
-
const moduleName = match[1];
|
|
182
|
-
const importList = match[2].trim();
|
|
183
|
-
|
|
184
|
-
if (importList === '*') {
|
|
185
|
-
imports.push({ module: moduleName, names: ['*'], type: 'star' });
|
|
186
|
-
} else {
|
|
187
|
-
const names = importList.split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n && n !== '(');
|
|
188
|
-
imports.push({ module: moduleName, names, type: 'from' });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
194
|
-
function extractGoImports(content, imports) {
|
|
195
|
-
const patterns = IMPORT_PATTERNS.go;
|
|
196
|
-
let match;
|
|
197
|
-
|
|
198
|
-
let regex = new RegExp(patterns.importSingle.source, 'g');
|
|
199
|
-
while ((match = regex.exec(content)) !== null) {
|
|
200
|
-
const pkg = match[1];
|
|
201
|
-
imports.push({ module: pkg, names: [path.basename(pkg)], type: 'single' });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
regex = new RegExp(patterns.importBlock.source, 'g');
|
|
205
|
-
while ((match = regex.exec(content)) !== null) {
|
|
206
|
-
const block = match[1];
|
|
207
|
-
const pkgMatches = block.matchAll(/"([^"]+)"/g);
|
|
208
|
-
for (const pkgMatch of pkgMatches) {
|
|
209
|
-
const pkg = pkgMatch[1];
|
|
210
|
-
imports.push({ module: pkg, names: [path.basename(pkg)], type: 'block' });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
216
|
-
function extractJavaImports(content, imports) {
|
|
217
|
-
const patterns = IMPORT_PATTERNS.java;
|
|
218
|
-
let match;
|
|
219
|
-
|
|
220
|
-
let regex = new RegExp(patterns.importStatement.source, 'g');
|
|
221
|
-
while ((match = regex.exec(content)) !== null) {
|
|
222
|
-
const fullImport = match[1];
|
|
223
|
-
const parts = fullImport.split('.');
|
|
224
|
-
const name = parts[parts.length - 1];
|
|
225
|
-
imports.push({ module: fullImport, names: name === '*' ? ['*'] : [name], type: 'import' });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** @deprecated Use AST-based findImportsInCode() from language modules. */
|
|
230
|
-
function extractRustImports(content, imports) {
|
|
231
|
-
const patterns = IMPORT_PATTERNS.rust;
|
|
232
|
-
let match;
|
|
233
|
-
|
|
234
|
-
let regex = new RegExp(patterns.useStatement.source, 'gm');
|
|
235
|
-
while ((match = regex.exec(content)) !== null) {
|
|
236
|
-
let raw = match[1].trim().split('{')[0].trim().split(' as ')[0].trim().replace(/::$/, '');
|
|
237
|
-
if (raw) {
|
|
238
|
-
imports.push({ module: raw, names: [], type: 'use' });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
regex = new RegExp(patterns.modDecl.source, 'gm');
|
|
243
|
-
while ((match = regex.exec(content)) !== null) {
|
|
244
|
-
imports.push({ module: `self::${match[1]}`, names: [match[1]], type: 'mod' });
|
|
245
|
-
}
|
|
36
|
+
return { imports: [] };
|
|
246
37
|
}
|
|
247
38
|
|
|
248
39
|
/**
|
|
@@ -252,7 +43,6 @@ function extractExports(content, language) {
|
|
|
252
43
|
// Normalize language name for parser
|
|
253
44
|
const normalizedLang = (language === 'typescript' || language === 'tsx') ? 'javascript' : language;
|
|
254
45
|
|
|
255
|
-
// Try AST-based extraction first
|
|
256
46
|
const langModule = getLanguageModule(normalizedLang);
|
|
257
47
|
if (langModule && typeof langModule.findExportsInCode === 'function') {
|
|
258
48
|
try {
|
|
@@ -262,123 +52,11 @@ function extractExports(content, language) {
|
|
|
262
52
|
return { exports: foundExports };
|
|
263
53
|
}
|
|
264
54
|
} catch (e) {
|
|
265
|
-
//
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Fallback to regex-based extraction (deprecated)
|
|
270
|
-
const foundExports = [];
|
|
271
|
-
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
272
|
-
extractJSExports(content, foundExports);
|
|
273
|
-
} else if (language === 'python') {
|
|
274
|
-
extractPythonExports(content, foundExports);
|
|
275
|
-
} else if (language === 'go') {
|
|
276
|
-
extractGoExports(content, foundExports);
|
|
277
|
-
} else if (language === 'java') {
|
|
278
|
-
extractJavaExports(content, foundExports);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return { exports: foundExports };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
285
|
-
function extractJSExports(content, exports) {
|
|
286
|
-
const patterns = IMPORT_PATTERNS.javascript;
|
|
287
|
-
let match;
|
|
288
|
-
|
|
289
|
-
let regex = new RegExp(patterns.exportNamed.source, 'gm');
|
|
290
|
-
while ((match = regex.exec(content)) !== null) {
|
|
291
|
-
exports.push({ name: match[1], type: 'named' });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
regex = new RegExp(patterns.exportDefault.source, 'gm');
|
|
295
|
-
while ((match = regex.exec(content)) !== null) {
|
|
296
|
-
exports.push({ name: match[1] || 'default', type: 'default' });
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
regex = new RegExp(patterns.exportList.source, 'gm');
|
|
300
|
-
while ((match = regex.exec(content)) !== null) {
|
|
301
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim()).filter(n => n);
|
|
302
|
-
for (const name of names) {
|
|
303
|
-
exports.push({ name, type: 'list' });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
regex = new RegExp(patterns.exportsNamed.source, 'gm');
|
|
308
|
-
while ((match = regex.exec(content)) !== null) {
|
|
309
|
-
exports.push({ name: match[1], type: 'commonjs-named' });
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// module.exports = { a, b, c } or module.exports = identifier
|
|
313
|
-
regex = new RegExp(patterns.moduleExports.source, 'gm');
|
|
314
|
-
while ((match = regex.exec(content)) !== null) {
|
|
315
|
-
if (match[1]) {
|
|
316
|
-
// Object literal: module.exports = { a, b, c }
|
|
317
|
-
const names = match[1].split(',').map(n => n.trim().split(/\s*:\s*/)[0].trim()).filter(n => n && !n.includes('('));
|
|
318
|
-
for (const name of names) {
|
|
319
|
-
exports.push({ name, type: 'commonjs-object' });
|
|
320
|
-
}
|
|
321
|
-
} else if (match[2]) {
|
|
322
|
-
// Single identifier: module.exports = SomeClass
|
|
323
|
-
exports.push({ name: match[2], type: 'commonjs-default' });
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
329
|
-
function extractPythonExports(content, exports) {
|
|
330
|
-
let match;
|
|
331
|
-
|
|
332
|
-
// Check for __all__
|
|
333
|
-
let regex = new RegExp(IMPORT_PATTERNS.python.exportAll.source, 'g');
|
|
334
|
-
while ((match = regex.exec(content)) !== null) {
|
|
335
|
-
const names = match[1].split(',').map(n => n.trim().replace(/['"]/g, '')).filter(n => n);
|
|
336
|
-
for (const name of names) {
|
|
337
|
-
exports.push({ name, type: 'explicit' });
|
|
55
|
+
// AST parsing failed
|
|
338
56
|
}
|
|
339
57
|
}
|
|
340
58
|
|
|
341
|
-
|
|
342
|
-
if (exports.length === 0) {
|
|
343
|
-
const funcRegex = /^def\s+([a-zA-Z]\w*)\s*\(/gm;
|
|
344
|
-
while ((match = funcRegex.exec(content)) !== null) {
|
|
345
|
-
if (!match[1].startsWith('_')) {
|
|
346
|
-
exports.push({ name: match[1], type: 'function' });
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const classRegex = /^class\s+([a-zA-Z]\w*)/gm;
|
|
351
|
-
while ((match = classRegex.exec(content)) !== null) {
|
|
352
|
-
if (!match[1].startsWith('_')) {
|
|
353
|
-
exports.push({ name: match[1], type: 'class' });
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
360
|
-
function extractGoExports(content, exports) {
|
|
361
|
-
const patterns = IMPORT_PATTERNS.go;
|
|
362
|
-
let match;
|
|
363
|
-
|
|
364
|
-
let regex = new RegExp(patterns.exportedFunc.source, 'gm');
|
|
365
|
-
while ((match = regex.exec(content)) !== null) {
|
|
366
|
-
exports.push({ name: match[1], type: 'function' });
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
regex = new RegExp(patterns.exportedType.source, 'gm');
|
|
370
|
-
while ((match = regex.exec(content)) !== null) {
|
|
371
|
-
exports.push({ name: match[1], type: 'type' });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** @deprecated Use AST-based findExportsInCode() from language modules. */
|
|
376
|
-
function extractJavaExports(content, exports) {
|
|
377
|
-
let match;
|
|
378
|
-
let regex = new RegExp(IMPORT_PATTERNS.java.exportedClass.source, 'g');
|
|
379
|
-
while ((match = regex.exec(content)) !== null) {
|
|
380
|
-
exports.push({ name: match[1], type: 'class' });
|
|
381
|
-
}
|
|
59
|
+
return { exports: [] };
|
|
382
60
|
}
|
|
383
61
|
|
|
384
62
|
// Cache for tsconfig lookups
|
|
@@ -636,6 +314,5 @@ function stripJsonComments(content) {
|
|
|
636
314
|
module.exports = {
|
|
637
315
|
extractImports,
|
|
638
316
|
extractExports,
|
|
639
|
-
resolveImport
|
|
640
|
-
IMPORT_PATTERNS
|
|
317
|
+
resolveImport
|
|
641
318
|
};
|
package/core/project.js
CHANGED
|
@@ -11,9 +11,15 @@ const crypto = require('crypto');
|
|
|
11
11
|
const { expandGlob, findProjectRoot, detectProjectPattern, isTestFile } = require('./discovery');
|
|
12
12
|
const { extractImports, extractExports, resolveImport } = require('./imports');
|
|
13
13
|
const { parseFile } = require('./parser');
|
|
14
|
-
const { detectLanguage, getParser, getLanguageModule, PARSE_OPTIONS } = require('../languages');
|
|
14
|
+
const { detectLanguage, getParser, getLanguageModule, PARSE_OPTIONS, safeParse } = require('../languages');
|
|
15
15
|
const { getTokenTypeAtPosition } = require('../languages/utils');
|
|
16
16
|
|
|
17
|
+
// Read UCN version for cache invalidation
|
|
18
|
+
const UCN_VERSION = require('../package.json').version;
|
|
19
|
+
|
|
20
|
+
// Lazy-initialized per-language keyword sets (populated on first isKeyword call)
|
|
21
|
+
let LANGUAGE_KEYWORDS = null;
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* Escape special regex characters
|
|
19
25
|
*/
|
|
@@ -413,25 +419,6 @@ class ProjectIndex {
|
|
|
413
419
|
return withCounts;
|
|
414
420
|
}
|
|
415
421
|
|
|
416
|
-
/**
|
|
417
|
-
* Count usages of a symbol across the codebase
|
|
418
|
-
*/
|
|
419
|
-
countUsages(name) {
|
|
420
|
-
let count = 0;
|
|
421
|
-
const regex = new RegExp('\\b' + escapeRegExp(name) + '\\b', 'g');
|
|
422
|
-
|
|
423
|
-
for (const [filePath, fileEntry] of this.files) {
|
|
424
|
-
try {
|
|
425
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
426
|
-
const matches = content.match(regex);
|
|
427
|
-
if (matches) count += matches.length;
|
|
428
|
-
} catch (e) {
|
|
429
|
-
// Skip unreadable files
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return count;
|
|
434
|
-
}
|
|
435
422
|
|
|
436
423
|
/**
|
|
437
424
|
* Count usages of a specific symbol (not just by name)
|
|
@@ -1004,7 +991,7 @@ class ProjectIndex {
|
|
|
1004
991
|
}
|
|
1005
992
|
|
|
1006
993
|
// Skip keywords and built-ins
|
|
1007
|
-
if (this.isKeyword(call.name)) continue;
|
|
994
|
+
if (this.isKeyword(call.name, language)) continue;
|
|
1008
995
|
|
|
1009
996
|
callees.set(call.name, (callees.get(call.name) || 0) + 1);
|
|
1010
997
|
}
|
|
@@ -1237,20 +1224,53 @@ class ProjectIndex {
|
|
|
1237
1224
|
/**
|
|
1238
1225
|
* Check if a name is a language keyword
|
|
1239
1226
|
*/
|
|
1240
|
-
isKeyword(name) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1227
|
+
isKeyword(name, language) {
|
|
1228
|
+
if (!LANGUAGE_KEYWORDS) {
|
|
1229
|
+
// Initialize on first use
|
|
1230
|
+
LANGUAGE_KEYWORDS = {
|
|
1231
|
+
javascript: new Set([
|
|
1232
|
+
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
|
|
1233
|
+
'continue', 'return', 'function', 'class', 'const', 'let', 'var',
|
|
1234
|
+
'new', 'this', 'super', 'import', 'export', 'default', 'from',
|
|
1235
|
+
'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
|
|
1236
|
+
'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'with'
|
|
1237
|
+
]),
|
|
1238
|
+
python: new Set([
|
|
1239
|
+
'if', 'else', 'elif', 'for', 'while', 'def', 'class', 'return',
|
|
1240
|
+
'import', 'from', 'try', 'except', 'finally', 'raise', 'async',
|
|
1241
|
+
'await', 'yield', 'with', 'as', 'lambda', 'pass', 'break',
|
|
1242
|
+
'continue', 'del', 'global', 'nonlocal', 'assert', 'is', 'not',
|
|
1243
|
+
'and', 'or', 'in', 'True', 'False', 'None', 'self', 'cls'
|
|
1244
|
+
]),
|
|
1245
|
+
go: new Set([
|
|
1246
|
+
'if', 'else', 'for', 'switch', 'case', 'break', 'continue',
|
|
1247
|
+
'return', 'func', 'type', 'struct', 'interface', 'package',
|
|
1248
|
+
'import', 'go', 'defer', 'select', 'chan', 'map', 'range',
|
|
1249
|
+
'fallthrough', 'goto', 'var', 'const', 'default'
|
|
1250
|
+
]),
|
|
1251
|
+
rust: new Set([
|
|
1252
|
+
'if', 'else', 'for', 'while', 'loop', 'fn', 'impl', 'pub',
|
|
1253
|
+
'mod', 'use', 'crate', 'self', 'super', 'match', 'unsafe',
|
|
1254
|
+
'move', 'ref', 'mut', 'where', 'let', 'const', 'struct',
|
|
1255
|
+
'enum', 'trait', 'async', 'await', 'return', 'break',
|
|
1256
|
+
'continue', 'type', 'as', 'in', 'dyn', 'static'
|
|
1257
|
+
]),
|
|
1258
|
+
java: new Set([
|
|
1259
|
+
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
|
|
1260
|
+
'continue', 'return', 'class', 'interface', 'enum', 'extends',
|
|
1261
|
+
'implements', 'new', 'this', 'super', 'import', 'package',
|
|
1262
|
+
'try', 'catch', 'finally', 'throw', 'throws', 'abstract',
|
|
1263
|
+
'static', 'final', 'synchronized', 'volatile', 'transient',
|
|
1264
|
+
'native', 'void', 'instanceof', 'default'
|
|
1265
|
+
])
|
|
1266
|
+
};
|
|
1267
|
+
// TypeScript/TSX share JavaScript keywords
|
|
1268
|
+
LANGUAGE_KEYWORDS.typescript = LANGUAGE_KEYWORDS.javascript;
|
|
1269
|
+
LANGUAGE_KEYWORDS.tsx = LANGUAGE_KEYWORDS.javascript;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const keywords = LANGUAGE_KEYWORDS[language];
|
|
1273
|
+
return keywords ? keywords.has(name) : false;
|
|
1254
1274
|
}
|
|
1255
1275
|
|
|
1256
1276
|
/**
|
|
@@ -2261,6 +2281,7 @@ class ProjectIndex {
|
|
|
2261
2281
|
...analysis
|
|
2262
2282
|
};
|
|
2263
2283
|
});
|
|
2284
|
+
this._clearTreeCache();
|
|
2264
2285
|
|
|
2265
2286
|
// Group by file if requested
|
|
2266
2287
|
const byFile = new Map();
|
|
@@ -2806,6 +2827,7 @@ class ProjectIndex {
|
|
|
2806
2827
|
}
|
|
2807
2828
|
}
|
|
2808
2829
|
}
|
|
2830
|
+
this._clearTreeCache();
|
|
2809
2831
|
|
|
2810
2832
|
return {
|
|
2811
2833
|
found: true,
|
|
@@ -2829,87 +2851,89 @@ class ProjectIndex {
|
|
|
2829
2851
|
}
|
|
2830
2852
|
|
|
2831
2853
|
/**
|
|
2832
|
-
* Analyze a call site to understand how it's being called
|
|
2854
|
+
* Analyze a call site to understand how it's being called (AST-based)
|
|
2855
|
+
* @param {object} call - Usage object with file, line, content
|
|
2856
|
+
* @param {string} funcName - Function name to find
|
|
2857
|
+
* @returns {object} { args, argCount, hasSpread, hasVariable }
|
|
2833
2858
|
*/
|
|
2834
2859
|
analyzeCallSite(call, funcName) {
|
|
2835
|
-
|
|
2860
|
+
try {
|
|
2861
|
+
const language = detectLanguage(call.file);
|
|
2862
|
+
if (!language) return { args: null, argCount: 0 };
|
|
2836
2863
|
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
if (!callMatch) {
|
|
2840
|
-
return { args: null, argCount: 0 };
|
|
2841
|
-
}
|
|
2864
|
+
const parser = getParser(language);
|
|
2865
|
+
if (!parser) return { args: null, argCount: 0 };
|
|
2842
2866
|
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2867
|
+
// Use tree cache to avoid re-parsing the same file in batch operations
|
|
2868
|
+
let tree = this._treeCache?.get(call.file);
|
|
2869
|
+
if (!tree) {
|
|
2870
|
+
const content = fs.readFileSync(call.file, 'utf-8');
|
|
2871
|
+
tree = safeParse(parser, content);
|
|
2872
|
+
if (!tree) return { args: null, argCount: 0 };
|
|
2873
|
+
if (!this._treeCache) this._treeCache = new Map();
|
|
2874
|
+
this._treeCache.set(call.file, tree);
|
|
2875
|
+
}
|
|
2847
2876
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2877
|
+
// Call node types vary by language
|
|
2878
|
+
const callTypes = new Set(['call_expression', 'call', 'method_invocation']);
|
|
2879
|
+
const targetRow = call.line - 1; // tree-sitter is 0-indexed
|
|
2850
2880
|
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
argCount:
|
|
2854
|
-
hasSpread: args.some(a => a.startsWith('...')),
|
|
2855
|
-
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a))
|
|
2856
|
-
};
|
|
2857
|
-
}
|
|
2881
|
+
// Find the call expression at the target line matching funcName
|
|
2882
|
+
const callNode = this._findCallNode(tree.rootNode, callTypes, targetRow, funcName);
|
|
2883
|
+
if (!callNode) return { args: null, argCount: 0 };
|
|
2858
2884
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
*/
|
|
2862
|
-
parseArguments(argsStr) {
|
|
2863
|
-
const args = [];
|
|
2864
|
-
let current = '';
|
|
2865
|
-
let depth = 0;
|
|
2866
|
-
let inString = false;
|
|
2867
|
-
let stringChar = '';
|
|
2868
|
-
|
|
2869
|
-
for (let i = 0; i < argsStr.length; i++) {
|
|
2870
|
-
const ch = argsStr[i];
|
|
2871
|
-
|
|
2872
|
-
if (inString) {
|
|
2873
|
-
current += ch;
|
|
2874
|
-
if (ch === stringChar && argsStr[i - 1] !== '\\') {
|
|
2875
|
-
inString = false;
|
|
2876
|
-
}
|
|
2877
|
-
continue;
|
|
2878
|
-
}
|
|
2885
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
2886
|
+
if (!argsNode) return { args: [], argCount: 0 };
|
|
2879
2887
|
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
current += ch;
|
|
2884
|
-
continue;
|
|
2888
|
+
const args = [];
|
|
2889
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2890
|
+
args.push(argsNode.namedChild(i).text.trim());
|
|
2885
2891
|
}
|
|
2886
2892
|
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2893
|
+
return {
|
|
2894
|
+
args,
|
|
2895
|
+
argCount: args.length,
|
|
2896
|
+
hasSpread: args.some(a => a.startsWith('...')),
|
|
2897
|
+
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a))
|
|
2898
|
+
};
|
|
2899
|
+
} catch (e) {
|
|
2900
|
+
return { args: null, argCount: 0 };
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2892
2903
|
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2904
|
+
/**
|
|
2905
|
+
* Find a call expression node at the target line matching funcName
|
|
2906
|
+
*/
|
|
2907
|
+
_findCallNode(node, callTypes, targetRow, funcName) {
|
|
2908
|
+
if (node.startPosition.row > targetRow || node.endPosition.row < targetRow) {
|
|
2909
|
+
return null; // Skip nodes that don't contain the target line
|
|
2910
|
+
}
|
|
2898
2911
|
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2912
|
+
if (callTypes.has(node.type) && node.startPosition.row === targetRow) {
|
|
2913
|
+
// Check if this call is for our target function
|
|
2914
|
+
const funcNode = node.childForFieldName('function') ||
|
|
2915
|
+
node.childForFieldName('name'); // Java method_invocation uses 'name'
|
|
2916
|
+
if (funcNode) {
|
|
2917
|
+
const funcText = funcNode.type === 'member_expression' || funcNode.type === 'selector_expression' || funcNode.type === 'field_expression' || funcNode.type === 'attribute'
|
|
2918
|
+
? (funcNode.childForFieldName('property') || funcNode.childForFieldName('field') || funcNode.childForFieldName('attribute') || funcNode.namedChild(funcNode.namedChildCount - 1))?.text
|
|
2919
|
+
: funcNode.text;
|
|
2920
|
+
if (funcText === funcName) return node;
|
|
2903
2921
|
}
|
|
2904
|
-
|
|
2905
|
-
current += ch;
|
|
2906
2922
|
}
|
|
2907
2923
|
|
|
2908
|
-
|
|
2909
|
-
|
|
2924
|
+
// Recurse into children
|
|
2925
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2926
|
+
const result = this._findCallNode(node.child(i), callTypes, targetRow, funcName);
|
|
2927
|
+
if (result) return result;
|
|
2910
2928
|
}
|
|
2929
|
+
return null;
|
|
2930
|
+
}
|
|
2911
2931
|
|
|
2912
|
-
|
|
2932
|
+
/**
|
|
2933
|
+
* Clear the AST tree cache (call after batch operations)
|
|
2934
|
+
*/
|
|
2935
|
+
_clearTreeCache() {
|
|
2936
|
+
this._treeCache = null;
|
|
2913
2937
|
}
|
|
2914
2938
|
|
|
2915
2939
|
/**
|
|
@@ -3317,6 +3341,7 @@ class ProjectIndex {
|
|
|
3317
3341
|
|
|
3318
3342
|
const cacheData = {
|
|
3319
3343
|
version: 4, // v4: className, memberType, isMethod for all languages
|
|
3344
|
+
ucnVersion: UCN_VERSION, // Invalidate cache when UCN is updated
|
|
3320
3345
|
root: this.root,
|
|
3321
3346
|
buildTime: this.buildTime,
|
|
3322
3347
|
timestamp: Date.now(),
|
|
@@ -3356,6 +3381,11 @@ class ProjectIndex {
|
|
|
3356
3381
|
return false;
|
|
3357
3382
|
}
|
|
3358
3383
|
|
|
3384
|
+
// Invalidate cache when UCN version changes (logic may have changed)
|
|
3385
|
+
if (cacheData.ucnVersion !== UCN_VERSION) {
|
|
3386
|
+
return false;
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3359
3389
|
// Validate cache structure has required fields
|
|
3360
3390
|
if (!Array.isArray(cacheData.files) ||
|
|
3361
3391
|
!Array.isArray(cacheData.symbols) ||
|