diff --git a/.changeset/weak-lamps-refuse.md b/.changeset/weak-lamps-refuse.md new file mode 100644 index 00000000..511cdac0 --- /dev/null +++ b/.changeset/weak-lamps-refuse.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-prettier": minor +--- + +feat: support non-js languages like `css` for `@eslint/css` and `json` for `@eslint/json` diff --git a/eslint-plugin-prettier.js b/eslint-plugin-prettier.js index 4b5c8c9a..46cb3448 100644 --- a/eslint-plugin-prettier.js +++ b/eslint-plugin-prettier.js @@ -7,6 +7,7 @@ /** * @import {AST, ESLint, Linter, Rule, SourceCode} from 'eslint' + * @import {Position} from 'estree' * @import {FileInfoOptions, Options as PrettierOptions} from 'prettier' * @import {Difference} from 'prettier-linter-helpers' */ @@ -57,6 +58,42 @@ let prettierFormat; // Rule Definition // ------------------------------------------------------------------------------ +/** @type {WeakMap} */ +const lineIndexesCache = new WeakMap(); + +/** + * Ponyfill `sourceCode.getLocFromIndex` when it's unavailable. + * + * See also `getLocFromIndex` in `@eslint/js`. + * + * @param {SourceCode} sourceCode + * @param {number} index + * @returns {Position} + */ +function getLocFromIndex(sourceCode, index) { + if (typeof sourceCode.getLocFromIndex === 'function') { + return sourceCode.getLocFromIndex(index); + } + + let lineIndexes = lineIndexesCache.get(sourceCode); + if (!lineIndexes) { + lineIndexes = [...sourceCode.text.matchAll(/\r?\n/g)].map( + match => match.index, + ); + // first line in the file starts at byte offset 0 + lineIndexes.unshift(0); + lineIndexesCache.set(sourceCode, lineIndexes); + } + + let line = 0; + while (line + 1 < lineIndexes.length && lineIndexes[line + 1] < index) { + line += 1; + } + const column = index - lineIndexes[line]; + + return { line: line + 1, column }; +} + /** * Reports a difference. * @@ -71,9 +108,9 @@ function reportDifference(context, difference) { // `context.getSourceCode()` was deprecated in ESLint v8.40.0 and replaced // with the `sourceCode` property. // TODO: Only use property when our eslint peerDependency is >=8.40.0. - const [start, end] = range.map(index => - (context.sourceCode ?? context.getSourceCode()).getLocFromIndex(index), - ); + const sourceCode = context.sourceCode ?? context.getSourceCode(); + + const [start, end] = range.map(index => getLocFromIndex(sourceCode, index)); context.report({ messageId: operation, @@ -168,7 +205,8 @@ const eslintPluginPrettier = { const source = sourceCode.text; return { - Program(node) { + /** @param {unknown} node */ + [sourceCode.ast.type](node) { if (!prettierFormat) { // Prettier is expensive to load, so only load it if needed. prettierFormat = /** @type {PrettierFormat} */ ( diff --git a/package.json b/package.json index 35a278d6..71facf57 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@commitlint/config-conventional": "^19.8.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", "@eslint/js": "^9.23.0", + "@eslint/json": "^0.12.0", "@graphql-eslint/eslint-plugin": "^4.3.0", "@html-eslint/parser": "^0.41.0", "@prettier/plugin-pug": "^3.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dbeaf6a..b1ba0f64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,7 @@ specifiers: '@commitlint/config-conventional': ^19.8.0 '@eslint-community/eslint-plugin-eslint-comments': ^4.4.1 '@eslint/js': ^9.23.0 + '@eslint/json': ^0.12.0 '@graphql-eslint/eslint-plugin': ^4.3.0 '@html-eslint/parser': ^0.41.0 '@prettier/plugin-pug': ^3.2.1 @@ -55,6 +56,7 @@ devDependencies: '@commitlint/config-conventional': 19.8.0 '@eslint-community/eslint-plugin-eslint-comments': 4.4.1_eslint@9.23.0 '@eslint/js': 9.23.0 + '@eslint/json': 0.12.0 '@graphql-eslint/eslint-plugin': 4.3.0_garjxkg63rquziivo3mxre4wya '@html-eslint/parser': 0.41.0 '@prettier/plugin-pug': 3.2.1_prettier@3.5.3 @@ -735,6 +737,16 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: true + /@eslint/json/0.12.0: + resolution: {integrity: sha512-n/7dz8HFStpEe4o5eYk0tdkBdGUS/ZGb0GQCeDWN1ZmRq67HMHK4vC33b0rQlTT6xdZoX935P4vstiWVk5Ying==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@eslint/core': 0.12.0 + '@eslint/plugin-kit': 0.2.7 + '@humanwhocodes/momoa': 3.3.8 + natural-compare: 1.4.0 + dev: true + /@eslint/object-schema/2.1.6: resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1098,6 +1110,11 @@ packages: engines: {node: '>=12.22'} dev: true + /@humanwhocodes/momoa/3.3.8: + resolution: {integrity: sha512-/3PZzor2imi/RLLcnHztkwA79txiVvW145Ve2cp5dxRcH5qOUNJPToasqLFHniTfw4B4lT7jGDdBOPXbXYlIMQ==} + engines: {node: '>=18'} + dev: true + /@humanwhocodes/retry/0.3.1: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} diff --git a/test/fixtures/empty.json b/test/fixtures/empty.json new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/fixtures/empty.json @@ -0,0 +1 @@ + diff --git a/test/invalid/json.txt b/test/invalid/json.txt new file mode 100644 index 00000000..f97a86d2 --- /dev/null +++ b/test/invalid/json.txt @@ -0,0 +1,22 @@ +CODE: +{ +"a": [ +"b", +{"c": +"d"} +] } + +OUTPUT: +{ + "a": ["b", { "c": "d" }] +} + +OPTIONS: +[] + +ERRORS: +[ + { + message: 'Replace `"a":·[⏎"b",⏎{"c":⏎"d"}⏎]·` with `··"a":·["b",·{·"c":·"d"·}]⏎`', + }, +] diff --git a/test/prettier.mjs b/test/prettier.mjs index be074907..1903de42 100644 --- a/test/prettier.mjs +++ b/test/prettier.mjs @@ -25,12 +25,15 @@ import eslintPluginPug from 'eslint-plugin-pug'; import vueEslintParser from 'vue-eslint-parser'; import * as eslintPluginGraphql from '@graphql-eslint/eslint-plugin'; import eslintMdx from 'eslint-mdx'; +import eslintPluginJson from '@eslint/json'; const rule = eslintPluginPrettier.rules.prettier; const RuleTester = eslintUnsupportedApi.FlatRuleTester ?? eslintPackage.RuleTester; const ESLint = eslintUnsupportedApi.FlatESLint ?? eslintPackage.ESLint; +const isESLint9 = !eslintUnsupportedApi.FlatRuleTester; + // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ @@ -380,6 +383,53 @@ runFixture('invalid-prettierrc/*', [ ], ]); +runFixture('*.json', [ + [ + { + column: 1, + endColumn: 1, + endLine: 2, + fix: { + range: [0, 1], + text: '', + }, + line: 1, + message: 'Delete `⏎`', + messageId: 'delete', + nodeType: null, + ruleId: 'prettier/prettier', + severity: 2, + }, + ], +]); + +if (isESLint9) { + const jsonRuleTester = new RuleTester({ + plugins: { + json: eslintPluginJson, + }, + language: 'json/json', + }); + + jsonRuleTester.run('@eslint/json', rule, { + valid: [ + { + code: '{}\n', + filename: 'empty.json', + }, + { + code: '{ "foo": 1 }\n', + filename: 'simple.json', + }, + ], + invalid: [ + Object.assign(loadInvalidFixture('json'), { + filename: 'invalid.json', + }), + ], + }); +} + // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ @@ -436,7 +486,7 @@ function getPrettierRcJsFilename(dir, file = 'dummy.js') { * @type {ESLint} * @import {ESLint} from 'eslint' */ -let eslint; +var eslint; // bad mocha: `ReferenceError: Cannot access 'eslint' before initialization` /** * @param {string} pattern @@ -504,11 +554,16 @@ async function runFixture(pattern, asserts, skip) { pug: eslintPluginPug, }, }, + { + files: ['**/*.json'], + plugins: { + json: eslintPluginJson, + }, + }, ], ignore: false, }); } - if (skip) { return; }