UNPKG

8.82 kBJavaScriptView Raw
1/**
2 * @file Runs `prettier` as an ESLint rule.
3 * @author Andres Suarez
4 */
5
6// @ts-check
7
8/**
9 * @typedef {import('eslint').AST.Range} Range
10 * @typedef {import('eslint').AST.SourceLocation} SourceLocation
11 * @typedef {import('eslint').ESLint.Plugin} Plugin
12 * @typedef {import('eslint').ESLint.ObjectMetaProperties} ObjectMetaProperties
13 * @typedef {import('prettier').FileInfoOptions} FileInfoOptions
14 * @typedef {import('prettier').Options} PrettierOptions
15 * @typedef {PrettierOptions & { onDiskFilepath: string, parserMeta?: ObjectMetaProperties['meta'], parserPath?: string, usePrettierrc?: boolean }} Options
16 */
17
18'use strict';
19
20// ------------------------------------------------------------------------------
21// Requirements
22// ------------------------------------------------------------------------------
23
24const {
25 showInvisibles,
26 generateDifferences,
27} = require('prettier-linter-helpers');
28const { name, version } = require('./package.json');
29
30// ------------------------------------------------------------------------------
31// Constants
32// ------------------------------------------------------------------------------
33
34const { INSERT, DELETE, REPLACE } = generateDifferences;
35
36// ------------------------------------------------------------------------------
37// Privates
38// ------------------------------------------------------------------------------
39
40// Lazily-loaded Prettier.
41/**
42 * @type {(source: string, options: Options, fileInfoOptions: FileInfoOptions) => string}
43 */
44let prettierFormat;
45
46// ------------------------------------------------------------------------------
47// Rule Definition
48// ------------------------------------------------------------------------------
49
50/**
51 * Reports a difference.
52 *
53 * @param {import('eslint').Rule.RuleContext} context - The ESLint rule context.
54 * @param {import('prettier-linter-helpers').Difference} difference - The difference object.
55 * @returns {void}
56 */
57function reportDifference(context, difference) {
58 const { operation, offset, deleteText = '', insertText = '' } = difference;
59 const range = /** @type {Range} */ ([offset, offset + deleteText.length]);
60 // `context.getSourceCode()` was deprecated in ESLint v8.40.0 and replaced
61 // with the `sourceCode` property.
62 // TODO: Only use property when our eslint peerDependency is >=8.40.0.
63 const [start, end] = range.map(index =>
64 (context.sourceCode ?? context.getSourceCode()).getLocFromIndex(index),
65 );
66
67 context.report({
68 messageId: operation,
69 data: {
70 deleteText: showInvisibles(deleteText),
71 insertText: showInvisibles(insertText),
72 },
73 loc: { start, end },
74 fix: fixer => fixer.replaceTextRange(range, insertText),
75 });
76}
77
78// ------------------------------------------------------------------------------
79// Module Definition
80// ------------------------------------------------------------------------------
81
82/**
83 * @type {Plugin}
84 */
85const eslintPluginPrettier = {
86 meta: { name, version },
87 configs: {
88 recommended: {
89 extends: ['prettier'],
90 plugins: ['prettier'],
91 rules: {
92 'prettier/prettier': 'error',
93 'arrow-body-style': 'off',
94 'prefer-arrow-callback': 'off',
95 },
96 },
97 },
98 rules: {
99 prettier: {
100 meta: {
101 docs: {
102 url: 'https://github.com/prettier/eslint-plugin-prettier#options',
103 },
104 type: 'layout',
105 fixable: 'code',
106 schema: [
107 // Prettier options:
108 {
109 type: 'object',
110 properties: {},
111 additionalProperties: true,
112 },
113 {
114 type: 'object',
115 properties: {
116 usePrettierrc: { type: 'boolean' },
117 fileInfoOptions: {
118 type: 'object',
119 properties: {},
120 additionalProperties: true,
121 },
122 },
123 additionalProperties: true,
124 },
125 ],
126 messages: {
127 [INSERT]: 'Insert `{{ insertText }}`',
128 [DELETE]: 'Delete `{{ deleteText }}`',
129 [REPLACE]: 'Replace `{{ deleteText }}` with `{{ insertText }}`',
130 },
131 },
132 create(context) {
133 const usePrettierrc =
134 !context.options[1] || context.options[1].usePrettierrc !== false;
135 /**
136 * @type {FileInfoOptions}
137 */
138 const fileInfoOptions =
139 (context.options[1] && context.options[1].fileInfoOptions) || {};
140
141 // `context.getSourceCode()` was deprecated in ESLint v8.40.0 and replaced
142 // with the `sourceCode` property.
143 // TODO: Only use property when our eslint peerDependency is >=8.40.0.
144 const sourceCode = context.sourceCode ?? context.getSourceCode();
145 // `context.getFilename()` was deprecated in ESLint v8.40.0 and replaced
146 // with the `filename` property.
147 // TODO: Only use property when our eslint peerDependency is >=8.40.0.
148 const filepath = context.filename ?? context.getFilename();
149
150 // Processors that extract content from a file, such as the markdown
151 // plugin extracting fenced code blocks may choose to specify virtual
152 // file paths. If this is the case then we need to resolve prettier
153 // config and file info using the on-disk path instead of the virtual
154 // path.
155 // `context.getPhysicalFilename()` was deprecated in ESLint v8.40.0 and replaced
156 // with the `physicalFilename` property.
157 // TODO: Only use property when our eslint peerDependency is >=8.40.0.
158 const onDiskFilepath =
159 context.physicalFilename ?? context.getPhysicalFilename();
160 const source = sourceCode.text;
161
162 return {
163 Program() {
164 if (!prettierFormat) {
165 // Prettier is expensive to load, so only load it if needed.
166 prettierFormat = require('synckit').createSyncFn(
167 require.resolve('./worker'),
168 );
169 }
170
171 /**
172 * @type {PrettierOptions}
173 */
174 const eslintPrettierOptions = context.options[0] || {};
175
176 const parser = context.languageOptions?.parser;
177
178 // prettier.format() may throw a SyntaxError if it cannot parse the
179 // source code it is given. Usually for JS files this isn't a
180 // problem as ESLint will report invalid syntax before trying to
181 // pass it to the prettier plugin. However this might be a problem
182 // for non-JS languages that are handled by a plugin. Notably Vue
183 // files throw an error if they contain unclosed elements, such as
184 // `<template><div></template>. In this case report an error at the
185 // point at which parsing failed.
186 /**
187 * @type {string}
188 */
189 let prettierSource;
190 try {
191 prettierSource = prettierFormat(
192 source,
193 {
194 ...eslintPrettierOptions,
195 filepath,
196 onDiskFilepath,
197 parserMeta:
198 parser &&
199 (parser.meta ?? {
200 name: parser.name,
201 version: parser.version,
202 }),
203 parserPath: context.parserPath,
204 usePrettierrc,
205 },
206 fileInfoOptions,
207 );
208 } catch (err) {
209 if (!(err instanceof SyntaxError)) {
210 throw err;
211 }
212
213 let message = 'Parsing error: ' + err.message;
214
215 const error =
216 /** @type {SyntaxError & {codeFrame: string; loc: SourceLocation}} */ (
217 err
218 );
219
220 // Prettier's message contains a codeframe style preview of the
221 // invalid code and the line/column at which the error occurred.
222 // ESLint shows those pieces of information elsewhere already so
223 // remove them from the message
224 if (error.codeFrame) {
225 message = message.replace(`\n${error.codeFrame}`, '');
226 }
227 if (error.loc) {
228 message = message.replace(/ \(\d+:\d+\)$/, '');
229 }
230
231 context.report({ message, loc: error.loc });
232
233 return;
234 }
235
236 if (prettierSource == null) {
237 return;
238 }
239
240 if (source !== prettierSource) {
241 const differences = generateDifferences(source, prettierSource);
242
243 for (const difference of differences) {
244 reportDifference(context, difference);
245 }
246 }
247 },
248 };
249 },
250 },
251 },
252};
253
254module.exports = eslintPluginPrettier;