UNPKG

9.66 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 },
136 additionalProperties: true
137 }
138 ]
139 },
140 create(context) {
141 const usePrettierrc =
142 !context.options[1] || context.options[1].usePrettierrc !== false;
143 const sourceCode = context.getSourceCode();
144 const filepath = context.getFilename();
145 const source = sourceCode.text;
146
147 if (prettier && prettier.clearConfigCache) {
148 prettier.clearConfigCache();
149 }
150
151 return {
152 Program() {
153 if (!prettier) {
154 // Prettier is expensive to load, so only load it if needed.
155 prettier = require('prettier');
156 }
157
158 const eslintPrettierOptions = context.options[0] || {};
159
160 const prettierRcOptions = usePrettierrc
161 ? prettier.resolveConfig.sync(filepath, {
162 editorconfig: true
163 })
164 : null;
165
166 const prettierFileInfo = prettier.getFileInfo.sync(filepath, {
167 ignorePath: '.prettierignore'
168 });
169
170 // Skip if file is ignored using a .prettierignore file
171 if (prettierFileInfo.ignored) {
172 return;
173 }
174
175 const initialOptions = {};
176
177 // ESLint suppports processors that let you extract and lint JS
178 // fragments within a non-JS language. In the cases where prettier
179 // supports the same language as a processor, we want to process
180 // the provided source code as javascript (as ESLint provides the
181 // rules with fragments of JS) instead of guessing the parser
182 // based off the filename. Otherwise, for instance, on a .md file we
183 // end up trying to run prettier over a fragment of JS using the
184 // markdown parser, which throws an error.
185 // If we can't infer the parser from from the filename, either
186 // because no filename was provided or because there is no parser
187 // found for the filename, use javascript.
188 // This is added to the options first, so that
189 // prettierRcOptions and eslintPrettierOptions can still override
190 // the parser.
191 //
192 // `parserBlocklist` should contain the list of prettier parser
193 // names for file types where:
194 // * Prettier supports parsing the file type
195 // * There is an ESLint processor that extracts JavaScript snippets
196 // from the file type.
197 const parserBlocklist = [null, 'graphql', 'markdown', 'html'];
198 if (
199 parserBlocklist.indexOf(prettierFileInfo.inferredParser) !== -1
200 ) {
201 initialOptions.parser = 'babylon';
202 }
203
204 const prettierOptions = Object.assign(
205 {},
206 initialOptions,
207 prettierRcOptions,
208 eslintPrettierOptions,
209 { filepath }
210 );
211
212 // prettier.format() may throw a SyntaxError if it cannot parse the
213 // source code it is given. Ususally for JS files this isn't a
214 // problem as ESLint will report invalid syntax before trying to
215 // pass it to the prettier plugin. However this might be a problem
216 // for non-JS languages that are handled by a plugin. Notably Vue
217 // files throw an error if they contain unclosed elements, such as
218 // `<template><div></template>. In this case report an error at the
219 // point at which parsing failed.
220 let prettierSource;
221 try {
222 prettierSource = prettier.format(source, prettierOptions);
223 } catch (err) {
224 if (!(err instanceof SyntaxError)) {
225 throw err;
226 }
227
228 let message = 'Parsing error: ' + err.message;
229
230 // Prettier's message contains a codeframe style preview of the
231 // invalid code and the line/column at which the error occured.
232 // ESLint shows those pieces of information elsewhere already so
233 // remove them from the message
234 if (err.codeFrame) {
235 message = message.replace(`\n${err.codeFrame}`, '');
236 }
237 if (err.loc) {
238 message = message.replace(/ \(\d+:\d+\)$/, '');
239 }
240
241 context.report({ message, loc: err.loc });
242
243 return;
244 }
245
246 if (source !== prettierSource) {
247 const differences = generateDifferences(source, prettierSource);
248
249 differences.forEach(difference => {
250 switch (difference.operation) {
251 case INSERT:
252 reportInsert(
253 context,
254 difference.offset,
255 difference.insertText
256 );
257 break;
258 case DELETE:
259 reportDelete(
260 context,
261 difference.offset,
262 difference.deleteText
263 );
264 break;
265 case REPLACE:
266 reportReplace(
267 context,
268 difference.offset,
269 difference.deleteText,
270 difference.insertText
271 );
272 break;
273 }
274 });
275 }
276 }
277 };
278 }
279 }
280 }
281};