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