/
eslint-plugin-prettier.js
302 lines (272 loc) · 11.3 KB
/
eslint-plugin-prettier.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
/**
* @file Runs `prettier` as an ESLint rule.
* @author Andres Suarez
*/
'use strict';
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const {
showInvisibles,
generateDifferences,
} = require('prettier-linter-helpers');
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
const { INSERT, DELETE, REPLACE } = generateDifferences;
// ------------------------------------------------------------------------------
// Privates
// ------------------------------------------------------------------------------
// Lazily-loaded Prettier.
/**
* @type {import('prettier')}
*/
let prettier;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/**
* Reports a difference.
*
* @param {import('eslint').Rule.RuleContext} context - The ESLint rule context.
* @param {import('prettier-linter-helpers').Difference} difference - The difference object.
* @returns {void}
*/
function reportDifference(context, difference) {
const { operation, offset, deleteText = '', insertText = '' } = difference;
const range = [offset, offset + deleteText.length];
const [start, end] = range.map(index =>
context.getSourceCode().getLocFromIndex(index),
);
context.report({
messageId: operation,
data: {
deleteText: showInvisibles(deleteText),
insertText: showInvisibles(insertText),
},
loc: { start, end },
fix: fixer => fixer.replaceTextRange(range, insertText),
});
}
// ------------------------------------------------------------------------------
// Module Definition
// ------------------------------------------------------------------------------
module.exports = {
configs: {
recommended: {
extends: ['prettier'],
plugins: ['prettier'],
rules: {
'prettier/prettier': 'error',
'arrow-body-style': 'off',
'prefer-arrow-callback': 'off',
},
},
},
rules: {
prettier: {
meta: {
docs: {
url: 'https://github.com/prettier/eslint-plugin-prettier#options',
},
type: 'layout',
fixable: 'code',
schema: [
// Prettier options:
{
type: 'object',
properties: {},
additionalProperties: true,
},
{
type: 'object',
properties: {
usePrettierrc: { type: 'boolean' },
fileInfoOptions: {
type: 'object',
properties: {},
additionalProperties: true,
},
},
additionalProperties: true,
},
],
messages: {
[INSERT]: 'Insert `{{ insertText }}`',
[DELETE]: 'Delete `{{ deleteText }}`',
[REPLACE]: 'Replace `{{ deleteText }}` with `{{ insertText }}`',
},
},
create(context) {
const usePrettierrc =
!context.options[1] || context.options[1].usePrettierrc !== false;
const eslintFileInfoOptions =
(context.options[1] && context.options[1].fileInfoOptions) || {};
const sourceCode = context.getSourceCode();
const filepath = context.getFilename();
// Processors that extract content from a file, such as the markdown
// plugin extracting fenced code blocks may choose to specify virtual
// file paths. If this is the case then we need to resolve prettier
// config and file info using the on-disk path instead of the virtual
// path.
const onDiskFilepath = context.getPhysicalFilename();
const source = sourceCode.text;
return {
// eslint-disable-next-line sonarjs/cognitive-complexity
Program() {
if (!prettier) {
// Prettier is expensive to load, so only load it if needed.
prettier = require('prettier');
}
const eslintPrettierOptions = context.options[0] || {};
const prettierRcOptions = usePrettierrc
? prettier.resolveConfig.sync(onDiskFilepath, {
editorconfig: true,
})
: null;
const { ignored, inferredParser } = prettier.getFileInfo.sync(
onDiskFilepath,
{
resolveConfig: false,
withNodeModules: false,
ignorePath: '.prettierignore',
plugins: prettierRcOptions ? prettierRcOptions.plugins : null,
...eslintFileInfoOptions,
},
);
// Skip if file is ignored using a .prettierignore file
if (ignored) {
return;
}
const initialOptions = {};
// ESLint supports processors that let you extract and lint JS
// fragments within a non-JS language. In the cases where prettier
// supports the same language as a processor, we want to process
// the provided source code as javascript (as ESLint provides the
// rules with fragments of JS) instead of guessing the parser
// based off the filename. Otherwise, for instance, on a .md file we
// end up trying to run prettier over a fragment of JS using the
// markdown parser, which throws an error.
// Processors may set virtual filenames for these extracted blocks.
// If they do so then we want to trust the file extension they
// provide, and no override is needed.
// If the processor does not set any virtual filename (signified by
// `filepath` and `onDiskFilepath` being equal) AND we can't
// infer the parser from the filename, either because no filename
// was provided or because there is no parser found for the
// filename, use javascript.
// This is added to the options first, so that
// prettierRcOptions and eslintPrettierOptions can still override
// the parser.
//
// `parserBlocklist` should contain the list of prettier parser
// names for file types where:
// * Prettier supports parsing the file type
// * There is an ESLint processor that extracts JavaScript snippets
// from the file type.
if (filepath === onDiskFilepath) {
// The following list means the plugin process source into js content
// but with same filename, so we need to change the parser to `babel`
// by default.
// Related ESLint plugins are:
// 1. `eslint-plugin-graphql` (replacement: `@graphql-eslint/eslint-plugin`)
// 2. `eslint-plugin-html`
// 3. `eslint-plugin-markdown@1` (replacement: `eslint-plugin-markdown@2+`)
// 4. `eslint-plugin-svelte3` (replacement: `eslint-plugin-svelte@2+`)
const parserBlocklist = [null, 'markdown', 'html'];
let inferParserToBabel = parserBlocklist.includes(inferredParser);
switch (inferredParser) {
// it could be processed by `@graphql-eslint/eslint-plugin` or `eslint-plugin-graphql`
case 'graphql': {
if (
// for `eslint-plugin-graphql`, see https://github.com/apollographql/eslint-plugin-graphql/blob/master/src/index.js#L416
source.startsWith('ESLintPluginGraphQLFile`')
) {
inferParserToBabel = true;
}
break;
}
// it could be processed by `@ota-meshi/eslint-plugin-svelte`, `eslint-plugin-svelte` or `eslint-plugin-svelte3`
case 'svelte': {
// The `source` would be modified by `eslint-plugin-svelte3`
if (!context.parserPath.includes('svelte-eslint-parser')) {
// We do not support `eslint-plugin-svelte3`,
// the users should run `prettier` on `.svelte` files manually
return;
}
}
}
if (inferParserToBabel) {
initialOptions.parser = 'babel';
}
} else {
// Similar to https://github.com/prettier/stylelint-prettier/pull/22
// In all of the following cases ESLint extracts a part of a file to
// be formatted and there exists a prettier parser for the whole file.
// If you're interested in prettier you'll want a fully formatted file so
// you're about to run prettier over the whole file anyway.
// Therefore running prettier over just the style section is wasteful, so
// skip it.
const parserBlocklist = [
'babel',
'babylon',
'flow',
'typescript',
'vue',
'markdown',
'html',
'mdx',
'angular',
'svelte',
];
if (parserBlocklist.includes(inferredParser)) {
return;
}
}
const prettierOptions = {
...initialOptions,
...prettierRcOptions,
...eslintPrettierOptions,
filepath,
};
// prettier.format() may throw a SyntaxError if it cannot parse the
// source code it is given. Usually for JS files this isn't a
// problem as ESLint will report invalid syntax before trying to
// pass it to the prettier plugin. However this might be a problem
// for non-JS languages that are handled by a plugin. Notably Vue
// files throw an error if they contain unclosed elements, such as
// `<template><div></template>. In this case report an error at the
// point at which parsing failed.
let prettierSource;
try {
prettierSource = prettier.format(source, prettierOptions);
} catch (err) {
if (!(err instanceof SyntaxError)) {
throw err;
}
let message = 'Parsing error: ' + err.message;
// Prettier's message contains a codeframe style preview of the
// invalid code and the line/column at which the error occurred.
// ESLint shows those pieces of information elsewhere already so
// remove them from the message
if (err.codeFrame) {
message = message.replace(`\n${err.codeFrame}`, '');
}
if (err.loc) {
message = message.replace(/ \(\d+:\d+\)$/, '');
}
context.report({ message, loc: err.loc });
return;
}
if (source !== prettierSource) {
const differences = generateDifferences(source, prettierSource);
for (const difference of differences) {
reportDifference(context, difference);
}
}
},
};
},
},
},
};