stylelint
Version:
A mighty CSS linter that helps you avoid errors and enforce conventions.
480 lines (382 loc) • 12.7 kB
JavaScript
// NOTICE: This file is generated by Rollup. To modify it,
// please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
'use strict';
const constants = require('./constants.cjs');
const configurationComment = require('./utils/configurationComment.cjs');
const validateTypes = require('./utils/validateTypes.cjs');
const typeGuards = require('./utils/typeGuards.cjs');
const isStandardSyntaxComment = require('./utils/isStandardSyntaxComment.cjs');
const cssTokenizer = require('@csstools/css-tokenizer');
/** @import {Node as PostcssNode, Comment as PostcssComment, Document as PostcssDocument, Root as PostcssRoot, Source as PostcssSource} from 'postcss' */
/** @import {DisabledRange, DisabledRangeObject, PostcssResult} from 'stylelint' */
/** @typedef {Pick<PostcssSource, 'start' | 'end'>} Source */
/**
* @param {PostcssNode} node
* @param {number} start
* @param {boolean} strictStart
* @param {string|undefined} description
* @param {number} [end]
* @param {boolean} [strictEnd]
* @returns {DisabledRange}
*/
function createDisableRange(node, start, strictStart, description, end, strictEnd) {
return {
node,
start,
end: end || undefined,
strictStart,
strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
description,
};
}
/**
* Run it like a PostCSS plugin
* @param {PostcssRoot | PostcssDocument} root
* @param {PostcssResult} result
* @returns {PostcssResult}
*/
function assignDisabledRanges(root, result) {
/**
* Most of the functions below work via side effects mutating this object
* @type {DisabledRangeObject}
*/
const disabledRanges = result.stylelint.disabledRanges;
/** @type {DisabledRange[]} */
const disabledRangesAll = [];
disabledRanges[constants.RULE_NAME_ALL] = disabledRangesAll;
// Work around postcss/postcss-scss#109 by merging adjacent `//` comments
// into a single node before passing to `checkComment`.
/** @type {PostcssComment?} */
let inlineEnd;
const configurationComment$1 = result.stylelint.config?.configurationComment;
root.walk((node) => {
if (typeGuards.isComment(node)) {
if (inlineEnd) {
// Ignore comments already processed by grouping with a previous one.
if (inlineEnd === node) inlineEnd = null;
return;
}
const nextComment = node.next();
// If any of these conditions are not met, do not merge comments.
if (
!(
!isStandardSyntaxComment(node) &&
configurationComment.isConfigurationComment(node.text, configurationComment$1) &&
nextComment &&
typeGuards.isComment(nextComment) &&
(node.text.includes('--') || nextComment.text.startsWith('--'))
)
) {
checkComment(node, node.source, node.text);
return;
}
let lastLine = node.source?.end?.line ?? 0;
const fullComment = node.clone();
let current = nextComment;
while (
!isStandardSyntaxComment(current) &&
!configurationComment.isConfigurationComment(current.text, configurationComment$1)
) {
const currentLine = current.source?.end?.line ?? 0;
if (lastLine + 1 !== currentLine) break;
fullComment.text += `\n${current.text}`;
if (fullComment.source && current.source) {
fullComment.source.end = current.source.end;
}
inlineEnd = current;
const next = current.next();
if (!next || !typeGuards.isComment(next)) break;
current = next;
lastLine = currentLine;
}
checkComment(fullComment, fullComment.source, fullComment.text);
}
if (typeGuards.isRule(node)) {
let offset = 0;
const selector = node.raws?.selector?.raw;
checkCommentsInNode(node, selector, offset);
offset += selector?.length ?? node.selector.length;
const between = node.raws?.between;
checkCommentsInNode(node, between, offset);
}
if (typeGuards.isAtRule(node)) {
let offset = node.name.length + 1; // `@` + name
const afterName = node.raws?.afterName;
checkCommentsInNode(node, afterName, offset);
offset += afterName?.length ?? 0;
const params = node.raws?.params?.raw;
checkCommentsInNode(node, params, offset);
offset += params?.length ?? node.params.length;
const between = node.raws?.between;
checkCommentsInNode(node, between, offset);
}
if (typeGuards.isDeclaration(node)) {
let offset = node.prop.length;
const between = node.raws?.between;
checkCommentsInNode(node, between, offset);
offset += between?.length ?? 0;
const value = node.raws?.value?.raw;
checkCommentsInNode(node, value, offset);
}
});
return result;
/**
* @param {PostcssNode} node
* @param {Source} source
* @param {string} text
*/
function processDisableLineCommand(node, source, text) {
if (source.start) {
const line = source.start.line;
const description = getDescription(text);
for (const ruleName of getCommandRules(configurationComment.DISABLE_LINE_COMMAND, text)) {
disableLine(node, line, ruleName, description);
}
}
}
/**
* @param {PostcssNode} node
* @param {Source} source
* @param {string} text
*/
function processDisableNextLineCommand(node, source, text) {
if (source.end) {
const line = source.end.line;
const description = getDescription(text);
for (const ruleName of getCommandRules(configurationComment.DISABLE_NEXT_LINE_COMMAND, text)) {
disableLine(node, line + 1, ruleName, description);
}
}
}
/**
* @param {PostcssNode} node
* @param {number} line
* @param {string} ruleName
* @param {string|undefined} description
*/
function disableLine(node, line, ruleName, description) {
if (ruleIsDisabled(constants.RULE_NAME_ALL)) {
throw node.error('All rules have already been disabled', {
plugin: 'stylelint',
});
}
if (ruleName === constants.RULE_NAME_ALL) {
for (const disabledRuleName of Object.keys(disabledRanges)) {
if (ruleIsDisabled(disabledRuleName)) continue;
const strict = disabledRuleName === constants.RULE_NAME_ALL;
startDisabledRange(node, line, disabledRuleName, strict, description);
endDisabledRange(line, disabledRuleName, strict);
}
} else {
if (ruleIsDisabled(ruleName)) {
throw node.error(`"${ruleName}" has already been disabled`, {
plugin: 'stylelint',
});
}
startDisabledRange(node, line, ruleName, true, description);
endDisabledRange(line, ruleName, true);
}
}
/**
* @param {PostcssNode} node
* @param {Source} source
* @param {string} text
*/
function processDisableCommand(node, source, text) {
const description = getDescription(text);
for (const ruleToDisable of getCommandRules(configurationComment.DISABLE_COMMAND, text)) {
const isAllRules = ruleToDisable === constants.RULE_NAME_ALL;
if (ruleIsDisabled(ruleToDisable)) {
throw node.error(
isAllRules
? 'All rules have already been disabled'
: `"${ruleToDisable}" has already been disabled`,
{
plugin: 'stylelint',
},
);
}
if (source.start) {
const line = source.start.line;
if (isAllRules) {
for (const ruleName of Object.keys(disabledRanges)) {
startDisabledRange(node, line, ruleName, ruleName === constants.RULE_NAME_ALL, description);
}
} else {
startDisabledRange(node, line, ruleToDisable, true, description);
}
}
}
}
/**
* @param {PostcssNode} node
* @param {Source} source
* @param {string} text
*/
function processEnableCommand(node, source, text) {
for (const ruleToEnable of getCommandRules(configurationComment.ENABLE_COMMAND, text)) {
// need fallback if endLine will be undefined
const endLine = source.end?.line;
validateTypes.assertNumber(endLine);
if (ruleToEnable === constants.RULE_NAME_ALL) {
if (
Object.values(disabledRanges).every((ranges) => {
if (ranges.length === 0) return true;
const lastRange = ranges[ranges.length - 1];
return lastRange && typeof lastRange.end === 'number';
})
) {
throw node.error('No rules have been disabled', {
plugin: 'stylelint',
});
}
for (const [ruleName, ranges] of Object.entries(disabledRanges)) {
const lastRange = ranges[ranges.length - 1];
if (!lastRange || !lastRange.end) {
endDisabledRange(endLine, ruleName, ruleName === constants.RULE_NAME_ALL);
}
}
continue;
}
if (ruleIsDisabled(constants.RULE_NAME_ALL) && disabledRanges[ruleToEnable] === undefined) {
// Get a starting point from the where all rules were disabled
disabledRanges[ruleToEnable] = disabledRangesAll.map(({ start, end, description }) =>
createDisableRange(node, start, false, description, end, false),
);
endDisabledRange(endLine, ruleToEnable, true);
continue;
}
if (ruleIsDisabled(ruleToEnable)) {
endDisabledRange(endLine, ruleToEnable, true);
continue;
}
throw node.error(`"${ruleToEnable}" has not been disabled`, {
plugin: 'stylelint',
});
}
}
/**
* @param {PostcssNode} node
* @param {string | undefined} part
* @param {number} offset
*/
function checkCommentsInNode(node, part, offset) {
if (!(part && part.includes('/*') && part.includes('*/'))) return;
cssTokenizer.tokenize({ css: part }).forEach((token) => {
if (!cssTokenizer.isTokenComment(token)) return;
const [, text, start, end] = token;
const source = node.rangeBy({
index: start + offset,
endIndex: end + 1 + offset,
});
checkComment(node, source, text.slice(2, -2).trim());
});
}
/**
* @param {PostcssNode} node
* @param {Source | undefined} source
* @param {string} text
*/
function checkComment(node, source, text) {
if (!source) return;
// Ignore comments that are not relevant commands
if (!configurationComment.isConfigurationComment(text, configurationComment$1)) {
return;
}
switch (configurationComment.extractConfigurationComment(text, configurationComment$1)) {
case configurationComment.DISABLE_LINE_COMMAND:
processDisableLineCommand(node, source, text);
break;
case configurationComment.DISABLE_NEXT_LINE_COMMAND:
processDisableNextLineCommand(node, source, text);
break;
case configurationComment.DISABLE_COMMAND:
processDisableCommand(node, source, text);
break;
case configurationComment.ENABLE_COMMAND:
processEnableCommand(node, source, text);
break;
}
}
/**
* @param {string} command
* @param {string} fullText
* @returns {string[]}
*/
function getCommandRules(command, fullText) {
// Allow for description (f.e. /* stylelint-disable a, b -- Description */).
const fullCommand = configurationComment.getConfigurationComment(command, configurationComment$1);
const rulesText = fullText.slice(fullCommand.length).split(/\s-{2,}\s/u)[0];
validateTypes.assertString(rulesText);
const rules = rulesText
.trim()
.split(',')
.filter(Boolean)
.map((r) => r.trim());
if (rules.length === 0) {
return [constants.RULE_NAME_ALL];
}
return rules;
}
/**
* @param {string} fullText
* @returns {string|undefined}
*/
function getDescription(fullText) {
const descriptionStart = fullText.indexOf('--');
if (descriptionStart === -1) return;
return fullText.slice(descriptionStart + 2).trim();
}
/**
* @param {PostcssNode} node
* @param {number} line
* @param {string} ruleName
* @param {boolean} strict
* @param {string|undefined} description
*/
function startDisabledRange(node, line, ruleName, strict, description) {
const rangeObj = createDisableRange(node, line, strict, description);
ensureRuleRanges(ruleName);
const range = disabledRanges[ruleName];
validateTypes.assert(range);
range.push(rangeObj);
}
/**
* @param {number} line
* @param {string} ruleName
* @param {boolean} strict
*/
function endDisabledRange(line, ruleName, strict) {
const ranges = disabledRanges[ruleName];
const lastRangeForRule = ranges ? ranges[ranges.length - 1] : null;
if (!lastRangeForRule) {
return;
}
// Add an `end` prop to the last range of that rule
lastRangeForRule.end = line;
lastRangeForRule.strictEnd = strict;
}
/**
* @param {string} ruleName
*/
function ensureRuleRanges(ruleName) {
if (!disabledRanges[ruleName]) {
disabledRanges[ruleName] = disabledRangesAll.map(({ node, start, end, description }) =>
createDisableRange(node, start, false, description, end, false),
);
}
}
/**
* @param {string} ruleName
* @returns {boolean}
*/
function ruleIsDisabled(ruleName) {
const ranges = disabledRanges[ruleName];
if (!ranges) return false;
const lastRange = ranges[ranges.length - 1];
if (!lastRange) return false;
if (!lastRange.end) return true;
return false;
}
}
module.exports = assignDisabledRanges;