mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
fix(ingestion): keep operator branches of a value-selecting source opaque (#3354)
The fan-out sent every branch of `??` / `||` / `or` / `?:` to emitAssignmentFact on its own, including branches that compute a value. `Handlers.fallback === run || fb` then emitted the comparison as a seed whose qualified text sliced to receiver `Handlers`, member `run`, and resolveBoundMemberCandidates minted a CALLS edge to `Handlers.run`. The same happened for `this.state !== run ?? this.fallback`, `this.state + run || this.fallback`, and Python `self.state != run or self.fallback`. Before the fan-out, the whole compound source was one opaque seed. A branch that is a binary operator expression now contributes nothing. The check uses the field vocabulary valueBranches already reads (a `left`/`right` pair, or an `operator`/`operators`/`op` token after the expression start), not grammar type names. Member accesses that field their `.`/`->` as `operator` (Ruby `call`, C/C++ `field_expression`) also field a member name through the list memberParts uses, now shared as memberNameNode, so they stay designators. Unary `&f`/`*fp` lead with their operator and stay designators. Call results were already dropped by emitAssignmentFact, and lambdas and callable references are unchanged. CALLS-edge diff over 87 lang-resolution fixtures (505 -> 501 edges): only the four false edges above were removed, and none were added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6eea55d7a4
commit
8ad0d8db7f
5 changed files with 157 additions and 13 deletions
|
|
@ -710,7 +710,8 @@ const RIGHT_SELECTING_OPERATORS = new Set(['&&', 'and']);
|
||||||
* branches, so each branch flows into the destination. `a && b` / `a and b`
|
* branches, so each branch flows into the destination. `a && b` / `a and b`
|
||||||
* can only yield a callable through `b`, so `x and f or g` reaches `f` and `g`.
|
* can only yield a callable through `b`, so `x and f or g` reaches `f` and `g`.
|
||||||
* Anything else is its own single alternative, which leaves every other
|
* Anything else is its own single alternative, which leaves every other
|
||||||
* source shape untouched.
|
* source shape untouched. A branch that is itself an operator expression
|
||||||
|
* (`x.kind === f || g`) yields a computed value, so it contributes nothing.
|
||||||
* `options.valueAlternatives` is consulted first for grammars whose shape the
|
* `options.valueAlternatives` is consulted first for grammars whose shape the
|
||||||
* field-based rule below cannot see.
|
* field-based rule below cannot see.
|
||||||
*/
|
*/
|
||||||
|
|
@ -726,7 +727,12 @@ function valueAlternatives(
|
||||||
for (let current = pending.pop(); current !== undefined; current = pending.pop()) {
|
for (let current = pending.pop(); current !== undefined; current = pending.pop()) {
|
||||||
const branches = valueBranches(current, options);
|
const branches = valueBranches(current, options);
|
||||||
if (branches === undefined) {
|
if (branches === undefined) {
|
||||||
out.push(current);
|
// An operator branch is opaque, as the whole compound source was before
|
||||||
|
// the fan-out: emitted alone, `x.kind === Handlers.run` becomes a seed
|
||||||
|
// whose qualified text slices to receiver `Handlers`, member `run`.
|
||||||
|
if (current === node || !isBinaryOperatorExpression(unwrapParentheses(current))) {
|
||||||
|
out.push(current);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Reverse push keeps the left-to-right branch order on output.
|
// Reverse push keeps the left-to-right branch order on output.
|
||||||
|
|
@ -744,12 +750,7 @@ function valueBranches(
|
||||||
node: SyntaxNode,
|
node: SyntaxNode,
|
||||||
options: CallableFlowCaptureOptions,
|
options: CallableFlowCaptureOptions,
|
||||||
): readonly SyntaxNode[] | undefined {
|
): readonly SyntaxNode[] | undefined {
|
||||||
let inner = node;
|
const inner = unwrapParentheses(node);
|
||||||
while (inner.type.includes('parenthesized') && inner.namedChildCount === 1) {
|
|
||||||
const child = inner.namedChild(0);
|
|
||||||
if (child === null) break;
|
|
||||||
inner = child;
|
|
||||||
}
|
|
||||||
const provided = options.valueAlternatives?.(inner);
|
const provided = options.valueAlternatives?.(inner);
|
||||||
if (provided !== undefined) {
|
if (provided !== undefined) {
|
||||||
return provided.length === 1 && provided[0]?.id === inner.id ? undefined : provided;
|
return provided.length === 1 && provided[0]?.id === inner.id ? undefined : provided;
|
||||||
|
|
@ -768,6 +769,38 @@ function valueBranches(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unwrapParentheses(node: SyntaxNode): SyntaxNode {
|
||||||
|
let inner = node;
|
||||||
|
while (inner.type.includes('parenthesized') && inner.namedChildCount === 1) {
|
||||||
|
const child = inner.namedChild(0);
|
||||||
|
if (child === null) break;
|
||||||
|
inner = child;
|
||||||
|
}
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True for an expression that computes a value from two operands (`a === b`,
|
||||||
|
* `a + b`, `a is b`), which designates neither operand. Read from the same
|
||||||
|
* field vocabulary as `valueBranches` rather than grammar type names: a
|
||||||
|
* `left`/`right` pair, or an operator token (`operator`, Python's
|
||||||
|
* `operators`, Swift's `op`) that follows the expression's start. A member
|
||||||
|
* access that fields its `.` / `->` as `operator` (Ruby `call`, C/C++
|
||||||
|
* `field_expression`) also fields its member name, so it stays a designator;
|
||||||
|
* a unary `&f` / `*fp` leads with its operator and stays one too.
|
||||||
|
*/
|
||||||
|
function isBinaryOperatorExpression(node: SyntaxNode): boolean {
|
||||||
|
if (node.childForFieldName('left') !== null && node.childForFieldName('right') !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (memberNameNode(node) !== null) return false;
|
||||||
|
const operator =
|
||||||
|
node.childForFieldName('operator') ??
|
||||||
|
node.childForFieldName('operators') ??
|
||||||
|
node.childForFieldName('op');
|
||||||
|
return operator !== null && operator.startIndex > node.startIndex;
|
||||||
|
}
|
||||||
|
|
||||||
function emitAssignmentFact(
|
function emitAssignmentFact(
|
||||||
assignment: AssignmentParts,
|
assignment: AssignmentParts,
|
||||||
knownCallableNames: ReadonlySet<string>,
|
knownCallableNames: ReadonlySet<string>,
|
||||||
|
|
@ -1202,11 +1235,7 @@ function memberParts(
|
||||||
// stay unaffected. Without it every `x.f(arg)` in such a grammar collapsed
|
// stay unaffected. Without it every `x.f(arg)` in such a grammar collapsed
|
||||||
// to a DIRECT call named `f` and the flow solver fanned the argument out to
|
// to a DIRECT call named `f` and the flow solver fanned the argument out to
|
||||||
// every same-named callable.
|
// every same-named callable.
|
||||||
const memberNode =
|
const memberNode = memberNameNode(node);
|
||||||
node.childForFieldName('property') ??
|
|
||||||
node.childForFieldName('field') ??
|
|
||||||
node.childForFieldName('method') ??
|
|
||||||
node.childForFieldName('member');
|
|
||||||
if (receiverNode === null || memberNode === null) return undefined;
|
if (receiverNode === null || memberNode === null) return undefined;
|
||||||
const receiver = operandSyntax(receiverNode, options);
|
const receiver = operandSyntax(receiverNode, options);
|
||||||
const member = operandSyntax(memberNode, options);
|
const member = operandSyntax(memberNode, options);
|
||||||
|
|
@ -1219,6 +1248,17 @@ function memberParts(
|
||||||
return { receiver, member, ...(operator !== undefined ? { operator: operator.text } : {}) };
|
return { receiver, member, ...(operator !== undefined ? { operator: operator.text } : {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The member-name child of a member access, under the field names the
|
||||||
|
* grammars use for it. */
|
||||||
|
function memberNameNode(node: SyntaxNode): SyntaxNode | null {
|
||||||
|
return (
|
||||||
|
node.childForFieldName('property') ??
|
||||||
|
node.childForFieldName('field') ??
|
||||||
|
node.childForFieldName('method') ??
|
||||||
|
node.childForFieldName('member')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function operandSyntax(
|
function operandSyntax(
|
||||||
input: SyntaxNode,
|
input: SyntaxNode,
|
||||||
options: CallableFlowCaptureOptions,
|
options: CallableFlowCaptureOptions,
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,32 @@ def ternary(fast):
|
||||||
def and_or(ready):
|
def and_or(ready):
|
||||||
run = ready and run_and or run_or_else
|
run = ready and run_and or run_or_else
|
||||||
run()
|
run()
|
||||||
|
|
||||||
|
|
||||||
|
# A comparison branch of `or` yields a bool, never what it compares against:
|
||||||
|
# none of these may reach `run`; the `self.fallback` branch still flows.
|
||||||
|
class Handlers:
|
||||||
|
@staticmethod
|
||||||
|
def run():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def comparison_branch(x, fb):
|
||||||
|
h = x.kind == Handlers.run or fb
|
||||||
|
h()
|
||||||
|
|
||||||
|
|
||||||
|
class Machine:
|
||||||
|
def run(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fallback(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def self_comparison(self):
|
||||||
|
h = self.state != self.run or self.fallback
|
||||||
|
h()
|
||||||
|
|
||||||
|
def bare_comparison(self, run):
|
||||||
|
h = self.state != run or self.fallback
|
||||||
|
h()
|
||||||
|
|
|
||||||
42
gitnexus/test/fixtures/lang-resolution/typescript-callable-alternatives/compare.ts
vendored
Normal file
42
gitnexus/test/fixtures/lang-resolution/typescript-callable-alternatives/compare.ts
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// A branch of `||` / `??` / `?:` that is a comparison yields a boolean, never
|
||||||
|
// the member it compares against. None of these bindings may reach `run`.
|
||||||
|
export class Handlers {
|
||||||
|
static run(x: unknown): void {}
|
||||||
|
static fallback(x: unknown): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function comparisonBranch(x: { kind: unknown }, fb: (x: unknown) => void) {
|
||||||
|
const h = x.kind === Handlers.run || fb;
|
||||||
|
h(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function staticComparison(run: unknown, fb: (x: unknown) => void) {
|
||||||
|
const h = Handlers.fallback === run || fb;
|
||||||
|
h(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Machine {
|
||||||
|
state: unknown;
|
||||||
|
run(): void {}
|
||||||
|
fallback(): void {}
|
||||||
|
|
||||||
|
thisComparison() {
|
||||||
|
const h = this.state !== this.run || this.fallback;
|
||||||
|
h();
|
||||||
|
}
|
||||||
|
|
||||||
|
bareComparison(run: unknown) {
|
||||||
|
const h = this.state !== run ?? this.fallback;
|
||||||
|
h();
|
||||||
|
}
|
||||||
|
|
||||||
|
arithmeticBranch(run: number) {
|
||||||
|
const h = this.state + run || this.fallback;
|
||||||
|
h();
|
||||||
|
}
|
||||||
|
|
||||||
|
ternaryComparison(fast: boolean) {
|
||||||
|
const h = fast ? this.state === this.run : this.fallback;
|
||||||
|
h();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,17 @@ describe('Python callable chosen by `or` / `and` / `x if c else y`', () => {
|
||||||
'and_or → run_or_else',
|
'and_or → run_or_else',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('a comparison branch of `or` flows nothing, while its designator sibling still does', () => {
|
||||||
|
expect(
|
||||||
|
callsOf(result).filter(
|
||||||
|
(edge) =>
|
||||||
|
edge.startsWith('comparison_branch → ') ||
|
||||||
|
edge.startsWith('self_comparison → ') ||
|
||||||
|
edge.startsWith('bare_comparison → '),
|
||||||
|
),
|
||||||
|
).toEqual(['bare_comparison → fallback', 'self_comparison → fallback']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Kotlin callable chosen by `?:` / `if` expression', () => {
|
describe('Kotlin callable chosen by `?:` / `if` expression', () => {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
* claiming `epistemic: "exact"`. Each branch of `??`, `||`, and `?:` can be
|
* claiming `epistemic: "exact"`. Each branch of `??`, `||`, and `?:` can be
|
||||||
* the value that is later invoked, so each branch is a flow into the binding.
|
* the value that is later invoked, so each branch is a flow into the binding.
|
||||||
* `a && b` can only yield a callable through `b`, so only `b` flows.
|
* `a && b` can only yield a callable through `b`, so only `b` flows.
|
||||||
|
* A branch that is itself a comparison or arithmetic expression yields a
|
||||||
|
* computed value, so it flows nothing (compare.ts).
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll } from 'vitest';
|
import { describe, it, expect, beforeAll } from 'vitest';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
@ -68,4 +70,24 @@ describe('TypeScript callable chosen by ?? / || / ?: / &&', () => {
|
||||||
const fromLogicalAnd = calls().filter((edge) => edge.startsWith('logicalAnd → '));
|
const fromLogicalAnd = calls().filter((edge) => edge.startsWith('logicalAnd → '));
|
||||||
expect(fromLogicalAnd).toEqual(['logicalAnd → runAndRight']);
|
expect(fromLogicalAnd).toEqual(['logicalAnd → runAndRight']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// `x.kind === Handlers.run` emitted as its own source becomes a seed whose
|
||||||
|
// qualified text slices to receiver `Handlers` and member `run`.
|
||||||
|
it('a comparison branch never reaches the static member it compares against', () => {
|
||||||
|
const fromComparisons = calls().filter(
|
||||||
|
(edge) => edge.startsWith('comparisonBranch → ') || edge.startsWith('staticComparison → '),
|
||||||
|
);
|
||||||
|
expect(fromComparisons).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a comparison or arithmetic branch never reaches a same-named method of `this`', () => {
|
||||||
|
const fromOperators = calls().filter(
|
||||||
|
(edge) =>
|
||||||
|
edge.startsWith('bareComparison → ') ||
|
||||||
|
edge.startsWith('arithmeticBranch → ') ||
|
||||||
|
edge.startsWith('thisComparison → ') ||
|
||||||
|
edge.startsWith('ternaryComparison → '),
|
||||||
|
);
|
||||||
|
expect(fromOperators).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue