diff --git a/package.json b/package.json index 85c9afb2..0d4f6b56 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "description": "TSLint rules for RxJS", "devDependencies": { + "@phenomnomnominal/tsquery": "^1.0.5", "@types/chai": "^4.0.0", "@types/decamelize": "^1.2.0", "@types/mocha": "^5.0.0", diff --git a/source/rules/rxjsJustRule.ts b/source/rules/rxjsJustRule.ts new file mode 100644 index 00000000..3d707ef0 --- /dev/null +++ b/source/rules/rxjsJustRule.ts @@ -0,0 +1,94 @@ +/** + * @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 { tsquery } from "@phenomnomnominal/tsquery"; +import { couldBeType } from "../support/util"; + +const defaultNamesRegExp = /^(canActivate|canActivateChild|canDeactivate|canLoad|intercept|resolve|validate)$/; +const defaultTypesRegExp = /^EventEmitter$/; + +export class Rule extends Lint.Rules.TypedRule { + + public static metadata: Lint.IRuleMetadata = { + description: "Enforces the use of a `just` alias for `of`.", + options: null, + optionsDescription: "Not configurable.", + requiresTypeInfo: true, + ruleName: "rxjs-just", + type: "style", + typescriptOnly: true + }; + + public static FAILURE_STRING = "Use just alias"; + + public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + + return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), program)); + } +} + +class Walker extends Lint.ProgramAwareRuleWalker { + + walk(sourceFile: ts.SourceFile): void { + + const typeChecker = this.getTypeChecker(); + let importIdentifier: ts.Identifier | undefined = undefined; + + const importDeclarations = tsquery( + sourceFile, + `ImportDeclaration:has(StringLiteral[value="rxjs"]),ImportDeclaration:has(StringLiteral[value="rxjs/observable/of"])` + ); + importDeclarations.forEach(importDeclaration => { + let importSpecifiers = tsquery( + importDeclaration, + `ImportSpecifier:has(Identifier[escapedText="of"])` + ); + importSpecifiers.forEach(importSpecifier => { + if (tsutils.isImportSpecifier(importSpecifier)) { + if (!importSpecifier.propertyName && (importSpecifier.name.getText() === "of")) { + importIdentifier = importSpecifier.name; + const fix = Lint.Replacement.replaceFromTo( + importIdentifier.getStart(), + importIdentifier.getStart() + importIdentifier.getWidth(), + "of as just" + ); + this.addFailureAtNode(importIdentifier, Rule.FAILURE_STRING, fix); + } + } + }); + }); + + if (importIdentifier) { + const callExpressions = tsquery( + sourceFile, + `CallExpression:has(Identifier[escapedText="of"])` + ); + callExpressions.forEach(callExpression => { + if (tsutils.isCallExpression(callExpression)) { + const expression = callExpression.expression; + if (tsutils.isIdentifier(expression)) { + + const symbol = typeChecker.getSymbolAtLocation(expression); + const [declaration] = symbol.getDeclarations(); + + if (declaration === importIdentifier.parent) { + const fix = Lint.Replacement.replaceFromTo( + expression.getStart(), + expression.getStart() + expression.getWidth(), + "just" + ); + this.addFailureAtNode(expression, Rule.FAILURE_STRING, fix); + } + } + } + }); + } + } +} diff --git a/test/v5/fixtures/just/default/fixture.ts.fix b/test/v5/fixtures/just/default/fixture.ts.fix new file mode 100644 index 00000000..e9c626c1 --- /dev/null +++ b/test/v5/fixtures/just/default/fixture.ts.fix @@ -0,0 +1,4 @@ +import { of as just } from "rxjs/observable/of"; + +const a = just("a"); + diff --git a/test/v5/fixtures/just/default/fixture.ts.lint b/test/v5/fixtures/just/default/fixture.ts.lint new file mode 100644 index 00000000..a8ffac1e --- /dev/null +++ b/test/v5/fixtures/just/default/fixture.ts.lint @@ -0,0 +1,7 @@ +import { of } from "rxjs/observable/of"; + ~~ [just] + +const a = of("a"); + ~~ [just] + +[just]: Use just alias diff --git a/test/v5/fixtures/just/default/tsconfig.json b/test/v5/fixtures/just/default/tsconfig.json new file mode 100644 index 00000000..690be78e --- /dev/null +++ b/test/v5/fixtures/just/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/v5/fixtures/just/default/tslint.json b/test/v5/fixtures/just/default/tslint.json new file mode 100644 index 00000000..aca02862 --- /dev/null +++ b/test/v5/fixtures/just/default/tslint.json @@ -0,0 +1,8 @@ +{ + "defaultSeverity": "error", + "jsRules": {}, + "rules": { + "rxjs-just": { "severity": "error" } + }, + "rulesDirectory": "../../../../../build/rules" +} diff --git a/test/v5/fixtures/just/user-land/fixture.ts.fix b/test/v5/fixtures/just/user-land/fixture.ts.fix new file mode 100644 index 00000000..66a32ee2 --- /dev/null +++ b/test/v5/fixtures/just/user-land/fixture.ts.fix @@ -0,0 +1,16 @@ +import { of as just } from "rxjs/observable/of"; + +function foo(): void { + function of(): void {} + of(); +} + +function bar(of: Function): void { + of(); +} + +function baz(): void { + const of = () => {}; + of(); +} + diff --git a/test/v5/fixtures/just/user-land/fixture.ts.lint b/test/v5/fixtures/just/user-land/fixture.ts.lint new file mode 100644 index 00000000..b0c4d072 --- /dev/null +++ b/test/v5/fixtures/just/user-land/fixture.ts.lint @@ -0,0 +1,18 @@ +import { of } from "rxjs/observable/of"; + ~~ [just] + +function foo(): void { + function of(): void {} + of(); +} + +function bar(of: Function): void { + of(); +} + +function baz(): void { + const of = () => {}; + of(); +} + +[just]: Use just alias diff --git a/test/v5/fixtures/just/user-land/tsconfig.json b/test/v5/fixtures/just/user-land/tsconfig.json new file mode 100644 index 00000000..690be78e --- /dev/null +++ b/test/v5/fixtures/just/user-land/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/v5/fixtures/just/user-land/tslint.json b/test/v5/fixtures/just/user-land/tslint.json new file mode 100644 index 00000000..aca02862 --- /dev/null +++ b/test/v5/fixtures/just/user-land/tslint.json @@ -0,0 +1,8 @@ +{ + "defaultSeverity": "error", + "jsRules": {}, + "rules": { + "rxjs-just": { "severity": "error" } + }, + "rulesDirectory": "../../../../../build/rules" +} diff --git a/test/v6-compat/fixtures/just/default/fixture.ts.fix b/test/v6-compat/fixtures/just/default/fixture.ts.fix new file mode 100644 index 00000000..e9c626c1 --- /dev/null +++ b/test/v6-compat/fixtures/just/default/fixture.ts.fix @@ -0,0 +1,4 @@ +import { of as just } from "rxjs/observable/of"; + +const a = just("a"); + diff --git a/test/v6-compat/fixtures/just/default/fixture.ts.lint b/test/v6-compat/fixtures/just/default/fixture.ts.lint new file mode 100644 index 00000000..a8ffac1e --- /dev/null +++ b/test/v6-compat/fixtures/just/default/fixture.ts.lint @@ -0,0 +1,7 @@ +import { of } from "rxjs/observable/of"; + ~~ [just] + +const a = of("a"); + ~~ [just] + +[just]: Use just alias diff --git a/test/v6-compat/fixtures/just/default/tsconfig.json b/test/v6-compat/fixtures/just/default/tsconfig.json new file mode 100644 index 00000000..690be78e --- /dev/null +++ b/test/v6-compat/fixtures/just/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-compat/fixtures/just/default/tslint.json b/test/v6-compat/fixtures/just/default/tslint.json new file mode 100644 index 00000000..aca02862 --- /dev/null +++ b/test/v6-compat/fixtures/just/default/tslint.json @@ -0,0 +1,8 @@ +{ + "defaultSeverity": "error", + "jsRules": {}, + "rules": { + "rxjs-just": { "severity": "error" } + }, + "rulesDirectory": "../../../../../build/rules" +} diff --git a/test/v6-compat/fixtures/just/user-land/fixture.ts.fix b/test/v6-compat/fixtures/just/user-land/fixture.ts.fix new file mode 100644 index 00000000..66a32ee2 --- /dev/null +++ b/test/v6-compat/fixtures/just/user-land/fixture.ts.fix @@ -0,0 +1,16 @@ +import { of as just } from "rxjs/observable/of"; + +function foo(): void { + function of(): void {} + of(); +} + +function bar(of: Function): void { + of(); +} + +function baz(): void { + const of = () => {}; + of(); +} + diff --git a/test/v6-compat/fixtures/just/user-land/fixture.ts.lint b/test/v6-compat/fixtures/just/user-land/fixture.ts.lint new file mode 100644 index 00000000..b0c4d072 --- /dev/null +++ b/test/v6-compat/fixtures/just/user-land/fixture.ts.lint @@ -0,0 +1,18 @@ +import { of } from "rxjs/observable/of"; + ~~ [just] + +function foo(): void { + function of(): void {} + of(); +} + +function bar(of: Function): void { + of(); +} + +function baz(): void { + const of = () => {}; + of(); +} + +[just]: Use just alias diff --git a/test/v6-compat/fixtures/just/user-land/tsconfig.json b/test/v6-compat/fixtures/just/user-land/tsconfig.json new file mode 100644 index 00000000..690be78e --- /dev/null +++ b/test/v6-compat/fixtures/just/user-land/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-compat/fixtures/just/user-land/tslint.json b/test/v6-compat/fixtures/just/user-land/tslint.json new file mode 100644 index 00000000..aca02862 --- /dev/null +++ b/test/v6-compat/fixtures/just/user-land/tslint.json @@ -0,0 +1,8 @@ +{ + "defaultSeverity": "error", + "jsRules": {}, + "rules": { + "rxjs-just": { "severity": "error" } + }, + "rulesDirectory": "../../../../../build/rules" +} diff --git a/test/v6/fixtures/just/default/fixture.ts.fix b/test/v6/fixtures/just/default/fixture.ts.fix new file mode 100644 index 00000000..b140d7d7 --- /dev/null +++ b/test/v6/fixtures/just/default/fixture.ts.fix @@ -0,0 +1,4 @@ +import { of as just } from "rxjs"; + +const a = just("a"); + diff --git a/test/v6/fixtures/just/default/fixture.ts.lint b/test/v6/fixtures/just/default/fixture.ts.lint new file mode 100644 index 00000000..66948595 --- /dev/null +++ b/test/v6/fixtures/just/default/fixture.ts.lint @@ -0,0 +1,7 @@ +import { of } from "rxjs"; + ~~ [just] + +const a = of("a"); + ~~ [just] + +[just]: Use just alias diff --git a/test/v6/fixtures/just/default/tsconfig.json b/test/v6/fixtures/just/default/tsconfig.json new file mode 100644 index 00000000..690be78e --- /dev/null +++ b/test/v6/fixtures/just/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/just/default/tslint.json b/test/v6/fixtures/just/default/tslint.json new file mode 100644 index 00000000..aca02862 --- /dev/null +++ b/test/v6/fixtures/just/default/tslint.json @@ -0,0 +1,8 @@ +{ + "defaultSeverity": "error", + "jsRules": {}, + "rules": { + "rxjs-just": { "severity": "error" } + }, + "rulesDirectory": "../../../../../build/rules" +} diff --git a/test/v6/fixtures/just/user-land/fixture.ts.fix b/test/v6/fixtures/just/user-land/fixture.ts.fix new file mode 100644 index 00000000..8378b89d --- /dev/null +++ b/test/v6/fixtures/just/user-land/fixture.ts.fix @@ -0,0 +1,16 @@ +import { of as just } from "rxjs"; + +function foo(): void { + function of(): void {} + of(); +} + +function bar(of: Function): void { + of(); +} + +function baz(): void { + const of = () => {}; + of(); +} + diff --git a/test/v6/fixtures/just/user-land/fixture.ts.lint b/test/v6/fixtures/just/user-land/fixture.ts.lint new file mode 100644 index 00000000..01e900c5 --- /dev/null +++ b/test/v6/fixtures/just/user-land/fixture.ts.lint @@ -0,0 +1,18 @@ +import { of } from "rxjs"; + ~~ [just] + +function foo(): void { + function of(): void {} + of(); +} + +function bar(of: Function): void { + of(); +} + +function baz(): void { + const of = () => {}; + of(); +} + +[just]: Use just alias diff --git a/test/v6/fixtures/just/user-land/tsconfig.json b/test/v6/fixtures/just/user-land/tsconfig.json new file mode 100644 index 00000000..690be78e --- /dev/null +++ b/test/v6/fixtures/just/user-land/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/just/user-land/tslint.json b/test/v6/fixtures/just/user-land/tslint.json new file mode 100644 index 00000000..aca02862 --- /dev/null +++ b/test/v6/fixtures/just/user-land/tslint.json @@ -0,0 +1,8 @@ +{ + "defaultSeverity": "error", + "jsRules": {}, + "rules": { + "rxjs-just": { "severity": "error" } + }, + "rulesDirectory": "../../../../../build/rules" +} diff --git a/yarn.lock b/yarn.lock index 4fa36d2c..4ac68173 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,12 @@ # yarn lockfile v1 +"@phenomnomnominal/tsquery@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-1.0.5.tgz#ba75d8e403f23a67b49d983b763918f6505441e4" + dependencies: + esquery "^1.0.1" + "@types/chai@^4.0.0": version "4.1.3" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.3.tgz#b8a74352977a23b604c01aa784f5b793443fb7dc" @@ -11,8 +17,8 @@ resolved "https://registry.yarnpkg.com/@types/decamelize/-/decamelize-1.2.0.tgz#aabc2306e6c229356636cab7fcb823cfe170f056" "@types/mocha@^5.0.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.0.tgz#b3c8e69f038835db1a7fdc0b3d879fc50506e29e" + version "5.2.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.1.tgz#465450aaf5cec6f7d35523748c6cc89a5e222dc5" "@types/node@*", "@types/node@^10.0.0": version "10.3.0" @@ -160,6 +166,16 @@ esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" +esquery@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + dependencies: + estraverse "^4.0.0" + +estraverse@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -217,8 +233,8 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" js-yaml@^3.7.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" + version "3.12.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: argparse "^1.0.7" esprima "^4.0.0"