diff --git a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts index 16edce0a516..513853e4614 100644 --- a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts +++ b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts @@ -19,10 +19,9 @@ namespace ts.codefix { const checker = context.program.getTypeChecker(); if (isClassLike(token.parent)) { - const classDecl = token.parent as ClassLikeDeclaration; - const startPos = classDecl.members.pos; + const classDeclaration = token.parent as ClassLikeDeclaration; - const extendsNode = getClassExtendsHeritageClauseElement(classDecl); + const extendsNode = getClassExtendsHeritageClauseElement(classDeclaration); const instantiatedExtendsType = checker.getTypeAtLocation(extendsNode); // Note that this is ultimately derived from a map indexed by symbol names, @@ -30,18 +29,12 @@ namespace ts.codefix { const extendsSymbols = checker.getPropertiesOfType(instantiatedExtendsType); const abstractAndNonPrivateExtendsSymbols = extendsSymbols.filter(symbolPointsToNonPrivateAndAbstractMember); - const insertion = getMissingMembersInsertion(classDecl, abstractAndNonPrivateExtendsSymbols, checker, context.newLineCharacter); - - if (insertion.length) { + const newNodes = createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, checker); + const changes = newNodesToChanges(newNodes, getOpenBraceOfClassLike(classDeclaration, sourceFile), context); + if(changes && changes.length > 0) { return [{ description: getLocaleSpecificMessage(Diagnostics.Implement_inherited_abstract_class), - changes: [{ - fileName: sourceFile.fileName, - textChanges: [{ - span: { start: startPos, length: 0 }, - newText: insertion - }] - }] + changes }]; } } diff --git a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts index 67b2242c8d9..a5bde70c011 100644 --- a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts +++ b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts @@ -16,7 +16,7 @@ namespace ts.codefix { return undefined; } - const startPos: number = classDecl.members.pos; + const openBrace = getOpenBraceOfClassLike(classDecl, sourceFile); const classType = checker.getTypeAtLocation(classDecl) as InterfaceType; const implementedTypeNodes = getClassImplementsHeritageClauseElements(classDecl); @@ -31,43 +31,46 @@ namespace ts.codefix { const implementedTypeSymbols = checker.getPropertiesOfType(implementedType); const nonPrivateMembers = implementedTypeSymbols.filter(symbol => !(getModifierFlags(symbol.valueDeclaration) & ModifierFlags.Private)); - let insertion = getMissingIndexSignatureInsertion(implementedType, IndexKind.Number, classDecl, hasNumericIndexSignature); - insertion += getMissingIndexSignatureInsertion(implementedType, IndexKind.String, classDecl, hasStringIndexSignature); - insertion += getMissingMembersInsertion(classDecl, nonPrivateMembers, checker, context.newLineCharacter); - + let newNodes: Node[] = []; + createAndAddMissingIndexSignatureDeclaration(implementedType, IndexKind.Number, hasNumericIndexSignature, newNodes); + createAndAddMissingIndexSignatureDeclaration(implementedType, IndexKind.String, hasStringIndexSignature, newNodes); + newNodes = newNodes.concat(createMissingMemberNodes(classDecl, nonPrivateMembers, checker)); const message = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Implement_interface_0), [implementedTypeNode.getText()]); - if (insertion) { - pushAction(result, insertion, message); + if (newNodes.length > 0) { + pushAction(result, newNodes, message); } } return result; - function getMissingIndexSignatureInsertion(type: InterfaceType, kind: IndexKind, enclosingDeclaration: ClassLikeDeclaration, hasIndexSigOfKind: boolean) { - if (!hasIndexSigOfKind) { - const IndexInfoOfKind = checker.getIndexInfoOfType(type, kind); - if (IndexInfoOfKind) { - const writer = getSingleLineStringWriter(); - checker.getSymbolDisplayBuilder().buildIndexSignatureDisplay(IndexInfoOfKind, writer, kind, enclosingDeclaration); - const result = writer.string(); - releaseStringWriter(writer); - - return result; - } + function createAndAddMissingIndexSignatureDeclaration(type: InterfaceType, kind: IndexKind, hasIndexSigOfKind: boolean, newNodes: Node[]): void { + if (hasIndexSigOfKind) { + return undefined; } - return ""; + + const indexInfoOfKind = checker.getIndexInfoOfType(type, kind); + + if (!indexInfoOfKind) { + return undefined; + } + const typeNode = checker.createTypeNode(indexInfoOfKind.type); + let name: string; + const newIndexSignatureDeclaration = createIndexSignatureDeclaration( + [createParameter( + /*decorators*/undefined + , /*modifiers*/ undefined + , /*dotDotDotToken*/ undefined + , name + , /*questionToken*/ undefined + , kind === IndexKind.String ? createKeywordTypeNode(SyntaxKind.StringKeyword) : createKeywordTypeNode(SyntaxKind.NumberKeyword))] + , typeNode); + newNodes.push(newIndexSignatureDeclaration); } - function pushAction(result: CodeAction[], insertion: string, description: string): void { + function pushAction(result: CodeAction[], newNodes: Node[], description: string): void { const newAction: CodeAction = { description: description, - changes: [{ - fileName: sourceFile.fileName, - textChanges: [{ - span: { start: startPos, length: 0 }, - newText: insertion - }] - }] + changes: newNodesToChanges(newNodes, openBrace, context) }; result.push(newAction); } diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index d20fc0129cd..0af13bf41e4 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -1,31 +1,53 @@ /* @internal */ namespace ts.codefix { + export function newNodesToChanges(newNodes: Node[], insertAfter: Node, context: CodeFixContext) { + const sourceFile = context.sourceFile; + if (!(newNodes)) { + // TODO: make the appropriate value flow through gracefully. + throw new Error("newNodesToChanges expects an array"); + } + + const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); + + for (let i = newNodes.length - 1; i >= 0; i--) { + changeTracker.insertNodeAfter(sourceFile, insertAfter, newNodes[i], { insertTrailingNewLine: true }); + } + return changeTracker.getChanges(); + } + /** * Finds members of the resolved type that are missing in the class pointed to by class decl * and generates source code for the missing members. * @param possiblyMissingSymbols The collection of symbols to filter and then get insertions for. * @returns Empty string iff there are no member insertions. */ - export function getMissingMembersInsertion(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: Symbol[], checker: TypeChecker, newlineChar: string): string { + export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: Symbol[], checker: TypeChecker): Node[] { const classMembers = classDeclaration.symbol.members; const missingMembers = possiblyMissingSymbols.filter(symbol => !classMembers.has(symbol.getName())); - let insertion = ""; - + let newNodes: Node[] = []; for (const symbol of missingMembers) { - insertion = insertion.concat(getInsertionForMemberSymbol(symbol, classDeclaration, checker, newlineChar)); + const newNode = getNewNodeForMemberSymbol(symbol, classDeclaration, checker); + if (newNode) { + if (Array.isArray(newNode)) { + newNodes = newNodes.concat(newNode); + } + else { + newNodes.push(newNode); + } + } } - return insertion; + return newNodes; } /** * @returns Empty string iff there we can't figure out a representation for `symbol` in `enclosingDeclaration`. */ - function getInsertionForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, checker: TypeChecker, newlineChar: string): string { + function getNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, checker: TypeChecker): Node[] | Node | undefined { const declarations = symbol.getDeclarations(); if (!(declarations && declarations.length)) { - return ""; + return undefined; } const declaration = declarations[0] as Declaration; @@ -39,9 +61,16 @@ namespace ts.codefix { case SyntaxKind.SetAccessor: case SyntaxKind.PropertySignature: case SyntaxKind.PropertyDeclaration: - const typeString = checker.typeToString(type, enclosingDeclaration, TypeFormatFlags.None); - return `${visibility}${name}: ${typeString};${newlineChar}`; - + const typeNode = checker.createTypeNode(type); + // TODO: add modifiers. + const property = createProperty( + /*decorators*/undefined + , /*modifiers*/ undefined + , name + , /*questionToken*/ undefined + , typeNode + , /*initializer*/ undefined); + return property; case SyntaxKind.MethodSignature: case SyntaxKind.MethodDeclaration: // The signature for the implementation appears as an entry in `signatures` iff @@ -53,18 +82,22 @@ namespace ts.codefix { // correspondence of declarations and signatures. const signatures = checker.getSignaturesOfType(type, SignatureKind.Call); if (!(signatures && signatures.length > 0)) { - return ""; + return undefined; } if (declarations.length === 1) { Debug.assert(signatures.length === 1); - const sigString = checker.signatureToString(signatures[0], enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); - return getStubbedMethod(visibility, name, sigString, newlineChar); + // TODO: extract signature declaration from a signature. + // const sigString = checker.signatureToString(signatures[0], enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); + // TODO: get parameters working. + // TODO: add support for type parameters. + return createStubbedMethod([visibility], name, /*typeParameters*/undefined, []); } - let result = ""; + let signatureDeclarations = []; for (let i = 0; i < signatures.length; i++) { - const sigString = checker.signatureToString(signatures[i], enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); - result += `${visibility}${name}${sigString};${newlineChar}`; + // const sigString = checker.signatureToString(signatures[i], enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); + // TODO: make signatures instead of methods + signatureDeclarations.push(createStubbedMethod([visibility], name, /*typeParameters*/undefined, [])); } // If there is a declaration with a body, it is the last declaration, @@ -77,12 +110,12 @@ namespace ts.codefix { Debug.assert(declarations.length === signatures.length); bodySig = createBodySignatureWithAnyTypes(signatures, enclosingDeclaration, checker); } - const sigString = checker.signatureToString(bodySig, enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); - result += getStubbedMethod(visibility, name, sigString, newlineChar); - - return result; + // const sigString = checker.signatureToString(bodySig, enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); + signatureDeclarations.push(createStubbedMethod([visibility], name, /*typeParameters*/undefined, [])); + + return signatureDeclarations; default: - return ""; + return undefined; } } @@ -138,22 +171,36 @@ namespace ts.codefix { } } - export function getStubbedMethod(visibility: string, name: string, sigString = "()", newlineChar: string): string { - return `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`; + export function createStubbedMethod(modifiers: Modifier[], name: string, typeParameters: TypeParameterDeclaration[] | undefined, parameters: ParameterDeclaration[], returnType?: TypeNode) { + return createMethod( + /*decorators*/undefined + , /*modifiers*/modifiers + , /*asteriskToken*/undefined + , name + , typeParameters + , parameters + , returnType + , createStubbedMethodBody()); } - function getMethodBodyStub(newlineChar: string) { - return ` {${newlineChar}throw new Error('Method not implemented.');${newlineChar}}${newlineChar}`; + function createStubbedMethodBody() { + return createBlock( + [createThrow( + createNew( + createIdentifier('Error') + , /*typeArguments*/undefined + , [createLiteral('Method not implemented.')]))] + , /*multiline*/true); } - function getVisibilityPrefixWithSpace(flags: ModifierFlags): string { + function getVisibilityPrefixWithSpace(flags: ModifierFlags) { if (flags & ModifierFlags.Public) { - return "public "; + return createToken(SyntaxKind.PublicKeyword); } else if (flags & ModifierFlags.Protected) { - return "protected "; + return createToken(SyntaxKind.ProtectedKeyword); } - return ""; + return undefined; } const SymbolConstructor = objectAllocator.getSymbolConstructor();