Skip to content

Commit

Permalink
refactor: Refactor how parser is constructed
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Gudahtt committed Dec 2, 2024
1 parent 7fe46f6 commit 6161d29
Showing 1 changed file with 156 additions and 99 deletions.
255 changes: 156 additions & 99 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
SpreadElement,
StringLiteral,
} from '@babel/types';
import type { Parser } from 'prettier';
import type { Parser, ParserOptions } from 'prettier';
import { parsers as babelParsers } from 'prettier/plugins/babel';

/**
Expand Down Expand Up @@ -198,109 +198,166 @@ function sortAst(
return ast;
}

export const parsers = {
json: {
...babelParsers.json,
async parse(text, options: any) {
const jsonRootAst = await babelParsers.json.parse(text, options);

// The Prettier JSON parser wraps the AST in a 'JsonRoot' node
// This ast variable is the real document root
const ast = jsonRootAst.node;

const { jsonRecursiveSort, jsonSortOrder } = options;

// Only objects are intended to be sorted by this plugin
// Arrays are considered only in recursive mode, so that we
// can get to nested objected.
if (
!(
ast.type === 'ObjectExpression' ||
(ast.type === 'ArrayExpression' && jsonRecursiveSort)
)
) {
return jsonRootAst;
}
/**
* JSON sorting options. See README for details.
*/
type SortJsonOptions = {
jsonRecursiveSort: boolean;
jsonSortOrder: Record<string, CategorySort | null>;
};

let sortCompareFunction: (a: string, b: string) => number = lexicalSort;
if (jsonSortOrder) {
let parsedCustomSort;
try {
parsedCustomSort = JSON.parse(jsonSortOrder);
} catch (error) {
// @ts-expect-error Error cause property not yet supported by '@types/node' (see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/61827)
throw new Error(`Failed to parse sort order option as JSON`, {
cause: error,
});
}
/**
* Parse JSON sort options from Prettier options.
*
* @param prettierOptions - Prettier options.
* @returns JSON sort options.
*/
function parseOptions(prettierOptions: ParserOptions): SortJsonOptions {
const jsonRecursiveSort = prettierOptions.jsonRecursiveSort ?? false;

if (
Array.isArray(parsedCustomSort) ||
typeof parsedCustomSort !== 'object'
) {
throw new Error(`Invalid custom sort order; must be an object`);
}
if (typeof jsonRecursiveSort !== 'boolean') {
throw new Error(
`Invalid 'jsonRecursiveSort' option; expected boolean, got '${typeof prettierOptions.jsonRecursiveSort}'`,
);
}

for (const categorySort of Object.values(parsedCustomSort)) {
if (!allowedCategorySortValues.includes(categorySort as any)) {
throw new Error(
`Invalid custom sort entry: value must be one of '${String(
allowedCategorySortValues,
)}', got '${String(categorySort)}'`,
);
}
}
const customSort = parsedCustomSort as Record<
string,
null | CategorySort
>;

const evaluateSortEntry = (value: string, entry: string): boolean => {
const regexRegex = /^\/(.+)\/([imsu]*)$/u;
if (entry.match(regexRegex)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [, regexSpec, flags]: string[] = entry.match(regexRegex)!;
// "regexSpec" guaranteed to be defined because of capture group. False positive for unnecessary type assertion.
const regex = new RegExp(regexSpec as string, flags);
return Boolean(value.match(regex));
}
return value === entry;
};

const sortEntries = Object.keys(customSort);

sortCompareFunction = (a: string, b: string): number => {
const aIndex = sortEntries.findIndex(evaluateSortEntry.bind(null, a));
const bIndex = sortEntries.findIndex(evaluateSortEntry.bind(null, b));

if (aIndex === -1 && bIndex === -1) {
return lexicalSort(a, b);
} else if (bIndex === -1) {
return -1;
} else if (aIndex === -1) {
return 1;
} else if (aIndex === bIndex) {
// Sort entry guaranteed to be non-null because index was found
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const sortEntry = sortEntries[aIndex]!;
// Guaranteed to be defined because `sortEntry` is derived from `Object.keys`
const categorySort = customSort[sortEntry] as null | CategorySort;
const categorySortFunction =
categorySort === null
? lexicalSort
: categorySortFunctions[categorySort];
return categorySortFunction(a, b);
}
return aIndex - bIndex;
};
const rawJsonSortOrder = prettierOptions.jsonSortOrder ?? null;
if (rawJsonSortOrder !== null && typeof rawJsonSortOrder !== 'string') {
throw new Error(
`Invalid 'jsonSortOrder' option; expected string, got '${typeof prettierOptions.rawJsonSortOrder}'`,
);
}

let jsonSortOrder = null;
if (rawJsonSortOrder !== null) {
try {
jsonSortOrder = JSON.parse(rawJsonSortOrder);
} catch (error) {
// @ts-expect-error Error cause property not yet supported by '@types/node' (see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/61827)
throw new Error(`Failed to parse sort order option as JSON`, {
cause: error,
});
}

if (Array.isArray(jsonSortOrder) || typeof jsonSortOrder !== 'object') {
throw new Error(`Invalid custom sort order; must be an object`);
}

for (const categorySort of Object.values(jsonSortOrder)) {
if (!allowedCategorySortValues.includes(categorySort as any)) {
throw new Error(
`Invalid custom sort entry: value must be one of '${String(
allowedCategorySortValues,
)}', got '${String(categorySort)}'`,
);
}
const sortedAst = sortAst(ast, jsonRecursiveSort, sortCompareFunction);
}
}

return { jsonRecursiveSort, jsonSortOrder };
}

return {
...jsonRootAst,
node: sortedAst,
};
},
/**
* Create sort compare function from a custom JSON sort order configuration.
*
* @param jsonSortOrder - JSON sort order configuration.
* @returns A sorting function for comparing Object keys.
*/
function createSortCompareFunction(
jsonSortOrder: Record<string, CategorySort | null>,
): (a: string, b: string) => number {
const evaluateSortEntry = (value: string, entry: string): boolean => {
const regexRegex = /^\/(.+)\/([imsu]*)$/u;
if (entry.match(regexRegex)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [, regexSpec, flags]: string[] = entry.match(regexRegex)!;
// "regexSpec" guaranteed to be defined because of capture group. False positive for unnecessary type assertion.
const regex = new RegExp(regexSpec as string, flags);
return Boolean(value.match(regex));
}
return value === entry;
};

const sortEntries = Object.keys(jsonSortOrder);

return (a: string, b: string): number => {
const aIndex = sortEntries.findIndex(evaluateSortEntry.bind(null, a));
const bIndex = sortEntries.findIndex(evaluateSortEntry.bind(null, b));

if (aIndex === -1 && bIndex === -1) {
return lexicalSort(a, b);
} else if (bIndex === -1) {
return -1;
} else if (aIndex === -1) {
return 1;
} else if (aIndex === bIndex) {
// Sort entry guaranteed to be non-null because index was found
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const sortEntry = sortEntries[aIndex]!;
// Guaranteed to be defined because `sortEntry` is derived from `Object.keys`
const categorySort = jsonSortOrder[sortEntry] as null | CategorySort;
const categorySortFunction =
categorySort === null
? lexicalSort
: categorySortFunctions[categorySort];
return categorySortFunction(a, b);
}
return aIndex - bIndex;
};
}

