mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-02 02:11:29 +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`
|
||||
* 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
|
||||
* 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
|
||||
* field-based rule below cannot see.
|
||||
*/
|
||||
|
|
@ -726,7 +727,12 @@ function valueAlternatives(
|
|||
for (let current = pending.pop(); current !== undefined; current = pending.pop()) {
|
||||
const branches = valueBranches(current, options);
|
||||
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;
|
||||
}
|
||||
// Reverse push keeps the left-to-right branch order on output.
|
||||
|
|
@ -744,12 +750,7 @@ function valueBranches(
|
|||
node: SyntaxNode,
|
||||
options: CallableFlowCaptureOptions,
|
||||
): readonly SyntaxNode[] | undefined {
|
||||
let inner = node;
|
||||
while (inner.type.includes('parenthesized') && inner.namedChildCount === 1) {
|
||||
const child = inner.namedChild(0);
|
||||
if (child === null) break;
|
||||
inner = child;
|
||||
}
|
||||
const inner = unwrapParentheses(node);
|
||||
const provided = options.valueAlternatives?.(inner);
|
||||
if (provided !== undefined) {
|
||||
return provided.length === 1 && provided[0]?.id === inner.id ? undefined : provided;
|
||||
|
|
@ -768,6 +769,38 @@ function valueBranches(
|
|||
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(
|
||||
assignment: AssignmentParts,
|
||||
knownCallableNames: ReadonlySet<string>,
|
||||
|
|
@ -1202,11 +1235,7 @@ function memberParts(
|
|||
// 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
|
||||
// every same-named callable.
|
||||
const memberNode =
|
||||
node.childForFieldName('property') ??
|
||||
node.childForFieldName('field') ??
|
||||
node.childForFieldName('method') ??
|
||||
node.childForFieldName('member');
|
||||
const memberNode = memberNameNode(node);
|
||||
if (receiverNode === null || memberNode === null) return undefined;
|
||||
const receiver = operandSyntax(receiverNode, options);
|
||||
const member = operandSyntax(memberNode, options);
|
||||
|
|
@ -1219,6 +1248,17 @@ function memberParts(
|
|||
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(
|
||||
input: SyntaxNode,
|
||||
options: CallableFlowCaptureOptions,
|
||||
|
|
|
|||
|
|
@ -31,3 +31,32 @@ def ternary(fast):
|
|||
def and_or(ready):
|
||||
run = ready and run_and or run_or_else
|
||||
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',
|
||||
]);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
* claiming `epistemic: "exact"`. Each branch of `??`, `||`, and `?:` can be
|
||||
* 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 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 path from 'path';
|
||||
|
|
@ -68,4 +70,24 @@ describe('TypeScript callable chosen by ?? / || / ?: / &&', () => {
|
|||
const fromLogicalAnd = calls().filter((edge) => edge.startsWith('logicalAnd → '));
|
||||
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