1 | /**
|
2 | * @fileoverview Runs `prettier` as an ESLint rule.
|
3 | * @author Andres Suarez
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | // ------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | // ------------------------------------------------------------------------------
|
11 |
|
12 | const {
|
13 | showInvisibles,
|
14 | generateDifferences
|
15 | } = require('prettier-linter-helpers');
|
16 |
|
17 | // ------------------------------------------------------------------------------
|
18 | // Constants
|
19 | // ------------------------------------------------------------------------------
|
20 |
|
21 | const { INSERT, DELETE, REPLACE } = generateDifferences;
|
22 |
|
23 | // ------------------------------------------------------------------------------
|
24 | // Privates
|
25 | // ------------------------------------------------------------------------------
|
26 |
|
27 | // Lazily-loaded Prettier.
|
28 | let prettier;
|
29 |
|
30 | // ------------------------------------------------------------------------------
|
31 | // Rule Definition
|
32 | // ------------------------------------------------------------------------------
|
33 |
|
34 | /**
|
35 | * Reports an "Insert ..." issue where text must be inserted.
|
36 | * @param {RuleContext} context - The ESLint rule context.
|
37 | * @param {number} offset - The source offset where to insert text.
|
38 | * @param {string} text - The text to be inserted.
|
39 | * @returns {void}
|
40 | */
|
41 | function reportInsert(context, offset, text) {
|
42 | const pos = context.getSourceCode().getLocFromIndex(offset);
|
43 | const range = [offset, offset];
|
44 | context.report({
|
45 | message: 'Insert `{{ code }}`',
|
46 | data: { code: showInvisibles(text) },
|
47 | loc: { start: pos, end: pos },
|
48 | fix(fixer) {
|
49 | return fixer.insertTextAfterRange(range, text);
|
50 | }
|
51 | });
|
52 | }
|
53 |
|
54 | /**
|
55 | * Reports a "Delete ..." issue where text must be deleted.
|
56 | * @param {RuleContext} context - The ESLint rule context.
|
57 | * @param {number} offset - The source offset where to delete text.
|
58 | * @param {string} text - The text to be deleted.
|
59 | * @returns {void}
|
60 | */
|
61 | function reportDelete(context, offset, text) {
|
62 | const start = context.getSourceCode().getLocFromIndex(offset);
|
63 | const end = context.getSourceCode().getLocFromIndex(offset + text.length);
|
64 | const range = [offset, offset + text.length];
|
65 | context.report({
|
66 | message: 'Delete `{{ code }}`',
|
67 | data: { code: showInvisibles(text) },
|
68 | loc: { start, end },
|
69 | fix(fixer) {
|
70 | return fixer.removeRange(range);
|
71 | }
|
72 | });
|
73 | }
|
74 |
|
75 | /**
|
76 | * Reports a "Replace ... with ..." issue where text must be replaced.
|
77 | * @param {RuleContext} context - The ESLint rule context.
|
78 | * @param {number} offset - The source offset where to replace deleted text
|
79 | with inserted text.
|
80 | * @param {string} deleteText - The text to be deleted.
|
81 | * @param {string} insertText - The text to be inserted.
|
82 | * @returns {void}
|
83 | */
|
84 | function reportReplace(context, offset, deleteText, insertText) {
|
85 | const start = context.getSourceCode().getLocFromIndex(offset);
|
86 | const end = context
|
87 | .getSourceCode()
|
88 | .getLocFromIndex(offset + deleteText.length);
|
89 | const range = [offset, offset + deleteText.length];
|
90 | context.report({
|
91 | message: 'Replace `{{ deleteCode }}` with `{{ insertCode }}`',
|
92 | data: {
|
93 | deleteCode: showInvisibles(deleteText),
|
94 | insertCode: showInvisibles(insertText)
|
95 | },
|
96 | loc: { start, end },
|
97 | fix(fixer) {
|
98 | return fixer.replaceTextRange(range, insertText);
|
99 | }
|
100 | });
|
101 | }
|
102 |
|
103 | // ------------------------------------------------------------------------------
|
104 | // Module Definition
|
105 | // ------------------------------------------------------------------------------
|
106 |
|
107 | module.exports = {
|
108 | configs: {
|
109 | recommended: {
|
110 | extends: ['prettier'],
|
111 | plugins: ['prettier'],
|
112 | rules: {
|
113 | 'prettier/prettier': 'error'
|
114 | }
|
115 | }
|
116 | },
|
117 | rules: {
|
118 | prettier: {
|
119 | meta: {
|
120 | docs: {
|
121 | url: 'https://github.com/prettier/eslint-plugin-prettier#options'
|
122 | },
|
123 | fixable: 'code',
|
124 | schema: [
|
125 | // Prettier options:
|
126 | {
|
127 | type: 'object',
|
128 | properties: {},
|
129 | additionalProperties: true
|
130 | },
|
131 | {
|
132 | type: 'object',
|
133 | properties: {
|
134 | usePrettierrc: { type: 'boolean' },
|
135 | fileInfoOptions: {
|
136 | type: 'object',
|
137 | properties: {},
|
138 | additionalProperties: true
|
139 | }
|
140 | },
|
141 | additionalProperties: true
|
142 | }
|
143 | ]
|
144 | },
|
145 | create(context) {
|
146 | const usePrettierrc =
|
147 | !context.options[1] || context.options[1].usePrettierrc !== false;
|
148 | const eslintFileInfoOptions =
|
149 | (context.options[1] && context.options[1].fileInfoOptions) || {};
|
150 | const sourceCode = context.getSourceCode();
|
151 | const filepath = context.getFilename();
|
152 | const source = sourceCode.text;
|
153 |
|
154 | if (prettier && prettier.clearConfigCache) {
|
155 | prettier.clearConfigCache();
|
156 | }
|
157 |
|
158 | return {
|
159 | Program() {
|
160 | if (!prettier) {
|
161 | // Prettier is expensive to load, so only load it if needed.
|
162 | prettier = require('prettier');
|
163 | }
|
164 |
|
165 | const eslintPrettierOptions = context.options[0] || {};
|
166 |
|
167 | const prettierRcOptions = usePrettierrc
|
168 | ? prettier.resolveConfig.sync(filepath, {
|
169 | editorconfig: true
|
170 | })
|
171 | : null;
|
172 |
|
173 | const prettierFileInfo = prettier.getFileInfo.sync(
|
174 | filepath,
|
175 | Object.assign(
|
176 | {},
|
177 | { ignorePath: '.prettierignore' },
|
178 | eslintFileInfoOptions
|
179 | )
|
180 | );
|
181 |
|
182 | // Skip if file is ignored using a .prettierignore file
|
183 | if (prettierFileInfo.ignored) {
|
184 | return;
|
185 | }
|
186 |
|
187 | const initialOptions = {};
|
188 |
|
189 | // ESLint suppports processors that let you extract and lint JS
|
190 | // fragments within a non-JS language. In the cases where prettier
|
191 | // supports the same language as a processor, we want to process
|
192 | // the provided source code as javascript (as ESLint provides the
|
193 | // rules with fragments of JS) instead of guessing the parser
|
194 | // based off the filename. Otherwise, for instance, on a .md file we
|
195 | // end up trying to run prettier over a fragment of JS using the
|
196 | // markdown parser, which throws an error.
|
197 | // If we can't infer the parser from from the filename, either
|
198 | // because no filename was provided or because there is no parser
|
199 | // found for the filename, use javascript.
|
200 | // This is added to the options first, so that
|
201 | // prettierRcOptions and eslintPrettierOptions can still override
|
202 | // the parser.
|
203 | //
|
204 | // `parserBlocklist` should contain the list of prettier parser
|
205 | // names for file types where:
|
206 | // * Prettier supports parsing the file type
|
207 | // * There is an ESLint processor that extracts JavaScript snippets
|
208 | // from the file type.
|
209 | const parserBlocklist = [null, 'graphql', 'markdown', 'html'];
|
210 | if (
|
211 | parserBlocklist.indexOf(prettierFileInfo.inferredParser) !== -1
|
212 | ) {
|
213 | // Prettier v1.16.0 renamed the `babylon` parser to `babel`
|
214 | // Use the modern name if available
|
215 | const supportBabelParser = prettier
|
216 | .getSupportInfo()
|
217 | .languages.some(language => language.parsers.includes('babel'));
|
218 |
|
219 | initialOptions.parser = supportBabelParser ? 'babel' : 'babylon';
|
220 | }
|
221 |
|
222 | const prettierOptions = Object.assign(
|
223 | {},
|
224 | initialOptions,
|
225 | prettierRcOptions,
|
226 | eslintPrettierOptions,
|
227 | { filepath }
|
228 | );
|
229 |
|
230 | // prettier.format() may throw a SyntaxError if it cannot parse the
|
231 | // source code it is given. Ususally for JS files this isn't a
|
232 | // problem as ESLint will report invalid syntax before trying to
|
233 | // pass it to the prettier plugin. However this might be a problem
|
234 | // for non-JS languages that are handled by a plugin. Notably Vue
|
235 | // files throw an error if they contain unclosed elements, such as
|
236 | // `<template><div></template>. In this case report an error at the
|
237 | // point at which parsing failed.
|
238 | let prettierSource;
|
239 | try {
|
240 | prettierSource = prettier.format(source, prettierOptions);
|
241 | } catch (err) {
|
242 | if (!(err instanceof SyntaxError)) {
|
243 | throw err;
|
244 | }
|
245 |
|
246 | let message = 'Parsing error: ' + err.message;
|
247 |
|
248 | // Prettier's message contains a codeframe style preview of the
|
249 | // invalid code and the line/column at which the error occured.
|
250 | // ESLint shows those pieces of information elsewhere already so
|
251 | // remove them from the message
|
252 | if (err.codeFrame) {
|
253 | message = message.replace(`\n${err.codeFrame}`, '');
|
254 | }
|
255 | if (err.loc) {
|
256 | message = message.replace(/ \(\d+:\d+\)$/, '');
|
257 | }
|
258 |
|
259 | context.report({ message, loc: err.loc });
|
260 |
|
261 | return;
|
262 | }
|
263 |
|
264 | if (source !== prettierSource) {
|
265 | const differences = generateDifferences(source, prettierSource);
|
266 |
|
267 | differences.forEach(difference => {
|
268 | switch (difference.operation) {
|
269 | case INSERT:
|
270 | reportInsert(
|
271 | context,
|
272 | difference.offset,
|
273 | difference.insertText
|
274 | );
|
275 | break;
|
276 | case DELETE:
|
277 | reportDelete(
|
278 | context,
|
279 | difference.offset,
|
280 | difference.deleteText
|
281 | );
|
282 | break;
|
283 | case REPLACE:
|
284 | reportReplace(
|
285 | context,
|
286 | difference.offset,
|
287 | difference.deleteText,
|
288 | difference.insertText
|
289 | );
|
290 | break;
|
291 | }
|
292 | });
|
293 | }
|
294 | }
|
295 | };
|
296 | }
|
297 | }
|
298 | }
|
299 | };
|