UNPKG

10.5 kBJavaScriptView Raw
1'use strict';
2
3const isStandardSyntaxComment = require('./utils/isStandardSyntaxComment');
4
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
12/** @typedef {import('postcss').Comment} PostcssComment */
13/** @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/**
19 * @param {PostcssComment} comment
20 * @param {number} start
21 * @param {boolean} strictStart
22 * @param {string|undefined} description
23 * @param {number} [end]
24 * @param {boolean} [strictEnd]
25 * @returns {DisabledRange}
26 */
27function createDisableRange(comment, start, strictStart, description, end, strictEnd) {
28 return {
29 comment,
30 start,
31 end: end || undefined,
32 strictStart,
33 strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
34 description,
35 };
36}
37
38/**
39 * Run it like a PostCSS plugin
40 * @param {PostcssRoot} root
41 * @param {PostcssResult} result
42 * @returns {PostcssResult}
43 */
44module.exports = function assignDisabledRanges(root, result) {
45 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;
60
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
67 root.walkComments((comment) => {
68 if (inlineEnd) {
69 // Ignore comments already processed by grouping with a previous one.
70 if (inlineEnd === comment) inlineEnd = null;
71
72 return;
73 }
74
75 const nextComment = comment.next();
76
77 // If any of these conditions are not met, do not merge comments.
78 if (
79 !(
80 !isStandardSyntaxComment(comment) &&
81 isStylelintCommand(comment) &&
82 nextComment &&
83 nextComment.type === 'comment' &&
84 (comment.text.includes('--') || nextComment.text.startsWith('--'))
85 )
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
95 let current = nextComment;
96
97 while (!isStandardSyntaxComment(current) && !isStylelintCommand(current)) {
98 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;
109 const next = current.next();
110
111 if (!next || next.type !== 'comment') break;
112
113 current = next;
114 lastLine = currentLine;
115 }
116
117 checkComment(fullComment);
118 });
119
120 return result;
121
122 /**
123 * @param {PostcssComment} comment
124 */
125 function isStylelintCommand(comment) {
126 return comment.text.startsWith(disableCommand) || comment.text.startsWith(enableCommand);
127 }
128
129 /**
130 * @param {PostcssComment} comment
131 */
132 function processDisableLineCommand(comment) {
133 if (comment.source && comment.source.start) {
134 const line = comment.source.start.line;
135 const description = getDescription(comment.text);
136
137 for (const ruleName of getCommandRules(disableLineCommand, comment.text)) {
138 disableLine(comment, line, ruleName, description);
139 }
140 }
141 }
142
143 /**
144 * @param {PostcssComment} comment
145 */
146 function processDisableNextLineCommand(comment) {
147 if (comment.source && comment.source.end) {
148 const line = comment.source.end.line;
149 const description = getDescription(comment.text);
150
151 for (const ruleName of getCommandRules(disableNextLineCommand, comment.text)) {
152 disableLine(comment, line + 1, ruleName, description);
153 }
154 }
155 }
156
157 /**
158 * @param {PostcssComment} comment
159 * @param {number} line
160 * @param {string} ruleName
161 * @param {string|undefined} description
162 */
163 function disableLine(comment, line, ruleName, description) {
164 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 for (const disabledRuleName of Object.keys(disabledRanges)) {
172 if (ruleIsDisabled(disabledRuleName)) continue;
173
174 const strict = disabledRuleName === ALL_RULES;
175
176 startDisabledRange(comment, line, disabledRuleName, strict, description);
177 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
186 startDisabledRange(comment, line, ruleName, true, description);
187 endDisabledRange(line, ruleName, true);
188 }
189 }
190
191 /**
192 * @param {PostcssComment} comment
193 */
194 function processDisableCommand(comment) {
195 const description = getDescription(comment.text);
196
197 for (const ruleToDisable of getCommandRules(disableCommand, comment.text)) {
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 for (const ruleName of Object.keys(disabledRanges)) {
216 startDisabledRange(comment, line, ruleName, ruleName === ALL_RULES, description);
217 }
218 } else {
219 startDisabledRange(comment, line, ruleToDisable, true, description);
220 }
221 }
222 }
223 }
224
225 /**
226 * @param {PostcssComment} comment
227 */
228 function processEnableCommand(comment) {
229 for (const ruleToEnable of getCommandRules(enableCommand, comment.text)) {
230 // TODO TYPES
231 // need fallback if endLine will be undefined
232 const endLine = /** @type {number} */ (
233 comment.source && comment.source.end && comment.source.end.line
234 );
235
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
247 for (const [ruleName, ranges] of Object.entries(disabledRanges)) {
248 const lastRange = ranges[ranges.length - 1];
249
250 if (!lastRange || !lastRange.end) {
251 endDisabledRange(endLine, ruleName, ruleName === ALL_RULES);
252 }
253 }
254
255 continue;
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]) {
261 disabledRanges[ruleToEnable] = disabledRanges.all.map(({ start, end, description }) =>
262 createDisableRange(comment, start, false, description, end, false),
263 );
264 } else {
265 const ranges = disabledRanges[ALL_RULES];
266 const range = ranges ? ranges[ranges.length - 1] : null;
267
268 if (range) {
269 disabledRanges[ruleToEnable].push({ ...range });
270 }
271 }
272
273 endDisabledRange(endLine, ruleToEnable, true);
274
275 continue;
276 }
277
278 if (ruleIsDisabled(ruleToEnable)) {
279 endDisabledRange(endLine, ruleToEnable, true);
280
281 continue;
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)
321 .split(/\s-{2,}\s/u)[0] // Allow for description (f.e. /* stylelint-disable a, b -- Description */).
322 .trim()
323 .split(',')
324 .filter(Boolean)
325 .map((r) => r.trim());
326
327 if (rules.length === 0) {
328 return [ALL_RULES];
329 }
330
331 return rules;
332 }
333
334 /**
335 * @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
348 * @param {number} line
349 * @param {string} ruleName
350 * @param {boolean} strict
351 * @param {string|undefined} description
352 */
353 function startDisabledRange(comment, line, ruleName, strict, description) {
354 const rangeObj = createDisableRange(comment, line, strict, description);
355
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) {
366 const ranges = disabledRanges[ruleName];
367 const lastRangeForRule = ranges ? ranges[ranges.length - 1] : null;
368
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]) {
383 disabledRanges[ruleName] = disabledRanges.all.map(({ comment, start, end, description }) =>
384 createDisableRange(comment, start, false, description, end, false),
385 );
386 }
387 }
388
389 /**
390 * @param {string} ruleName
391 * @returns {boolean}
392 */
393 function ruleIsDisabled(ruleName) {
394 const ranges = disabledRanges[ruleName];
395
396 if (!ranges) return false;
397
398 const lastRange = ranges[ranges.length - 1];
399
400 if (!lastRange) return false;
401
402 if (!lastRange.end) return true;
403
404 return false;
405 }
406};