From 4af5d45cd8d3204a71b843a990fd8ec5aa832be5 Mon Sep 17 00:00:00 2001 From: Nicholas Jamieson Date: Sat, 25 Apr 2020 09:02:15 +1000 Subject: [PATCH] feat: Add no-unsafe-subject-next rule. --- package.json | 2 +- source/rules/rxjsNoUnsafeSubjectNextRule.ts | 79 +++++++++++++++++++ .../default/fixture.ts.lint | 31 ++++++++ .../default/tsconfig.json | 13 +++ .../default/tslint.json | 8 ++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 source/rules/rxjsNoUnsafeSubjectNextRule.ts create mode 100644 test/v6/fixtures/no-unsafe-subject-next/default/fixture.ts.lint create mode 100644 test/v6/fixtures/no-unsafe-subject-next/default/tsconfig.json create mode 100644 test/v6/fixtures/no-unsafe-subject-next/default/tslint.json diff --git a/package.json b/package.json index d595f0a7..76346369 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "test": "yarn run lint && yarn run test:build && yarn run test:mocha && yarn run test:tslint-v5 && yarn run test:tslint-v6 && yarn run test:tslint-v6-compat", "test:build": "yarn run test:clean && tsc -p tsconfig.json", "test:clean": "rimraf build", - "test:debug": "tslint --test \"./test/v6/fixtures/issues/115/tslint.json\"", + "test:debug": "tslint --test \"./test/v6/fixtures/no-unsafe-subject-next/**/tslint.json\"", "test:issues": "yarn run test:clean && tsc -p tsconfig.json && tslint --test \"./test/v6/fixtures/issues/**/tslint.json\"", "test:mocha": "mocha \"./build/**/*-spec.js\"", "test:tslint-v5": "yarn --cwd ./test/v5 install && yarn --cwd ./test/v5 upgrade && tslint --test \"./test/v5/fixtures/**/tslint.json\"", diff --git a/source/rules/rxjsNoUnsafeSubjectNextRule.ts b/source/rules/rxjsNoUnsafeSubjectNextRule.ts new file mode 100644 index 00000000..24374a5f --- /dev/null +++ b/source/rules/rxjsNoUnsafeSubjectNextRule.ts @@ -0,0 +1,79 @@ +/** + * @license Use of this source code is governed by an MIT-style license that + * can be found in the LICENSE file at https://github.com/cartant/rxjs-tslint-rules + */ + +import { tsquery } from "@phenomnomnominal/tsquery"; +import * as Lint from "tslint"; +import * as tsutils from "tsutils"; +import * as ts from "typescript"; +import * as peer from "../support/peer"; +import { couldBeType, isReferenceType, isUnionType } from "../support/util"; + +export class Rule extends Lint.Rules.TypedRule { + public static metadata: Lint.IRuleMetadata = { + deprecationMessage: peer.v5 ? peer.v5NotSupportedMessage : undefined, + description: "Disallows unsafe optional `next` calls.", + options: null, + optionsDescription: "Not configurable.", + requiresTypeInfo: true, + ruleName: "rxjs-no-unsafe-subject-next", + type: "functionality", + typescriptOnly: true + }; + + public static FAILURE_STRING = "Unsafe optional next calls are forbidden"; + + public applyWithProgram( + sourceFile: ts.SourceFile, + program: ts.Program + ): Lint.RuleFailure[] { + const failures: Lint.RuleFailure[] = []; + const typeChecker = program.getTypeChecker(); + + const callExpressions = tsquery( + sourceFile, + `CallExpression[expression.name.text="next"]` + ); + callExpressions.forEach(node => { + const callExpression = node as ts.CallExpression; + const { arguments: args } = callExpression; + if (args.length === 0) { + if (tsutils.isPropertyAccessExpression(callExpression.expression)) { + const { expression, name } = callExpression.expression; + const type = typeChecker.getTypeAtLocation(expression); + if (isReferenceType(type) && couldBeType(type, "Subject")) { + const [typeArg] = typeChecker.getTypeArguments(type); + if (tsutils.isTypeFlagSet(typeArg, ts.TypeFlags.Any)) { + return; + } + if (tsutils.isTypeFlagSet(typeArg, ts.TypeFlags.Unknown)) { + return; + } + if (tsutils.isTypeFlagSet(typeArg, ts.TypeFlags.Void)) { + return; + } + if ( + isUnionType(typeArg) && + typeArg.types.some(t => + tsutils.isTypeFlagSet(t, ts.TypeFlags.Void) + ) + ) { + return; + } + failures.push( + new Lint.RuleFailure( + sourceFile, + name.getStart(), + name.getStart() + name.getWidth(), + Rule.FAILURE_STRING, + this.ruleName + ) + ); + } + } + } + }); + return failures; + } +} diff --git a/test/v6/fixtures/no-unsafe-subject-next/default/fixture.ts.lint b/test/v6/fixtures/no-unsafe-subject-next/default/fixture.ts.lint new file mode 100644 index 00000000..7486ccb0 --- /dev/null +++ b/test/v6/fixtures/no-unsafe-subject-next/default/fixture.ts.lint @@ -0,0 +1,31 @@ +import { ReplaySubject, Subject } from "rxjs"; + +const a = new Subject(); +a.next(42); +a.next(); + ~~~~ [no-unsafe-subject-next] + +const b = new Subject(); +b.next(); + +const c = new ReplaySubject(); +c.next(42); +c.next(); + ~~~~ [no-unsafe-subject-next] + +const d = new Subject(); +d.next(42); +d.next(); + +const e = new Subject(); +e.next(42); +e.next(); + +const f = new Subject(); +f.next(42); +f.next(); + +const g = new Subject(); +g.next(); + +[no-unsafe-subject-next]: Unsafe optional next calls are forbidden diff --git a/test/v6/fixtures/no-unsafe-subject-next/default/tsconfig.json b/test/v6/fixtures/no-unsafe-subject-next/default/tsconfig.json new file mode 100644 index 00000000..690be78e --- /dev/null +++ b/test/v6/fixtures/no-unsafe-subject-next/default/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "lib": ["es2015"], + "noEmit": true, + "paths": { + "rxjs": ["../../node_modules/rxjs"] + }, + "skipLibCheck": true, + "target": "es5" + }, + "include": ["fixture.ts"] +} diff --git a/test/v6/fixtures/no-unsafe-subject-next/default/tslint.json b/test/v6/fixtures/no-unsafe-subject-next/default/tslint.json new file mode 100644 index 00000000..a7e5306b --- /dev/null +++ b/test/v6/fixtures/no-unsafe-subject-next/default/tslint.json @@ -0,0 +1,8 @@ +{ + "defaultSeverity": "error", + "jsRules": {}, + "rules": { + "rxjs-no-unsafe-subject-next": { "severity": "error" } + }, + "rulesDirectory": "../../../../../build/rules" +}