mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
fix(ingestion): flow the right operand of && / and into callable bindings (#3354)
`a && b` / `a and b` yields `a` when it is falsy and `b` otherwise. A falsy value is never a callable, so the right operand is the only one that can be invoked later. The capture left `&&` unexpanded, and the compound source became a qualified seed that resolves to nothing: `const run = x && f; run()` gained no edge to `f` while impact claimed `exact`. Python `x and f or g` reached only `g`. The shared expansion now maps `&&` / `and` to the right branch only. It recurses, so `x and f or g` reaches both `f` and `g`. Where `&&` yields a boolean (Java, C#, Go, Rust, C, C++, PHP, Zig), the destination cannot be invoked, so the flow never meets a call. A before/after CALLS diff over all 83 lang-resolution fixtures that contain `&&` or `and` shows exactly one new edge, logicalAnd -> runAndRight. PHP and Ruby bind low-precedence `and` looser than `=`, so `$g = $x and $y` never reaches this rule. The Python golden digest changes only for the extended python-callable-alternatives fixture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
83b029f09e
commit
6eea55d7a4
6 changed files with 45 additions and 21 deletions
|
|
@ -695,11 +695,22 @@ function assignmentParts(
|
|||
/** Operators whose result is one of their operands, not a computed value. */
|
||||
const VALUE_SELECTING_OPERATORS = new Set(['??', '||', 'or']);
|
||||
|
||||
/**
|
||||
* Operators that yield their left operand when it is falsy and their right
|
||||
* operand otherwise. A falsy value is never a callable, so only the RIGHT
|
||||
* operand can be the callable that is later invoked. Where `&&` / `and`
|
||||
* yields a boolean instead (Java, C#, Go, Rust, C, C++, PHP, Zig), the
|
||||
* destination is not callable, so the flow never meets an invoke.
|
||||
*/
|
||||
const RIGHT_SELECTING_OPERATORS = new Set(['&&', 'and']);
|
||||
|
||||
/**
|
||||
* The operands a value-selecting expression can evaluate to (#3354):
|
||||
* `a ?? b`, `a || b`, `a or b`, and `c ? a : b` each yield one of their
|
||||
* branches, so each branch flows into the destination. Anything else is its
|
||||
* own single alternative, which leaves every other source shape untouched.
|
||||
* 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.
|
||||
* `options.valueAlternatives` is consulted first for grammars whose shape the
|
||||
* field-based rule below cannot see.
|
||||
*/
|
||||
|
|
@ -751,9 +762,9 @@ function valueBranches(
|
|||
const left = inner.childForFieldName('left');
|
||||
const right = inner.childForFieldName('right');
|
||||
const operator = inner.childForFieldName('operator')?.type;
|
||||
if (left !== null && right !== null && operator && VALUE_SELECTING_OPERATORS.has(operator)) {
|
||||
return [left, right];
|
||||
}
|
||||
if (left === null || right === null || !operator) return undefined;
|
||||
if (VALUE_SELECTING_OPERATORS.has(operator)) return [left, right];
|
||||
if (RIGHT_SELECTING_OPERATORS.has(operator)) return [right];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ def run_else():
|
|||
pass
|
||||
|
||||
|
||||
def run_and():
|
||||
pass
|
||||
|
||||
|
||||
def run_or_else():
|
||||
pass
|
||||
|
||||
|
||||
def logical_or(override):
|
||||
run = override or run_sweep
|
||||
run()
|
||||
|
|
@ -18,3 +26,8 @@ def logical_or(override):
|
|||
def ternary(fast):
|
||||
run = run_then if fast else run_else
|
||||
run()
|
||||
|
||||
|
||||
def and_or(ready):
|
||||
run = ready and run_and or run_or_else
|
||||
run()
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ export async function callableLeft(env: { fallback: Handler }) {
|
|||
await run(env);
|
||||
}
|
||||
|
||||
// `&&` is not expanded into branches: a callable left operand is truthy, so
|
||||
// it is never the value. The assignment keeps its pre-#3354 single source.
|
||||
// `a && b` yields `a` when it is falsy and `b` otherwise. A falsy value is
|
||||
// never a callable, so only the right operand can be the one invoked.
|
||||
export async function logicalAnd(env: unknown) {
|
||||
const run = runAndLeft && runAndRight;
|
||||
await run(env);
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@
|
|||
"digest": "9c7f1c54a9b0ba3bfa4410e1843bf5edb570777789ee659a00d7dd6771f3670f"
|
||||
},
|
||||
"python-callable-alternatives/app.py": {
|
||||
"captureGroups": 23,
|
||||
"digest": "5209559e5d1a906536fa4a61b8fc130220e8fea0328b7877d93641d381724e0e"
|
||||
"captureGroups": 35,
|
||||
"digest": "e35b269b282c3955c4a196c6ecc2292c82bbba1c9579590cc970fe2ddee6f3c5"
|
||||
},
|
||||
"python-calls/one.py": {
|
||||
"captureGroups": 4,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const callsOf = (result: PipelineResult): string[] =>
|
|||
getRelationships(result, 'CALLS').filter((edge) => edge.rel.reason === 'callable-value-flow'),
|
||||
);
|
||||
|
||||
describe('Python callable chosen by `or` / `x if c else y`', () => {
|
||||
describe('Python callable chosen by `or` / `and` / `x if c else y`', () => {
|
||||
let result: PipelineResult;
|
||||
beforeAll(async () => {
|
||||
result = await runFixture('python-callable-alternatives');
|
||||
|
|
@ -45,6 +45,13 @@ describe('Python callable chosen by `or` / `x if c else y`', () => {
|
|||
expect.arrayContaining(['ternary → run_then', 'ternary → run_else']),
|
||||
);
|
||||
});
|
||||
|
||||
it('`x and f or g` reaches f and g, never x', () => {
|
||||
expect(callsOf(result).filter((edge) => edge.startsWith('and_or → '))).toEqual([
|
||||
'and_or → run_and',
|
||||
'and_or → run_or_else',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kotlin callable chosen by `?:` / `if` expression', () => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
* of `runSweep`, and `impact` answered with one caller fewer while still
|
||||
* 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.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
|
|
@ -19,15 +20,7 @@ import {
|
|||
type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
/**
|
||||
* `&&` is not value-selecting for a callable, so #3354 leaves it on the single-
|
||||
* source path: whatever the pre-#3354 capture (one seed for the last operand,
|
||||
* qualified by the whole expression, byte-identical before and after this
|
||||
* change) resolved to must stay exactly that: no edge at all.
|
||||
*/
|
||||
const PRE_3354_LOGICAL_AND_EDGES: string[] = [];
|
||||
|
||||
describe('TypeScript callable chosen by ?? / || / ?:', () => {
|
||||
describe('TypeScript callable chosen by ?? / || / ?: / &&', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
|
|
@ -71,8 +64,8 @@ describe('TypeScript callable chosen by ?? / || / ?:', () => {
|
|||
expect(calls()).toContain('callableLeft → runLeft');
|
||||
});
|
||||
|
||||
it('`&&` is not expanded: its left operand gains no edge', () => {
|
||||
it('`a && fn` reaches fn and never the left operand', () => {
|
||||
const fromLogicalAnd = calls().filter((edge) => edge.startsWith('logicalAnd → '));
|
||||
expect(fromLogicalAnd).toEqual(PRE_3354_LOGICAL_AND_EDGES);
|
||||
expect(fromLogicalAnd).toEqual(['logicalAnd → runAndRight']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue