report.cjs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. // NOTICE: This file is generated by Rollup. To modify it,
  2. // please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
  3. 'use strict';
  4. const validateTypes = require('./validateTypes.cjs');
  5. const constants = require('../constants.cjs');
  6. const addSemicolonForEditInfo = require('./addSemicolonForEditInfo.cjs');
  7. const appendRuleName = require('./appendRuleName.cjs');
  8. const emitWarning = require('./emitWarning.cjs');
  9. const narrowFixRange = require('./narrowFixRange.cjs');
  10. const rangesOverlap = require('./rangesOverlap.cjs');
  11. /** @import { Config, DisabledRangeObject, FixCallback, FixObject, Problem, Range, RuleMessage, StylelintPostcssResult, Utils, WarningOptions } from 'stylelint' */
  12. /** @import { Position as PostcssPosition, Node as PostcssNode } from 'postcss' */
  13. /**
  14. * Report a problem.
  15. *
  16. * This function accounts for `disabledRanges` attached to the result.
  17. * That is, if the reported problem is within a disabledRange,
  18. * it is ignored. Otherwise, it is attached to the result as a
  19. * postcss warning.
  20. *
  21. * It also accounts for the rule's severity.
  22. *
  23. * You *must* pass *either* a node or a line number.
  24. *
  25. * @type {Utils['report']}
  26. */
  27. function report(problem) {
  28. const { node, index, endIndex, line, start, end, result, ruleName, word, fix, ...rest } = problem;
  29. checkProblemRangeDeprecations(problem);
  30. const {
  31. disabledRanges,
  32. quiet,
  33. ruleSeverities,
  34. config: { defaultSeverity, ignoreDisables } = {},
  35. customMessages: { [ruleName]: message = rest.message },
  36. customUrls: { [ruleName]: customUrl },
  37. ruleMetadata: { [ruleName]: metadata },
  38. } = result.stylelint;
  39. const { messageArgs = [], severity = ruleSeverities[ruleName] } = rest;
  40. const ruleSeverity =
  41. (validateTypes.isFunction(severity) ? severity(...messageArgs) : severity) ?? defaultSeverity ?? constants.DEFAULT_SEVERITY;
  42. // In quiet mode, mere warnings are ignored
  43. if (quiet && ruleSeverity === constants.SEVERITY_WARNING) return;
  44. if ((validateTypes.isFunction(fix) || isFixObject(fix)) && metadata && !metadata.fixable) {
  45. throw new Error(
  46. `The "${ruleName}" rule requires "meta.fixable" to be truthy if the "fix" callback is being passed`,
  47. );
  48. }
  49. // If a line is not passed, use the node.rangeBy method to get the
  50. // line number that the complaint pertains to
  51. const startLine = line ?? node?.rangeBy({ index, endIndex }).start.line;
  52. if (!startLine) {
  53. throw new Error(
  54. `The "${ruleName}" rule failed to pass either a node or a line number to the \`report()\` function.`,
  55. );
  56. }
  57. if (isFixApplied({ ...problem, line: startLine })) return;
  58. if (isDisabledOnLine(ruleName, startLine, disabledRanges)) {
  59. // Collect disabled warnings
  60. // Used to report `needlessDisables` in subsequent processing.
  61. const disabledWarnings = (result.stylelint.disabledWarnings ||= []);
  62. disabledWarnings.push({
  63. rule: ruleName,
  64. line: startLine,
  65. });
  66. if (!ignoreDisables) return;
  67. }
  68. if (!result.stylelint.stylelintError && ruleSeverity === constants.SEVERITY_ERROR) {
  69. result.stylelint.stylelintError = true;
  70. }
  71. if (!result.stylelint.stylelintWarning && ruleSeverity === constants.SEVERITY_WARNING) {
  72. result.stylelint.stylelintWarning = true;
  73. }
  74. /** @type {WarningOptions} */
  75. const warningProperties = {
  76. severity: ruleSeverity,
  77. rule: ruleName,
  78. };
  79. if (node) {
  80. warningProperties.node = node;
  81. }
  82. if (start) {
  83. warningProperties.start = start;
  84. } else if (validateTypes.isNumber(index)) {
  85. warningProperties.index = index;
  86. }
  87. if (end) {
  88. warningProperties.end = end;
  89. } else if (validateTypes.isNumber(endIndex)) {
  90. warningProperties.endIndex = endIndex;
  91. }
  92. if (word) {
  93. warningProperties.word = word;
  94. }
  95. if (customUrl) {
  96. warningProperties.url = customUrl;
  97. }
  98. warningProperties.fix = computeEditInfo({ ...problem, line: startLine });
  99. const warningMessage = buildWarningMessage(message, messageArgs, ruleName);
  100. result.warn(warningMessage, warningProperties);
  101. }
  102. /**
  103. * @param {Problem} problem
  104. */
  105. function checkProblemRangeDeprecations(problem) {
  106. if (problem.result.stylelint.quietDeprecationWarnings) return;
  107. if (!problem.node) {
  108. emitWarning.emitDeprecationWarning(
  109. `Omitting the \`node\` argument in the \`utils.report()\` function is deprecated ("${problem.ruleName}").`,
  110. 'REPORT_AMBIGUOUS_POSITION',
  111. `Please pass a \`node\` argument in the \`utils.report()\` function of "${problem.ruleName}".`,
  112. );
  113. }
  114. if (!validateTypes.isRange(problem) && ('start' in problem || 'end' in problem)) {
  115. emitWarning.emitDeprecationWarning(
  116. `Partial position information in the \`utils.report()\` function is deprecated ("${problem.ruleName}").`,
  117. 'REPORT_AMBIGUOUS_POSITION',
  118. `Please pass both a valid \`start\` and \`end\` argument in the \`utils.report()\` function of "${problem.ruleName}".`,
  119. );
  120. }
  121. if (!hasIndices(problem) && ('index' in problem || 'endIndex' in problem)) {
  122. emitWarning.emitDeprecationWarning(
  123. `Partial position information in the \`utils.report()\` function is deprecated ("${problem.ruleName}").`,
  124. 'REPORT_AMBIGUOUS_POSITION',
  125. `Please pass both \`index\` and \`endIndex\` as arguments in the \`utils.report()\` function of "${problem.ruleName}".`,
  126. );
  127. }
  128. if ('line' in problem) {
  129. emitWarning.emitDeprecationWarning(
  130. `Providing the \`line\` argument in the \`utils.report()\` function is deprecated ("${problem.ruleName}").`,
  131. 'REPORT_AMBIGUOUS_POSITION',
  132. `Please pass both \`index\` and \`endIndex\` as arguments in the \`utils.report()\` function of "${problem.ruleName}" instead.`,
  133. );
  134. }
  135. }
  136. /**
  137. * @param {RuleMessage} message
  138. * @param {NonNullable<Problem['messageArgs']>} messageArgs
  139. * @param {string} ruleName
  140. * @returns {string}
  141. */
  142. function buildWarningMessage(message, messageArgs, ruleName) {
  143. return appendRuleName(
  144. validateTypes.isString(message) ? printfLike(message, ...messageArgs) : message(...messageArgs),
  145. ruleName,
  146. );
  147. }
  148. /**
  149. * @param {string} format
  150. * @param {Array<unknown>} args
  151. * @returns {string}
  152. */
  153. function printfLike(format, ...args) {
  154. return args.reduce((/** @type {string} */ result, arg) => {
  155. return result.replace(/%[ds]/, String(arg));
  156. }, format);
  157. }
  158. /**
  159. * Check whether a rule is disabled for a given line
  160. * @param {string} ruleName
  161. * @param {number} startLine
  162. * @param {DisabledRangeObject} disabledRanges
  163. */
  164. function isDisabledOnLine(ruleName, startLine, disabledRanges) {
  165. const ranges = disabledRanges[ruleName] ?? disabledRanges[constants.RULE_NAME_ALL] ?? [];
  166. for (const range of ranges) {
  167. if (
  168. // If the problem is within a disabledRange,
  169. // and that disabledRange's rules include this one
  170. range.start <= startLine &&
  171. (range.end === undefined || range.end >= startLine) &&
  172. /** @todo populate rules in assignDisabledRanges util */
  173. (!range.rules || range.rules.includes(ruleName))
  174. ) {
  175. return true;
  176. }
  177. }
  178. return false;
  179. }
  180. /**
  181. * @param {Problem & { line: number }} problem
  182. * @returns {boolean}
  183. */
  184. function isFixApplied({ fix, line, result: { stylelint }, ruleName }) {
  185. if (!fix) return false;
  186. const { disabledRanges, config = {}, fixersData } = stylelint;
  187. if (!config.fix) return false;
  188. if (isFixDisabled(line, ruleName, config, disabledRanges)) return false;
  189. const apply = isFixObject(fix) ? fix.apply : fix;
  190. if (!validateTypes.isFunction(apply)) return false;
  191. apply();
  192. incrementFixCounter({ fixersData, ruleName });
  193. return true;
  194. }
  195. /**
  196. * @param {Problem & { line: number }} problem
  197. * @returns {{range: [number, number], text: string} | undefined}
  198. */
  199. function computeEditInfo({ fix, line, result, ruleName }) {
  200. if (!fix) return;
  201. const { disabledRanges, config = {}, rangesOfComputedEditInfos } = result.stylelint;
  202. if (!config.computeEditInfo || config.fix) return;
  203. if (isFixDisabled(line, ruleName, config, disabledRanges)) return;
  204. if (!isFixObject(fix) || !fix.apply || !fix.node) return;
  205. const { apply, node } = fix;
  206. if (!validateTypes.isNumber(node.source?.start?.offset) || !validateTypes.isNumber(node.source?.end?.offset)) return;
  207. /** @type [number, number] */
  208. const fixedNodeRange = [node.source.start.offset, node.source.end.offset];
  209. // When recording edit info we want to ensure that there is no overlap with any other fix.
  210. // We only record the first fix for each node.
  211. if (rangesOfComputedEditInfos.some((range) => rangesOverlap(range, fixedNodeRange))) {
  212. return;
  213. }
  214. // Apply the fix
  215. apply();
  216. let fixData = { range: fixedNodeRange, text: node.toString(result.opts?.syntax) };
  217. fixData = addSemicolonForEditInfo(node, fixData);
  218. // Compute the smallest range and text of the fix
  219. fixData = narrowFixRange(node, fixData);
  220. // Mark the fixed range as mutated
  221. rangesOfComputedEditInfos.push(fixData.range);
  222. return fixData;
  223. }
  224. /**
  225. * @param {number} line
  226. * @param {string} ruleName
  227. * @param {Config} config
  228. * @param {DisabledRangeObject} disabledRanges
  229. * @returns {boolean}
  230. */
  231. function isFixDisabled(line, ruleName, config, disabledRanges) {
  232. if (config.rules?.[ruleName][1]?.disableFix) return true;
  233. if (!config.ignoreDisables && isDisabledOnLine(ruleName, line, disabledRanges)) return true;
  234. return false;
  235. }
  236. /**
  237. * @param {object} o
  238. * @param {StylelintPostcssResult['fixersData']} o.fixersData
  239. * @param {string} o.ruleName
  240. */
  241. function incrementFixCounter({ fixersData, ruleName }) {
  242. fixersData[ruleName] ??= 0;
  243. fixersData[ruleName]++;
  244. }
  245. /**
  246. * @param {unknown} value
  247. * @returns {value is { index: number, endIndex: number }}
  248. */
  249. function hasIndices(value) {
  250. if (!validateTypes.isPlainObject(value)) return false;
  251. if (!validateTypes.isNumber(value.index)) return false;
  252. if (!validateTypes.isNumber(value.endIndex)) return false;
  253. return true;
  254. }
  255. /**
  256. * @param {unknown} value
  257. * @returns {value is FixObject}
  258. */
  259. function isFixObject(value) {
  260. if (!validateTypes.isPlainObject(value)) return false;
  261. if (!value.node) return false;
  262. if (!validateTypes.isFunction(value.apply)) return false;
  263. return true;
  264. }
  265. module.exports = report;