eslint-plugin-prettier.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * @file 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. /**
  22. * @type {import('prettier')}
  23. */
  24. let prettier;
  25. // ------------------------------------------------------------------------------
  26. // Rule Definition
  27. // ------------------------------------------------------------------------------
  28. /**
  29. * Reports a difference.
  30. *
  31. * @param {import('eslint').Rule.RuleContext} context - The ESLint rule context.
  32. * @param {import('prettier-linter-helpers').Difference} difference - The difference object.
  33. * @returns {void}
  34. */
  35. function reportDifference(context, difference) {
  36. const { operation, offset, deleteText = '', insertText = '' } = difference;
  37. const range = [offset, offset + deleteText.length];
  38. const [start, end] = range.map(index =>
  39. context.getSourceCode().getLocFromIndex(index),
  40. );
  41. context.report({
  42. messageId: operation,
  43. data: {
  44. deleteText: showInvisibles(deleteText),
  45. insertText: showInvisibles(insertText),
  46. },
  47. loc: { start, end },
  48. fix: fixer => fixer.replaceTextRange(range, insertText),
  49. });
  50. }
  51. // ------------------------------------------------------------------------------
  52. // Module Definition
  53. // ------------------------------------------------------------------------------
  54. module.exports = {
  55. configs: {
  56. recommended: {
  57. extends: ['prettier'],
  58. plugins: ['prettier'],
  59. rules: {
  60. 'prettier/prettier': 'error',
  61. 'arrow-body-style': 'off',
  62. 'prefer-arrow-callback': 'off',
  63. },
  64. },
  65. },
  66. rules: {
  67. prettier: {
  68. meta: {
  69. docs: {
  70. url: 'https://github.com/prettier/eslint-plugin-prettier#options',
  71. },
  72. type: 'layout',
  73. fixable: 'code',
  74. schema: [
  75. // Prettier options:
  76. {
  77. type: 'object',
  78. properties: {},
  79. additionalProperties: true,
  80. },
  81. {
  82. type: 'object',
  83. properties: {
  84. usePrettierrc: { type: 'boolean' },
  85. fileInfoOptions: {
  86. type: 'object',
  87. properties: {},
  88. additionalProperties: true,
  89. },
  90. },
  91. additionalProperties: true,
  92. },
  93. ],
  94. messages: {
  95. [INSERT]: 'Insert `{{ insertText }}`',
  96. [DELETE]: 'Delete `{{ deleteText }}`',
  97. [REPLACE]: 'Replace `{{ deleteText }}` with `{{ insertText }}`',
  98. },
  99. },
  100. create(context) {
  101. const usePrettierrc =
  102. !context.options[1] || context.options[1].usePrettierrc !== false;
  103. const eslintFileInfoOptions =
  104. (context.options[1] && context.options[1].fileInfoOptions) || {};
  105. const sourceCode = context.getSourceCode();
  106. const filepath = context.getFilename();
  107. // Processors that extract content from a file, such as the markdown
  108. // plugin extracting fenced code blocks may choose to specify virtual
  109. // file paths. If this is the case then we need to resolve prettier
  110. // config and file info using the on-disk path instead of the virtual
  111. // path.
  112. const onDiskFilepath = context.getPhysicalFilename();
  113. const source = sourceCode.text;
  114. return {
  115. // eslint-disable-next-line sonarjs/cognitive-complexity
  116. Program() {
  117. if (!prettier) {
  118. // Prettier is expensive to load, so only load it if needed.
  119. prettier = require('prettier');
  120. }
  121. const eslintPrettierOptions = context.options[0] || {};
  122. const prettierRcOptions = usePrettierrc
  123. ? prettier.resolveConfig.sync(onDiskFilepath, {
  124. editorconfig: true,
  125. })
  126. : null;
  127. const { ignored, inferredParser } = prettier.getFileInfo.sync(
  128. onDiskFilepath,
  129. {
  130. resolveConfig: false,
  131. withNodeModules: false,
  132. ignorePath: '.prettierignore',
  133. plugins: prettierRcOptions ? prettierRcOptions.plugins : null,
  134. ...eslintFileInfoOptions,
  135. },
  136. );
  137. // Skip if file is ignored using a .prettierignore file
  138. if (ignored) {
  139. return;
  140. }
  141. const initialOptions = {};
  142. // ESLint supports processors that let you extract and lint JS
  143. // fragments within a non-JS language. In the cases where prettier
  144. // supports the same language as a processor, we want to process
  145. // the provided source code as javascript (as ESLint provides the
  146. // rules with fragments of JS) instead of guessing the parser
  147. // based off the filename. Otherwise, for instance, on a .md file we
  148. // end up trying to run prettier over a fragment of JS using the
  149. // markdown parser, which throws an error.
  150. // Processors may set virtual filenames for these extracted blocks.
  151. // If they do so then we want to trust the file extension they
  152. // provide, and no override is needed.
  153. // If the processor does not set any virtual filename (signified by
  154. // `filepath` and `onDiskFilepath` being equal) AND we can't
  155. // infer the parser from the filename, either because no filename
  156. // was provided or because there is no parser found for the
  157. // filename, use javascript.
  158. // This is added to the options first, so that
  159. // prettierRcOptions and eslintPrettierOptions can still override
  160. // the parser.
  161. //
  162. // `parserBlocklist` should contain the list of prettier parser
  163. // names for file types where:
  164. // * Prettier supports parsing the file type
  165. // * There is an ESLint processor that extracts JavaScript snippets
  166. // from the file type.
  167. if (filepath === onDiskFilepath) {
  168. // The following list means the plugin process source into js content
  169. // but with same filename, so we need to change the parser to `babel`
  170. // by default.
  171. // Related ESLint plugins are:
  172. // 1. `eslint-plugin-graphql` (replacement: `@graphql-eslint/eslint-plugin`)
  173. // 2. `eslint-plugin-html`
  174. // 3. `eslint-plugin-markdown@1` (replacement: `eslint-plugin-markdown@2+`)
  175. // 4. `eslint-plugin-svelte3` (replacement: `eslint-plugin-svelte@2+`)
  176. const parserBlocklist = [null, 'markdown', 'html'];
  177. let inferParserToBabel = parserBlocklist.includes(inferredParser);
  178. switch (inferredParser) {
  179. // it could be processed by `@graphql-eslint/eslint-plugin` or `eslint-plugin-graphql`
  180. case 'graphql': {
  181. if (
  182. // for `eslint-plugin-graphql`, see https://github.com/apollographql/eslint-plugin-graphql/blob/master/src/index.js#L416
  183. source.startsWith('ESLintPluginGraphQLFile`')
  184. ) {
  185. inferParserToBabel = true;
  186. }
  187. break;
  188. }
  189. // it could be processed by `@ota-meshi/eslint-plugin-svelte`, `eslint-plugin-svelte` or `eslint-plugin-svelte3`
  190. case 'svelte': {
  191. // The `source` would be modified by `eslint-plugin-svelte3`
  192. if (!context.parserPath.includes('svelte-eslint-parser')) {
  193. // We do not support `eslint-plugin-svelte3`,
  194. // the users should run `prettier` on `.svelte` files manually
  195. return;
  196. }
  197. }
  198. }
  199. if (inferParserToBabel) {
  200. initialOptions.parser = 'babel';
  201. }
  202. } else {
  203. // Similar to https://github.com/prettier/stylelint-prettier/pull/22
  204. // In all of the following cases ESLint extracts a part of a file to
  205. // be formatted and there exists a prettier parser for the whole file.
  206. // If you're interested in prettier you'll want a fully formatted file so
  207. // you're about to run prettier over the whole file anyway.
  208. // Therefore running prettier over just the style section is wasteful, so
  209. // skip it.
  210. const parserBlocklist = [
  211. 'babel',
  212. 'babylon',
  213. 'flow',
  214. 'typescript',
  215. 'vue',
  216. 'markdown',
  217. 'html',
  218. 'mdx',
  219. 'angular',
  220. 'svelte',
  221. ];
  222. if (parserBlocklist.includes(inferredParser)) {
  223. return;
  224. }
  225. }
  226. const prettierOptions = {
  227. ...initialOptions,
  228. ...prettierRcOptions,
  229. ...eslintPrettierOptions,
  230. filepath,
  231. };
  232. // prettier.format() may throw a SyntaxError if it cannot parse the
  233. // source code it is given. Usually for JS files this isn't a
  234. // problem as ESLint will report invalid syntax before trying to
  235. // pass it to the prettier plugin. However this might be a problem
  236. // for non-JS languages that are handled by a plugin. Notably Vue
  237. // files throw an error if they contain unclosed elements, such as
  238. // `<template><div></template>. In this case report an error at the
  239. // point at which parsing failed.
  240. let prettierSource;
  241. try {
  242. prettierSource = prettier.format(source, prettierOptions);
  243. } catch (err) {
  244. if (!(err instanceof SyntaxError)) {
  245. throw err;
  246. }
  247. let message = 'Parsing error: ' + err.message;
  248. // Prettier's message contains a codeframe style preview of the
  249. // invalid code and the line/column at which the error occurred.
  250. // ESLint shows those pieces of information elsewhere already so
  251. // remove them from the message
  252. if (err.codeFrame) {
  253. message = message.replace(`\n${err.codeFrame}`, '');
  254. }
  255. if (err.loc) {
  256. message = message.replace(/ \(\d+:\d+\)$/, '');
  257. }
  258. context.report({ message, loc: err.loc });
  259. return;
  260. }
  261. if (source !== prettierSource) {
  262. const differences = generateDifferences(source, prettierSource);
  263. for (const difference of differences) {
  264. reportDifference(context, difference);
  265. }
  266. }
  267. },
  268. };
  269. },
  270. },
  271. },
  272. };