| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- import {
- isFunction as isFn,
- isNumber,
- isPlainObject,
- isRange,
- isString,
- } from '../utils/validateTypes.mjs';
- import {
- DEFAULT_SEVERITY,
- RULE_NAME_ALL,
- SEVERITY_ERROR,
- SEVERITY_WARNING,
- } from '../constants.mjs';
- import addSemicolonForEditInfo from './addSemicolonForEditInfo.mjs';
- import appendRuleName from './appendRuleName.mjs';
- import { emitDeprecationWarning } from './emitWarning.mjs';
- import narrowFixRange from './narrowFixRange.mjs';
- import rangesOverlap from './rangesOverlap.mjs';
- /** @import { Config, DisabledRangeObject, FixCallback, FixObject, Problem, Range, RuleMessage, StylelintPostcssResult, Utils, WarningOptions } from 'stylelint' */
- /** @import { Position as PostcssPosition, Node as PostcssNode } from 'postcss' */
- /**
- * Report a problem.
- *
- * This function accounts for `disabledRanges` attached to the result.
- * That is, if the reported problem is within a disabledRange,
- * it is ignored. Otherwise, it is attached to the result as a
- * postcss warning.
- *
- * It also accounts for the rule's severity.
- *
- * You *must* pass *either* a node or a line number.
- *
- * @type {Utils['report']}
- */
- export default function report(problem) {
- const { node, index, endIndex, line, start, end, result, ruleName, word, fix, ...rest } = problem;
- checkProblemRangeDeprecations(problem);
- const {
- disabledRanges,
- quiet,
- ruleSeverities,
- config: { defaultSeverity, ignoreDisables } = {},
- customMessages: { [ruleName]: message = rest.message },
- customUrls: { [ruleName]: customUrl },
- ruleMetadata: { [ruleName]: metadata },
- } = result.stylelint;
- const { messageArgs = [], severity = ruleSeverities[ruleName] } = rest;
- const ruleSeverity =
- (isFn(severity) ? severity(...messageArgs) : severity) ?? defaultSeverity ?? DEFAULT_SEVERITY;
- // In quiet mode, mere warnings are ignored
- if (quiet && ruleSeverity === SEVERITY_WARNING) return;
- if ((isFn(fix) || isFixObject(fix)) && metadata && !metadata.fixable) {
- throw new Error(
- `The "${ruleName}" rule requires "meta.fixable" to be truthy if the "fix" callback is being passed`,
- );
- }
- // If a line is not passed, use the node.rangeBy method to get the
- // line number that the complaint pertains to
- const startLine = line ?? node?.rangeBy({ index, endIndex }).start.line;
- if (!startLine) {
- throw new Error(
- `The "${ruleName}" rule failed to pass either a node or a line number to the \`report()\` function.`,
- );
- }
- if (isFixApplied({ ...problem, line: startLine })) return;
- if (isDisabledOnLine(ruleName, startLine, disabledRanges)) {
- // Collect disabled warnings
- // Used to report `needlessDisables` in subsequent processing.
- const disabledWarnings = (result.stylelint.disabledWarnings ||= []);
- disabledWarnings.push({
- rule: ruleName,
- line: startLine,
- });
- if (!ignoreDisables) return;
- }
- if (!result.stylelint.stylelintError && ruleSeverity === SEVERITY_ERROR) {
- result.stylelint.stylelintError = true;
- }
- if (!result.stylelint.stylelintWarning && ruleSeverity === SEVERITY_WARNING) {
- result.stylelint.stylelintWarning = true;
- }
- /** @type {WarningOptions} */
- const warningProperties = {
- severity: ruleSeverity,
- rule: ruleName,
- };
- if (node) {
- warningProperties.node = node;
- }
- if (start) {
- warningProperties.start = start;
- } else if (isNumber(index)) {
- warningProperties.index = index;
- }
- if (end) {
- warningProperties.end = end;
- } else if (isNumber(endIndex)) {
- warningProperties.endIndex = endIndex;
- }
- if (word) {
- warningProperties.word = word;
- }
- if (customUrl) {
- warningProperties.url = customUrl;
- }
- warningProperties.fix = computeEditInfo({ ...problem, line: startLine });
- const warningMessage = buildWarningMessage(message, messageArgs, ruleName);
- result.warn(warningMessage, warningProperties);
- }
- /**
- * @param {Problem} problem
- */
- function checkProblemRangeDeprecations(problem) {
- if (problem.result.stylelint.quietDeprecationWarnings) return;
- if (!problem.node) {
- emitDeprecationWarning(
- `Omitting the \`node\` argument in the \`utils.report()\` function is deprecated ("${problem.ruleName}").`,
- 'REPORT_AMBIGUOUS_POSITION',
- `Please pass a \`node\` argument in the \`utils.report()\` function of "${problem.ruleName}".`,
- );
- }
- if (!isRange(problem) && ('start' in problem || 'end' in problem)) {
- emitDeprecationWarning(
- `Partial position information in the \`utils.report()\` function is deprecated ("${problem.ruleName}").`,
- 'REPORT_AMBIGUOUS_POSITION',
- `Please pass both a valid \`start\` and \`end\` argument in the \`utils.report()\` function of "${problem.ruleName}".`,
- );
- }
- if (!hasIndices(problem) && ('index' in problem || 'endIndex' in problem)) {
- emitDeprecationWarning(
- `Partial position information in the \`utils.report()\` function is deprecated ("${problem.ruleName}").`,
- 'REPORT_AMBIGUOUS_POSITION',
- `Please pass both \`index\` and \`endIndex\` as arguments in the \`utils.report()\` function of "${problem.ruleName}".`,
- );
- }
- if ('line' in problem) {
- emitDeprecationWarning(
- `Providing the \`line\` argument in the \`utils.report()\` function is deprecated ("${problem.ruleName}").`,
- 'REPORT_AMBIGUOUS_POSITION',
- `Please pass both \`index\` and \`endIndex\` as arguments in the \`utils.report()\` function of "${problem.ruleName}" instead.`,
- );
- }
- }
- /**
- * @param {RuleMessage} message
- * @param {NonNullable<Problem['messageArgs']>} messageArgs
- * @param {string} ruleName
- * @returns {string}
- */
- function buildWarningMessage(message, messageArgs, ruleName) {
- return appendRuleName(
- isString(message) ? printfLike(message, ...messageArgs) : message(...messageArgs),
- ruleName,
- );
- }
- /**
- * @param {string} format
- * @param {Array<unknown>} args
- * @returns {string}
- */
- function printfLike(format, ...args) {
- return args.reduce((/** @type {string} */ result, arg) => {
- return result.replace(/%[ds]/, String(arg));
- }, format);
- }
- /**
- * Check whether a rule is disabled for a given line
- * @param {string} ruleName
- * @param {number} startLine
- * @param {DisabledRangeObject} disabledRanges
- */
- function isDisabledOnLine(ruleName, startLine, disabledRanges) {
- const ranges = disabledRanges[ruleName] ?? disabledRanges[RULE_NAME_ALL] ?? [];
- for (const range of ranges) {
- if (
- // If the problem is within a disabledRange,
- // and that disabledRange's rules include this one
- range.start <= startLine &&
- (range.end === undefined || range.end >= startLine) &&
- /** @todo populate rules in assignDisabledRanges util */
- (!range.rules || range.rules.includes(ruleName))
- ) {
- return true;
- }
- }
- return false;
- }
- /**
- * @param {Problem & { line: number }} problem
- * @returns {boolean}
- */
- function isFixApplied({ fix, line, result: { stylelint }, ruleName }) {
- if (!fix) return false;
- const { disabledRanges, config = {}, fixersData } = stylelint;
- if (!config.fix) return false;
- if (isFixDisabled(line, ruleName, config, disabledRanges)) return false;
- const apply = isFixObject(fix) ? fix.apply : fix;
- if (!isFn(apply)) return false;
- apply();
- incrementFixCounter({ fixersData, ruleName });
- return true;
- }
- /**
- * @param {Problem & { line: number }} problem
- * @returns {{range: [number, number], text: string} | undefined}
- */
- function computeEditInfo({ fix, line, result, ruleName }) {
- if (!fix) return;
- const { disabledRanges, config = {}, rangesOfComputedEditInfos } = result.stylelint;
- if (!config.computeEditInfo || config.fix) return;
- if (isFixDisabled(line, ruleName, config, disabledRanges)) return;
- if (!isFixObject(fix) || !fix.apply || !fix.node) return;
- const { apply, node } = fix;
- if (!isNumber(node.source?.start?.offset) || !isNumber(node.source?.end?.offset)) return;
- /** @type [number, number] */
- const fixedNodeRange = [node.source.start.offset, node.source.end.offset];
- // When recording edit info we want to ensure that there is no overlap with any other fix.
- // We only record the first fix for each node.
- if (rangesOfComputedEditInfos.some((range) => rangesOverlap(range, fixedNodeRange))) {
- return;
- }
- // Apply the fix
- apply();
- let fixData = { range: fixedNodeRange, text: node.toString(result.opts?.syntax) };
- fixData = addSemicolonForEditInfo(node, fixData);
- // Compute the smallest range and text of the fix
- fixData = narrowFixRange(node, fixData);
- // Mark the fixed range as mutated
- rangesOfComputedEditInfos.push(fixData.range);
- return fixData;
- }
- /**
- * @param {number} line
- * @param {string} ruleName
- * @param {Config} config
- * @param {DisabledRangeObject} disabledRanges
- * @returns {boolean}
- */
- function isFixDisabled(line, ruleName, config, disabledRanges) {
- if (config.rules?.[ruleName][1]?.disableFix) return true;
- if (!config.ignoreDisables && isDisabledOnLine(ruleName, line, disabledRanges)) return true;
- return false;
- }
- /**
- * @param {object} o
- * @param {StylelintPostcssResult['fixersData']} o.fixersData
- * @param {string} o.ruleName
- */
- function incrementFixCounter({ fixersData, ruleName }) {
- fixersData[ruleName] ??= 0;
- fixersData[ruleName]++;
- }
- /**
- * @param {unknown} value
- * @returns {value is { index: number, endIndex: number }}
- */
- function hasIndices(value) {
- if (!isPlainObject(value)) return false;
- if (!isNumber(value.index)) return false;
- if (!isNumber(value.endIndex)) return false;
- return true;
- }
- /**
- * @param {unknown} value
- * @returns {value is FixObject}
- */
- function isFixObject(value) {
- if (!isPlainObject(value)) return false;
- if (!value.node) return false;
- if (!isFn(value.apply)) return false;
- return true;
- }
|