| 'use strict'; |
| |
| const isStandardSyntaxComment = require('./utils/isStandardSyntaxComment'); |
| |
| 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 {PostcssComment} comment |
| * @param {number} start |
| * @param {boolean} strictStart |
| * @param {string|undefined} description |
| * @param {number} [end] |
| * @param {boolean} [strictEnd] |
| * @returns {DisabledRange} |
| */ |
| function createDisableRange(comment, start, strictStart, description, end, strictEnd) { |
| return { |
| comment, |
| start, |
| end: end || undefined, |
| strictStart, |
| strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined, |
| description, |
| }; |
| } |
| |
| /** |
| * Run it like a PostCSS plugin |
| * @param {PostcssRoot} root |
| * @param {PostcssResult} result |
| * @returns {PostcssResult} |
| */ |
| module.exports = function assignDisabledRanges(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; |
| |
| // Work around postcss/postcss-scss#109 by merging adjacent `//` comments |
| // into a single node before passing to `checkComment`. |
| |
| /** @type {PostcssComment?} */ |
| let inlineEnd; |
| |
| root.walkComments((comment) => { |
| if (inlineEnd) { |
| // Ignore comments already processed by grouping with a previous one. |
| if (inlineEnd === comment) inlineEnd = null; |
| |
| return; |
| } |
| |
| const nextComment = comment.next(); |
| |
| // If any of these conditions are not met, do not merge comments. |
| if ( |
| !( |
| !isStandardSyntaxComment(comment) && |
| isStylelintCommand(comment) && |
| nextComment && |
| nextComment.type === 'comment' && |
| (comment.text.includes('--') || nextComment.text.startsWith('--')) |
| ) |
| ) { |
| checkComment(comment); |
| |
| return; |
| } |
| |
| let lastLine = (comment.source && comment.source.end && comment.source.end.line) || 0; |
| const fullComment = comment.clone(); |
| |
| let current = nextComment; |
| |
| while (!isStandardSyntaxComment(current) && !isStylelintCommand(current)) { |
| const currentLine = (current.source && current.source.end && 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 || next.type !== 'comment') break; |
| |
| current = next; |
| lastLine = currentLine; |
| } |
| |
| checkComment(fullComment); |
| }); |
| |
| return result; |
| |
| /** |
| * @param {PostcssComment} comment |
| */ |
| function isStylelintCommand(comment) { |
| return comment.text.startsWith(disableCommand) || comment.text.startsWith(enableCommand); |
| } |
| |
| /** |
| * @param {PostcssComment} comment |
| */ |
| function processDisableLineCommand(comment) { |
| if (comment.source && comment.source.start) { |
| const line = comment.source.start.line; |
| const description = getDescription(comment.text); |
| |
| getCommandRules(disableLineCommand, comment.text).forEach((ruleName) => { |
| disableLine(comment, line, ruleName, description); |
| }); |
| } |
| } |
| |
| /** |
| * @param {PostcssComment} comment |
| */ |
| function processDisableNextLineCommand(comment) { |
| if (comment.source && comment.source.end) { |
| const line = comment.source.end.line; |
| const description = getDescription(comment.text); |
| |
| getCommandRules(disableNextLineCommand, comment.text).forEach((ruleName) => { |
| disableLine(comment, line + 1, ruleName, description); |
| }); |
| } |
| } |
| |
| /** |
| * @param {PostcssComment} comment |
| * @param {number} line |
| * @param {string} ruleName |
| * @param {string|undefined} description |
| */ |
| function disableLine(comment, line, ruleName, description) { |
| 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(comment, line, disabledRuleName, strict, description); |
| endDisabledRange(line, disabledRuleName, strict); |
| }); |
| } else { |
| if (ruleIsDisabled(ruleName)) { |
| throw comment.error(`"${ruleName}" has already been disabled`, { |
| plugin: 'stylelint', |
| }); |
| } |
| |
| startDisabledRange(comment, line, ruleName, true, description); |
| endDisabledRange(line, ruleName, true); |
| } |
| } |
| |
| /** |
| * @param {PostcssComment} comment |
| */ |
| function processDisableCommand(comment) { |
| const description = getDescription(comment.text); |
| |
| 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(comment, line, ruleName, ruleName === ALL_RULES, description); |
| }); |
| } else { |
| startDisabledRange(comment, line, ruleToDisable, true, description); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * @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.entries(disabledRanges).forEach(([ruleName, ranges]) => { |
| const lastRange = ranges[ranges.length - 1]; |
| |
| if (!lastRange || !lastRange.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, description }) => |
| createDisableRange(comment, start, false, description, end, false), |
| ); |
| } else { |
| const ranges = disabledRanges[ALL_RULES]; |
| const range = ranges ? ranges[ranges.length - 1] : null; |
| |
| 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(/\s-{2,}\s/u)[0] // Allow for description (f.e. /* stylelint-disable a, b -- Description */). |
| .trim() |
| .split(',') |
| .filter(Boolean) |
| .map((r) => r.trim()); |
| |
| if (rules.length === 0) { |
| return [ALL_RULES]; |
| } |
| |
| 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 {PostcssComment} comment |
| * @param {number} line |
| * @param {string} ruleName |
| * @param {boolean} strict |
| * @param {string|undefined} description |
| */ |
| function startDisabledRange(comment, line, ruleName, strict, description) { |
| const rangeObj = createDisableRange(comment, line, strict, description); |
| |
| ensureRuleRanges(ruleName); |
| disabledRanges[ruleName].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] = disabledRanges.all.map(({ comment, start, end, description }) => |
| createDisableRange(comment, 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; |
| } |
| }; |