ucn 3.1.8 → 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 +120 -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,12 +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
17
|
// Read UCN version for cache invalidation
|
|
18
18
|
const UCN_VERSION = require('../package.json').version;
|
|
19
19
|
|
|
20
|
+
// Lazy-initialized per-language keyword sets (populated on first isKeyword call)
|
|
21
|
+
let LANGUAGE_KEYWORDS = null;
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* Escape special regex characters
|
|
22
25
|
*/
|
|
@@ -416,25 +419,6 @@ class ProjectIndex {
|
|
|
416
419
|
return withCounts;
|
|
417
420
|
}
|
|
418
421
|
|
|
419
|
-
/**
|
|
420
|
-
* Count usages of a symbol across the codebase
|
|
421
|
-
*/
|
|
422
|
-
countUsages(name) {
|
|
423
|
-
let count = 0;
|
|
424
|
-
const regex = new RegExp('\\b' + escapeRegExp(name) + '\\b', 'g');
|
|
425
|
-
|
|
426
|
-
for (const [filePath, fileEntry] of this.files) {
|
|
427
|
-
try {
|
|
428
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
429
|
-
const matches = content.match(regex);
|
|
430
|
-
if (matches) count += matches.length;
|
|
431
|
-
} catch (e) {
|
|
432
|
-
// Skip unreadable files
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return count;
|
|
437
|
-
}
|
|
438
422
|
|
|
439
423
|
/**
|
|
440
424
|
* Count usages of a specific symbol (not just by name)
|
|
@@ -1007,7 +991,7 @@ class ProjectIndex {
|
|
|
1007
991
|
}
|
|
1008
992
|
|
|
1009
993
|
// Skip keywords and built-ins
|
|
1010
|
-
if (this.isKeyword(call.name)) continue;
|
|
994
|
+
if (this.isKeyword(call.name, language)) continue;
|
|
1011
995
|
|
|
1012
996
|
callees.set(call.name, (callees.get(call.name) || 0) + 1);
|
|
1013
997
|
}
|
|
@@ -1240,20 +1224,53 @@ class ProjectIndex {
|
|
|
1240
1224
|
/**
|
|
1241
1225
|
* Check if a name is a language keyword
|
|
1242
1226
|
*/
|
|
1243
|
-
isKeyword(name) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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;
|
|
1257
1274
|
}
|
|
1258
1275
|
|
|
1259
1276
|
/**
|
|
@@ -2264,6 +2281,7 @@ class ProjectIndex {
|
|
|
2264
2281
|
...analysis
|
|
2265
2282
|
};
|
|
2266
2283
|
});
|
|
2284
|
+
this._clearTreeCache();
|
|
2267
2285
|
|
|
2268
2286
|
// Group by file if requested
|
|
2269
2287
|
const byFile = new Map();
|
|
@@ -2809,6 +2827,7 @@ class ProjectIndex {
|
|
|
2809
2827
|
}
|
|
2810
2828
|
}
|
|
2811
2829
|
}
|
|
2830
|
+
this._clearTreeCache();
|
|
2812
2831
|
|
|
2813
2832
|
return {
|
|
2814
2833
|
found: true,
|
|
@@ -2832,87 +2851,89 @@ class ProjectIndex {
|
|
|
2832
2851
|
}
|
|
2833
2852
|
|
|
2834
2853
|
/**
|
|
2835
|
-
* 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 }
|
|
2836
2858
|
*/
|
|
2837
2859
|
analyzeCallSite(call, funcName) {
|
|
2838
|
-
|
|
2860
|
+
try {
|
|
2861
|
+
const language = detectLanguage(call.file);
|
|
2862
|
+
if (!language) return { args: null, argCount: 0 };
|
|
2839
2863
|
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
if (!callMatch) {
|
|
2843
|
-
return { args: null, argCount: 0 };
|
|
2844
|
-
}
|
|
2864
|
+
const parser = getParser(language);
|
|
2865
|
+
if (!parser) return { args: null, argCount: 0 };
|
|
2845
2866
|
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
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
|
+
}
|
|
2850
2876
|
|
|
2851
|
-
|
|
2852
|
-
|
|
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
|
|
2853
2880
|
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
argCount:
|
|
2857
|
-
hasSpread: args.some(a => a.startsWith('...')),
|
|
2858
|
-
hasVariable: args.some(a => /^[a-zA-Z_]\w*$/.test(a))
|
|
2859
|
-
};
|
|
2860
|
-
}
|
|
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 };
|
|
2861
2884
|
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
*/
|
|
2865
|
-
parseArguments(argsStr) {
|
|
2866
|
-
const args = [];
|
|
2867
|
-
let current = '';
|
|
2868
|
-
let depth = 0;
|
|
2869
|
-
let inString = false;
|
|
2870
|
-
let stringChar = '';
|
|
2871
|
-
|
|
2872
|
-
for (let i = 0; i < argsStr.length; i++) {
|
|
2873
|
-
const ch = argsStr[i];
|
|
2874
|
-
|
|
2875
|
-
if (inString) {
|
|
2876
|
-
current += ch;
|
|
2877
|
-
if (ch === stringChar && argsStr[i - 1] !== '\\') {
|
|
2878
|
-
inString = false;
|
|
2879
|
-
}
|
|
2880
|
-
continue;
|
|
2881
|
-
}
|
|
2885
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
2886
|
+
if (!argsNode) return { args: [], argCount: 0 };
|
|
2882
2887
|
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
current += ch;
|
|
2887
|
-
continue;
|
|
2888
|
+
const args = [];
|
|
2889
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
2890
|
+
args.push(argsNode.namedChild(i).text.trim());
|
|
2888
2891
|
}
|
|
2889
2892
|
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
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
|
+
}
|
|
2895
2903
|
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
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
|
+
}
|
|
2901
2911
|
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
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;
|
|
2906
2921
|
}
|
|
2907
|
-
|
|
2908
|
-
current += ch;
|
|
2909
2922
|
}
|
|
2910
2923
|
|
|
2911
|
-
|
|
2912
|
-
|
|
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;
|
|
2913
2928
|
}
|
|
2929
|
+
return null;
|
|
2930
|
+
}
|
|
2914
2931
|
|
|
2915
|
-
|
|
2932
|
+
/**
|
|
2933
|
+
* Clear the AST tree cache (call after batch operations)
|
|
2934
|
+
*/
|
|
2935
|
+
_clearTreeCache() {
|
|
2936
|
+
this._treeCache = null;
|
|
2916
2937
|
}
|
|
2917
2938
|
|
|
2918
2939
|
/**
|