stylelint
Version:
A mighty, modern CSS linter.
229 lines (184 loc) • 6.41 kB
JavaScript
/* @flow */
;
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';
/*:: type disableRange = {
start: number,
end?: number,
strictStart: boolean,
strictEnd?: boolean
}
*/
/*:: type disabledRangeObject = {
[ruleName: string]: Array<disableRange>
}*/
function createDisableRange(
start /*: number*/,
strictStart /*: boolean*/,
end /*: ?number*/,
strictEnd /*: ?boolean*/,
) /*: disableRange*/ {
return {
start,
end: end || undefined,
strictStart,
strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
};
}
// Run it like a plugin ...
module.exports = function(root /*: Object*/, result /*: Object*/) /*: postcss$result*/ {
result.stylelint = result.stylelint || {};
// Most of the functions below work via side effects mutating
// this object
const disabledRanges /*: disabledRangeObject*/ = {
all: [],
};
result.stylelint.disabledRanges = disabledRanges;
root.walkComments(checkComment);
return result;
function processDisableLineCommand(comment /*: postcss$comment*/) {
getCommandRules(disableLineCommand, comment.text).forEach((ruleName) => {
disableLine(comment.source.start.line, ruleName, comment);
});
}
function processDisableNextLineCommand(comment /*: postcss$comment*/) {
getCommandRules(disableNextLineCommand, comment.text).forEach((ruleName) => {
disableLine(comment.source.start.line + 1, ruleName, comment);
});
}
function disableLine(line /*: number*/, ruleName /*: string*/, comment /*: postcss$comment*/) {
if (ruleIsDisabled(ALL_RULES)) {
throw comment.error('All rules have already been disabled', {
plugin: 'stylelint',
});
}
if (ruleIsDisabled(ruleName)) {
throw comment.error(`"${ruleName}" has already been disabled`, {
plugin: 'stylelint',
});
}
if (ruleName === ALL_RULES) {
Object.keys(disabledRanges).forEach((disabledRuleName) => {
const strict = disabledRuleName === ALL_RULES;
startDisabledRange(line, disabledRuleName, strict);
endDisabledRange(line, disabledRuleName, strict);
});
} else {
startDisabledRange(line, ruleName, true);
endDisabledRange(line, ruleName, true);
}
}
function processDisableCommand(comment /*: postcss$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 (isAllRules) {
Object.keys(disabledRanges).forEach((ruleName) => {
startDisabledRange(comment.source.start.line, ruleName, ruleName === ALL_RULES);
});
} else {
startDisabledRange(comment.source.start.line, ruleToDisable, true);
}
});
}
function processEnableCommand(comment /*: postcss$comment*/) {
getCommandRules(enableCommand, comment.text).forEach((ruleToEnable) => {
if (ruleToEnable === ALL_RULES) {
if (_.values(disabledRanges).every((ranges) => _.isEmpty(ranges) || !!_.last(ranges.end))) {
throw comment.error('No rules have been disabled', {
plugin: 'stylelint',
});
}
Object.keys(disabledRanges).forEach((ruleName) => {
if (!_.get(_.last(disabledRanges[ruleName]), 'end')) {
endDisabledRange(comment.source.end.line, 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 {
disabledRanges[ruleToEnable].push(_.clone(_.last(disabledRanges[ALL_RULES])));
}
endDisabledRange(comment.source.end.line, ruleToEnable, true);
return;
}
if (ruleIsDisabled(ruleToEnable)) {
endDisabledRange(comment.source.end.line, ruleToEnable, true);
return;
}
throw comment.error(`"${ruleToEnable}" has not been disabled`, {
plugin: 'stylelint',
});
});
}
function checkComment(comment /*: postcss$comment*/) {
const text = comment.text;
// Ignore comments that are not relevant commands
if (text.indexOf(COMMAND_PREFIX) !== 0) {
return result;
}
if (text.indexOf(disableLineCommand) === 0) {
processDisableLineCommand(comment);
} else if (text.indexOf(disableNextLineCommand) === 0) {
processDisableNextLineCommand(comment);
} else if (text.indexOf(disableCommand) === 0) {
processDisableCommand(comment);
} else if (text.indexOf(enableCommand) === 0) {
processEnableCommand(comment);
}
}
function getCommandRules(command /*: string*/, fullText /*: string*/) /*: Array<string>*/ {
const rules = _.compact(fullText.slice(command.length).split(',')).map((r) => r.trim());
if (_.isEmpty(rules)) {
return [ALL_RULES];
}
return rules;
}
function startDisabledRange(line /*: number*/, ruleName /*: string*/, strict /*: boolean*/) {
const rangeObj = createDisableRange(line, strict);
ensureRuleRanges(ruleName);
disabledRanges[ruleName].push(rangeObj);
}
function endDisabledRange(line /*: number*/, ruleName /*: string*/, strict /*: boolean*/) {
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;
}
function ensureRuleRanges(ruleName /*: string*/) {
if (!disabledRanges[ruleName]) {
disabledRanges[ruleName] = disabledRanges.all.map(({ start, end }) =>
createDisableRange(start, false, end, false),
);
}
}
function ruleIsDisabled(ruleName /*: string*/) /*: boolean*/ {
if (disabledRanges[ruleName] === undefined) return false;
if (_.last(disabledRanges[ruleName]) === undefined) return false;
if (_.get(_.last(disabledRanges[ruleName]), 'end') === undefined) return true;
return false;
}
};