Skip to content

Commit 53d392f

Browse files
authored
Handle else blocks and comments in unbuffered code (#609)
1 parent 63c6f61 commit 53d392f

File tree

4 files changed

+87
-11
lines changed

4 files changed

+87
-11
lines changed

src/printer.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,8 +1823,7 @@ export class PugPrinter {
18231823
return val.slice(0, -1);
18241824
}
18251825

1826-
// Since every line is parsed independently, babel will throw a SyntaxError if the line of code is only valid when there is another statement after it. This is a hack to get babel to properly parse what would otherwise be an invalid standalone JS line (e.g., `if (foo)`)
1827-
private async formatRawCodeWithFallback(
1826+
private async formatRawCodeWithFallbackNoElse(
18281827
val: string,
18291828
useSemi: boolean,
18301829
): Promise<string> {
@@ -1844,14 +1843,38 @@ export class PugPrinter {
18441843
// At this point, we know the SyntaxError is from the fact that there's no statement after our val's statement, implying we likely need a block after it. Using an empty block to get babel to parse it without affecting the code semantics.
18451844
// Example: `if (foo)` is not valid JS on its own, but `if (foo) {}` is.
18461845
try {
1847-
val = await this.formatRawCode(val + '{}', useSemi);
1846+
// Look for comments at the end of the line, since we have to insert the block in between the statement and the comment or else the block will potentially be commented out
1847+
const commentIndex: number = val.search(/\/(?:\/|\*).*$/);
1848+
if (commentIndex === -1) {
1849+
val += '{}';
1850+
} else {
1851+
val = `${val.slice(0, commentIndex)}{}${val.slice(commentIndex)}`;
1852+
}
1853+
1854+
val = await this.formatRawCode(val, useSemi);
1855+
1856+
/*
1857+
Strip out the empty block, which prettier has now formatted to split across two lines, and if there was a comment, it's now at the end of the second line. The first \s is to account for the space prettier inserted between the statement and the empty block.
1858+
Input:
1859+
`if (foo) // comment`
18481860
1849-
// Dynamically find the index of the last `{` in the code, which is the start of the empty block, since it's been reformatted at this point and a newline has likely been added between (shouldn't rely on that behavior though)
1850-
// Also account for any newly-introduced whitespace right before the empty block
1851-
const cutoffIndex: number = val.search(/\s?{\s?}\s?$/);
1852-
if (cutoffIndex === -1) throw new Error('No empty block found');
1861+
Transformed:
1862+
`if (foo) {}// comment`
18531863
1854-
return val.slice(0, cutoffIndex);
1864+
Initial formatted:
1865+
`if (foo) {
1866+
} // comment`
1867+
1868+
Final formatted:
1869+
`if (foo) // comment`
1870+
*/
1871+
const ma: RegExpExecArray | null = /\s{\n?}(.*)$/.exec(val);
1872+
if (!ma) throw new Error('No follow-up block found');
1873+
1874+
const comment: string | undefined = ma[1];
1875+
1876+
val = val.slice(0, ma.index) + (comment ?? '');
1877+
return val.trim();
18551878
} catch (secondError: unknown) {
18561879
logger.debug('[PugPrinter] fallback format error', secondError);
18571880
// throw original error since our fallback didn't work
@@ -1860,6 +1883,25 @@ export class PugPrinter {
18601883
}
18611884
}
18621885

1886+
// Since every line is parsed independently, babel will throw a SyntaxError if the line of code is only valid when there is another statement after it, or if the line starts with `else if` or `else`. This is a hack to get babel to properly parse what would otherwise be an invalid standalone JS line (e.g., `if (foo)`, `else if (bar)`, `else`)
1887+
private async formatRawCodeWithFallback(
1888+
val: string,
1889+
useSemi: boolean,
1890+
): Promise<string> {
1891+
if (val.startsWith('else')) {
1892+
// If the code starts with `else`, then we can format the code without the `else` keyword, and then add it back onto the start.
1893+
// We can call the same helper function in each case, just with different inputs, so we can easily handle all `if`, `else if`, and `else` cases without having to write out each one.
1894+
const noElse: string = await this.formatRawCodeWithFallbackNoElse(
1895+
val.slice(4),
1896+
useSemi,
1897+
);
1898+
// `noElse` will either be an empty string or it will contain a comment. Now we just prepend `else` onto the start and trim in case `noElse` is empty
1899+
return `else ${noElse}`.trim();
1900+
} else {
1901+
return await this.formatRawCodeWithFallbackNoElse(val, useSemi);
1902+
}
1903+
}
1904+
18631905
private async code(token: CodeToken): Promise<string> {
18641906
let result: string = this.computedIndent;
18651907
if (!token.mustEscape && token.buffer) {
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1-
- if (obj?.name)
1+
- if (obj?.name) /* Block comment */
22
p #{ obj.name } is the name, if it exists
3+
- if (obj?.name2) // Double-slash comment
4+
p #{ obj.name2 } is the second name, if it exists
5+
- if (obj?.name3)
6+
p #{ obj.name3 } is the third name, if it exists
7+
- else if (obj?.color) /* Comment */
8+
p #{ obj.color } is the color, if it exists
9+
- else // Comment, plus the else has whitespace before it
10+
p No name or color
11+
- if (obj?.color2)
12+
p #{ obj.color2 } is the second color, if it exists
13+
- else
14+
p Else without a comment
15+
- for (const k of obj?.items || []) // Comment
16+
p #{ k }
317
- for (const k of obj?.items || [])
418
p #{ k }
19+
- let n = 2;
20+
- while (n--) // Comment!
21+
p #{ n }
522
p This should always appear

tests/unbuffered-code/unbuffered-code.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { compareFiles } from 'tests/common';
22
import { describe, expect, it } from 'vitest';
33

44
describe('Unbuffered code', () => {
5-
it('should handle JS statements where the isolated line is only parsable when there is another statement afterwards', async () => {
5+
it('should handle JS statements where the isolated line is only parsable when there is another statement before/afterwards', async () => {
66
const { expected, actual } = await compareFiles(import.meta.url);
77
expect(actual).toBe(expected);
88
});
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1-
- if ( obj?.name )
1+
- if ( obj?.name ) /* Block comment */
22
p #{ obj.name } is the name, if it exists
3+
- if ( obj?.name2 ) // Double-slash comment
4+
p #{ obj.name2 } is the second name, if it exists
5+
- if ( obj?.name3 )
6+
p #{ obj.name3 } is the third name, if it exists
7+
- else if ( obj?.color ) /* Comment */
8+
p #{ obj.color } is the color, if it exists
9+
- else // Comment, plus the else has whitespace before it
10+
p No name or color
11+
- if ( obj?.color2 )
12+
p #{ obj.color2 } is the second color, if it exists
13+
- else
14+
p Else without a comment
15+
- for (const k of obj?.items || []) // Comment
16+
p #{ k }
317
- for (const k of obj?.items || [])
418
p #{ k }
19+
- let n = 2;
20+
- while ( n-- ) // Comment!
21+
p #{ n }
522
p This should always appear

0 commit comments

Comments
 (0)