ucn 5.5.1 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +35 -9
- package/.claude/skills/ucn/references/commands.md +38 -16
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +30 -21
- package/cli/index.js +1 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +35 -15
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +1 -0
- package/core/output/reporting.js +6 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +255 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +2 -2
package/core/provenance.js
CHANGED
|
@@ -7,6 +7,13 @@ const isOffset = value => Number.isFinite(value) && value >= 0;
|
|
|
7
7
|
|
|
8
8
|
const { codeUnitCompare } = require('./shared');
|
|
9
9
|
|
|
10
|
+
// Modules read lazily (load cycles), each resolved once: a require() per
|
|
11
|
+
// call in hot paths was measurable (fix #436).
|
|
12
|
+
let lazyProvenanceOverloadModuleCache = null;
|
|
13
|
+
const lazyProvenanceOverloadModule = () => lazyProvenanceOverloadModuleCache || (lazyProvenanceOverloadModuleCache = require('./provenance-overload'));
|
|
14
|
+
let lazyReceiverTypesModuleCache = null;
|
|
15
|
+
const lazyReceiverTypesModule = () => lazyReceiverTypesModuleCache || (lazyReceiverTypesModuleCache = require('./receiver-types'));
|
|
16
|
+
|
|
10
17
|
const TYPE_SOURCE_RULES = Object.freeze({
|
|
11
18
|
annotation: 'receiver-annotation', constructor: 'constructor-typed',
|
|
12
19
|
flow: 'return-flow', field: 'field-hop', literal: 'literal-receiver',
|
|
@@ -85,6 +92,124 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
85
92
|
? { verdict: 'establishes-target' }
|
|
86
93
|
: { verdict: 'establishes-other', declaration };
|
|
87
94
|
if (!targets.length || targets.some(t => !validIdentity(t))) return incomplete('missing-target-identity');
|
|
95
|
+
if (facts.implicitBaseBinding) {
|
|
96
|
+
const owner = facts.implicitBaseBinding.binding?.owner;
|
|
97
|
+
return validIdentity(owner) && require('./provenance-constructor-base').validateImplicitBase(facts.implicitBaseBinding, owner, facts.site)
|
|
98
|
+
? verdictFor(owner) : incomplete('unproven-implicit-base');
|
|
99
|
+
}
|
|
100
|
+
if (facts.constructionBinding) {
|
|
101
|
+
const owner = facts.constructionBinding.owner;
|
|
102
|
+
return validIdentity(owner) && require('./provenance-type').validateTypeLookup(facts.constructionBinding, owner, facts.site)
|
|
103
|
+
? verdictFor(owner) : incomplete('unproven-construction-type');
|
|
104
|
+
}
|
|
105
|
+
if (facts.constructorCall) {
|
|
106
|
+
const constructor = facts.constructorCall;
|
|
107
|
+
const { owner, members, selected, classBinding, call, overload } = constructor;
|
|
108
|
+
const constructed = constructor.constructedOwner || owner;
|
|
109
|
+
// A same-type delegation (`this(...)`) runs in one of the owner's
|
|
110
|
+
// constructors, which is never its target: Java JLS 8.8.7, C#
|
|
111
|
+
// CS0516, C++ [class.base.init] (fix #419).
|
|
112
|
+
const delegating = constructor.delegating;
|
|
113
|
+
// The parts of a C# partial type are one type (fix #430): a member
|
|
114
|
+
// lies in one of them. Every part names the owner's type.
|
|
115
|
+
const parts = constructor.parts === undefined ? [owner] : constructor.parts;
|
|
116
|
+
if (constructor.parts !== undefined && (!Array.isArray(parts) || !validIdentity(owner) ||
|
|
117
|
+
!parts.some(part => sameDeclaration(part, owner)) ||
|
|
118
|
+
parts.some(part => !validIdentity(part) || part.name !== owner.name || part.kind !== owner.kind ||
|
|
119
|
+
(part.namespace || null) !== (owner.namespace || null) ||
|
|
120
|
+
(part.enclosingType || null) !== (owner.enclosingType || null)))) {
|
|
121
|
+
return inconsistent('constructor-owner-mismatch');
|
|
122
|
+
}
|
|
123
|
+
const inPart = def => parts.some(part => def.file === part.file &&
|
|
124
|
+
def.startLine >= part.startLine && def.endLine <= part.endLine);
|
|
125
|
+
if (delegating !== undefined) {
|
|
126
|
+
const site = facts.site;
|
|
127
|
+
// An out-of-class definition (fix #421) holds the site and
|
|
128
|
+
// defines the in-class declaration: same owner scope path and
|
|
129
|
+
// parameter signature.
|
|
130
|
+
const body = constructor.delegatingDefinition || delegating;
|
|
131
|
+
const scopePath = def => [def.namespace, def.enclosingType].filter(Boolean).join('::');
|
|
132
|
+
if (!validIdentity(owner) || !validIdentity(delegating) || call?.delegation !== 'this' ||
|
|
133
|
+
call.syntax !== 'delegation' || delegating.kind !== 'constructor' ||
|
|
134
|
+
delegating.className !== owner.name || !inPart(delegating) ||
|
|
135
|
+
(delegating.namespace || null) !== (owner.namespace || null) ||
|
|
136
|
+
!site || site.file !== body.file || site.line < body.startLine || site.line > body.endLine ||
|
|
137
|
+
(members || []).some(member => sameDeclaration(member, delegating))) {
|
|
138
|
+
return inconsistent('constructor-delegation-mismatch');
|
|
139
|
+
}
|
|
140
|
+
if (constructor.delegatingDefinition && (!validIdentity(body) || body.kind !== 'constructor' ||
|
|
141
|
+
body.className !== owner.name || body.name !== delegating.name ||
|
|
142
|
+
scopePath(body) !== [owner.namespace, owner.enclosingType].filter(Boolean).join('::') ||
|
|
143
|
+
typeof body.signature !== 'string' || body.signature !== constructor.delegatingSignature)) {
|
|
144
|
+
return inconsistent('constructor-delegation-mismatch');
|
|
145
|
+
}
|
|
146
|
+
if (targets.every(t => sameDeclaration(t, delegating))) {
|
|
147
|
+
return { verdict: 'establishes-other', declaration: delegating, diagnostic: 'constructor-delegation-self' };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const structural = require('../languages').langTraits(facts.language)?.typeSystem === 'structural';
|
|
151
|
+
if (!validIdentity(owner) || !validIdentity(selected) || !Array.isArray(members) || !members.length ||
|
|
152
|
+
!call || !(call.isConstructor || require('../languages').langTraits(facts.language)?.classesCallableWithoutNew ||
|
|
153
|
+
require('../languages').langTraits(facts.language)?.functionalTypeConstruction) ||
|
|
154
|
+
call.isFunctionReference || call.isPotentialCallback) return incomplete('missing-constructor-facts');
|
|
155
|
+
if (!classBinding?.facts?.site || classBinding.facts.site.file !== facts.site?.file ||
|
|
156
|
+
classBinding.facts.site.line !== facts.site?.line ||
|
|
157
|
+
classBinding.facts.site.column !== facts.site?.column ||
|
|
158
|
+
(structural || validateConfirmation(classBinding, constructed).verdict !== 'establishes-target') &&
|
|
159
|
+
!require('./provenance-type').validateTypeLookup(constructor.typeLookup, constructed, facts.site)) {
|
|
160
|
+
return incomplete('unproven-construction-binding');
|
|
161
|
+
}
|
|
162
|
+
if (require('../languages').langTraits(facts.language)?.inheritedOverloadSignatures &&
|
|
163
|
+
overload?.call?.argumentFile !== facts.site?.file) {
|
|
164
|
+
return incomplete('unproven-construction-argument-source');
|
|
165
|
+
}
|
|
166
|
+
if (!structural && !['implicit', 'delegation', 'enum'].includes(call.syntax) &&
|
|
167
|
+
(constructor.nameScope?.query?.name !== overload?.call?.name ||
|
|
168
|
+
constructor.nameScope?.query?.qualifier !== (overload?.call?.constructorQualifier || overload?.call?.receiver || null) ||
|
|
169
|
+
!require('./provenance-type').validateTypeLookup(constructor.nameScope, constructed, facts.site))) {
|
|
170
|
+
return incomplete('unproven-construction-name-scope');
|
|
171
|
+
}
|
|
172
|
+
if (call.syntax === 'implicit') {
|
|
173
|
+
const base = classBinding.facts.implicitBaseBinding;
|
|
174
|
+
if (!base || !require('./provenance-constructor-base').validateImplicitBase(base, constructed, facts.site)) {
|
|
175
|
+
return incomplete('unproven-implicit-base');
|
|
176
|
+
}
|
|
177
|
+
if (base.base.potential) return incomplete('constructor-default-unresolved');
|
|
178
|
+
}
|
|
179
|
+
if (!sameDeclaration(owner, constructed) &&
|
|
180
|
+
!require('./constructor-hierarchy').validateHierarchy(constructor.hierarchy, constructed, owner, members)) {
|
|
181
|
+
return incomplete('unproven-constructor-inheritance');
|
|
182
|
+
}
|
|
183
|
+
const constructorProtocol = require('../languages').langTraits(facts.language)?.constructorProtocol;
|
|
184
|
+
if (members.some(member => (member.kind !== 'constructor' && member.name !== constructorProtocol?.initializer) || member.className !== owner.name ||
|
|
185
|
+
!inPart(member) ||
|
|
186
|
+
(member.namespace || null) !== (owner.namespace || null) ||
|
|
187
|
+
(member.enclosingType || null) !== (owner.enclosingType || null)) ||
|
|
188
|
+
!members.some(member => sameDeclaration(member, selected))) return inconsistent('constructor-owner-mismatch');
|
|
189
|
+
if (overload?.candidates?.some(def => def.type === 'constructor' &&
|
|
190
|
+
(!Array.isArray(def.modifiers) || def.modifiers.includes('static')))) {
|
|
191
|
+
return incomplete('constructor-instance-flags');
|
|
192
|
+
}
|
|
193
|
+
if (!call.isConstructor && require('../languages').langTraits(facts.language)?.functionalTypeConstruction) {
|
|
194
|
+
if (!(constructor.typeLookup?.query?.functional &&
|
|
195
|
+
require('./provenance-type').validateTypeLookup(constructor.typeLookup, owner, facts.site))) {
|
|
196
|
+
return incomplete('constructor-functional-binding');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const protocol = require('../languages').langTraits(facts.language)?.constructorProtocol;
|
|
200
|
+
if (protocol) {
|
|
201
|
+
if (!require('./constructor-protocol').validateConstructorProtocol(constructor.protocol, constructed, members, facts.language)) {
|
|
202
|
+
return incomplete('constructor-protocol-unresolved');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (invalidCall && members.length !== 1) return incomplete('constructor-overload-unselected');
|
|
206
|
+
if (!require('../languages').langTraits(facts.language)?.hasArityOverloads && members.length > 1 &&
|
|
207
|
+
!overload?.candidates?.some(def => def.isSignature === true)) return incomplete('constructor-definition-ambiguous');
|
|
208
|
+
if (!lazyProvenanceOverloadModule().validateOverload(overload, members, selected, invalidCall, false, facts.site)) {
|
|
209
|
+
return incomplete('constructor-overload-unselected');
|
|
210
|
+
}
|
|
211
|
+
return verdictFor(selected);
|
|
212
|
+
}
|
|
88
213
|
if (facts.receiverOrigin?.externalFactory) {
|
|
89
214
|
const factory = facts.receiverOrigin.externalFactory;
|
|
90
215
|
const empty = value => Array.isArray(value) && !value.length;
|
|
@@ -237,13 +362,33 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
237
362
|
}
|
|
238
363
|
}
|
|
239
364
|
if (facts.builtinReceiver) {
|
|
240
|
-
if (!
|
|
365
|
+
if (!lazyReceiverTypesModule().isProvenanceBuiltinReceiver(facts.receiverType, facts.language) ||
|
|
241
366
|
facts.builtinReceiver.type !== facts.receiverType ||
|
|
242
367
|
facts.builtinReceiver.language !== facts.language) return inconsistent('builtin-receiver-mismatch');
|
|
243
368
|
if (!facts.receiverOrigin || ['unknown', 'guess'].includes(facts.receiverTypeSource) ||
|
|
244
369
|
facts.receiverOrigin.source !== facts.receiverTypeSource) return incomplete('builtin-origin-not-established');
|
|
245
370
|
return { verdict: 'establishes-other', declaration: { builtin: facts.receiverType, language: facts.language } };
|
|
246
371
|
}
|
|
372
|
+
if (facts.typeReference) {
|
|
373
|
+
const reference = facts.typeReference;
|
|
374
|
+
if (!validIdentity(reference.declaration) || !reference.written ||
|
|
375
|
+
String(reference.written).split(/::|\./).pop() !== reference.declaration.name) {
|
|
376
|
+
return inconsistent('type-reference-mismatch');
|
|
377
|
+
}
|
|
378
|
+
return verdictFor(reference.declaration);
|
|
379
|
+
}
|
|
380
|
+
const castThis = facts.receiverCastThis;
|
|
381
|
+
if (castThis) {
|
|
382
|
+
const receiver = facts.lookup?.receiver;
|
|
383
|
+
if (!validIdentity(castThis.enclosing) || castThis.interfaceType !== facts.receiverType ||
|
|
384
|
+
(receiver && (castThis.enclosing.className !== receiver.name ||
|
|
385
|
+
castThis.enclosing.file !== receiver.file))) return inconsistent('receiver-this-cast-mismatch');
|
|
386
|
+
// Knowing the enclosing class does not prove the implementation an
|
|
387
|
+
// interface cast of this invokes. A subclass may reimplement it.
|
|
388
|
+
// This also applies when a qualified cast prevented member lookup:
|
|
389
|
+
// that missing lookup must not fall through as an unsupported proof.
|
|
390
|
+
return incomplete('receiver-this-cast-dispatch');
|
|
391
|
+
}
|
|
247
392
|
if (facts.lookup) {
|
|
248
393
|
const { receiver, steps, selected } = facts.lookup;
|
|
249
394
|
if (!validIdentity(receiver) || !validIdentity(selected) || !Array.isArray(steps) || !steps.length) {
|
|
@@ -253,9 +398,15 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
253
398
|
if (!sameDeclaration(receiver, facts.receiverResolvedIn)) return inconsistent('receiver-identity-mismatch');
|
|
254
399
|
const aliases = facts.receiverAliases || [];
|
|
255
400
|
const typeDeclaration = facts.receiverTypeDeclaration || receiver;
|
|
401
|
+
// The written qualifier spells the declaration's enclosing type, its
|
|
402
|
+
// namespace, or (fix #430) its namespace and enclosing types, in full
|
|
403
|
+
// or from an enclosing namespace of the site.
|
|
404
|
+
const qualifiedPath = [typeDeclaration.namespace, typeDeclaration.enclosingType].filter(Boolean).join('.');
|
|
256
405
|
if (facts.receiverTypeQualifier && ['java', 'csharp'].includes(facts.language) &&
|
|
257
406
|
facts.receiverTypeQualifier !== typeDeclaration.enclosingType &&
|
|
258
|
-
facts.receiverTypeQualifier !== typeDeclaration.namespace
|
|
407
|
+
facts.receiverTypeQualifier !== typeDeclaration.namespace &&
|
|
408
|
+
!(typeDeclaration.enclosingType && (qualifiedPath === facts.receiverTypeQualifier ||
|
|
409
|
+
qualifiedPath.endsWith(`.${facts.receiverTypeQualifier}`)))) {
|
|
259
410
|
return inconsistent('receiver-qualifier-mismatch');
|
|
260
411
|
}
|
|
261
412
|
if (facts.receiverImportChain && validateConfirmation({ facts: {
|
|
@@ -275,12 +426,8 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
275
426
|
!bound.bounds?.includes(typeDeclaration.name) || !sameDeclaration(bound.selected, typeDeclaration))) {
|
|
276
427
|
return inconsistent('receiver-generic-bound-mismatch');
|
|
277
428
|
}
|
|
278
|
-
const castThis = facts.receiverCastThis;
|
|
279
|
-
if (castThis && (!validIdentity(castThis.enclosing) ||
|
|
280
|
-
castThis.enclosing.className !== receiver.name || castThis.enclosing.file !== receiver.file ||
|
|
281
|
-
castThis.interfaceType !== facts.receiverType)) return inconsistent('receiver-this-cast-mismatch');
|
|
282
429
|
const overload = facts.lookup.overload;
|
|
283
|
-
const group = overload &&
|
|
430
|
+
const group = overload && lazyProvenanceOverloadModule().overloadMemberGroup(steps);
|
|
284
431
|
if (overload && !group) return incomplete('missing-overload-declarations');
|
|
285
432
|
if (overload && !group.some(member => sameDeclaration(declarationIdentity(member), selected))) {
|
|
286
433
|
return inconsistent('selected-member-outside-overload-group');
|
|
@@ -301,15 +448,70 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
301
448
|
return incomplete('missing-member-lookup-facts');
|
|
302
449
|
}
|
|
303
450
|
const named = step.members.filter(member => member.name === selected.name);
|
|
451
|
+
const delegation = step.constructorDelegation;
|
|
452
|
+
if (delegation && (i !== 0 || facts.receiver !== 'super' || selected.kind !== 'constructor' ||
|
|
453
|
+
delegation.enclosing?.kind !== 'constructor' ||
|
|
454
|
+
delegation.enclosing.className !== step.owner.name ||
|
|
455
|
+
delegation.enclosing.file !== step.owner.file ||
|
|
456
|
+
facts.site?.line < delegation.enclosing.startLine || facts.site?.line > delegation.enclosing.endLine ||
|
|
457
|
+
!Array.isArray(delegation.members) || named.length ||
|
|
458
|
+
delegation.members.some(member => member.kind !== 'constructor' || member.className !== step.owner.name))) {
|
|
459
|
+
return inconsistent('constructor-delegation-mismatch');
|
|
460
|
+
}
|
|
461
|
+
// A super lookup (fix #428) passes over the enclosing class's
|
|
462
|
+
// own members: only the first step, for the language's super
|
|
463
|
+
// receiver inside a member of that class.
|
|
464
|
+
const superLookup = step.superLookup;
|
|
465
|
+
if (superLookup) {
|
|
466
|
+
const enclosing = facts.receiverOrigin?.declaration;
|
|
467
|
+
const keyword = require('../languages').langTraits(facts.language)?.superReceiver;
|
|
468
|
+
if (i !== 0 || !keyword || facts.receiver !== keyword || named.length ||
|
|
469
|
+
facts.receiverOrigin?.source !== 'same-class' || facts.receiverOrigin.superLookup !== true ||
|
|
470
|
+
!validIdentity(enclosing) || enclosing.className !== step.owner.name ||
|
|
471
|
+
enclosing.file !== facts.site?.file || facts.site.line < enclosing.startLine ||
|
|
472
|
+
facts.site.line > enclosing.endLine || !Array.isArray(superLookup.skipped) ||
|
|
473
|
+
superLookup.skipped.some(member => member.className !== step.owner.name)) {
|
|
474
|
+
return inconsistent('super-lookup-mismatch');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// The parts of a C# partial type declare one class's members.
|
|
478
|
+
const parts = Array.isArray(step.partialParts) ? step.partialParts : null;
|
|
479
|
+
if (parts && (facts.language !== 'csharp' || !parts.some(part => sameDeclaration(part, step.owner)) ||
|
|
480
|
+
parts.some(part => !validIdentity(part) || part.name !== step.owner.name || part.kind !== step.owner.kind ||
|
|
481
|
+
(part.namespace || null) !== (step.owner.namespace || null) ||
|
|
482
|
+
(part.enclosingType || null) !== (step.owner.enclosingType || null)))) {
|
|
483
|
+
return inconsistent('partial-part-mismatch');
|
|
484
|
+
}
|
|
304
485
|
if (named.some(member => member.className !== step.owner.name ||
|
|
305
486
|
(member.namespace || null) !== (step.owner.namespace || null) ||
|
|
306
487
|
(member.enclosingType || null) !== (step.owner.enclosingType || null) ||
|
|
307
|
-
(facts.language !== 'go' && member.file !== step.owner.file
|
|
488
|
+
(facts.language !== 'go' && member.file !== step.owner.file &&
|
|
489
|
+
!parts?.some(part => part.file === member.file) &&
|
|
490
|
+
// Rust impl blocks may sit in any file of the crate.
|
|
491
|
+
!require('../languages').langTraits(facts.language)?.itemScopeResolution))) {
|
|
308
492
|
return inconsistent('member-owner-mismatch');
|
|
309
493
|
}
|
|
310
494
|
if (i + 1 < steps.length) {
|
|
311
495
|
if (named.length && !overload) return inconsistent('overriding-member-before-target');
|
|
312
|
-
|
|
496
|
+
const next = steps[i + 1];
|
|
497
|
+
if (next.enclosingOf) {
|
|
498
|
+
// A lexical hop (fix #428): a bare call, a hierarchy that
|
|
499
|
+
// ends here with no member of the name since the type
|
|
500
|
+
// it started from, and the declaration enclosing it.
|
|
501
|
+
const inner = next.enclosingOf;
|
|
502
|
+
let start = 0;
|
|
503
|
+
for (let j = i; j > 0; j--) if (steps[j].enclosingOf) { start = j; break; }
|
|
504
|
+
const segments = String(inner?.enclosingType || '').split('.');
|
|
505
|
+
const outerName = segments.pop();
|
|
506
|
+
if (facts.receiver || facts.receiverOrigin?.source !== 'same-class' || !['java', 'csharp'].includes(facts.language) ||
|
|
507
|
+
step.parents.length !== 0 || !validIdentity(inner) || !sameDeclaration(inner, steps[start].owner) ||
|
|
508
|
+
steps.slice(start, i + 1).some(prior => prior.members.some(member => member.name === selected.name)) ||
|
|
509
|
+
next.owner.name !== outerName || next.owner.file !== inner.file ||
|
|
510
|
+
(next.owner.enclosingType || null) !== (segments.length ? segments.join('.') : null) ||
|
|
511
|
+
next.owner.startLine > inner.startLine || (next.owner.endLine ?? next.owner.startLine) < inner.endLine) {
|
|
512
|
+
return inconsistent('unproven-enclosing-hop');
|
|
513
|
+
}
|
|
514
|
+
} else if (!step.parents.some(parent => sameDeclaration(parent, next.owner))) {
|
|
313
515
|
return inconsistent('unproven-inheritance-hop');
|
|
314
516
|
}
|
|
315
517
|
} else if (configurationItem) {
|
|
@@ -328,13 +530,13 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
328
530
|
} else if (!overload && (named.length !== 1 || !sameDeclaration(named[0], selected))) {
|
|
329
531
|
const propertyRead = facts.valueReference && step.propertyRead &&
|
|
330
532
|
sameDeclaration(propertyReadMember(named), selected);
|
|
331
|
-
if (!propertyRead && !
|
|
533
|
+
if (!propertyRead && !lazyProvenanceOverloadModule().validateOverload(step.overload, named, selected, false, false, facts.site)) {
|
|
332
534
|
return incomplete('ambiguous-or-missing-member');
|
|
333
535
|
}
|
|
334
536
|
}
|
|
335
537
|
}
|
|
336
|
-
if (overload && !
|
|
337
|
-
overload, group.map(declarationIdentity), selected, invalidCall, ambiguousOverload)) {
|
|
538
|
+
if (overload && !lazyProvenanceOverloadModule().validateOverload(
|
|
539
|
+
overload, group.map(declarationIdentity), selected, invalidCall, ambiguousOverload, facts.site)) {
|
|
338
540
|
return incomplete(overload.outcome === 'no-fit' ? 'no-applicable-overload' : 'ambiguous-or-missing-member');
|
|
339
541
|
}
|
|
340
542
|
if (facts.receiverTypeSource === 'guess' || facts.receiverTypeSource === 'unknown') {
|
|
@@ -348,7 +550,9 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
348
550
|
}
|
|
349
551
|
if (memberSetMembers) {
|
|
350
552
|
if (targets.some(target => memberSetMembers.some(member => sameDeclaration(target, member)))) {
|
|
351
|
-
|
|
553
|
+
// A choice among them the replay cannot state (fix #428).
|
|
554
|
+
return facts.lookupGap ? { verdict: 'incomplete', diagnostic: facts.lookupGap, gap: true }
|
|
555
|
+
: incomplete('ambiguous-or-missing-member');
|
|
352
556
|
}
|
|
353
557
|
return { verdict: 'establishes-other', declarations: memberSetMembers };
|
|
354
558
|
}
|
|
@@ -411,6 +615,12 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
411
615
|
last.declaration.name !== last.importedName) return incomplete('missing-import-declaration');
|
|
412
616
|
return verdictFor(last.declaration);
|
|
413
617
|
}
|
|
618
|
+
// A lookup this collector cannot state yet (fix #428: extension
|
|
619
|
+
// invocations, impl selection, members a using-declaration brings in,
|
|
620
|
+
// const/ref-qualified pairs, trait-provided methods, Rust name scopes)
|
|
621
|
+
// proves nothing: for exclusion and receiver migration it is incomplete,
|
|
622
|
+
// as before; on an edge another rule confirmed it reads `unsupported`.
|
|
623
|
+
if (facts.lookupGap) return { verdict: 'incomplete', diagnostic: facts.lookupGap, gap: true };
|
|
414
624
|
// An unsupported rule is an instrumentation result, not a failed proof.
|
|
415
625
|
// A collector that entered a supported lookup must still fail closed if
|
|
416
626
|
// its witness is absent (including when a saved witness was tampered with).
|
|
@@ -420,6 +630,12 @@ function validateConfirmation(provenance, target, invalidCall = false) {
|
|
|
420
630
|
|
|
421
631
|
/** An invalid member-family call is a diagnostic, never a confirmed edge. */
|
|
422
632
|
function validateCallMismatch(provenance, target) {
|
|
633
|
+
const constructor = provenance?.facts?.constructorCall;
|
|
634
|
+
if (constructor) {
|
|
635
|
+
if (constructor.overload?.outcome !== 'no-fit' ||
|
|
636
|
+
!constructor.members?.some(member => sameDeclaration(member, target))) return false;
|
|
637
|
+
return validateConfirmation(provenance, constructor.members, true).verdict === 'establishes-target';
|
|
638
|
+
}
|
|
423
639
|
const lookup = provenance?.facts?.lookup;
|
|
424
640
|
if (lookup?.overload?.outcome !== 'no-fit') return false;
|
|
425
641
|
const members = lookup.steps.flatMap(step => step.members || []);
|
|
@@ -433,13 +649,16 @@ function createProvenance(evidence, resolution) {
|
|
|
433
649
|
const rules = [];
|
|
434
650
|
if (evidence.possibleDispatch) rules.push('possible-dispatch');
|
|
435
651
|
if (evidence.methodAmbiguous) rules.push('method-ambiguous');
|
|
436
|
-
|
|
652
|
+
// A qualified static call resolved to its class is type-qualified, not
|
|
653
|
+
// a same-class receiver (fix #428).
|
|
654
|
+
if (evidence.resolvedBySameClass && !evidence.qualifiedOwner) rules.push('same-class');
|
|
437
655
|
if (evidence.extensionMethod) rules.push('extension-method');
|
|
438
656
|
if (evidence.hasReceiverType || evidence.resolvedByReceiverHint) {
|
|
439
657
|
rules.push(TYPE_SOURCE_RULES[source] || 'receiver-type');
|
|
440
658
|
}
|
|
441
659
|
if (evidence.typeQualifiedReceiver) rules.push('type-qualified');
|
|
442
660
|
if (evidence.moduleOwnedPath) rules.push('module-owned');
|
|
661
|
+
if (evidence.itemScope) rules.push('item-scope');
|
|
443
662
|
if (evidence.hasBindingId) rules.push('binding');
|
|
444
663
|
if (evidence.hasSingleOwnerEvidence) rules.push('single-owner');
|
|
445
664
|
if (evidence.hasImportEvidence) {
|
|
@@ -466,7 +685,7 @@ function createProvenance(evidence, resolution) {
|
|
|
466
685
|
const provenance = { rule: rules[0], rules: [...new Set(rules)], facts };
|
|
467
686
|
if (facts.targets?.length) {
|
|
468
687
|
const checked = validateConfirmation(provenance, facts.targets);
|
|
469
|
-
provenance.validation = checked.verdict;
|
|
688
|
+
provenance.validation = checked.gap ? 'unsupported' : checked.verdict;
|
|
470
689
|
if (checked.diagnostic) provenance.diagnostic = checked.diagnostic;
|
|
471
690
|
} else {
|
|
472
691
|
provenance.validation = 'incomplete';
|
package/core/public-command.js
CHANGED
|
@@ -5,8 +5,14 @@ const { CANONICAL_COMMANDS } = require('./registry');
|
|
|
5
5
|
const NAME_COMMANDS = new Set(['show', 'find', 'usages', 'trace', 'tests', 'plan']);
|
|
6
6
|
|
|
7
7
|
function parseSourceTarget(arg, params) {
|
|
8
|
-
if (params.range) return { ...params, file: params.file, range: params.range };
|
|
9
8
|
const value = String(arg || '').trim();
|
|
9
|
+
// `source <file> --range=N-M`: the positional is the file (fix #433). A
|
|
10
|
+
// positional beside an explicit file stays a name, which the handler
|
|
11
|
+
// refuses with the range.
|
|
12
|
+
if (params.range) {
|
|
13
|
+
if (!params.file && value) return { ...params, file: value, range: params.range };
|
|
14
|
+
return { ...params, ...(value && { name: value }), range: params.range };
|
|
15
|
+
}
|
|
10
16
|
const twoPart = value.match(/^(.+?)\s+(\d+(?:-\d+)?)$/);
|
|
11
17
|
if (twoPart) return { ...params, file: params.file || twoPart[1], range: twoPart[2] };
|
|
12
18
|
// A single-colon file:range target is a line request. Stable symbol
|
|
@@ -16,6 +22,15 @@ function parseSourceTarget(arg, params) {
|
|
|
16
22
|
return { ...params, name: arg };
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
/** MCP and direct execute() params: with a range and no file, `name` is the
|
|
26
|
+
* file (`source` with name=<file>, range="N-M"; fix #433). */
|
|
27
|
+
function sourceRangeTarget(params) {
|
|
28
|
+
if (params.range == null || (params.file && String(params.file).trim()) ||
|
|
29
|
+
typeof params.name !== 'string' || !params.name.trim()) return params;
|
|
30
|
+
const { name, ...rest } = params;
|
|
31
|
+
return { ...rest, file: name.trim() };
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
/** Build normalized execute() params from the shared CLI-style positional arg. */
|
|
20
35
|
function buildPublicParams(command, arg, params = {}) {
|
|
21
36
|
const clean = { ...params };
|
|
@@ -47,4 +62,4 @@ function isPublicCommand(command) {
|
|
|
47
62
|
return CANONICAL_COMMANDS.includes(command);
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
module.exports = { buildPublicParams, isPublicCommand, parseSourceTarget };
|
|
65
|
+
module.exports = { buildPublicParams, isPublicCommand, parseSourceTarget, sourceRangeTarget };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { langTraits, getParser, getLanguageAdapter, safeParse } = require('../languages');
|
|
4
|
+
const { getUsageResult, storeUsageResult } = require('./cache');
|
|
5
|
+
const { indexedContent, syntaxMode } = require('./source-memo');
|
|
6
|
+
const localNameTrees = new WeakSet();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Bodies whose declarations cannot bind a name outside their own scope.
|
|
10
|
+
* A block is damaged when an enclosing ERROR node or an enclosing block or
|
|
11
|
+
* callable with an error holds it. One native query lists those nodes in
|
|
12
|
+
* document order; a stack of the open ones stands for each block's
|
|
13
|
+
* ancestors (fix #435: every block walked its parent chain). In document
|
|
14
|
+
* order a node that starts before an open non-empty node ends lies inside it.
|
|
15
|
+
*/
|
|
16
|
+
function bodyRanges(tree, kinds) {
|
|
17
|
+
if (!tree) return null;
|
|
18
|
+
const ranges = [];
|
|
19
|
+
if (!tree.rootNode.hasError) {
|
|
20
|
+
// No node of an error-free tree has an error. A block's first and
|
|
21
|
+
// last tokens are its first and last characters (an error-free tree
|
|
22
|
+
// has no missing tokens), read from the parsed text when it is at
|
|
23
|
+
// hand instead of asking for two child nodes (fix #436).
|
|
24
|
+
const text = typeof tree.input === 'string' ? tree.input : null;
|
|
25
|
+
for (const body of tree.rootNode.descendantsOfType('block')) {
|
|
26
|
+
const start = body.startIndex, end = body.endIndex;
|
|
27
|
+
if (text !== null ? end - start >= 2 && text[start] === '{' && text[end - 1] === '}'
|
|
28
|
+
: body.firstChild?.type === '{' && body.lastChild?.type === '}') {
|
|
29
|
+
ranges.push([start + 1, end - 1]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return ranges.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
|
|
33
|
+
}
|
|
34
|
+
const open = []; // [endIndex, damages descendants]
|
|
35
|
+
for (const node of tree.rootNode.descendantsOfType(['block', 'ERROR', ...kinds])) {
|
|
36
|
+
const start = node.startIndex, end = node.endIndex;
|
|
37
|
+
while (open.length && open[open.length - 1][0] <= start) open.pop();
|
|
38
|
+
const damaged = open.some(entry => entry[1]);
|
|
39
|
+
const hasError = node.hasError;
|
|
40
|
+
if (node.type === 'block' && !damaged && !hasError && node.firstChild?.type === '{' &&
|
|
41
|
+
node.lastChild?.type === '}') ranges.push([start + 1, end - 1]);
|
|
42
|
+
if (end > start) open.push([end, node.type === 'ERROR' || hasError]);
|
|
43
|
+
}
|
|
44
|
+
// Bindings in a block stay in that block. Retain nested ranges so a
|
|
45
|
+
// query-bearing function can still skip unrelated sibling scopes.
|
|
46
|
+
return ranges.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function viewKey(owns) { return owns == null ? 'primary' : [...owns].sort((a, b) => a - b).join(','); }
|
|
50
|
+
|
|
51
|
+
// Recovery remains visible at the same positions, in the same containers.
|
|
52
|
+
// Never prune an erroneous callable or anything inside an ERROR node.
|
|
53
|
+
function errorShape(tree) {
|
|
54
|
+
const errors = [], pending = [tree.rootNode];
|
|
55
|
+
while (pending.length) {
|
|
56
|
+
const node = pending.pop();
|
|
57
|
+
if (node.type === 'ERROR' || node.isMissing) {
|
|
58
|
+
const parents = [];
|
|
59
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
60
|
+
parents.push([parent.type, parent.startIndex, parent.endIndex]);
|
|
61
|
+
}
|
|
62
|
+
errors.push([node.type, node.startIndex, node.endIndex, !!node.isMissing, parents]);
|
|
63
|
+
} else if (node.hasError) pending.push(...node.children);
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify(errors);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readRanges(value, content) {
|
|
69
|
+
if (!Array.isArray(value) || !value.length) return null;
|
|
70
|
+
const views = new Map();
|
|
71
|
+
for (const record of value) {
|
|
72
|
+
if (!Array.isArray(record) || record.length !== 3 || typeof record[0] !== 'string' ||
|
|
73
|
+
views.has(record[0]) || !Array.isArray(record[1]) || typeof record[2] !== 'string') return null;
|
|
74
|
+
let previous = -1;
|
|
75
|
+
const ends = [];
|
|
76
|
+
for (const range of record[1]) {
|
|
77
|
+
if (!Array.isArray(range) || range.length !== 2 ||
|
|
78
|
+
!range.every(Number.isInteger) || range[0] <= previous || range[0] < 1 ||
|
|
79
|
+
range[1] < range[0] || range[1] >= content.length ||
|
|
80
|
+
content[range[0] - 1] !== '{' || content[range[1]] !== '}') return null;
|
|
81
|
+
while (ends.length && range[0] >= ends[ends.length - 1]) ends.pop();
|
|
82
|
+
if (ends.length && range[1] >= ends[ends.length - 1]) return null;
|
|
83
|
+
ends.push(range[1]);
|
|
84
|
+
previous = range[0];
|
|
85
|
+
}
|
|
86
|
+
views.set(record[0], { ranges: record[1], errors: record[2] });
|
|
87
|
+
}
|
|
88
|
+
return views.has('primary') ? views : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function blankBodies(source, ranges, name) {
|
|
92
|
+
const chunks = [];
|
|
93
|
+
let at = 0;
|
|
94
|
+
let occurrence = source.indexOf(name);
|
|
95
|
+
for (const [start, end] of ranges) {
|
|
96
|
+
if (start < at) continue;
|
|
97
|
+
while (occurrence >= 0 && occurrence < start) occurrence = source.indexOf(name, occurrence + name.length);
|
|
98
|
+
if (occurrence >= start && occurrence < end) continue;
|
|
99
|
+
const body = source.slice(start, end);
|
|
100
|
+
const newline = char => char === '\r' || char === '\n';
|
|
101
|
+
let first = 0, last = body.length - 2;
|
|
102
|
+
while (first < last && (newline(body[first]) || newline(body[first + 1]))) first++;
|
|
103
|
+
while (last > first + 1 && (newline(body[last]) || newline(body[last + 1]))) last--;
|
|
104
|
+
if (last <= first + 1) continue;
|
|
105
|
+
// All opted-in grammars have non-nesting block comments. One opaque
|
|
106
|
+
// token avoids parsing the unrelated body while preserving every
|
|
107
|
+
// UTF-16 offset and newline. Existing terminators stay inside it.
|
|
108
|
+
const comment = body.slice(first + 2, last).replace(/\*\//g, '* ');
|
|
109
|
+
chunks.push(source.slice(at, start), body.slice(0, first), '/*', comment, '*/', body.slice(last + 2));
|
|
110
|
+
at = end;
|
|
111
|
+
}
|
|
112
|
+
return chunks.length ? chunks.join('') + source.slice(at) : source;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Keep every declaration header and every body containing the queried text.
|
|
117
|
+
* The persisted facts are AST ranges, never type or ownership resolutions.
|
|
118
|
+
* These private trees never enter the index's complete parsed-tree caches.
|
|
119
|
+
*/
|
|
120
|
+
function localQueryTree(index, file, entry, language, name, content) {
|
|
121
|
+
const traits = langTraits(language);
|
|
122
|
+
if (!traits.closedQueryBodies?.length || !entry.hash || !index._usageResultCache ||
|
|
123
|
+
index.isProvenanceFactIndex || !indexedContent(index, file, entry, content)) return null;
|
|
124
|
+
const mode = syntaxMode('valueBodies', [language, traits.closedQueryBodies, entry.conditionalViews, entry.recoveryBlanks]);
|
|
125
|
+
const key = '\x01body-ranges';
|
|
126
|
+
const value = getUsageResult(index, file, key, mode);
|
|
127
|
+
const ranges = readRanges(value, content);
|
|
128
|
+
const persistent = index._persistentParsedTree?.(file, language);
|
|
129
|
+
const warm = index._opTreeCache?.has(file) || persistent?.fileHash === entry.hash;
|
|
130
|
+
const parser = getParser(language);
|
|
131
|
+
if (ranges && !warm) {
|
|
132
|
+
let complete = true;
|
|
133
|
+
const transform = (source, owns) => {
|
|
134
|
+
const view = ranges.get(viewKey(owns));
|
|
135
|
+
if (!view) { complete = false; return source; }
|
|
136
|
+
return blankBodies(source, view.ranges, name);
|
|
137
|
+
};
|
|
138
|
+
const tree = traits.queryBodyTree
|
|
139
|
+
? traits.queryBodyTree(content, parser, entry, transform)
|
|
140
|
+
: safeParse(parser, transform(content, null));
|
|
141
|
+
const views = traits.queryBodyViews?.(tree) || [{ tree }];
|
|
142
|
+
if (complete && views.every(view => view.tree &&
|
|
143
|
+
errorShape(view.tree) === ranges.get(viewKey(view.owns))?.errors)) {
|
|
144
|
+
for (const view of views) localNameTrees.add(view.tree);
|
|
145
|
+
return tree;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const tree = index._getParsedTree(file, content, language) ||
|
|
149
|
+
getLanguageAdapter(language)?.queryTree?.(content, parser, entry) || safeParse(parser, content);
|
|
150
|
+
if (!ranges && tree) {
|
|
151
|
+
const views = traits.queryBodyViews?.(tree) || [{ tree }];
|
|
152
|
+
const kinds = new Set(traits.closedQueryBodies);
|
|
153
|
+
const records = views.map(view => [viewKey(view.owns), bodyRanges(view.tree, kinds), errorShape(view.tree)]);
|
|
154
|
+
if (records.every(record => record[1] !== null)) storeUsageResult(index, file, key, mode, records);
|
|
155
|
+
}
|
|
156
|
+
return tree;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { localQueryTree, isLocalNameTree: tree => localNameTrees.has(tree), bodyRangesForTest: bodyRanges };
|