UNPKG

8.23 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 const prettierSource = prettier.format(source, prettierOptions);
213 if (source !== prettierSource) {
214 const differences = generateDifferences(source, prettierSource);
215
216 differences.forEach(difference => {
217 switch (difference.operation) {
218 case INSERT:
219 reportInsert(
220 context,
221 difference.offset,
222 difference.insertText
223 );
224 break;
225 case DELETE:
226 reportDelete(
227 context,
228 difference.offset,
229 difference.deleteText
230 );
231 break;
232 case REPLACE:
233 reportReplace(
234 context,
235 difference.offset,
236 difference.deleteText,
237 difference.insertText
238 );
239 break;
240 }
241 });
242 }
243 }
244 };
245 }
246 }
247 }
248};