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