diff --git a/Jakefile.js b/Jakefile.js index 15fe40141f6..46573e701ff 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -140,6 +140,7 @@ var harnessSources = harnessCoreSources.concat([ "transform.ts", "customTransforms.ts", "programMissingFiles.ts", + "symbolWalker.ts", ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d2d3697f175..d69c6cdebda 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1,5 +1,6 @@ /// /// +/// /* @internal */ namespace ts { @@ -204,6 +205,7 @@ namespace ts { getEmitResolver, getExportsOfModule: getExportsOfModuleAsArray, getExportsAndPropertiesOfModule, + getSymbolWalker: createGetSymbolWalker(getRestTypeOfSignature, getReturnTypeOfSignature, getBaseTypes, resolveStructuredTypeMembers, getTypeOfSymbol, getResolvedSymbol, getIndexTypeOfStructuredType), getAmbientModules, getAllAttributesTypeFromJsxOpeningLikeElement: node => { node = getParseTreeNode(node, isJsxOpeningLikeElement); diff --git a/src/compiler/symbolWalker.ts b/src/compiler/symbolWalker.ts new file mode 100644 index 00000000000..c85661df29a --- /dev/null +++ b/src/compiler/symbolWalker.ts @@ -0,0 +1,163 @@ +namespace ts { + export function createGetSymbolWalker( + getRestTypeOfSignature: (sig: Signature) => Type, + getReturnTypeOfSignature: (sig: Signature) => Type, + getBaseTypes: (type: Type) => Type[], + resolveStructuredTypeMembers: (type: ObjectType) => ResolvedType, + getTypeOfSymbol: (sym: Symbol) => Type, + getResolvedSymbol: (node: Node) => Symbol, + getIndexTypeOfStructuredType: (type: Type, kind: IndexKind) => Type) { + + return getSymbolWalker; + + function getSymbolWalker(accept: (symbol: Symbol) => boolean = () => true): SymbolWalker { + let visited: Type[] = []; + let visitedSymbols: Symbol[] = []; + + return { + visitType, + visitSymbol, + reset: (newCallback: (symbol: Symbol) => boolean = () => true) => { + accept = newCallback; + visited = []; + visitedSymbols = []; + } + }; + + function visitType(type: Type): void { + if (!type) { + return; + } + if (contains(visited, type)) { + return; + } + visited.push(type); + + // Reuse visitSymbol to visit the type's symbol, + // but be sure to bail on recuring into the type if accept declines the symbol. + const shouldBail = visitSymbol(type.symbol); + if (shouldBail) return; + + // Visit the type's related types, if any + if (type.flags & TypeFlags.Object) { + const objectType = type as ObjectType; + const objectFlags = objectType.objectFlags; + if (objectFlags & ObjectFlags.Reference) { + visitTypeReference(type as TypeReference); + } + if (objectFlags & (ObjectFlags.Class | ObjectFlags.Interface)) { + visitInterfaceType(type as InterfaceType); + } + if (objectFlags & (ObjectFlags.Tuple | ObjectFlags.Anonymous)) { + visitObjectType(objectType); + } + } + if (type.flags & TypeFlags.TypeParameter) { + visitTypeParameter(type as TypeParameter); + } + if (type.flags & TypeFlags.UnionOrIntersection) { + visitUnionOrIntersectionType(type as UnionOrIntersectionType); + } + } + + function visitTypeList(types: Type[]): void { + if (!types) { + return; + } + for (let i = 0; i < types.length; i++) { + visitType(types[i]); + } + } + + function visitTypeReference(type: TypeReference): void { + visitType(type.target); + visitTypeList(type.typeArguments); + } + + function visitTypeParameter(type: TypeParameter): void { + visitType(type.constraint); + } + + function visitUnionOrIntersectionType(type: UnionOrIntersectionType): void { + visitTypeList(type.types); + } + + function visitSignature(signature: Signature): void { + if (signature.typePredicate) { + visitType(signature.typePredicate.type); + } + visitTypeList(signature.typeParameters); + + for (const parameter of signature.parameters){ + visitSymbol(parameter); + } + visitType(getRestTypeOfSignature(signature)); + visitType(getReturnTypeOfSignature(signature)); + } + + function visitInterfaceType(interfaceT: InterfaceType): void { + visitObjectType(interfaceT); + visitTypeList(interfaceT.typeParameters); + visitTypeList(getBaseTypes(interfaceT)); + visitType(interfaceT.thisType); + } + + function visitObjectType(type: ObjectType): void { + const stringIndexType = getIndexTypeOfStructuredType(type, IndexKind.String); + visitType(stringIndexType); + const numberIndexType = getIndexTypeOfStructuredType(type, IndexKind.String); + visitType(numberIndexType); + + // The two checks above *should* have already resolved the type (if needed), so this should be cached + const resolved = resolveStructuredTypeMembers(type); + for (const signature of resolved.callSignatures) { + visitSignature(signature); + } + for (const signature of resolved.constructSignatures) { + visitSignature(signature); + } + for (const p of resolved.properties) { + visitSymbol(p); + } + } + + function visitSymbol(symbol: Symbol): boolean { + if (!symbol) { + return; + } + if (contains(visitedSymbols, symbol)) { + return; + } + visitedSymbols.push(symbol); + if (!accept(symbol)) { + return true; + } + const t = getTypeOfSymbol(symbol); + visitType(t); // Should handle members on classes and such + if (symbol.flags & SymbolFlags.HasExports) { + symbol.exports.forEach(visitSymbol); + } + forEach(symbol.declarations, d => { + // Type queries are too far resolved when we just visit the symbol's type + // (their type resolved directly to the member deeply referenced) + // So to get the intervening symbols, we need to check if there's a type + // query node on any of the symbol's declarations and get symbols there + if ((d as any).type && (d as any).type.kind === SyntaxKind.TypeQuery) { + const query = (d as any).type as TypeQueryNode; + const entity = leftmostSymbol(query.exprName); + visitSymbol(entity); + } + }); + } + } + + function leftmostSymbol(expr: QualifiedName | Identifier): Symbol { + if (expr.kind === SyntaxKind.Identifier) { + return getResolvedSymbol(expr as Identifier); + } + else { + return leftmostSymbol((expr as QualifiedName).left); + } + } + } +} \ No newline at end of file diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 3709d65b7fd..c048359fcb7 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -14,6 +14,7 @@ "parser.ts", "utilities.ts", "binder.ts", + "symbolWalker.ts", "checker.ts", "factory.ts", "visitor.ts", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 608bc779042..7bce522d5ba 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2625,6 +2625,8 @@ namespace ts { /* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol | undefined; + getSymbolWalker(accept?: (symbol: Symbol) => boolean): SymbolWalker; + // Should not be called directly. Should only be accessed through the Program instance. /* @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[]; /* @internal */ getGlobalDiagnostics(): Diagnostic[]; @@ -2669,6 +2671,12 @@ namespace ts { InTypeAlias = 1 << 23, // Writing type in type alias declaration } + export interface SymbolWalker { + visitType(type: Type): void; + visitSymbol(symbol: Symbol): void; + reset(accept?: (symbol: Symbol) => boolean): void; + } + export interface SymbolDisplayBuilder { buildTypeDisplay(type: Type, writer: SymbolWriter, enclosingDeclaration?: Node, flags?: TypeFormatFlags): void; buildSymbolDisplay(symbol: Symbol, writer: SymbolWriter, enclosingDeclaration?: Node, meaning?: SymbolFlags, flags?: SymbolFormatFlags): void; diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 66ca2fc3f48..9165e59cb0a 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -21,6 +21,7 @@ "../compiler/parser.ts", "../compiler/utilities.ts", "../compiler/binder.ts", + "../compiler/symbolWalker.ts", "../compiler/checker.ts", "../compiler/factory.ts", "../compiler/visitor.ts", @@ -103,6 +104,7 @@ "./unittests/services/preProcessFile.ts", "./unittests/services/patternMatcher.ts", "./unittests/session.ts", + "./unittests/symbolWalker.ts", "./unittests/versionCache.ts", "./unittests/convertToBase64.ts", "./unittests/transpile.ts", diff --git a/src/harness/unittests/symbolWalker.ts b/src/harness/unittests/symbolWalker.ts new file mode 100644 index 00000000000..275c37d41f6 --- /dev/null +++ b/src/harness/unittests/symbolWalker.ts @@ -0,0 +1,53 @@ +/// + +namespace ts { + describe("Symbol Walker", () => { + function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker, walker: SymbolWalker) => void) { + it(description, () => { + let {result} = Harness.Compiler.compileFiles([{ + unitName: "main.ts", + content: source + }], [], {}, {}, "/"); + let file = result.program.getSourceFile("main.ts"); + let checker = result.program.getTypeChecker(); + let walker = checker.getSymbolWalker(); + verifier(file, checker, walker); + + result = undefined; + file = undefined; + checker = undefined; + walker = undefined; + }); + } + + test("can be created", ` +interface Bar { + x: number; + y: number; + history: Bar[]; +} +export default function foo(a: number, b: Bar): void {}`, (file, checker, walker) => { + let foundCount = 0; + let stdLibRefSymbols = 0; + const expectedSymbols = ["default", "a", "b", "Bar", "x", "y", "history"]; + walker.reset(symbol => { + const isStdLibSymbol = forEach(symbol.declarations, d => { + return getSourceFileOfNode(d).hasNoDefaultLib; + }); + if (isStdLibSymbol) { + stdLibRefSymbols++; + return false; // Don't traverse into the stdlib. That's unnecessary for this test. + } + assert.equal(symbol.name, expectedSymbols[foundCount]); + foundCount++; + return true; + }); + const symbols = checker.getExportsOfModule(file.symbol); + for (const symbol of symbols) { + walker.visitSymbol(symbol); + } + assert.equal(foundCount, expectedSymbols.length); + assert.equal(stdLibRefSymbols, 1); // Expect 1 stdlib entry symbol - the implicit Array referenced by Bar.history + }); + }); +} \ No newline at end of file diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index f4ca2a7f130..d73014a93a2 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -14,6 +14,7 @@ "../compiler/parser.ts", "../compiler/utilities.ts", "../compiler/binder.ts", + "../compiler/symbolWalker.ts", "../compiler/checker.ts", "../compiler/factory.ts", "../compiler/visitor.ts",