Skip to content

Commit

Permalink
feat(no-unsafe-scope): Add rule.
Browse files Browse the repository at this point in the history
  • Loading branch information
cartant committed Mar 30, 2018
1 parent bf9dc19 commit da22b6d
Show file tree
Hide file tree
Showing 15 changed files with 355 additions and 0 deletions.
1 change: 1 addition & 0 deletions fixtures/no-unsafe-scope/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const THOUSAND = 1000;
4 changes: 4 additions & 0 deletions fixtures/no-unsafe-scope/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum Vertical {
Up = 1,
Down
}
11 changes: 11 additions & 0 deletions fixtures/no-unsafe-scope/fixture-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators/map";
import { THOUSAND } from "./constants";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";

const HUNDRED = 100;

Observable.of(1).map(value => value * HUNDRED * THOUSAND).subscribe();
of(1).pipe(map(value => value * HUNDRED * THOUSAND)).subscribe();
9 changes: 9 additions & 0 deletions fixtures/no-unsafe-scope/fixture-do.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { tap } from "rxjs/operators/tap";
import "rxjs/add/observable/of";
import "rxjs/add/operator/do";

let outer: any;
Observable.of(1).do(value => outer = value).subscribe();
of(1).pipe(tap(value => outer = value)).subscribe();
19 changes: 19 additions & 0 deletions fixtures/no-unsafe-scope/fixture-enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators/map";
import { Vertical } from "./enums";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";

enum Horizontal {
Left = 1,
Right
}

Observable.of(1)
.map(() => Horizontal.Left)
.map(() => Vertical.Up).subscribe();
of(1).pipe(
map(() => Horizontal.Left),
map(() => Vertical.Up)
).subscribe();
9 changes: 9 additions & 0 deletions fixtures/no-unsafe-scope/fixture-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators/map";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";

let outer: any;
Observable.of(1).map(function (value) { return outer = value; }).subscribe();
of(1).pipe(map(function (value) { return outer = value; })).subscribe();
9 changes: 9 additions & 0 deletions fixtures/no-unsafe-scope/fixture-globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators/map";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";

let outer: any;
Observable.of("1").map(value => parseInt(value, 10)).subscribe();
of("1").pipe(map(value => parseInt(value, 10))).subscribe();
9 changes: 9 additions & 0 deletions fixtures/no-unsafe-scope/fixture-math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators/map";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";

let outer: any;
Observable.of(1).map(value => Math.ceil(value)).subscribe();
of(1).pipe(map(value => Math.ceil(value))).subscribe();
18 changes: 18 additions & 0 deletions fixtures/no-unsafe-scope/fixture-safe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators/map";
import { mergeMap } from "rxjs/operators/mergeMap";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";
import "rxjs/add/operator/mergeMap";

Observable.of(1)
.mergeMap(value => Observable.of(null)
.map(() => value)
).subscribe();

of(1).pipe(
mergeMap(value => of(null).pipe(
map(() => value)
))
).subscribe();
12 changes: 12 additions & 0 deletions fixtures/no-unsafe-scope/fixture-this.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators/map";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";

class User {
constructor(private name: string) {
Observable.of("Hello").map(value => `${value}, ${this.name}.`).subscribe();
of("Hello").pipe(map(value => `${value}, ${this.name}.`)).subscribe();
}
}
9 changes: 9 additions & 0 deletions fixtures/no-unsafe-scope/fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Observable } from "rxjs";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators/map";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";

let outer: any;
Observable.of(1).map(value => outer = value).subscribe();
of(1).pipe(map(value => outer = value)).subscribe();
23 changes: 23 additions & 0 deletions fixtures/no-unsafe-scope/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"baseUrl": ".",
"lib": ["es2015"],
"noEmit": true,
"paths": {
"rxjs": ["../node_modules/rxjs"]
},
"skipLibCheck": true,
"target": "es5"
},
"include": [
"fixture.ts",
"fixture-constants.ts",
"fixture-do.ts",
"fixture-enums.ts",
"fixture-functions.ts",
"fixture-globals.ts",
"fixture-math.ts",
"fixture-safe.ts",
"fixture-this.ts"
]
}
8 changes: 8 additions & 0 deletions fixtures/no-unsafe-scope/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"defaultSeverity": "error",
"jsRules": {},
"rules": {
"rxjs-no-unsafe-scope": { "severity": "error" }
},
"rulesDirectory": "../../build/rules"
}
69 changes: 69 additions & 0 deletions source/fixtures-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,75 @@ describe("fixtures", function (): void {
});
});

