blob: 9d94c89807630628ce7a3e37fb3b77e403879f50 [file] [log] [blame]
Mathias Bynens79e2cf02020-05-29 16:46:17 +02001'use strict';
2
Tim van der Lippe16b82282021-11-08 13:50:26 +00003const isStandardSyntaxComment = require('./utils/isStandardSyntaxComment');
Mathias Bynens79e2cf02020-05-29 16:46:17 +02004
5const COMMAND_PREFIX = 'stylelint-';
6const disableCommand = `${COMMAND_PREFIX}disable`;
7const enableCommand = `${COMMAND_PREFIX}enable`;
8const disableLineCommand = `${COMMAND_PREFIX}disable-line`;
9const disableNextLineCommand = `${COMMAND_PREFIX}disable-next-line`;
10const ALL_RULES = 'all';
11
Tim van der Lippe16b82282021-11-08 13:50:26 +000012/** @typedef {import('postcss').Comment} PostcssComment */
Mathias Bynens79e2cf02020-05-29 16:46:17 +020013/** @typedef {import('postcss').Root} PostcssRoot */
14/** @typedef {import('stylelint').PostcssResult} PostcssResult */
15/** @typedef {import('stylelint').DisabledRangeObject} DisabledRangeObject */
16/** @typedef {import('stylelint').DisabledRange} DisabledRange */
17
18/**
Tim van der Lippeefb716a2020-12-01 12:54:04 +000019 * @param {PostcssComment} comment
Mathias Bynens79e2cf02020-05-29 16:46:17 +020020 * @param {number} start
21 * @param {boolean} strictStart
Tim van der Lippeefb716a2020-12-01 12:54:04 +000022 * @param {string|undefined} description
Mathias Bynens79e2cf02020-05-29 16:46:17 +020023 * @param {number} [end]
24 * @param {boolean} [strictEnd]
25 * @returns {DisabledRange}
26 */
Tim van der Lippeefb716a2020-12-01 12:54:04 +000027function createDisableRange(comment, start, strictStart, description, end, strictEnd) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +020028 return {
Tim van der Lippeefb716a2020-12-01 12:54:04 +000029 comment,
Mathias Bynens79e2cf02020-05-29 16:46:17 +020030 start,
31 end: end || undefined,
32 strictStart,
33 strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
Tim van der Lippeefb716a2020-12-01 12:54:04 +000034 description,
Mathias Bynens79e2cf02020-05-29 16:46:17 +020035 };
36}
37
38/**
Tim van der Lippe16b82282021-11-08 13:50:26 +000039 * Run it like a PostCSS plugin
Mathias Bynens79e2cf02020-05-29 16:46:17 +020040 * @param {PostcssRoot} root
41 * @param {PostcssResult} result
42 * @returns {PostcssResult}
43 */
Tim van der Lippe16b82282021-11-08 13:50:26 +000044module.exports = function assignDisabledRanges(root, result) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +020045 result.stylelint = result.stylelint || {
46 disabledRanges: {},
47 ruleSeverities: {},
48 customMessages: {},
49 };
50
51 /**
52 * Most of the functions below work via side effects mutating this object
53 * @type {DisabledRangeObject}
54 */
55 const disabledRanges = {
56 all: [],
57 };
58
59 result.stylelint.disabledRanges = disabledRanges;
Tim van der Lippeefb716a2020-12-01 12:54:04 +000060
61 // Work around postcss/postcss-scss#109 by merging adjacent `//` comments
62 // into a single node before passing to `checkComment`.
63
64 /** @type {PostcssComment?} */
65 let inlineEnd;
66
Tim van der Lippe16b82282021-11-08 13:50:26 +000067 root.walkComments((comment) => {
Tim van der Lippeefb716a2020-12-01 12:54:04 +000068 if (inlineEnd) {
69 // Ignore comments already processed by grouping with a previous one.
70 if (inlineEnd === comment) inlineEnd = null;
71
72 return;
73 }
74
Tim van der Lippe38208902021-05-11 16:37:59 +010075 const nextComment = comment.next();
Tim van der Lippeefb716a2020-12-01 12:54:04 +000076
77 // If any of these conditions are not met, do not merge comments.
78 if (
79 !(
Tim van der Lippe16b82282021-11-08 13:50:26 +000080 !isStandardSyntaxComment(comment) &&
Tim van der Lippeefb716a2020-12-01 12:54:04 +000081 isStylelintCommand(comment) &&
Tim van der Lippe38208902021-05-11 16:37:59 +010082 nextComment &&
83 nextComment.type === 'comment' &&
84 (comment.text.includes('--') || nextComment.text.startsWith('--'))
Tim van der Lippeefb716a2020-12-01 12:54:04 +000085 )
86 ) {
87 checkComment(comment);
88
89 return;
90 }
91
92 let lastLine = (comment.source && comment.source.end && comment.source.end.line) || 0;
93 const fullComment = comment.clone();
94
Tim van der Lippe38208902021-05-11 16:37:59 +010095 let current = nextComment;
Tim van der Lippeefb716a2020-12-01 12:54:04 +000096
Tim van der Lippe16b82282021-11-08 13:50:26 +000097 while (!isStandardSyntaxComment(current) && !isStylelintCommand(current)) {
Tim van der Lippeefb716a2020-12-01 12:54:04 +000098 const currentLine = (current.source && current.source.end && current.source.end.line) || 0;
99
100 if (lastLine + 1 !== currentLine) break;
101
102 fullComment.text += `\n${current.text}`;
103
104 if (fullComment.source && current.source) {
105 fullComment.source.end = current.source.end;
106 }
107
108 inlineEnd = current;
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000109 const next = current.next();
110
111 if (!next || next.type !== 'comment') break;
112
113 current = next;
114 lastLine = currentLine;
115 }
Tim van der Lippecc71b282021-02-12 15:51:14 +0000116
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000117 checkComment(fullComment);
118 });
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200119
120 return result;
121
122 /**
123 * @param {PostcssComment} comment
124 */
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000125 function isStylelintCommand(comment) {
126 return comment.text.startsWith(disableCommand) || comment.text.startsWith(enableCommand);
127 }
128
129 /**
130 * @param {PostcssComment} comment
131 */
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200132 function processDisableLineCommand(comment) {
133 if (comment.source && comment.source.start) {
134 const line = comment.source.start.line;
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000135 const description = getDescription(comment.text);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200136
137 getCommandRules(disableLineCommand, comment.text).forEach((ruleName) => {
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000138 disableLine(comment, line, ruleName, description);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200139 });
140 }
141 }
142
143 /**
144 * @param {PostcssComment} comment
145 */
146 function processDisableNextLineCommand(comment) {
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000147 if (comment.source && comment.source.end) {
148 const line = comment.source.end.line;
149 const description = getDescription(comment.text);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200150
151 getCommandRules(disableNextLineCommand, comment.text).forEach((ruleName) => {
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000152 disableLine(comment, line + 1, ruleName, description);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200153 });
154 }
155 }
156
157 /**
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000158 * @param {PostcssComment} comment
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200159 * @param {number} line
160 * @param {string} ruleName
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000161 * @param {string|undefined} description
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200162 */
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000163 function disableLine(comment, line, ruleName, description) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200164 if (ruleIsDisabled(ALL_RULES)) {
165 throw comment.error('All rules have already been disabled', {
166 plugin: 'stylelint',
167 });
168 }
169
170 if (ruleName === ALL_RULES) {
171 Object.keys(disabledRanges).forEach((disabledRuleName) => {
172 if (ruleIsDisabled(disabledRuleName)) return;
173
174 const strict = disabledRuleName === ALL_RULES;
175
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000176 startDisabledRange(comment, line, disabledRuleName, strict, description);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200177 endDisabledRange(line, disabledRuleName, strict);
178 });
179 } else {
180 if (ruleIsDisabled(ruleName)) {
181 throw comment.error(`"${ruleName}" has already been disabled`, {
182 plugin: 'stylelint',
183 });
184 }
185
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000186 startDisabledRange(comment, line, ruleName, true, description);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200187 endDisabledRange(line, ruleName, true);
188 }
189 }
190
191 /**
192 * @param {PostcssComment} comment
193 */
194 function processDisableCommand(comment) {
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000195 const description = getDescription(comment.text);
196
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200197 getCommandRules(disableCommand, comment.text).forEach((ruleToDisable) => {
198 const isAllRules = ruleToDisable === ALL_RULES;
199
200 if (ruleIsDisabled(ruleToDisable)) {
201 throw comment.error(
202 isAllRules
203 ? 'All rules have already been disabled'
204 : `"${ruleToDisable}" has already been disabled`,
205 {
206 plugin: 'stylelint',
207 },
208 );
209 }
210
211 if (comment.source && comment.source.start) {
212 const line = comment.source.start.line;
213
214 if (isAllRules) {
215 Object.keys(disabledRanges).forEach((ruleName) => {
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000216 startDisabledRange(comment, line, ruleName, ruleName === ALL_RULES, description);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200217 });
218 } else {
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000219 startDisabledRange(comment, line, ruleToDisable, true, description);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200220 }
221 }
222 });
223 }
224
225 /**
226 * @param {PostcssComment} comment
227 */
228 function processEnableCommand(comment) {
229 getCommandRules(enableCommand, comment.text).forEach((ruleToEnable) => {
230 // TODO TYPES
231 // need fallback if endLine will be undefined
Tim van der Lippe16b82282021-11-08 13:50:26 +0000232 const endLine = /** @type {number} */ (
233 comment.source && comment.source.end && comment.source.end.line
234 );
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200235
236 if (ruleToEnable === ALL_RULES) {
237 if (
238 Object.values(disabledRanges).every(
239 (ranges) => ranges.length === 0 || typeof ranges[ranges.length - 1].end === 'number',
240 )
241 ) {
242 throw comment.error('No rules have been disabled', {
243 plugin: 'stylelint',
244 });
245 }
246
Tim van der Lippe16b82282021-11-08 13:50:26 +0000247 Object.entries(disabledRanges).forEach(([ruleName, ranges]) => {
248 const lastRange = ranges[ranges.length - 1];
249
250 if (!lastRange || !lastRange.end) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200251 endDisabledRange(endLine, ruleName, ruleName === ALL_RULES);
252 }
253 });
254
255 return;
256 }
257
258 if (ruleIsDisabled(ALL_RULES) && disabledRanges[ruleToEnable] === undefined) {
259 // Get a starting point from the where all rules were disabled
260 if (!disabledRanges[ruleToEnable]) {
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000261 disabledRanges[ruleToEnable] = disabledRanges.all.map(({ start, end, description }) =>
262 createDisableRange(comment, start, false, description, end, false),
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200263 );
264 } else {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000265 const ranges = disabledRanges[ALL_RULES];
266 const range = ranges ? ranges[ranges.length - 1] : null;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200267
268 if (range) {
269 disabledRanges[ruleToEnable].push({ ...range });
270 }
271 }
272
273 endDisabledRange(endLine, ruleToEnable, true);
274
275 return;
276 }
277
278 if (ruleIsDisabled(ruleToEnable)) {
279 endDisabledRange(endLine, ruleToEnable, true);
280
281 return;
282 }
283
284 throw comment.error(`"${ruleToEnable}" has not been disabled`, {
285 plugin: 'stylelint',
286 });
287 });
288 }
289
290 /**
291 * @param {PostcssComment} comment
292 */
293 function checkComment(comment) {
294 const text = comment.text;
295
296 // Ignore comments that are not relevant commands
297
298 if (text.indexOf(COMMAND_PREFIX) !== 0) {
299 return result;
300 }
301
302 if (text.startsWith(disableLineCommand)) {
303 processDisableLineCommand(comment);
304 } else if (text.startsWith(disableNextLineCommand)) {
305 processDisableNextLineCommand(comment);
306 } else if (text.startsWith(disableCommand)) {
307 processDisableCommand(comment);
308 } else if (text.startsWith(enableCommand)) {
309 processEnableCommand(comment);
310 }
311 }
312
313 /**
314 * @param {string} command
315 * @param {string} fullText
316 * @returns {string[]}
317 */
318 function getCommandRules(command, fullText) {
319 const rules = fullText
320 .slice(command.length)
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000321 .split(/\s-{2,}\s/u)[0] // Allow for description (f.e. /* stylelint-disable a, b -- Description */).
322 .trim()
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200323 .split(',')
324 .filter(Boolean)
325 .map((r) => r.trim());
326
Tim van der Lippe16b82282021-11-08 13:50:26 +0000327 if (rules.length === 0) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200328 return [ALL_RULES];
329 }
330
331 return rules;
332 }
333
334 /**
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000335 * @param {string} fullText
336 * @returns {string|undefined}
337 */
338 function getDescription(fullText) {
339 const descriptionStart = fullText.indexOf('--');
340
341 if (descriptionStart === -1) return;
342
343 return fullText.slice(descriptionStart + 2).trim();
344 }
345
346 /**
347 * @param {PostcssComment} comment
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200348 * @param {number} line
349 * @param {string} ruleName
350 * @param {boolean} strict
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000351 * @param {string|undefined} description
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200352 */
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000353 function startDisabledRange(comment, line, ruleName, strict, description) {
354 const rangeObj = createDisableRange(comment, line, strict, description);
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200355
356 ensureRuleRanges(ruleName);
357 disabledRanges[ruleName].push(rangeObj);
358 }
359
360 /**
361 * @param {number} line
362 * @param {string} ruleName
363 * @param {boolean} strict
364 */
365 function endDisabledRange(line, ruleName, strict) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000366 const ranges = disabledRanges[ruleName];
367 const lastRangeForRule = ranges ? ranges[ranges.length - 1] : null;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200368
369 if (!lastRangeForRule) {
370 return;
371 }
372
373 // Add an `end` prop to the last range of that rule
374 lastRangeForRule.end = line;
375 lastRangeForRule.strictEnd = strict;
376 }
377
378 /**
379 * @param {string} ruleName
380 */
381 function ensureRuleRanges(ruleName) {
382 if (!disabledRanges[ruleName]) {
Tim van der Lippeefb716a2020-12-01 12:54:04 +0000383 disabledRanges[ruleName] = disabledRanges.all.map(({ comment, start, end, description }) =>
384 createDisableRange(comment, start, false, description, end, false),
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200385 );
386 }
387 }
388
389 /**
390 * @param {string} ruleName
391 * @returns {boolean}
392 */
393 function ruleIsDisabled(ruleName) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000394 const ranges = disabledRanges[ruleName];
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200395
Tim van der Lippe16b82282021-11-08 13:50:26 +0000396 if (!ranges) return false;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200397
Tim van der Lippe16b82282021-11-08 13:50:26 +0000398 const lastRange = ranges[ranges.length - 1];
399
400 if (!lastRange) return false;
401
402 if (!lastRange.end) return true;
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200403
404 return false;
405 }
406};