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 | },
|
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 | };
|