Skip to content

Commit 77d6d5b

Browse files
authored
feat: support TS syntax in no-unused-expressions (#19564)
1 parent 90228e5 commit 77d6d5b

File tree

3 files changed

+477
-56
lines changed

3 files changed

+477
-56
lines changed

docs/src/rules/no-unused-expressions.md

+61
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,64 @@ const myFragment = <></>;
276276
```
277277

278278
:::
279+
280+
### TypeScript Support
281+
282+
This rule supports TypeScript-specific expressions and follows these guidelines:
283+
284+
1. Directives (like `'use strict'`) are allowed in module and namespace declarations
285+
2. Type-related expressions are treated as unused if their wrapped value expressions are unused:
286+
* Type assertions (`x as number`, `<number>x`)
287+
* Non-null assertions (`x!`)
288+
* Type instantiations (`Set<number>`)
289+
290+
**Note**: Although type expressions never have runtime side effects (e.g., `x!` is equivalent to `x` at runtime), they can be used to assert types for testing purposes.
291+
292+
Examples of **correct** code for this rule when using TypeScript:
293+
294+
::: correct
295+
296+
```ts
297+
/* eslint no-unused-expressions: "error" */
298+
299+
// Type expressions wrapping function calls are allowed
300+
function getSet() {
301+
return Set;
302+
}
303+
getSet()<number>;
304+
getSet() as Set<unknown>;
305+
getSet()!;
306+
307+
// Directives in modules and namespaces
308+
module Foo {
309+
'use strict';
310+
'hello world';
311+
}
312+
313+
namespace Bar {
314+
'use strict';
315+
export class Baz {}
316+
}
317+
```
318+
319+
:::
320+
321+
Examples of **incorrect** code for this rule when using TypeScript:
322+
323+
::: incorrect
324+
325+
```ts
326+
/* eslint no-unused-expressions: "error" */
327+
328+
// Standalone type expressions
329+
Set<number>;
330+
1 as number;
331+
window!;
332+
333+
// Expressions inside namespaces
334+
namespace Bar {
335+
123;
336+
}
337+
```
338+
339+
:::

lib/rules/no-unused-expressions.js

+14-56
Original file line numberDiff line numberDiff line change
@@ -83,61 +83,6 @@ module.exports = {
8383
},
8484
] = context.options;
8585

86-
/**
87-
* Has AST suggesting a directive.
88-
* @param {ASTNode} node any node
89-
* @returns {boolean} whether the given node structurally represents a directive
90-
*/
91-
function looksLikeDirective(node) {
92-
return (
93-
node.type === "ExpressionStatement" &&
94-
node.expression.type === "Literal" &&
95-
typeof node.expression.value === "string"
96-
);
97-
}
98-
99-
/**
100-
* Gets the leading sequence of members in a list that pass the predicate.
101-
* @param {Function} predicate ([a] -> Boolean) the function used to make the determination
102-
* @param {a[]} list the input list
103-
* @returns {a[]} the leading sequence of members in the given list that pass the given predicate
104-
*/
105-
function takeWhile(predicate, list) {
106-
for (let i = 0; i < list.length; ++i) {
107-
if (!predicate(list[i])) {
108-
return list.slice(0, i);
109-
}
110-
}
111-
return list.slice();
112-
}
113-
114-
/**
115-
* Gets leading directives nodes in a Node body.
116-
* @param {ASTNode} node a Program or BlockStatement node
117-
* @returns {ASTNode[]} the leading sequence of directive nodes in the given node's body
118-
*/
119-
function directives(node) {
120-
return takeWhile(looksLikeDirective, node.body);
121-
}
122-
123-
/**
124-
* Detect if a Node is a directive.
125-
* @param {ASTNode} node any node
126-
* @returns {boolean} whether the given node is considered a directive in its current position
127-
*/
128-
function isDirective(node) {
129-
/**
130-
* https://tc39.es/ecma262/#directive-prologue
131-
*
132-
* Only `FunctionBody`, `ScriptBody` and `ModuleBody` can have directive prologue.
133-
* Class static blocks do not have directive prologue.
134-
*/
135-
return (
136-
astUtils.isTopLevelExpressionStatement(node) &&
137-
directives(node.parent).includes(node)
138-
);
139-
}
140-
14186
/**
14287
* The member functions return `true` if the type has no side-effects.
14388
* Unknown nodes are handled as `false`, then this rule ignores those.
@@ -190,13 +135,26 @@ module.exports = {
190135
UnaryExpression(node) {
191136
return node.operator !== "void" && node.operator !== "delete";
192137
},
138+
// TypeScript-specific node types
139+
TSAsExpression(node) {
140+
return Checker.isDisallowed(node.expression);
141+
},
142+
TSTypeAssertion(node) {
143+
return Checker.isDisallowed(node.expression);
144+
},
145+
TSNonNullExpression(node) {
146+
return Checker.isDisallowed(node.expression);
147+
},
148+
TSInstantiationExpression(node) {
149+
return Checker.isDisallowed(node.expression);
150+
},
193151
});
194152

195153
return {
196154
ExpressionStatement(node) {
197155
if (
198156
Checker.isDisallowed(node.expression) &&
199-
!isDirective(node)
157+
!astUtils.isDirective(node)
200158
) {
201159
context.report({ node, messageId: "unusedExpression" });
202160
}

0 commit comments

Comments
 (0)