describe.only("no-unsafe-scope", () => {

it("should effect 'rxjs-no-unsafe-scope' errors", () => {

const result = lint("no-unsafe-scope", "tslint.json");

expect(result).to.have.property("errorCount", 2);
result.failures.forEach(failure => expect(failure).to.have.property("ruleName", "rxjs-no-unsafe-scope"));
});

it("should effect 'rxjs-no-unsafe-scope' errors for non-arrow functions", () => {

const result = lint("no-unsafe-scope", "tslint.json", "fixture-functions.ts");

expect(result).to.have.property("errorCount", 2);
result.failures.forEach(failure => expect(failure).to.have.property("ruleName", "rxjs-no-unsafe-scope"));
});

it("should not effect 'rxjs-no-unsafe-scope' errors for safe usage", () => {

const result = lint("no-unsafe-scope", "tslint.json", "fixture-safe.ts");

expect(result).to.have.property("errorCount", 0);
});

it("should not effect 'rxjs-no-unsafe-scope' errors for do/tap", () => {

const result = lint("no-unsafe-scope", "tslint.json", "fixture-do.ts");

expect(result).to.have.property("errorCount", 0);
});

it("should not effect 'rxjs-no-unsafe-scope' errors for globals", () => {

const result = lint("no-unsafe-scope", "tslint.json", "fixture-globals.ts");

expect(result).to.have.property("errorCount", 0);
});

it("should not effect 'rxjs-no-unsafe-scope' errors for Math", () => {

const result = lint("no-unsafe-scope", "tslint.json", "fixture-math.ts");

expect(result).to.have.property("errorCount", 0);
});

it("should not effect 'rxjs-no-unsafe-scope' errors for constants", () => {

const result = lint("no-unsafe-scope", "tslint.json", "fixture-constants.ts");

expect(result).to.have.property("errorCount", 0);
});

it("should not effect 'rxjs-no-unsafe-scope' errors for enums", () => {

const result = lint("no-unsafe-scope", "tslint.json", "fixture-enums.ts");

expect(result).to.have.property("errorCount", 0);
});

it("should effect 'rxjs-no-unsafe-scope' errors for this", () => {

const result = lint("no-unsafe-scope", "tslint.json", "fixture-this.ts");

expect(result).to.have.property("errorCount", 2);
result.failures.forEach(failure => expect(failure).to.have.property("ruleName", "rxjs-no-unsafe-scope"));
});
});

describe("throw-error", () => {

it("should effect 'rxjs-throw-error' errors", () => {
Expand Down
145 changes: 145 additions & 0 deletions source/rules/rxjsNoUnsafeScopeRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* @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
*/
/*tslint:disable:no-use-before-declare*/

import * as Lint from "tslint";
import * as ts from "typescript";
import * as tsutils from "tsutils";
import { knownOperators, knownPipeableOperators } from "../support/knowns";
import { couldBeType } from "../support/util";

export class Rule extends Lint.Rules.TypedRule {

public static metadata: Lint.IRuleMetadata = {
description: "Disallows unsafe scopes.",
options: null,
optionsDescription: "Not configurable.",
requiresTypeInfo: true,
ruleName: "rxjs-no-unsafe-scopes",
type: "functionality",
typescriptOnly: true
};

public static FAILURE_STRING = "Unsafe scopes are forbidden";

public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {

return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), program));
}
}

class Walker extends Lint.ProgramAwareRuleWalker {

private callbackMap: Map<ts.Node, string> = new Map<ts.Node, string>();
private callbackStack: (ts.ArrowFunction | ts.FunctionExpression)[] = [];

protected visitArrowFunction(node: ts.ArrowFunction): void {

if (this.callbackMap.has(node)) {
this.callbackStack.push(node);
super.visitArrowFunction(node);
this.callbackStack.pop();
} else {
super.visitArrowFunction(node);
}
}

protected visitCallExpression(node: ts.CallExpression): void {

const { arguments: args, expression } = node;
let name: string;

if (tsutils.isIdentifier(expression)) {
name = expression.getText();
} else if (tsutils.isPropertyAccessExpression(expression)) {
const { name: propertyName } = expression;
name = propertyName.getText();
}

if (name && (knownOperators[name] || knownPipeableOperators[name])) {
const callbacks = args.filter(arg => tsutils.isArrowFunction(arg) || tsutils.isFunctionExpression(arg));
callbacks.forEach(callback => this.callbackMap.set(callback, name));
super.visitCallExpression(node);
callbacks.forEach(callback => this.callbackMap.delete(callback));
} else {
super.visitCallExpression(node);
}
}

protected visitFunctionExpression(node: ts.FunctionExpression): void {

if (this.callbackMap.has(node)) {
this.callbackStack.push(node);
super.visitFunctionExpression(node);
this.callbackStack.pop();
} else {
super.visitFunctionExpression(node);
}
}

protected visitNode(node: ts.Node): void {

if (this.callbackStack.length) {
const validateNode = tsutils.isIdentifier(node) || isThis(node);
if (validateNode && this.isUnsafe(node)) {
this.addFailureAtNode(node, Rule.FAILURE_STRING);
}
}
super.visitNode(node);
}

private isUnsafe(node: ts.Node): boolean {

const { callbackMap, callbackStack } = this;
const leafCallback = callbackStack[callbackStack.length - 1];
const rootCallback = callbackStack[0];

if (/(do|tap)/.test(callbackMap.get(leafCallback))) {
return false;
}

const typeChecker = this.getTypeChecker();
const symbol = typeChecker.getSymbolAtLocation(node);
const [declaration] = symbol.getDeclarations();

if ((declaration.pos >= rootCallback.pos) && (declaration.pos < rootCallback.end)) {
return false;
}
if (tsutils.isCallExpression(node.parent)) {
return false;
}
if (tsutils.isNewExpression(node.parent)) {
return false;
}
if (tsutils.isPropertyAccessExpression(node.parent)) {
if (node === node.parent.name) {
return false;
} else if (tsutils.isCallExpression(node.parent.parent)) {
return false;
}
const type = typeChecker.getTypeAtLocation(node.parent.name);
/*tslint:disable-next-line:no-bitwise*/
if ((type.flags & ts.TypeFlags.EnumLiteral) !== 0) {
return false;
}
}

if (tsutils.isVariableDeclarationList(declaration.parent)) {
if (tsutils.getVariableDeclarationKind(declaration.parent) === tsutils.VariableDeclarationKind.Const) {
return false;
}
}

if (tsutils.isImportSpecifier(declaration)) {
return false;
}

return true;
}
}

function isThis(node: ts.Node): boolean {
return node.kind === ts.SyntaxKind.ThisKeyword;
}

0 comments on commit da22b6d

Please sign in to comment.