Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions prettier-plugin-fake-chaining/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { parsers as typescriptParsers } from "prettier/plugins/typescript";

// Mimics how prettier-plugin-tailwindcss composes: on every preprocess/parse
// call it re-resolves the underlying parser by walking `options.plugins` and
// `Object.assign`-ing each plugin's parser for this name on top of its own. The
// resulting object's preprocess/parse therefore points at whichever plugin sits
// later in the chain. The important detail is that this lookup can re-enter
// another plugin's parser during the same format call.
const PLUGIN_NAME = "prettier-plugin-fake-chaining";
const baseParser = typescriptParsers.typescript;

function waitForNextTask() {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}

function resolveUnderlyingParser(parserName, options) {
const merged = { ...baseParser };

if (!options || !options.plugins) {
return merged;
}

for (const plugin of options.plugins) {
if (
typeof plugin !== "object" ||
plugin === null ||
plugin.name === PLUGIN_NAME ||
!plugin.parsers ||
!(parserName in plugin.parsers)
) {
continue;
}

Object.assign(merged, plugin.parsers[parserName]);
}

return merged;
}

export const name = PLUGIN_NAME;

export const parsers = {
typescript: {
...baseParser,

preprocess(text, options) {
const underlying = resolveUnderlyingParser("typescript", options);

return underlying.preprocess
? underlying.preprocess(text, options)
: text;
},

async parse(text, options) {
await waitForNextTask();

const underlying = resolveUnderlyingParser("typescript", options);

return underlying.parse(text, options);
},
},
};
9 changes: 9 additions & 0 deletions prettier-plugin-fake-chaining/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "prettier-plugin-fake-chaining",
"version": "1.0.0",
"private": true,
"type": "module",
"files": [
"index.js"
]
}
71 changes: 50 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,53 +228,82 @@ const defaultOptions: JsdocOptions = {
jsdocBracketSpacing: options.jsdocBracketSpacing.default,
};

const parserCache = new Map<string, prettier.Parser>();

function getMergedParser(
originalParser: prettier.Parser,
parserName: string,
): prettier.Parser {
let cached = parserCache.get(parserName);
if (!cached) {
cached = mergeParsers(originalParser, parserName);
parserCache.set(parserName, cached);
}

return cached;
}

const parsers = {
// JS - Babel
get babel() {
const parser = parserBabel.parsers.babel;
return mergeParsers(parser, "babel");
return getMergedParser(parserBabel.parsers.babel, "babel");
},
get "babel-flow"() {
const parser = parserBabel.parsers["babel-flow"];
return mergeParsers(parser, "babel-flow");
return getMergedParser(parserBabel.parsers["babel-flow"], "babel-flow");
},
get "babel-ts"() {
const parser = parserBabel.parsers["babel-ts"];
return mergeParsers(parser, "babel-ts");
return getMergedParser(parserBabel.parsers["babel-ts"], "babel-ts");
},
// JS - Flow
get flow() {
const parser = parserFlow.parsers.flow;
return mergeParsers(parser, "flow");
return getMergedParser(parserFlow.parsers.flow, "flow");
},
// JS - TypeScript
get typescript(): prettier.Parser {
const parser = parserTypescript.parsers.typescript;

return mergeParsers(parser, "typescript");
// require("./parser-typescript").parsers.typescript;
return getMergedParser(parserTypescript.parsers.typescript, "typescript");
},
get "jsdoc-parser"() {
// Backward compatible, don't use this in new version since 1.0.0
const parser = parserBabel.parsers["babel-ts"];

return mergeParsers(parser, "babel-ts");
return getMergedParser(parserBabel.parsers["babel-ts"], "babel-ts");
},
};

