UNPKG

10.4 kBJavaScriptView Raw
1/**
2 * @fileoverview Runs `prettier` as an ESLint rule.
3 * @author Andres Suarez
4 */
5
6'use strict';
7
8// ------------------------------------------------------------------------------
9// Requirements
10// ------------------------------------------------------------------------------
11
12const {
13 showInvisibles,
14 generateDifferences
15} = require('prettier-linter-helpers');
16
17// ------------------------------------------------------------------------------
18// Constants
19// ------------------------------------------------------------------------------
20
21const { INSERT, DELETE, REPLACE } = generateDifferences;
22
23// ------------------------------------------------------------------------------
24// Privates
25// ------------------------------------------------------------------------------
26
27// Lazily-loaded Prettier.
28let 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 */
41function 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 */
61function 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 */
84function 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
107module.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 { resolveConfig: true, 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};