mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-05 08:06:02 +00:00
* Add Python taint source sink model * Fix Python taint argument and class shadowing * Address Python taint review cleanup * test(cfg): align Python keyword argument harvest --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
408 lines
19 KiB
TypeScript
408 lines
19 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { createRequire } from 'node:module';
|
|
import { createPythonCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/python.js';
|
|
import type { FunctionCfg, SiteRecord } from '../../../src/core/ingestion/cfg/types.js';
|
|
import {
|
|
makeCfgHarness,
|
|
type CfgHarness,
|
|
block,
|
|
edgeKinds,
|
|
reaches,
|
|
reachable,
|
|
bindingIdx,
|
|
} from '../../helpers/cfg-harness.js';
|
|
import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js';
|
|
import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js';
|
|
|
|
// The Python CfgVisitor — one hazard per test (real-parser regression, NOT
|
|
// snapshot-pinning). Each fixture's distinctive statement text (a(), one(),
|
|
// done(), …) lets us locate the block for a region by text and assert the
|
|
// control-flow topology around it. Python is the most structurally divergent
|
|
// target: indentation blocks, elif, for/while-else, with, try/except/else/finally,
|
|
// match/case. The `while True:` EXIT-reachability + CDG regressions are
|
|
// load-bearing.
|
|
|
|
const pyGrammar = createRequire(import.meta.url)('tree-sitter-python') as Parameters<
|
|
typeof makeCfgHarness
|
|
>[0];
|
|
|
|
const py: CfgHarness = makeCfgHarness(pyGrammar, createPythonCfgVisitor(), 'fixture.py');
|
|
|
|
const hasDef = (cfg: FunctionCfg, idx: number): boolean =>
|
|
cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx)));
|
|
const hasUse = (cfg: FunctionCfg, idx: number): boolean =>
|
|
cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(idx)));
|
|
const hasMayDef = (cfg: FunctionCfg, idx: number): boolean =>
|
|
cfg.blocks.some((bl) => bl.statements?.some((s) => (s.mayDefs ?? []).includes(idx)));
|
|
|
|
/** An edge {from, to, kind} exists. */
|
|
const hasEdge = (cfg: FunctionCfg, from: number, to: number, kind: string): boolean =>
|
|
cfg.edges.some((e) => e.from === from && e.to === to && e.kind === kind);
|
|
|
|
describe('Python CfgVisitor — structure', () => {
|
|
it('straight-line body: ENTRY → block → EXIT (seq)', () => {
|
|
const cfg = py.cfgOf(`def f():\n a()\n b()\n c()\n`);
|
|
expect(cfg.blocks.filter((b) => b.kind === 'normal')).toHaveLength(1);
|
|
const body = block(cfg, 'a()');
|
|
expect(cfg.edges).toContainEqual({ from: cfg.entryIndex, to: body, kind: 'seq' });
|
|
expect(reaches(cfg, body, cfg.exitIndex)).toBe(true);
|
|
});
|
|
|
|
it('empty body (pass only): ENTRY → block → EXIT', () => {
|
|
const cfg = py.cfgOf(`def f():\n pass\n`);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
});
|
|
|
|
it('function and lambda are both CFG-bearing', () => {
|
|
const cfgs = py.cfgsOf(`def f():\n x()\n\ng = lambda y: y + 1\n`);
|
|
expect(cfgs.length).toBeGreaterThanOrEqual(2);
|
|
for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
});
|
|
|
|
it('async def is the same node; its body builds a CFG', () => {
|
|
const cfg = py.cfgOf(`async def f(c):\n await c\n work()\n`);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
expect(reachable(cfg, block(cfg, 'work()'))).toBe(true);
|
|
});
|
|
|
|
it('unmodeled / no-body shape → undefined, never throws', () => {
|
|
const root = py.parse(`class C:\n x = 1\n`);
|
|
const fns = py.collectFunctions(root);
|
|
for (const fn of fns) {
|
|
expect(() => createPythonCfgVisitor().buildFunctionCfg(fn, 'f.py')).not.toThrow();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — if / elif / else', () => {
|
|
it('if/elif/else: branch senses; every arm reaches the join', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(x):\n if x > 0:\n a()\n elif x < 0:\n b()\n else:\n c()\n after()\n`,
|
|
);
|
|
const kinds = edgeKinds(cfg);
|
|
expect(kinds.has('cond-true')).toBe(true);
|
|
expect(kinds.has('cond-false')).toBe(true);
|
|
const join = block(cfg, 'after()');
|
|
expect(reaches(cfg, block(cfg, 'a()'), join)).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'b()'), join)).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'c()'), join)).toBe(true);
|
|
// the if-true arm is reached by cond-true; the elif chains on cond-false.
|
|
const ifHeader = block(cfg, 'x > 0');
|
|
const elifHeader = block(cfg, 'x < 0');
|
|
expect(hasEdge(cfg, ifHeader, block(cfg, 'a()'), 'cond-true')).toBe(true);
|
|
expect(hasEdge(cfg, ifHeader, elifHeader, 'cond-false')).toBe(true);
|
|
expect(hasEdge(cfg, elifHeader, block(cfg, 'b()'), 'cond-true')).toBe(true);
|
|
expect(hasEdge(cfg, elifHeader, block(cfg, 'c()'), 'cond-false')).toBe(true);
|
|
});
|
|
|
|
it('if with no else: cond-true to the body, seq fall-through to the join', () => {
|
|
// Mirrors the shared TS/Go visitor contract: a no-else `if` emits only the
|
|
// taken `cond-true` arm; the not-taken path falls through to the join as
|
|
// `seq`, which the CDG pass treats as the complement (false) arm.
|
|
const cfg = py.cfgOf(`def f(x):\n if x:\n a()\n after()\n`);
|
|
const header = cfg.blocks.find((b) => b.text === 'x')!.index;
|
|
const after = block(cfg, 'after()');
|
|
expect(hasEdge(cfg, header, block(cfg, 'a()'), 'cond-true')).toBe(true);
|
|
// both the taken arm and the fall-through reach the join.
|
|
expect(reaches(cfg, header, after)).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'a()'), after)).toBe(true);
|
|
// the `if` is a real control point — CDG is non-empty.
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — for / while with the loop else', () => {
|
|
it('for: header + body + loop-back; loop var is a def, iterable a use', () => {
|
|
const cfg = py.cfgOf(`def f(xs):\n for i in xs:\n step(i)\n done()\n`);
|
|
const body = block(cfg, 'step(i)');
|
|
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
|
expect(edgeKinds(cfg).has('loop-back')).toBe(true);
|
|
const header = cfg.edges.find((e) => e.kind === 'loop-back' && e.from === body)?.to;
|
|
expect(header).toBeDefined();
|
|
expect(reaches(cfg, header!, block(cfg, 'done()'))).toBe(true);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'i'))).toBe(true);
|
|
expect(hasUse(cfg, bindingIdx(cfg, 'xs'))).toBe(true);
|
|
});
|
|
|
|
it('for-else: the else runs on NORMAL completion (cond-false), NOT on break', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(xs):\n for i in xs:\n if i:\n break\n else:\n noBreak()\n after()\n`,
|
|
);
|
|
const header = cfg.blocks.find((b) => b.text.startsWith('for '))!.index;
|
|
const elseB = block(cfg, 'noBreak()');
|
|
const breakB = block(cfg, 'break');
|
|
// the else is on the cond-false (normal completion) edge from the header...
|
|
expect(hasEdge(cfg, header, elseB, 'cond-false')).toBe(true);
|
|
// ...and the break does NOT route through the else.
|
|
expect(reaches(cfg, breakB, elseB)).toBe(false);
|
|
// both the else and the break reach the post-loop join.
|
|
expect(reaches(cfg, elseB, block(cfg, 'after()'))).toBe(true);
|
|
expect(reaches(cfg, breakB, block(cfg, 'after()'))).toBe(true);
|
|
});
|
|
|
|
it('while-else: same else-on-normal-completion / not-on-break semantics', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(x):\n while x > 0:\n if cond():\n break\n x -= 1\n else:\n clean()\n after()\n`,
|
|
);
|
|
const header = block(cfg, 'x > 0');
|
|
const elseB = block(cfg, 'clean()');
|
|
const breakB = block(cfg, 'break');
|
|
expect(hasEdge(cfg, header, elseB, 'cond-false')).toBe(true);
|
|
expect(reaches(cfg, breakB, elseB)).toBe(false);
|
|
expect(reaches(cfg, breakB, block(cfg, 'after()'))).toBe(true);
|
|
});
|
|
|
|
it('while True: keeps EXIT reverse-reachable AND emits CDG > 0', () => {
|
|
const cfg = py.cfgOf(`def f(x):\n while True:\n if x:\n g()\n`);
|
|
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('continue re-tests the loop header', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(xs):\n for i in xs:\n if i:\n continue\n use(i)\n`,
|
|
);
|
|
const header = cfg.blocks.find((b) => b.text.startsWith('for '))!.index;
|
|
expect(edgeKinds(cfg).has('continue')).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'continue'), header)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — with (deterministic dispose on normal AND exception exit)', () => {
|
|
it('with runs the body then __exit__ (dispose) on the normal path', () => {
|
|
const cfg = py.cfgOf(`def f():\n with open('x') as fh:\n use(fh)\n after()\n`);
|
|
const dispose = cfg.blocks.find((b) => b.text.startsWith('with '))!.index;
|
|
const body = block(cfg, 'use(fh)');
|
|
// body → dispose → after on the normal path.
|
|
expect(reaches(cfg, body, dispose)).toBe(true);
|
|
expect(reaches(cfg, dispose, block(cfg, 'after()'))).toBe(true);
|
|
// the `as fh` alias is a def.
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'fh'))).toBe(true);
|
|
});
|
|
|
|
it('with runs __exit__ on the EXCEPTION path too (body raises → dispose via throw)', () => {
|
|
const cfg = py.cfgOf(`def f():\n with lock() as l:\n boom()\n after()\n`);
|
|
const dispose = cfg.blocks.find((b) => b.text.startsWith('with '))!.index;
|
|
const body = block(cfg, 'boom()');
|
|
// an exception inside the body routes to the dispose via a throw edge.
|
|
expect(hasEdge(cfg, body, dispose, 'throw')).toBe(true);
|
|
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
|
});
|
|
|
|
it('return inside a with threads through the dispose', () => {
|
|
const cfg = py.cfgOf(`def f():\n with open('x') as fh:\n return read(fh)\n`);
|
|
const dispose = cfg.blocks.find((b) => b.text.startsWith('with '))!.index;
|
|
const ret = block(cfg, 'return read(fh)');
|
|
expect(reaches(cfg, ret, dispose)).toBe(true);
|
|
expect(reaches(cfg, dispose, cfg.exitIndex)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — try / except / else / finally', () => {
|
|
it('try/except/else/finally: completion edges; finally runs on all paths', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f():\n try:\n body()\n except ValueError as e:\n handle(e)\n else:\n noerr()\n finally:\n cleanup()\n after()\n`,
|
|
);
|
|
const finallyB = block(cfg, 'cleanup()');
|
|
expect(reaches(cfg, block(cfg, 'body()'), finallyB)).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'handle(e)'), finallyB)).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'noerr()'), finallyB)).toBe(true);
|
|
expect(reaches(cfg, finallyB, block(cfg, 'after()'))).toBe(true);
|
|
// the protected body can throw to the handler.
|
|
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
|
// the else runs only after the body (no exception).
|
|
expect(reaches(cfg, block(cfg, 'body()'), block(cfg, 'noerr()'))).toBe(true);
|
|
// `except E as e` binds e (catch-kind).
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'e'))).toBe(true);
|
|
});
|
|
|
|
it('except group (except*) is a handler from the body', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f():\n try:\n body()\n except* ValueError as e:\n handle(e)\n after()\n`,
|
|
);
|
|
expect(reaches(cfg, block(cfg, 'body()'), block(cfg, 'handle(e)'))).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'handle(e)'), block(cfg, 'after()'))).toBe(true);
|
|
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
|
});
|
|
|
|
it('multiple except clauses are both reachable from the protected body', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f():\n try:\n body()\n except ValueError:\n handleV()\n except (TypeError, KeyError):\n handleT()\n after()\n`,
|
|
);
|
|
expect(reaches(cfg, block(cfg, 'body()'), block(cfg, 'handleV()'))).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'body()'), block(cfg, 'handleT()'))).toBe(true);
|
|
});
|
|
|
|
it('return inside try threads through finally', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(x):\n try:\n if x:\n return 1\n body()\n finally:\n cleanup()\n`,
|
|
);
|
|
const finallyB = block(cfg, 'cleanup()');
|
|
expect(reaches(cfg, block(cfg, 'return 1'), finallyB)).toBe(true);
|
|
expect(edgeKinds(cfg).has('finally-return')).toBe(true);
|
|
expect(reaches(cfg, finallyB, cfg.exitIndex)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — match / case (no fallthrough)', () => {
|
|
it('match dispatches across cases; no fallthrough between case bodies', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(x):\n match x:\n case 1:\n one()\n case 2:\n two()\n case _:\n other()\n after()\n`,
|
|
);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
// each case body rejoins after the match...
|
|
expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'after()'))).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'two()'), block(cfg, 'after()'))).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'other()'), block(cfg, 'after()'))).toBe(true);
|
|
// ...but case 1 does NOT fall into case 2 (no implicit fallthrough).
|
|
expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'two()'))).toBe(false);
|
|
});
|
|
|
|
it('match with no wildcard keeps EXIT reverse-reachable (no-match path)', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(x):\n match x:\n case 1:\n one()\n case 2:\n two()\n after()\n`,
|
|
);
|
|
// the dispatch reaches the exit directly (no `case _`) so after() is reachable.
|
|
const dispatch = cfg.blocks.find((b) => b.text.startsWith('match '))!.index;
|
|
expect(reaches(cfg, dispatch, block(cfg, 'after()'))).toBe(true);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
});
|
|
|
|
it('a case guard is harvested as a conditional use on the dispatch', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(x, k):\n match x:\n case y if y > k:\n use(y)\n case _:\n other()\n`,
|
|
);
|
|
expect(hasUse(cfg, bindingIdx(cfg, 'k'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — def/use harvest', () => {
|
|
it('x = a then use(x): def then use', () => {
|
|
const cfg = py.cfgOf(`def f(a):\n x = a\n use(x)\n`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
expect(hasDef(cfg, x)).toBe(true);
|
|
expect(hasUse(cfg, x)).toBe(true);
|
|
});
|
|
|
|
it('tuple unpack a, b = f() defines BOTH a and b', () => {
|
|
const cfg = py.cfgOf(`def f():\n a, b = load()\n use(a, b)\n`);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'a'))).toBe(true);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'b'))).toBe(true);
|
|
expect(hasUse(cfg, bindingIdx(cfg, 'a'))).toBe(true);
|
|
expect(hasUse(cfg, bindingIdx(cfg, 'b'))).toBe(true);
|
|
});
|
|
|
|
it('list unpack [c, d] = pair() and star target first, *rest = seq() define all', () => {
|
|
const cfg = py.cfgOf(`def f():\n [c, d] = pair()\n first, *rest = seq()\n`);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'c'))).toBe(true);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'd'))).toBe(true);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'first'))).toBe(true);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'rest'))).toBe(true);
|
|
});
|
|
|
|
it('augmented assignment x += 1 reads AND writes the lvalue', () => {
|
|
const cfg = py.cfgOf(`def f():\n x = 1\n x += 3\n`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
expect(hasDef(cfg, x)).toBe(true);
|
|
expect(hasUse(cfg, x)).toBe(true);
|
|
});
|
|
|
|
it('walrus (n := f()) defines n; the condition uses it', () => {
|
|
const cfg = py.cfgOf(`def f():\n if (n := compute()) > 0:\n use(n)\n`);
|
|
const n = bindingIdx(cfg, 'n');
|
|
expect(hasDef(cfg, n)).toBe(true);
|
|
expect(hasUse(cfg, n)).toBe(true);
|
|
});
|
|
|
|
it('comprehension target [i for i in xs] defines i; xs is a use', () => {
|
|
const cfg = py.cfgOf(`def f(xs):\n a = [i for i in xs if i > 0]\n use(a)\n`);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'i'))).toBe(true);
|
|
expect(hasUse(cfg, bindingIdx(cfg, 'xs'))).toBe(true);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'a'))).toBe(true);
|
|
});
|
|
|
|
it('with ... as t defines t; for ... in src uses src', () => {
|
|
const cfg = py.cfgOf(`def f(src):\n with src as t:\n use(t)\n`);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 't'))).toBe(true);
|
|
expect(hasUse(cfg, bindingIdx(cfg, 'src'))).toBe(true);
|
|
});
|
|
|
|
it('a walrus in a ternary arm is a may-def (conditional context)', () => {
|
|
const cfg = py.cfgOf(`def f(c, p, q):\n x = (a := p) if c else (b := q)\n use(x)\n`);
|
|
// both arm walruses are conditionally evaluated → may-defs, not must-defs.
|
|
expect(hasMayDef(cfg, bindingIdx(cfg, 'a'))).toBe(true);
|
|
expect(hasMayDef(cfg, bindingIdx(cfg, 'b'))).toBe(true);
|
|
expect(hasDef(cfg, bindingIdx(cfg, 'a'))).toBe(false);
|
|
});
|
|
|
|
it('a walrus in an or/and right operand is a may-def', () => {
|
|
const cfg = py.cfgOf(`def f(a, b):\n z = a or (w := b)\n use(z)\n`);
|
|
expect(hasMayDef(cfg, bindingIdx(cfg, 'w'))).toBe(true);
|
|
});
|
|
|
|
it('attribute write obj.f = 1 is NOT a scalar def; root is a use', () => {
|
|
const cfg = py.cfgOf(`def f(obj):\n obj.field = 1\n use(obj)\n`);
|
|
expect(hasUse(cfg, bindingIdx(cfg, 'obj'))).toBe(true);
|
|
// no `field` binding exists (member writes are not scalar defs).
|
|
expect((cfg.bindings ?? []).some((b) => b.name === 'field')).toBe(false);
|
|
});
|
|
|
|
it('parameters (incl. *args / **kwargs / defaults) define at ENTRY', () => {
|
|
const cfg = py.cfgOf(`def f(a, b=1, *args, **kwargs):\n use(a, b, args, kwargs)\n`);
|
|
for (const name of ['a', 'b', 'args', 'kwargs']) {
|
|
expect(hasDef(cfg, bindingIdx(cfg, name))).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('global names resolve to a shared synthetic module binding (no false local)', () => {
|
|
const cfg = py.cfgOf(`def f():\n global G\n G = 1\n use(G)\n`);
|
|
const g = bindingIdx(cfg, 'G');
|
|
expect((cfg.bindings ?? [])[g].synthetic).toBe(true);
|
|
expect(hasUse(cfg, g)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — functionStartColumn', () => {
|
|
it('two same-line lambdas get distinct functionStartColumn', () => {
|
|
const cfgs = py.cfgsOf(`d = {"a": lambda: x(), "b": lambda: y()}\n`);
|
|
expect(cfgs.length).toBeGreaterThanOrEqual(2);
|
|
expect(cfgs[0].functionStartLine).toBe(cfgs[1].functionStartLine);
|
|
expect(cfgs[0].functionStartColumn).not.toBe(cfgs[1].functionStartColumn);
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — production CDG probe (plan-required)', () => {
|
|
it('while True / nested if gives exitReachable=true and CDG edges > 0', () => {
|
|
const cfg = py.cfgOf(`def f(x):\n while True:\n if x:\n g()\n`);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Python CfgVisitor — taint-site substrate', () => {
|
|
it('harvests call, member-read, argument, receiver, and result-def sites', () => {
|
|
const cfg = py.cfgOf(
|
|
`def f(request, db):\n db.query(request.args)\n value = sanitize(request.form)\n`,
|
|
);
|
|
const request = bindingIdx(cfg, 'request');
|
|
const db = bindingIdx(cfg, 'db');
|
|
const value = bindingIdx(cfg, 'value');
|
|
const sites: SiteRecord[] = cfg.blocks.flatMap((b) =>
|
|
(b.statements ?? []).flatMap((s) => [...(s.sites ?? [])]),
|
|
);
|
|
|
|
const query = sites.find((s) => s.kind === 'call' && s.callee === 'db.query');
|
|
expect(query?.receiver).toBe(db);
|
|
expect(query?.args?.[0]).toContain(request);
|
|
expect(query?.at).toEqual([2, 4]);
|
|
|
|
expect(
|
|
sites.some((s) => s.kind === 'member-read' && s.object === request && s.property === 'args'),
|
|
).toBe(true);
|
|
|
|
const sanitize = sites.find((s) => s.kind === 'call' && s.callee === 'sanitize');
|
|
expect(sanitize?.resultDefs).toEqual([value]);
|
|
expect(sanitize?.at).toEqual([3, 12]);
|
|
});
|
|
});
|