diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e12036322bd..25fa0b3e8f4 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -74,6 +74,7 @@ namespace ts { getGlobalDiagnostics, getTypeOfSymbolAtLocation, getSymbolsOfParameterPropertyDeclaration, + getTypeOfSymbol, getDeclaredTypeOfSymbol, getPropertiesOfType, getPropertyOfType, @@ -84,6 +85,7 @@ namespace ts { resolveStructuredTypeMembers, getNonNullableType, getSymbolsInScope, + getSymbolOfNode, getSymbolAtLocation, getShorthandAssignmentValueSymbol, getExportSpecifierLocalTargetSymbol, @@ -4352,6 +4354,9 @@ namespace ts { setStructuredTypeMembers(type, emptySymbols, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); } + /** + * Converts an AnonymousType to a ResolvedType. + */ function resolveAnonymousTypeMembers(type: AnonymousType) { const symbol = type.symbol; if (type.target) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ece3dab6ecc..5bf20b1c0bf 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2245,6 +2245,7 @@ namespace ts { export interface TypeChecker { getTypeOfSymbolAtLocation(symbol: Symbol, node: Node): Type; + getTypeOfSymbol(symbol: Symbol): Type; getDeclaredTypeOfSymbol(symbol: Symbol): Type; getPropertiesOfType(type: Type): Symbol[]; getPropertyOfType(type: Type, propertyName: string): Symbol; @@ -2256,6 +2257,7 @@ namespace ts { getNonNullableType(type: Type): Type; getSymbolsInScope(location: Node, meaning: SymbolFlags): Symbol[]; + getSymbolOfNode(node: Node): Symbol; getSymbolAtLocation(node: Node): Symbol; getSymbolsOfParameterPropertyDeclaration(parameter: ParameterDeclaration, parameterName: string): Symbol[]; getShorthandAssignmentValueSymbol(location: Node): Symbol; @@ -2760,7 +2762,7 @@ namespace ts { objectFlags: ObjectFlags; } - // Class and interface types (TypeFlags.Class and TypeFlags.Interface) + /** Class and interface types (TypeFlags.Class and TypeFlags.Interface). */ export interface InterfaceType extends ObjectType { typeParameters: TypeParameter[]; // Type parameters (undefined if non-generic) outerTypeParameters: TypeParameter[]; // Outer type parameters (undefined if none) @@ -2780,14 +2782,16 @@ namespace ts { declaredNumberIndexInfo: IndexInfo; // Declared numeric indexing info } - // Type references (TypeFlags.Reference). When a class or interface has type parameters or - // a "this" type, references to the class or interface are made using type references. The - // typeArguments property specifies the types to substitute for the type parameters of the - // class or interface and optionally includes an extra element that specifies the type to - // substitute for "this" in the resulting instantiation. When no extra argument is present, - // the type reference itself is substituted for "this". The typeArguments property is undefined - // if the class or interface has no type parameters and the reference isn't specifying an - // explicit "this" argument. + /** + * Type references (TypeFlags.Reference). When a class or interface has type parameters or + * a "this" type, references to the class or interface are made using type references. The + * typeArguments property specifies the types to substitute for the type parameters of the + * class or interface and optionally includes an extra element that specifies the type to + * substitute for "this" in the resulting instantiation. When no extra argument is present, + * the type reference itself is substituted for "this". The typeArguments property is undefined + * if the class or interface has no type parameters and the reference isn't specifying an + * explicit "this" argument. + */ export interface TypeReference extends ObjectType { target: GenericType; // Type reference target typeArguments: Type[]; // Type reference type arguments (undefined if none) @@ -2825,7 +2829,6 @@ namespace ts { finalArrayType?: Type; // Final array type of evolving array type } - /* @internal */ // Resolved object, union, or intersection type export interface ResolvedType extends ObjectType, UnionOrIntersectionType { members: SymbolTable; // Properties by name diff --git a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts index 3a209fbc27d..23b10fc91ba 100644 --- a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts +++ b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts @@ -11,17 +11,20 @@ namespace ts.codefix { if (token.kind === SyntaxKind.Identifier && isClassLike(token.parent)) { const classDeclaration = token.parent; const startPos = classDeclaration.members.pos; - const abstractClassMembers = ts.map(getNamedAbstractClassMembers(classDeclaration), member => member.name.getText()); - const trackingAddedMembers: string[] = []; - const extendsClause = ts.getClassExtendsHeritageClauseElement(classDeclaration); - const textChanges = getCodeFixChanges(extendsClause, abstractClassMembers, startPos, checker, /*reference*/ false, trackingAddedMembers, context.newLineCharacter); - - if (textChanges.length > 0) { + // const abstractClassMembers = ts.map(getNamedAbstractClassMembers(classDeclaration), member => member.name.getText()); + // const trackingAddedMembers: string[] = []; + // const extendsClause = ts.getClassExtendsHeritageClauseElement(classDeclaration); + // const textChanges = getCodeFixChanges(extendsClause, abstractClassMembers, startPos, checker, /*reference*/ false, trackingAddedMembers, context.newLineCharacter); + const insertion = getMissingAbstractMemberInsertion(classDeclaration, checker, context.newLineCharacter); + if (insertion.length > 0) { return [{ description: getLocaleSpecificMessage(Diagnostics.Implement_inherited_abstract_class), changes: [{ fileName: sourceFile.fileName, - textChanges: textChanges + textChanges: [{ + span: {start: startPos, length: 0}, + newText: insertion + }] }] }]; } diff --git a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts index c8c84715a3d..3244cc0afb9 100644 --- a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts +++ b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts @@ -19,6 +19,7 @@ namespace ts.codefix { for (let i = 0; interfaceClauses && i < interfaceClauses.length; i++) { const newChanges = getCodeFixChanges(interfaceClauses[i], classMembers, startPos, checker, /*reference*/ false, trackingAddedMembers, context.newLineCharacter); + // getMissingAbstractMemberChanges(classDeclaration, checker, context.newLineCharacter); textChanges = textChanges ? textChanges.concat(newChanges) : newChanges; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 3777d8b8ac3..4d732c06c6d 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1359,60 +1359,139 @@ namespace ts { }; } - /* - const classMembers: TypeElement[]; // TODO: this - const implementedInterfaceMembers: TypeElement[] = []; - const parentAbstractMembers: TypeElement[] = [] - const missingMembers: TypeElement[] = []; - // const typeWiththis = checker.getTypeWithThisArgument(classType); - // const staticType = checker.getTypeOfSymbol(symbol); - // const classType = checker.getTypeAtLocation(classDecl); - */ + /* + const classMembers: TypeElement[]; // TODO: this + const implementedInterfaceMembers: TypeElement[] = []; + const parentAbstractMembers: TypeElement[] = [] + const missingMembers: TypeElement[] = []; + // const typeWiththis = checker.getTypeWithThisArgument(classType); + // const staticType = checker.getTypeOfSymbol(symbol); + // const classType = checker.getTypeAtLocation(classDecl); + */ - export function getUnimplementedMemberChanges(classDecl: ClassDeclaration, checker: TypeChecker): TextChange[] { + // TODO: (arozga) Get changes for interface as well. + // const implementedTypeNodes = getClassImplementsHeritageClauseElements(classDecl); + export function getMissingAbstractMemberInsertion(classDecl: ClassDeclaration, checker: TypeChecker, newlineChar: string): string { const baseTypeNode: ExpressionWithTypeArguments = getClassExtendsHeritageClauseElement(classDecl); - if (!baseTypeNode) - { - return []; + if (!baseTypeNode) { + return ""; // TODO: (arozga) undefined? } - const classSymbol = checker.getSymbolAtLocation(classDecl); - const classType = checker.getDeclaredTypeOfSymbol(classSymbol); - - const baseTypes = checker.getBaseTypes(classType); - Debug.assert(baseTypes.length === 1); - const baseType = baseTypes[0]; + const classSymbol = checker.getSymbolOfNode(classDecl); + // TODO: (arozga) Should this be getTypeOfSymbol? + // We want the once that gets the members. I think that's the instance, so we want typeofsymbol. + const classType = checker.getTypeOfSymbol(classSymbol); - const resolvedClassType = checker.resolveStructuredTypeMembers(classType); - const resolvedBaseType = checker.resolveStructuredTypeMembers(baseType); + const baseTypes = checker.getBaseTypes(classType); + Debug.assert(baseTypes.length === 1); + const baseType = baseTypes[0]; - const missingMembers = filterMissingMembers(resolvedClassType.members, resolvedBaseType.members); - return insertionsForMembers(missingMembers); - } + // TODO: (arozga) Does this give us the correct instantiations for generics? + // TODO: (arozga) If not, how do we get them? + const resolvedClassType = checker.resolveStructuredTypeMembers(classType); + const resolvedBaseType = checker.resolveStructuredTypeMembers(baseType); - // TODO: (arozga) Get changes for interface as well. - // const implementedTypeNodes = getClassImplementsHeritageClauseElements(classDecl); + // TODO: (arozga) handle private members as well. + const missingMembers = filterMissingMembers(filterAbstract(resolvedBaseType.members), resolvedClassType.members); + + return insertionsForMembers(missingMembers, newlineChar); } - function insertionsForMembers(symbols: Symbol[]): TextChange[] { - let changes: TextChange[] = []; - for (const symbol of symbols) { - const decl = getdeclaration - switch (member.kind) { - case SyntaxKind.PropertySignature: - case SyntaxKind.PropertyDeclaration: - break; - case SyntaxKind.MethodSignature: - case SyntaxKind.MethodDeclaration: - break; - default: - break; + function filterSymbolMapByDecl(symbolMap: Map, pred: (decl: Declaration) => boolean): Map { + let result = createMap(); + for (const key in symbolMap) { + const decl = symbolMap[key].getDeclarations(); + Debug.assert(!!(decl && decl.length)); + if (pred(decl[0])) { + result[key] = symbolMap[key]; } } - return changes; + return result; } - // TODO: (arozga) simplify to quadratic time solution. + function filterAbstract(symbolMap: Map) { + return filterSymbolMapByDecl(symbolMap, decl => !!(getModifierFlags(decl) & ModifierFlags.Abstract)); + } + + function filterNonPrivate(symbolMap: Map) { + return filterSymbolMapByDecl(symbolMap, decl => !(getModifierFlags(decl) & ModifierFlags.Private)); + } + + function insertionsForMembers(symbols: Symbol[], newlineChar: string): string { + let insertion = ""; + for (const symbol of symbols) { + const decls = symbol.getDeclarations(); + if(!(decls && decls.length)) { + return ""; + } + insertion = insertion.concat(getInsertion(decls[0], newlineChar)); + } + return insertion; + } + + const stubMethodBody = "{\nthrow new Error('Method not Implemented');\n}"; + const functionPrefix = "function "; + + // TODO: (arozga) needs a re-write that takes the symbol and type (with type parameter instantiations) + // and figures out how to print it. + function getInsertion(decl: Declaration, newlineChar: string): string { + let insertion = ""; + switch (decl.kind) { + case SyntaxKind.PropertySignature: + case SyntaxKind.PropertyDeclaration: + insertion = decl.getText(); + + // TODO: (arozga) need to remove trailing comma + if (insertion.length && insertion.charAt(insertion.length - 1) !== ";") { + insertion += ";"; + } + return insertion; + case SyntaxKind.MethodSignature: + case SyntaxKind.MethodDeclaration: + const method = decl as MethodSignature | MethodDeclaration; + // TODO: (arozga) figure out how to do proper instantiation of generic values based on heritage clause. + const typeParameters = method.typeParameters && method.typeParameters.length > 0 ? + getCommaSeparatedString(method.typeParameters.map(param => param.getText()), "<", ">") : ""; + const parameters = getCommaSeparatedString(method.parameters.map(param => param.getText()), "(", ")"); + insertion += functionPrefix + method.name + typeParameters + parameters + stubMethodBody; + break; + default: + break; + } + + return insertion += newlineChar; + + /** + * Flattens params into a comma-separated list, sandwiched by prefix + * and suffix on either end. + */ + function getCommaSeparatedString(params: string[], prefix: string, suffix: string) { + let result = prefix; + for(let i = 0; params && i < params.length; ++i) { + result += (i > 0 ? "," : "") + params[i]; + } + return result + suffix; + } + } + + /** + * Finds the symbols in source but not target. + */ + function filterMissingMembers(sourceSymbols: Map, targetSymbols: Map): Symbol[] { + let result: Symbol[] = []; + outer: + for(const sourceName in sourceSymbols) { + for(const targetName in targetSymbols) { + if(sourceName === targetName) { + continue outer; + } + } + result.push(sourceSymbols[sourceName]); + } + return result; + } + + /* function filterMissingMembers(sourceSymbols: Map, targetSymbols: Map): Symbol[] { let missingMembers: Symbol[] = []; const sortedSourceKeys = Object.keys(sourceSymbols).sort(); @@ -1439,6 +1518,7 @@ namespace ts { return missingMembers; } + */ /** * Generates codefix changes to insert @@ -1456,19 +1536,22 @@ namespace ts { for (const member of missingMembers) { if (member.kind === SyntaxKind.PropertySignature || member.kind === SyntaxKind.PropertyDeclaration) { const interfaceProperty = member; - if (trackingAddedMembers.indexOf(interfaceProperty.name.getText()) === -1) { - let propertyText = ""; - if (reference) { - propertyText = `${interfaceProperty.name.getText()} : ${getDefaultValue(interfaceProperty.type.kind)},${newLineCharacter}`; - } - else { - propertyText = interfaceProperty.getText(); - const stringToAdd = !(propertyText.match(/;$/)) ? `;${newLineCharacter}` : newLineCharacter; - propertyText += stringToAdd; - } - changesArray.push({ newText: propertyText, span: { start: startPos, length: 0 } }); - trackingAddedMembers.push(interfaceProperty.name.getText()); + if (trackingAddedMembers.indexOf(interfaceProperty.name.getText()) !== -1) { + continue; } + + let propertyText = ""; + if (reference) { + propertyText = `${interfaceProperty.name.getText()} : ${getDefaultValue(interfaceProperty.type.kind)},${newLineCharacter}`; + } + else { + propertyText = interfaceProperty.getText(); + const stringToAdd = !(propertyText.match(/;$/)) ? `;${newLineCharacter}` : newLineCharacter; + propertyText += stringToAdd; + } + changesArray.push({ newText: propertyText, span: { start: startPos, length: 0 } }); + trackingAddedMembers.push(interfaceProperty.name.getText()); + } else if (member.kind === SyntaxKind.MethodSignature || member.kind === SyntaxKind.MethodDeclaration) { const interfaceMethod = member;