1 |
|
2 |
|
3 | 'use strict';
|
4 |
|
5 | const _ = require('lodash');
|
6 | const beforeBlockString = require('../../utils/beforeBlockString');
|
7 | const hasBlock = require('../../utils/hasBlock');
|
8 | const optionsMatches = require('../../utils/optionsMatches');
|
9 | const report = require('../../utils/report');
|
10 | const ruleMessages = require('../../utils/ruleMessages');
|
11 | const styleSearch = require('style-search');
|
12 | const validateOptions = require('../../utils/validateOptions');
|
13 |
|
14 | const ruleName = 'indentation';
|
15 | const messages = ruleMessages(ruleName, {
|
16 | expected: (x) => `Expected indentation of ${x}`,
|
17 | });
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | function rule(space, options = {}, context) {
|
25 | const isTab = space === 'tab';
|
26 | const indentChar = isTab ? '\t' : ' '.repeat(space);
|
27 | const warningWord = isTab ? 'tab' : 'space';
|
28 |
|
29 | return (root, result) => {
|
30 | const validOptions = validateOptions(
|
31 | result,
|
32 | ruleName,
|
33 | {
|
34 | actual: space,
|
35 | possible: [_.isNumber, 'tab'],
|
36 | },
|
37 | {
|
38 | actual: options,
|
39 | possible: {
|
40 | baseIndentLevel: [_.isNumber, 'auto'],
|
41 | except: ['block', 'value', 'param'],
|
42 | ignore: ['value', 'param', 'inside-parens'],
|
43 | indentInsideParens: ['twice', 'once-at-root-twice-in-block'],
|
44 | indentClosingBrace: [_.isBoolean],
|
45 | },
|
46 | optional: true,
|
47 | },
|
48 | );
|
49 |
|
50 | if (!validOptions) {
|
51 | return;
|
52 | }
|
53 |
|
54 |
|
55 | root.walk((node) => {
|
56 | if (node.type === 'root') {
|
57 |
|
58 | return;
|
59 | }
|
60 |
|
61 | const nodeLevel = indentationLevel(node);
|
62 |
|
63 |
|
64 | const before = (node.raws.before || '').replace(/[*_]$/, '');
|
65 | const after = node.raws.after || '';
|
66 | const parent = node.parent;
|
67 |
|
68 | const expectedOpeningBraceIndentation = indentChar.repeat(nodeLevel);
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 | const isFirstChild = parent.type === 'root' && parent.first === node;
|
76 | const lastIndexOfNewline = before.lastIndexOf('\n');
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 | if (
|
83 | (lastIndexOfNewline !== -1 ||
|
84 | (isFirstChild && (!getDocument(parent) || parent.raws.beforeStart.endsWith('\n')))) &&
|
85 | before.slice(lastIndexOfNewline + 1) !== expectedOpeningBraceIndentation
|
86 | ) {
|
87 | if (context.fix) {
|
88 | if (isFirstChild && _.isString(node.raws.before)) {
|
89 | node.raws.before = node.raws.before.replace(
|
90 | /^[ \t]*(?=\S|$)/,
|
91 | expectedOpeningBraceIndentation,
|
92 | );
|
93 | }
|
94 |
|
95 | node.raws.before = fixIndentation(node.raws.before, expectedOpeningBraceIndentation);
|
96 | } else {
|
97 | report({
|
98 | message: messages.expected(legibleExpectation(nodeLevel)),
|
99 | node,
|
100 | result,
|
101 | ruleName,
|
102 | });
|
103 | }
|
104 | }
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | const closingBraceLevel = options.indentClosingBrace ? nodeLevel + 1 : nodeLevel;
|
111 | const expectedClosingBraceIndentation = indentChar.repeat(closingBraceLevel);
|
112 |
|
113 | if (
|
114 | hasBlock(node) &&
|
115 | after &&
|
116 | after.includes('\n') &&
|
117 | after.slice(after.lastIndexOf('\n') + 1) !== expectedClosingBraceIndentation
|
118 | ) {
|
119 | if (context.fix) {
|
120 | node.raws.after = fixIndentation(node.raws.after, expectedClosingBraceIndentation);
|
121 | } else {
|
122 | report({
|
123 | message: messages.expected(legibleExpectation(closingBraceLevel)),
|
124 | node,
|
125 | index: node.toString().length - 1,
|
126 | result,
|
127 | ruleName,
|
128 | });
|
129 | }
|
130 | }
|
131 |
|
132 |
|
133 | if (node.value) {
|
134 | checkValue(node, nodeLevel);
|
135 | }
|
136 |
|
137 |
|
138 | if (node.selector) {
|
139 | checkSelector(node, nodeLevel);
|
140 | }
|
141 |
|
142 |
|
143 | if (node.type === 'atrule') {
|
144 | checkAtRuleParams(node, nodeLevel);
|
145 | }
|
146 | });
|
147 |
|
148 | function indentationLevel(node, level = 0) {
|
149 | if (node.parent.type === 'root') {
|
150 | return level + getRootBaseIndentLevel(node.parent, options.baseIndentLevel, space);
|
151 | }
|
152 |
|
153 | let calculatedLevel;
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | calculatedLevel = indentationLevel(node.parent, level + 1);
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | if (
|
164 | optionsMatches(options, 'except', 'block') &&
|
165 | (node.type === 'rule' || node.type === 'atrule') &&
|
166 | hasBlock(node)
|
167 | ) {
|
168 | calculatedLevel--;
|
169 | }
|
170 |
|
171 | return calculatedLevel;
|
172 | }
|
173 |
|
174 | function checkValue(decl, declLevel) {
|
175 | if (!decl.value.includes('\n')) {
|
176 | return;
|
177 | }
|
178 |
|
179 | if (optionsMatches(options, 'ignore', 'value')) {
|
180 | return;
|
181 | }
|
182 |
|
183 | const declString = decl.toString();
|
184 | const valueLevel = optionsMatches(options, 'except', 'value') ? declLevel : declLevel + 1;
|
185 |
|
186 | checkMultilineBit(declString, valueLevel, decl);
|
187 | }
|
188 |
|
189 | function checkSelector(rule, ruleLevel) {
|
190 | const selector = rule.selector;
|
191 |
|
192 |
|
193 | if (rule.params) {
|
194 | ruleLevel += 1;
|
195 | }
|
196 |
|
197 | checkMultilineBit(selector, ruleLevel, rule);
|
198 | }
|
199 |
|
200 | function checkAtRuleParams(atRule, ruleLevel) {
|
201 | if (optionsMatches(options, 'ignore', 'param')) {
|
202 | return;
|
203 | }
|
204 |
|
205 |
|
206 |
|
207 | const paramLevel =
|
208 | optionsMatches(options, 'except', 'param') ||
|
209 | atRule.name === 'nest' ||
|
210 | atRule.name === 'at-root'
|
211 | ? ruleLevel
|
212 | : ruleLevel + 1;
|
213 |
|
214 | checkMultilineBit(beforeBlockString(atRule).trim(), paramLevel, atRule);
|
215 | }
|
216 |
|
217 | function checkMultilineBit(source, newlineIndentLevel, node) {
|
218 | if (!source.includes('\n')) {
|
219 | return;
|
220 | }
|
221 |
|
222 |
|
223 | const fixPositions = [];
|
224 |
|
225 |
|
226 |
|
227 | let parentheticalDepth = 0;
|
228 |
|
229 | styleSearch(
|
230 | {
|
231 | source,
|
232 | target: '\n',
|
233 | outsideParens: optionsMatches(options, 'ignore', 'inside-parens'),
|
234 | },
|
235 | (match, matchCount) => {
|
236 | const precedesClosingParenthesis = /^[ \t]*\)/.test(source.slice(match.startIndex + 1));
|
237 |
|
238 | if (
|
239 | optionsMatches(options, 'ignore', 'inside-parens') &&
|
240 | (precedesClosingParenthesis || match.insideParens)
|
241 | ) {
|
242 | return;
|
243 | }
|
244 |
|
245 | let expectedIndentLevel = newlineIndentLevel;
|
246 |
|
247 |
|
248 | if (!optionsMatches(options, 'ignore', 'inside-parens') && match.insideParens) {
|
249 |
|
250 | if (matchCount === 1) parentheticalDepth -= 1;
|
251 |
|
252 |
|
253 | let newlineIndex = match.startIndex;
|
254 |
|
255 | if (source[match.startIndex - 1] === '\r') {
|
256 | newlineIndex--;
|
257 | }
|
258 |
|
259 | const followsOpeningParenthesis = /\([ \t]*$/.test(source.slice(0, newlineIndex));
|
260 |
|
261 | if (followsOpeningParenthesis) {
|
262 | parentheticalDepth += 1;
|
263 | }
|
264 |
|
265 | const followsOpeningBrace = /{[ \t]*$/.test(source.slice(0, newlineIndex));
|
266 |
|
267 | if (followsOpeningBrace) {
|
268 | parentheticalDepth += 1;
|
269 | }
|
270 |
|
271 | const startingClosingBrace = /^[ \t]*}/.test(source.slice(match.startIndex + 1));
|
272 |
|
273 | if (startingClosingBrace) {
|
274 | parentheticalDepth -= 1;
|
275 | }
|
276 |
|
277 | expectedIndentLevel += parentheticalDepth;
|
278 |
|
279 |
|
280 |
|
281 | if (precedesClosingParenthesis) {
|
282 | parentheticalDepth -= 1;
|
283 | }
|
284 |
|
285 | switch (options.indentInsideParens) {
|
286 | case 'twice':
|
287 | if (!precedesClosingParenthesis || options.indentClosingBrace) {
|
288 | expectedIndentLevel += 1;
|
289 | }
|
290 |
|
291 | break;
|
292 | case 'once-at-root-twice-in-block':
|
293 | if (node.parent === node.root()) {
|
294 | if (precedesClosingParenthesis && !options.indentClosingBrace) {
|
295 | expectedIndentLevel -= 1;
|
296 | }
|
297 |
|
298 | break;
|
299 | }
|
300 |
|
301 | if (!precedesClosingParenthesis || options.indentClosingBrace) {
|
302 | expectedIndentLevel += 1;
|
303 | }
|
304 |
|
305 | break;
|
306 | default:
|
307 | if (precedesClosingParenthesis && !options.indentClosingBrace) {
|
308 | expectedIndentLevel -= 1;
|
309 | }
|
310 | }
|
311 | }
|
312 |
|
313 |
|
314 |
|
315 |
|
316 | const afterNewlineSpaceMatches = /^([ \t]*)\S/.exec(source.slice(match.startIndex + 1));
|
317 |
|
318 | if (!afterNewlineSpaceMatches) {
|
319 | return;
|
320 | }
|
321 |
|
322 | const afterNewlineSpace = afterNewlineSpaceMatches[1];
|
323 | const expectedIndentation = indentChar.repeat(
|
324 | expectedIndentLevel > 0 ? expectedIndentLevel : 0,
|
325 | );
|
326 |
|
327 | if (afterNewlineSpace !== expectedIndentation) {
|
328 | if (context.fix) {
|
329 |
|
330 | fixPositions.unshift({
|
331 | expectedIndentation,
|
332 | currentIndentation: afterNewlineSpace,
|
333 | startIndex: match.startIndex,
|
334 | });
|
335 | } else {
|
336 | report({
|
337 | message: messages.expected(legibleExpectation(expectedIndentLevel)),
|
338 | node,
|
339 | index: match.startIndex + afterNewlineSpace.length + 1,
|
340 | result,
|
341 | ruleName,
|
342 | });
|
343 | }
|
344 | }
|
345 | },
|
346 | );
|
347 |
|
348 | if (fixPositions.length) {
|
349 | if (node.type === 'rule') {
|
350 | fixPositions.forEach((fixPosition) => {
|
351 | node.selector = replaceIndentation(
|
352 | node.selector,
|
353 | fixPosition.currentIndentation,
|
354 | fixPosition.expectedIndentation,
|
355 | fixPosition.startIndex,
|
356 | );
|
357 | });
|
358 | }
|
359 |
|
360 | if (node.type === 'decl') {
|
361 | const declProp = node.prop;
|
362 | const declBetween = node.raws.between;
|
363 |
|
364 | fixPositions.forEach((fixPosition) => {
|
365 | if (fixPosition.startIndex < declProp.length + declBetween.length) {
|
366 | node.raws.between = replaceIndentation(
|
367 | declBetween,
|
368 | fixPosition.currentIndentation,
|
369 | fixPosition.expectedIndentation,
|
370 | fixPosition.startIndex - declProp.length,
|
371 | );
|
372 | } else {
|
373 | node.value = replaceIndentation(
|
374 | node.value,
|
375 | fixPosition.currentIndentation,
|
376 | fixPosition.expectedIndentation,
|
377 | fixPosition.startIndex - declProp.length - declBetween.length,
|
378 | );
|
379 | }
|
380 | });
|
381 | }
|
382 |
|
383 | if (node.type === 'atrule') {
|
384 | const atRuleName = node.name;
|
385 | const atRuleAfterName = node.raws.afterName;
|
386 | const atRuleParams = node.params;
|
387 |
|
388 | fixPositions.forEach((fixPosition) => {
|
389 |
|
390 | if (fixPosition.startIndex < 1 + atRuleName.length + atRuleAfterName.length) {
|
391 | node.raws.afterName = replaceIndentation(
|
392 | atRuleAfterName,
|
393 | fixPosition.currentIndentation,
|
394 | fixPosition.expectedIndentation,
|
395 | fixPosition.startIndex - atRuleName.length - 1,
|
396 | );
|
397 | } else {
|
398 | node.params = replaceIndentation(
|
399 | atRuleParams,
|
400 | fixPosition.currentIndentation,
|
401 | fixPosition.expectedIndentation,
|
402 | fixPosition.startIndex - atRuleName.length - atRuleAfterName.length - 1,
|
403 | );
|
404 | }
|
405 | });
|
406 | }
|
407 | }
|
408 | }
|
409 | };
|
410 |
|
411 | function legibleExpectation(level) {
|
412 | const count = isTab ? level : level * space;
|
413 | const quantifiedWarningWord = count === 1 ? warningWord : `${warningWord}s`;
|
414 |
|
415 | return `${count} ${quantifiedWarningWord}`;
|
416 | }
|
417 | }
|
418 |
|
419 | function getRootBaseIndentLevel(root, baseIndentLevel, space) {
|
420 | const document = getDocument(root);
|
421 |
|
422 | if (!document) {
|
423 | return 0;
|
424 | }
|
425 |
|
426 | let indentLevel = root.source.baseIndentLevel;
|
427 |
|
428 | if (!Number.isSafeInteger(indentLevel)) {
|
429 | indentLevel = inferRootIndentLevel(root, baseIndentLevel, () =>
|
430 | inferDocIndentSize(document, space),
|
431 | );
|
432 | root.source.baseIndentLevel = indentLevel;
|
433 | }
|
434 |
|
435 | return indentLevel;
|
436 | }
|
437 |
|
438 | function getDocument(node) {
|
439 | const document = node.document;
|
440 |
|
441 | if (document) {
|
442 | return document;
|
443 | }
|
444 |
|
445 | const root = node.root();
|
446 |
|
447 | return root && root.document;
|
448 | }
|
449 |
|
450 | function inferDocIndentSize(document, space) {
|
451 | let indentSize = document.source.indentSize;
|
452 |
|
453 | if (Number.isSafeInteger(indentSize)) {
|
454 | return indentSize;
|
455 | }
|
456 |
|
457 | const source = document.source.input.css;
|
458 | const indents = source.match(/^ *(?=\S)/gm);
|
459 |
|
460 | if (indents) {
|
461 | const scores = {};
|
462 | let lastIndentSize = 0;
|
463 | let lastLeadingSpacesLength = 0;
|
464 | const vote = (leadingSpacesLength) => {
|
465 | if (leadingSpacesLength) {
|
466 | lastIndentSize = Math.abs(leadingSpacesLength - lastLeadingSpacesLength) || lastIndentSize;
|
467 |
|
468 | if (lastIndentSize > 1) {
|
469 | if (scores[lastIndentSize]) {
|
470 | scores[lastIndentSize]++;
|
471 | } else {
|
472 | scores[lastIndentSize] = 1;
|
473 | }
|
474 | }
|
475 | } else {
|
476 | lastIndentSize = 0;
|
477 | }
|
478 |
|
479 | lastLeadingSpacesLength = leadingSpacesLength;
|
480 | };
|
481 |
|
482 | indents.forEach((leadingSpaces) => {
|
483 | vote(leadingSpaces.length);
|
484 | });
|
485 |
|
486 | let bestScore = 0;
|
487 |
|
488 | for (const indentSizeDate in scores) {
|
489 | if (Object.prototype.hasOwnProperty.call(scores, indentSizeDate)) {
|
490 | const score = scores[indentSizeDate];
|
491 |
|
492 | if (score > bestScore) {
|
493 | bestScore = score;
|
494 | indentSize = indentSizeDate;
|
495 | }
|
496 | }
|
497 | }
|
498 | }
|
499 |
|
500 | indentSize = Number(indentSize) || (indents && indents[0].length) || Number(space) || 2;
|
501 | document.source.indentSize = indentSize;
|
502 |
|
503 | return indentSize;
|
504 | }
|
505 |
|
506 | function inferRootIndentLevel(root, baseIndentLevel, indentSize) {
|
507 | function getIndentLevel(indent) {
|
508 | let tabCount = indent.match(/\t/g);
|
509 |
|
510 | tabCount = tabCount ? tabCount.length : 0;
|
511 | let spaceCount = indent.match(/ /g);
|
512 |
|
513 | spaceCount = spaceCount ? Math.round(spaceCount.length / indentSize()) : 0;
|
514 |
|
515 | return tabCount + spaceCount;
|
516 | }
|
517 |
|
518 | if (!Number.isSafeInteger(baseIndentLevel)) {
|
519 | let source = root.source.input.css;
|
520 |
|
521 | source = source.replace(/^[^\r\n]+/, (firstLine) => {
|
522 | if (/(?:^|\n)([ \t]*)$/.test(root.raws.beforeStart)) {
|
523 | return RegExp.$1 + firstLine;
|
524 | }
|
525 |
|
526 | return '';
|
527 | });
|
528 |
|
529 | const indents = source.match(/^[ \t]*(?=\S)/gm);
|
530 |
|
531 | if (indents) {
|
532 | return Math.min(...indents.map(getIndentLevel));
|
533 | }
|
534 |
|
535 | baseIndentLevel = 1;
|
536 | }
|
537 |
|
538 | const indents = [];
|
539 | const foundIndents = /(?:^|\n)([ \t]*)\S[^\r\n]*(?:\r?\n\s*)*$/m.exec(root.raws.beforeStart);
|
540 |
|
541 |
|
542 | if (foundIndents) {
|
543 | let shortest = Number.MAX_SAFE_INTEGER;
|
544 | let i = 0;
|
545 |
|
546 | while (++i < foundIndents.length) {
|
547 | const current = getIndentLevel(foundIndents[i]);
|
548 |
|
549 | if (current < shortest) {
|
550 | shortest = current;
|
551 |
|
552 | if (shortest === 0) {
|
553 | break;
|
554 | }
|
555 | }
|
556 | }
|
557 |
|
558 | if (shortest !== Number.MAX_SAFE_INTEGER) {
|
559 | indents.push(new Array(shortest).fill(' ').join(''));
|
560 | }
|
561 | }
|
562 |
|
563 | const after = root.raws.after;
|
564 |
|
565 | if (after) {
|
566 | let afterEnd;
|
567 |
|
568 | if (after.endsWith('\n')) {
|
569 | const document = root.document;
|
570 |
|
571 | if (document) {
|
572 | const nextRoot = document.nodes[document.nodes.indexOf(root) + 1];
|
573 |
|
574 | if (nextRoot) {
|
575 | afterEnd = nextRoot.raws.beforeStart;
|
576 | } else {
|
577 | afterEnd = document.raws.afterEnd;
|
578 | }
|
579 | } else {
|
580 |
|
581 | const parent = root.parent;
|
582 |
|
583 | const nextRoot = parent.nodes[parent.nodes.indexOf(root) + 1];
|
584 |
|
585 | if (nextRoot) {
|
586 | afterEnd = nextRoot.raws.beforeStart;
|
587 | } else {
|
588 | afterEnd = root.raws.afterEnd;
|
589 | }
|
590 | }
|
591 | } else {
|
592 | afterEnd = after;
|
593 | }
|
594 |
|
595 | indents.push(afterEnd.match(/^[ \t]*/)[0]);
|
596 | }
|
597 |
|
598 | if (indents.length) {
|
599 | return Math.max(...indents.map(getIndentLevel)) + baseIndentLevel;
|
600 | }
|
601 |
|
602 | return baseIndentLevel;
|
603 | }
|
604 |
|
605 | function fixIndentation(str, whitespace) {
|
606 | if (!_.isString(str)) {
|
607 | return str;
|
608 | }
|
609 |
|
610 | return str.replace(/\n[ \t]*(?=\S|$)/g, `\n${whitespace}`);
|
611 | }
|
612 |
|
613 | function replaceIndentation(input, searchString, replaceString, startIndex) {
|
614 | const offset = startIndex + 1;
|
615 | const stringStart = input.slice(0, offset);
|
616 | const stringEnd = input.slice(offset + searchString.length);
|
617 |
|
618 | return stringStart + replaceString + stringEnd;
|
619 | }
|
620 |
|
621 | rule.ruleName = ruleName;
|
622 | rule.messages = messages;
|
623 | module.exports = rule;
|