forked from ReactiveX/rxjs-tslint
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request ReactiveX#13 from ReactiveX/minko/observable-static
feat(rules): migrate static operations
- Loading branch information
Showing
9 changed files
with
287 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,6 @@ dist | |
npm-debug.log | ||
.vscode | ||
yarn.lock | ||
demo.ts | ||
tsconfig-demo.json | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,29 +7,25 @@ | |
"docs": "ts-node build/buildDocs.ts", | ||
"lint": "tslint -c tslint.json \"src/**/*.ts\" \"test/**/*.ts\"", | ||
"lint:fix": "npm run lint -- --fix", | ||
"release": "npm run build && rimraf dist && tsc -p tsconfig-release.json && npm run copy:common && npm run prepare:package && BUILD_TYPE=prod npm run set:vars", | ||
"release": | ||
"npm run build && rimraf dist && tsc -p tsconfig-release.json && npm run copy:common && npm run prepare:package && BUILD_TYPE=prod npm run set:vars", | ||
"build": "rimraf dist && tsc && npm run lint && npm t", | ||
"copy:common": "cp README.md dist", | ||
"prepare:package": "cat package.json | ts-node build/package.ts > dist/package.json", | ||
"test": "rimraf dist && tsc && mocha -R nyan dist/test --recursive", | ||
"test:watch": "rimraf dist && tsc && BUILD_TYPE=dev npm run set:vars && mocha -R nyan dist/test --watch --recursive", | ||
"test:watch": | ||
"rimraf dist && tsc && BUILD_TYPE=dev npm run set:vars && mocha -R nyan dist/test --watch --recursive", | ||
"set:vars": "ts-node build/vars.ts --src ./dist", | ||
"tscv": "tsc --version", | ||
"tsc": "tsc", | ||
"tsc:watch": "tsc --w" | ||
}, | ||
"contributors": [ | ||
"Minko Gechev <[email protected]>" | ||
], | ||
"contributors": ["Minko Gechev <[email protected]>"], | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/mgechev/tslint-rules.git" | ||
}, | ||
"keywords": [ | ||
"rxjs", | ||
"lint", | ||
"tslint" | ||
], | ||
"keywords": ["rxjs", "lint", "tslint"], | ||
"author": { | ||
"name": "Minko Gechev", | ||
"email": "[email protected]" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { Rule as CollapseRxjsImports } from './collapseRxjsImportsRule'; | ||
export { Rule as UpdateRxjsImports } from './updateRxjsImportsRule'; | ||
export { Rule as MigrateToPipeableOperators } from './migrateToPipeableOperatorsRule'; | ||
export { Rule as MigrateStaticObservableMethods } from './migrateStaticObservableMethodsRule'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
import * as Lint from 'tslint'; | ||
import * as tsutils from 'tsutils'; | ||
import * as ts from 'typescript'; | ||
import { subtractSets, concatSets, isObservable, returnsObservable, computeInsertionIndexForImports } from './utils'; | ||
|
||
/** | ||
* A typed TSLint rule that inspects observable | ||
* static methods and turns them into function calls. | ||
*/ | ||
export class Rule extends Lint.Rules.TypedRule { | ||
static metadata: Lint.IRuleMetadata = { | ||
ruleName: 'migrate-static-observable-methods', | ||
description: 'Updates the static methods of the Observable class.', | ||
optionsDescription: '', | ||
options: null, | ||
typescriptOnly: true, | ||
type: 'functionality' | ||
}; | ||
static IMPORT_FAILURE_STRING = 'prefer operator imports with no side-effects'; | ||
static OBSERVABLE_FAILURE_STRING = 'prefer function calls'; | ||
|
||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { | ||
const failure = this.applyWithFunction(sourceFile, ctx => this.walk(ctx, program)); | ||
return failure; | ||
} | ||
private walk(ctx: Lint.WalkContext<void>, program: ts.Program) { | ||
this.removePatchedOperatorImports(ctx); | ||
const sourceFile = ctx.sourceFile; | ||
const typeChecker = program.getTypeChecker(); | ||
const insertionStart = computeInsertionIndexForImports(sourceFile); | ||
let rxjsOperatorImports = new Set<OperatorWithAlias>( | ||
Array.from(findImportedRxjsOperators(sourceFile)).map(o => OPERATOR_WITH_ALIAS_MAP[o]) | ||
); | ||
|
||
function checkPatchableOperatorUsage(node: ts.Node) { | ||
if (!isRxjsStaticOperatorCallExpression(node, typeChecker)) { | ||
return ts.forEachChild(node, checkPatchableOperatorUsage); | ||
} | ||
|
||
const callExpr = node as ts.CallExpression; | ||
if (!tsutils.isPropertyAccessExpression(callExpr.expression)) { | ||
return ts.forEachChild(node, checkPatchableOperatorUsage); | ||
} | ||
|
||
const propAccess = callExpr.expression as ts.PropertyAccessExpression; | ||
const name = propAccess.name.getText(sourceFile); | ||
const operatorName = OPERATOR_RENAMES[name] || name; | ||
const start = propAccess.getStart(sourceFile); | ||
const end = propAccess.getEnd(); | ||
const operatorsToImport = new Set<OperatorWithAlias>([OPERATOR_WITH_ALIAS_MAP[operatorName]]); | ||
const operatorsToAdd = subtractSets(operatorsToImport, rxjsOperatorImports); | ||
const imports = createImportReplacements(operatorsToAdd, insertionStart); | ||
rxjsOperatorImports = concatSets(rxjsOperatorImports, operatorsToAdd); | ||
ctx.addFailure( | ||
start, | ||
end, | ||
Rule.OBSERVABLE_FAILURE_STRING, | ||
[Lint.Replacement.replaceFromTo(start, end, operatorAlias(operatorName))].concat(imports) | ||
); | ||
return ts.forEachChild(node, checkPatchableOperatorUsage); | ||
} | ||
|
||
return ts.forEachChild(ctx.sourceFile, checkPatchableOperatorUsage); | ||
} | ||
|
||
private removePatchedOperatorImports(ctx: Lint.WalkContext<void>): void { | ||
const sourceFile = ctx.sourceFile; | ||
for (const importStatement of sourceFile.statements.filter(tsutils.isImportDeclaration)) { | ||
const moduleSpecifier = importStatement.moduleSpecifier.getText(); | ||
if (!moduleSpecifier.startsWith(`'rxjs/add/observable/`)) { | ||
continue; | ||
} | ||
const importStatementStart = importStatement.getStart(sourceFile); | ||
const importStatementEnd = importStatement.getEnd(); | ||
ctx.addFailure( | ||
importStatementStart, | ||
importStatementEnd, | ||
Rule.IMPORT_FAILURE_STRING, | ||
Lint.Replacement.deleteFromTo(importStatementStart, importStatementEnd) | ||
); | ||
} | ||
} | ||
} | ||
|
||
function isRxjsStaticOperator(node: ts.PropertyAccessExpression) { | ||
return 'Observable' === node.expression.getText() && RXJS_OPERATORS.has(node.name.getText()); | ||
} | ||
|
||
function isRxjsStaticOperatorCallExpression(node: ts.Node, typeChecker: ts.TypeChecker) { | ||
// Expression is of the form fn() | ||
if (!tsutils.isCallExpression(node)) { | ||
return false; | ||
} | ||
// Expression is of the form foo.fn | ||
if (!tsutils.isPropertyAccessExpression(node.expression)) { | ||
return false; | ||
} | ||
// fn is one of RxJs instance operators | ||
if (!isRxjsStaticOperator(node.expression)) { | ||
return false; | ||
} | ||
// fn(): k. Checks if k is an observable. Required to distinguish between | ||
// array operators with same name as RxJs operators. | ||
if (!returnsObservable(node, typeChecker)) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
function findImportedRxjsOperators(sourceFile: ts.SourceFile): Set<string> { | ||
return new Set<string>( | ||
sourceFile.statements.filter(tsutils.isImportDeclaration).reduce((current, decl) => { | ||
if (!decl.importClause) { | ||
return current; | ||
} | ||
if (!decl.moduleSpecifier.getText().startsWith(`'rxjs'`)) { | ||
return current; | ||
} | ||
if (!decl.importClause.namedBindings) { | ||
return current; | ||
} | ||
const bindings = decl.importClause.namedBindings; | ||
if (ts.isNamedImports(bindings)) { | ||
return [ | ||
...current, | ||
...(Array.from(bindings.elements) || []).map(element => { | ||
return element.name.getText(); | ||
}) | ||
]; | ||
} | ||
return current; | ||
}, []) | ||
); | ||
} | ||
|
||
function operatorAlias(operator: string) { | ||
return 'observable' + operator[0].toUpperCase() + operator.substring(1, operator.length); | ||
} | ||
|
||
function createImportReplacements(operatorsToAdd: Set<OperatorWithAlias>, startIndex: number): Lint.Replacement[] { | ||
return [...Array.from(operatorsToAdd.values())].map(tuple => | ||
Lint.Replacement.appendText(startIndex, `\nimport {${tuple.operator} as ${tuple.alias}} from 'rxjs';\n`) | ||
); | ||
} | ||
|
||
/* | ||
* https://github.com/ReactiveX/rxjs/tree/master/compat/add/observable | ||
*/ | ||
const RXJS_OPERATORS = new Set([ | ||
'bindCallback', | ||
'bindNodeCallback', | ||
'combineLatest', | ||
'concat', | ||
'defer', | ||
'empty', | ||
'forkJoin', | ||
'from', | ||
'fromEvent', | ||
'fromEventPattern', | ||
'fromPromise', | ||
'generate', | ||
'if', | ||
'interval', | ||
'merge', | ||
'never', | ||
'of', | ||
'onErrorResumeNext', | ||
'pairs', | ||
'rase', | ||
'range', | ||
'throw', | ||
'timer', | ||
'using', | ||
'zip' | ||
]); | ||
|
||
// Not handling NEVER and EMPTY | ||
const OPERATOR_RENAMES: { [key: string]: string } = { | ||
throw: 'throwError', | ||
if: 'iif', | ||
fromPromise: 'from' | ||
}; | ||
|
||
type OperatorWithAlias = { operator: string; alias: string }; | ||
type OperatorWithAliasMap = { [key: string]: OperatorWithAlias }; | ||
|
||
const OPERATOR_WITH_ALIAS_MAP: OperatorWithAliasMap = Array.from(RXJS_OPERATORS).reduce((a, o) => { | ||
const operatorName = OPERATOR_RENAMES[o] || o; | ||
a[operatorName] = { | ||
operator: operatorName, | ||
alias: operatorAlias(operatorName) | ||
}; | ||
return a; | ||
}, {}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.