feat(steps): a run of calls that happens once per item says so

A body drawn once, with nothing to say it repeats, is a quiet lie about the
order — so the reading now reads the loops a site is written inside, the same
way it reads its conditions: one climb up the same ancestors, per language,
`for` / `foreach` / `for … in` / `while` / `do` / `repeat`, with the header as
written (`item of items`, `queue.length > 0`) and where the loop starts.

Loops and forks nest in either direction, and neither reading knows about the
other, so the block builder merges them by where each construct BEGINS: on one
ancestor chain the outer one always starts first, which rebuilds the nesting
from the positions alone. A `for` inside an `if` and an `if` inside a `for` come
out the way the code has them.

With it, the per-framework readings are pinned: an Express handler with its
helper drawn inside the reply it builds, a FastAPI `raise HTTPException` ending
the arm it is in, a Spring early `return` as the other arm of its `if` (with
the comparison flipped, not wrapped), an ASP.NET handler's two outcomes, and a
Nest controller read on through the service it delegates to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
This commit is contained in:
Colby McHenry
2026-08-29 13:40:05 -05:00
co-authored by Claude Opus 5
parent 9acab0020f
commit 7b6704a70d
9 changed files with 484 additions and 43 deletions
+116 -1
View File
@@ -7,7 +7,7 @@
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { initGrammars } from '../src/extraction/grammars';
import { callSiteInSource, decoratorsInSource, guardsInSource, guardLabel, memberTypesInSource, supportsBranchGuards } from '../src/graph/branch-guards';
import { callSiteInSource, decoratorsInSource, guardsInSource, guardLabel, loopsInSource, memberTypesInSource, supportsBranchGuards } from '../src/graph/branch-guards';
import type { Language } from '../src/types';
beforeAll(async () => {
@@ -334,3 +334,118 @@ public class OrderService : IOrderService {
expect(Object.fromEntries(types)).toEqual({ _orderRepository: 'IRepository<Order>', Mailer: 'IEmailSender', orderRepository: 'IRepository<Order>', uriComposer: 'IUriComposer' });
});
});
describe('loops a site is written inside', () => {
/** The loop headers at the site, outermost first, as `<kind> <text>`. */
async function loopsAt(src: string, needle: string, language: Language) {
const line = lineOf(src, needle);
const column = src.split('\n')[line - 1]!.indexOf(needle);
return (await loopsInSource(src, language, line, column)).map((l) => `${l.kind} ${l.text}`);
}
it('reads a JS for-of and a while', async () => {
const src = `
function run(items) {
for (const item of items) {
save(item)
}
while (queue.length > 0) {
drain()
}
}`;
expect(await loopsAt(src, 'save(item)', 'javascript')).toEqual(['each item of items']);
expect(await loopsAt(src, 'drain()', 'javascript')).toEqual(['while queue.length > 0']);
});
it('reads nested loops outermost first', async () => {
const src = `
function run(rows) {
for (const row of rows) {
for (const cell of row) {
draw(cell)
}
}
}`;
expect(await loopsAt(src, 'draw(cell)', 'javascript')).toEqual(['each row of rows', 'each cell of row']);
});
it('reads nothing for a site outside every loop', async () => {
const src = `
function run(items) {
begin()
for (const item of items) { save(item) }
}`;
expect(await loopsAt(src, 'begin()', 'javascript')).toEqual([]);
});
it('reads a Python for and a while', async () => {
const src = `
def run(items):
for item in items:
save(item)
while pending:
drain()
`;
expect(await loopsAt(src, 'save(item)', 'python')).toEqual(['each item in items']);
expect(await loopsAt(src, 'drain()', 'python')).toEqual(['while pending']);
});
it('reads a Java enhanced for', async () => {
const src = `
class A {
void run(List<Item> items) {
for (Item item : items) {
save(item);
}
}
}`;
expect(await loopsAt(src, 'save(item)', 'java')).toEqual(['each Item item : items']);
});
it('reads a Go range loop', async () => {
const src = `
func run(items []Item) {
for _, item := range items {
save(item)
}
}`;
expect(await loopsAt(src, 'save(item)', 'go')).toEqual(['each _, item := range items']);
});
it('reads a C# foreach', async () => {
const src = `
class A {
void Run(List<Item> items) {
foreach (var item in items) {
Save(item);
}
}
}`;
// The binding word is noise in a header a person reads: `var` goes.
expect(await loopsAt(src, 'Save(item)', 'csharp')).toEqual(['each item in items']);
});
it('reads a Swift for-in', async () => {
const src = `
func run(items: [Item]) {
for item in items {
save(item)
}
}`;
expect(await loopsAt(src, 'save(item)', 'swift')).toEqual(['each item in items']);
});
it('reads a Kotlin for', async () => {
const src = `
fun run(items: List<Item>) {
for (item in items) {
save(item)
}
}`;
expect(await loopsAt(src, 'save(item)', 'kotlin')).toEqual(['each item in items']);
});
it('reads nothing for a language without rules', async () => {
expect(await loopsInSource('def f\n xs.each { save }\nend\n', 'ruby', 2, 2)).toEqual([]);
});
});