308 lines
7.4 KiB
JavaScript
308 lines
7.4 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
const _ = require('lodash');
|
||
|
|
|
||
|
|
const COMMAND_PREFIX = 'stylelint-';
|
||
|
|
const disableCommand = `${COMMAND_PREFIX}disable`;
|
||
|
|
const enableCommand = `${COMMAND_PREFIX}enable`;
|
||
|
|
const disableLineCommand = `${COMMAND_PREFIX}disable-line`;
|
||
|
|
const disableNextLineCommand = `${COMMAND_PREFIX}disable-next-line`;
|
||
|
|
const ALL_RULES = 'all';
|
||
|
|
|
||
|
|
/** @typedef {import('postcss').Comment} PostcssComment */
|
||
|
|
/** @typedef {import('postcss').Root} PostcssRoot */
|
||
|
|
/** @typedef {import('stylelint').PostcssResult} PostcssResult */
|
||
|
|
/** @typedef {import('stylelint').DisabledRangeObject} DisabledRangeObject */
|
||
|
|
/** @typedef {import('stylelint').DisabledRange} DisabledRange */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number} start
|
||
|
|
* @param {boolean} strictStart
|
||
|
|
* @param {number} [end]
|
||
|
|
* @param {boolean} [strictEnd]
|
||
|
|
* @returns {DisabledRange}
|
||
|
|
*/
|
||
|
|
function createDisableRange(start, strictStart, end, strictEnd) {
|
||
|
|
return {
|
||
|
|
start,
|
||
|
|
end: end || undefined,
|
||
|
|
strictStart,
|
||
|
|
strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Run it like a plugin ...
|
||
|
|
* @param {PostcssRoot} root
|
||
|
|
* @param {PostcssResult} result
|
||
|
|
* @returns {PostcssResult}
|
||
|
|
*/
|
||
|
|
module.exports = function(root, result) {
|
||
|
|
result.stylelint = result.stylelint || {
|
||
|
|
disabledRanges: {},
|
||
|
|
ruleSeverities: {},
|
||
|
|
customMessages: {},
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Most of the functions below work via side effects mutating this object
|
||
|
|
* @type {DisabledRangeObject}
|
||
|
|
*/
|
||
|
|
const disabledRanges = {
|
||
|
|
all: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
result.stylelint.disabledRanges = disabledRanges;
|
||
|
|
root.walkComments(checkComment);
|
||
|
|
|
||
|
|
return result;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {PostcssComment} comment
|
||
|
|
*/
|
||
|
|
function processDisableLineCommand(comment) {
|
||
|
|
if (comment.source && comment.source.start) {
|
||
|
|
const line = comment.source.start.line;
|
||
|
|
|
||
|
|
getCommandRules(disableLineCommand, comment.text).forEach((ruleName) => {
|
||
|
|
disableLine(line, ruleName, comment);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {PostcssComment} comment
|
||
|
|
*/
|
||
|
|
function processDisableNextLineCommand(comment) {
|
||
|
|
if (comment.source && comment.source.start) {
|
||
|
|
const line = comment.source.start.line;
|
||
|
|
|
||
|
|
getCommandRules(disableNextLineCommand, comment.text).forEach((ruleName) => {
|
||
|
|
disableLine(line + 1, ruleName, comment);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number} line
|
||
|
|
* @param {string} ruleName
|
||
|
|
* @param {PostcssComment} comment
|
||
|
|
*/
|
||
|
|
function disableLine(line, ruleName, comment) {
|
||
|
|
if (ruleIsDisabled(ALL_RULES)) {
|
||
|
|
throw comment.error('All rules have already been disabled', {
|
||
|
|
plugin: 'stylelint',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ruleName === ALL_RULES) {
|
||
|
|
Object.keys(disabledRanges).forEach((disabledRuleName) => {
|
||
|
|
if (ruleIsDisabled(disabledRuleName)) return;
|
||
|
|
|
||
|
|
const strict = disabledRuleName === ALL_RULES;
|
||
|
|
|
||
|
|
startDisabledRange(line, disabledRuleName, strict);
|
||
|
|
endDisabledRange(line, disabledRuleName, strict);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
if (ruleIsDisabled(ruleName)) {
|
||
|
|
throw comment.error(`"${ruleName}" has already been disabled`, {
|
||
|
|
plugin: 'stylelint',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
startDisabledRange(line, ruleName, true);
|
||
|
|
endDisabledRange(line, ruleName, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {PostcssComment} comment
|
||
|
|
*/
|
||
|
|
function processDisableCommand(comment) {
|
||
|
|
getCommandRules(disableCommand, comment.text).forEach((ruleToDisable) => {
|
||
|
|
const isAllRules = ruleToDisable === ALL_RULES;
|
||
|
|
|
||
|
|
if (ruleIsDisabled(ruleToDisable)) {
|
||
|
|
throw comment.error(
|
||
|
|
isAllRules
|
||
|
|
? 'All rules have already been disabled'
|
||
|
|
: `"${ruleToDisable}" has already been disabled`,
|
||
|
|
{
|
||
|
|
plugin: 'stylelint',
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (comment.source && comment.source.start) {
|
||
|
|
const line = comment.source.start.line;
|
||
|
|
|
||
|
|
if (isAllRules) {
|
||
|
|
Object.keys(disabledRanges).forEach((ruleName) => {
|
||
|
|
startDisabledRange(line, ruleName, ruleName === ALL_RULES);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
startDisabledRange(line, ruleToDisable, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {PostcssComment} comment
|
||
|
|
*/
|
||
|
|
function processEnableCommand(comment) {
|
||
|
|
getCommandRules(enableCommand, comment.text).forEach((ruleToEnable) => {
|
||
|
|
// TODO TYPES
|
||
|
|
// need fallback if endLine will be undefined
|
||
|
|
const endLine =
|
||
|
|
/** @type {number} */ (comment.source && comment.source.end && comment.source.end.line);
|
||
|
|
|
||
|
|
if (ruleToEnable === ALL_RULES) {
|
||
|
|
if (
|
||
|
|
Object.values(disabledRanges).every(
|
||
|
|
(ranges) => ranges.length === 0 || typeof ranges[ranges.length - 1].end === 'number',
|
||
|
|
)
|
||
|
|
) {
|
||
|
|
throw comment.error('No rules have been disabled', {
|
||
|
|
plugin: 'stylelint',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
Object.keys(disabledRanges).forEach((ruleName) => {
|
||
|
|
if (!_.get(_.last(disabledRanges[ruleName]), 'end')) {
|
||
|
|
endDisabledRange(endLine, ruleName, ruleName === ALL_RULES);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ruleIsDisabled(ALL_RULES) && disabledRanges[ruleToEnable] === undefined) {
|
||
|
|
// Get a starting point from the where all rules were disabled
|
||
|
|
if (!disabledRanges[ruleToEnable]) {
|
||
|
|
disabledRanges[ruleToEnable] = disabledRanges.all.map(({ start, end }) =>
|
||
|
|
createDisableRange(start, false, end, false),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
const range = _.last(disabledRanges[ALL_RULES]);
|
||
|
|
|
||
|
|
if (range) {
|
||
|
|
disabledRanges[ruleToEnable].push({ ...range });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
endDisabledRange(endLine, ruleToEnable, true);
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ruleIsDisabled(ruleToEnable)) {
|
||
|
|
endDisabledRange(endLine, ruleToEnable, true);
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
throw comment.error(`"${ruleToEnable}" has not been disabled`, {
|
||
|
|
plugin: 'stylelint',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {PostcssComment} comment
|
||
|
|
*/
|
||
|
|
function checkComment(comment) {
|
||
|
|
const text = comment.text;
|
||
|
|
|
||
|
|
// Ignore comments that are not relevant commands
|
||
|
|
|
||
|
|
if (text.indexOf(COMMAND_PREFIX) !== 0) {
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (text.startsWith(disableLineCommand)) {
|
||
|
|
processDisableLineCommand(comment);
|
||
|
|
} else if (text.startsWith(disableNextLineCommand)) {
|
||
|
|
processDisableNextLineCommand(comment);
|
||
|
|
} else if (text.startsWith(disableCommand)) {
|
||
|
|
processDisableCommand(comment);
|
||
|
|
} else if (text.startsWith(enableCommand)) {
|
||
|
|
processEnableCommand(comment);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {string} command
|
||
|
|
* @param {string} fullText
|
||
|
|
* @returns {string[]}
|
||
|
|
*/
|
||
|
|
function getCommandRules(command, fullText) {
|
||
|
|
const rules = fullText
|
||
|
|
.slice(command.length)
|
||
|
|
.split(',')
|
||
|
|
.filter(Boolean)
|
||
|
|
.map((r) => r.trim());
|
||
|
|
|
||
|
|
if (_.isEmpty(rules)) {
|
||
|
|
return [ALL_RULES];
|
||
|
|
}
|
||
|
|
|
||
|
|
return rules;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number} line
|
||
|
|
* @param {string} ruleName
|
||
|
|
* @param {boolean} strict
|
||
|
|
*/
|
||
|
|
function startDisabledRange(line, ruleName, strict) {
|
||
|
|
const rangeObj = createDisableRange(line, strict);
|
||
|
|
|
||
|
|
ensureRuleRanges(ruleName);
|
||
|
|
disabledRanges[ruleName].push(rangeObj);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number} line
|
||
|
|
* @param {string} ruleName
|
||
|
|
* @param {boolean} strict
|
||
|
|
*/
|
||
|
|
function endDisabledRange(line, ruleName, strict) {
|
||
|
|
const lastRangeForRule = _.last(disabledRanges[ruleName]);
|
||
|
|
|
||
|
|
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] = disabledRanges.all.map(({ start, end }) =>
|
||
|
|
createDisableRange(start, false, end, false),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {string} ruleName
|
||
|
|
* @returns {boolean}
|
||
|
|
*/
|
||
|
|
function ruleIsDisabled(ruleName) {
|
||
|
|
if (disabledRanges[ruleName] === undefined) return false;
|
||
|
|
|
||
|
|
if (_.last(disabledRanges[ruleName]) === undefined) return false;
|
||
|
|
|
||
|
|
if (_.get(_.last(disabledRanges[ruleName]), 'end') === undefined) return true;
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
};
|