diff --git a/Jakefile.js b/Jakefile.js index 5ae887ee2385e..b3e18e8cb1a3d 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -142,6 +142,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 551b7063169d1..af45b00c518ef 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, getConstraintFromTypeParameter, getFirstIdentifier), getAmbientModules, getAllAttributesTypeFromJsxOpeningLikeElement: node => { node = getParseTreeNode(node, isJsxOpeningLikeElement); diff --git a/src/compiler/symbolWalker.ts b/src/compiler/symbolWalker.ts new file mode 100644 index 0000000000000..e20ef9f9d9877 --- /dev/null +++ b/src/compiler/symbolWalker.ts @@ -0,0 +1,191 @@ +/** @internal */ +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, + getConstraintFromTypeParameter: (typeParameter: TypeParameter) => Type, + getFirstIdentifier: (node: EntityNameOrEntityNameExpression) => Identifier) { + + return getSymbolWalker; + + function getSymbolWalker(accept: (symbol: Symbol) => boolean = () => true): SymbolWalker { + const visitedTypes = createMap(); // Key is id as string + const visitedSymbols = createMap(); // Key is id as string + + return { + walkType: type => { + visitedTypes.clear(); + visitedSymbols.clear(); + visitType(type); + return { visitedTypes: arrayFrom(visitedTypes.values()), visitedSymbols: arrayFrom(visitedSymbols.values()) }; + }, + walkSymbol: symbol => { + visitedTypes.clear(); + visitedSymbols.clear(); + visitSymbol(symbol); + return { visitedTypes: arrayFrom(visitedTypes.values()), visitedSymbols: arrayFrom(visitedSymbols.values()) }; + }, + }; + + function visitType(type: Type): void { + if (!type) { + return; + } + + const typeIdString = type.id.toString(); + if (visitedTypes.has(typeIdString)) { + return; + } + visitedTypes.set(typeIdString, 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.Mapped) { + visitMappedType(type as MappedType); + } + 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); + } + if (type.flags & TypeFlags.Index) { + visitIndexType(type as IndexType); + } + if (type.flags & TypeFlags.IndexedAccess) { + visitIndexedAccessType(type as IndexedAccessType); + } + } + + 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(getConstraintFromTypeParameter(type)); + } + + function visitUnionOrIntersectionType(type: UnionOrIntersectionType): void { + visitTypeList(type.types); + } + + function visitIndexType(type: IndexType): void { + visitType(type.type); + } + + function visitIndexedAccessType(type: IndexedAccessType): void { + visitType(type.objectType); + visitType(type.indexType); + visitType(type.constraint); + } + + function visitMappedType(type: MappedType): void { + visitType(type.typeParameter); + visitType(type.constraintType); + visitType(type.templateType); + visitType(type.modifiersType); + } + + 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.Number); + 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; + } + const symbolIdString = getSymbolId(symbol).toString(); + if (visitedSymbols.has(symbolIdString)) { + return; + } + visitedSymbols.set(symbolIdString, 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 = getResolvedSymbol(getFirstIdentifier(query.exprName)); + visitSymbol(entity); + } + }); + } + } + } +} \ No newline at end of file diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 3709d65b7fd23..c048359fcb7a4 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 608bc779042df..26bf5e1a91f85 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2625,6 +2625,8 @@ namespace ts { /* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol | undefined; + /* @internal */ 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,14 @@ namespace ts { InTypeAlias = 1 << 23, // Writing type in type alias declaration } + /* @internal */ + export interface SymbolWalker { + /** Note: Return values are not ordered. */ + walkType(root: Type): { visitedTypes: ReadonlyArray, visitedSymbols: ReadonlyArray }; + /** Note: Return values are not ordered. */ + walkSymbol(root: Symbol): { visitedTypes: ReadonlyArray, visitedSymbols: ReadonlyArray }; + } + 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; @@ -3367,6 +3377,7 @@ namespace ts { // Type parameters (TypeFlags.TypeParameter) export interface TypeParameter extends TypeVariable { + /** Retrieve using getConstraintFromTypeParameter */ constraint: Type; // Constraint default?: Type; /* @internal */ diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 66ca2fc3f4837..9165e59cb0a1a 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 0000000000000..6d38fbb5198c9 --- /dev/null +++ b/src/harness/unittests/symbolWalker.ts @@ -0,0 +1,51 @@ +/// + +namespace ts { + describe("Symbol Walker", () => { + function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => 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(); + verifier(file, checker); + + result = undefined; + file = undefined; + checker = undefined; + }); + } + + test("can be created", ` +interface Bar { + x: number; + y: number; + history: Bar[]; +} +export default function foo(a: number, b: Bar): void {}`, (file, checker) => { + let foundCount = 0; + let stdLibRefSymbols = 0; + const expectedSymbols = ["default", "a", "b", "Bar", "x", "y", "history"]; + const walker = checker.getSymbolWalker(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.walkSymbol(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 f4ca2a7f130f5..d73014a93a24a 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",