eslint-plugin-prettier.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /**
  2. * @fileoverview Runs `prettier` as an ESLint rule.
  3. * @author Andres Suarez
  4. */
  5. 'use strict';
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const {
  10. showInvisibles,
  11. generateDifferences
  12. } = require('prettier-linter-helpers');
  13. // ------------------------------------------------------------------------------
  14. // Constants
  15. // ------------------------------------------------------------------------------
  16. const { INSERT, DELETE, REPLACE } = generateDifferences;
  17. // ------------------------------------------------------------------------------
  18. // Privates
  19. // ------------------------------------------------------------------------------
  20. // Lazily-loaded Prettier.
  21. let prettier;
  22. // ------------------------------------------------------------------------------
  23. // Rule Definition
  24. // ------------------------------------------------------------------------------
  25. /**
  26. * Reports an "Insert ..." issue where text must be inserted.
  27. * @param {RuleContext} context - The ESLint rule context.
  28. * @param {number} offset - The source offset where to insert text.
  29. * @param {string} text - The text to be inserted.
  30. * @returns {void}
  31. */
  32. function reportInsert(context, offset, text) {
  33. const pos = context.getSourceCode().getLocFromIndex(offset);
  34. const range = [offset, offset];
  35. context.report({
  36. message: 'Insert `{{ code }}`',
  37. data: { code: showInvisibles(text) },
  38. loc: { start: pos, end: pos },
  39. fix(fixer) {
  40. return fixer.insertTextAfterRange(range, text);
  41. }
  42. });
  43. }
  44. /**
  45. * Reports a "Delete ..." issue where text must be deleted.
  46. * @param {RuleContext} context - The ESLint rule context.
  47. * @param {number} offset - The source offset where to delete text.
  48. * @param {string} text - The text to be deleted.
  49. * @returns {void}
  50. */
  51. function reportDelete(context, offset, text) {
  52. const start = context.getSourceCode().getLocFromIndex(offset);
  53. const end = context.getSourceCode().getLocFromIndex(offset + text.length);
  54. const range = [offset, offset + text.length];
  55. context.report({
  56. message: 'Delete `{{ code }}`',
  57. data: { code: showInvisibles(text) },
  58. loc: { start, end },
  59. fix(fixer) {
  60. return fixer.removeRange(range);
  61. }
  62. });
  63. }
  64. /**
  65. * Reports a "Replace ... with ..." issue where text must be replaced.
  66. * @param {RuleContext} context - The ESLint rule context.
  67. * @param {number} offset - The source offset where to replace deleted text
  68. with inserted text.
  69. * @param {string} deleteText - The text to be deleted.
  70. * @param {string} insertText - The text to be inserted.
  71. * @returns {void}
  72. */
  73. function reportReplace(context, offset, deleteText, insertText) {
  74. const start = context.getSourceCode().getLocFromIndex(offset);
  75. const end = context
  76. .getSourceCode()
  77. .getLocFromIndex(offset + deleteText.length);
  78. const range = [offset, offset + deleteText.length];
  79. context.report({
  80. message: 'Replace `{{ deleteCode }}` with `{{ insertCode }}`',
  81. data: {
  82. deleteCode: showInvisibles(deleteText),
  83. insertCode: showInvisibles(insertText)
  84. },
  85. loc: { start, end },
  86. fix(fixer) {
  87. return fixer.replaceTextRange(range, insertText);
  88. }
  89. });
  90. }
  91. // ------------------------------------------------------------------------------
  92. // Module Definition
  93. // ------------------------------------------------------------------------------
  94. module.exports = {
  95. configs: {
  96. recommended: {
  97. extends: ['prettier'],
  98. plugins: ['prettier'],
  99. rules: {
  100. 'prettier/prettier': 'error'
  101. }
  102. }
  103. },
  104. rules: {
  105. prettier: {
  106. meta: {
  107. docs: {
  108. url: 'https://github.com/prettier/eslint-plugin-prettier#options'
  109. },
  110. type: 'layout',
  111. fixable: 'code',
  112. schema: [
  113. // Prettier options:
  114. {
  115. type: 'object',
  116. properties: {},
  117. additionalProperties: true
  118. },
  119. {
  120. type: 'object',
  121. properties: {
  122. usePrettierrc: { type: 'boolean' },
  123. fileInfoOptions: {
  124. type: 'object',
  125. properties: {},
  126. additionalProperties: true
  127. }
  128. },
  129. additionalProperties: true
  130. }
  131. ]
  132. },
  133. create(context) {
  134. const usePrettierrc =
  135. !context.options[1] || context.options[1].usePrettierrc !== false;
  136. const eslintFileInfoOptions =
  137. (context.options[1] && context.options[1].fileInfoOptions) || {};
  138. const sourceCode = context.getSourceCode();
  139. const filepath = context.getFilename();
  140. const source = sourceCode.text;
  141. // This allows long-running ESLint processes (e.g. vscode-eslint) to
  142. // pick up changes to .prettierrc without restarting the editor. This
  143. // will invalidate the prettier plugin cache on every file as well which
  144. // will make ESLint very slow, so it would probably be a good idea to
  145. // find a better way to do this.
  146. if (usePrettierrc && prettier && prettier.clearConfigCache) {
  147. prettier.clearConfigCache();
  148. }
  149. return {
  150. Program() {
  151. if (!prettier) {
  152. // Prettier is expensive to load, so only load it if needed.
  153. prettier = require('prettier');
  154. }
  155. const eslintPrettierOptions = context.options[0] || {};
  156. const prettierRcOptions = usePrettierrc
  157. ? prettier.resolveConfig.sync(filepath, {
  158. editorconfig: true
  159. })
  160. : null;
  161. const prettierFileInfo = prettier.getFileInfo.sync(
  162. filepath,
  163. Object.assign(
  164. {},
  165. { resolveConfig: true, ignorePath: '.prettierignore' },
  166. eslintFileInfoOptions
  167. )
  168. );
  169. // Skip if file is ignored using a .prettierignore file
  170. if (prettierFileInfo.ignored) {
  171. return;
  172. }
  173. const initialOptions = {};
  174. // ESLint suppports processors that let you extract and lint JS
  175. // fragments within a non-JS language. In the cases where prettier
  176. // supports the same language as a processor, we want to process
  177. // the provided source code as javascript (as ESLint provides the
  178. // rules with fragments of JS) instead of guessing the parser
  179. // based off the filename. Otherwise, for instance, on a .md file we
  180. // end up trying to run prettier over a fragment of JS using the
  181. // markdown parser, which throws an error.
  182. // If we can't infer the parser from from the filename, either
  183. // because no filename was provided or because there is no parser
  184. // found for the filename, use javascript.
  185. // This is added to the options first, so that
  186. // prettierRcOptions and eslintPrettierOptions can still override
  187. // the parser.
  188. //
  189. // `parserBlocklist` should contain the list of prettier parser
  190. // names for file types where:
  191. // * Prettier supports parsing the file type
  192. // * There is an ESLint processor that extracts JavaScript snippets
  193. // from the file type.
  194. const parserBlocklist = [null, 'graphql', 'markdown', 'html'];
  195. if (
  196. parserBlocklist.indexOf(prettierFileInfo.inferredParser) !== -1
  197. ) {
  198. // Prettier v1.16.0 renamed the `babylon` parser to `babel`
  199. // Use the modern name if available
  200. const supportBabelParser = prettier
  201. .getSupportInfo()
  202. .languages.some(language => language.parsers.includes('babel'));
  203. initialOptions.parser = supportBabelParser ? 'babel' : 'babylon';
  204. }
  205. const prettierOptions = Object.assign(
  206. {},
  207. initialOptions,
  208. prettierRcOptions,
  209. eslintPrettierOptions,
  210. { filepath }
  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. let message = 'Parsing error: ' + err.message;
  228. // Prettier's message contains a codeframe style preview of the
  229. // invalid code and the line/column at which the error occured.
  230. // ESLint shows those pieces of information elsewhere already so
  231. // remove them from the message
  232. if (err.codeFrame) {
  233. message = message.replace(`\n${err.codeFrame}`, '');
  234. }
  235. if (err.loc) {
  236. message = message.replace(/ \(\d+:\d+\)$/, '');
  237. }
  238. context.report({ message, loc: err.loc });
  239. return;
  240. }
  241. if (source !== prettierSource) {
  242. const differences = generateDifferences(source, prettierSource);
  243. differences.forEach(difference => {
  244. switch (difference.operation) {
  245. case INSERT:
  246. reportInsert(
  247. context,
  248. difference.offset,
  249. difference.insertText
  250. );
  251. break;
  252. case DELETE:
  253. reportDelete(
  254. context,
  255. difference.offset,
  256. difference.deleteText
  257. );
  258. break;
  259. case REPLACE:
  260. reportReplace(
  261. context,
  262. difference.offset,
  263. difference.deleteText,
  264. difference.insertText
  265. );
  266. break;
  267. }
  268. });
  269. }
  270. }
  271. };
  272. }
  273. }
  274. }
  275. };