eslint-plugin-prettier.js 10 KB

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