Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/bun-test-extended/src/jest-extended.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,15 @@ test('it counts a single call with toHaveBeenCalledOnce', () => {
const fn = mock(() => null);

fn();

expect(fn).toHaveBeenCalledOnce();
});

test('it checks the single call and its args with toHaveBeenCalledExactlyOnceWith', () => {
const fn = mock((value: string) => value);

fn('a');

expect(fn).toHaveBeenCalledExactlyOnceWith('a');
});

Expand All @@ -210,6 +212,7 @@ test('it orders mock calls with toHaveBeenCalledBefore and toHaveBeenCalledAfter

first();
second();

expect(first).toHaveBeenCalledBefore(second);
expect(second).toHaveBeenCalledAfter(first);
});
Expand Down
5 changes: 4 additions & 1 deletion packages/format-codemod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ between two kinds takes a blank line:
- `const`/`let`/`var` declarations
- `using`/`await using` declarations
- bare calls and method calls — two distinct kinds, split by the first call in reading order:
`expect(x).toBe(y)` opens with a bare function, `fs.writeFileSync(…)` opens with a member, and a
`use(x).report()` opens with a bare function, `fs.writeFileSync(…)` opens with a member, and a
member call chained onto a bare call's result is still a bare call
- `expect` assertions — a statement whose first call in reading order is `expect` or an `expect.*`
helper (`expect(x).toBe(y)`, `expect.assertions(1)`) is its own kind, so a run of assertions stays
glued while the boundary with any acting statement is padded
- assignments and increments

Two axes overlay the kinds and pad their own boundaries:
Expand Down
19 changes: 19 additions & 0 deletions packages/format-codemod/src/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,3 +591,22 @@ test('it is a no-op when run again on code with removed blank lines', () => {
expect(once).not.toBe(src);
expect(twice).toBe(once);
});

test('it pads expect runs apart from acting statements and keeps them glued', () => {
const src = `function f() {\n advance();\n expect(store.getState().generation).toBe(1);\n expect(store.getState().initialized).toBeFalse();\n advance();\n expect(store.getState().generation).toBe(2);\n}\n`;
const output = transform(src).output;

expect(output).toMatchInlineSnapshot(`
"function f() {
advance();

expect(store.getState().generation).toBe(1);
expect(store.getState().initialized).toBeFalse();

advance();

expect(store.getState().generation).toBe(2);
}
"
`);
});
42 changes: 40 additions & 2 deletions packages/format-codemod/src/transform/needs-blank-line.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ test('it does not pad between two statements of the same kind', () => {
});

test('it pads at the boundary between a method call and a bare call', () => {
const pair = parsePair("fs.writeFileSync(file, src);\nexpect(check(file)).toBe('changed');");
const pair = parsePair('fs.writeFileSync(file, src);\ncheck(file);');

expect(needsBlankLine(pair.container, pair.prev, pair.next)).toBeTrue();
});
Expand All @@ -136,7 +136,7 @@ test('it does not pad between two method calls', () => {
});

test('it treats a member call chained onto a bare call result as a bare call', () => {
const pair = parsePair('expect(a).toBe(1);\nuse(b);');
const pair = parsePair('parse(a).check();\nuse(b);');

expect(needsBlankLine(pair.container, pair.prev, pair.next)).toBeFalse();
});
Expand Down Expand Up @@ -232,3 +232,41 @@ test('it does not pad between kindless statements', () => {

expect(needsBlankLine(pair.container, pair.prev, pair.next)).toBeFalse();
});

test('it pads at the boundary between a bare call and an expect statement', () => {
const before = parsePair('advance();\nexpect(x).toBe(1);');
const after = parsePair('expect(x).toBe(1);\nadvance();');

expect(needsBlankLine(before.container, before.prev, before.next)).toBeTrue();
expect(needsBlankLine(after.container, after.prev, after.next)).toBeTrue();
});

test('it does not pad between two expect statements', () => {
const pair = parsePair('expect(x).toBe(1);\nexpect(x).toBeFalse();');

expect(needsBlankLine(pair.container, pair.prev, pair.next)).toBeFalse();
});

test('it treats an expect.* helper call as an expect statement', () => {
const pair = parsePair('expect.assertions(1);\nexpect(x).toBe(1);');

expect(needsBlankLine(pair.container, pair.prev, pair.next)).toBeFalse();
});

test('it pads at the boundary between a method call and an expect.* helper call', () => {
const pair = parsePair('store.reset();\nexpect.assertions(1);');

expect(needsBlankLine(pair.container, pair.prev, pair.next)).toBeTrue();
});

test('it treats a wrapping await as not changing the expect kind', () => {
const pair = parsePair('await expect(p).toReject();\nawait expect(q).toReject();');

expect(needsBlankLine(pair.container, pair.prev, pair.next)).toBeFalse();
});

test('it treats expect in argument position as not making an expect statement', () => {
const pair = parsePair('use(expect(x));\nrun(y);');

expect(needsBlankLine(pair.container, pair.prev, pair.next)).toBeFalse();
});
26 changes: 23 additions & 3 deletions packages/format-codemod/src/transform/needs-blank-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const CONTROL_FLOW_TYPES = new Set([
* declaration, on both sides of a control-flow block — its closing brace
* ends a visual unit just like its opening keyword starts one —
* and at the boundary between statement kinds: bare call vs method call vs
* mutation, instantiation vs anything else, and awaited vs non-awaited. Any
* expect assertion vs mutation, instantiation vs anything else, and awaited
* vs non-awaited. Any
* match means exactly one blank line; a pair matching no rule sits flush.
*/
export function needsBlankLine(container: ASTNode, prev: ASTNode, next: ASTNode): boolean {
Expand Down Expand Up @@ -183,15 +184,34 @@ function getStatementKind(node: ASTNode): string | null {
}

if (isExpressionStatementOf(node, CALL_TYPES)) {
return pickCallKind(node);
return isExpectHeaded(node) ? 'expect' : pickCallKind(node);
}

return isExpressionStatementOf(node, MUTATION_TYPES) ? 'mutation' : null;
}

/**
* An assertion statement: the first call a reader meets is `expect` itself or
* an `expect.*` helper (`expect.soft(x)`, `expect.assertions(1)`). Asserting
* is observation rather than action, so a run of assertions stays tight — one
* checklist — while the boundary with any acting statement is padded.
*/
function isExpectHeaded(node: ASTNode): boolean {
const expression = node['expression'];
const callee = isASTNode(expression) ? findDeepestCallee(expression) : null;

if (callee === null) {
return false;
}

const head = collectHeadChain(callee).at(-1);

return head?.type === 'Identifier' && head['name'] === 'expect';
}

/**
* Call statements split into two kinds by the first call a reader meets:
* `expect(x).toBe(y)` opens with a bare function, `fs.writeFileSync(...)`
* `use(x).report()` opens with a bare function, `fs.writeFileSync(...)`
* opens with a member. The deepest call in the head chain decides, so a
* member call chained onto a bare call's result is still a bare call, and a
* wrapping await doesn't change the kind.
Expand Down