index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. 'use strict';
  2. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  5. const parseSelector = require('../../utils/parseSelector');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const validateOptions = require('../../utils/validateOptions');
  9. const valueParser = require('postcss-value-parser');
  10. const { isBoolean, assertString } = require('../../utils/validateTypes');
  11. const { isAtRule } = require('../../utils/typeGuards');
  12. const ruleName = 'string-quotes';
  13. const messages = ruleMessages(ruleName, {
  14. expected: (q) => `Expected ${q} quotes`,
  15. });
  16. const meta = {
  17. url: 'https://stylelint.io/user-guide/rules/string-quotes',
  18. fixable: true,
  19. deprecated: true,
  20. };
  21. const singleQuote = `'`;
  22. const doubleQuote = `"`;
  23. /** @type {import('stylelint').Rule} */
  24. const rule = (primary, secondaryOptions, context) => {
  25. const correctQuote = primary === 'single' ? singleQuote : doubleQuote;
  26. const erroneousQuote = primary === 'single' ? doubleQuote : singleQuote;
  27. return (root, result) => {
  28. const validOptions = validateOptions(
  29. result,
  30. ruleName,
  31. {
  32. actual: primary,
  33. possible: ['single', 'double'],
  34. },
  35. {
  36. actual: secondaryOptions,
  37. possible: {
  38. avoidEscape: [isBoolean],
  39. },
  40. optional: true,
  41. },
  42. );
  43. if (!validOptions) {
  44. return;
  45. }
  46. const avoidEscape =
  47. secondaryOptions && secondaryOptions.avoidEscape !== undefined
  48. ? secondaryOptions.avoidEscape
  49. : true;
  50. root.walk((node) => {
  51. switch (node.type) {
  52. case 'atrule':
  53. checkDeclOrAtRule(node, node.params, atRuleParamIndex);
  54. break;
  55. case 'decl':
  56. checkDeclOrAtRule(node, node.value, declarationValueIndex);
  57. break;
  58. case 'rule':
  59. checkRule(node);
  60. break;
  61. }
  62. });
  63. /**
  64. * @param {import('postcss').Rule} ruleNode
  65. * @returns {void}
  66. */
  67. function checkRule(ruleNode) {
  68. if (!isStandardSyntaxRule(ruleNode)) {
  69. return;
  70. }
  71. if (!ruleNode.selector.includes('[') || !ruleNode.selector.includes('=')) {
  72. return;
  73. }
  74. /** @type {number[]} */
  75. const fixPositions = [];
  76. parseSelector(ruleNode.selector, result, ruleNode, (selectorTree) => {
  77. let selectorFixed = false;
  78. selectorTree.walkAttributes((attributeNode) => {
  79. if (!attributeNode.quoted) {
  80. return;
  81. }
  82. if (attributeNode.quoteMark === correctQuote && avoidEscape) {
  83. assertString(attributeNode.value);
  84. const needsCorrectEscape = attributeNode.value.includes(correctQuote);
  85. const needsOtherEscape = attributeNode.value.includes(erroneousQuote);
  86. if (needsOtherEscape) {
  87. return;
  88. }
  89. if (needsCorrectEscape) {
  90. if (context.fix) {
  91. selectorFixed = true;
  92. attributeNode.quoteMark = erroneousQuote;
  93. } else {
  94. report({
  95. message: messages.expected(primary === 'single' ? 'double' : primary),
  96. node: ruleNode,
  97. index: attributeNode.sourceIndex + attributeNode.offsetOf('value'),
  98. result,
  99. ruleName,
  100. });
  101. }
  102. }
  103. }
  104. if (attributeNode.quoteMark === erroneousQuote) {
  105. if (avoidEscape) {
  106. assertString(attributeNode.value);
  107. const needsCorrectEscape = attributeNode.value.includes(correctQuote);
  108. const needsOtherEscape = attributeNode.value.includes(erroneousQuote);
  109. if (needsOtherEscape) {
  110. if (context.fix) {
  111. selectorFixed = true;
  112. attributeNode.quoteMark = correctQuote;
  113. } else {
  114. report({
  115. message: messages.expected(primary),
  116. node: ruleNode,
  117. index: attributeNode.sourceIndex + attributeNode.offsetOf('value'),
  118. result,
  119. ruleName,
  120. });
  121. }
  122. return;
  123. }
  124. if (needsCorrectEscape) {
  125. return;
  126. }
  127. }
  128. if (context.fix) {
  129. selectorFixed = true;
  130. attributeNode.quoteMark = correctQuote;
  131. } else {
  132. report({
  133. message: messages.expected(primary),
  134. node: ruleNode,
  135. index: attributeNode.sourceIndex + attributeNode.offsetOf('value'),
  136. result,
  137. ruleName,
  138. });
  139. }
  140. }
  141. });
  142. if (selectorFixed) {
  143. ruleNode.selector = selectorTree.toString();
  144. }
  145. });
  146. for (const fixIndex of fixPositions) {
  147. ruleNode.selector = replaceQuote(ruleNode.selector, fixIndex, correctQuote);
  148. }
  149. }
  150. /**
  151. * @template {import('postcss').AtRule | import('postcss').Declaration} T
  152. * @param {T} node
  153. * @param {string} value
  154. * @param {(node: T) => number} getIndex
  155. * @returns {void}
  156. */
  157. function checkDeclOrAtRule(node, value, getIndex) {
  158. /** @type {number[]} */
  159. const fixPositions = [];
  160. // Get out quickly if there are no erroneous quotes
  161. if (!value.includes(erroneousQuote)) {
  162. return;
  163. }
  164. if (isAtRule(node) && node.name === 'charset') {
  165. // allow @charset rules to have double quotes, in spite of the configuration
  166. // TODO: @charset should always use double-quotes, see https://github.com/stylelint/stylelint/issues/2788
  167. return;
  168. }
  169. valueParser(value).walk((valueNode) => {
  170. if (valueNode.type === 'string' && valueNode.quote === erroneousQuote) {
  171. const needsEscape = valueNode.value.includes(correctQuote);
  172. if (avoidEscape && needsEscape) {
  173. // don't consider this an error
  174. return;
  175. }
  176. const openIndex = valueNode.sourceIndex;
  177. // we currently don't fix escapes
  178. if (context.fix && !needsEscape) {
  179. const closeIndex = openIndex + valueNode.value.length + erroneousQuote.length;
  180. fixPositions.push(openIndex, closeIndex);
  181. } else {
  182. report({
  183. message: messages.expected(primary),
  184. node,
  185. index: getIndex(node) + openIndex,
  186. result,
  187. ruleName,
  188. });
  189. }
  190. }
  191. });
  192. for (const fixIndex of fixPositions) {
  193. if (isAtRule(node)) {
  194. node.params = replaceQuote(node.params, fixIndex, correctQuote);
  195. } else {
  196. node.value = replaceQuote(node.value, fixIndex, correctQuote);
  197. }
  198. }
  199. }
  200. };
  201. };
  202. /**
  203. * @param {string} string
  204. * @param {number} index
  205. * @param {string} replace
  206. * @returns {string}
  207. */
  208. function replaceQuote(string, index, replace) {
  209. return string.substring(0, index) + replace + string.substring(index + replace.length);
  210. }
  211. rule.ruleName = ruleName;
  212. rule.messages = messages;
  213. rule.meta = meta;
  214. module.exports = rule;