Add isSupported function support for language (#17331)

Co-authored-by: fisker Cheung <lionkay@gmail.com>
Co-authored-by: fisker <lionkay@gmail.com>
This commit is contained in:
JounQinandfisker Cheung authored and GitHub committed 2025-04-14 09:15:43 +08:00
1 parent 7e892c4613
commit c93393c0be
11 files changed
+163 -16

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
#### Add `isSupported` function support for `languages` API (#17331 by @JounQin)
Previously, `languages` API for custom plugin only supported to infer parser based on the file basename or extension.
Prettier main added `isSupported: (file: string) => boolean` function to allow plugin check if file is supported based on the full path (eg: files in a specific directory), the `file` parameter could be a normal path or a url string like `file:///C:/test.txt`.
If no `isSupported` provided, it just behaviors the same way as before.
```js
export const languages = [
{
name: "foo",
parsers: ["foo"],
isSupported: (file) => file.includes(".foo"),
},
];
```
+1
View File
@@ -124,6 +124,7 @@ The support information looks like this:
filenames?: string[];
linguistLanguageId?: number;
vscodeLanguageIds?: string[];
isSupported?(file: string): boolean;
}>;
}
```
+1 -1
View File
@@ -70,7 +70,7 @@ Strings provided to `plugins` are ultimately passed to [`import()` expression](h
- [`prettier-plugin-sql-cst`](https://github.com/nene/prettier-plugin-sql-cst) by [**@nene**](https://github.com/nene)
- [`prettier-plugin-solidity`](https://github.com/prettier-solidity/prettier-plugin-solidity) by [**@mattiaerre**](https://github.com/mattiaerre)
- [`prettier-plugin-svelte`](https://github.com/sveltejs/prettier-plugin-svelte) by [**@sveltejs**](https://github.com/sveltejs)
- [`prettier-plugin-toml`](https://github.com/bd82/toml-tools/tree/master/packages/prettier-plugin-toml) by [**@bd82**](https://github.com/bd82)
- [`prettier-plugin-toml`](https://github.com/un-ts/prettier/tree/master/packages/toml) by [**@JounQin**](https://github.com/JounQin) and [**@so1ve**](https://github.com/so1ve)
## Developing Plugins
+2 -1
View File
@@ -1,3 +1,4 @@
import path from "node:path";
import stringify from "fast-json-stable-stringify";
import { format, getFileInfo } from "../index.js";
import { printToScreen } from "./utils.js";
@@ -11,7 +12,7 @@ async function logFileInfoOrDie(context) {
config,
} = context.argv;
const fileInfo = await getFileInfo(file, {
const fileInfo = await getFileInfo(path.resolve(file), {
ignorePath,
withNodeModules,
plugins,
+9 -6
View File
@@ -239,10 +239,12 @@ async function formatStdin(context) {
// TODO[@fisker]: Exit if no input.
// `prettier --config-precedence cli-override`
const absoluteFilepath = filepath ? path.resolve(filepath) : undefined;
let isFileIgnored = false;
if (filepath) {
if (absoluteFilepath) {
const isIgnored = await createIsIgnoredFromContextOrDie(context);
isFileIgnored = isIgnored(filepath);
isFileIgnored = isIgnored(absoluteFilepath);
}
if (isFileIgnored) {
@@ -250,10 +252,11 @@ async function formatStdin(context) {
return;
}
const options = await getOptionsForFile(
context,
filepath ? path.resolve(filepath) : undefined,
);
const options = {
...(await getOptionsForFile(context, absoluteFilepath)),
// `getOptionsForFile` forwards `--stdin-filepath` directly, which can be a relative path
filepath: absoluteFilepath,
};
if (await listDifferent(context, input, options, "(stdin)")) {
return;
+1
View File
@@ -674,6 +674,7 @@ export interface SupportLanguage {
linguistLanguageId?: number | undefined;
vscodeLanguageIds?: string[] | undefined;
interpreters?: string[] | undefined;
isSupported?: ((file: string) => boolean) | undefined;
}
export interface SupportOptionRange {
+45 -5
View File
@@ -1,12 +1,20 @@
import getInterpreter from "./get-interpreter.js";
import isNonEmptyArray from "./is-non-empty-array.js";
/** @import {Options} from "../index.js" */
/** @import {Options, SupportLanguage} from "../index.js" */
// Didn't use `path.basename` since this module need work in browsers too
// And `file` can be a `URL`
/**
* Didn't use `path.basename` since this module should work in browsers too
* And `file` can be a `URL`
* @param {string | URL} file
*/
const getFileBasename = (file) => String(file).split(/[/\\]/u).pop();
/**
* @param {SupportLanguage[]} languages
* @param {string | URL | undefined} [file]
* @returns {SupportLanguage | undefined}
*/
function getLanguageByFileName(languages, file) {
if (!file) {
return;
@@ -24,6 +32,11 @@ function getLanguageByFileName(languages, file) {
);
}
/**
* @param {SupportLanguage[]} languages
* @param {string | undefined} [languageName]
* @returns {SupportLanguage | undefined}
*/
function getLanguageByLanguageName(languages, languageName) {
if (!languageName) {
return;
@@ -36,6 +49,11 @@ function getLanguageByLanguageName(languages, languageName) {
);
}
/**
* @param {SupportLanguage[]} languages
* @param {string | URL | undefined} [file]
* @returns {SupportLanguage | undefined}
*/
function getLanguageByInterpreter(languages, file) {
if (
process.env.PRETTIER_TARGET === "universal" ||
@@ -65,10 +83,30 @@ function getLanguageByInterpreter(languages, file) {
);
}
/**
* @param {SupportLanguage[]} languages
* @param {string | URL | undefined} [file]
* @returns {SupportLanguage | undefined}
*/
function getLanguageByIsSupported(languages, file) {
if (!file) {
return;
}
/*
We can't use `url.fileURLToPath` here since this module should work in browsers too
`URL#pathname` won't work either since `new URL('file:///C:/path/to/file').pathname`
equals to `/C:/path/to/file`, try to improve this part in future
*/
file = String(file);
return languages.find(({ isSupported }) => isSupported?.(file));
}
/**
* @param {Options} options
* @param {{physicalFile?: string | URL, file?: string | URL, language?: string}} fileInfo
* @returns {string | void} matched parser name if found
* @param {{physicalFile?: string | URL | undefined, file?: string | URL | undefined, language?: string | undefined}} fileInfo
* @returns {string | undefined} matched parser name if found
*/
function inferParser(options, fileInfo) {
const languages = options.plugins.flatMap(
@@ -84,6 +122,8 @@ function inferParser(options, fileInfo) {
getLanguageByLanguageName(languages, fileInfo.language) ??
getLanguageByFileName(languages, fileInfo.physicalFile) ??
getLanguageByFileName(languages, fileInfo.file) ??
getLanguageByIsSupported(languages, fileInfo.physicalFile) ??
getLanguageByIsSupported(languages, fileInfo.file) ??
getLanguageByInterpreter(languages, fileInfo.physicalFile);
return language?.parsers[0];
@@ -80,17 +80,34 @@ exports[`Interpreters (stdout) 1`] = `"{ "ignored": false, "inferredParser": "ba
exports[`Known/Unknown (stdout) 1`] = `"known.js"`;
exports[`isSupported (stdout) 1`] = `
"{
"ignored": false,
"inferredParser": "parser-name-inferred-from-language-is-supported"
}"
`;
exports[`isSupported (stdout) 2`] = `
"content from file
formatted by 'parser-name-inferred-from-language-is-supported' parser"
`;
exports[`isSupported (stdout) 3`] = `
"content from stdin
formatted by 'parser-name-inferred-from-language-is-supported' parser"
`;
exports[`stdin no path and no parser --check logs error but exits with 0 (stderr) 1`] = `"[error] No parser and no file path given, couldn't infer a parser."`;
exports[`stdin no path and no parser --list-different logs error but exits with 0 (stderr) 1`] = `"[error] No parser and no file path given, couldn't infer a parser."`;
exports[`stdin no path and no parser logs error and exits with 2 (stderr) 1`] = `"[error] No parser and no file path given, couldn't infer a parser."`;
exports[`stdin with unknown path and no parser --check logs error but exits with 0 (stderr) 1`] = `"[error] No parser could be inferred for file "foo"."`;
exports[`stdin with unknown path and no parser --check logs error but exits with 0 (stderr) 1`] = `"[error] No parser could be inferred for file "<cli>/infer-parser/foo"."`;
exports[`stdin with unknown path and no parser --list-different logs error but exits with 0 (stderr) 1`] = `"[error] No parser could be inferred for file "foo"."`;
exports[`stdin with unknown path and no parser --list-different logs error but exits with 0 (stderr) 1`] = `"[error] No parser could be inferred for file "<cli>/infer-parser/foo"."`;
exports[`stdin with unknown path and no parser logs error and exits with 2 (stderr) 1`] = `"[error] No parser could be inferred for file "foo"."`;
exports[`stdin with unknown path and no parser logs error and exits with 2 (stderr) 1`] = `"[error] No parser could be inferred for file "<cli>/infer-parser/foo"."`;
exports[`unknown path and no parser multiple files (stderr) 1`] = `"[error] No parser could be inferred for file "<cli>/infer-parser/FOO"."`;
@@ -199,3 +199,41 @@ describe("Interpreters", () => {
write: [],
});
});
describe("isSupported", () => {
runCli("cli/infer-parser", [
"--plugin",
"../../plugins/languages/is-supported.js",
"--file-info",
".husky/pre-commit",
]).test({
status: 0,
stderr: "",
write: [],
});
runCli("cli/infer-parser", [
"--plugin",
"../../plugins/languages/is-supported.js",
".husky/pre-commit",
]).test({
status: 0,
stderr: "",
write: [],
});
runCli(
"cli/infer-parser",
[
"--plugin",
"../../plugins/languages/is-supported.js",
"--stdin-filepath",
".husky/pre-commit",
],
{ input: "content from stdin" },
).test({
status: 0,
stderr: "",
write: [],
});
});
@@ -0,0 +1 @@
content from file
@@ -0,0 +1,28 @@
import path from "node:path";
import createPlugin from "../../../config/utils/create-plugin.cjs";
const PARSER_NAME = "parser-name-inferred-from-language-is-supported";
const PRINT_MARK = `formatted by '${PARSER_NAME}' parser`
const languages = [
{
name: "language-name-does-not-matter",
parsers: [PARSER_NAME],
isSupported(file) {
if (!path.isAbsolute(file)) {
throw new Error("Unexpected non absolute path");
}
return /(?<separator>[\\/])\.husky\k<separator>[^\\/]+$/u.test(file);
},
},
];
export default {
...createPlugin({
name: PARSER_NAME,
print: (content) => `${content.replace(PRINT_MARK,"").trim()}\n${PRINT_MARK}`,
finalNewLine: false,
}),
languages,
};