Skip to content

Commit 5a759aa

Browse files
feat(no-misused-observables): check jsx attributes
1 parent 7e67633 commit 5a759aa

File tree

3 files changed

+77
-2
lines changed

3 files changed

+77
-2
lines changed

src/etc/is.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export function isImportSpecifier(node: TSESTree.Node): node is TSESTree.ImportS
7878
return node.type === AST_NODE_TYPES.ImportSpecifier;
7979
}
8080

81+
export function isJSXExpressionContainer(node: TSESTree.Node): node is TSESTree.JSXExpressionContainer {
82+
return node.type === AST_NODE_TYPES.JSXExpressionContainer;
83+
}
84+
8185
export function isLiteral(node: TSESTree.Node): node is TSESTree.Literal {
8286
return node.type === AST_NODE_TYPES.Literal;
8387
}

src/rules/no-misused-observables.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TSESTree as es, ESLintUtils, TSESLint } from '@typescript-eslint/utils';
22
import * as tsutils from 'ts-api-utils';
33
import ts from 'typescript';
4-
import { getTypeServices } from '../etc';
4+
import { getTypeServices, isJSXExpressionContainer } from '../etc';
55
import { ruleCreator } from '../utils';
66

77
// The implementation of this rule is similar to typescript-eslint's no-misused-promises. MIT License.
@@ -50,7 +50,7 @@ export const noMisusedObservablesRule = ruleCreator({
5050
const voidReturnChecks: TSESLint.RuleListener = {
5151
CallExpression: checkArguments,
5252
NewExpression: checkArguments,
53-
// JSXAttribute: checkJSXAttribute,
53+
JSXAttribute: checkJSXAttribute,
5454
// ClassDeclaration: checkClassLikeOrInterfaceNode,
5555
// ClassExpression: checkClassLikeOrInterfaceNode,
5656
// TSInterfaceDeclaration: checkClassLikeOrInterfaceNode,
@@ -92,6 +92,19 @@ export const noMisusedObservablesRule = ruleCreator({
9292
}
9393
}
9494

95+
function checkJSXAttribute(node: es.JSXAttribute): void {
96+
if (!node.value || !isJSXExpressionContainer(node.value)) {
97+
return;
98+
}
99+
100+
if (couldReturnObservable(node.value.expression)) {
101+
context.report({
102+
messageId: 'forbiddenVoidReturnAttribute',
103+
node: node.value,
104+
});
105+
}
106+
}
107+
95108
return {
96109
...(checksVoidReturn ? voidReturnChecks : {}),
97110
...(checksSpreads ? spreadChecks : {}),

tests/rules/no-misused-observables.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,32 @@ ruleTester({ types: true }).run('no-misused-observables', noMisusedObservablesRu
3737
3838
[1, 2, 3].forEach(i => { return of(i); });
3939
`,
40+
{
41+
code: stripIndent`
42+
// void return attribute; explicitly allowed
43+
import { Observable, of } from "rxjs";
44+
import React, { FC } from "react";
45+
46+
const Component: FC<{ foo: () => void }> = () => <div />;
47+
return (
48+
<Component foo={() => of(42)} />
49+
);
50+
`,
51+
options: [{ checksVoidReturn: false }],
52+
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
53+
},
54+
{
55+
code: stripIndent`
56+
// void return attribute; unrelated
57+
import React, { FC } from "react";
58+
59+
const Component: FC<{ foo: () => void }> = () => <div />;
60+
return (
61+
<Component foo={() => 42} />
62+
);
63+
`,
64+
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
65+
},
4066
{
4167
code: stripIndent`
4268
// spread; explicitly allowed
@@ -102,6 +128,38 @@ ruleTester({ types: true }).run('no-misused-observables', noMisusedObservablesRu
102128
~~~~~~~~~~~~ [forbiddenVoidReturnArgument]
103129
`,
104130
),
131+
fromFixture(
132+
stripIndent`
133+
// void return attribute; block body
134+
import { Observable, of } from "rxjs";
135+
import React, { FC } from "react";
136+
137+
const Component: FC<{ foo: () => void }> = () => <div />;
138+
return (
139+
<Component foo={(): Observable<number> => { return of(42); }} />
140+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [forbiddenVoidReturnAttribute]
141+
);
142+
`,
143+
{
144+
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
145+
},
146+
),
147+
fromFixture(
148+
stripIndent`
149+
// void return attribute; inline body
150+
import { Observable, of } from "rxjs";
151+
import React, { FC } from "react";
152+
153+
const Component: FC<{ foo: () => void }> = () => <div />;
154+
return (
155+
<Component foo={() => of(42)} />
156+
~~~~~~~~~~~~~~ [forbiddenVoidReturnAttribute]
157+
);
158+
`,
159+
{
160+
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
161+
},
162+
),
105163
fromFixture(
106164
stripIndent`
107165
// spread variable

0 commit comments

Comments
 (0)