203 lines
5.3 KiB
JavaScript
203 lines
5.3 KiB
JavaScript
import { isFunction as isFn, isNumber, isString } from '../utils/validateTypes.mjs';
|
|
|
|
import {
|
|
DEFAULT_SEVERITY,
|
|
RULE_NAME_ALL,
|
|
SEVERITY_ERROR,
|
|
SEVERITY_WARNING,
|
|
} from '../constants.mjs';
|
|
|
|
/** @import { DisabledRangeObject, Problem, Range, RuleMessage, StylelintPostcssResult, Utils, WarningOptions } from 'stylelint' */
|
|
|
|
/**
|
|
* 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;
|
|
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) && 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 (isDisabled(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;
|
|
}
|
|
|
|
const warningMessage = buildWarningMessage(message, messageArgs);
|
|
|
|
result.warn(warningMessage, warningProperties);
|
|
}
|
|
|
|
/**
|
|
* @param {RuleMessage} message
|
|
* @param {NonNullable<Problem['messageArgs']>} messageArgs
|
|
* @returns {string}
|
|
*/
|
|
function buildWarningMessage(message, messageArgs) {
|
|
if (isString(message)) {
|
|
return printfLike(message, ...messageArgs);
|
|
}
|
|
|
|
return message(...messageArgs);
|
|
}
|
|
|
|
/**
|
|
* @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 isDisabled(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 */
|
|
function isFixApplied({ fix, line, result: { stylelint }, ruleName }) {
|
|
const { disabledRanges, config = {}, fixersData } = stylelint;
|
|
|
|
if (!isFn(fix)) {
|
|
addFixData({ fixersData, ruleName, fixed: false });
|
|
|
|
return false;
|
|
}
|
|
|
|
const shouldFix = Boolean(config.fix && !config.rules?.[ruleName][1]?.disableFix);
|
|
const mayFix =
|
|
shouldFix && (config.ignoreDisables || !isDisabled(ruleName, line, disabledRanges));
|
|
|
|
addFixData({ fixersData, ruleName, fixed: mayFix });
|
|
|
|
if (!mayFix) return false;
|
|
|
|
fix();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {object} o
|
|
* @param {StylelintPostcssResult['fixersData']} o.fixersData
|
|
* @param {string} o.ruleName
|
|
* @param {Range} [o.range]
|
|
* @param {boolean} o.fixed
|
|
* @todo stylelint/stylelint#7192
|
|
*/
|
|
function addFixData({ fixersData, ruleName, range, fixed }) {
|
|
const ruleFixers = (fixersData[ruleName] ??= []);
|
|
|
|
ruleFixers.push({ range, fixed });
|
|
}
|