123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- '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/lib/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 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;
-
- // Work around postcss/postcss-scss#109 by merging adjacent `//` comments
- // into a single node before passing to `checkComment`.
-
- /** @type {PostcssComment?} */
- let inlineEnd;
-
- root.walkComments((/** @type {PostcssComment} */ 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 (
- !(
- isInlineComment(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 (isInlineComment(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 isInlineComment(comment) {
- // We check both here because the Sass parser uses `raws.inline` to indicate
- // inline comments, while the Less parser uses `inline`.
- return comment.inline || comment.raws.inline;
- }
-
- /**
- * @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.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, description }) =>
- createDisableRange(comment, start, false, description, 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(/\s-{2,}\s/u)[0] // Allow for description (f.e. /* stylelint-disable a, b -- Description */).
- .trim()
- .split(',')
- .filter(Boolean)
- .map((r) => r.trim());
-
- if (_.isEmpty(rules)) {
- 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 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(({ comment, start, end, description }) =>
- createDisableRange(comment, start, false, description, 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;
- }
- };
|