Skip to content

Commit 60f9022

Browse files
authored
Handle unexpected missing block SyntaxError when parsing unbuffered code (#601)
1 parent 25329d1 commit 60f9022

File tree

5 files changed

+69
-9
lines changed

5 files changed

+69
-9
lines changed

src/printer.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,54 @@ export class PugPrinter {
18121812
return result;
18131813
}
18141814

1815+
private async formatRawCode(val: string, useSemi: boolean): Promise<string> {
1816+
val = await format(val, {
1817+
parser: 'babel',
1818+
...this.codeInterpolationOptions,
1819+
semi: useSemi,
1820+
// Always pass endOfLine 'lf' here to be sure that the next `val.slice(0, -1)` call is always working
1821+
endOfLine: 'lf',
1822+
});
1823+
return val.slice(0, -1);
1824+
}
1825+
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(
1828+
val: string,
1829+
useSemi: boolean,
1830+
): Promise<string> {
1831+
try {
1832+
return await this.formatRawCode(val, useSemi);
1833+
} catch (error: unknown) {
1834+
if (!(error instanceof SyntaxError)) throw error;
1835+
1836+
const m: RegExpExecArray | null = /Unexpected token \(1:(\d+)\)/.exec(
1837+
error.message,
1838+
);
1839+
const n: number = Number(m?.[1]);
1840+
1841+
// If the error is not from the very end of the code, then this fallback approach won't work, so we throw the original error.
1842+
if (val.length + 1 !== n) throw error;
1843+
1844+
// 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.
1845+
// Example: `if (foo)` is not valid JS on its own, but `if (foo) {}` is.
1846+
try {
1847+
val = await this.formatRawCode(val + '{}', useSemi);
1848+
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');
1853+
1854+
return val.slice(0, cutoffIndex);
1855+
} catch (secondError: unknown) {
1856+
logger.debug('[PugPrinter] fallback format error', secondError);
1857+
// throw original error since our fallback didn't work
1858+
throw error;
1859+
}
1860+
}
1861+
}
1862+
18151863
private async code(token: CodeToken): Promise<string> {
18161864
let result: string = this.computedIndent;
18171865
if (!token.mustEscape && token.buffer) {
@@ -1827,14 +1875,7 @@ export class PugPrinter {
18271875
let val: string = token.val;
18281876
try {
18291877
const valBackup: string = val;
1830-
val = await format(val, {
1831-
parser: 'babel',
1832-
...this.codeInterpolationOptions,
1833-
semi: useSemi,
1834-
// Always pass endOfLine 'lf' here to be sure that the next `val.slice(0, -1)` call is always working
1835-
endOfLine: 'lf',
1836-
});
1837-
val = val.slice(0, -1);
1878+
val = await this.formatRawCodeWithFallback(val, useSemi);
18381879
if (val[0] === ';') {
18391880
val = val.slice(1);
18401881
}

tests/pug-tests/blocks-in-if.formatted.pug

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
- var ajax = true;
44

5-
- if( ajax )
5+
- if (ajax)
66
//- return only contents if ajax requests
77
block contents
88
p ajax contents
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- if (obj?.name)
2+
p #{ obj.name } is the name, if it exists
3+
- for (const k of obj?.items || [])
4+
p #{ k }
5+
p This should always appear
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { compareFiles } from 'tests/common';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('Unbuffered code', () => {
5+
it('should handle JS statements where the isolated line is only parsable when there is another statement afterwards', async () => {
6+
const { expected, actual } = await compareFiles(import.meta.url);
7+
expect(actual).toBe(expected);
8+
});
9+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- if ( obj?.name )
2+
p #{ obj.name } is the name, if it exists
3+
- for (const k of obj?.items || [])
4+
p #{ k }
5+
p This should always appear

0 commit comments

Comments
 (0)