stylelint-prettier.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. const stylelint = require('stylelint');
  2. const {
  3. showInvisibles,
  4. generateDifferences,
  5. } = require('prettier-linter-helpers');
  6. const {INSERT, DELETE, REPLACE} = generateDifferences;
  7. let prettier;
  8. const ruleName = 'prettier/prettier';
  9. const messages = stylelint.utils.ruleMessages(ruleName, {
  10. insert: (code) => `Insert "${showInvisibles(code)}"`,
  11. delete: (code) => `Delete "${showInvisibles(code)}"`,
  12. replace: (deleteCode, insertCode) =>
  13. `Replace "${showInvisibles(deleteCode)}" with "${showInvisibles(
  14. insertCode
  15. )}"`,
  16. });
  17. module.exports = stylelint.createPlugin(
  18. ruleName,
  19. (expectation, options, context) => {
  20. return (root, result) => {
  21. const validOptions = stylelint.utils.validateOptions(result, ruleName, {
  22. actual: expectation,
  23. });
  24. if (!validOptions) {
  25. return;
  26. }
  27. // Stylelint can handle css-in-js, in which it formats object literals.
  28. // We don't want to run these extracts of JS through prettier
  29. if (root.source.lang === 'object-literal') {
  30. return;
  31. }
  32. const stylelintPrettierOptions = omitStylelintSpecificOptions(options);
  33. if (!prettier) {
  34. // Prettier is expensive to load, so only load it if needed.
  35. prettier = require('prettier');
  36. }
  37. // Default to '<input>' if a filepath was not provided.
  38. // This mimics eslint's behaviour
  39. const filepath = root.source.input.file || '<input>';
  40. const source = root.source.input.css;
  41. const prettierRcOptions = prettier.resolveConfig.sync(filepath, {
  42. editorconfig: true,
  43. });
  44. const prettierFileInfo = prettier.getFileInfo.sync(filepath, {
  45. resolveConfig: true,
  46. ignorePath: '.prettierignore',
  47. });
  48. // Skip if file is ignored using a .prettierignore file
  49. if (prettierFileInfo.ignored) {
  50. return;
  51. }
  52. const initialOptions = {};
  53. // If no filepath was provided then assume the CSS parser
  54. // This is added to the options first, so that
  55. // prettierRcOptions and stylelintPrettierOptions can still override
  56. // the parser.
  57. if (filepath == '<input>') {
  58. initialOptions.parser = 'css';
  59. }
  60. // Stylelint supports languages that may contain multiple types of style
  61. // languages, thus we can't rely on guessing the parser based off the
  62. // filename.
  63. // In all of the following cases stylelint extracts a part of a file to
  64. // be formatted and there exists a prettier parser for the whole file.
  65. // If you're interested in prettier you'll want a fully formatted file so
  66. // you're about to run prettier over the whole file anyway.
  67. // Therefore running prettier over just the style section is wasteful, so
  68. // skip it.
  69. const parserBlockList = [
  70. 'babel',
  71. 'flow',
  72. 'typescript',
  73. 'vue',
  74. 'markdown',
  75. 'html',
  76. 'angular', // .component.html files
  77. 'svelte',
  78. ];
  79. if (parserBlockList.indexOf(prettierFileInfo.inferredParser) !== -1) {
  80. return;
  81. }
  82. const prettierOptions = Object.assign(
  83. {},
  84. initialOptions,
  85. prettierRcOptions,
  86. stylelintPrettierOptions,
  87. {filepath}
  88. );
  89. try {
  90. prettierSource = prettier.format(source, prettierOptions);
  91. } catch (err) {
  92. if (!(err instanceof SyntaxError)) {
  93. throw err;
  94. }
  95. let message = 'Parsing error: ' + err.message;
  96. // Prettier's message contains a codeframe style preview of the
  97. // invalid code and the line/column at which the error occurred.
  98. // ESLint shows those pieces of information elsewhere already so
  99. // remove them from the message
  100. if (err.codeFrame) {
  101. message = message.replace(`\n${err.codeFrame}`, '');
  102. }
  103. if (err.loc) {
  104. message = message.replace(/ \(\d+:\d+\)$/, '');
  105. }
  106. stylelint.utils.report({
  107. ruleName,
  108. result,
  109. message,
  110. node: root,
  111. index: getIndexFromLoc(source, err.loc.start),
  112. });
  113. return;
  114. }
  115. // Everything is the same. Nothing to do here;
  116. if (source === prettierSource) {
  117. return;
  118. }
  119. // Otherwise let's generate some differences
  120. const differences = generateDifferences(source, prettierSource);
  121. const report = (message, index) => {
  122. return stylelint.utils.report({
  123. ruleName,
  124. result,
  125. message,
  126. node: root,
  127. index,
  128. });
  129. };
  130. if (context.fix) {
  131. // Fixes must be processed in reverse order, as an early delete shall
  132. // change the modification offsets for anything after it
  133. const rawData = differences.reverse().reduce((rawData, difference) => {
  134. let insertText = '';
  135. let deleteText = '';
  136. switch (difference.operation) {
  137. case INSERT:
  138. insertText = difference.insertText;
  139. break;
  140. case DELETE:
  141. deleteText = difference.deleteText;
  142. break;
  143. case REPLACE:
  144. insertText = difference.insertText;
  145. deleteText = difference.deleteText;
  146. break;
  147. }
  148. return (
  149. rawData.substring(0, difference.offset) +
  150. insertText +
  151. rawData.substring(difference.offset + deleteText.length)
  152. );
  153. }, root.source.input.css);
  154. // If root.source.syntax exists then it means stylelint had to use
  155. // postcss-syntax to guess the postcss parser that it should use based
  156. // upon the input filename.
  157. // In that case we want to use the parser that postcss-syntax picked.
  158. // Otherwise use the syntax parser that was provided in the options
  159. const syntax = root.source.syntax || result.opts.syntax;
  160. const newRoot = syntax.parse(rawData);
  161. // For reasons I don't really understand, when the original input does
  162. // not have a trailing newline, newRoot generates a trailing newline but
  163. // it does not get included in the output.
  164. // Cleaning the root raws (to remove any existing whitespace), then
  165. // adding the final new line into the root raws seems to fix this
  166. root.removeAll();
  167. root.cleanRaws();
  168. root.append(newRoot);
  169. // Use the EOL whitespace from the rawData, as it could be \n or \r\n
  170. const trailingWhitespace = rawData.match(/[\s\uFEFF\xA0]+$/);
  171. if (trailingWhitespace) {
  172. root.raws.after = trailingWhitespace[0];
  173. }
  174. return;
  175. }
  176. // Report in the the order the differences appear in the content
  177. differences.forEach((difference) => {
  178. switch (difference.operation) {
  179. case INSERT:
  180. report(messages.insert(difference.insertText), difference.offset);
  181. break;
  182. case DELETE:
  183. report(messages.delete(difference.deleteText), difference.offset);
  184. break;
  185. case REPLACE:
  186. report(
  187. messages.replace(difference.deleteText, difference.insertText),
  188. difference.offset
  189. );
  190. break;
  191. }
  192. });
  193. };
  194. }
  195. );
  196. function omitStylelintSpecificOptions(options) {
  197. const prettierOptions = Object.assign({}, options);
  198. delete prettierOptions.message;
  199. delete prettierOptions.severity;
  200. return prettierOptions;
  201. }
  202. function getIndexFromLoc(source, {line, column}) {
  203. function nthIndex(str, searchValue, n) {
  204. let i = -1;
  205. while (n-- && i++ < str.length) {
  206. i = str.indexOf(searchValue, i);
  207. if (i < 0) {
  208. break;
  209. }
  210. }
  211. return i;
  212. }
  213. if (line === 1) {
  214. return column - 1;
  215. }
  216. return nthIndex(source, '\n', line - 1) + column;
  217. }
  218. module.exports.ruleName = ruleName;
  219. module.exports.messages = messages;