diff --git a/package.json b/package.json index 09a5a1e..73132ee 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev" }, "dependencies": { + "bind-decorator": "^1.0.11", "tslib": "^1.8.1" }, "engines": { diff --git a/tsconfig.json b/tsconfig.json index 6c6d1be..951519d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "lib": ["es2016"], "skipLibCheck": true, "declaration": true, - "importHelpers": true + "importHelpers": true, + "experimentalDecorators": true }, "exclude": [ "node_modules", diff --git a/tslint.json b/tslint.json index a4b97ff..367396b 100644 --- a/tslint.json +++ b/tslint.json @@ -113,7 +113,8 @@ "switch-final-break": true, "trailing-comma": [true, { "singleline": "never", - "multiline": "always" + "multiline": "always", + "esSpecCompliant": true }], "triple-equals": [true, "allow-null-check"], "typedef-whitespace": [true, { diff --git a/util/index.ts b/util/index.ts index 9e93ff6..eab7c99 100644 --- a/util/index.ts +++ b/util/index.ts @@ -3,3 +3,4 @@ export * from './usage'; export * from './control-flow'; export * from './type'; export * from './convert-ast'; +export * from './resolver'; diff --git a/util/resolver.ts b/util/resolver.ts new file mode 100644 index 0000000..cfde8c2 --- /dev/null +++ b/util/resolver.ts @@ -0,0 +1,1070 @@ +import * as ts from 'typescript'; +import { + ScopeBoundarySelector, + isScopeBoundary, + isBlockScopedVariableDeclarationList, + getPropertyName, + getDeclarationOfBindingElement, + ScopeBoundary, + isBlockScopeBoundary, + isNodeKind, + isAmbientModule, +} from './util'; +import { getUsageDomain, getDeclarationDomain } from './usage'; +import bind from 'bind-decorator'; +import { isExportSpecifier, isInterfaceDeclaration, isClassDeclaration, isFunctionDeclaration } from '../typeguard'; + +export enum Domain { + None = 0, + Namespace = 1 << 0, + Type = 1 << 1, + Value = 1 << 2, + Any = Type | Value | Namespace, + ValueOrNamespace = Value | Namespace, + // @internal + Lazy = 1 << 3, + /** Mark this use as unsafe as we cannot know for sure if it really resolves to a given declaration. */ + // @internal + DoNotUse = 1 << 4, +} + +interface Declaration { + name: string; + node: ts.NamedDeclaration | undefined; + domain: Domain; + selector: ScopeBoundarySelector; +} +interface Symbol { + name: string; + domain: Domain; + declarations: Declaration[]; +} +export interface Use { + location: ts.Identifier | ts.SuperExpression | ts.ThisExpression | ts.ThisTypeNode; + domain: Domain; +} + +type TypeCheckerFactory = () => ts.TypeChecker | undefined; +export type TypeCheckerOrFactory = + | ts.TypeChecker + | {getTypeChecker(): ts.TypeChecker | undefined} + | {program: ts.Program | undefined} + | (() => ts.TypeChecker | ts.Program | undefined); + +export interface Resolver { + findReferences(declaration: ts.Identifier, domain?: Domain, getChecker?: TypeCheckerOrFactory): Use[] | undefined; + findDeclarations(use: ts.Identifier, getChecker?: TypeCheckerOrFactory): ts.NamedDeclaration[] | undefined; +} + +export function createResolver(): Resolver { + return new ResolverImpl(); +} + +function makeCheckerFactory(checkerOrFactory: TypeCheckerOrFactory | undefined): TypeCheckerFactory { + let checker: ts.TypeChecker | undefined | null = null; // tslint:disable-line:no-null-keyword + return getChecker; + function getChecker() { + if (checker === null) + checker = createChecker(checkerOrFactory); + return checker; + } +} + +function createChecker(checkerOrFactory: TypeCheckerOrFactory | undefined): ts.TypeChecker | undefined { + if (checkerOrFactory === undefined) + return; + let result: {getTypeChecker(): ts.TypeChecker | undefined} | ts.TypeChecker | undefined; + if (typeof checkerOrFactory === 'function') { + result = checkerOrFactory(); + } else if ('program' in checkerOrFactory) { + result = checkerOrFactory.program; + } else { + result = checkerOrFactory; + } + if (result !== undefined && 'getTypeChecker' in result) + result = result.getTypeChecker(); + return result; +} + +function getUseDomain(node: Use['location']): Domain { + switch (node.kind) { + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.SuperKeyword: + return Domain.Value; + case ts.SyntaxKind.ThisType: + return Domain.Type; + default: + return getUsageDomain(node)! & Domain.Any; + } +} + +class ResolverImpl implements Resolver { + private _scopeMap = new WeakMap(); + + public findParentScope(node: ts.Node): Scope | undefined { + if (node.kind === ts.SyntaxKind.SourceFile) + return; + return this.getOrCreateScope(findScopeBoundary(node.parent!, -1)).getDelegateScope(node); + } + + public findReferences(declaration: ts.Identifier, domain = Domain.Any, getChecker?: TypeCheckerOrFactory): Use[] | undefined { + // TODO allow 'this'-parameter + domain &= getDeclarationDomain(declaration)!; // TODO + if (domain === 0) + return; // not a declaration + const selector = getScopeBoundarySelector(declaration); + if (selector === undefined) + throw new Error(`unhandled declaration '${ts.SyntaxKind[declaration.parent!.kind]}'`); // shouldn't happen + let scopeNode = findScopeBoundary(declaration.parent!, selector.selector); + if (selector.outer) + scopeNode = findScopeBoundary(scopeNode.parent!, selector.selector); + const scope = this.getOrCreateScope(scopeNode).getDelegateScope(declaration); + const symbol = scope.getSymbol(declaration); + if (symbol === undefined) + return; // something went wrong, possibly a syntax error + const result = []; + for (const use of scope.getUses(symbol, domain, makeCheckerFactory(getChecker))) { + if (use.domain & Domain.DoNotUse) + return; + result.push(use); + } + return result; + } + + public findDeclarations(use: Use['location'], getChecker?: TypeCheckerOrFactory) { + const domain = getUseDomain(use); + if (domain === 0) + return; + const symbol = this.getOrCreateScope(findScopeBoundary(use.parent!, -1)).lookupSymbol(use, domain, makeCheckerFactory(getChecker)); + if (symbol === undefined) + return []; + if (symbol.domain & Domain.DoNotUse) + return; + const result: ts.NamedDeclaration[] = []; + for (const declaration of symbol.declarations) + if (declaration.node !== undefined) + result.push(declaration.node); + return result; + } + + public getOrCreateScope(node: ts.Node) { + let scope = this._scopeMap.get(node); + if (scope === undefined) { + scope = this._createScope(node); + this._scopeMap.set(node, scope); + } + return scope; + } + + private _createScope(node: ts.Node): Scope { + switch (node.kind) { + case ts.SyntaxKind.SourceFile: + return new BaseScope(node, ScopeBoundary.Function, this); + case ts.SyntaxKind.MappedType: + return new BaseScope(node, ScopeBoundary.Type, this); + case ts.SyntaxKind.InterfaceDeclaration: + return new InterfaceScope( + node, + ScopeBoundary.Type, + this, + { + name: (node).name.text, + domain: Domain.Type, + node: node, + selector: ScopeBoundarySelector.Type, + }, + ); + case ts.SyntaxKind.TypeAliasDeclaration: + return new DeclarationScope( + node, + ScopeBoundary.Type, + this, + { + name: (node).name.text, + domain: Domain.Type, + node: node, + selector: ScopeBoundarySelector.Type, + }, + ); + case ts.SyntaxKind.EnumDeclaration: + return new NamespaceScope( + node, + ScopeBoundary.Block, + this, + { + name: (node).name.text, + domain: Domain.ValueOrNamespace, + node: node, + selector: ScopeBoundarySelector.Function, + }, + ); + case ts.SyntaxKind.ModuleDeclaration: + return new NamespaceScope( + node, + ScopeBoundary.Function, + this, + (node).name.kind === ts.SyntaxKind.StringLiteral || node.flags & ts.NodeFlags.GlobalAugmentation + ? undefined + : { + name: (node).name.text, + domain: Domain.ValueOrNamespace | Domain.Lazy, + node: node, + selector: ScopeBoundarySelector.Function, + }, + ); + case ts.SyntaxKind.ConditionalType: + return new ConditionalTypeScope(node, ScopeBoundary.ConditionalType, this); + // TODO handling of ClassLikeDeclaration might need change when https://github.com/Microsoft/TypeScript/issues/28472 is resolved + case ts.SyntaxKind.ClassExpression: + if ((node).name !== undefined) + return new NamedDeclarationExpressionScope(node, this, new ClassLikeScope( + node, + ScopeBoundary.Function, + this, + { + name: (node).name!.text, + domain: Domain.Type | Domain.Value, + node: node, + selector: ScopeBoundarySelector.Block, + }, + )); + // falls through + case ts.SyntaxKind.ClassDeclaration: + return new ClassLikeScope( + node, + ScopeBoundary.Function, + this, + (node).name === undefined + ? undefined + : { + name: (node).name!.text, + domain: Domain.Type | Domain.Value, + node: node, + selector: ScopeBoundarySelector.Block, + }, + ); + case ts.SyntaxKind.FunctionExpression: + if ((node).name !== undefined) + return new NamedDeclarationExpressionScope( + node, + this, + new FunctionLikeScope(node, this), + ); + // falls through + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ConstructSignature: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + return new FunctionLikeScope(node, this); + case ts.SyntaxKind.WithStatement: + return new WithStatementScope(node, ScopeBoundary.Block, this); + default: + if (isBlockScopeBoundary(node)) + return new BaseScope(node, ScopeBoundary.Block, this); + throw new Error(`unhandled Scope ${ts.SyntaxKind[node.kind]}`); + } + } +} + +function findScopeBoundary(node: ts.Node, selector: ScopeBoundarySelector): ts.Node { + let prev = node; + while ( + ( + (isScopeBoundary(node) & selector) === 0 || + // InferTypes belong to the ConditionalType where they occur in the `extendsType` + selector === ScopeBoundarySelector.InferType && (node).extendsType !== prev + ) && + node.parent !== undefined + ) { + prev = node; + node = node.parent; + } + return node; +} + +interface DeclarationBoundary { + selector: ScopeBoundarySelector; + outer: boolean; +} + +function getScopeBoundarySelector(node: ts.Identifier): DeclarationBoundary | undefined { + switch (node.parent!.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.EnumDeclaration: + return {selector: ScopeBoundarySelector.Block, outer: true}; + case ts.SyntaxKind.EnumMember: + if ((node.parent).name !== node) + return; + // falls through + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.FunctionExpression: // this is not entirely correct, but works for our purpose + case ts.SyntaxKind.ClassExpression: + return {selector: ScopeBoundarySelector.Block, outer: false}; + case ts.SyntaxKind.ModuleDeclaration: + if (node.parent.flags & ts.NodeFlags.GlobalAugmentation) + return; + // falls through + case ts.SyntaxKind.FunctionDeclaration: + return {selector: ScopeBoundarySelector.Function, outer: true}; + case ts.SyntaxKind.Parameter: + if (node.originalKeywordKind === ts.SyntaxKind.ThisKeyword || node.parent!.parent!.kind === ts.SyntaxKind.IndexSignature) + return; + return {selector: ScopeBoundarySelector.Function, outer: false}; + case ts.SyntaxKind.VariableDeclaration: { + const parent = (node.parent).parent!; + return { + selector: parent.kind === ts.SyntaxKind.CatchClause || isBlockScopedVariableDeclarationList(parent) + ? ScopeBoundarySelector.Block + : ScopeBoundarySelector.Function, + outer: false, + }; + } + case ts.SyntaxKind.BindingElement: { + const declaration = getDeclarationOfBindingElement(node.parent); + const blockScoped = declaration.kind === ts.SyntaxKind.Parameter || + declaration.parent!.kind === ts.SyntaxKind.CatchClause || + isBlockScopedVariableDeclarationList(declaration.parent); + return {selector: blockScoped ? ScopeBoundarySelector.Block : ScopeBoundarySelector.Function, outer: false}; + } + case ts.SyntaxKind.TypeParameter: + return { + selector: node.parent!.parent!.kind === ts.SyntaxKind.InferType + ? ScopeBoundarySelector.InferType + : ScopeBoundarySelector.Type, + outer: false, + }; + case ts.SyntaxKind.ImportEqualsDeclaration: + case ts.SyntaxKind.ImportSpecifier: + if ((node.parent).name !== node) + return; + // falls through + case ts.SyntaxKind.ImportClause: + case ts.SyntaxKind.NamespaceImport: + return {selector: ScopeBoundarySelector.Function, outer: false}; + default: + return; + } +} + +function getLazyDeclarationDomain(declaration: ts.NamedDeclaration, checker: ts.TypeChecker): Domain { + let symbol = checker.getSymbolAtLocation(declaration)!; + if (symbol.flags & ts.SymbolFlags.Alias) + symbol = checker.getAliasedSymbol(symbol); + return getDomainOfSymbol(symbol); +} + +function getDomainOfSymbol(symbol: ts.Symbol) { + let domain = Domain.None; + if (symbol.flags & ts.SymbolFlags.Type) + domain |= Domain.Type; + if (symbol.flags & (ts.SymbolFlags.Value | ts.SymbolFlags.ValueModule)) + domain |= Domain.Value; + if (symbol.flags & ts.SymbolFlags.Namespace) + domain |= Domain.Namespace; + return domain; +} + +function* lazyFilterUses>( + uses: Iterable, + getChecker: TypeCheckerFactory, + inclusive: boolean, + resolveDomain: (checker: ts.TypeChecker, ...args: T) => Domain, + ...args: T +) { + let resolvedDomain: Domain | undefined; + for (const use of uses) { + if (resolvedDomain === undefined) { + const checker = getChecker(); + if (checker === undefined) { + yield {location: use.location, domain: use.domain | Domain.DoNotUse}; + continue; + } + resolvedDomain = resolveDomain(checker, ...args); + } + if (((use.domain & resolvedDomain) !== 0) === inclusive) + yield use; + } +} + +function resolveLazySymbolDomain(checker: ts.TypeChecker, symbol: Symbol): Domain { + return resolveLazySymbol(checker, symbol).domain; +} + +function resolveLazySymbol(checker: ts.TypeChecker, symbol: Symbol): Symbol { + const result: Symbol = { + name: symbol.name, + domain: Domain.None, + declarations: [], + }; + for (const declaration of symbol.declarations) { + if (declaration.domain & Domain.Lazy) { + const domain = getLazyDeclarationDomain(declaration.node!, checker); + result.declarations.push({...declaration, domain}); + result.domain |= domain; + } else { + result.declarations.push(declaration); + result.domain |= declaration.domain; + } + } + return result; +} + +function filterSymbol(symbol: Symbol, domain: Domain): Symbol { + const result: Symbol = { + name: symbol.name, + domain: Domain.None, + declarations: [], + }; + for (const declaration of symbol.declarations) { + if ((declaration.domain & domain) === 0) + continue; + result.declarations.push(declaration); + result.domain |= declaration.domain; + } + return result; +} + +interface Scope { + node: ts.Node; + parent?: Scope; + getDeclarationsForParent(): Iterable; + getUses(symbol: Symbol, domain: Domain, getChecker: TypeCheckerFactory): Iterable; + getUsesInScope(symbol: Symbol, domain: Domain, getChecker: TypeCheckerFactory): Iterable; + getSymbol(declaration: ts.Identifier): Symbol | undefined; + getDelegateScope(location: ts.Node): Scope; + lookupSymbol(use: Use['location'], domain: Domain, getChecker: TypeCheckerFactory): Symbol | undefined; +} + +interface MatchRange extends ts.TextRange { + domain: Domain; +} + +namespace MatchRange { + export function create(domain: Domain, {pos, end}: ts.TextRange): MatchRange { + return {domain, pos, end}; + } +} + +function getDomainOfMatchingRange(pos: number, ranges: ReadonlyArray) { + for (const range of ranges) + if (isInRange(pos, range)) + return range.domain; + return Domain.None; +} + +function isInRange(pos: number, range: ts.TextRange): boolean { + return pos >= range.pos && pos < range.end; +} + +function scopeBoundaryToDomain(boundary: ScopeBoundary): Domain { + switch (boundary) { + case ScopeBoundary.Block: + return Domain.Type | Domain.Value; + case ScopeBoundary.Function: + return Domain.Any; + case ScopeBoundary.Type: + case ScopeBoundary.ConditionalType: + return Domain.Type; + default: + return Domain.None; + } +} + +function getUseName(node: Use['location']) { + switch (node.kind) { + case ts.SyntaxKind.ThisKeyword: + case ts.SyntaxKind.ThisType: + return 'this'; + case ts.SyntaxKind.SuperKeyword: + return 'super'; + default: + return node.text; + } +} + +class BaseScope implements Scope { + public parent: Scope | undefined = undefined; + private _initial = true; + private _uses: Use[] = []; + private _symbols = new Map(); + private _scopes: Scope[] = []; + private _propagatedRanges: ReadonlyArray = this._collectPropagatedRanges(); + protected _declarationsForParent: Declaration[] = []; + private _declarationsForParentInitialized = false; + + constructor(public node: T, protected _boundary: ScopeBoundary, protected _resolver: ResolverImpl) {} + + public getDelegateScope(_location: ts.Node): Scope { + return this; + } + + public getDeclarationsForParent() { + this._initialize(); + if (!this._declarationsForParentInitialized) { + for (const scope of this._scopes) + this._declarationsForParent.push(...scope.getDeclarationsForParent()); + this._declarationsForParentInitialized = true; + } + return this._declarationsForParent; + } + + public* getUsesInScope(symbol: Symbol, domain: Domain, getChecker: TypeCheckerFactory): Iterable { + this._initialize(); + if (this._propagatedRanges.length !== 0) + yield* this._match(symbol, domain, getChecker, this._propagatedRanges, true); + + let ownSymbol = this._symbols.get(symbol.name); + if (ownSymbol !== undefined) { + ownSymbol = filterSymbol(ownSymbol, domain); + if (ownSymbol.domain & Domain.Lazy) + // we don't know exactly what we have to deal with -> search uses syntactically and filter or abort later + yield* lazyFilterUses( + this._match(symbol, domain, getChecker, this._propagatedRanges, false), + getChecker, + false, + resolveLazySymbolDomain, + ownSymbol, + ); + domain &= ~ownSymbol.domain; + symbol = filterSymbol(symbol, domain); + } + yield* this._match(symbol, domain, getChecker, this._propagatedRanges, false); + } + + private* _match(symbol: Symbol, domain: Domain, getChecker: TypeCheckerFactory, ranges: ReadonlyArray, include: boolean) { + for (const use of this._uses) + if ( + use.domain & domain && + getUseName(use.location) === symbol.name && + ((use.domain & getDomainOfMatchingRange(use.location.pos, ranges)) !== 0) === include + ) + yield use; + for (const scope of this._scopes) { + let propagatedDomain = getDomainOfMatchingRange(scope.node.pos, ranges); + if (!include) + propagatedDomain = ~propagatedDomain; + const d = domain & propagatedDomain; + if (d !== 0) + yield* scope.getUsesInScope(filterSymbol(symbol, d), d, getChecker); + } + } + + public getUses(symbol: Symbol, domain: Domain, getChecker: TypeCheckerFactory): Iterable { + symbol = filterSymbol(symbol, domain); + let uses = this._match(symbol, domain, getChecker, this._propagatedRanges, false); + if (symbol.domain & Domain.Lazy) + uses = lazyFilterUses(uses, getChecker, true, resolveLazySymbolDomain, symbol); + return uses; + } + + public getSymbol(declaration: ts.Identifier) { + this._initialize(); + return this._symbols.get(declaration.text); + } + + public lookupSymbol(location: Use['location'], domain: Domain, getChecker: TypeCheckerFactory) { + if ( + (domain & scopeBoundaryToDomain(this._boundary)) !== 0 && + (domain & getDomainOfMatchingRange(location.pos, this._propagatedRanges)) === 0 + ) { + const ownSymbol = this._getOwnSymbol(getUseName(location), domain, getChecker); + if (ownSymbol !== undefined) + return ownSymbol; + } + const parent = this.parent || this._resolver.findParentScope(this.node); + return parent && parent.lookupSymbol(location, domain, getChecker); + } + + protected _getOwnSymbol(name: string, domain: Domain, getChecker: TypeCheckerFactory) { + this._initialize(); + let ownSymbol = this._symbols.get(name); + if (ownSymbol === undefined || (ownSymbol.domain & domain) === 0) + return; + if (ownSymbol.domain & Domain.Lazy) { + const checker = getChecker(); + if (checker === undefined) + return {...ownSymbol, domain: ownSymbol.domain | Domain.DoNotUse}; + ownSymbol = resolveLazySymbol(checker, ownSymbol); + if ((ownSymbol.domain & domain) === 0) + return; + } + return filterSymbol(ownSymbol, domain); + } + + protected _addUse(use: Use) { + this._uses.push(use); + } + + protected _addDeclaration(declaration: Declaration) { + if (!this._isOwnDeclaration(declaration)) { + this._declarationsForParent.push(declaration); + return; + } + const symbol = this._symbols.get(declaration.name); + if (symbol !== undefined) { + symbol.domain |= declaration.domain; + symbol.declarations.push(declaration); + } else { + this._symbols.set(declaration.name, { + name: declaration.name, + domain: declaration.domain, + declarations: [declaration], + }); + } + } + + protected _addChildScope(scope: Scope) { + this._scopes.push(scope); + } + + protected _initialize() { + if (this._initial) { + this._analyze(); + if (this._boundary === ScopeBoundary.Function || this._boundary === ScopeBoundary.ConditionalType) { + // only ConditionalType and Function can get declarations from a child scope + for (const scope of this._scopes) + for (const decl of scope.getDeclarationsForParent()) + this._addDeclaration(decl); + this._declarationsForParentInitialized = true; + } + this._initial = false; + } + } + + protected _analyze() { + ts.forEachChild(this.node, this._analyzeNode); + } + + protected _isOwnDeclaration(declaration: Declaration) { + return (declaration.selector & this._boundary) !== 0; + } + + // tslint:disable-next-line:prefer-function-over-method + protected _collectPropagatedRanges(): MatchRange[] { + return []; + } + + @bind + protected _analyzeNode(node: ts.Node): void { + if (isScopeBoundary(node)) { + this._addChildScope(this._resolver.getOrCreateScope(node)); + return; + } + switch (node.kind) { + case ts.SyntaxKind.VariableDeclarationList: + return this._handleVariableDeclarationList(node); + case ts.SyntaxKind.VariableDeclaration: + // catch binding + return this._handleBindingName((node).name, true); + case ts.SyntaxKind.Parameter: + if (node.parent!.kind === ts.SyntaxKind.IndexSignature) + return (node).type && this._analyzeNode((node).type!); + return this._handleVariableLikeDeclaration(node, false); + case ts.SyntaxKind.EnumMember: + this._addDeclaration({ + name: getPropertyName((node).name)!, + domain: Domain.Value, + node: node, + selector: ScopeBoundarySelector.Block, + }); + if ((node).initializer !== undefined) + this._analyzeNode((node).initializer!); + return; + case ts.SyntaxKind.ImportClause: + if ((node).name !== undefined) + this._addDeclaration({ + name: (node).name!.text, + domain: Domain.Any | Domain.Lazy, + node: node, + selector: ScopeBoundarySelector.Function, + }); + if ((node).namedBindings !== undefined) + this._analyzeNode((node).namedBindings!); + return; + case ts.SyntaxKind.ImportEqualsDeclaration: + this._analyzeNode((node).moduleReference); + // falls through + case ts.SyntaxKind.ImportSpecifier: + case ts.SyntaxKind.NamespaceImport: + this._addDeclaration({ + name: ((node).name).text, + domain: Domain.Any | Domain.Lazy, + node: node, + selector: ScopeBoundarySelector.Function, + }); + return; + case ts.SyntaxKind.TypeParameter: + this._addDeclaration({ + name: (node).name.text, + domain: Domain.Type, + node: (node).name, + selector: node.parent!.kind === ts.SyntaxKind.InferType ? ScopeBoundarySelector.InferType : ScopeBoundarySelector.Type, + }); + if ((node).constraint !== undefined) + this._analyzeNode((node).constraint!); + if ((node).default !== undefined) + this._analyzeNode((node).default!); + return; + case ts.SyntaxKind.ThisType: + this._addUse({location: node, domain: Domain.Type}); + return; + case ts.SyntaxKind.ThisKeyword: + this._addUse({location: node, domain: Domain.Value}); + return; + case ts.SyntaxKind.SuperKeyword: + this._addUse({location: node, domain: Domain.Value}); + return; + case ts.SyntaxKind.Identifier: { + const domain = getUsageDomain(node); + if (domain !== undefined) // TODO + this._addUse({location: node, domain: domain | 0}); + return; + } + } + if (isNodeKind(node.kind)) + return ts.forEachChild(node, this._analyzeNode); + } + + private _handleVariableDeclarationList(list: ts.VariableDeclarationList) { + const blockScoped = isBlockScopedVariableDeclarationList(list); + for (const declaration of list.declarations) + this._handleVariableLikeDeclaration(declaration, blockScoped); + } + + private _handleVariableLikeDeclaration(declaration: ts.VariableDeclaration | ts.ParameterDeclaration, blockScoped: boolean) { + this._handleBindingName(declaration.name, blockScoped); + if (declaration.type !== undefined) + this._analyzeNode(declaration.type); + if (declaration.initializer !== undefined) + this._analyzeNode(declaration.initializer); + } + + private _handleBindingName(name: ts.BindingName, blockScoped: boolean) { + const selector = blockScoped ? ScopeBoundarySelector.Block : ScopeBoundarySelector.Function; + if (name.kind === ts.SyntaxKind.Identifier) + // tslint:disable-next-line:object-shorthand-properties-first + return this._addDeclaration({name: name.text, domain: Domain.Value, node: name, selector}); + + for (const element of name.elements) { + if (element.kind === ts.SyntaxKind.OmittedExpression) + continue; + if (element.propertyName !== undefined && element.propertyName.kind === ts.SyntaxKind.ComputedPropertyName) + this._analyzeNode(element.propertyName); + this._handleBindingName(element.name, blockScoped); + if (element.initializer !== undefined) + this._analyzeNode(element.initializer); + } + } +} + +class WithStatementScope extends BaseScope { + // tslint:disable-next-line:prefer-function-over-method + public getDeclarationsForParent() { + return []; // nothing to do here + } + public* getUsesInScope(symbol: Symbol, domain: Domain, getChecker: TypeCheckerFactory) { + // we don't know what could be in scope here + for (const use of super.getUsesInScope(symbol, domain, getChecker)) { + if (use.domain & Domain.Value) { + yield {location: use.location, domain: use.domain | Domain.DoNotUse}; + } else { + yield use; + } + } + } + + // tslint:disable-next-line:prefer-function-over-method + protected _getOwnSymbol(name: string, domain: Domain) { + // we don't need to call super here, as a WithStatement should never have any own declaration + return domain & Domain.Value + ? {name, domain: domain | Domain.DoNotUse, declarations: []} + : undefined; + } +} + +class DeclarationScope extends BaseScope { + constructor(node: T, boundary: ScopeBoundary, resolver: ResolverImpl, declaration?: Declaration) { + super(node, boundary, resolver); + if (declaration) + this._declarationsForParent.push(declaration); + } + + public getDeclarationsForParent() { + return this._declarationsForParent; + } +} + +class DecoratableDeclarationScope< + T extends ts.ClassLikeDeclaration | ts.SignatureDeclaration = ts.ClassLikeDeclaration | ts.SignatureDeclaration, +> extends DeclarationScope { + protected _collectPropagatedRanges() { + // decorators cannot access parameters and type parameters of the declaration they decorate + return this.node.decorators === undefined + ? [] + : [MatchRange.create(Domain.Any, this.node.decorators)]; + + } +} + +class InterfaceScope extends DeclarationScope { + protected _analyze() { + this._addDeclaration({name: 'this', domain: Domain.Type, node: undefined, selector: ScopeBoundarySelector.Type}); + super._analyze(); + } +} + +class ClassLikeScope extends DecoratableDeclarationScope { + protected _collectPropagatedRanges() { + const result = super._collectPropagatedRanges(); // decorators + if (this.node.heritageClauses !== undefined) { + const [clause] = this.node.heritageClauses; + if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length !== 0) + // expression in 'extends' cannot access 'this' and 'super' of the class + result.push(MatchRange.create(Domain.ValueOrNamespace, clause.types[0].expression)); + } + return result; + } + + protected _analyze() { + this._addDeclaration( + {name: 'this', domain: Domain.Type | Domain.Value, node: undefined, selector: ScopeBoundarySelector.Function}, + ); + this._addDeclaration({name: 'super', domain: Domain.Value, node: undefined, selector: ScopeBoundarySelector.Function}); + super._analyze(); + } +} + +function resolveNamespaceExportDomain(checker: ts.TypeChecker, node: ts.ModuleDeclaration | ts.EnumDeclaration, name: string) { + const exports = checker.getSymbolAtLocation(node)!.exports!; + const ambientModule = node.kind === ts.SyntaxKind.ModuleDeclaration && isAmbientModule(node); + if (ambientModule) { + // 'export default class C' is visible as 'C' in merged ambient modules + // it shadows named exports of the same name in that ambient module + const exportDefault = exports.get(ts.InternalSymbolName.Default); + if (exportDefault !== undefined) { + const declaration = exportDefault.declarations![0]; + if ( + (isInterfaceDeclaration(declaration) || isClassDeclaration(declaration) || isFunctionDeclaration(declaration)) && + declaration.name !== undefined && + declaration.name.text === name + ) + return getDomainOfSymbol(exportDefault); + } + + } + const exportedSymbol = exports.get(ts.escapeLeadingUnderscores(name)); + if (exportedSymbol === undefined) + return Domain.None; + if (ambientModule && + exportedSymbol.flags === ts.SymbolFlags.Alias && + exportedSymbol.declarations !== undefined && + exportedSymbol.declarations.some(isExportSpecifier) + ) + // 'export {Foo as Bar}' of ambient module is not in scope + return Domain.None; + return node.kind === ts.SyntaxKind.EnumDeclaration + ? Domain.Value + : getDomainOfSymbol(exportedSymbol); +} + +class NamespaceScope extends DeclarationScope { + public getUsesInScope(symbol: Symbol, domain: Domain, getChecker: TypeCheckerFactory) { + // TODO allow non-value uses in Enum even without the checker + return lazyFilterUses( + super.getUsesInScope(symbol, domain, getChecker), + getChecker, + false, + resolveNamespaceExportDomain, + this.node, + symbol.name, + ); + } + + protected _getOwnSymbol(name: string, domain: Domain, getChecker: TypeCheckerFactory) { + let result = super._getOwnSymbol(name, domain, getChecker); + if (result === undefined) { + if (this.node.kind === ts.SyntaxKind.EnumDeclaration && (domain & Domain.Value) === 0) + return; + const checker = getChecker(); + if (checker === undefined) { + result = {name, declarations: [], domain: domain | Domain.DoNotUse}; + } else { + const resolvedDomain = resolveNamespaceExportDomain(checker, this.node, name); + if (resolvedDomain !== 0) + result = {name, declarations: [], domain: resolvedDomain}; + } + } + return result; + } +} + +class ConditionalTypeScope extends BaseScope { + protected _isOwnDeclaration(declaration: Declaration) { + return super._isOwnDeclaration(declaration) && isInRange(declaration.node!.pos, this.node.extendsType); + } + + protected _collectPropagatedRanges() { + return [ + {domain: Domain.Any, pos: this.node.pos, end: this.node.extendsType.end}, + MatchRange.create(Domain.Any, this.node.falseType), + ]; + } +} + +class NamedDeclarationExpressionScope extends BaseScope { + // tslint:disable-next-line:parameter-properties + constructor(node: ts.NamedDeclaration, resolver: ResolverImpl, private _childScope: Scope) { + super(node, ScopeBoundary.Function, resolver); + this._childScope.parent = this; + } + + // tslint:disable-next-line:prefer-function-over-method + public getDeclarationsForParent() { + return []; + } + + protected _analyze() { + this._addChildScope(this._childScope); + } + + public getDelegateScope(location: ts.Node): Scope { + return location === this.node.name + ? this + : this._childScope.getDelegateScope(location); + } +} + +class FunctionLikeInnerScope extends BaseScope { + // tslint:disable-next-line:prefer-function-over-method + public getDeclarationsForParent() { + return []; + } + + protected _analyze() { + if (this.node.type !== undefined) + this._analyzeNode(this.node.type); + this._analyzeNode(this.node.body); + } +} + +class FunctionLikeScope extends DecoratableDeclarationScope { + private _innerScope: FunctionLikeInnerScope | undefined = undefined; + + constructor(node: ts.SignatureDeclaration, resolver: ResolverImpl) { + super( + node, + ScopeBoundary.Function, + resolver, + node.kind !== ts.SyntaxKind.FunctionDeclaration && node.kind !== ts.SyntaxKind.FunctionExpression || node.name === undefined + ? undefined + : { + name: node.name.text, + domain: Domain.Value, + node, // tslint:disable-line:object-shorthand-properties-first + selector: ScopeBoundarySelector.Function, + }, + ); + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.Constructor: + this._addDeclaration({name: 'arguments', domain: Domain.Value, node: undefined, selector: ScopeBoundarySelector.Function}); + this._addDeclaration( + {name: 'this', domain: Domain.Type | Domain.Value, node: undefined, selector: ScopeBoundarySelector.Function}, + ); + this._addDeclaration({name: 'super', domain: Domain.Value, node: undefined, selector: ScopeBoundarySelector.Function}); + } + if ('body' in node && node.body !== undefined) { // don't create a nested scope if there is no body and therefore no scoping problem + this._innerScope = new FunctionLikeInnerScope( + this.node, + ScopeBoundary.Function, + this._resolver, + ); + this._innerScope.parent = this; + } + } + + public getDelegateScope(location: ts.Node): Scope { + return this._innerScope === undefined || location.pos < this.node.parameters.end + ? this + : this._innerScope.getDelegateScope(location); + } + + protected _analyze() { + if (this.node.decorators !== undefined) + this.node.decorators.forEach(this._analyzeNode); + if (this.node.name !== undefined && this.node.name.kind === ts.SyntaxKind.ComputedPropertyName) + this._analyzeNode(this.node.name.expression); + if (this.node.typeParameters !== undefined) + this.node.typeParameters.forEach(this._analyzeNode); + this.node.parameters.forEach(this._analyzeNode); + if (this._innerScope !== undefined) { + this._addChildScope(this._innerScope); + } else if (this.node.type !== undefined) { + this._analyzeNode(this.node.type); + } + } + + protected _collectPropagatedRanges() { + const result = super._collectPropagatedRanges(); // method decorators + if (this.node.typeParameters !== undefined) + // 'typeof' in type parameters cannot access parameters of that function + result.push(MatchRange.create(Domain.Value, this.node.typeParameters)); + if (this.node.name !== undefined && this.node.name.kind === ts.SyntaxKind.ComputedPropertyName) + // computed method name cannot access method generics and parameters + result.push(MatchRange.create(Domain.Any, this.node.name)); + for (const parameter of this.node.parameters) + if (parameter.decorators !== undefined) + // references in parameter decorators are resolved outside of the class declaration + result.push(MatchRange.create(Domain.Any, parameter.decorators)); + return result; + } +} + +// * nested conditional types +// * function/class decorated with itself +// * type parmeters shadowing declaration name +// * type parameter cannot reference parameter +// * member decorator accessing class generics +// * MappedType type parameter referencing itself in its constraint +// type and namespace use in enum +// * type and namespace use in with statement +// * type-only namespace not shadowing value +// exporting partially shadowed declaration from ambient namespace only uses closest declaration +// domain of 'export import = ' in namespace +// * ConditionalType using 'typeof' in function's type parameter constraint +// * FunctionScope in Decorator has no access to parameters and generics +// * with statement +// * parameter decorator cannot access parameters and type parameters +// * handle arguments +// * computed property names of methods cannot access parameters and generics +// * computed property names access class generics (which is reported as error) +// * parameter decorators resolve outside of class declaration +// * expression in class 'extends' clause resolves outside of class declaration +// * type arguments in class 'extends' resolve inside class declaration +// * in ambient **module** exclude alias exports 'export {T as V}' +// * make sure namespace import is treated as namespace +// * 'export default class C' is visible in merged ambient module +// * track 'this' and 'super' + +// add an API to get an arbitrary Symbol by name at a certain location + +// expose Iterable API for findReferences + +// statically analyze merged namespaces and enums +// statically determine if namespace or enum can merge with something else +// add function to determine if symbol is exported or global + +// maybe optimize subtrees by collecting all uses of children and permanently assign them to the symbols in that scope +// this introduces a lot of arrays that contain a lot of duplicates, as each scope holds a list of all uses for its own parent