UNPKG

9.65 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
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/lib/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 {number} start
20 * @param {boolean} strictStart
21 * @param {string|undefined} description
22 * @param {number} [end]
23 * @param {boolean} [strictEnd]
24 * @returns {DisabledRange}
25 */
26function createDisableRange(start, strictStart, description, end, strictEnd) {
27 return {
28 start,
29 end: end || undefined,
30 strictStart,
31 strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
32 description,
33 };
34}
35
36/**
37 * Run it like a plugin ...
38 * @param {PostcssRoot} root
39 * @param {PostcssResult} result
40 * @returns {PostcssResult}
41 */
42module.exports = function (root, result) {
43 result.stylelint = result.stylelint || {
44 disabledRanges: {},
45 ruleSeverities: {},
46 customMessages: {},
47 };
48
49 /**
50 * Most of the functions below work via side effects mutating this object
51 * @type {DisabledRangeObject}
52 */
53 const disabledRanges = {
54 all: [],
55 };
56
57 result.stylelint.disabledRanges = disabledRanges;
58
59 // Work around postcss/postcss-scss#109 by merging adjacent `//` comments
60 // into a single node before passing to `checkComment`.
61
62 /** @type {PostcssComment?} */
63 let inlineEnd;
64
65 root.walkComments((/** @type {PostcssComment} */ comment) => {
66 if (inlineEnd) {
67 // Ignore comments already processed by grouping with a previous one.
68 if (inlineEnd === comment) inlineEnd = null;
69 } else if (isInlineComment(comment)) {
70 const fullComment = comment.clone();
71 let next = comment.next();
72
73 while (next && next.type === 'comment') {
74 /** @type {PostcssComment} */
75 const current = next;
76
77 if (!isInlineComment(current)) break;
78
79 fullComment.text += `\n${current.text}`;
80
81 if (fullComment.source && current.source) {
82 fullComment.source.end = current.source.end;
83 }
84
85 inlineEnd = current;
86 next = current.next();
87 }
88 checkComment(fullComment);
89 } else {
90 checkComment(comment);
91 }
92 });
93
94 return result;
95
96 /**
97 * @param {PostcssComment} comment
98 */
99 function isInlineComment(comment) {
100 // We check both here because the Sass parser uses `raws.inline` to indicate
101 // inline comments, while the Less parser uses `inline`.
102 return comment.inline || comment.raws.inline;
103 }
104
105 /**
106 * @param {PostcssComment} comment
107 */
108 function processDisableLineCommand(comment) {
109 if (comment.source && comment.source.start) {
110 const line = comment.source.start.line;
111 const description = getDescription(comment.text);
112
113 getCommandRules(disableLineCommand, comment.text).forEach((ruleName) => {
114 disableLine(line, ruleName, comment, description);
115 });
116 }
117 }
118
119 /**
120 * @param {PostcssComment} comment
121 */
122 function processDisableNextLineCommand(comment) {
123 if (comment.source && comment.source.end) {
124 const line = comment.source.end.line;
125 const description = getDescription(comment.text);
126
127 getCommandRules(disableNextLineCommand, comment.text).forEach((ruleName) => {
128 disableLine(line + 1, ruleName, comment, description);
129 });
130 }
131 }
132
133 /**
134 * @param {number} line
135 * @param {string} ruleName
136 * @param {PostcssComment} comment
137 * @param {string|undefined} description
138 */
139 function disableLine(line, ruleName, comment, description) {
140 if (ruleIsDisabled(ALL_RULES)) {
141 throw comment.error('All rules have already been disabled', {
142 plugin: 'stylelint',
143 });
144 }
145
146 if (ruleName === ALL_RULES) {
147 Object.keys(disabledRanges).forEach((disabledRuleName) => {
148 if (ruleIsDisabled(disabledRuleName)) return;
149
150 const strict = disabledRuleName === ALL_RULES;
151
152 startDisabledRange(line, disabledRuleName, strict, description);
153 endDisabledRange(line, disabledRuleName, strict);
154 });
155 } else {
156 if (ruleIsDisabled(ruleName)) {
157 throw comment.error(`"${ruleName}" has already been disabled`, {
158 plugin: 'stylelint',
159 });
160 }
161
162 startDisabledRange(line, ruleName, true, description);
163 endDisabledRange(line, ruleName, true);
164 }
165 }
166
167 /**
168 * @param {PostcssComment} comment
169 */
170 function processDisableCommand(comment) {
171 const description = getDescription(comment.text);
172
173 getCommandRules(disableCommand, comment.text).forEach((ruleToDisable) => {
174 const isAllRules = ruleToDisable === ALL_RULES;
175
176 if (ruleIsDisabled(ruleToDisable)) {
177 throw comment.error(
178 isAllRules
179 ? 'All rules have already been disabled'
180 : `"${ruleToDisable}" has already been disabled`,
181 {
182 plugin: 'stylelint',
183 },
184 );
185 }
186
187 if (comment.source && comment.source.start) {
188 const line = comment.source.start.line;
189
190 if (isAllRules) {
191 Object.keys(disabledRanges).forEach((ruleName) => {
192 startDisabledRange(line, ruleName, ruleName === ALL_RULES, description);
193 });
194 } else {
195 startDisabledRange(line, ruleToDisable, true, description);
196 }
197 }
198 });
199 }
200
201 /**
202 * @param {PostcssComment} comment
203 */
204 function processEnableCommand(comment) {
205 getCommandRules(enableCommand, comment.text).forEach((ruleToEnable) => {
206 // TODO TYPES
207 // need fallback if endLine will be undefined
208 const endLine = /** @type {number} */ (comment.source &&
209 comment.source.end &&
210 comment.source.end.line);
211
212 if (ruleToEnable === ALL_RULES) {
213 if (
214 Object.values(disabledRanges).every(
215 (ranges) => ranges.length === 0 || typeof ranges[ranges.length - 1].end === 'number',
216 )
217 ) {
218 throw comment.error('No rules have been disabled', {
219 plugin: 'stylelint',
220 });
221 }
222
223 Object.keys(disabledRanges).forEach((ruleName) => {
224 if (!_.get(_.last(disabledRanges[ruleName]), 'end')) {
225 endDisabledRange(endLine, ruleName, ruleName === ALL_RULES);
226 }
227 });
228
229 return;
230 }
231
232 if (ruleIsDisabled(ALL_RULES) && disabledRanges[ruleToEnable] === undefined) {
233 // Get a starting point from the where all rules were disabled
234 if (!disabledRanges[ruleToEnable]) {
235 disabledRanges[ruleToEnable] = disabledRanges.all.map(({ start, end, description }) =>
236 createDisableRange(start, false, description, end, false),
237 );
238 } else {
239 const range = _.last(disabledRanges[ALL_RULES]);
240
241 if (range) {
242 disabledRanges[ruleToEnable].push({ ...range });
243 }
244 }
245
246 endDisabledRange(endLine, ruleToEnable, true);
247
248 return;
249 }
250
251 if (ruleIsDisabled(ruleToEnable)) {
252 endDisabledRange(endLine, ruleToEnable, true);
253
254 return;
255 }
256
257 throw comment.error(`"${ruleToEnable}" has not been disabled`, {
258 plugin: 'stylelint',
259 });
260 });
261 }
262
263 /**
264 * @param {PostcssComment} comment
265 */
266 function checkComment(comment) {
267 const text = comment.text;
268
269 // Ignore comments that are not relevant commands
270
271 if (text.indexOf(COMMAND_PREFIX) !== 0) {
272 return result;
273 }
274
275 if (text.startsWith(disableLineCommand)) {
276 processDisableLineCommand(comment);
277 } else if (text.startsWith(disableNextLineCommand)) {
278 processDisableNextLineCommand(comment);
279 } else if (text.startsWith(disableCommand)) {
280 processDisableCommand(comment);
281 } else if (text.startsWith(enableCommand)) {
282 processEnableCommand(comment);
283 }
284 }
285
286 /**
287 * @param {string} command
288 * @param {string} fullText
289 * @returns {string[]}
290 */
291 function getCommandRules(command, fullText) {
292 const rules = fullText
293 .slice(command.length)
294 .split(/\s-{2,}\s/u)[0] // Allow for description (f.e. /* stylelint-disable a, b -- Description */).
295 .trim()
296 .split(',')
297 .filter(Boolean)
298 .map((r) => r.trim());
299
300 if (_.isEmpty(rules)) {
301 return [ALL_RULES];
302 }
303
304 return rules;
305 }
306
307 /**
308 * @param {string} fullText
309 * @returns {string|undefined}
310 */
311 function getDescription(fullText) {
312 const descriptionStart = fullText.indexOf('--');
313
314 if (descriptionStart === -1) return;
315
316 return fullText.slice(descriptionStart + 2).trim();
317 }
318
319 /**
320 * @param {number} line
321 * @param {string} ruleName
322 * @param {boolean} strict
323 * @param {string|undefined} description
324 */
325 function startDisabledRange(line, ruleName, strict, description) {
326 const rangeObj = createDisableRange(line, strict, description);
327
328 ensureRuleRanges(ruleName);
329 disabledRanges[ruleName].push(rangeObj);
330 }
331
332 /**
333 * @param {number} line
334 * @param {string} ruleName
335 * @param {boolean} strict
336 */
337 function endDisabledRange(line, ruleName, strict) {
338 const lastRangeForRule = _.last(disabledRanges[ruleName]);
339
340 if (!lastRangeForRule) {
341 return;
342 }
343
344 // Add an `end` prop to the last range of that rule
345 lastRangeForRule.end = line;
346 lastRangeForRule.strictEnd = strict;
347 }
348
349 /**
350 * @param {string} ruleName
351 */
352 function ensureRuleRanges(ruleName) {
353 if (!disabledRanges[ruleName]) {
354 disabledRanges[ruleName] = disabledRanges.all.map(({ start, end, description }) =>
355 createDisableRange(start, false, description, end, false),
356 );
357 }
358 }
359
360 /**
361 * @param {string} ruleName
362 * @returns {boolean}
363 */
364 function ruleIsDisabled(ruleName) {
365 if (disabledRanges[ruleName] === undefined) return false;
366
367 if (_.last(disabledRanges[ruleName]) === undefined) return false;
368
369 if (_.get(_.last(disabledRanges[ruleName]), 'end') === undefined) return true;
370
371 return false;
372 }
373};