function mergeParsers(originalParser: prettier.Parser, parserName: string) {
const jsDocParse = getParser(originalParser.parse, parserName) as any;
let hasPreprocessed = false;
// Chaining plugins can re-resolve parsers and re-enter this parser during the
// same format call. Recursive entries fall back to Prettier's raw parser so
// the chain can unwind, while separate options objects still format normally.
const activePreprocesses = new WeakSet<prettier.ParserOptions>();
const activeParses = new WeakSet<prettier.ParserOptions>();

const innerParse = getParser(originalParser.parse, parserName);
const jsDocParse = async (
text: string,
parsersOrOptions: any,
maybeOptions?: any,
) => {
const options = (maybeOptions ??
parsersOrOptions) as prettier.ParserOptions;

if (activeParses.has(options)) {
return originalParser.parse(text, options);
}

activeParses.add(options);
try {
return await innerParse(text, parsersOrOptions, maybeOptions);
} finally {
activeParses.delete(options);
}
};

const jsDocPreprocess = (text: string, options: prettier.ParserOptions) => {
normalizeOptions(options as any);

// Prevent infinite recursion by checking if we've already preprocessed
if (hasPreprocessed) {
if (activePreprocesses.has(options)) {
return text;
}

hasPreprocessed = true;
activePreprocesses.add(options);
try {
const tsPluginParser = findPluginByParser(parserName, options);

Expand All @@ -288,7 +317,7 @@ function mergeParsers(originalParser: prettier.Parser, parserName: string) {
tsPluginParser?.preprocess || originalParser.preprocess;
return preprocess ? preprocess(text, options) : text;
} finally {
hasPreprocessed = false;
activePreprocesses.delete(options);
}
};

Expand Down
11 changes: 10 additions & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,17 @@ const {
description: descriptionTokenizer,
} = tokenizers;

type JsdocParser = (
text: string,
parsersOrOptions: Parameters<Parser["parse"]>[1],
maybeOptions?: AllOptions,
) => ReturnType<Parser["parse"]>;

/** @link https://prettier.io/docs/en/api.html#custom-parser-api} */
export const getParser = (originalParse: Parser["parse"], parserName: string) =>
export const getParser = (
originalParse: Parser["parse"],
parserName: string,
): JsdocParser =>
async function jsdocParser(
text: string,
parsersOrOptions: Parameters<Parser["parse"]>[1],
Expand Down
39 changes: 39 additions & 0 deletions tests/__snapshots__/compatibleWithPlugins.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,42 @@ import a from "a";
const testFunction = (text, defaultValue, optionalNumber) => true;
"
`;

exports[`combined with a plugin that chains Should format concurrent calls consistently 1`] = `
[
"/**
* @param {String | Number} text - Some text description
* @param {String} [defaultValue="defaultTest"] TODO. Default is \`"defaultTest"\`
* @returns {Boolean} Description for returns
*/
const testFunction = (text, defaultValue) => true;
",
"/**
* @param {String | Number} text - Some text description
* @param {String} [defaultValue="defaultTest"] TODO. Default is \`"defaultTest"\`
* @returns {Boolean} Description for returns
*/
const testFunction = (text, defaultValue) => true;
",
]
`;

exports[`combined with a plugin that chains Should format without infinite recursion (chaining plugin first) 1`] = `
"/**
* @param {String | Number} text - Some text description
* @param {String} [defaultValue="defaultTest"] TODO. Default is \`"defaultTest"\`
* @returns {Boolean} Description for returns
*/
const testFunction = (text, defaultValue) => true;
"
`;

exports[`combined with a plugin that chains Should format without infinite recursion (chaining plugin last) 1`] = `
"/**
* @param {String | Number} text - Some text description
* @param {String} [defaultValue="defaultTest"] TODO. Default is \`"defaultTest"\`
* @returns {Boolean} Description for returns
*/
const testFunction = (text, defaultValue) => true;
"
`;
46 changes: 46 additions & 0 deletions tests/compatibleWithPlugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,49 @@ async function example(name: string): Promise<void> {}
});
});
});

describe("combined with a plugin that chains", () => {
function format(code: string, plugins: string[]) {
return prettier.format(code, {
parser: "typescript",
plugins,
} as AllOptions);
}

const code = `/**
* @param {String|Number} text - some text description
* @param {String} [defaultValue="defaultTest"] TODO
* @returns {Boolean} Description for returns
*/
const testFunction = (text, defaultValue) => true;
`;

test("Should format without infinite recursion (chaining plugin first)", async () => {
const result = await format(code, [
"./prettier-plugin-fake-chaining/index.js",
"prettier-plugin-jsdoc",
]);
expect(result).toMatchSnapshot();
});

test("Should format without infinite recursion (chaining plugin last)", async () => {
const result = await format(code, [
"prettier-plugin-jsdoc",
"./prettier-plugin-fake-chaining/index.js",
]);
expect(result).toMatchSnapshot();
});

test("Should format concurrent calls consistently", async () => {
const plugins = [
"./prettier-plugin-fake-chaining/index.js",
"prettier-plugin-jsdoc",
];
const results = await Promise.all([
format(code, plugins),
format(code, plugins),
]);

expect(results).toMatchSnapshot();
});
});
Loading