Skip to content

Commit 0a8af2c

Browse files
feat(no-misused-observables): properties
1 parent 02b8925 commit 0a8af2c

File tree

2 files changed

+130
-1
lines changed

2 files changed

+130
-1
lines changed

src/rules/no-misused-observables.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const noMisusedObservablesRule = ruleCreator({
5454
ClassDeclaration: checkClassLikeOrInterfaceNode,
5555
ClassExpression: checkClassLikeOrInterfaceNode,
5656
TSInterfaceDeclaration: checkClassLikeOrInterfaceNode,
57-
// Property: checkProperty,
57+
Property: checkProperty,
5858
// ReturnStatement: checkReturnStatement,
5959
// AssignmentExpression: checkAssignment,
6060
// VariableDeclarator: checkVariableDeclarator,
@@ -150,6 +150,27 @@ export const noMisusedObservablesRule = ruleCreator({
150150
}
151151
}
152152

153+
function checkProperty(node: es.Property): void {
154+
const tsNode = esTreeNodeToTSNodeMap.get(node);
155+
156+
const contextualType = getPropertyContextualType(checker, tsNode);
157+
if (contextualType === undefined) {
158+
return;
159+
}
160+
161+
if (!isVoidReturningFunctionType(contextualType)) {
162+
return;
163+
}
164+
if (!couldReturnObservable(node.value)) {
165+
return;
166+
}
167+
168+
context.report({
169+
messageId: 'forbiddenVoidReturnProperty',
170+
node: node.value,
171+
});
172+
}
173+
153174
return {
154175
...(checksVoidReturn ? voidReturnChecks : {}),
155176
...(checksSpreads ? spreadChecks : {}),
@@ -224,3 +245,36 @@ function isStaticMember(node: es.Node): boolean {
224245
return (isMethodDefinition(node) || isPropertyDefinition(node))
225246
&& node.static;
226247
}
248+
249+
function getPropertyContextualType(
250+
checker: ts.TypeChecker,
251+
tsNode: ts.Node,
252+
): ts.Type | undefined {
253+
if (ts.isPropertyAssignment(tsNode)) {
254+
// { a: 1 }
255+
return checker.getContextualType(tsNode.initializer);
256+
} else if (ts.isShorthandPropertyAssignment(tsNode)) {
257+
// { a }
258+
return checker.getContextualType(tsNode.name);
259+
} else if (ts.isMethodDeclaration(tsNode)) {
260+
// { a() {} }
261+
if (ts.isComputedPropertyName(tsNode.name)) {
262+
return;
263+
}
264+
const obj = tsNode.parent;
265+
if (!ts.isObjectLiteralExpression(obj)) {
266+
return;
267+
}
268+
const objType = checker.getContextualType(obj);
269+
if (objType === undefined) {
270+
return;
271+
}
272+
const propertySymbol = checker.getPropertyOfType(objType, tsNode.name.text);
273+
if (propertySymbol === undefined) {
274+
return;
275+
}
276+
return checker.getTypeOfSymbolAtLocation(propertySymbol, tsNode.name);
277+
} else {
278+
return undefined;
279+
}
280+
}

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,55 @@ ruleTester({ types: true }).run('no-misused-observables', noMisusedObservablesRu
122122
}
123123
`,
124124
// #endregion valid; void return inherited method
125+
// #region valid; void return property
126+
{
127+
code: stripIndent`
128+
// void return property; explicitly allowed
129+
import { Observable, of } from "rxjs";
130+
131+
type Foo = { a: () => void, b: () => void, c: () => void };
132+
const b: () => Observable<number> = () => of(42);
133+
const foo: Foo = {
134+
a: (): Observable<number> => of(42),
135+
b,
136+
c(): Observable<number> { return of(42); },
137+
};
138+
`,
139+
options: [{ checksVoidReturn: false }],
140+
},
141+
stripIndent`
142+
// void return property; not void
143+
import { Observable, of } from "rxjs";
144+
145+
type Foo = { a: () => Observable<number>, b: () => Observable<number>, c: () => Observable<number> };
146+
const b: () => Observable<number> = () => of(42);
147+
const foo: Foo = {
148+
a: () => of(42),
149+
b,
150+
c(): Observable<number> { return of(42); },
151+
};
152+
`,
153+
stripIndent`
154+
// void return property; unrelated
155+
type Foo = { a: () => void, b: () => void, c: () => void };
156+
const b: () => number = () => 42;
157+
const foo: Foo = {
158+
a: () => 42,
159+
b,
160+
c(): number { return 42; },
161+
};
162+
`,
163+
stripIndent`
164+
// couldReturnType is bugged for variables (#66)
165+
import { Observable, of } from "rxjs";
166+
167+
type Foo = { bar: () => void };
168+
const bar: () => Observable<number> = () => of(42);
169+
const foo: Foo = {
170+
bar,
171+
};
172+
`,
173+
// #endregion valid; void return property
125174
// #region valid; spread
126175
{
127176
code: stripIndent`
@@ -584,6 +633,32 @@ ruleTester({ types: true }).run('no-misused-observables', noMisusedObservablesRu
584633
`,
585634
),
586635
// #endregion invalid; void return inherited method
636+
// #region invalid; void return property
637+
fromFixture(
638+
stripIndent`
639+
// void return property; arrow function
640+
import { of } from "rxjs";
641+
642+
type Foo = { bar: () => void };
643+
const foo: Foo = {
644+
bar: () => of(42),
645+
~~~~~~~~~~~~ [forbiddenVoidReturnProperty]
646+
};
647+
`,
648+
),
649+
fromFixture(
650+
stripIndent`
651+
// void return property; function
652+
import { Observable, of } from "rxjs";
653+
654+
type Foo = { bar: () => void };
655+
const foo: Foo = {
656+
bar(): Observable<number> { return of(42); },
657+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [forbiddenVoidReturnProperty]
658+
};
659+
`,
660+
),
661+
// #endregion invalid; void return property
587662
// #region invalid; spread
588663
fromFixture(
589664
stripIndent`

0 commit comments

Comments
 (0)