UNPKG

7.63 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').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 {number} [end]
22 * @param {boolean} [strictEnd]
23 * @returns {DisabledRange}
24 */
25function createDisableRange(start, strictStart, end, strictEnd) {
26 return {
27 start,
28 end: end || undefined,
29 strictStart,
30 strictEnd: typeof strictEnd === 'boolean' ? strictEnd : undefined,
31 };
32}
33
34/**
35 * Run it like a plugin ...
36 * @param {PostcssRoot} root
37 * @param {PostcssResult} result
38 * @returns {PostcssResult}
39 */
40module.exports = function (root, result) {
41 result.stylelint = result.stylelint || {
42 disabledRanges: {},
43 ruleSeverities: {},
44 customMessages: {},
45 };
46
47 /**
48 * Most of the functions below work via side effects mutating this object
49 * @type {DisabledRangeObject}
50 */
51 const disabledRanges = {
52 all: [],
53 };
54
55 result.stylelint.disabledRanges = disabledRanges;
56 root.walkComments(checkComment);
57
58 return result;
59
60 /**
61 * @param {PostcssComment} comment
62 */
63 function processDisableLineCommand(comment) {
64 if (comment.source && comment.source.start) {
65 const line = comment.source.start.line;
66
67 getCommandRules(disableLineCommand, comment.text).forEach((ruleName) => {
68 disableLine(line, ruleName, comment);
69 });
70 }
71 }
72
73 /**
74 * @param {PostcssComment} comment
75 */
76 function processDisableNextLineCommand(comment) {
77 if (comment.source && comment.source.start) {
78 const line = comment.source.start.line;
79
80 getCommandRules(disableNextLineCommand, comment.text).forEach((ruleName) => {
81 disableLine(line + 1, ruleName, comment);
82 });
83 }
84 }
85
86 /**
87 * @param {number} line
88 * @param {string} ruleName
89 * @param {PostcssComment} comment
90 */
91 function disableLine(line, ruleName, comment) {
92 if (ruleIsDisabled(ALL_RULES)) {
93 throw comment.error('All rules have already been disabled', {
94 plugin: 'stylelint',
95 });
96 }
97
98 if (ruleName === ALL_RULES) {
99 Object.keys(disabledRanges).forEach((disabledRuleName) => {
100 if (ruleIsDisabled(disabledRuleName)) return;
101
102 const strict = disabledRuleName === ALL_RULES;
103
104 startDisabledRange(line, disabledRuleName, strict);
105 endDisabledRange(line, disabledRuleName, strict);
106 });
107 } else {
108 if (ruleIsDisabled(ruleName)) {
109 throw comment.error(`"${ruleName}" has already been disabled`, {
110 plugin: 'stylelint',
111 });
112 }
113
114 startDisabledRange(line, ruleName, true);
115 endDisabledRange(line, ruleName, true);
116 }
117 }
118
119 /**
120 * @param {PostcssComment} comment
121 */
122 function processDisableCommand(comment) {
123 getCommandRules(disableCommand, comment.text).forEach((ruleToDisable) => {
124 const isAllRules = ruleToDisable === ALL_RULES;
125
126 if (ruleIsDisabled(ruleToDisable)) {
127 throw comment.error(
128 isAllRules
129 ? 'All rules have already been disabled'
130 : `"${ruleToDisable}" has already been disabled`,
131 {
132 plugin: 'stylelint',
133 },
134 );
135 }
136
137 if (comment.source && comment.source.start) {
138 const line = comment.source.start.line;
139
140 if (isAllRules) {
141 Object.keys(disabledRanges).forEach((ruleName) => {
142 startDisabledRange(line, ruleName, ruleName === ALL_RULES);
143 });
144 } else {
145 startDisabledRange(line, ruleToDisable, true);
146 }
147 }
148 });
149 }
150
151 /**
152 * @param {PostcssComment} comment
153 */
154 function processEnableCommand(comment) {
155 getCommandRules(enableCommand, comment.text).forEach((ruleToEnable) => {
156 // TODO TYPES
157 // need fallback if endLine will be undefined
158 const endLine = /** @type {number} */ (comment.source &&
159 comment.source.end &&
160 comment.source.end.line);
161
162 if (ruleToEnable === ALL_RULES) {
163 if (
164 Object.values(disabledRanges).every(
165 (ranges) => ranges.length === 0 || typeof ranges[ranges.length - 1].end === 'number',
166 )
167 ) {
168 throw comment.error('No rules have been disabled', {
169 plugin: 'stylelint',
170 });
171 }
172
173 Object.keys(disabledRanges).forEach((ruleName) => {
174 if (!_.get(_.last(disabledRanges[ruleName]), 'end')) {
175 endDisabledRange(endLine, ruleName, ruleName === ALL_RULES);
176 }
177 });
178
179 return;
180 }
181
182 if (ruleIsDisabled(ALL_RULES) && disabledRanges[ruleToEnable] === undefined) {
183 // Get a starting point from the where all rules were disabled
184 if (!disabledRanges[ruleToEnable]) {
185 disabledRanges[ruleToEnable] = disabledRanges.all.map(({ start, end }) =>
186 createDisableRange(start, false, end, false),
187 );
188 } else {
189 const range = _.last(disabledRanges[ALL_RULES]);
190
191 if (range) {
192 disabledRanges[ruleToEnable].push({ ...range });
193 }
194 }
195
196 endDisabledRange(endLine, ruleToEnable, true);
197
198 return;
199 }
200
201 if (ruleIsDisabled(ruleToEnable)) {
202 endDisabledRange(endLine, ruleToEnable, true);
203
204 return;
205 }
206
207 throw comment.error(`"${ruleToEnable}" has not been disabled`, {
208 plugin: 'stylelint',
209 });
210 });
211 }
212
213 /**
214 * @param {PostcssComment} comment
215 */
216 function checkComment(comment) {
217 const text = comment.text;
218
219 // Ignore comments that are not relevant commands
220
221 if (text.indexOf(COMMAND_PREFIX) !== 0) {
222 return result;
223 }
224
225 if (text.startsWith(disableLineCommand)) {
226 processDisableLineCommand(comment);
227 } else if (text.startsWith(disableNextLineCommand)) {
228 processDisableNextLineCommand(comment);
229 } else if (text.startsWith(disableCommand)) {
230 processDisableCommand(comment);
231 } else if (text.startsWith(enableCommand)) {
232 processEnableCommand(comment);
233 }
234 }
235
236 /**
237 * @param {string} command
238 * @param {string} fullText
239 * @returns {string[]}
240 */
241 function getCommandRules(command, fullText) {
242 const rules = fullText
243 .slice(command.length)
244 .split(',')
245 .filter(Boolean)
246 .map((r) => r.trim());
247
248 if (_.isEmpty(rules)) {
249 return [ALL_RULES];
250 }
251
252 return rules;
253 }
254
255 /**
256 * @param {number} line
257 * @param {string} ruleName
258 * @param {boolean} strict
259 */
260 function startDisabledRange(line, ruleName, strict) {
261 const rangeObj = createDisableRange(line, strict);
262
263 ensureRuleRanges(ruleName);
264 disabledRanges[ruleName].push(rangeObj);
265 }
266
267 /**
268 * @param {number} line
269 * @param {string} ruleName
270 * @param {boolean} strict
271 */
272 function endDisabledRange(line, ruleName, strict) {
273 const lastRangeForRule = _.last(disabledRanges[ruleName]);
274
275 if (!lastRangeForRule) {
276 return;
277 }
278
279 // Add an `end` prop to the last range of that rule
280 lastRangeForRule.end = line;
281 lastRangeForRule.strictEnd = strict;
282 }
283
284 /**
285 * @param {string} ruleName
286 */
287 function ensureRuleRanges(ruleName) {
288 if (!disabledRanges[ruleName]) {
289 disabledRanges[ruleName] = disabledRanges.all.map(({ start, end }) =>
290 createDisableRange(start, false, end, false),
291 );
292 }
293 }
294
295 /**
296 * @param {string} ruleName
297 * @returns {boolean}
298 */
299 function ruleIsDisabled(ruleName) {
300 if (disabledRanges[ruleName] === undefined) return false;
301
302 if (_.last(disabledRanges[ruleName]) === undefined) return false;
303
304 if (_.get(_.last(disabledRanges[ruleName]), 'end') === undefined) return true;
305
306 return false;
307 }
308};