Skip to content

Commit 4db3d25

Browse files
authored
refactor: Refactor how parser is constructed (#249)
Refactor how the JSON parser is constructed, how options are parsed, and how the sort compare function is created. These are each handled by separate functions now, rather than being inlined into the custom parser. This is intended to make it easier to extend different types of JSON parsers (e.g. #144).
1 parent 7fe46f6 commit 4db3d25

File tree

1 file changed

+156
-99
lines changed

1 file changed

+156
-99
lines changed

src/index.ts

+156-99
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
SpreadElement,
88
StringLiteral,
99
} from '@babel/types';
10-
import type { Parser } from 'prettier';
10+
import type { Parser, ParserOptions } from 'prettier';
1111
import { parsers as babelParsers } from 'prettier/plugins/babel';
1212

1313
/**
@@ -198,109 +198,166 @@ function sortAst(
198198
return ast;
199199
}
200200

201-
export const parsers = {
202-
json: {
203-
...babelParsers.json,
204-
async parse(text, options: any) {
205-
const jsonRootAst = await babelParsers.json.parse(text, options);
206-
207-
// The Prettier JSON parser wraps the AST in a 'JsonRoot' node
208-
// This ast variable is the real document root
209-
const ast = jsonRootAst.node;
210-
211-
const { jsonRecursiveSort, jsonSortOrder } = options;
212-
213-
// Only objects are intended to be sorted by this plugin
214-
// Arrays are considered only in recursive mode, so that we
215-
// can get to nested objected.
216-
if (
217-
!(
218-
ast.type === 'ObjectExpression' ||
219-
(ast.type === 'ArrayExpression' && jsonRecursiveSort)
220-
)
221-
) {
222-
return jsonRootAst;
223-
}
201+
/**
202+
* JSON sorting options. See README for details.
203+
*/
204+
type SortJsonOptions = {
205+
jsonRecursiveSort: boolean;
206+
jsonSortOrder: Record<string, CategorySort | null>;
207+
};
224208

225-
let sortCompareFunction: (a: string, b: string) => number = lexicalSort;
226-
if (jsonSortOrder) {
227-
let parsedCustomSort;
228-
try {
229-
parsedCustomSort = JSON.parse(jsonSortOrder);
230-
} catch (error) {
231-
// @ts-expect-error Error cause property not yet supported by '@types/node' (see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/61827)
232-
throw new Error(`Failed to parse sort order option as JSON`, {
233-
cause: error,
234-
});
235-
}
209+
/**
210+
* Parse JSON sort options from Prettier options.
211+
*
212+
* @param prettierOptions - Prettier options.
213+
* @returns JSON sort options.
214+
*/
215+
function parseOptions(prettierOptions: ParserOptions): SortJsonOptions {
216+
const jsonRecursiveSort = prettierOptions.jsonRecursiveSort ?? false;
236217

237-
if (
238-
Array.isArray(parsedCustomSort) ||
239-
typeof parsedCustomSort !== 'object'
240-
) {
241-
throw new Error(`Invalid custom sort order; must be an object`);
242-
}
218+
if (typeof jsonRecursiveSort !== 'boolean') {
219+
throw new Error(
220+
`Invalid 'jsonRecursiveSort' option; expected boolean, got '${typeof prettierOptions.jsonRecursiveSort}'`,
221+
);
222+
}
243223

244-
for (const categorySort of Object.values(parsedCustomSort)) {
245-
if (!allowedCategorySortValues.includes(categorySort as any)) {
246-
throw new Error(
247-
`Invalid custom sort entry: value must be one of '${String(
248-
allowedCategorySortValues,
249-
)}', got '${String(categorySort)}'`,
250-
);
251-
}
252-
}
253-
const customSort = parsedCustomSort as Record<
254-
string,
255-
null | CategorySort
256-
>;
257-
258-
const evaluateSortEntry = (value: string, entry: string): boolean => {
259-
const regexRegex = /^\/(.+)\/([imsu]*)$/u;
260-
if (entry.match(regexRegex)) {
261-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262-
const [, regexSpec, flags]: string[] = entry.match(regexRegex)!;
263-
// "regexSpec" guaranteed to be defined because of capture group. False positive for unnecessary type assertion.
264-
const regex = new RegExp(regexSpec as string, flags);
265-
return Boolean(value.match(regex));
266-
}
267-
return value === entry;
268-
};
269-
270-
const sortEntries = Object.keys(customSort);
271-
272-
sortCompareFunction = (a: string, b: string): number => {
273-
const aIndex = sortEntries.findIndex(evaluateSortEntry.bind(null, a));
274-
const bIndex = sortEntries.findIndex(evaluateSortEntry.bind(null, b));
275-
276-
if (aIndex === -1 && bIndex === -1) {
277-
return lexicalSort(a, b);
278-
} else if (bIndex === -1) {
279-
return -1;
280-
} else if (aIndex === -1) {
281-
return 1;
282-
} else if (aIndex === bIndex) {
283-
// Sort entry guaranteed to be non-null because index was found
284-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
285-
const sortEntry = sortEntries[aIndex]!;
286-
// Guaranteed to be defined because `sortEntry` is derived from `Object.keys`
287-
const categorySort = customSort[sortEntry] as null | CategorySort;
288-
const categorySortFunction =
289-
categorySort === null
290-
? lexicalSort
291-
: categorySortFunctions[categorySort];
292-
return categorySortFunction(a, b);
293-
}
294-
return aIndex - bIndex;
295-
};
224+
const rawJsonSortOrder = prettierOptions.jsonSortOrder ?? null;
225+
if (rawJsonSortOrder !== null && typeof rawJsonSortOrder !== 'string') {
226+
throw new Error(
227+
`Invalid 'jsonSortOrder' option; expected string, got '${typeof prettierOptions.rawJsonSortOrder}'`,
228+
);
229+
}
230+
231+
let jsonSortOrder = null;
232+
if (rawJsonSortOrder !== null) {
233+
try {
234+
jsonSortOrder = JSON.parse(rawJsonSortOrder);
235+
} catch (error) {
236+
// @ts-expect-error Error cause property not yet supported by '@types/node' (see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/61827)
237+
throw new Error(`Failed to parse sort order option as JSON`, {
238+
cause: error,
239+
});
240+
}
241+
242+
if (Array.isArray(jsonSortOrder) || typeof jsonSortOrder !== 'object') {
243+
throw new Error(`Invalid custom sort order; must be an object`);
244+
}
245+
246+
for (const categorySort of Object.values(jsonSortOrder)) {
247+
if (!allowedCategorySortValues.includes(categorySort as any)) {
248+
throw new Error(
249+
`Invalid custom sort entry: value must be one of '${String(
250+
allowedCategorySortValues,
251+
)}', got '${String(categorySort)}'`,
252+
);
296253
}
297-
const sortedAst = sortAst(ast, jsonRecursiveSort, sortCompareFunction);
254+
}
255+
}
256+
257+
return { jsonRecursiveSort, jsonSortOrder };
258+
}
298259

299-
return {
300-
...jsonRootAst,
301-
node: sortedAst,
302-
};
303-
},
260+
/**
261+
* Create sort compare function from a custom JSON sort order configuration.
262+
*
263+
* @param jsonSortOrder - JSON sort order configuration.
264+
* @returns A sorting function for comparing Object keys.
265+
*/
266+
function createSortCompareFunction(
267+
jsonSortOrder: Record<string, CategorySort | null>,
268+
): (a: string, b: string) => number {
269+
const evaluateSortEntry = (value: string, entry: string): boolean => {
270+
const regexRegex = /^\/(.+)\/([imsu]*)$/u;
271+
if (entry.match(regexRegex)) {
272+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
273+
const [, regexSpec, flags]: string[] = entry.match(regexRegex)!;
274+
// "regexSpec" guaranteed to be defined because of capture group. False positive for unnecessary type assertion.
275+
const regex = new RegExp(regexSpec as string, flags);
276+
return Boolean(value.match(regex));
277+
}
278+
return value === entry;
279+
};
280+
281+
const sortEntries = Object.keys(jsonSortOrder);
282+
283+
return (a: string, b: string): number => {
284+
const aIndex = sortEntries.findIndex(evaluateSortEntry.bind(null, a));
285+
const bIndex = sortEntries.findIndex(evaluateSortEntry.bind(null, b));
286+
287+
if (aIndex === -1 && bIndex === -1) {
288+
return lexicalSort(a, b);
289+
} else if (bIndex === -1) {
290+
return -1;
291+
} else if (aIndex === -1) {
292+
return 1;
293+
} else if (aIndex === bIndex) {
294+
// Sort entry guaranteed to be non-null because index was found
295+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296+
const sortEntry = sortEntries[aIndex]!;
297+
// Guaranteed to be defined because `sortEntry` is derived from `Object.keys`
298+
const categorySort = jsonSortOrder[sortEntry] as null | CategorySort;
299+
const categorySortFunction =
300+
categorySort === null
301+
? lexicalSort
302+
: categorySortFunctions[categorySort];
303+
return categorySortFunction(a, b);
304+
}
305+
return aIndex - bIndex;
306+
};
307+
}
308+
309+
/**
310+
* Prettier JSON parsers.
311+
*/
312+
type JsonParser = 'json';
313+
314+
/**
315+
* Create a JSON sorting parser based upon the specified Prettier parser.
316+
*
317+
* @param parser - The Prettier JSON parser to base the sorting on.
318+
* @returns The JSON sorting parser.
319+
*/
320+
function createParser(
321+
parser: JsonParser,
322+
): (text: string, options: ParserOptions) => Promise<any> {
323+
return async (text: string, prettierOptions: ParserOptions): Promise<any> => {
324+
const { jsonRecursiveSort, jsonSortOrder } = parseOptions(prettierOptions);
325+
326+
const jsonRootAst = await babelParsers[parser].parse(text, prettierOptions);
327+
328+
// The Prettier JSON parser wraps the AST in a 'JsonRoot' node
329+
// This ast variable is the real document root
330+
const ast = jsonRootAst.node;
331+
332+
// Only objects are intended to be sorted by this plugin
333+
// Arrays are considered only in recursive mode, so that we
334+
// can get to nested objected.
335+
if (
336+
!(
337+
ast.type === 'ObjectExpression' ||
338+
(ast.type === 'ArrayExpression' && jsonRecursiveSort)
339+
)
340+
) {
341+
return jsonRootAst;
342+
}
343+
344+
let sortCompareFunction: (a: string, b: string) => number = lexicalSort;
345+
if (jsonSortOrder) {
346+
sortCompareFunction = createSortCompareFunction(jsonSortOrder);
347+
}
348+
const sortedAst = sortAst(ast, jsonRecursiveSort, sortCompareFunction);
349+
350+
return {
351+
...jsonRootAst,
352+
node: sortedAst,
353+
};
354+
};
355+
}
356+
357+
export const parsers = {
358+
json: {
359+
...babelParsers.json,
360+
parse: createParser('json'),
304361
},
305362
} as Record<string, Parser>;
306363

0 commit comments

Comments
 (0)