Skip to content

Resuscitate the SymbolWalker API #17844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 23, 2017
1 change: 1 addition & 0 deletions Jakefile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference path="moduleNameResolver.ts"/>
/// <reference path="binder.ts"/>
/// <reference path="symbolWalker.ts" />

/* @internal */
namespace ts {
Expand Down Expand Up @@ -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);
Expand Down
191 changes: 191 additions & 0 deletions src/compiler/symbolWalker.ts
Original file line number Diff line number Diff line change
@@ -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<Type>(); // Key is id as string
const visitedSymbols = createMap<Symbol>(); // 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);
}
});
}
}
}
}
1 change: 1 addition & 0 deletions src/compiler/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"parser.ts",
"utilities.ts",
"binder.ts",
"symbolWalker.ts",
"checker.ts",
"factory.ts",
"visitor.ts",
Expand Down
11 changes: 11 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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<Type>, visitedSymbols: ReadonlyArray<Symbol> };
/** Note: Return values are not ordered. */
walkSymbol(root: Symbol): { visitedTypes: ReadonlyArray<Type>, visitedSymbols: ReadonlyArray<Symbol> };
}

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;
Expand Down Expand Up @@ -3367,6 +3377,7 @@ namespace ts {

// Type parameters (TypeFlags.TypeParameter)
export interface TypeParameter extends TypeVariable {
/** Retrieve using getConstraintFromTypeParameter */
constraint: Type; // Constraint
default?: Type;
/* @internal */
Expand Down
2 changes: 2 additions & 0 deletions src/harness/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions src/harness/unittests/symbolWalker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/// <reference path="..\harness.ts" />

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
});
});
}
1 change: 1 addition & 0 deletions src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down