Skip to content

Commit b39de82

Browse files
authored
perf(fmt): optimize Yuku parser selection (#312)
1 parent 12708f8 commit b39de82

2 files changed

Lines changed: 51 additions & 28 deletions

File tree

packages/rstack/src/fmt/yukuPlugin.ts

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import * as prettierEstreePlugin from 'prettier/plugins/estree';
22
import type { Parser, ParserOptions, Plugin, SupportLanguage } from 'prettier';
33
import {
4+
langFromPath,
45
parse as parseWithYuku,
56
type Comment,
67
type Diagnostic,
78
type ParseOptions,
89
type ParseResult,
9-
type SourceLang,
1010
type SourceType,
1111
} from 'yuku-parser';
1212

1313
const AST_FORMAT = 'estree-yuku';
14-
const JSX_REGEXP = /^[^"'`]*<\/|^[^/]{2}.*\/>/m;
1514
const SOURCE_TYPE_COMBINATIONS: SourceType[] = ['module', 'commonjs'];
1615

1716
type Range = [start: number, end: number];
@@ -200,15 +199,24 @@ const mergeNestedJsdocComments = (comments: PrettierComment[]): void => {
200199
};
201200

202201
const stripComments = (originalText: string, comments: PrettierComment[]): string => {
203-
let text = originalText;
202+
if (comments.length === 0) {
203+
return originalText;
204+
}
204205

206+
const chunks: string[] = [];
207+
let cursor = 0;
208+
209+
// Yuku returns comments in source order, so mask each range while copying the source only once.
205210
for (const comment of comments) {
206211
const start = locStart(comment);
207212
const end = locEnd(comment);
208-
text = text.slice(0, start) + text.slice(start, end).replace(/[^\n]/g, ' ') + text.slice(end);
213+
chunks.push(originalText.slice(cursor, start));
214+
chunks.push(originalText.slice(start, end).replace(/[^\n]/g, ' '));
215+
cursor = end;
209216
}
210217

211-
return text;
218+
chunks.push(originalText.slice(cursor));
219+
return chunks.join('');
212220
};
213221

214222
const setContentEnd = (
@@ -441,11 +449,7 @@ const parseWithOptions = (text: string, options: ParseOptions): ParseResult => {
441449
return result;
442450
};
443451

444-
const getSourceType = (filepath: unknown): SourceType | undefined => {
445-
if (typeof filepath !== 'string') {
446-
return undefined;
447-
}
448-
452+
const getSourceType = (filepath: string): SourceType | undefined => {
449453
if (/\.(?:mjs|mts)$/i.test(filepath)) {
450454
return 'module';
451455
}
@@ -457,20 +461,6 @@ const getSourceType = (filepath: unknown): SourceType | undefined => {
457461
return undefined;
458462
};
459463

460-
const getLanguageCombinations = (text: string, filepath: unknown): SourceLang[] => {
461-
if (typeof filepath === 'string') {
462-
if (/\.(?:jsx|tsx)$/i.test(filepath)) {
463-
return ['tsx'];
464-
}
465-
466-
if (filepath.toLowerCase().endsWith('.d.ts')) {
467-
return ['dts'];
468-
}
469-
}
470-
471-
return JSX_REGEXP.test(text) ? ['tsx', 'ts', 'dts'] : ['ts', 'tsx', 'dts'];
472-
};
473-
474464
const tryCombinations = (combinations: (() => ParseResult)[]): ParseResult => {
475465
let firstError: unknown;
476466
let hasError = false;
@@ -505,9 +495,9 @@ const parseJavaScript = (text: string, options: ParserOptions<AstNode>): AstNode
505495

506496
const parseTypeScript = (text: string, options: ParserOptions<AstNode>): AstNode => {
507497
const sourceType = getSourceType(options.filepath);
508-
const languages = getLanguageCombinations(text, options.filepath);
509-
const combinations = (sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS).flatMap((candidate) =>
510-
languages.map((lang) => () => parseWithOptions(text, { sourceType: candidate, lang })),
498+
const lang = langFromPath(options.filepath.toLowerCase());
499+
const combinations = (sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS).map(
500+
(candidate) => () => parseWithOptions(text, { sourceType: candidate, lang }),
511501
);
512502
const { program, comments } = tryCombinations(combinations);
513503

packages/rstack/tests/fmt/yukuPlugin.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ const formatWithYuku = (
77
options: Options & { parser: 'yuku' | 'yuku-ts' },
88
): Promise<string> =>
99
format(source, {
10-
filepath: `example.${options.parser === 'yuku' ? 'js' : 'ts'}`,
1110
plugins: [yukuPlugin],
1211
...options,
12+
filepath: options.filepath ?? `example.${options.parser === 'yuku' ? 'js' : 'ts'}`,
1313
});
1414

1515
test('exposes the same JavaScript and TypeScript language mappings as the official plugin', async () => {
@@ -34,6 +34,39 @@ test('exposes the same JavaScript and TypeScript language mappings as the offici
3434
]);
3535
});
3636

37+
test('parses JSX in JavaScript files', async () => {
38+
await expect(
39+
formatWithYuku('const view=<Component/>', {
40+
filepath: 'example.js',
41+
parser: 'yuku',
42+
}),
43+
).resolves.toBe('const view = <Component />;\n');
44+
});
45+
46+
test.each(['example.ts', 'example.mts', 'example.cts'])(
47+
'rejects JSX syntax in %s',
48+
async (filepath) => {
49+
await expect(
50+
formatWithYuku('const view=<Component/>', {
51+
filepath,
52+
parser: 'yuku-ts',
53+
}),
54+
).rejects.toThrow();
55+
},
56+
);
57+
58+
test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])(
59+
'rejects function implementations in %s',
60+
async (filepath) => {
61+
await expect(
62+
formatWithYuku('export function value() { return 1; }', {
63+
filepath,
64+
parser: 'yuku-ts',
65+
}),
66+
).rejects.toThrow('An implementation cannot be declared in ambient contexts');
67+
},
68+
);
69+
3770
test.each([
3871
{
3972
name: 'hashbangs and unicode locations',

0 commit comments

Comments
 (0)