1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | const diff = require('fast-diff');
|
13 | const docblock = require('jest-docblock');
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | const FB_PRETTIER_OPTIONS = {
|
21 | singleQuote: true,
|
22 | trailingComma: 'all',
|
23 | bracketSpacing: false,
|
24 | jsxBracketSameLine: true,
|
25 | parser: 'flow'
|
26 | };
|
27 |
|
28 | const LINE_ENDING_RE = /\r\n|[\r\n\u2028\u2029]/;
|
29 |
|
30 | const OPERATION_INSERT = 'insert';
|
31 | const OPERATION_DELETE = 'delete';
|
32 | const OPERATION_REPLACE = 'replace';
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | let prettier;
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 | function getLocFromIndex(context, index) {
|
52 |
|
53 |
|
54 | const sourceCode = context.getSourceCode();
|
55 | if (typeof sourceCode.getLocFromIndex === 'function') {
|
56 | return sourceCode.getLocFromIndex(index);
|
57 | }
|
58 | const text = sourceCode.getText();
|
59 | if (typeof index !== 'number') {
|
60 | throw new TypeError('Expected `index` to be a number.');
|
61 | }
|
62 | if (index < 0 || index > text.length) {
|
63 | throw new RangeError('Index out of range.');
|
64 | }
|
65 |
|
66 |
|
67 | const lineEndingPattern = /\r\n|[\r\n\u2028\u2029]/g;
|
68 | let offset = 0;
|
69 | let line = 0;
|
70 | let match;
|
71 | while ((match = lineEndingPattern.exec(text))) {
|
72 | const next = match.index + match[0].length;
|
73 | if (index < next) {
|
74 | break;
|
75 | }
|
76 | line++;
|
77 | offset = next;
|
78 | }
|
79 | return {
|
80 | line: line + 1,
|
81 | column: index - offset
|
82 | };
|
83 | }
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | function showInvisibles(str) {
|
91 | let ret = '';
|
92 | for (let i = 0; i < str.length; i++) {
|
93 | switch (str[i]) {
|
94 | case ' ':
|
95 | ret += '·';
|
96 | break;
|
97 | case '\n':
|
98 | ret += '⏎';
|
99 | break;
|
100 | case '\t':
|
101 | ret += '↹';
|
102 | break;
|
103 | default:
|
104 | ret += str[i];
|
105 | break;
|
106 | }
|
107 | }
|
108 | return ret;
|
109 | }
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 | function generateDifferences(source, prettierSource) {
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 | const results = diff(source, prettierSource);
|
133 | const differences = [];
|
134 |
|
135 | const batch = [];
|
136 | let offset = 0;
|
137 | while (results.length) {
|
138 | const result = results.shift();
|
139 | const op = result[0];
|
140 | const text = result[1];
|
141 | switch (op) {
|
142 | case diff.INSERT:
|
143 | case diff.DELETE:
|
144 | batch.push(result);
|
145 | break;
|
146 | case diff.EQUAL:
|
147 | if (results.length) {
|
148 | if (batch.length) {
|
149 | if (LINE_ENDING_RE.test(text)) {
|
150 | flush();
|
151 | offset += text.length;
|
152 | } else {
|
153 | batch.push(result);
|
154 | }
|
155 | } else {
|
156 | offset += text.length;
|
157 | }
|
158 | }
|
159 | break;
|
160 | default:
|
161 | throw new Error(`Unexpected fast-diff operation "${op}"`);
|
162 | }
|
163 | if (batch.length && !results.length) {
|
164 | flush();
|
165 | }
|
166 | }
|
167 |
|
168 | return differences;
|
169 |
|
170 | function flush() {
|
171 | let aheadDeleteText = '';
|
172 | let aheadInsertText = '';
|
173 | while (batch.length) {
|
174 | const next = batch.shift();
|
175 | const op = next[0];
|
176 | const text = next[1];
|
177 | switch (op) {
|
178 | case diff.INSERT:
|
179 | aheadInsertText += text;
|
180 | break;
|
181 | case diff.DELETE:
|
182 | aheadDeleteText += text;
|
183 | break;
|
184 | case diff.EQUAL:
|
185 | aheadDeleteText += text;
|
186 | aheadInsertText += text;
|
187 | break;
|
188 | }
|
189 | }
|
190 | if (aheadDeleteText && aheadInsertText) {
|
191 | differences.push({
|
192 | offset,
|
193 | operation: OPERATION_REPLACE,
|
194 | insertText: aheadInsertText,
|
195 | deleteText: aheadDeleteText
|
196 | });
|
197 | } else if (!aheadDeleteText && aheadInsertText) {
|
198 | differences.push({
|
199 | offset,
|
200 | operation: OPERATION_INSERT,
|
201 | insertText: aheadInsertText
|
202 | });
|
203 | } else if (aheadDeleteText && !aheadInsertText) {
|
204 | differences.push({
|
205 | offset,
|
206 | operation: OPERATION_DELETE,
|
207 | deleteText: aheadDeleteText
|
208 | });
|
209 | }
|
210 | offset += aheadDeleteText.length;
|
211 | }
|
212 | }
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 | function reportInsert(context, offset, text) {
|
226 | const pos = getLocFromIndex(context, offset);
|
227 | const range = [offset, offset];
|
228 | context.report({
|
229 | message: 'Insert `{{ code }}`',
|
230 | data: { code: showInvisibles(text) },
|
231 | loc: { start: pos, end: pos },
|
232 | fix(fixer) {
|
233 | return fixer.insertTextAfterRange(range, text);
|
234 | }
|
235 | });
|
236 | }
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 | function reportDelete(context, offset, text) {
|
246 | const start = getLocFromIndex(context, offset);
|
247 | const end = getLocFromIndex(context, offset + text.length);
|
248 | const range = [offset, offset + text.length];
|
249 | context.report({
|
250 | message: 'Delete `{{ code }}`',
|
251 | data: { code: showInvisibles(text) },
|
252 | loc: { start, end },
|
253 | fix(fixer) {
|
254 | return fixer.removeRange(range);
|
255 | }
|
256 | });
|
257 | }
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | function reportReplace(context, offset, deleteText, insertText) {
|
269 | const start = getLocFromIndex(context, offset);
|
270 | const end = getLocFromIndex(context, offset + deleteText.length);
|
271 | const range = [offset, offset + deleteText.length];
|
272 | context.report({
|
273 | message: 'Replace `{{ deleteCode }}` with `{{ insertCode }}`',
|
274 | data: {
|
275 | deleteCode: showInvisibles(deleteText),
|
276 | insertCode: showInvisibles(insertText)
|
277 | },
|
278 | loc: { start, end },
|
279 | fix(fixer) {
|
280 | return fixer.replaceTextRange(range, insertText);
|
281 | }
|
282 | });
|
283 | }
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 | module.exports = {
|
290 | showInvisibles,
|
291 | generateDifferences,
|
292 | configs: {
|
293 | recommended: {
|
294 | extends: ['prettier'],
|
295 | plugins: ['prettier'],
|
296 | rules: {
|
297 | 'prettier/prettier': 'error'
|
298 | }
|
299 | }
|
300 | },
|
301 | rules: {
|
302 | prettier: {
|
303 | meta: {
|
304 | fixable: 'code',
|
305 | schema: [
|
306 |
|
307 | {
|
308 | anyOf: [
|
309 | { enum: [null, 'fb'] },
|
310 | { type: 'object', properties: {}, additionalProperties: true }
|
311 | ]
|
312 | },
|
313 |
|
314 | { type: 'string', pattern: '^@\\w+$' }
|
315 | ]
|
316 | },
|
317 | create(context) {
|
318 | const pragma = context.options[1]
|
319 | ? context.options[1].slice(1)
|
320 | : null;
|
321 |
|
322 | const sourceCode = context.getSourceCode();
|
323 | const source = sourceCode.text;
|
324 |
|
325 |
|
326 |
|
327 | if (pragma) {
|
328 |
|
329 |
|
330 | const hasShebang = source.startsWith('#!');
|
331 | const allComments = sourceCode.getAllComments();
|
332 | const firstComment = hasShebang ? allComments[1] : allComments[0];
|
333 | if (
|
334 | !(
|
335 | firstComment &&
|
336 | firstComment.type === 'Block' &&
|
337 | firstComment.loc.start.line === (hasShebang ? 2 : 1) &&
|
338 | firstComment.loc.start.column === 0
|
339 | )
|
340 | ) {
|
341 | return {};
|
342 | }
|
343 | const parsed = docblock.parse(firstComment.value);
|
344 | if (parsed[pragma] !== '') {
|
345 | return {};
|
346 | }
|
347 | }
|
348 |
|
349 | if (prettier && prettier.clearConfigCache) {
|
350 | prettier.clearConfigCache();
|
351 | }
|
352 |
|
353 | return {
|
354 | Program() {
|
355 | if (!prettier) {
|
356 |
|
357 | prettier = require('prettier');
|
358 | }
|
359 |
|
360 | const eslintPrettierOptions =
|
361 | context.options[0] === 'fb'
|
362 | ? FB_PRETTIER_OPTIONS
|
363 | : context.options[0];
|
364 | const prettierRcOptions =
|
365 | prettier.resolveConfig && prettier.resolveConfig.sync
|
366 | ? prettier.resolveConfig.sync(context.getFilename())
|
367 | : null;
|
368 | const prettierOptions = Object.assign(
|
369 | {},
|
370 | prettierRcOptions,
|
371 | eslintPrettierOptions
|
372 | );
|
373 |
|
374 | const prettierSource = prettier.format(source, prettierOptions);
|
375 | if (source !== prettierSource) {
|
376 | const differences = generateDifferences(source, prettierSource);
|
377 |
|
378 | differences.forEach(difference => {
|
379 | switch (difference.operation) {
|
380 | case OPERATION_INSERT:
|
381 | reportInsert(
|
382 | context,
|
383 | difference.offset,
|
384 | difference.insertText
|
385 | );
|
386 | break;
|
387 | case OPERATION_DELETE:
|
388 | reportDelete(
|
389 | context,
|
390 | difference.offset,
|
391 | difference.deleteText
|
392 | );
|
393 | break;
|
394 | case OPERATION_REPLACE:
|
395 | reportReplace(
|
396 | context,
|
397 | difference.offset,
|
398 | difference.deleteText,
|
399 | difference.insertText
|
400 | );
|
401 | break;
|
402 | }
|
403 | });
|
404 | }
|
405 | }
|
406 | };
|
407 | }
|
408 | }
|
409 | }
|
410 | };
|