ucn 5.3.3 → 5.3.5
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 +23 -1
- package/cli/index.js +23 -12
- package/core/account.js +5 -1
- package/core/analysis.js +18 -0
- package/core/cache.js +106 -1
- package/core/callers.js +939 -122
- package/core/confidence.js +26 -14
- package/core/imports.js +18 -4
- package/core/index-ir.js +1 -1
- package/core/ir.js +1 -1
- package/core/output/analysis.js +20 -8
- package/core/output/provenance.js +41 -0
- package/core/output/public.js +2 -1
- package/core/output/shared.js +3 -0
- package/core/project.js +9 -0
- package/core/provenance-facts.js +287 -0
- package/core/provenance-overload.js +118 -0
- package/core/provenance.js +423 -0
- package/core/python-fixture-flow.js +216 -0
- package/core/receiver-types.js +26 -0
- package/core/rust-result-flow.js +206 -0
- package/core/verify.js +7 -3
- package/languages/adapter.js +2 -0
- package/languages/c-family.js +14 -1
- package/languages/csharp.js +21 -9
- package/languages/go.js +105 -86
- package/languages/java.js +23 -10
- package/languages/javascript.js +129 -18
- package/languages/python.js +202 -35
- package/languages/rust.js +173 -73
- package/languages/type-evidence.js +63 -0
- package/mcp/server.js +7 -2
- package/package.json +2 -2
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { codeUnitCompare } = require('./shared');
|
|
4
|
+
|
|
5
|
+
const TYPE_SOURCE_RULES = Object.freeze({
|
|
6
|
+
annotation: 'receiver-annotation', constructor: 'constructor-typed',
|
|
7
|
+
flow: 'return-flow', field: 'field-hop', literal: 'literal-receiver',
|
|
8
|
+
'with-binding': 'with-binding', cast: 'receiver-cast',
|
|
9
|
+
'type-assertion': 'receiver-type-assertion', guess: 'receiver-guess',
|
|
10
|
+
fixture: 'pytest-fixture',
|
|
11
|
+
unknown: 'receiver-type', 'type-qualified': 'type-qualified',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/** Declaration identity includes lexical ownership; sharing a file is not identity. */
|
|
15
|
+
function declarationIdentity(definition) {
|
|
16
|
+
if (!definition) return null;
|
|
17
|
+
return {
|
|
18
|
+
file: definition.relativePath || definition.file,
|
|
19
|
+
startLine: definition.startLine,
|
|
20
|
+
endLine: definition.endLine,
|
|
21
|
+
name: definition.name,
|
|
22
|
+
kind: definition.kind || definition.type,
|
|
23
|
+
className: definition.className || null,
|
|
24
|
+
namespace: definition.namespace || null,
|
|
25
|
+
enclosingType: definition.enclosingType || null,
|
|
26
|
+
lexicalScopeStartLine: definition.lexicalScopeStartLine || null,
|
|
27
|
+
lexicalScopeEndLine: definition.lexicalScopeEndLine || null,
|
|
28
|
+
...(definition.bindingId && { bindingId: definition.bindingId }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function identityKey(identity) {
|
|
33
|
+
if (!identity || !identity.file || !Number.isInteger(identity.startLine) ||
|
|
34
|
+
!identity.name || !identity.kind) return null;
|
|
35
|
+
return JSON.stringify([
|
|
36
|
+
identity.file, identity.startLine, identity.endLine ?? null,
|
|
37
|
+
identity.name, identity.kind, identity.className || null,
|
|
38
|
+
identity.namespace || null, identity.enclosingType || null,
|
|
39
|
+
identity.lexicalScopeStartLine || null, identity.lexicalScopeEndLine || null,
|
|
40
|
+
identity.bindingId || null,
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sameDeclaration(left, right) {
|
|
45
|
+
const key = identityKey(left);
|
|
46
|
+
return key !== null && key === identityKey(right);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function propertyReadMember(members) {
|
|
50
|
+
const readers = members.filter(member => ['getter', 'property'].includes(member.kind));
|
|
51
|
+
return readers.length === 1 && members.every(member =>
|
|
52
|
+
['getter', 'property', 'setter'].includes(member.kind)) ? readers[0] : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check a data-only witness, never infer a negative from a failed proof.
|
|
57
|
+
* `establishes-other` requires its own complete lookup/binding witness.
|
|
58
|
+
* Reports without a migrated witness remain explicitly incomplete (#356).
|
|
59
|
+
*/
|
|
60
|
+
function validateConfirmation(provenance, target, invalidCall = false) {
|
|
61
|
+
const targets = (Array.isArray(target) ? target : [target]).filter(Boolean);
|
|
62
|
+
const facts = provenance?.facts || {};
|
|
63
|
+
const incomplete = diagnostic => ({ verdict: 'incomplete', diagnostic });
|
|
64
|
+
const inconsistent = diagnostic => ({ verdict: 'inconsistent', diagnostic });
|
|
65
|
+
const verdictFor = declaration => targets.some(t => sameDeclaration(t, declaration))
|
|
66
|
+
? { verdict: 'establishes-target' }
|
|
67
|
+
: { verdict: 'establishes-other', declaration };
|
|
68
|
+
if (!targets.length || targets.some(t => !identityKey(t))) return incomplete('missing-target-identity');
|
|
69
|
+
if (facts.receiverOrigin?.externalFactory) {
|
|
70
|
+
const factory = facts.receiverOrigin.externalFactory;
|
|
71
|
+
const empty = value => Array.isArray(value) && !value.length;
|
|
72
|
+
const span = origin => Number.isInteger(origin?.start) && Number.isInteger(origin?.end) && origin.end > origin.start;
|
|
73
|
+
if (facts.language !== 'python' || facts.receiverTypeSource !== 'flow' ||
|
|
74
|
+
!identityKey(factory.owner) || !identityKey(factory.enclosing) ||
|
|
75
|
+
factory.owner.file !== facts.site?.file || factory.enclosing.file !== factory.owner.file ||
|
|
76
|
+
factory.enclosing.className !== factory.owner.name || !factory.field ||
|
|
77
|
+
facts.site.line < factory.enclosing.startLine || facts.site.line > factory.enclosing.endLine ||
|
|
78
|
+
facts.receiverPath?.length !== 2 || facts.receiverPath[0] !== 'self' || facts.receiverPath[1] !== factory.field ||
|
|
79
|
+
!Array.isArray(factory.assignments) || !factory.assignments.length) return incomplete('missing-external-field-flow');
|
|
80
|
+
for (const write of factory.assignments) {
|
|
81
|
+
const binding = write.binding, imported = ['from', 'relative'].includes(binding?.kind);
|
|
82
|
+
if (!binding || !write.module || write.module !== binding.module || !write.producer ||
|
|
83
|
+
!Array.isArray(write.path) || (binding.alias || binding.name) !== write.path[0] ||
|
|
84
|
+
write.path.length !== (imported ? 1 : 2) || write.producer !== (imported ? binding.name : write.path[1]) ||
|
|
85
|
+
!Number.isInteger(write.depth) || write.depth < 1 || write.depth > 3 ||
|
|
86
|
+
!span(write.assignment) || write.assignment.nodeType !== 'assignment' ||
|
|
87
|
+
write.assignment.line < factory.owner.startLine || write.assignment.line > factory.owner.endLine ||
|
|
88
|
+
!span(write.expression) || write.expression.nodeType !== 'call' ||
|
|
89
|
+
!empty(write.projectDeclarations) || !empty(write.projectBindings) || write.resolvedModule !== null) {
|
|
90
|
+
return incomplete('invalid-external-factory-binding');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (new Set(factory.assignments.map(w => `${w.module}.${w.producer}:${w.depth}`)).size !== 1) {
|
|
94
|
+
return inconsistent('conflicting-external-field-producers');
|
|
95
|
+
}
|
|
96
|
+
// Source ownership establishes runtime uncertainty, never a concrete
|
|
97
|
+
// project target or an exclusion: an external factory may return one.
|
|
98
|
+
return { verdict: 'establishes-dispatch' };
|
|
99
|
+
}
|
|
100
|
+
if (facts.receiverTypeSource === 'fixture' &&
|
|
101
|
+
!require('./python-fixture-flow').validatePythonFixtureBinding(facts.receiverOrigin?.fixtureBinding, facts)) {
|
|
102
|
+
return incomplete('invalid-pytest-fixture-binding');
|
|
103
|
+
}
|
|
104
|
+
if (facts.receiverOrigin?.aliasBinding) {
|
|
105
|
+
const binding = facts.receiverOrigin.aliasBinding;
|
|
106
|
+
if (facts.language !== 'rust' || binding.type !== facts.receiverType || binding.origin?.type !== binding.type ||
|
|
107
|
+
!binding.variable || !['copy', 'borrow', 'reassignment'].includes(binding.kind) ||
|
|
108
|
+
!['annotation', 'constructor', 'flow'].includes(binding.origin?.source) ||
|
|
109
|
+
(binding.referenceAnnotation && (binding.origin.source !== 'annotation' ||
|
|
110
|
+
!binding.referenceAnnotation.trim().startsWith('&'))) ||
|
|
111
|
+
!Number.isInteger(binding.assignment?.start) || !Number.isInteger(binding.assignment?.end)) {
|
|
112
|
+
return incomplete('invalid-copied-receiver-binding');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (facts.receiverOrigin?.assignments) {
|
|
116
|
+
const assignments = facts.receiverOrigin.assignments;
|
|
117
|
+
if (facts.language !== 'python' || !Array.isArray(assignments) || !assignments.length) {
|
|
118
|
+
return incomplete('invalid-field-assignments');
|
|
119
|
+
}
|
|
120
|
+
const literals = { dictionary: 'dict', dictionary_comprehension: 'dict', list: 'list',
|
|
121
|
+
list_comprehension: 'list', set: 'set', set_comprehension: 'set', tuple: 'tuple' };
|
|
122
|
+
for (const write of assignments) {
|
|
123
|
+
if (write.type !== facts.receiverType || !Number.isInteger(write.assignment?.start) ||
|
|
124
|
+
!Number.isInteger(write.assignment?.end) || !Number.isInteger(write.expression?.start) ||
|
|
125
|
+
!Number.isInteger(write.expression?.end)) return incomplete('invalid-field-assignment-origin');
|
|
126
|
+
if (write.literal) {
|
|
127
|
+
if (write.expression.nodeType !== write.literal || literals[write.literal] !== write.type) {
|
|
128
|
+
return inconsistent('field-literal-contract-mismatch');
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
const call = write.importedCall;
|
|
132
|
+
const binding = call?.binding;
|
|
133
|
+
if (!binding || call.externalModule !== binding.module || write.expression.nodeType !== 'call' ||
|
|
134
|
+
call.type !== write.type || require('../languages/python').getBuiltinCallReturnType(
|
|
135
|
+
binding.module, binding.name) !== write.type) return inconsistent('field-call-contract-mismatch');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (facts.receiverOrigin?.moduleProducer) {
|
|
140
|
+
const producer = facts.receiverOrigin.moduleProducer;
|
|
141
|
+
const call = producer.call, declaration = producer.declaration;
|
|
142
|
+
if (facts.language !== 'rust' || !call?.receiver || !call.name || !call.file ||
|
|
143
|
+
!Number.isInteger(call.start) || !Number.isInteger(call.end) ||
|
|
144
|
+
!identityKey(declaration) || declaration.className || !declaration.returnType ||
|
|
145
|
+
declaration.name !== call.name || declaration.file !== producer.module?.file) {
|
|
146
|
+
return incomplete('invalid-module-producer-declaration');
|
|
147
|
+
}
|
|
148
|
+
const segments = call.receiver.split('::');
|
|
149
|
+
const binding = producer.binding;
|
|
150
|
+
if (binding && (binding.alias || binding.name) !== segments[0]) {
|
|
151
|
+
return inconsistent('module-producer-binding-mismatch');
|
|
152
|
+
}
|
|
153
|
+
const specifier = binding ? [binding.module, ...segments.slice(1)].join('::') : call.receiver;
|
|
154
|
+
const fileParts = producer.module.file.split('/');
|
|
155
|
+
const base = fileParts.pop().replace(/\.rs$/, '');
|
|
156
|
+
if (specifier !== producer.module.specifier ||
|
|
157
|
+
specifier.split('::').at(-1) !== (base === 'mod' ? fileParts.pop() : base)) {
|
|
158
|
+
return inconsistent('module-producer-path-mismatch');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (facts.standardWrapper) {
|
|
162
|
+
const wrapper = facts.standardWrapper;
|
|
163
|
+
if (facts.language !== 'rust' || wrapper.callKind !== 'method' || !wrapper.receiver ||
|
|
164
|
+
!['unwrap', 'expect'].includes(wrapper.method)) {
|
|
165
|
+
return incomplete('invalid-standard-wrapper-contract');
|
|
166
|
+
}
|
|
167
|
+
let kind;
|
|
168
|
+
if (wrapper.contract) {
|
|
169
|
+
if (!require('./rust-result-flow').validateRustWrapperContract(wrapper.contract, true)) {
|
|
170
|
+
return incomplete('invalid-standard-wrapper-contract');
|
|
171
|
+
}
|
|
172
|
+
kind = wrapper.contract.kind;
|
|
173
|
+
} else {
|
|
174
|
+
const annotation = wrapper.annotationReceiver;
|
|
175
|
+
const empty = value => Array.isArray(value) && !value.length;
|
|
176
|
+
if (!annotation || !['Result', 'Option'].includes(annotation.type) ||
|
|
177
|
+
annotation.origin?.source !== 'annotation' || annotation.genericParameter !== false ||
|
|
178
|
+
!empty(annotation.wildcardImports) || !empty(annotation.rootDeclarations) || !empty(annotation.rootBindings) ||
|
|
179
|
+
!Array.isArray(annotation.bindings)) return incomplete('invalid-standard-wrapper-annotation');
|
|
180
|
+
const module = annotation.type === 'Result' ? 'result' : 'option';
|
|
181
|
+
const paths = [`std::${module}::${annotation.type}`, `core::${module}::${annotation.type}`];
|
|
182
|
+
if (annotation.qualifier ? !paths.includes(`${annotation.qualifier}::${annotation.type}`)
|
|
183
|
+
: !empty(annotation.localDeclarations) || annotation.bindings.some(b =>
|
|
184
|
+
(b.alias || b.name) !== annotation.type || !paths.includes(b.module))) {
|
|
185
|
+
return incomplete('shadowed-standard-wrapper-annotation');
|
|
186
|
+
}
|
|
187
|
+
kind = annotation.type;
|
|
188
|
+
}
|
|
189
|
+
return { verdict: 'establishes-other', declaration: {
|
|
190
|
+
builtin: kind, method: wrapper.method, language: 'rust',
|
|
191
|
+
} };
|
|
192
|
+
}
|
|
193
|
+
if (facts.receiverOrigin?.wrapperUnwrap || facts.receiverOrigin?.wrapperPattern) {
|
|
194
|
+
const pattern = facts.receiverOrigin.wrapperPattern;
|
|
195
|
+
const unwrap = pattern || facts.receiverOrigin.wrapperUnwrap;
|
|
196
|
+
const validProjection = pattern
|
|
197
|
+
? ['Some', 'Ok'].includes(pattern.variant) &&
|
|
198
|
+
pattern.contract?.kind === (pattern.variant === 'Some' ? 'Option' : 'Result') &&
|
|
199
|
+
Array.isArray(pattern.shadowDeclarations) && !pattern.shadowDeclarations.length &&
|
|
200
|
+
Array.isArray(pattern.shadowBindings) && !pattern.shadowBindings.length &&
|
|
201
|
+
(pattern.source?.variable || (Number.isInteger(pattern.source?.start) &&
|
|
202
|
+
Number.isInteger(pattern.source?.end) && pattern.source.end > pattern.source.start))
|
|
203
|
+
: ['unwrap', 'expect'].includes(unwrap.method);
|
|
204
|
+
if (facts.language !== 'rust' || !validProjection ||
|
|
205
|
+
!require('./rust-result-flow').validateRustWrapperContract(unwrap.contract)) {
|
|
206
|
+
return incomplete('invalid-wrapper-unwrapping-contract');
|
|
207
|
+
}
|
|
208
|
+
if (facts.receiverOrigin.type !== unwrap.contract.type ||
|
|
209
|
+
facts.receiverTypeFlowFile !== unwrap.contract.payload.declaration.file ||
|
|
210
|
+
!sameDeclaration(unwrap.contract.payload.declaration,
|
|
211
|
+
facts.receiverTypeDeclaration || facts.receiverResolvedIn)) {
|
|
212
|
+
return inconsistent('wrapper-payload-identity-mismatch');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (facts.builtinReceiver) {
|
|
216
|
+
if (!require('./receiver-types').isProvenanceBuiltinReceiver(facts.receiverType, facts.language) ||
|
|
217
|
+
facts.builtinReceiver.type !== facts.receiverType ||
|
|
218
|
+
facts.builtinReceiver.language !== facts.language) return inconsistent('builtin-receiver-mismatch');
|
|
219
|
+
if (!facts.receiverOrigin || ['unknown', 'guess'].includes(facts.receiverTypeSource) ||
|
|
220
|
+
facts.receiverOrigin.source !== facts.receiverTypeSource) return incomplete('builtin-origin-not-established');
|
|
221
|
+
return { verdict: 'establishes-other', declaration: { builtin: facts.receiverType, language: facts.language } };
|
|
222
|
+
}
|
|
223
|
+
if (facts.lookup) {
|
|
224
|
+
const { receiver, steps, selected } = facts.lookup;
|
|
225
|
+
if (!identityKey(receiver) || !identityKey(selected) || !Array.isArray(steps) || !steps.length) {
|
|
226
|
+
return incomplete('missing-receiver-lookup');
|
|
227
|
+
}
|
|
228
|
+
if (!sameDeclaration(receiver, steps[0].owner)) return inconsistent('lookup-origin-mismatch');
|
|
229
|
+
if (!sameDeclaration(receiver, facts.receiverResolvedIn)) return inconsistent('receiver-identity-mismatch');
|
|
230
|
+
const aliases = facts.receiverAliases || [];
|
|
231
|
+
const typeDeclaration = facts.receiverTypeDeclaration || receiver;
|
|
232
|
+
if (facts.receiverTypeQualifier && ['java', 'csharp'].includes(facts.language) &&
|
|
233
|
+
facts.receiverTypeQualifier !== typeDeclaration.enclosingType &&
|
|
234
|
+
facts.receiverTypeQualifier !== typeDeclaration.namespace) {
|
|
235
|
+
return inconsistent('receiver-qualifier-mismatch');
|
|
236
|
+
}
|
|
237
|
+
if (facts.receiverImportChain && validateConfirmation({ facts: {
|
|
238
|
+
importChain: facts.receiverImportChain,
|
|
239
|
+
} }, typeDeclaration).verdict !== 'establishes-target') return inconsistent('receiver-import-chain-mismatch');
|
|
240
|
+
let resolvedAlias = typeDeclaration;
|
|
241
|
+
for (const alias of aliases) {
|
|
242
|
+
if (!sameDeclaration(alias.declaration, resolvedAlias) ||
|
|
243
|
+
!alias.aliasOf || !alias.name || !identityKey(alias.target)) return inconsistent('receiver-alias-chain-mismatch');
|
|
244
|
+
const head = String(alias.aliasOf).replace(/^[*&\s]+/, '').split(/[<[]/, 1)[0].trim().split(/::|\./).pop();
|
|
245
|
+
if (head !== alias.name.split(/::|\./).pop() || head !== alias.target.name) return inconsistent('receiver-alias-target-mismatch');
|
|
246
|
+
resolvedAlias = alias.target;
|
|
247
|
+
}
|
|
248
|
+
if (!sameDeclaration(resolvedAlias, receiver)) return inconsistent('receiver-alias-end-mismatch');
|
|
249
|
+
const bound = facts.receiverGenericBound;
|
|
250
|
+
if (bound && (!identityKey(bound.declaration) || bound.parameter !== facts.receiverType ||
|
|
251
|
+
!bound.bounds?.includes(typeDeclaration.name) || !sameDeclaration(bound.selected, typeDeclaration))) {
|
|
252
|
+
return inconsistent('receiver-generic-bound-mismatch');
|
|
253
|
+
}
|
|
254
|
+
const castThis = facts.receiverCastThis;
|
|
255
|
+
if (castThis && (!identityKey(castThis.enclosing) ||
|
|
256
|
+
castThis.enclosing.className !== receiver.name || castThis.enclosing.file !== receiver.file ||
|
|
257
|
+
castThis.interfaceType !== facts.receiverType)) return inconsistent('receiver-this-cast-mismatch');
|
|
258
|
+
const overload = facts.lookup.overload;
|
|
259
|
+
const group = overload && require('./provenance-overload').overloadMemberGroup(steps);
|
|
260
|
+
if (overload && !group) return incomplete('missing-overload-declarations');
|
|
261
|
+
if (overload && !group.some(member => sameDeclaration(declarationIdentity(member), selected))) {
|
|
262
|
+
return inconsistent('selected-member-outside-overload-group');
|
|
263
|
+
}
|
|
264
|
+
const ambiguousOverload = overload?.outcome === 'ambiguous';
|
|
265
|
+
for (let i = 0; i < steps.length; i++) {
|
|
266
|
+
const step = steps[i];
|
|
267
|
+
if (!identityKey(step.owner) || !Array.isArray(step.members) || !Array.isArray(step.parents)) {
|
|
268
|
+
return incomplete('missing-member-lookup-facts');
|
|
269
|
+
}
|
|
270
|
+
const named = step.members.filter(member => member.name === selected.name);
|
|
271
|
+
if (named.some(member => member.className !== step.owner.name ||
|
|
272
|
+
(member.namespace || null) !== (step.owner.namespace || null) ||
|
|
273
|
+
(member.enclosingType || null) !== (step.owner.enclosingType || null) ||
|
|
274
|
+
(facts.language !== 'go' && member.file !== step.owner.file))) {
|
|
275
|
+
return inconsistent('member-owner-mismatch');
|
|
276
|
+
}
|
|
277
|
+
if (i + 1 < steps.length) {
|
|
278
|
+
if (named.length && !overload) return inconsistent('overriding-member-before-target');
|
|
279
|
+
if (!step.parents.some(parent => sameDeclaration(parent, steps[i + 1].owner))) {
|
|
280
|
+
return inconsistent('unproven-inheritance-hop');
|
|
281
|
+
}
|
|
282
|
+
} else if (!overload && (named.length !== 1 || !sameDeclaration(named[0], selected))) {
|
|
283
|
+
const propertyRead = facts.valueReference && step.propertyRead &&
|
|
284
|
+
sameDeclaration(propertyReadMember(named), selected);
|
|
285
|
+
if (!propertyRead && !require('./provenance-overload').validateOverload(step.overload, named, selected)) {
|
|
286
|
+
return incomplete('ambiguous-or-missing-member');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (overload && !require('./provenance-overload').validateOverload(
|
|
291
|
+
overload, group.map(declarationIdentity), selected, invalidCall, ambiguousOverload)) {
|
|
292
|
+
return incomplete(overload.outcome === 'no-fit' ? 'no-applicable-overload' : 'ambiguous-or-missing-member');
|
|
293
|
+
}
|
|
294
|
+
if (facts.receiverTypeSource === 'guess' || facts.receiverTypeSource === 'unknown') {
|
|
295
|
+
return incomplete('receiver-origin-not-established');
|
|
296
|
+
}
|
|
297
|
+
if (!facts.receiverOrigin || facts.receiverOrigin.source === 'unknown') {
|
|
298
|
+
return incomplete('missing-receiver-origin');
|
|
299
|
+
}
|
|
300
|
+
if (facts.receiverOrigin.source !== facts.receiverTypeSource) {
|
|
301
|
+
return inconsistent('receiver-source-mismatch');
|
|
302
|
+
}
|
|
303
|
+
if (ambiguousOverload) {
|
|
304
|
+
// Every overload is known, but the argument shape cannot choose
|
|
305
|
+
// one. This proves no particular target. It can still establish
|
|
306
|
+
// that an unrelated target is outside the entire member group.
|
|
307
|
+
const declarations = group.map(declarationIdentity);
|
|
308
|
+
if (targets.some(target => declarations.some(member => sameDeclaration(target, member)))) {
|
|
309
|
+
return incomplete('ambiguous-or-missing-member');
|
|
310
|
+
}
|
|
311
|
+
return { verdict: 'establishes-other', declarations };
|
|
312
|
+
}
|
|
313
|
+
return verdictFor(selected);
|
|
314
|
+
}
|
|
315
|
+
if (facts.binding) {
|
|
316
|
+
if (!identityKey(facts.binding.declaration) || !facts.binding.referenceId ||
|
|
317
|
+
facts.binding.referenceId !== facts.binding.declaration.bindingId) {
|
|
318
|
+
return incomplete('missing-binding-witness');
|
|
319
|
+
}
|
|
320
|
+
return verdictFor(facts.binding.declaration);
|
|
321
|
+
}
|
|
322
|
+
if (facts.importChain) {
|
|
323
|
+
const chain = facts.importChain;
|
|
324
|
+
if (!chain.length) return incomplete('empty-import-chain');
|
|
325
|
+
for (let i = 0; i < chain.length; i++) {
|
|
326
|
+
const hop = chain[i];
|
|
327
|
+
if (!hop.localName || !hop.importedName || !hop.fromFile || !hop.toFile) {
|
|
328
|
+
return incomplete('missing-import-name-hop');
|
|
329
|
+
}
|
|
330
|
+
if (hop.wildcards) {
|
|
331
|
+
const sources = hop.wildcards;
|
|
332
|
+
if (!Array.isArray(sources) || !sources.length || sources.some(source =>
|
|
333
|
+
!source.file || source.binding?.name !== '*' || !source.binding.topLevel || !source.binding.module ||
|
|
334
|
+
!Number.isInteger(source.binding.origin?.start) || source.exports?.name !== '__all__' ||
|
|
335
|
+
!Number.isInteger(source.exports.origin?.start) || !Array.isArray(source.exports.literals) ||
|
|
336
|
+
!Array.isArray(source.exports.otherReferences) || source.exports.otherReferences.length ||
|
|
337
|
+
source.exports.literals.some(literal => typeof literal.value !== 'string' ||
|
|
338
|
+
literal.origin?.nodeType !== 'string' || !Number.isInteger(literal.origin.start)))) {
|
|
339
|
+
return incomplete('missing-wildcard-export-list');
|
|
340
|
+
}
|
|
341
|
+
const selected = sources.filter(source => source.exports.literals.some(literal => literal.value === hop.importedName));
|
|
342
|
+
if (selected.length !== 1 || selected[0].file !== hop.toFile ||
|
|
343
|
+
selected[0].binding.module !== hop.module || hop.localName !== hop.importedName) {
|
|
344
|
+
return inconsistent('wildcard-export-owner-mismatch');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (i > 0 && (chain[i - 1].toFile !== hop.fromFile ||
|
|
348
|
+
chain[i - 1].importedName !== hop.localName)) {
|
|
349
|
+
return inconsistent('import-name-chain-mismatch');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const last = chain.at(-1);
|
|
353
|
+
if (!identityKey(last.declaration) || last.declaration.file !== last.toFile ||
|
|
354
|
+
last.declaration.name !== last.importedName) return incomplete('missing-import-declaration');
|
|
355
|
+
return verdictFor(last.declaration);
|
|
356
|
+
}
|
|
357
|
+
// An unsupported rule is an instrumentation result, not a failed proof.
|
|
358
|
+
// A collector that entered a supported lookup must still fail closed if
|
|
359
|
+
// its witness is absent (including when a saved witness was tampered with).
|
|
360
|
+
if (facts.receiverResolvedIn && !facts.lookupUnsupported) return incomplete('missing-receiver-lookup');
|
|
361
|
+
return { verdict: 'unsupported', diagnostic: 'rule-not-yet-validated' };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** An invalid member-family call is a diagnostic, never a confirmed edge. */
|
|
365
|
+
function validateCallMismatch(provenance, target) {
|
|
366
|
+
const lookup = provenance?.facts?.lookup;
|
|
367
|
+
if (lookup?.overload?.outcome !== 'no-fit') return false;
|
|
368
|
+
const members = lookup.steps.flatMap(step => step.members || []);
|
|
369
|
+
if (!members.some(member => sameDeclaration(member, target))) return false;
|
|
370
|
+
return validateConfirmation(provenance, members, true).verdict === 'establishes-target';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function createProvenance(evidence, resolution) {
|
|
374
|
+
const facts = { ...(evidence.facts || {}) };
|
|
375
|
+
const source = facts.receiverTypeSource || 'unknown';
|
|
376
|
+
const rules = [];
|
|
377
|
+
if (evidence.possibleDispatch) rules.push('possible-dispatch');
|
|
378
|
+
if (evidence.methodAmbiguous) rules.push('method-ambiguous');
|
|
379
|
+
if (evidence.resolvedBySameClass) rules.push('same-class');
|
|
380
|
+
if (evidence.extensionMethod) rules.push('extension-method');
|
|
381
|
+
if (evidence.hasReceiverType || evidence.resolvedByReceiverHint) {
|
|
382
|
+
rules.push(TYPE_SOURCE_RULES[source] || 'receiver-type');
|
|
383
|
+
}
|
|
384
|
+
if (evidence.typeQualifiedReceiver) rules.push('type-qualified');
|
|
385
|
+
if (evidence.moduleOwnedPath) rules.push('module-owned');
|
|
386
|
+
if (evidence.hasBindingId) rules.push('binding');
|
|
387
|
+
if (evidence.hasSingleOwnerEvidence) rules.push('single-owner');
|
|
388
|
+
if (evidence.hasImportEvidence) rules.push(facts.importChain ? 'import-chain' : 'import-supported');
|
|
389
|
+
if (evidence.hasReceiverEvidence) rules.push('receiver-binding');
|
|
390
|
+
if (evidence.hasSamePackageEvidence) rules.push('same-package');
|
|
391
|
+
if (!rules.length) rules.push(evidence.reason || resolution || 'unknown');
|
|
392
|
+
const provenance = { rule: rules[0], rules: [...new Set(rules)], facts };
|
|
393
|
+
if (facts.targets?.length) {
|
|
394
|
+
const checked = validateConfirmation(provenance, facts.targets);
|
|
395
|
+
provenance.validation = checked.verdict;
|
|
396
|
+
if (checked.diagnostic) provenance.diagnostic = checked.diagnostic;
|
|
397
|
+
} else {
|
|
398
|
+
provenance.validation = 'incomplete';
|
|
399
|
+
provenance.diagnostic = 'missing-target-identity';
|
|
400
|
+
}
|
|
401
|
+
return provenance;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function summarizeProvenance(sites) {
|
|
405
|
+
if (!sites?.length) return null;
|
|
406
|
+
const provenance = sites.map(site => site.provenance || site).filter(p => p.rule);
|
|
407
|
+
const rules = [...new Set(provenance.flatMap(p => p.rules || [p.rule]))].sort(codeUnitCompare);
|
|
408
|
+
return {
|
|
409
|
+
// Equal scores retain occurrence order; weaker evidence sets the
|
|
410
|
+
// aggregate rule while every occurrence keeps its own full witness.
|
|
411
|
+
rule: sites.reduce((weakest, site) =>
|
|
412
|
+
(site.evidenceScore ?? Infinity) < (weakest.evidenceScore ?? Infinity) ? site : weakest
|
|
413
|
+
).provenance?.rule || provenance[0].rule,
|
|
414
|
+
rules,
|
|
415
|
+
validation: provenance.every(p => p.validation === 'establishes-target')
|
|
416
|
+
? 'establishes-target' : 'incomplete',
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
module.exports = {
|
|
421
|
+
TYPE_SOURCE_RULES, declarationIdentity, identityKey, sameDeclaration, propertyReadMember,
|
|
422
|
+
validateConfirmation, validateCallMismatch, createProvenance, summarizeProvenance,
|
|
423
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { declarationIdentity, identityKey, sameDeclaration } = require('./provenance');
|
|
5
|
+
const { getLanguageAdapter, getParser } = require('../languages');
|
|
6
|
+
|
|
7
|
+
const metadata = new WeakMap();
|
|
8
|
+
const moduleMetadata = new WeakMap();
|
|
9
|
+
|
|
10
|
+
function moduleEvidence(index, file) {
|
|
11
|
+
const entry = index.files.get(file);
|
|
12
|
+
if (entry?.language !== 'python') return null;
|
|
13
|
+
const cached = moduleMetadata.get(entry);
|
|
14
|
+
if (cached?.hash === entry.hash) return cached.value;
|
|
15
|
+
const value = getLanguageAdapter('python').findPythonModuleEvidence(index._readFile(file), getParser('python'));
|
|
16
|
+
moduleMetadata.set(entry, { hash: entry.hash, value });
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Wildcard hops require a closed literal __all__ on every competing source. */
|
|
21
|
+
function pythonFixtureType(index, file, name, accepts, seen = new Set()) {
|
|
22
|
+
const key = `${file}\0${name}`;
|
|
23
|
+
if (seen.has(key) || seen.size >= 8) return null;
|
|
24
|
+
const visited = new Set(seen).add(key), entry = index.files.get(file);
|
|
25
|
+
if (!entry || (entry.moduleAssignedNames || []).includes(name)) return null;
|
|
26
|
+
const direct = require('./provenance-facts').namedDeclaration(index, file, name, accepts);
|
|
27
|
+
if (direct) return direct;
|
|
28
|
+
const parts = name.split('.');
|
|
29
|
+
if (parts.length === 2) {
|
|
30
|
+
const bindings = (entry.importBindings || []).filter(b => (b.alias || b.name) === parts[0]);
|
|
31
|
+
if (bindings.length !== 1 || (index.symbols.get(parts[0]) || []).some(d => d.file === file)) return null;
|
|
32
|
+
const binding = bindings[0], relative = entry.moduleResolved?.[binding.module];
|
|
33
|
+
const next = relative && pythonFixtureType(index, path.resolve(index.root, relative), parts[1], accepts, visited);
|
|
34
|
+
return next ? { declaration: next.declaration, chain: [{ fromFile: entry.relativePath, toFile: relative,
|
|
35
|
+
localName: name, importedName: parts[1], module: binding.module,
|
|
36
|
+
...(!next.chain.length && { declaration: declarationIdentity(next.declaration) }) }, ...next.chain] } : null;
|
|
37
|
+
}
|
|
38
|
+
if (parts.length !== 1 || (entry.importBindings || []).some(b => (b.alias || b.name) === name) ||
|
|
39
|
+
(index.symbols.get(name) || []).some(d => d.file === file)) return null;
|
|
40
|
+
const wildcards = moduleEvidence(index, file)?.wildcards;
|
|
41
|
+
if (!wildcards?.length || wildcards.some(b => !b.topLevel || !b.module)) return null;
|
|
42
|
+
const sources = [];
|
|
43
|
+
for (const binding of wildcards) {
|
|
44
|
+
const relative = entry.moduleResolved?.[binding.module];
|
|
45
|
+
const evidence = relative && moduleEvidence(index, path.resolve(index.root, relative));
|
|
46
|
+
if (!evidence?.exports) return null;
|
|
47
|
+
sources.push({ binding, file: relative, exports: evidence.exports });
|
|
48
|
+
}
|
|
49
|
+
const candidates = sources.filter(s => s.exports.literals.some(literal => literal.value === name));
|
|
50
|
+
if (candidates.length !== 1) return null;
|
|
51
|
+
const selected = candidates[0], next = pythonFixtureType(index, path.resolve(index.root, selected.file), name, accepts, visited);
|
|
52
|
+
return next ? { declaration: next.declaration, chain: [{ fromFile: entry.relativePath, toFile: selected.file,
|
|
53
|
+
localName: name, importedName: name, module: selected.binding.module, wildcards: sources,
|
|
54
|
+
...(!next.chain.length && { declaration: declarationIdentity(next.declaration) }) }, ...next.chain] } : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function functionsIn(index, file) {
|
|
58
|
+
const entry = index.files.get(file);
|
|
59
|
+
if (entry?.language !== 'python') return [];
|
|
60
|
+
const cached = metadata.get(entry);
|
|
61
|
+
if (cached?.hash === entry.hash) return cached.functions;
|
|
62
|
+
const functions = getLanguageAdapter('python').findPytestFunctions(index._readFile(file), getParser('python'));
|
|
63
|
+
metadata.set(entry, { hash: entry.hash, functions });
|
|
64
|
+
return functions;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function importedPath(index, file, parts, module, member) {
|
|
68
|
+
if (!parts?.length) return null;
|
|
69
|
+
const entry = index.files.get(file);
|
|
70
|
+
if ((entry.moduleAssignedNames || []).includes(parts[0]) ||
|
|
71
|
+
(index.symbols.get(parts[0]) || []).some(d => d.file === file)) return null;
|
|
72
|
+
const bindings = (entry.importBindings || []).filter(b => (b.alias || b.name) === parts[0]);
|
|
73
|
+
if (bindings.length !== 1) return null;
|
|
74
|
+
const binding = bindings[0];
|
|
75
|
+
const resolved = binding.kind === 'from' || binding.kind === 'from-import'
|
|
76
|
+
? [binding.module, binding.name, ...parts.slice(1)].join('.')
|
|
77
|
+
: [binding.module, ...parts.slice(1)].join('.');
|
|
78
|
+
if (resolved !== `${module}.${member}` || entry.moduleResolved?.[binding.module]) return null;
|
|
79
|
+
return { ...binding };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Resolve only the conventional, undecorated module test's unchanged
|
|
83
|
+
* parameter. The nearest explicit fixture overrides parent conftest files;
|
|
84
|
+
* parametrized tests, dynamic fixture names and undecidable imports abstain.
|
|
85
|
+
*/
|
|
86
|
+
function pythonFixtureReceiver(index, file, call, options) {
|
|
87
|
+
const receiver = call.receiver || call.receiverRoot;
|
|
88
|
+
if (!receiver || call.receiverType || call.receiverRootType ||
|
|
89
|
+
call.receiverFlowInvalidated || !/^test_.*\.py$|^.*_test\.py$/.test(path.basename(file))) return null;
|
|
90
|
+
const test = functionsIn(index, file).find(fn => fn.name.startsWith('test_') &&
|
|
91
|
+
fn.startLine <= call.line && fn.endLine >= call.line && !fn.async && !fn.decorators.length);
|
|
92
|
+
const parameter = test?.parameters.find(p => p.name === receiver && p.unchanged);
|
|
93
|
+
if (!parameter || call.enclosingFunction?.name !== test.name) return null;
|
|
94
|
+
const sources = [file];
|
|
95
|
+
for (let dir = path.dirname(file); dir === index.root || dir.startsWith(index.root + path.sep); dir = path.dirname(dir)) {
|
|
96
|
+
const candidate = path.join(dir, 'conftest.py');
|
|
97
|
+
if (candidate !== file && index.files.has(candidate)) sources.push(candidate);
|
|
98
|
+
if (dir === index.root) break;
|
|
99
|
+
}
|
|
100
|
+
for (const source of sources) {
|
|
101
|
+
const entry = index.files.get(source);
|
|
102
|
+
if ((entry?.moduleAssignedNames || []).includes(receiver) ||
|
|
103
|
+
(entry?.importBindings || []).some(b => (b.alias || b.name) === receiver || b.name === '*')) return null;
|
|
104
|
+
const candidates = functionsIn(index, source).filter(fn => fn.decorators.length &&
|
|
105
|
+
(fn.name === receiver || fn.decorators.some(d => d.alias === receiver || d.dynamicName)));
|
|
106
|
+
if (!candidates.length) continue;
|
|
107
|
+
if (candidates.length !== 1) return null;
|
|
108
|
+
const fixture = candidates[0];
|
|
109
|
+
if (fixture.async || fixture.decorators.length !== 1 || !fixture.valuePath) return null;
|
|
110
|
+
const decorator = fixture.decorators[0];
|
|
111
|
+
const binding = !decorator.dynamicName && importedPath(index, source, decorator.path, 'pytest', 'fixture');
|
|
112
|
+
if (!binding || (decorator.alias || fixture.name) !== receiver || !options.externalModule(source, binding.module)) return null;
|
|
113
|
+
let iteratorBinding;
|
|
114
|
+
if (fixture.yields) {
|
|
115
|
+
const container = fixture.container?.at(-1);
|
|
116
|
+
if (!['Iterator', 'Generator'].includes(container)) return null;
|
|
117
|
+
iteratorBinding = importedPath(index, source, fixture.container, 'typing', container) ||
|
|
118
|
+
importedPath(index, source, fixture.container, 'collections.abc', container);
|
|
119
|
+
if (!iteratorBinding || !options.externalModule(source, iteratorBinding.module)) return null;
|
|
120
|
+
}
|
|
121
|
+
const parts = [...fixture.valuePath], type = parts.pop();
|
|
122
|
+
const root = options.resolveType(source, type, parts.join('.') || undefined);
|
|
123
|
+
if (!root?.declaration) return null;
|
|
124
|
+
const definition = (index.symbols.get(fixture.name) || []).find(d => d.file === source &&
|
|
125
|
+
d.startLine === fixture.startLine && d.endLine === fixture.endLine && !d.className);
|
|
126
|
+
const testDefinition = (index.symbols.get(test.name) || []).find(d => d.file === file &&
|
|
127
|
+
d.startLine === test.startLine && d.endLine === test.endLine && !d.className);
|
|
128
|
+
if (!definition || !testDefinition) return null;
|
|
129
|
+
const proof = { parameter: { ...parameter, test: declarationIdentity(testDefinition) },
|
|
130
|
+
fixture: { ...declarationIdentity(definition), returnType: fixture.returnType },
|
|
131
|
+
decorator: { ...decorator, binding }, iteratorBinding: iteratorBinding || null,
|
|
132
|
+
yields: fixture.yields, container: fixture.container || null, returnOrigin: fixture.returnOrigin,
|
|
133
|
+
valuePath: fixture.valuePath, value: declarationIdentity(root.declaration),
|
|
134
|
+
valueImportChain: root.chain || [], searchFiles: sources.slice(0, sources.indexOf(source) + 1)
|
|
135
|
+
.map(f => path.relative(index.root, f)), fields: [] };
|
|
136
|
+
let result = { type, fromFile: root.declaration.file };
|
|
137
|
+
for (const field of call.receiverRoot ? (call.receiverFields || [call.receiverField]) : []) {
|
|
138
|
+
const next = options.field(result.type, result.fromFile, field);
|
|
139
|
+
if (!next) return null;
|
|
140
|
+
proof.fields.push(next.fact);
|
|
141
|
+
result = { type: next.type, fromFile: next.fromFile };
|
|
142
|
+
}
|
|
143
|
+
return { ...result, fixtureBinding: proof };
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Replay fixture selection, annotation binding and every declared property hop.
|
|
149
|
+
* A framework label alone cannot establish the value supplied to a parameter.
|
|
150
|
+
*/
|
|
151
|
+
function validatePythonFixtureBinding(proof, facts) {
|
|
152
|
+
const empty = value => Array.isArray(value) && !value.length;
|
|
153
|
+
const span = origin => Number.isInteger(origin?.start) && Number.isInteger(origin?.end) && origin.end > origin.start;
|
|
154
|
+
const bindingPath = (binding, parts) => {
|
|
155
|
+
if (!binding || !parts?.length || (binding.alias || binding.name) !== parts[0]) return null;
|
|
156
|
+
return ['from', 'from-import'].includes(binding.kind)
|
|
157
|
+
? [binding.module, binding.name, ...parts.slice(1)].join('.')
|
|
158
|
+
: [binding.module, ...parts.slice(1)].join('.');
|
|
159
|
+
};
|
|
160
|
+
const typed = (file, name, declaration, chain) => {
|
|
161
|
+
if (!identityKey(declaration) || !Array.isArray(chain)) return false;
|
|
162
|
+
if (!chain.length) return declaration.file === file && declaration.name === name;
|
|
163
|
+
return chain[0].fromFile === file && chain[0].localName === name &&
|
|
164
|
+
require('./provenance').validateConfirmation({ facts: { importChain: chain } }, declaration)
|
|
165
|
+
.verdict === 'establishes-target';
|
|
166
|
+
};
|
|
167
|
+
if (facts.language !== 'python' || facts.receiverTypeSource !== 'fixture' ||
|
|
168
|
+
!proof || !identityKey(proof.fixture) || !identityKey(proof.parameter?.test)) return false;
|
|
169
|
+
const { parameter, fixture, decorator, searchFiles } = proof;
|
|
170
|
+
if (!parameter.unchanged || !span(parameter.origin) || !parameter.name ||
|
|
171
|
+
!Array.isArray(facts.receiverPath) || facts.receiverPath[0] !== parameter.name ||
|
|
172
|
+
!Array.isArray(proof.fields) || proof.fields.length !== facts.receiverPath.length - 1 ||
|
|
173
|
+
proof.fields.some((field, i) => field.member?.name !== facts.receiverPath[i + 1]) ||
|
|
174
|
+
parameter.test.file !== facts.site?.file || !parameter.test.name.startsWith('test_') ||
|
|
175
|
+
parameter.test.className || fixture.className || facts.site.line < parameter.test.startLine ||
|
|
176
|
+
facts.site.line > parameter.test.endLine || !span(decorator?.origin) || decorator.dynamicName ||
|
|
177
|
+
(decorator.alias || fixture.name) !== parameter.name ||
|
|
178
|
+
bindingPath(decorator.binding, decorator.path) !== 'pytest.fixture' ||
|
|
179
|
+
!span(proof.returnOrigin) || !Array.isArray(searchFiles) || !searchFiles.length ||
|
|
180
|
+
searchFiles[0] !== parameter.test.file || searchFiles.at(-1) !== fixture.file ||
|
|
181
|
+
!proof.valuePath?.length) return false;
|
|
182
|
+
let dir = path.posix.dirname(parameter.test.file);
|
|
183
|
+
for (const file of searchFiles.slice(1)) {
|
|
184
|
+
const next = path.posix.dirname(file);
|
|
185
|
+
if (path.posix.basename(file) !== 'conftest.py' ||
|
|
186
|
+
!(next === dir || next === '.' || dir.startsWith(next + '/'))) return false;
|
|
187
|
+
dir = next;
|
|
188
|
+
}
|
|
189
|
+
const valueName = proof.valuePath.join('.');
|
|
190
|
+
if (proof.yields) {
|
|
191
|
+
const container = proof.container?.at(-1);
|
|
192
|
+
const qualified = bindingPath(proof.iteratorBinding, proof.container);
|
|
193
|
+
if (!['Iterator', 'Generator'].includes(container) ||
|
|
194
|
+
![`typing.${container}`, `collections.abc.${container}`].includes(qualified) ||
|
|
195
|
+
fixture.returnType.replace(/\s/g, '') !== `${proof.container.join('.')}[${valueName}${
|
|
196
|
+
container === 'Generator' ? ',None,None' : ''}]`) return false;
|
|
197
|
+
} else if (proof.iteratorBinding || proof.container || fixture.returnType !== valueName) return false;
|
|
198
|
+
if (!typed(fixture.file, valueName, proof.value, proof.valueImportChain)) return false;
|
|
199
|
+
let previous = proof.value;
|
|
200
|
+
for (const field of proof.fields || []) {
|
|
201
|
+
if (!sameDeclaration(field.owner, previous) || !identityKey(field.member) ||
|
|
202
|
+
field.member.file !== previous.file || field.member.className !== previous.name ||
|
|
203
|
+
!['property', 'getter', 'field'].includes(field.member.kind) || !field.annotation) return false;
|
|
204
|
+
if (field.result?.builtin) {
|
|
205
|
+
if (field.annotation !== field.result.builtin || field.result.language !== 'python' ||
|
|
206
|
+
!require('./receiver-types').isProvenanceBuiltinReceiver(field.result.builtin, 'python') ||
|
|
207
|
+
!empty(field.builtinShadowDeclarations) || !empty(field.builtinShadowBindings)) return false;
|
|
208
|
+
} else if (!typed(field.member.file, field.annotation, field.result, field.importChain)) return false;
|
|
209
|
+
previous = field.result;
|
|
210
|
+
}
|
|
211
|
+
return previous.builtin ? previous.builtin === facts.receiverType
|
|
212
|
+
: previous.file === facts.receiverTypeFlowFile &&
|
|
213
|
+
sameDeclaration(previous, facts.receiverTypeDeclaration || facts.receiverResolvedIn);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { pythonFixtureReceiver, pythonFixtureType, validatePythonFixtureBinding };
|