diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 163cd7234a299..ee4743f1afb74 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -46,7 +46,7 @@ namespace ts.codefix { const { program, fixId } = context; const checker = program.getTypeChecker(); const seen = new Map(); - const typeDeclToMembers = new Map(); + const typeDeclToMembers = new Map(); return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { eachDiagnostic(context, errorCodes, diag => { @@ -68,7 +68,7 @@ namespace ts.codefix { if (info.kind === InfoKind.Enum) { addEnumMemberDeclaration(changes, checker, info); } - if (info.kind === InfoKind.ClassOrInterface) { + if (info.kind === InfoKind.TypeLikeDeclaration) { const { parentDeclaration, token } = info; const infos = getOrUpdate(typeDeclToMembers, parentDeclaration, () => []); if (!infos.some(i => i.token.text === token.text)) { @@ -78,11 +78,11 @@ namespace ts.codefix { } }); - typeDeclToMembers.forEach((infos, classDeclaration) => { - const supers = getAllSupers(classDeclaration, checker); + typeDeclToMembers.forEach((infos, declaration) => { + const supers = isTypeLiteralNode(declaration) ? undefined : getAllSupers(declaration, checker); for (const info of infos) { // If some superclass added this property, don't add it again. - if (supers.some(superClassOrInterface => { + if (supers?.some(superClassOrInterface => { const superInfos = typeDeclToMembers.get(superClassOrInterface); return !!superInfos && superInfos.some(({ token }) => token.text === info.token.text); })) continue; @@ -93,11 +93,11 @@ namespace ts.codefix { addMethodDeclaration(context, changes, call, token, modifierFlags & ModifierFlags.Static, parentDeclaration, declSourceFile); } else { - if (isJSFile && !isInterfaceDeclaration(parentDeclaration)) { + if (isJSFile && !isInterfaceDeclaration(parentDeclaration) && !isTypeLiteralNode(parentDeclaration)) { addMissingMemberInJs(changes, declSourceFile, parentDeclaration, token, !!(modifierFlags & ModifierFlags.Static)); } else { - const typeNode = getTypeNode(program.getTypeChecker(), parentDeclaration, token); + const typeNode = getTypeNode(checker, parentDeclaration, token); addPropertyDeclaration(changes, declSourceFile, parentDeclaration, token.text, typeNode, modifierFlags & ModifierFlags.Static); } } @@ -107,8 +107,8 @@ namespace ts.codefix { }, }); - const enum InfoKind { Enum, ClassOrInterface, Function, ObjectLiteral, JsxAttributes } - type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo; + const enum InfoKind { TypeLikeDeclaration, Enum, Function, ObjectLiteral, JsxAttributes } + type Info = TypeLikeDeclarationInfo | EnumInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo; interface EnumInfo { readonly kind: InfoKind.Enum; @@ -116,12 +116,12 @@ namespace ts.codefix { readonly parentDeclaration: EnumDeclaration; } - interface ClassOrInterfaceInfo { - readonly kind: InfoKind.ClassOrInterface; + interface TypeLikeDeclarationInfo { + readonly kind: InfoKind.TypeLikeDeclaration; readonly call: CallExpression | undefined; readonly token: Identifier | PrivateIdentifier; readonly modifierFlags: ModifierFlags; - readonly parentDeclaration: ClassOrInterface; + readonly parentDeclaration: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; readonly declSourceFile: SourceFile; readonly isJSFile: boolean; } @@ -220,16 +220,17 @@ namespace ts.codefix { if (!classDeclaration && isPrivateIdentifier(token)) return undefined; // Prefer to change the class instead of the interface if they are merged - const classOrInterface = classDeclaration || find(symbol.declarations, isInterfaceDeclaration); - if (classOrInterface && !isSourceFileFromLibrary(program, classOrInterface.getSourceFile())) { - const makeStatic = ((leftExpressionType as TypeReference).target || leftExpressionType) !== checker.getDeclaredTypeOfSymbol(symbol); - if (makeStatic && (isPrivateIdentifier(token) || isInterfaceDeclaration(classOrInterface))) return undefined; - - const declSourceFile = classOrInterface.getSourceFile(); - const modifierFlags = (makeStatic ? ModifierFlags.Static : 0) | (startsWithUnderscore(token.text) ? ModifierFlags.Private : 0); + const declaration = classDeclaration || find(symbol.declarations, d => isInterfaceDeclaration(d) || isTypeLiteralNode(d)) as InterfaceDeclaration | TypeLiteralNode | undefined; + if (declaration && !isSourceFileFromLibrary(program, declaration.getSourceFile())) { + const makeStatic = !isTypeLiteralNode(declaration) && ((leftExpressionType as TypeReference).target || leftExpressionType) !== checker.getDeclaredTypeOfSymbol(symbol); + if (makeStatic && (isPrivateIdentifier(token) || isInterfaceDeclaration(declaration))) return undefined; + + const declSourceFile = declaration.getSourceFile(); + const modifierFlags = isTypeLiteralNode(declaration) ? ModifierFlags.None : + (makeStatic ? ModifierFlags.Static : ModifierFlags.None) | (startsWithUnderscore(token.text) ? ModifierFlags.Private : ModifierFlags.None); const isJSFile = isSourceFileJS(declSourceFile); const call = tryCast(parent.parent, isCallExpression); - return { kind: InfoKind.ClassOrInterface, token, call, modifierFlags, parentDeclaration: classOrInterface, declSourceFile, isJSFile }; + return { kind: InfoKind.TypeLikeDeclaration, token, call, modifierFlags, parentDeclaration: declaration, declSourceFile, isJSFile }; } const enumDeclaration = find(symbol.declarations, isEnumDeclaration); @@ -244,13 +245,13 @@ namespace ts.codefix { return program.isSourceFileFromExternalLibrary(node) || program.isSourceFileDefaultLibrary(node); } - function getActionsForMissingMemberDeclaration(context: CodeFixContext, info: ClassOrInterfaceInfo): CodeFixAction[] | undefined { + function getActionsForMissingMemberDeclaration(context: CodeFixContext, info: TypeLikeDeclarationInfo): CodeFixAction[] | undefined { return info.isJSFile ? singleElementArray(createActionForAddMissingMemberInJavascriptFile(context, info)) : createActionsForAddMissingMemberInTypeScriptFile(context, info); } - function createActionForAddMissingMemberInJavascriptFile(context: CodeFixContext, { parentDeclaration, declSourceFile, modifierFlags, token }: ClassOrInterfaceInfo): CodeFixAction | undefined { - if (isInterfaceDeclaration(parentDeclaration)) { + function createActionForAddMissingMemberInJavascriptFile(context: CodeFixContext, { parentDeclaration, declSourceFile, modifierFlags, token }: TypeLikeDeclarationInfo): CodeFixAction | undefined { + if (isInterfaceDeclaration(parentDeclaration) || isTypeLiteralNode(parentDeclaration)) { return undefined; } @@ -265,7 +266,7 @@ namespace ts.codefix { return createCodeFixAction(fixMissingMember, changes, [diagnostic, token.text], fixMissingMember, Diagnostics.Add_all_missing_members); } - function addMissingMemberInJs(changeTracker: textChanges.ChangeTracker, declSourceFile: SourceFile, classDeclaration: ClassLikeDeclaration, token: Identifier | PrivateIdentifier, makeStatic: boolean): void { + function addMissingMemberInJs(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, classDeclaration: ClassLikeDeclaration, token: Identifier | PrivateIdentifier, makeStatic: boolean): void { const tokenName = token.text; if (makeStatic) { if (classDeclaration.kind === SyntaxKind.ClassExpression) { @@ -273,7 +274,7 @@ namespace ts.codefix { } const className = classDeclaration.name!.getText(); const staticInitialization = initializePropertyToUndefined(factory.createIdentifier(className), tokenName); - changeTracker.insertNodeAfter(declSourceFile, classDeclaration, staticInitialization); + changeTracker.insertNodeAfter(sourceFile, classDeclaration, staticInitialization); } else if (isPrivateIdentifier(token)) { const property = factory.createPropertyDeclaration( @@ -286,10 +287,10 @@ namespace ts.codefix { const lastProp = getNodeToInsertPropertyAfter(classDeclaration); if (lastProp) { - changeTracker.insertNodeAfter(declSourceFile, lastProp, property); + changeTracker.insertNodeAfter(sourceFile, lastProp, property); } else { - changeTracker.insertNodeAtClassStart(declSourceFile, classDeclaration, property); + changeTracker.insertMemberAtStart(sourceFile, classDeclaration, property); } } else { @@ -298,7 +299,7 @@ namespace ts.codefix { return; } const propertyInitialization = initializePropertyToUndefined(factory.createThis(), tokenName); - changeTracker.insertNodeAtConstructorEnd(declSourceFile, classConstructor, propertyInitialization); + changeTracker.insertNodeAtConstructorEnd(sourceFile, classConstructor, propertyInitialization); } } @@ -306,7 +307,7 @@ namespace ts.codefix { return factory.createExpressionStatement(factory.createAssignment(factory.createPropertyAccessExpression(obj, propertyName), createUndefined())); } - function createActionsForAddMissingMemberInTypeScriptFile(context: CodeFixContext, { parentDeclaration, declSourceFile, modifierFlags, token }: ClassOrInterfaceInfo): CodeFixAction[] | undefined { + function createActionsForAddMissingMemberInTypeScriptFile(context: CodeFixContext, { parentDeclaration, declSourceFile, modifierFlags, token }: TypeLikeDeclarationInfo): CodeFixAction[] | undefined { const memberName = token.text; const isStatic = modifierFlags & ModifierFlags.Static; const typeNode = getTypeNode(context.program.getTypeChecker(), parentDeclaration, token); @@ -325,13 +326,13 @@ namespace ts.codefix { return actions; } - function getTypeNode(checker: TypeChecker, classDeclaration: ClassOrInterface, token: Node) { + function getTypeNode(checker: TypeChecker, node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, token: Node) { let typeNode: TypeNode | undefined; if (token.parent.parent.kind === SyntaxKind.BinaryExpression) { const binaryExpression = token.parent.parent as BinaryExpression; const otherExpression = token.parent === binaryExpression.left ? binaryExpression.right : binaryExpression.left; const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(otherExpression))); - typeNode = checker.typeToTypeNode(widenedType, classDeclaration, NodeBuilderFlags.NoTruncation); + typeNode = checker.typeToTypeNode(widenedType, node, NodeBuilderFlags.NoTruncation); } else { const contextualType = checker.getContextualType(token.parent as Expression); @@ -340,35 +341,33 @@ namespace ts.codefix { return typeNode || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword); } - function addPropertyDeclaration(changeTracker: textChanges.ChangeTracker, declSourceFile: SourceFile, classDeclaration: ClassOrInterface, tokenName: string, typeNode: TypeNode, modifierFlags: ModifierFlags): void { - const property = factory.createPropertyDeclaration( - /*decorators*/ undefined, - /*modifiers*/ modifierFlags ? factory.createNodeArray(factory.createModifiersFromModifierFlags(modifierFlags)) : undefined, - tokenName, - /*questionToken*/ undefined, - typeNode, - /*initializer*/ undefined); + function addPropertyDeclaration(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, tokenName: string, typeNode: TypeNode, modifierFlags: ModifierFlags): void { + const modifiers = modifierFlags ? factory.createNodeArray(factory.createModifiersFromModifierFlags(modifierFlags)) : undefined; + + const property = isClassLike(node) + ? factory.createPropertyDeclaration(/*decorators*/ undefined, modifiers, tokenName, /*questionToken*/ undefined, typeNode, /*initializer*/ undefined) + : factory.createPropertySignature(/*modifiers*/ undefined, tokenName, /*questionToken*/ undefined, typeNode); - const lastProp = getNodeToInsertPropertyAfter(classDeclaration); + const lastProp = getNodeToInsertPropertyAfter(node); if (lastProp) { - changeTracker.insertNodeAfter(declSourceFile, lastProp, property); + changeTracker.insertNodeAfter(sourceFile, lastProp, property); } else { - changeTracker.insertNodeAtClassStart(declSourceFile, classDeclaration, property); + changeTracker.insertMemberAtStart(sourceFile, node, property); } } // Gets the last of the first run of PropertyDeclarations, or undefined if the class does not start with a PropertyDeclaration. - function getNodeToInsertPropertyAfter(cls: ClassOrInterface): PropertyDeclaration | undefined { + function getNodeToInsertPropertyAfter(node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode): PropertyDeclaration | undefined { let res: PropertyDeclaration | undefined; - for (const member of cls.members) { + for (const member of node.members) { if (!isPropertyDeclaration(member)) break; res = member; } return res; } - function createAddIndexSignatureAction(context: CodeFixContext, declSourceFile: SourceFile, classDeclaration: ClassOrInterface, tokenName: string, typeNode: TypeNode): CodeFixAction { + function createAddIndexSignatureAction(context: CodeFixContext, sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, tokenName: string, typeNode: TypeNode): CodeFixAction { // Index signatures cannot have the static modifier. const stringTypeNode = factory.createKeywordTypeNode(SyntaxKind.StringKeyword); const indexingParameter = factory.createParameterDeclaration( @@ -385,12 +384,12 @@ namespace ts.codefix { [indexingParameter], typeNode); - const changes = textChanges.ChangeTracker.with(context, t => t.insertNodeAtClassStart(declSourceFile, classDeclaration, indexSignature)); + const changes = textChanges.ChangeTracker.with(context, t => t.insertMemberAtStart(sourceFile, node, indexSignature)); // No fixId here because code-fix-all currently only works on adding individual named properties. return createCodeFixActionWithoutFixAll(fixMissingMember, changes, [Diagnostics.Add_index_signature_for_property_0, tokenName]); } - function getActionsForMissingMethodDeclaration(context: CodeFixContext, info: ClassOrInterfaceInfo): CodeFixAction[] | undefined { + function getActionsForMissingMethodDeclaration(context: CodeFixContext, info: TypeLikeDeclarationInfo): CodeFixAction[] | undefined { const { parentDeclaration, declSourceFile, modifierFlags, token, call } = info; if (call === undefined) { return undefined; @@ -416,17 +415,18 @@ namespace ts.codefix { callExpression: CallExpression, name: Identifier, modifierFlags: ModifierFlags, - parentDeclaration: ClassOrInterface, + parentDeclaration: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, sourceFile: SourceFile, ): void { const importAdder = createImportAdder(sourceFile, context.program, context.preferences, context.host); - const methodDeclaration = createSignatureDeclarationFromCallExpression(SyntaxKind.MethodDeclaration, context, importAdder, callExpression, name, modifierFlags, parentDeclaration) as MethodDeclaration; - const containingMethodDeclaration = findAncestor(callExpression, n => isMethodDeclaration(n) || isConstructorDeclaration(n)); - if (containingMethodDeclaration && containingMethodDeclaration.parent === parentDeclaration) { - changes.insertNodeAfter(sourceFile, containingMethodDeclaration, methodDeclaration); + const kind = isClassLike(parentDeclaration) ? SyntaxKind.MethodDeclaration : SyntaxKind.MethodSignature; + const signatureDeclaration = createSignatureDeclarationFromCallExpression(kind, context, importAdder, callExpression, name, modifierFlags, parentDeclaration) as MethodDeclaration; + const containingMethodDeclaration = tryGetContainingMethodDeclaration(parentDeclaration, callExpression); + if (containingMethodDeclaration) { + changes.insertNodeAfter(sourceFile, containingMethodDeclaration, signatureDeclaration); } else { - changes.insertNodeAtClassStart(sourceFile, parentDeclaration, methodDeclaration); + changes.insertMemberAtStart(sourceFile, parentDeclaration, signatureDeclaration); } importAdder.writeFixes(changes); } @@ -601,4 +601,12 @@ namespace ts.codefix { return filter(targetProps, targetProp => isIdentifierText(targetProp.name, target, LanguageVariant.JSX) && !((targetProp.flags & SymbolFlags.Optional || getCheckFlags(targetProp) & CheckFlags.Partial) || seenNames.has(targetProp.escapedName))); } + + function tryGetContainingMethodDeclaration(node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, callExpression: CallExpression) { + if (isTypeLiteralNode(node)) { + return undefined; + } + const declaration = findAncestor(callExpression, n => isMethodDeclaration(n) || isConstructorDeclaration(n)); + return declaration && declaration.parent === node ? declaration : undefined; + } } diff --git a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts index bfcd890dea9b4..31e78e811413b 100644 --- a/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts +++ b/src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts @@ -42,7 +42,7 @@ namespace ts.codefix { const abstractAndNonPrivateExtendsSymbols = checker.getPropertiesOfType(instantiatedExtendsType).filter(symbolPointsToNonPrivateAndAbstractMember); const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host); - createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, sourceFile, context, preferences, importAdder, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member as ClassElement)); + createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, sourceFile, context, preferences, importAdder, member => changeTracker.insertMemberAtStart(sourceFile, classDeclaration, member as ClassElement)); importAdder.writeFixes(changeTracker); } diff --git a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts index f090bdabf8468..bb7d032390fb3 100644 --- a/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts +++ b/src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts @@ -80,7 +80,7 @@ namespace ts.codefix { changeTracker.insertNodeAfter(sourceFile, constructor, newElement); } else { - changeTracker.insertNodeAtClassStart(sourceFile, cls, newElement); + changeTracker.insertMemberAtStart(sourceFile, cls, newElement); } } } diff --git a/src/services/codefixes/generateAccessors.ts b/src/services/codefixes/generateAccessors.ts index 1e02160fbfc1d..92ad1f62f7b70 100644 --- a/src/services/codefixes/generateAccessors.ts +++ b/src/services/codefixes/generateAccessors.ts @@ -214,7 +214,7 @@ namespace ts.codefix { } function insertAccessor(changeTracker: textChanges.ChangeTracker, file: SourceFile, accessor: AccessorDeclaration, declaration: AcceptedDeclaration, container: ContainerDeclaration) { - isParameterPropertyDeclaration(declaration, declaration.parent) ? changeTracker.insertNodeAtClassStart(file, container as ClassLikeDeclaration, accessor) : + isParameterPropertyDeclaration(declaration, declaration.parent) ? changeTracker.insertMemberAtStart(file, container as ClassLikeDeclaration, accessor) : isPropertyAssignment(declaration) ? changeTracker.insertNodeAfterComma(file, declaration, accessor) : changeTracker.insertNodeAfter(file, declaration, accessor); } diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index c34148453b4f1..cc6575912beb1 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -277,7 +277,7 @@ namespace ts.codefix { } export function createSignatureDeclarationFromCallExpression( - kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionDeclaration, + kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionDeclaration | SyntaxKind.MethodSignature, context: CodeFixContextBase, importAdder: ImportAdder, call: CallExpression, @@ -313,29 +313,42 @@ namespace ts.codefix { ? undefined : checker.typeToTypeNode(contextualType, contextNode, /*flags*/ undefined, tracker); - if (kind === SyntaxKind.MethodDeclaration) { - return factory.createMethodDeclaration( - /*decorators*/ undefined, - modifiers, - asteriskToken, - name, - /*questionToken*/ undefined, - typeParameters, - parameters, - type, - isInterfaceDeclaration(contextNode) ? undefined : createStubbedMethodBody(quotePreference) - ); + switch (kind) { + case SyntaxKind.MethodDeclaration: + return factory.createMethodDeclaration( + /*decorators*/ undefined, + modifiers, + asteriskToken, + name, + /*questionToken*/ undefined, + typeParameters, + parameters, + type, + createStubbedMethodBody(quotePreference) + ); + case SyntaxKind.MethodSignature: + return factory.createMethodSignature( + modifiers, + name, + /*questionToken*/ undefined, + typeParameters, + parameters, + type + ); + case SyntaxKind.FunctionDeclaration: + return factory.createFunctionDeclaration( + /*decorators*/ undefined, + modifiers, + asteriskToken, + name, + typeParameters, + parameters, + type, + createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference) + ); + default: + Debug.fail("Unexpected kind"); } - return factory.createFunctionDeclaration( - /*decorators*/ undefined, - modifiers, - asteriskToken, - name, - typeParameters, - parameters, - type, - createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference) - ); } export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder: ImportAdder, type: Type, contextNode: Node | undefined, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined { diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index a4e0557222da3..be02cb0a4f26f 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -618,27 +618,27 @@ namespace ts.textChanges { }); } - public insertNodeAtClassStart(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration, newElement: ClassElement): void { - this.insertNodeAtStartWorker(sourceFile, cls, newElement); + public insertMemberAtStart(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, newElement: ClassElement | PropertySignature | MethodSignature): void { + this.insertNodeAtStartWorker(sourceFile, node, newElement); } public insertNodeAtObjectStart(sourceFile: SourceFile, obj: ObjectLiteralExpression, newElement: ObjectLiteralElementLike): void { this.insertNodeAtStartWorker(sourceFile, obj, newElement); } - private insertNodeAtStartWorker(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, newElement: ClassElement | ObjectLiteralElementLike): void { - const indentation = this.guessIndentationFromExistingMembers(sourceFile, cls) ?? this.computeIndentationForNewMember(sourceFile, cls); - this.insertNodeAt(sourceFile, getMembersOrProperties(cls).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, cls, indentation)); + private insertNodeAtStartWorker(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode, newElement: ClassElement | ObjectLiteralElementLike | PropertySignature | MethodSignature): void { + const indentation = this.guessIndentationFromExistingMembers(sourceFile, node) ?? this.computeIndentationForNewMember(sourceFile, node); + this.insertNodeAt(sourceFile, getMembersOrProperties(node).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation)); } /** * Tries to guess the indentation from the existing members of a class/interface/object. All members must be on * new lines and must share the same indentation. */ - private guessIndentationFromExistingMembers(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) { + private guessIndentationFromExistingMembers(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) { let indentation: number | undefined; - let lastRange: TextRange = cls; - for (const member of getMembersOrProperties(cls)) { + let lastRange: TextRange = node; + for (const member of getMembersOrProperties(node)) { if (rangeStartPositionsAreOnSameLine(lastRange, member, sourceFile)) { // each indented member must be on a new line return undefined; @@ -657,13 +657,13 @@ namespace ts.textChanges { return indentation; } - private computeIndentationForNewMember(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) { - const clsStart = cls.getStart(sourceFile); - return formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(clsStart, sourceFile), clsStart, sourceFile, this.formatContext.options) + private computeIndentationForNewMember(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) { + const nodeStart = node.getStart(sourceFile); + return formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(nodeStart, sourceFile), nodeStart, sourceFile, this.formatContext.options) + (this.formatContext.options.indentSize ?? 4); } - private getInsertNodeAtStartInsertOptions(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, indentation: number): InsertNodeOptions { + private getInsertNodeAtStartInsertOptions(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode, indentation: number): InsertNodeOptions { // Rules: // - Always insert leading newline. // - For object literals: @@ -674,11 +674,11 @@ namespace ts.textChanges { // - Only insert a trailing newline if body is single-line and there are no other insertions for the node. // NOTE: This is handled in `finishClassesWithNodesInsertedAtStart`. - const members = getMembersOrProperties(cls); + const members = getMembersOrProperties(node); const isEmpty = members.length === 0; - const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(cls), { node: cls, sourceFile }); - const insertTrailingComma = isObjectLiteralExpression(cls) && (!isJsonSourceFile(sourceFile) || !isEmpty); - const insertLeadingComma = isObjectLiteralExpression(cls) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion; + const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(node), { node, sourceFile }); + const insertTrailingComma = isObjectLiteralExpression(node) && (!isJsonSourceFile(sourceFile) || !isEmpty); + const insertLeadingComma = isObjectLiteralExpression(node) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion; return { indentation, prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter, @@ -998,8 +998,8 @@ namespace ts.textChanges { const close = findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile); return [open?.end, close?.end]; } - function getMembersOrProperties(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression): NodeArray { - return isObjectLiteralExpression(cls) ? cls.properties : cls.members; + function getMembersOrProperties(node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode): NodeArray { + return isObjectLiteralExpression(node) ? node.properties : node.members; } export type ValidateNonFormattedText = (node: Node, text: string) => void; diff --git a/tests/cases/fourslash/codeFixAddMissingMember22.ts b/tests/cases/fourslash/codeFixAddMissingMember22.ts new file mode 100644 index 0000000000000..c4847f3f16a2a --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingMember22.ts @@ -0,0 +1,15 @@ +/// + +////[|type Foo = {};|] +////function f(foo: Foo) { +//// foo.y; +////} + +verify.codeFix({ + description: [ts.Diagnostics.Declare_property_0.message, "y"], + index: 0, + newRangeContent: +`type Foo = { + y: any; +};` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingMember23.ts b/tests/cases/fourslash/codeFixAddMissingMember23.ts new file mode 100644 index 0000000000000..a51fa47aa703a --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingMember23.ts @@ -0,0 +1,15 @@ +/// + +////[|type Foo = {};|] +////function f(foo: Foo) { +//// foo.y = 1; +////} + +verify.codeFix({ + description: [ts.Diagnostics.Declare_property_0.message, "y"], + index: 0, + newRangeContent: +`type Foo = { + y: number; +};` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingMember24.ts b/tests/cases/fourslash/codeFixAddMissingMember24.ts new file mode 100644 index 0000000000000..3c58dc9eddd41 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingMember24.ts @@ -0,0 +1,15 @@ +/// + +////[|type Foo = {};|] +////function f(foo: Foo) { +//// foo.test(1, 1, ""); +////} + +verify.codeFix({ + description: [ts.Diagnostics.Declare_method_0.message, "test"], + index: 0, + newRangeContent: +`type Foo = { + test(arg0: number, arg1: number, arg2: string); +};` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingMember25.ts b/tests/cases/fourslash/codeFixAddMissingMember25.ts new file mode 100644 index 0000000000000..86f5488a0a319 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingMember25.ts @@ -0,0 +1,18 @@ +/// + +////[|type Foo = { +//// y: number; +////};|] +////function f(foo: Foo) { +//// foo.x = 1; +////} + +verify.codeFix({ + description: [ts.Diagnostics.Declare_property_0.message, "x"], + index: 0, + newRangeContent: +`type Foo = { + x: number; + y: number; +};` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingMember26.ts b/tests/cases/fourslash/codeFixAddMissingMember26.ts new file mode 100644 index 0000000000000..71f598793273d --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingMember26.ts @@ -0,0 +1,15 @@ +/// + +////[|type Foo = {};|] +////function f(foo: Foo) { +//// foo.x = 1; +////} + +verify.codeFix({ + description: [ts.Diagnostics.Add_index_signature_for_property_0.message, "x"], + index: 1, + newRangeContent: +`type Foo = { + [x: string]: number; +};` +}); diff --git a/tests/cases/fourslash/codeFixAddMissingMember_all.ts b/tests/cases/fourslash/codeFixAddMissingMember_all.ts index cd82089109342..aed949bf358f1 100644 --- a/tests/cases/fourslash/codeFixAddMissingMember_all.ts +++ b/tests/cases/fourslash/codeFixAddMissingMember_all.ts @@ -24,6 +24,13 @@ //// ////enum En {} ////En.A; +//// +////type T = {}; +////function foo(t: T) { +//// t.x; +//// t.y = 1; +//// t.test(1, 2); +////} verify.codeFixAll({ fixId: "fixMissingMember", @@ -60,5 +67,16 @@ class Unrelated { enum En { A } -En.A;`, +En.A; + +type T = { + x: any; + y: number; + test(arg0: number, arg1: number); +}; +function foo(t: T) { + t.x; + t.y = 1; + t.test(1, 2); +}`, });