UNPKG

7.62 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 =
159 /** @type {number} */ (comment.source && comment.source.end && comment.source.end.line);
160
161 if (ruleToEnable === ALL_RULES) {
162 if (
163 Object.values(disabledRanges).every(
164 (ranges) => ranges.length === 0 || typeof ranges[ranges.length - 1].end === 'number',
165 )
166 ) {
167 throw comment.error('No rules have been disabled', {
168 plugin: 'stylelint',
169 });
170 }
171
172 Object.keys(disabledRanges).forEach((ruleName) => {
173 if (!_.get(_.last(disabledRanges[ruleName]), 'end')) {
174 endDisabledRange(endLine, ruleName, ruleName === ALL_RULES);
175 }
176 });
177
178 return;
179 }
180
181 if (ruleIsDisabled(ALL_RULES) && disabledRanges[ruleToEnable] === undefined) {
182 // Get a starting point from the where all rules were disabled
183 if (!disabledRanges[ruleToEnable]) {
184 disabledRanges[ruleToEnable] = disabledRanges.all.map(({ start, end }) =>
185 createDisableRange(start, false, end, false),
186 );
187 } else {
188 const range = _.last(disabledRanges[ALL_RULES]);
189
190 if (range) {
191 disabledRanges[ruleToEnable].push({ ...range });
192 }
193 }
194
195 endDisabledRange(endLine, ruleToEnable, true);
196
197 return;
198 }
199
200 if (ruleIsDisabled(ruleToEnable)) {
201 endDisabledRange(endLine, ruleToEnable, true);
202
203 return;
204 }
205
206 throw comment.error(`"${ruleToEnable}" has not been disabled`, {
207 plugin: 'stylelint',
208 });
209 });
210 }
211
212 /**
213 * @param {PostcssComment} comment
214 */
215 function checkComment(comment) {
216 const text = comment.text;
217
218 // Ignore comments that are not relevant commands
219
220 if (text.indexOf(COMMAND_PREFIX) !== 0) {
221 return result;
222 }
223
224 if (text.startsWith(disableLineCommand)) {
225 processDisableLineCommand(comment);
226 } else if (text.startsWith(disableNextLineCommand)) {
227 processDisableNextLineCommand(comment);
228 } else if (text.startsWith(disableCommand)) {
229 processDisableCommand(comment);
230 } else if (text.startsWith(enableCommand)) {
231 processEnableCommand(comment);
232 }
233 }
234
235 /**
236 * @param {string} command
237 * @param {string} fullText
238 * @returns {string[]}
239 */
240 function getCommandRules(command, fullText) {
241 const rules = fullText
242 .slice(command.length)
243 .split(',')
244 .filter(Boolean)
245 .map((r) => r.trim());
246
247 if (_.isEmpty(rules)) {
248 return [ALL_RULES];
249 }
250
251 return rules;
252 }
253
254 /**
255 * @param {number} line
256 * @param {string} ruleName
257 * @param {boolean} strict
258 */
259 function startDisabledRange(line, ruleName, strict) {
260 const rangeObj = createDisableRange(line, strict);
261
262 ensureRuleRanges(ruleName);
263 disabledRanges[ruleName].push(rangeObj);
264 }
265
266 /**
267 * @param {number} line
268 * @param {string} ruleName
269 * @param {boolean} strict
270 */
271 function endDisabledRange(line, ruleName, strict) {
272 const lastRangeForRule = _.last(disabledRanges[ruleName]);
273
274 if (!lastRangeForRule) {
275 return;
276 }
277
278 // Add an `end` prop to the last range of that rule
279 lastRangeForRule.end = line;
280 lastRangeForRule.strictEnd = strict;
281 }
282
283 /**
284 * @param {string} ruleName
285 */
286 function ensureRuleRanges(ruleName) {
287 if (!disabledRanges[ruleName]) {
288 disabledRanges[ruleName] = disabledRanges.all.map(({ start, end }) =>
289 createDisableRange(start, false, end, false),
290 );
291 }
292 }
293
294 /**
295 * @param {string} ruleName
296 * @returns {boolean}
297 */
298 function ruleIsDisabled(ruleName) {
299 if (disabledRanges[ruleName] === undefined) return false;
300
301 if (_.last(disabledRanges[ruleName]) === undefined) return false;
302
303 if (_.get(_.last(disabledRanges[ruleName]), 'end') === undefined) return true;
304
305 return false;
306 }
307};