UNPKG

10.6 kBJavaScriptView Raw
1'use strict';
2
3const isStandardSyntaxComment = require('./utils/isStandardSyntaxComment');
4const { assert, assertNumber, assertString } = require('./utils/validateTypes');
5
6const COMMAND_PREFIX = 'stylelint-';
7const disableCommand = `${COMMAND_PREFIX}disable`;
8const enableCommand = `${COMMAND_PREFIX}enable`;
9const disableLineCommand = `${COMMAND_PREFIX}disable-line`;
10const disableNextLineCommand = `${COMMAND_PREFIX}disable-next-line`;
11const ALL_RULES = 'all';
12
13/** @typedef {import('postcss').Comment} PostcssComment */
14/** @typedef {import('postcss').Root} PostcssRoot */
15/** @typedef {import('postcss').Document} PostcssDocument */
16/** @typedef {import('stylelint').PostcssResult} PostcssResult */
17/** @typedef {import('stylelint').DisabledRangeObject} DisabledRangeObject */
18/** @typedef {import('stylelint').DisabledRange} DisabledRange */
19
20/**
21 * @param {PostcssComment} comment
22 * @param {number} start
23 * @param {boolean} strictStart
24 * @param {string|undefined} description
25 * @param {number} [end]
26 * @param {boolean} [strictEnd]
27 * @returns {DisabledRange}
28 */
29function createDisableRange(comment, start, strictStart, description, end, strictEnd) {
30 return {
31 comment,
32 start,
33 end: end || undefined,
34 strictStart,
35 strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
36 description,
37 };
38}
39
40/**
41 * Run it like a PostCSS plugin
42 * @param {PostcssRoot | PostcssDocument} root
43 * @param {PostcssResult} result
44 * @returns {PostcssResult}
45 */
46module.exports = function assignDisabledRanges(root, result) {
47 result.stylelint = result.stylelint || {
48 disabledRanges: {},
49 ruleSeverities: {},
50 customMessages: {},
51 ruleMetadata: {},
52 };
53
54 /**
55 * Most of the functions below work via side effects mutating this object
56 * @type {DisabledRangeObject & { all: DisabledRange[] }}
57 */
58 const disabledRanges = {
59 [ALL_RULES]: [],
60 };
61
62 result.stylelint.disabledRanges = disabledRanges;
63
64 // Work around postcss/postcss-scss#109 by merging adjacent `//` comments
65 // into a single node before passing to `checkComment`.
66
67 /** @type {PostcssComment?} */
68 let inlineEnd;
69
70 root.walkComments((comment) => {
71 if (inlineEnd) {
72 // Ignore comments already processed by grouping with a previous one.
73 if (inlineEnd === comment) inlineEnd = null;
74
75 return;
76 }
77
78 const nextComment = comment.next();
79
80 // If any of these conditions are not met, do not merge comments.
81 if (
82 !(
83 !isStandardSyntaxComment(comment) &&
84 isStylelintCommand(comment) &&
85 nextComment &&
86 nextComment.type === 'comment' &&
87 (comment.text.includes('--') || nextComment.text.startsWith('--'))
88 )
89 ) {
90 checkComment(comment);
91
92 return;
93 }
94
95 let lastLine = (comment.source && comment.source.end && comment.source.end.line) || 0;
96 const fullComment = comment.clone();
97
98 let current = nextComment;
99
100 while (!isStandardSyntaxComment(current) && !isStylelintCommand(current)) {
101 const currentLine = (current.source && current.source.end && current.source.end.line) || 0;
102
103 if (lastLine + 1 !== currentLine) break;
104
105 fullComment.text += `\n${current.text}`;
106
107 if (fullComment.source && current.source) {
108 fullComment.source.end = current.source.end;
109 }
110
111 inlineEnd = current;
112 const next = current.next();
113
114 if (!next || next.type !== 'comment') break;
115
116 current = next;
117 lastLine = currentLine;
118 }
119
120 checkComment(fullComment);
121 });
122
123 return result;
124
125 /**
126 * @param {PostcssComment} comment
127 */
128 function isStylelintCommand(comment) {
129 return comment.text.startsWith(disableCommand) || comment.text.startsWith(enableCommand);
130 }
131
132 /**
133 * @param {PostcssComment} comment
134 */
135 function processDisableLineCommand(comment) {
136 if (comment.source && comment.source.start) {
137 const line = comment.source.start.line;
138 const description = getDescription(comment.text);
139
140 for (const ruleName of getCommandRules(disableLineCommand, comment.text)) {
141 disableLine(comment, line, ruleName, description);
142 }
143 }
144 }
145
146 /**
147 * @param {PostcssComment} comment
148 */
149 function processDisableNextLineCommand(comment) {
150 if (comment.source && comment.source.end) {
151 const line = comment.source.end.line;
152 const description = getDescription(comment.text);
153
154 for (const ruleName of getCommandRules(disableNextLineCommand, comment.text)) {
155 disableLine(comment, line + 1, ruleName, description);
156 }
157 }
158 }
159
160 /**
161 * @param {PostcssComment} comment
162 * @param {number} line
163 * @param {string} ruleName
164 * @param {string|undefined} description
165 */
166 function disableLine(comment, line, ruleName, description) {
167 if (ruleIsDisabled(ALL_RULES)) {
168 throw comment.error('All rules have already been disabled', {
169 plugin: 'stylelint',
170 });
171 }
172
173 if (ruleName === ALL_RULES) {
174 for (const disabledRuleName of Object.keys(disabledRanges)) {
175 if (ruleIsDisabled(disabledRuleName)) continue;
176
177 const strict = disabledRuleName === ALL_RULES;
178
179 startDisabledRange(comment, line, disabledRuleName, strict, description);
180 endDisabledRange(line, disabledRuleName, strict);
181 }
182 } else {
183 if (ruleIsDisabled(ruleName)) {
184 throw comment.error(`"${ruleName}" has already been disabled`, {
185 plugin: 'stylelint',
186 });
187 }
188
189 startDisabledRange(comment, line, ruleName, true, description);
190 endDisabledRange(line, ruleName, true);
191 }
192 }
193
194 /**
195 * @param {PostcssComment} comment
196 */
197 function processDisableCommand(comment) {
198 const description = getDescription(comment.text);
199
200 for (const ruleToDisable of getCommandRules(disableCommand, comment.text)) {
201 const isAllRules = ruleToDisable === ALL_RULES;
202
203 if (ruleIsDisabled(ruleToDisable)) {
204 throw comment.error(
205 isAllRules
206 ? 'All rules have already been disabled'
207 : `"${ruleToDisable}" has already been disabled`,
208 {
209 plugin: 'stylelint',
210 },
211 );
212 }
213
214 if (comment.source && comment.source.start) {
215 const line = comment.source.start.line;
216
217 if (isAllRules) {
218 for (const ruleName of Object.keys(disabledRanges)) {
219 startDisabledRange(comment, line, ruleName, ruleName === ALL_RULES, description);
220 }
221 } else {
222 startDisabledRange(comment, line, ruleToDisable, true, description);
223 }
224 }
225 }
226 }
227
228 /**
229 * @param {PostcssComment} comment
230 */
231 function processEnableCommand(comment) {
232 for (const ruleToEnable of getCommandRules(enableCommand, comment.text)) {
233 // need fallback if endLine will be undefined
234 const endLine = comment.source && comment.source.end && comment.source.end.line;
235
236 assertNumber(endLine);
237
238 if (ruleToEnable === ALL_RULES) {
239 if (
240 Object.values(disabledRanges).every((ranges) => {
241 if (ranges.length === 0) return true;
242
243 const lastRange = ranges[ranges.length - 1];
244
245 return lastRange && typeof lastRange.end === 'number';
246 })
247 ) {
248 throw comment.error('No rules have been disabled', {
249 plugin: 'stylelint',
250 });
251 }
252
253 for (const [ruleName, ranges] of Object.entries(disabledRanges)) {
254 const lastRange = ranges[ranges.length - 1];
255
256 if (!lastRange || !lastRange.end) {
257 endDisabledRange(endLine, ruleName, ruleName === ALL_RULES);
258 }
259 }
260
261 continue;
262 }
263
264 if (ruleIsDisabled(ALL_RULES) && disabledRanges[ruleToEnable] === undefined) {
265 // Get a starting point from the where all rules were disabled
266 disabledRanges[ruleToEnable] = disabledRanges[ALL_RULES].map(
267 ({ start, end, description }) =>
268 createDisableRange(comment, start, false, description, end, false),
269 );
270
271 endDisabledRange(endLine, ruleToEnable, true);
272
273 continue;
274 }
275
276 if (ruleIsDisabled(ruleToEnable)) {
277 endDisabledRange(endLine, ruleToEnable, true);
278
279 continue;
280 }
281
282 throw comment.error(`"${ruleToEnable}" has not been disabled`, {
283 plugin: 'stylelint',
284 });
285 }
286 }
287
288 /**
289 * @param {PostcssComment} comment
290 */
291 function checkComment(comment) {
292 const text = comment.text;
293
294 // Ignore comments that are not relevant commands
295
296 if (text.indexOf(COMMAND_PREFIX) !== 0) {
297 return;
298 }
299
300 if (text.startsWith(disableLineCommand)) {
301 processDisableLineCommand(comment);
302 } else if (text.startsWith(disableNextLineCommand)) {
303 processDisableNextLineCommand(comment);
304 } else if (text.startsWith(disableCommand)) {
305 processDisableCommand(comment);
306 } else if (text.startsWith(enableCommand)) {
307 processEnableCommand(comment);
308 }
309 }
310
311 /**
312 * @param {string} command
313 * @param {string} fullText
314 * @returns {string[]}
315 */
316 function getCommandRules(command, fullText) {
317 // Allow for description (f.e. /* stylelint-disable a, b -- Description */).
318 const splitted = fullText.slice(command.length).split(/\s-{2,}\s/u)[0];
319
320 assertString(splitted);
321 const rules = splitted
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
358 const range = disabledRanges[ruleName];
359
360 assert(range);
361 range.push(rangeObj);
362 }
363
364 /**
365 * @param {number} line
366 * @param {string} ruleName
367 * @param {boolean} strict
368 */
369 function endDisabledRange(line, ruleName, strict) {
370 const ranges = disabledRanges[ruleName];
371 const lastRangeForRule = ranges ? ranges[ranges.length - 1] : null;
372
373 if (!lastRangeForRule) {
374 return;
375 }
376
377 // Add an `end` prop to the last range of that rule
378 lastRangeForRule.end = line;
379 lastRangeForRule.strictEnd = strict;
380 }
381
382 /**
383 * @param {string} ruleName
384 */
385 function ensureRuleRanges(ruleName) {
386 if (!disabledRanges[ruleName]) {
387 disabledRanges[ruleName] = disabledRanges[ALL_RULES].map(
388 ({ comment, start, end, description }) =>
389 createDisableRange(comment, start, false, description, end, false),
390 );
391 }
392 }
393
394 /**
395 * @param {string} ruleName
396 * @returns {boolean}
397 */
398 function ruleIsDisabled(ruleName) {
399 const ranges = disabledRanges[ruleName];
400
401 if (!ranges) return false;
402
403 const lastRange = ranges[ranges.length - 1];
404
405 if (!lastRange) return false;
406
407 if (!lastRange.end) return true;
408
409 return false;
410 }
411};