]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/rules/semi.js
import 8.41.0 source
[pve-eslint.git] / eslint / lib / rules / semi.js
CommitLineData
eb39fafa
DC
1/**
2 * @fileoverview Rule to flag missing semicolons.
3 * @author Nicholas C. Zakas
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const FixTracker = require("./utils/fix-tracker");
12const astUtils = require("./utils/ast-utils");
13
14//------------------------------------------------------------------------------
15// Rule Definition
16//------------------------------------------------------------------------------
17
34eeec05 18/** @type {import('../shared/types').Rule} */
eb39fafa
DC
19module.exports = {
20 meta: {
21 type: "layout",
22
23 docs: {
8f9d1d4d 24 description: "Require or disallow semicolons instead of ASI",
eb39fafa 25 recommended: false,
f2a92ac6 26 url: "https://eslint.org/docs/latest/rules/semi"
eb39fafa
DC
27 },
28
29 fixable: "code",
30
31 schema: {
32 anyOf: [
33 {
34 type: "array",
35 items: [
36 {
37 enum: ["never"]
38 },
39 {
40 type: "object",
41 properties: {
42 beforeStatementContinuationChars: {
43 enum: ["always", "any", "never"]
44 }
45 },
46 additionalProperties: false
47 }
48 ],
49 minItems: 0,
50 maxItems: 2
51 },
52 {
53 type: "array",
54 items: [
55 {
56 enum: ["always"]
57 },
58 {
59 type: "object",
60 properties: {
f2a92ac6
DC
61 omitLastInOneLineBlock: { type: "boolean" },
62 omitLastInOneLineClassBody: { type: "boolean" }
eb39fafa
DC
63 },
64 additionalProperties: false
65 }
66 ],
67 minItems: 0,
68 maxItems: 2
69 }
70 ]
71 },
72
73 messages: {
74 missingSemi: "Missing semicolon.",
75 extraSemi: "Extra semicolon."
76 }
77 },
78
79 create(context) {
80
81 const OPT_OUT_PATTERN = /^[-[(/+`]/u; // One of [(/+-`
609c276f
TL
82 const unsafeClassFieldNames = new Set(["get", "set", "static"]);
83 const unsafeClassFieldFollowers = new Set(["*", "in", "instanceof"]);
eb39fafa
DC
84 const options = context.options[1];
85 const never = context.options[0] === "never";
86 const exceptOneLine = Boolean(options && options.omitLastInOneLineBlock);
f2a92ac6 87 const exceptOneLineClassBody = Boolean(options && options.omitLastInOneLineClassBody);
eb39fafa 88 const beforeStatementContinuationChars = options && options.beforeStatementContinuationChars || "any";
f2a92ac6 89 const sourceCode = context.sourceCode;
eb39fafa
DC
90
91 //--------------------------------------------------------------------------
92 // Helpers
93 //--------------------------------------------------------------------------
94
95 /**
96 * Reports a semicolon error with appropriate location and message.
97 * @param {ASTNode} node The node with an extra or missing semicolon.
98 * @param {boolean} missing True if the semicolon is missing.
99 * @returns {void}
100 */
101 function report(node, missing) {
102 const lastToken = sourceCode.getLastToken(node);
103 let messageId,
104 fix,
105 loc;
106
107 if (!missing) {
108 messageId = "missingSemi";
109 loc = {
110 start: lastToken.loc.end,
111 end: astUtils.getNextLocation(sourceCode, lastToken.loc.end)
112 };
113 fix = function(fixer) {
114 return fixer.insertTextAfter(lastToken, ";");
115 };
116 } else {
117 messageId = "extraSemi";
118 loc = lastToken.loc;
119 fix = function(fixer) {
120
121 /*
122 * Expand the replacement range to include the surrounding
123 * tokens to avoid conflicting with no-extra-semi.
124 * https://github.com/eslint/eslint/issues/7928
125 */
126 return new FixTracker(fixer, sourceCode)
127 .retainSurroundingTokens(lastToken)
128 .remove(lastToken);
129 };
130 }
131
132 context.report({
133 node,
134 loc,
135 messageId,
136 fix
137 });
138
139 }
140
141 /**
142 * Check whether a given semicolon token is redundant.
143 * @param {Token} semiToken A semicolon token to check.
144 * @returns {boolean} `true` if the next token is `;` or `}`.
145 */
146 function isRedundantSemi(semiToken) {
147 const nextToken = sourceCode.getTokenAfter(semiToken);
148
149 return (
150 !nextToken ||
151 astUtils.isClosingBraceToken(nextToken) ||
152 astUtils.isSemicolonToken(nextToken)
153 );
154 }
155
156 /**
157 * Check whether a given token is the closing brace of an arrow function.
158 * @param {Token} lastToken A token to check.
159 * @returns {boolean} `true` if the token is the closing brace of an arrow function.
160 */
161 function isEndOfArrowBlock(lastToken) {
162 if (!astUtils.isClosingBraceToken(lastToken)) {
163 return false;
164 }
165 const node = sourceCode.getNodeByRangeIndex(lastToken.range[0]);
166
167 return (
168 node.type === "BlockStatement" &&
169 node.parent.type === "ArrowFunctionExpression"
170 );
171 }
172
609c276f
TL
173 /**
174 * Checks if a given PropertyDefinition node followed by a semicolon
175 * can safely remove that semicolon. It is not to safe to remove if
176 * the class field name is "get", "set", or "static", or if
177 * followed by a generator method.
178 * @param {ASTNode} node The node to check.
179 * @returns {boolean} `true` if the node cannot have the semicolon
180 * removed.
181 */
182 function maybeClassFieldAsiHazard(node) {
183
184 if (node.type !== "PropertyDefinition") {
185 return false;
186 }
187
188 /*
189 * Computed property names and non-identifiers are always safe
190 * as they can be distinguished from keywords easily.
191 */
192 const needsNameCheck = !node.computed && node.key.type === "Identifier";
193
194 /*
195 * Certain names are problematic unless they also have a
196 * a way to distinguish between keywords and property
197 * names.
198 */
199 if (needsNameCheck && unsafeClassFieldNames.has(node.key.name)) {
200
201 /*
202 * Special case: If the field name is `static`,
203 * it is only valid if the field is marked as static,
204 * so "static static" is okay but "static" is not.
205 */
206 const isStaticStatic = node.static && node.key.name === "static";
207
208 /*
209 * For other unsafe names, we only care if there is no
210 * initializer. No initializer = hazard.
211 */
212 if (!isStaticStatic && !node.value) {
213 return true;
214 }
215 }
216
217 const followingToken = sourceCode.getTokenAfter(node);
218
219 return unsafeClassFieldFollowers.has(followingToken.value);
220 }
221
eb39fafa
DC
222 /**
223 * Check whether a given node is on the same line with the next token.
224 * @param {Node} node A statement node to check.
225 * @returns {boolean} `true` if the node is on the same line with the next token.
226 */
227 function isOnSameLineWithNextToken(node) {
228 const prevToken = sourceCode.getLastToken(node, 1);
229 const nextToken = sourceCode.getTokenAfter(node);
230
231 return !!nextToken && astUtils.isTokenOnSameLine(prevToken, nextToken);
232 }
233
234 /**
235 * Check whether a given node can connect the next line if the next line is unreliable.
236 * @param {Node} node A statement node to check.
237 * @returns {boolean} `true` if the node can connect the next line.
238 */
239 function maybeAsiHazardAfter(node) {
240 const t = node.type;
241
242 if (t === "DoWhileStatement" ||
243 t === "BreakStatement" ||
244 t === "ContinueStatement" ||
245 t === "DebuggerStatement" ||
246 t === "ImportDeclaration" ||
247 t === "ExportAllDeclaration"
248 ) {
249 return false;
250 }
251 if (t === "ReturnStatement") {
252 return Boolean(node.argument);
253 }
254 if (t === "ExportNamedDeclaration") {
255 return Boolean(node.declaration);
256 }
257 if (isEndOfArrowBlock(sourceCode.getLastToken(node, 1))) {
258 return false;
259 }
260
261 return true;
262 }
263
264 /**
265 * Check whether a given token can connect the previous statement.
266 * @param {Token} token A token to check.
267 * @returns {boolean} `true` if the token is one of `[`, `(`, `/`, `+`, `-`, ```, `++`, and `--`.
268 */
269 function maybeAsiHazardBefore(token) {
270 return (
271 Boolean(token) &&
272 OPT_OUT_PATTERN.test(token.value) &&
273 token.value !== "++" &&
274 token.value !== "--"
275 );
276 }
277
278 /**
279 * Check if the semicolon of a given node is unnecessary, only true if:
280 * - next token is a valid statement divider (`;` or `}`).
281 * - next token is on a new line and the node is not connectable to the new line.
282 * @param {Node} node A statement node to check.
283 * @returns {boolean} whether the semicolon is unnecessary.
284 */
285 function canRemoveSemicolon(node) {
286 if (isRedundantSemi(sourceCode.getLastToken(node))) {
287 return true; // `;;` or `;}`
288 }
609c276f
TL
289 if (maybeClassFieldAsiHazard(node)) {
290 return false;
291 }
eb39fafa
DC
292 if (isOnSameLineWithNextToken(node)) {
293 return false; // One liner.
294 }
609c276f
TL
295
296 // continuation characters should not apply to class fields
297 if (
298 node.type !== "PropertyDefinition" &&
299 beforeStatementContinuationChars === "never" &&
300 !maybeAsiHazardAfter(node)
301 ) {
eb39fafa
DC
302 return true; // ASI works. This statement doesn't connect to the next.
303 }
304 if (!maybeAsiHazardBefore(sourceCode.getTokenAfter(node))) {
305 return true; // ASI works. The next token doesn't connect to this statement.
306 }
307
308 return false;
309 }
310
311 /**
609c276f
TL
312 * Checks a node to see if it's the last item in a one-liner block.
313 * Block is any `BlockStatement` or `StaticBlock` node. Block is a one-liner if its
314 * braces (and consequently everything between them) are on the same line.
eb39fafa 315 * @param {ASTNode} node The node to check.
609c276f 316 * @returns {boolean} whether the node is the last item in a one-liner block.
eb39fafa 317 */
609c276f 318 function isLastInOneLinerBlock(node) {
eb39fafa
DC
319 const parent = node.parent;
320 const nextToken = sourceCode.getTokenAfter(node);
321
322 if (!nextToken || nextToken.value !== "}") {
323 return false;
324 }
609c276f
TL
325
326 if (parent.type === "BlockStatement") {
327 return parent.loc.start.line === parent.loc.end.line;
328 }
329
330 if (parent.type === "StaticBlock") {
331 const openingBrace = sourceCode.getFirstToken(parent, { skip: 1 }); // skip the `static` token
332
333 return openingBrace.loc.start.line === parent.loc.end.line;
334 }
335
336 return false;
eb39fafa
DC
337 }
338
f2a92ac6
DC
339 /**
340 * Checks a node to see if it's the last item in a one-liner `ClassBody` node.
341 * ClassBody is a one-liner if its braces (and consequently everything between them) are on the same line.
342 * @param {ASTNode} node The node to check.
343 * @returns {boolean} whether the node is the last item in a one-liner ClassBody.
344 */
345 function isLastInOneLinerClassBody(node) {
346 const parent = node.parent;
347 const nextToken = sourceCode.getTokenAfter(node);
348
349 if (!nextToken || nextToken.value !== "}") {
350 return false;
351 }
352
353 if (parent.type === "ClassBody") {
354 return parent.loc.start.line === parent.loc.end.line;
355 }
356
357 return false;
358 }
359
eb39fafa
DC
360 /**
361 * Checks a node to see if it's followed by a semicolon.
362 * @param {ASTNode} node The node to check.
363 * @returns {void}
364 */
365 function checkForSemicolon(node) {
366 const isSemi = astUtils.isSemicolonToken(sourceCode.getLastToken(node));
367
368 if (never) {
369 if (isSemi && canRemoveSemicolon(node)) {
370 report(node, true);
609c276f
TL
371 } else if (
372 !isSemi && beforeStatementContinuationChars === "always" &&
373 node.type !== "PropertyDefinition" &&
374 maybeAsiHazardBefore(sourceCode.getTokenAfter(node))
375 ) {
eb39fafa
DC
376 report(node);
377 }
378 } else {
609c276f 379 const oneLinerBlock = (exceptOneLine && isLastInOneLinerBlock(node));
f2a92ac6
DC
380 const oneLinerClassBody = (exceptOneLineClassBody && isLastInOneLinerClassBody(node));
381 const oneLinerBlockOrClassBody = oneLinerBlock || oneLinerClassBody;
eb39fafa 382
f2a92ac6 383 if (isSemi && oneLinerBlockOrClassBody) {
eb39fafa 384 report(node, true);
f2a92ac6 385 } else if (!isSemi && !oneLinerBlockOrClassBody) {
eb39fafa
DC
386 report(node);
387 }
388 }
389 }
390
391 /**
392 * Checks to see if there's a semicolon after a variable declaration.
393 * @param {ASTNode} node The node to check.
394 * @returns {void}
395 */
396 function checkForSemicolonForVariableDeclaration(node) {
397 const parent = node.parent;
398
399 if ((parent.type !== "ForStatement" || parent.init !== node) &&
400 (!/^For(?:In|Of)Statement/u.test(parent.type) || parent.left !== node)
401 ) {
402 checkForSemicolon(node);
403 }
404 }
405
406 //--------------------------------------------------------------------------
407 // Public API
408 //--------------------------------------------------------------------------
409
410 return {
411 VariableDeclaration: checkForSemicolonForVariableDeclaration,
412 ExpressionStatement: checkForSemicolon,
413 ReturnStatement: checkForSemicolon,
414 ThrowStatement: checkForSemicolon,
415 DoWhileStatement: checkForSemicolon,
416 DebuggerStatement: checkForSemicolon,
417 BreakStatement: checkForSemicolon,
418 ContinueStatement: checkForSemicolon,
419 ImportDeclaration: checkForSemicolon,
420 ExportAllDeclaration: checkForSemicolon,
421 ExportNamedDeclaration(node) {
422 if (!node.declaration) {
423 checkForSemicolon(node);
424 }
425 },
426 ExportDefaultDeclaration(node) {
427 if (!/(?:Class|Function)Declaration/u.test(node.declaration.type)) {
428 checkForSemicolon(node);
429 }
609c276f
TL
430 },
431 PropertyDefinition: checkForSemicolon
eb39fafa
DC
432 };
433
434 }
435};