UNPKG

7.3 kBPlain TextView Raw
1// tslint:disable try-catch-first
2import * as ts from 'typescript';
3import * as Lint from 'tslint';
4import { ErrorTolerantWalker } from './ErrorTolerantWalker';
5import { AstUtils } from './AstUtils';
6import { Scope } from './Scope';
7
8/**
9 * This exists so that you can try to tell the types of variables
10 * and identifiers in the current scope. It builds the current scope
11 * from the SourceFile then -> Module -> Class -> Function
12 */
13export class ScopedSymbolTrackingWalker extends ErrorTolerantWalker {
14 private typeChecker?: ts.TypeChecker;
15 private scope: Scope;
16
17 constructor(sourceFile: ts.SourceFile, options: Lint.IOptions, program?: ts.Program) {
18 super(sourceFile, options);
19
20 if (program) {
21 this.typeChecker = program.getTypeChecker();
22 }
23 }
24
25 protected isExpressionEvaluatingToFunction(expression: ts.Expression): boolean {
26 if (expression.kind === ts.SyntaxKind.ArrowFunction || expression.kind === ts.SyntaxKind.FunctionExpression) {
27 return true; // arrow function literals and arrow functions are definitely functions
28 }
29
30 const isString =
31 expression.kind === ts.SyntaxKind.StringLiteral ||
32 expression.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral ||
33 expression.kind === ts.SyntaxKind.TemplateExpression ||
34 expression.kind === ts.SyntaxKind.TaggedTemplateExpression;
35 if (isString || expression.kind === ts.SyntaxKind.BinaryExpression) {
36 return false; // strings and binary expressions are definitely not functions
37 }
38
39 // is the symbol something we are tracking in scope ourselves?
40 if (this.scope.isFunctionSymbol(expression.getText())) {
41 return true;
42 }
43
44 if (expression.kind === ts.SyntaxKind.Identifier && this.typeChecker) {
45 const tsSymbol = this.typeChecker.getSymbolAtLocation(expression);
46 if (tsSymbol && tsSymbol.flags === ts.SymbolFlags.Function) {
47 return true; // variables with type function are OK to pass
48 }
49 return false;
50 }
51
52 if (expression.kind === ts.SyntaxKind.CallExpression) {
53 // calling Function.bind is a special case that makes tslint throw an exception
54 if ((<any>expression).expression.name && (<any>expression).expression.name.getText() === 'bind') {
55 return true; // for now assume invoking a function named bind returns a function. Follow up with tslint.
56 }
57
58 try {
59 // seems like another tslint error of some sort
60 if (!this.typeChecker) {
61 return true;
62 }
63
64 const signature: ts.Signature = this.typeChecker.getResolvedSignature(<ts.CallExpression>expression);
65 const expressionType: ts.Type = this.typeChecker.getReturnTypeOfSignature(signature);
66 return this.isFunctionType(expressionType, this.typeChecker);
67 } catch (error) {
68 // this exception is only thrown in unit tests, not the node debugger :(
69 return false;
70 }
71 }
72
73 if (!this.typeChecker) {
74 return true;
75 }
76
77 return this.isFunctionType(this.typeChecker.getTypeAtLocation(expression), this.typeChecker);
78 }
79
80 private isFunctionType(expressionType: ts.Type, typeChecker: ts.TypeChecker): boolean {
81 const signatures: ts.Signature[] = typeChecker.getSignaturesOfType(expressionType, ts.SignatureKind.Call);
82 if (signatures != null && signatures.length > 0) {
83 const signatureDeclaration: ts.SignatureDeclaration = signatures[0].declaration;
84 if (signatureDeclaration.kind === ts.SyntaxKind.FunctionType) {
85 return true; // variables of type function are allowed to be passed as parameters
86 }
87 }
88 return false;
89 }
90
91 protected visitArrowFunction(node: ts.ArrowFunction): void {
92 this.scope = new Scope(this.scope);
93 this.scope.addParameters(node.parameters);
94 super.visitArrowFunction(node);
95 this.scope = this.scope.parent;
96 }
97
98 protected visitClassDeclaration(node: ts.ClassDeclaration): void {
99 this.scope = new Scope(this.scope);
100 node.members.forEach(
101 (element: ts.ClassElement): void => {
102 const prefix: string = AstUtils.isStatic(element) ? node.name.getText() + '.' : 'this.';
103
104 if (element.kind === ts.SyntaxKind.MethodDeclaration) {
105 // add all declared methods as valid functions
106 this.scope.addFunctionSymbol(prefix + (<ts.MethodDeclaration>element).name.getText());
107 } else if (element.kind === ts.SyntaxKind.PropertyDeclaration) {
108 const prop: ts.PropertyDeclaration = <ts.PropertyDeclaration>element;
109 // add all declared function properties as valid functions
110 if (AstUtils.isDeclarationFunctionType(prop)) {
111 this.scope.addFunctionSymbol(prefix + (<ts.MethodDeclaration>element).name.getText());
112 } else {
113 this.scope.addNonFunctionSymbol(prefix + (<ts.MethodDeclaration>element).name.getText());
114 }
115 }
116 }
117 );
118 super.visitClassDeclaration(node);
119 this.scope = this.scope.parent;
120 }
121
122 protected visitConstructorDeclaration(node: ts.ConstructorDeclaration): void {
123 this.scope = new Scope(this.scope);
124 this.scope.addParameters(node.parameters);
125 super.visitConstructorDeclaration(node);
126 this.scope = this.scope.parent;
127 }
128
129 protected visitFunctionDeclaration(node: ts.FunctionDeclaration): void {
130 this.scope = new Scope(this.scope);
131 this.scope.addParameters(node.parameters);
132 super.visitFunctionDeclaration(node);
133 this.scope = this.scope.parent;
134 }
135
136 protected visitFunctionExpression(node: ts.FunctionExpression): void {
137 this.scope = new Scope(this.scope);
138 this.scope.addParameters(node.parameters);
139 super.visitFunctionExpression(node);
140 this.scope = this.scope.parent;
141 }
142
143 protected visitMethodDeclaration(node: ts.MethodDeclaration): void {
144 this.scope = new Scope(this.scope);
145 this.scope.addParameters(node.parameters);
146 super.visitMethodDeclaration(node);
147 this.scope = this.scope.parent;
148 }
149
150 protected visitModuleDeclaration(node: ts.ModuleDeclaration): void {
151 this.scope = new Scope(this.scope);
152 this.scope.addGlobalScope(node.body, this.getSourceFile(), this.getOptions());
153 super.visitModuleDeclaration(node);
154 this.scope = this.scope.parent;
155 }
156
157 protected visitSetAccessor(node: ts.AccessorDeclaration): void {
158 this.scope = new Scope(this.scope);
159 this.scope.addParameters(node.parameters);
160 super.visitSetAccessor(node);
161 this.scope = this.scope.parent;
162 }
163
164 protected visitSourceFile(node: ts.SourceFile): void {
165 this.scope = new Scope(null);
166 this.scope.addGlobalScope(node, node, this.getOptions());
167 super.visitSourceFile(node);
168 this.scope = null;
169 }
170}