UNPKG

stylelint

Version:

A mighty, modern CSS linter.

309 lines (259 loc) 7.63 kB
'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; } };