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.
@@ -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 - Semantic Code Navigation
8
+ # UCN - Universal Code Navigator
9
9
 
10
- ## When to Use
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
- UCN parses code into ASTs, so it understands structure - not just text patterns.
12
+ ## When to Use vs Skip
13
13
 
14
- **Use UCN when:**
15
- - You need to understand relationships (callers, callees, dependencies)
16
- - You're about to modify code and need to know the impact
17
- - You want to extract a specific function without reading the whole file
18
- - Codebase is 1000+ LOC (indexing overhead pays off)
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, TODOs, literals)
22
- - Codebase is tiny (< 500 LOC) - just read the files
23
- - Language not supported (C, Ruby, PHP, etc.)
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
- ## Commands
26
+ ## Command Format
26
27
 
27
- ### Understanding Code
28
- ```bash
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
- ### Finding Code
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 find <name> # Find definitions
40
- ucn usages <name> # All usages by type
41
- ucn toc # Table of contents
42
- ucn tests <name> # Find tests for a function
43
- ucn deadcode # Find unused functions/classes
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
- ### Extracting Code
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 fn <name> # Extract function
49
- ucn fn <name> --file routes # Disambiguate by path
50
- ucn class <name> # Extract class
114
+ ucn about handleRequest
51
115
  ```
52
116
 
53
- ### Dependencies
117
+ **Before modifying a function:**
54
118
  ```bash
55
- ucn imports <file> # What this file imports
56
- ucn exporters <file> # Who imports this file
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
- ## Flags
123
+ **Extract one function from a large file:**
124
+ ```bash
125
+ ucn fn handleRequest --file=api # Disambiguate by file path
126
+ ```
62
127
 
63
- - `--file <pattern>` - Filter by file path
64
- - `--exclude=test,mock` - Exclude files
65
- - `--depth=N` - Tree depth
66
- - `--context=N` - Lines of context around matches
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
- ## More Info
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 --help # Full command reference
141
+ ucn --interactive
77
142
  ```
package/README.md CHANGED
@@ -1,38 +1,140 @@
1
1
  # UCN - Universal Code Navigator
2
2
 
3
- When working with large codebases or even vibe coding all into a single large file, AI agents often need to read entire files just to understand a single function. This eats up the context rather fast, so the intent is to keep the agents perform better with their context clean than rather cluttered.
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
- ## Example
5
+ ## Examples
6
6
 
7
- A 2000-line file and need to understand `handleRequest`:
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
- ```bash
10
- $ ucn fn handleRequest
11
- src/api/routes.js:145
12
- [145-162] handleRequest(req, res)
13
- ────────────────────────────────────────────────────────
14
- function handleRequest(req, res) {
15
- const validated = validateInput(req.body);
16
- const result = processData(validated);
17
- return sendResponse(res, result);
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
- $ ucn context handleRequest
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
- CALLEES (3):
27
- validateInput - src/utils/validation.js:12
28
- processData - src/services/data.js:89
29
- sendResponse - src/utils/response.js:34
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
- 18 lines of context instead of 2000.
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
- UNDERSTAND CODE
85
- about <name> Full picture (definition, callers, callees, tests, code)
86
- context <name> Who calls this + what it calls
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
- // Fall through to regex-based detection
532
+ // AST parsing failed usages will be empty, only definitions shown
533
533
  }
534
534
  }
535
535
 
536
- // Fallback to regex-based detection (for unsupported languages)
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
@@ -107,8 +107,7 @@ const TEST_PATTERNS = {
107
107
  ],
108
108
  rust: [
109
109
  /.*_test\.rs$/,
110
- /\/tests\//,
111
- /mod tests/
110
+ /\/tests\//
112
111
  ]
113
112
  };
114
113
 
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
- // Fall through to regex-based extraction
32
+ // AST parsing failed
77
33
  }
78
34
  }
79
35
 
80
- // Fallback to regex-based extraction (deprecated)
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
- // Fall through to regex-based extraction
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
- // If no __all__, look for public names
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
- const keywords = new Set([
1242
- 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break',
1243
- 'continue', 'return', 'function', 'class', 'const', 'let', 'var',
1244
- 'new', 'this', 'super', 'import', 'export', 'default', 'from',
1245
- 'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
1246
- 'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'with',
1247
- 'def', 'print', 'range', 'len', 'str', 'int', 'float', 'list',
1248
- 'dict', 'set', 'tuple', 'True', 'False', 'None', 'self', 'cls',
1249
- 'func', 'type', 'struct', 'interface', 'package', 'make', 'append',
1250
- 'fn', 'impl', 'pub', 'mod', 'use', 'crate', 'self', 'super',
1251
- 'match', 'loop', 'unsafe', 'move', 'ref', 'mut', 'where'
1252
- ]);
1253
- return keywords.has(name);
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
- const content = call.content;
2860
+ try {
2861
+ const language = detectLanguage(call.file);
2862
+ if (!language) return { args: null, argCount: 0 };
2836
2863
 
2837
- // Extract arguments from the call
2838
- const callMatch = new RegExp('\\b' + escapeRegExp(funcName) + '\\s*\\(([^)]*)\\)').exec(content);
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
- const argsStr = callMatch[1].trim();
2844
- if (!argsStr) {
2845
- return { args: [], argCount: 0 };
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
- // Simple arg parsing (doesn't handle nested parens/strings perfectly but good enough)
2849
- const args = this.parseArguments(argsStr);
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
- return {
2852
- args,
2853
- argCount: args.length,
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
- * Parse function call arguments (simple version)
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
- if (ch === '"' || ch === "'" || ch === '`') {
2881
- inString = true;
2882
- stringChar = ch;
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
- if (ch === '(' || ch === '[' || ch === '{') {
2888
- depth++;
2889
- current += ch;
2890
- continue;
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
- if (ch === ')' || ch === ']' || ch === '}') {
2894
- depth--;
2895
- current += ch;
2896
- continue;
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
- if (ch === ',' && depth === 0) {
2900
- args.push(current.trim());
2901
- current = '';
2902
- continue;
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
- if (current.trim()) {
2909
- args.push(current.trim());
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
- return args;
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) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "3.1.7",
3
+ "version": "3.2.0",
4
4
  "description": "Code navigation built by AI, for AI. Reduces context usage when working with large codebases.",
5
5
  "main": "index.js",
6
6
  "bin": {