ucn 5.3.2 → 5.3.3
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 +9 -1
- package/core/cache.js +1 -1
- package/core/callers.js +192 -5
- package/core/project.js +7 -2
- package/core/search.js +1 -1
- package/core/shared.js +1 -1
- package/languages/java.js +24 -4
- package/languages/rust.js +8 -0
- package/package.json +1 -1
|
@@ -161,7 +161,15 @@ Treat `deadcode` as a candidate generator. Before deletion, inspect `usages`, `i
|
|
|
161
161
|
(JSX children, HTML markup and attributes) in an `OTHER TEXT` section unless
|
|
162
162
|
`--code-only` is set, so it lists every line the `ACCOUNT` counts. This is a
|
|
163
163
|
literal-name inventory, not exact target binding. Identifier boundaries are
|
|
164
|
-
Unicode-aware
|
|
164
|
+
Unicode-aware and match `grep -w`: `hit` never matches inside `hitΔ`, while
|
|
165
|
+
`$` is a boundary (`buy${...}`, `$fail`, `ws$close()` all count).
|
|
166
|
+
|
|
167
|
+
Aliased and qualified calls resolve in every language: Rust `use m::f as g; g()`
|
|
168
|
+
is a caller of `f` (listed as a beyond-text caller, since the line holds no
|
|
169
|
+
target token); Java `pkg.Type.method()` and C# `Ns.Type.Method()` /
|
|
170
|
+
`using T = Ns.Type; T.Method()` pick the type the qualifier names when several
|
|
171
|
+
same-name types exist. A qualifier the resolver cannot place stays visible as
|
|
172
|
+
`method-ambiguous`, never confirmed by first-definition order.
|
|
165
173
|
|
|
166
174
|
`endpoints` recognizes client receivers by evidence (a receiver typed to an
|
|
167
175
|
HTTP client class, or bound to a pytest fixture that constructs one), not only
|
package/core/cache.js
CHANGED
|
@@ -699,7 +699,7 @@ function clearAllCaches() {
|
|
|
699
699
|
// v212 (fix #342): extendsGraph/extendedByGraph no longer persisted (rebuilt on load).
|
|
700
700
|
// v213 (fix #348): Python from-import records carry per-name `renames` so
|
|
701
701
|
// import bindings pair each alias with its own module.
|
|
702
|
-
const CACHE_FORMAT_VERSION =
|
|
702
|
+
const CACHE_FORMAT_VERSION = 214;
|
|
703
703
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
704
704
|
|
|
705
705
|
/**
|
package/core/callers.js
CHANGED
|
@@ -10,7 +10,8 @@ const path = require('path');
|
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
const { detectLanguage, getParser, getLanguageAdapter, langTraits } = require('../languages');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
|
-
const { NON_CALLABLE_TYPES, isOverrideMarked, codeUnitCompare, isTestPath } = require('./shared');
|
|
13
|
+
const { NON_CALLABLE_TYPES, isOverrideMarked, codeUnitCompare, isTestPath, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
14
|
+
const { _resolveJavaPackageImport } = require('./graph-build');
|
|
14
15
|
const { scoreEdge, tierForResolution, TIER } = require('./confidence');
|
|
15
16
|
const { findGoModule, resolveRustImport } = require('./imports');
|
|
16
17
|
|
|
@@ -634,6 +635,17 @@ function findCallers(index, name, options = {}) {
|
|
|
634
635
|
langTraits(fileEntry.language)?.typeSystem === 'structural';
|
|
635
636
|
|
|
636
637
|
for (let call of calls) {
|
|
638
|
+
// fix #353: C# `Beta.Helper.Widget()` — the parser records a
|
|
639
|
+
// field hop rooted at `this` (Beta is no local). When the
|
|
640
|
+
// prefix names a project NAMESPACE that declares the last
|
|
641
|
+
// segment as a type, the receiver is that type, namespace-
|
|
642
|
+
// qualified; the hop shape would only ever fail the field
|
|
643
|
+
// walk and route method-ambiguous.
|
|
644
|
+
if (fileEntry.language === 'csharp') {
|
|
645
|
+
const rewritten = _csharpNamespaceQualifiedReceiver(index, call,
|
|
646
|
+
index.findEnclosingFunction(filePath, call.line, true));
|
|
647
|
+
if (rewritten) call = rewritten;
|
|
648
|
+
}
|
|
637
649
|
// Skip if not matching our target name (also check alias resolution)
|
|
638
650
|
let calledAs = null; // surface name when matched via an import/export rename
|
|
639
651
|
const typeQualifierReference = targetIsTypeQuery &&
|
|
@@ -2808,6 +2820,16 @@ function findCallers(index, name, options = {}) {
|
|
|
2808
2820
|
const paired = nameBindings.filter(b => b.alias === call.name);
|
|
2809
2821
|
if (paired.length > 0) nameBindings = paired;
|
|
2810
2822
|
}
|
|
2823
|
+
// Scope-granular import bindings (fix #352): a
|
|
2824
|
+
// function-local `from x import name` binds the name for
|
|
2825
|
+
// ITS function only. Three such imports in one file used
|
|
2826
|
+
// to make three file-level bindings, so a bare call in any
|
|
2827
|
+
// of the functions scope-matched every pin (investment
|
|
2828
|
+
// run_validation: 6 defs, each claiming the others'
|
|
2829
|
+
// sites). The nearest enclosing binder owns the name; a
|
|
2830
|
+
// binding inside a function that does not enclose the
|
|
2831
|
+
// call is out of scope (#215 discipline, line-granular).
|
|
2832
|
+
nameBindings = _scopeImportBindings(fileEntry, nameBindings, call.line);
|
|
2811
2833
|
const tFiles = new Set(targetDefs.map(d => d.file).filter(Boolean));
|
|
2812
2834
|
// fix #215 (rich-measured: 225 builtin `print(...)` calls
|
|
2813
2835
|
// confirmed against rich's def via file-level import edges):
|
|
@@ -4075,12 +4097,17 @@ function findCallers(index, name, options = {}) {
|
|
|
4075
4097
|
if (call.isPathCall && receiverName) {
|
|
4076
4098
|
receiverName = String(receiverName).split('::').pop();
|
|
4077
4099
|
}
|
|
4100
|
+
let aliasResolvedFile = null;
|
|
4078
4101
|
if (receiverName && !tTypes.has(receiverName)) {
|
|
4079
4102
|
for (const im of (fileEntry.importBindings || [])) {
|
|
4080
4103
|
if (im.name !== receiverName) continue;
|
|
4081
|
-
|
|
4104
|
+
// fix #353: C# `using BH = Beta.Helper` (and Java
|
|
4105
|
+
// dotted paths) split on `.`; Rust paths on `::`.
|
|
4106
|
+
const orig = String(im.module || '').split(/::|\./).pop();
|
|
4082
4107
|
if (orig && orig !== receiverName && tTypes.has(orig)) {
|
|
4083
4108
|
receiverName = orig;
|
|
4109
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[im.module];
|
|
4110
|
+
if (rel) aliasResolvedFile = path.join(index.root, rel);
|
|
4084
4111
|
break;
|
|
4085
4112
|
}
|
|
4086
4113
|
}
|
|
@@ -4108,7 +4135,14 @@ function findCallers(index, name, options = {}) {
|
|
|
4108
4135
|
// name fallback above handles multi-definition names; this
|
|
4109
4136
|
// covers single-definition targets that skip it.
|
|
4110
4137
|
if (typeQualifiedReceiver) {
|
|
4111
|
-
|
|
4138
|
+
// fix #353: an alias binding that RESOLVED to a file is
|
|
4139
|
+
// the type's identity (`using BH = Beta.Helper`); a
|
|
4140
|
+
// parser-recorded qualifier (`beta.Helper`, `Beta.Helper`)
|
|
4141
|
+
// is a namespace/package hint for the resolver.
|
|
4142
|
+
const identity = aliasResolvedFile
|
|
4143
|
+
? (targetDefs2.some(d => d.file === aliasResolvedFile) ? 'target' : 'other')
|
|
4144
|
+
: _resolveReceiverTypeIdentity(index, filePath, receiverName, targetDefs2, call.line,
|
|
4145
|
+
call.receiverIsTypeQualified ? call.receiverTypeQualifier : undefined);
|
|
4112
4146
|
if (identity === 'other') {
|
|
4113
4147
|
recordExcluded(filePath, call.line, 'path-type-mismatch');
|
|
4114
4148
|
continue;
|
|
@@ -5344,6 +5378,12 @@ function findCallees(index, definition, options = {}) {
|
|
|
5344
5378
|
for (let call of calls) {
|
|
5345
5379
|
siteOrdinal++;
|
|
5346
5380
|
const siteId = siteOrdinal;
|
|
5381
|
+
if (language === 'csharp') {
|
|
5382
|
+
// fix #353: `Beta.Helper.Widget()` — namespace-qualified type
|
|
5383
|
+
// receiver (see the findCallers twin).
|
|
5384
|
+
const rewritten = _csharpNamespaceQualifiedReceiver(index, call, def);
|
|
5385
|
+
if (rewritten) call = rewritten;
|
|
5386
|
+
}
|
|
5347
5387
|
if (language === 'go' && call.isMethod &&
|
|
5348
5388
|
!call.receiverType && call.receiverIndexField) {
|
|
5349
5389
|
const indexedType = _goIndexedReceiverType(index, def.file, call);
|
|
@@ -9853,6 +9893,42 @@ function _projectTopLevelNames(index) {
|
|
|
9853
9893
|
return names;
|
|
9854
9894
|
}
|
|
9855
9895
|
|
|
9896
|
+
/**
|
|
9897
|
+
* Fix #352: restrict import bindings of a name to those in scope at a call
|
|
9898
|
+
* line. A binding whose import line sits inside a function body is local to
|
|
9899
|
+
* that function (Python function-body imports, JS function-scoped require);
|
|
9900
|
+
* the innermost enclosing binder wins, bindings in non-enclosing functions
|
|
9901
|
+
* are dropped, module-level bindings survive only when no enclosing function
|
|
9902
|
+
* binds the name. Bindings without a line (older records) are kept as-is.
|
|
9903
|
+
*/
|
|
9904
|
+
function _scopeImportBindings(fileEntry, bindings, callLine) {
|
|
9905
|
+
if (!bindings || bindings.length < 2 || callLine == null) return bindings;
|
|
9906
|
+
if (!bindings.some(b => b.line != null && b.deferred)) return bindings;
|
|
9907
|
+
const scopes = (fileEntry.symbols || []).filter(s =>
|
|
9908
|
+
s.startLine != null && s.endLine != null && s.endLine > s.startLine &&
|
|
9909
|
+
CALLABLE_SYMBOL_KINDS.has(s.type));
|
|
9910
|
+
const innermost = line => {
|
|
9911
|
+
let best = null;
|
|
9912
|
+
for (const s of scopes) {
|
|
9913
|
+
if (line < s.startLine || line > s.endLine) continue;
|
|
9914
|
+
if (!best || (s.endLine - s.startLine) < (best.endLine - best.startLine)) best = s;
|
|
9915
|
+
}
|
|
9916
|
+
return best;
|
|
9917
|
+
};
|
|
9918
|
+
const local = [];
|
|
9919
|
+
const moduleLevel = [];
|
|
9920
|
+
for (const b of bindings) {
|
|
9921
|
+
if (b.line == null) { moduleLevel.push(b); continue; }
|
|
9922
|
+
const scope = innermost(b.line);
|
|
9923
|
+
if (!scope) { moduleLevel.push(b); continue; }
|
|
9924
|
+
if (callLine < scope.startLine || callLine > scope.endLine) continue;
|
|
9925
|
+
local.push({ b, size: scope.endLine - scope.startLine });
|
|
9926
|
+
}
|
|
9927
|
+
if (local.length === 0) return moduleLevel;
|
|
9928
|
+
const nearest = Math.min(...local.map(l => l.size));
|
|
9929
|
+
return local.filter(l => l.size === nearest).map(l => l.b);
|
|
9930
|
+
}
|
|
9931
|
+
|
|
9856
9932
|
/**
|
|
9857
9933
|
* Is an UNRESOLVED module specifier a resolver gap rather than externality
|
|
9858
9934
|
* evidence? (fix #337b) Relative specifiers and first segments naming a
|
|
@@ -9980,6 +10056,32 @@ function _isGenericParamReceiverType(index, filePath, line, typeName) {
|
|
|
9980
10056
|
return _isEnclosingGenericParam(index, filePath, line, typeName);
|
|
9981
10057
|
}
|
|
9982
10058
|
|
|
10059
|
+
/**
|
|
10060
|
+
* fix #353: C# namespace-qualified type receivers. `Beta.Helper.Widget()` is
|
|
10061
|
+
* recorded by the parser as a this-rooted field hop (Beta is not a local);
|
|
10062
|
+
* when the dotted prefix names a project namespace (exactly, or relative to
|
|
10063
|
+
* the call's own namespace) that declares the last segment as a type, the
|
|
10064
|
+
* call is a type-qualified static call on that type. Returns a rewritten
|
|
10065
|
+
* record or null (unknown prefixes keep the parser's shape).
|
|
10066
|
+
*/
|
|
10067
|
+
function _csharpNamespaceQualifiedReceiver(index, call, enclosing) {
|
|
10068
|
+
if (!call.isMethod || call.receiverType || !Array.isArray(call.receiverFields) ||
|
|
10069
|
+
call.receiverFields.length < 2 || call.receiverRoot !== 'this') return null;
|
|
10070
|
+
const typeName = call.receiverFields[call.receiverFields.length - 1];
|
|
10071
|
+
if (!/^[A-Z]/.test(typeName)) return null;
|
|
10072
|
+
const prefix = call.receiverFields.slice(0, -1).join('.');
|
|
10073
|
+
const typeDefs = (index.symbols.get(typeName) || []).filter(d =>
|
|
10074
|
+
IDENTITY_TYPE_KINDS.has(d.type) && d.namespace);
|
|
10075
|
+
if (typeDefs.length === 0) return null;
|
|
10076
|
+
const enclosingNs = enclosing?.namespace || null;
|
|
10077
|
+
const candidates = enclosingNs ? [prefix, `${enclosingNs}.${prefix}`] : [prefix];
|
|
10078
|
+
const ns = candidates.find(c => typeDefs.some(d => d.namespace === c));
|
|
10079
|
+
if (!ns) return null;
|
|
10080
|
+
const { receiverRoot, receiverField, receiverFields, receiverRootType, receiverRootNamespace, ...rest } = call;
|
|
10081
|
+
void receiverRoot; void receiverField; void receiverFields; void receiverRootType; void receiverRootNamespace;
|
|
10082
|
+
return { ...rest, receiver: typeName, receiverIsTypeQualified: true, receiverTypeQualifier: ns };
|
|
10083
|
+
}
|
|
10084
|
+
|
|
9983
10085
|
/**
|
|
9984
10086
|
* Java same-package check across Maven/Gradle source roots (fix #246):
|
|
9985
10087
|
* src/main/java/<pkg> and src/test/java/<pkg> hold the SAME package —
|
|
@@ -10019,6 +10121,16 @@ function _resolveReceiverTypeIdentity(index, filePath, knownType, targetDefs, li
|
|
|
10019
10121
|
// cannot resolve it. That is not exclusion evidence.
|
|
10020
10122
|
return 'unknown';
|
|
10021
10123
|
}
|
|
10124
|
+
if (language === 'java' && namespaceHint && /^[a-z_]/.test(namespaceHint)) {
|
|
10125
|
+
// fix #353: a lowercase dotted qualifier is a PACKAGE
|
|
10126
|
+
// (`beta.Helper.widget()`); the package + type name resolve to one
|
|
10127
|
+
// file exactly like an import of `beta.Helper` would. Unresolvable
|
|
10128
|
+
// packages (external, resolver gap) are never exclusion evidence.
|
|
10129
|
+
const resolved = _resolveJavaPackageImport(index, `${namespaceHint}.${knownType}`, null);
|
|
10130
|
+
if (!resolved) return 'unknown';
|
|
10131
|
+
return targetDefs.some(d => d.className === knownType && d.file === resolved)
|
|
10132
|
+
? 'target' : 'other';
|
|
10133
|
+
}
|
|
10022
10134
|
if (language === 'java' && line == null) {
|
|
10023
10135
|
// Return-flow annotations are interpreted in the PRODUCER definition's
|
|
10024
10136
|
// file. That origin is stronger than same-package lookup and is
|
|
@@ -10965,6 +11077,23 @@ function _calleeStructuralBindingRoute(index, fileEntry, call, language, binding
|
|
|
10965
11077
|
let sawProjectish = false;
|
|
10966
11078
|
let sawUnknown = false;
|
|
10967
11079
|
for (const binding of bindings) {
|
|
11080
|
+
// fix #353 (Rust): `use alpha::widget as renamed; renamed()` — the
|
|
11081
|
+
// binding's module is the ITEM path; the item is its last segment
|
|
11082
|
+
// and the owning module file resolves through the Rust resolver.
|
|
11083
|
+
if (language === 'rust') {
|
|
11084
|
+
const segs = String(binding.module || '').split('::').filter(Boolean);
|
|
11085
|
+
const item = segs[segs.length - 1];
|
|
11086
|
+
const owners = _rustBindingResolvedFiles(index, fileEntry, fileEntry.path, binding);
|
|
11087
|
+
if (item && owners.size > 0) {
|
|
11088
|
+
sawProjectish = true;
|
|
11089
|
+
for (const moduleFile of owners) {
|
|
11090
|
+
const routed = _calleeExportDefinitions(index, moduleFile, item, language, call, {});
|
|
11091
|
+
for (const d of routed.matches) matches.set(`${d.file}:${d.startLine}`, d);
|
|
11092
|
+
if (routed.unknown) sawUnknown = true;
|
|
11093
|
+
}
|
|
11094
|
+
continue;
|
|
11095
|
+
}
|
|
11096
|
+
}
|
|
10968
11097
|
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[binding.module];
|
|
10969
11098
|
if (!rel) {
|
|
10970
11099
|
if (_unresolvedModuleIsGap(index, binding.module, binding)) {
|
|
@@ -11722,6 +11851,42 @@ function _nonCallableFieldMember(index, typeName, name, language) {
|
|
|
11722
11851
|
* (#215): the class defined in this file or a file binding of the name —
|
|
11723
11852
|
* an unbound capitalized receiver may be a parameter or local.
|
|
11724
11853
|
*/
|
|
11854
|
+
/**
|
|
11855
|
+
* fix #353: resolve the files that OWN a type-qualified static receiver from
|
|
11856
|
+
* the qualifier in the call (Java package / C# namespace), the file's alias
|
|
11857
|
+
* or name import binding of the receiver, or the caller's own namespace.
|
|
11858
|
+
* Returns a Set of absolute files, or null when nothing pins the owner.
|
|
11859
|
+
*/
|
|
11860
|
+
function _qualifiedStaticOwnerFiles(index, fileEntry, call, typeName, language, def) {
|
|
11861
|
+
const files = new Set();
|
|
11862
|
+
const typeDefs = (index.symbols.get(typeName) || []).filter(d =>
|
|
11863
|
+
IDENTITY_TYPE_KINDS.has(d.type) && d.file);
|
|
11864
|
+
const qual = call.receiverIsTypeQualified ? call.receiverTypeQualifier : null;
|
|
11865
|
+
if (qual) {
|
|
11866
|
+
if (language === 'java' && /^[a-z_]/.test(qual)) {
|
|
11867
|
+
const resolved = _resolveJavaPackageImport(index, `${qual}.${typeName}`, null);
|
|
11868
|
+
if (resolved) files.add(resolved);
|
|
11869
|
+
return files.size > 0 ? files : null;
|
|
11870
|
+
}
|
|
11871
|
+
if (language === 'csharp') {
|
|
11872
|
+
const enclosingNs = def?.namespace || null;
|
|
11873
|
+
const candidates = enclosingNs ? [qual, `${enclosingNs}.${qual}`] : [qual];
|
|
11874
|
+
for (const d of typeDefs) {
|
|
11875
|
+
if (candidates.includes(d.namespace)) files.add(d.file);
|
|
11876
|
+
}
|
|
11877
|
+
return files.size > 0 ? files : null;
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
for (const b of (fileEntry?.importBindings || [])) {
|
|
11881
|
+
const bindsReceiver = b.name === call.receiver || b.alias === call.receiver ||
|
|
11882
|
+
(b.name === typeName && !b.alias);
|
|
11883
|
+
if (!bindsReceiver) continue;
|
|
11884
|
+
const rel = fileEntry.moduleResolved && fileEntry.moduleResolved[b.module];
|
|
11885
|
+
if (rel) files.add(path.join(index.root, rel));
|
|
11886
|
+
}
|
|
11887
|
+
return files.size > 0 ? files : null;
|
|
11888
|
+
}
|
|
11889
|
+
|
|
11725
11890
|
/**
|
|
11726
11891
|
* Namespace/module-container resolution (fix #254, W8 BUG-4 — verify's
|
|
11727
11892
|
* BUG-BX rule brought into the engine, range-based): `Utils.slug()` where a
|
|
@@ -11835,7 +12000,8 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
11835
12000
|
if (typeDefs.length === 0) {
|
|
11836
12001
|
for (const im of (fileEntry?.importBindings || [])) {
|
|
11837
12002
|
if (im.name !== receiver) continue;
|
|
11838
|
-
|
|
12003
|
+
// fix #353: C# `using BH = Beta.Helper` splits on `.`
|
|
12004
|
+
const orig = String(im.module || '').split(/::|\./).pop();
|
|
11839
12005
|
if (orig && orig !== receiver && typeKindsOf(orig).length > 0) {
|
|
11840
12006
|
receiver = orig;
|
|
11841
12007
|
typeDefs = typeKindsOf(orig);
|
|
@@ -11877,9 +12043,30 @@ function _calleeTypeQualifiedReceiver(index, def, fileEntry, call, language) {
|
|
|
11877
12043
|
_normalizedAliasBase(index, d)));
|
|
11878
12044
|
if (bases.size === 1) candidateTypes.push(bases.values().next().value);
|
|
11879
12045
|
}
|
|
11880
|
-
const
|
|
12046
|
+
const allSymbols = index.symbols.get(call.name) || [];
|
|
11881
12047
|
const isCallable = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
|
|
11882
12048
|
(s.type === 'field' && s.fieldType && /^func\b/.test(s.fieldType));
|
|
12049
|
+
// fix #353: the qualifier that is right there in the call owns the type —
|
|
12050
|
+
// `beta.Helper.widget()` (package), `Beta.Helper.Widget()` (namespace),
|
|
12051
|
+
// `using BH = Beta.Helper; BH.Widget()` (alias binding), `import
|
|
12052
|
+
// beta.Helper;` (name binding). Same-name types in other packages/
|
|
12053
|
+
// namespaces leave the candidate set BEFORE member-group construction
|
|
12054
|
+
// (the group dedupes identical signatures, so the first same-name type
|
|
12055
|
+
// used to swallow the second). An unresolvable qualifier keeps them all.
|
|
12056
|
+
const ownerFiles = (language === 'java' || language === 'csharp')
|
|
12057
|
+
? _qualifiedStaticOwnerFiles(index, fileEntry, call, receiver, language, def)
|
|
12058
|
+
: null;
|
|
12059
|
+
// A package qualifier the resolver cannot place (`org.external.Helper`)
|
|
12060
|
+
// over a project type of the same name: unpinnable — visible, never
|
|
12061
|
+
// confirmed by first-definition selection (#206 discipline).
|
|
12062
|
+
if (language === 'java' && call.receiverIsTypeQualified &&
|
|
12063
|
+
call.receiverTypeQualifier && /^[a-z_]/.test(call.receiverTypeQualifier) &&
|
|
12064
|
+
!ownerFiles) {
|
|
12065
|
+
return { unverified: 'method-ambiguous' };
|
|
12066
|
+
}
|
|
12067
|
+
const symbols = ownerFiles
|
|
12068
|
+
? allSymbols.filter(s => !candidateTypes.includes(s.className) || ownerFiles.has(s.file))
|
|
12069
|
+
: allSymbols;
|
|
11883
12070
|
if (language === 'java' || language === 'csharp') {
|
|
11884
12071
|
// Class-qualified Java/C# calls see the compiler member group on the
|
|
11885
12072
|
// qualifier. The helper handles inherited slots and C# name hiding.
|
package/core/project.js
CHANGED
|
@@ -1946,7 +1946,7 @@ class ProjectIndex {
|
|
|
1946
1946
|
* the usage scan saw (and may have deliberately dropped), as opposed to
|
|
1947
1947
|
* JSX children, HTML markup, or other non-code text (fix #350).
|
|
1948
1948
|
*/
|
|
1949
|
-
isIdentifierAtPosition(content, lineNum, column, filePath) {
|
|
1949
|
+
isIdentifierAtPosition(content, lineNum, column, filePath, name) {
|
|
1950
1950
|
const language = detectLanguage(filePath, this.root);
|
|
1951
1951
|
if (!language) return false;
|
|
1952
1952
|
try {
|
|
@@ -1954,7 +1954,12 @@ class ProjectIndex {
|
|
|
1954
1954
|
safeParse(getParser(language), content);
|
|
1955
1955
|
if (!tree) return false;
|
|
1956
1956
|
const node = tree.rootNode.descendantForPosition({ row: lineNum - 1, column });
|
|
1957
|
-
|
|
1957
|
+
if (!node || !/identifier|^name$|^word$/.test(node.type)) return false;
|
|
1958
|
+
// fix #352: the identifier node must BE the name — a hyphenated JSX
|
|
1959
|
+
// attribute (`data-cell-state`) is one property_identifier whose
|
|
1960
|
+
// text merely contains `cell`; the ACCOUNT counts that line as
|
|
1961
|
+
// other-text, so usages must list it (the #350 equality).
|
|
1962
|
+
return node.text === name;
|
|
1958
1963
|
} catch (e) {
|
|
1959
1964
|
return false;
|
|
1960
1965
|
}
|
package/core/search.js
CHANGED
|
@@ -84,7 +84,7 @@ function appendTextComplements(index, {
|
|
|
84
84
|
// the AST scan classified and then deliberately dropped (Rust enum
|
|
85
85
|
// variants against a struct pin, #234) is code, not text: skip it.
|
|
86
86
|
if (!commentOrString &&
|
|
87
|
-
index.isIdentifierAtPosition(content, lineNum, match.index, filePath)) continue;
|
|
87
|
+
index.isIdentifierAtPosition(content, lineNum, match.index, filePath, name)) continue;
|
|
88
88
|
const usage = {
|
|
89
89
|
file: filePath,
|
|
90
90
|
relativePath: fileEntry.relativePath,
|
package/core/shared.js
CHANGED
|
@@ -126,7 +126,7 @@ function addTestExclusions(exclude) {
|
|
|
126
126
|
*/
|
|
127
127
|
function literalNameRegex(name, flags = '') {
|
|
128
128
|
return new RegExp(
|
|
129
|
-
`(?<![\\p{L}\\p{N}_
|
|
129
|
+
`(?<![\\p{L}\\p{N}_])${escapeRegExp(name)}(?![\\p{L}\\p{N}_])`,
|
|
130
130
|
flags.includes('u') ? flags : flags + 'u');
|
|
131
131
|
}
|
|
132
132
|
|
package/languages/java.js
CHANGED
|
@@ -1502,14 +1502,34 @@ function findCallsInCode(code, parser) {
|
|
|
1502
1502
|
const valueNode = receiverNode.childForFieldName('value');
|
|
1503
1503
|
if (valueNode?.type === 'identifier') castReceiverName = valueNode.text;
|
|
1504
1504
|
}
|
|
1505
|
-
|
|
1505
|
+
let receiver = castReceiverName ||
|
|
1506
1506
|
((receiverNode?.type === 'identifier' || receiverNode?.type === 'this')
|
|
1507
1507
|
? receiverNode.text : undefined);
|
|
1508
|
+
// fix #353: `beta.Helper.widget()` — a lowercase dotted root
|
|
1509
|
+
// under a capitalized member is a PACKAGE-qualified type
|
|
1510
|
+
// (Java packages are lowercase by convention, types
|
|
1511
|
+
// capitalized); the receiver is the type and the package is
|
|
1512
|
+
// its qualifier, so same-name static methods in two packages
|
|
1513
|
+
// resolve by the qualifier that is right there in the call.
|
|
1514
|
+
let packageQualifier;
|
|
1515
|
+
if (!receiver && receiverNode?.type === 'field_access') {
|
|
1516
|
+
const rootNode = receiverNode.childForFieldName('object');
|
|
1517
|
+
const fldNode = receiverNode.childForFieldName('field');
|
|
1518
|
+
const rootText = rootNode?.text || '';
|
|
1519
|
+
const rootHead = rootText.split('.')[0];
|
|
1520
|
+
if (fldNode?.type === 'identifier' && /^[A-Z]/.test(fldNode.text) &&
|
|
1521
|
+
/^[a-z_][\w]*(\.[a-z_][\w]*)*$/.test(rootText) &&
|
|
1522
|
+
!getReceiverType(rootHead) && !isDeclaredLocal(rootHead) &&
|
|
1523
|
+
!hasEnclosingField(node, rootHead)) {
|
|
1524
|
+
receiver = fldNode.text;
|
|
1525
|
+
packageQualifier = rootText;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1508
1528
|
const receiverType = castReceiverType ||
|
|
1509
1529
|
((receiver && receiver !== 'this') ? getReceiverType(receiver) : undefined);
|
|
1510
|
-
const receiverTypeQualifier =
|
|
1511
|
-
? getReceiverTypeQualifier(receiver) : undefined;
|
|
1512
|
-
const receiverIsTypeQualified = !!(receiverNode?.type === 'identifier' &&
|
|
1530
|
+
const receiverTypeQualifier = packageQualifier ||
|
|
1531
|
+
(!castReceiverType && receiver ? getReceiverTypeQualifier(receiver) : undefined);
|
|
1532
|
+
const receiverIsTypeQualified = !!((receiverNode?.type === 'identifier' || packageQualifier) &&
|
|
1513
1533
|
receiver && /^[A-Z]/.test(receiver) && !receiverType &&
|
|
1514
1534
|
!isDeclaredLocal(receiver) && !hasEnclosingField(node, receiver));
|
|
1515
1535
|
// fix #202: one-hop declared-field receivers —
|
package/languages/rust.js
CHANGED
|
@@ -3152,6 +3152,14 @@ function findImportsInCode(code, parser) {
|
|
|
3152
3152
|
if (pathNode && aliasNode) {
|
|
3153
3153
|
addLeaf(joinUsePath(prefix, pathNode.text), aliasNode.text,
|
|
3154
3154
|
'use', false, line);
|
|
3155
|
+
// fix #353: `use alpha::widget as renamed; renamed()` — the
|
|
3156
|
+
// alias pairing feeds findCallers' import-rename surface
|
|
3157
|
+
// (calledAs), exactly like Python/JS `import x as y`.
|
|
3158
|
+
const original = String(pathNode.text).split('::').pop();
|
|
3159
|
+
if (original && original !== aliasNode.text && original !== 'self') {
|
|
3160
|
+
if (!imports.aliases) imports.aliases = [];
|
|
3161
|
+
imports.aliases.push({ original, local: aliasNode.text });
|
|
3162
|
+
}
|
|
3155
3163
|
}
|
|
3156
3164
|
return;
|
|
3157
3165
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.3",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
|
|
6
6
|
"main": "index.js",
|