1 | /**
|
2 | * @file Runs `prettier` as an ESLint rule.
|
3 | * @author Andres Suarez
|
4 | */
|
5 |
|
6 | // @ts-check
|
7 |
|
8 | /**
|
9 | * @typedef {import('eslint').AST.Range} Range
|
10 | * @typedef {import('eslint').AST.SourceLocation} SourceLocation
|
11 | * @typedef {import('eslint').ESLint.Plugin} Plugin
|
12 | * @typedef {import('eslint').ESLint.ObjectMetaProperties} ObjectMetaProperties
|
13 | * @typedef {import('prettier').FileInfoOptions} FileInfoOptions
|
14 | * @typedef {import('prettier').Options} PrettierOptions
|
15 | * @typedef {PrettierOptions & { onDiskFilepath: string, parserMeta?: ObjectMetaProperties['meta'], parserPath?: string, usePrettierrc?: boolean }} Options
|
16 | */
|
17 |
|
18 | ;
|
19 |
|
20 | // ------------------------------------------------------------------------------
|
21 | // Requirements
|
22 | // ------------------------------------------------------------------------------
|
23 |
|
24 | const {
|
25 | showInvisibles,
|
26 | generateDifferences,
|
27 | } = require('prettier-linter-helpers');
|
28 | const { name, version } = require('./package.json');
|
29 |
|
30 | // ------------------------------------------------------------------------------
|
31 | // Constants
|
32 | // ------------------------------------------------------------------------------
|
33 |
|
34 | const { INSERT, DELETE, REPLACE } = generateDifferences;
|
35 |
|
36 | // ------------------------------------------------------------------------------
|
37 | // Privates
|
38 | // ------------------------------------------------------------------------------
|
39 |
|
40 | // Lazily-loaded Prettier.
|
41 | /**
|
42 | * @type {(source: string, options: Options, fileInfoOptions: FileInfoOptions) => string}
|
43 | */
|
44 | let prettierFormat;
|
45 |
|
46 | // ------------------------------------------------------------------------------
|
47 | // Rule Definition
|
48 | // ------------------------------------------------------------------------------
|
49 |
|
50 | /**
|
51 | * Reports a difference.
|
52 | *
|
53 | * @param {import('eslint').Rule.RuleContext} context - The ESLint rule context.
|
54 | * @param {import('prettier-linter-helpers').Difference} difference - The difference object.
|
55 | * @returns {void}
|
56 | */
|
57 | function reportDifference(context, difference) {
|
58 | const { operation, offset, deleteText = '', insertText = '' } = difference;
|
59 | const range = /** @type {Range} */ ([offset, offset + deleteText.length]);
|
60 | // `context.getSourceCode()` was deprecated in ESLint v8.40.0 and replaced
|
61 | // with the `sourceCode` property.
|
62 | // TODO: Only use property when our eslint peerDependency is >=8.40.0.
|
63 | const [start, end] = range.map(index =>
|
64 | (context.sourceCode ?? context.getSourceCode()).getLocFromIndex(index),
|
65 | );
|
66 |
|
67 | context.report({
|
68 | messageId: operation,
|
69 | data: {
|
70 | deleteText: showInvisibles(deleteText),
|
71 | insertText: showInvisibles(insertText),
|
72 | },
|
73 | loc: { start, end },
|
74 | fix: fixer => fixer.replaceTextRange(range, insertText),
|
75 | });
|
76 | }
|
77 |
|
78 | // ------------------------------------------------------------------------------
|
79 | // Module Definition
|
80 | // ------------------------------------------------------------------------------
|
81 |
|
82 | /**
|
83 | * @type {Plugin}
|
84 | */
|
85 | const eslintPluginPrettier = {
|
86 | meta: { name, version },
|
87 | configs: {
|
88 | recommended: {
|
89 | extends: ['prettier'],
|
90 | plugins: ['prettier'],
|
91 | rules: {
|
92 | 'prettier/prettier': 'error',
|
93 | 'arrow-body-style': 'off',
|
94 | 'prefer-arrow-callback': 'off',
|
95 | },
|
96 | },
|
97 | },
|
98 | rules: {
|
99 | prettier: {
|
100 | meta: {
|
101 | docs: {
|
102 | url: 'https://github.com/prettier/eslint-plugin-prettier#options',
|
103 | },
|
104 | type: 'layout',
|
105 | fixable: 'code',
|
106 | schema: [
|
107 | // Prettier options:
|
108 | {
|
109 | type: 'object',
|
110 | properties: {},
|
111 | additionalProperties: true,
|
112 | },
|
113 | {
|
114 | type: 'object',
|
115 | properties: {
|
116 | usePrettierrc: { type: 'boolean' },
|
117 | fileInfoOptions: {
|
118 | type: 'object',
|
119 | properties: {},
|
120 | additionalProperties: true,
|
121 | },
|
122 | },
|
123 | additionalProperties: true,
|
124 | },
|
125 | ],
|
126 | messages: {
|
127 | [INSERT]: 'Insert `{{ insertText }}`',
|
128 | [DELETE]: 'Delete `{{ deleteText }}`',
|
129 | [REPLACE]: 'Replace `{{ deleteText }}` with `{{ insertText }}`',
|
130 | },
|
131 | },
|
132 | create(context) {
|
133 | const usePrettierrc =
|
134 | !context.options[1] || context.options[1].usePrettierrc !== false;
|
135 | /**
|
136 | * @type {FileInfoOptions}
|
137 | */
|
138 | const fileInfoOptions =
|
139 | (context.options[1] && context.options[1].fileInfoOptions) || {};
|
140 |
|
141 | // `context.getSourceCode()` was deprecated in ESLint v8.40.0 and replaced
|
142 | // with the `sourceCode` property.
|
143 | // TODO: Only use property when our eslint peerDependency is >=8.40.0.
|
144 | const sourceCode = context.sourceCode ?? context.getSourceCode();
|
145 | // `context.getFilename()` was deprecated in ESLint v8.40.0 and replaced
|
146 | // with the `filename` property.
|
147 | // TODO: Only use property when our eslint peerDependency is >=8.40.0.
|
148 | const filepath = context.filename ?? context.getFilename();
|
149 |
|
150 | // Processors that extract content from a file, such as the markdown
|
151 | // plugin extracting fenced code blocks may choose to specify virtual
|
152 | // file paths. If this is the case then we need to resolve prettier
|
153 | // config and file info using the on-disk path instead of the virtual
|
154 | // path.
|
155 | // `context.getPhysicalFilename()` was deprecated in ESLint v8.40.0 and replaced
|
156 | // with the `physicalFilename` property.
|
157 | // TODO: Only use property when our eslint peerDependency is >=8.40.0.
|
158 | const onDiskFilepath =
|
159 | context.physicalFilename ?? context.getPhysicalFilename();
|
160 | const source = sourceCode.text;
|
161 |
|
162 | return {
|
163 | Program() {
|
164 | if (!prettierFormat) {
|
165 | // Prettier is expensive to load, so only load it if needed.
|
166 | prettierFormat = require('synckit').createSyncFn(
|
167 | require.resolve('./worker'),
|
168 | );
|
169 | }
|
170 |
|
171 | /**
|
172 | * @type {PrettierOptions}
|
173 | */
|
174 | const eslintPrettierOptions = context.options[0] || {};
|
175 |
|
176 | const parser = context.languageOptions?.parser;
|
177 |
|
178 | // prettier.format() may throw a SyntaxError if it cannot parse the
|
179 | // source code it is given. Usually for JS files this isn't a
|
180 | // problem as ESLint will report invalid syntax before trying to
|
181 | // pass it to the prettier plugin. However this might be a problem
|
182 | // for non-JS languages that are handled by a plugin. Notably Vue
|
183 | // files throw an error if they contain unclosed elements, such as
|
184 | // `<template><div></template>. In this case report an error at the
|
185 | // point at which parsing failed.
|
186 | /**
|
187 | * @type {string}
|
188 | */
|
189 | let prettierSource;
|
190 | try {
|
191 | prettierSource = prettierFormat(
|
192 | source,
|
193 | {
|
194 | ...eslintPrettierOptions,
|
195 | filepath,
|
196 | onDiskFilepath,
|
197 | parserMeta:
|
198 | parser &&
|
199 | (parser.meta ?? {
|
200 | name: parser.name,
|
201 | version: parser.version,
|
202 | }),
|
203 | parserPath: context.parserPath,
|
204 | usePrettierrc,
|
205 | },
|
206 | fileInfoOptions,
|
207 | );
|
208 | } catch (err) {
|
209 | if (!(err instanceof SyntaxError)) {
|
210 | throw err;
|
211 | }
|
212 |
|
213 | let message = 'Parsing error: ' + err.message;
|
214 |
|
215 | const error =
|
216 | /** @type {SyntaxError & {codeFrame: string; loc: SourceLocation}} */ (
|
217 | err
|
218 | );
|
219 |
|
220 | // Prettier's message contains a codeframe style preview of the
|
221 | // invalid code and the line/column at which the error occurred.
|
222 | // ESLint shows those pieces of information elsewhere already so
|
223 | // remove them from the message
|
224 | if (error.codeFrame) {
|
225 | message = message.replace(`\n${error.codeFrame}`, '');
|
226 | }
|
227 | if (error.loc) {
|
228 | message = message.replace(/ \(\d+:\d+\)$/, '');
|
229 | }
|
230 |
|
231 | context.report({ message, loc: error.loc });
|
232 |
|
233 | return;
|
234 | }
|
235 |
|
236 | if (prettierSource == null) {
|
237 | return;
|
238 | }
|
239 |
|
240 | if (source !== prettierSource) {
|
241 | const differences = generateDifferences(source, prettierSource);
|
242 |
|
243 | for (const difference of differences) {
|
244 | reportDifference(context, difference);
|
245 | }
|
246 | }
|
247 | },
|
248 | };
|
249 | },
|
250 | },
|
251 | },
|
252 | };
|
253 |
|
254 | module.exports = eslintPluginPrettier;
|