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:
co-authored by
Claude Opus 5
parent
9acab0020f
commit
7b6704a70d
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user