/**
* Prettier JSON parsers.
*/
type JsonParser = 'json';

/**
* Create a JSON sorting parser based upon the specified Prettier parser.
*
* @param parser - The Prettier JSON parser to base the sorting on.
* @returns The JSON sorting parser.
*/
function createParser(
parser: JsonParser,
): (text: string, options: ParserOptions) => Promise<any> {
return async (text: string, prettierOptions: ParserOptions): Promise<any> => {
const { jsonRecursiveSort, jsonSortOrder } = parseOptions(prettierOptions);

const jsonRootAst = await babelParsers[parser].parse(text, prettierOptions);

// The Prettier JSON parser wraps the AST in a 'JsonRoot' node
// This ast variable is the real document root
const ast = jsonRootAst.node;

// Only objects are intended to be sorted by this plugin
// Arrays are considered only in recursive mode, so that we
// can get to nested objected.
if (
!(
ast.type === 'ObjectExpression' ||
(ast.type === 'ArrayExpression' && jsonRecursiveSort)
)
) {
return jsonRootAst;
}

let sortCompareFunction: (a: string, b: string) => number = lexicalSort;
if (jsonSortOrder) {
sortCompareFunction = createSortCompareFunction(jsonSortOrder);
}
const sortedAst = sortAst(ast, jsonRecursiveSort, sortCompareFunction);

return {
...jsonRootAst,
node: sortedAst,
};
};
}

export const parsers = {
json: {
...babelParsers.json,
parse: createParser('json'),
},
} as Record<string, Parser>;

Expand Down

0 comments on commit 6161d29

Please sign in to comment.