UNPKG

5.53 kBJavaScriptView Raw
1'use strict';
2
3const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
4const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
5const report = require('../../utils/report');
6const ruleMessages = require('../../utils/ruleMessages');
7const styleSearch = require('style-search');
8const validateOptions = require('../../utils/validateOptions');
9
10const ruleName = 'no-extra-semicolons';
11
12const messages = ruleMessages(ruleName, {
13 rejected: 'Unexpected extra semicolon',
14});
15
16const meta = {
17 url: 'https://stylelint.io/user-guide/rules/list/no-extra-semicolons',
18};
19
20/**
21 * @param {import('postcss').Node} node
22 * @returns {number}
23 */
24function getOffsetByNode(node) {
25 // @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Document | Container<ChildNode>'
26 if (node.parent && node.parent.document) {
27 return 0;
28 }
29
30 const root = node.root();
31
32 if (!root.source) throw new Error('The root node must have a source');
33
34 if (!node.source) throw new Error('The node must have a source');
35
36 if (!node.source.start) throw new Error('The source must have a start position');
37
38 const string = root.source.input.css;
39 const nodeColumn = node.source.start.column;
40 const nodeLine = node.source.start.line;
41 let line = 1;
42 let column = 1;
43 let index = 0;
44
45 for (let i = 0; i < string.length; i++) {
46 if (column === nodeColumn && nodeLine === line) {
47 index = i;
48 break;
49 }
50
51 if (string[i] === '\n') {
52 column = 1;
53 line += 1;
54 } else {
55 column += 1;
56 }
57 }
58
59 return index;
60}
61
62/** @type {import('stylelint').Rule} */
63const rule = (primary, _secondaryOptions, context) => {
64 return (root, result) => {
65 const validOptions = validateOptions(result, ruleName, { actual: primary });
66
67 if (!validOptions) {
68 return;
69 }
70
71 if (root.raws.after && root.raws.after.trim().length !== 0) {
72 const rawAfterRoot = root.raws.after;
73
74 /** @type {number[]} */
75 const fixSemiIndices = [];
76
77 styleSearch({ source: rawAfterRoot, target: ';' }, (match) => {
78 if (context.fix) {
79 fixSemiIndices.push(match.startIndex);
80
81 return;
82 }
83
84 if (!root.source) throw new Error('The root node must have a source');
85
86 complain(root.source.input.css.length - rawAfterRoot.length + match.startIndex);
87 });
88
89 // fix
90 if (fixSemiIndices.length) {
91 root.raws.after = removeIndices(rawAfterRoot, fixSemiIndices);
92 }
93 }
94
95 root.walk((node) => {
96 if (node.type === 'atrule' && !isStandardSyntaxAtRule(node)) {
97 return;
98 }
99
100 if (node.type === 'rule' && !isStandardSyntaxRule(node)) {
101 return;
102 }
103
104 if (node.raws.before && node.raws.before.trim().length !== 0) {
105 const rawBeforeNode = node.raws.before;
106 const allowedSemi = 0;
107
108 const rawBeforeIndexStart = 0;
109
110 /** @type {number[]} */
111 const fixSemiIndices = [];
112
113 styleSearch({ source: rawBeforeNode, target: ';' }, (match, count) => {
114 if (count === allowedSemi) {
115 return;
116 }
117
118 if (context.fix) {
119 fixSemiIndices.push(match.startIndex - rawBeforeIndexStart);
120
121 return;
122 }
123
124 complain(getOffsetByNode(node) - rawBeforeNode.length + match.startIndex);
125 });
126
127 // fix
128 if (fixSemiIndices.length) {
129 node.raws.before = removeIndices(rawBeforeNode, fixSemiIndices);
130 }
131 }
132
133 if (typeof node.raws.after === 'string' && node.raws.after.trim().length !== 0) {
134 const rawAfterNode = node.raws.after;
135
136 /**
137 * If the last child is a Less mixin followed by more than one semicolon,
138 * node.raws.after will be populated with that semicolon.
139 * Since we ignore Less mixins, exit here
140 */
141 if (
142 'last' in node &&
143 node.last &&
144 node.last.type === 'atrule' &&
145 !isStandardSyntaxAtRule(node.last)
146 ) {
147 return;
148 }
149
150 /** @type {number[]} */
151 const fixSemiIndices = [];
152
153 styleSearch({ source: rawAfterNode, target: ';' }, (match) => {
154 if (context.fix) {
155 fixSemiIndices.push(match.startIndex);
156
157 return;
158 }
159
160 const index =
161 getOffsetByNode(node) +
162 node.toString().length -
163 1 -
164 rawAfterNode.length +
165 match.startIndex;
166
167 complain(index);
168 });
169
170 // fix
171 if (fixSemiIndices.length) {
172 node.raws.after = removeIndices(rawAfterNode, fixSemiIndices);
173 }
174 }
175
176 if (typeof node.raws.ownSemicolon === 'string') {
177 const rawOwnSemicolon = node.raws.ownSemicolon;
178 const allowedSemi = 0;
179
180 /** @type {number[]} */
181 const fixSemiIndices = [];
182
183 styleSearch({ source: rawOwnSemicolon, target: ';' }, (match, count) => {
184 if (count === allowedSemi) {
185 return;
186 }
187
188 if (context.fix) {
189 fixSemiIndices.push(match.startIndex);
190
191 return;
192 }
193
194 const index =
195 getOffsetByNode(node) +
196 node.toString().length -
197 rawOwnSemicolon.length +
198 match.startIndex;
199
200 complain(index);
201 });
202
203 // fix
204 if (fixSemiIndices.length) {
205 node.raws.ownSemicolon = removeIndices(rawOwnSemicolon, fixSemiIndices);
206 }
207 }
208 });
209
210 /**
211 * @param {number} index
212 */
213 function complain(index) {
214 report({
215 message: messages.rejected,
216 node: root,
217 index,
218 result,
219 ruleName,
220 });
221 }
222
223 /**
224 * @param {string} str
225 * @param {number[]} indices
226 * @returns {string}
227 */
228 function removeIndices(str, indices) {
229 for (const index of indices.reverse()) {
230 str = str.slice(0, index) + str.slice(index + 1);
231 }
232
233 return str;
234 }
235 };
236};
237
238rule.ruleName = ruleName;
239rule.messages = messages;
240rule.meta = meta;
241module.exports = rule;