diff --git a/src/services/codefixes/addMissingDeclareMethod.ts b/src/services/codefixes/addMissingDeclareMethod.ts new file mode 100644 index 0000000000000..17ff1280f40c0 --- /dev/null +++ b/src/services/codefixes/addMissingDeclareMethod.ts @@ -0,0 +1,814 @@ +import { + createCodeFixAction, + createCodeFixActionWithoutFixAll, + createCombinedCodeActions, + createImportAdder, + createSignatureDeclarationFromCallExpression, + createSignatureDeclarationFromSignature, + createStubbedBody, + eachDiagnostic, + ImportAdder, + registerCodeFix, +} from "../_namespaces/ts.codefix.js"; +import { + __String, + addToSeen, + arrayFrom, + BigIntLiteralType, + BinaryExpression, + CallExpression, + CheckFlags, + ClassLikeDeclaration, + CodeFixAction, + CodeFixContext, + CodeFixContextBase, + concatenate, + createPropertyNameNodeForIdentifierOrLiteral, + Debug, + Diagnostics, + emptyArray, + EnumDeclaration, + Expression, + factory, + filter, + find, + findAncestor, + findIndex, + firstDefined, + firstOrUndefined, + firstOrUndefinedIterator, + FunctionExpression, + getCheckFlags, + getClassExtendsHeritageElement, + getClassLikeDeclarationOfSymbol, + getEmitScriptTarget, + getEscapedTextOfJsxAttributeName, + getFirstConstructorWithBody, + getNodeId, + getObjectFlags, + getOrUpdate, + getQuotePreference, + getSourceFileOfNode, + getTokenAtPosition, + hasAbstractModifier, + hasInitializer, + Identifier, + idText, + InterfaceDeclaration, + InternalNodeBuilderFlags, + isCallExpression, + isClassLike, + isComputedPropertyName, + isConstructorDeclaration, + isEnumDeclaration, + isFunctionTypeNode, + isIdentifier, + isIdentifierText, + isInterfaceDeclaration, + isJsxAttribute, + isJsxExpression, + isJsxOpeningLikeElement, + isJsxSpreadAttribute, + isMemberName, + isMethodDeclaration, + isMethodSignature, + isModuleDeclaration, + isObjectLiteralExpression, + isParameter, + isPrivateIdentifier, + isPropertyAccessExpression, + isPropertyDeclaration, + isReturnStatement, + isSatisfiesExpression, + isSourceFile, + isSourceFileFromLibrary, + isSourceFileJS, + isTransientSymbol, + isTypeLiteralNode, + isYieldExpression, + JsxOpeningLikeElement, + LanguageVariant, + lastOrUndefined, + length, + map, + MethodDeclaration, + ModifierFlags, + ModuleDeclaration, + Node, + NodeBuilderFlags, + NumberLiteralType, + ObjectFlags, + ObjectLiteralExpression, + or, + PrivateIdentifier, + Program, + PropertyDeclaration, + QuotePreference, + ReturnStatement, + ScriptTarget, + setParent, + Signature, + SignatureKind, + singleElementArray, + singleOrUndefined, + skipConstraint, + some, + SourceFile, + startsWithUnderscore, + StringLiteralType, + Symbol, + SymbolFlags, + SyntaxKind, + textChanges, + tryCast, + Type, + TypeChecker, + TypeFlags, + TypeLiteralNode, + TypeNode, + TypeReference, + UnionType, +} from "../_namespaces/ts.js"; + +const fixMissingMember = "fixMissingMember"; +const fixMissingProperties = "fixMissingProperties"; +const fixMissingAttributes = "fixMissingAttributes"; +const fixMissingFunctionDeclaration = "fixMissingFunctionDeclaration"; + +const errorCodes = [ + Diagnostics.Property_0_does_not_exist_on_type_1.code, + Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code, + Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2.code, + Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2.code, + Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more.code, + Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code, + Diagnostics.Cannot_find_name_0.code, + Diagnostics.Type_0_does_not_satisfy_the_expected_type_1.code, +]; + +enum InfoKind { + TypeLikeDeclaration, + Enum, + Function, + ObjectLiteral, + JsxAttributes, + Signature, +} + +registerCodeFix({ + errorCodes, + getCodeActions(context) { + const typeChecker = context.program.getTypeChecker(); + const info = getInfo(context.sourceFile, context.span.start, context.errorCode, typeChecker, context.program); + if (!info) { + return undefined; + } + if (info.kind === InfoKind.ObjectLiteral) { + const changes = textChanges.ChangeTracker.with(context, t => addObjectLiteralProperties(t, context, info)); + return [createCodeFixAction(fixMissingProperties, changes, Diagnostics.Add_missing_properties, fixMissingProperties, Diagnostics.Add_all_missing_properties)]; + } + if (info.kind === InfoKind.JsxAttributes) { + const changes = textChanges.ChangeTracker.with(context, t => addJsxAttributes(t, context, info)); + return [createCodeFixAction(fixMissingAttributes, changes, Diagnostics.Add_missing_attributes, fixMissingAttributes, Diagnostics.Add_all_missing_attributes)]; + } + if (info.kind === InfoKind.Function || info.kind === InfoKind.Signature) { + const changes = textChanges.ChangeTracker.with(context, t => addFunctionDeclaration(t, context, info)); + return [createCodeFixAction(fixMissingFunctionDeclaration, changes, [Diagnostics.Add_missing_function_declaration_0, info.token.text], fixMissingFunctionDeclaration, Diagnostics.Add_all_missing_function_declarations)]; + } + if (info.kind === InfoKind.Enum) { + const changes = textChanges.ChangeTracker.with(context, t => addEnumMemberDeclaration(t, context.program.getTypeChecker(), info)); + return [createCodeFixAction(fixMissingMember, changes, [Diagnostics.Add_missing_enum_member_0, info.token.text], fixMissingMember, Diagnostics.Add_all_missing_members)]; + } + return concatenate(getActionsForMissingMethodDeclaration(context, info), getActionsForMissingMemberDeclaration(context, info)); + }, + fixIds: [fixMissingMember, fixMissingFunctionDeclaration, fixMissingProperties, fixMissingAttributes], + getAllCodeActions: context => { + const { program, fixId } = context; + const checker = program.getTypeChecker(); + const seen = new Set(); + const typeDeclToMembers = new Map(); + + return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { + eachDiagnostic(context, errorCodes, diag => { + const info = getInfo(diag.file, diag.start, diag.code, checker, context.program); + if (info === undefined) return; + + const nodeId = getNodeId(info.parentDeclaration) + "#" + (info.kind === InfoKind.ObjectLiteral ? info.identifier || getNodeId(info.token) : info.token.text); + if (!addToSeen(seen, nodeId)) return; + + if (fixId === fixMissingFunctionDeclaration && (info.kind === InfoKind.Function || info.kind === InfoKind.Signature)) { + addFunctionDeclaration(changes, context, info); + } + else if (fixId === fixMissingProperties && info.kind === InfoKind.ObjectLiteral) { + addObjectLiteralProperties(changes, context, info); + } + else if (fixId === fixMissingAttributes && info.kind === InfoKind.JsxAttributes) { + addJsxAttributes(changes, context, info); + } + else { + if (info.kind === InfoKind.Enum) { + addEnumMemberDeclaration(changes, checker, info); + } + if (info.kind === InfoKind.TypeLikeDeclaration) { + const { parentDeclaration, token } = info; + const infos = getOrUpdate(typeDeclToMembers, parentDeclaration, () => []); + if (!infos.some(i => i.token.text === token.text)) { + infos.push(info); + } + } + } + }); + + 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 => { + const superInfos = typeDeclToMembers.get(superClassOrInterface); + return !!superInfos && superInfos.some(({ token }) => token.text === info.token.text); + }) + ) continue; + + const { parentDeclaration, declSourceFile, modifierFlags, token, call, isJSFile } = info; + // Always prefer to add a method declaration if possible. + if (call && !isPrivateIdentifier(token)) { + addMethodDeclaration(context, changes, call, token, modifierFlags & ModifierFlags.Static, parentDeclaration, declSourceFile); + } + else { + if (isJSFile && !isInterfaceDeclaration(parentDeclaration) && !isTypeLiteralNode(parentDeclaration)) { + addMissingMemberInJs(changes, declSourceFile, parentDeclaration, token, !!(modifierFlags & ModifierFlags.Static)); + } + else { + const typeNode = getTypeNode(checker, parentDeclaration, token); + addPropertyDeclaration(changes, declSourceFile, parentDeclaration, token.text, typeNode, modifierFlags & ModifierFlags.Static); + } + } + } + }); + })); + }, +}); + +type Info = TypeLikeDeclarationInfo | EnumInfo | FunctionInfo | ObjectLiteralInfo | JsxAttributesInfo | SignatureInfo; + +interface EnumInfo { + readonly kind: InfoKind.Enum; + readonly token: Identifier; + readonly parentDeclaration: EnumDeclaration; +} + +interface TypeLikeDeclarationInfo { + readonly kind: InfoKind.TypeLikeDeclaration; + readonly call: CallExpression | undefined; + readonly token: Identifier | PrivateIdentifier; + readonly modifierFlags: ModifierFlags; + readonly parentDeclaration: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode; + readonly declSourceFile: SourceFile; + readonly isJSFile: boolean; +} + +interface FunctionInfo { + readonly kind: InfoKind.Function; + readonly call: CallExpression; + readonly token: Identifier; + readonly sourceFile: SourceFile; + readonly modifierFlags: ModifierFlags; + readonly parentDeclaration: SourceFile | ModuleDeclaration | ReturnStatement; +} + +interface ObjectLiteralInfo { + readonly kind: InfoKind.ObjectLiteral; + readonly token: Node; + readonly identifier: string | undefined; + readonly properties: Symbol[]; + readonly parentDeclaration: ObjectLiteralExpression; + readonly indentation?: number; +} + +interface JsxAttributesInfo { + readonly kind: InfoKind.JsxAttributes; + readonly token: Identifier; + readonly attributes: Symbol[]; + readonly parentDeclaration: JsxOpeningLikeElement; +} + +interface SignatureInfo { + readonly kind: InfoKind.Signature; + readonly token: Identifier; + readonly signature: Signature; + readonly sourceFile: SourceFile; + readonly parentDeclaration: Node; +} + +function getInfo(sourceFile: SourceFile, tokenPos: number, errorCode: number, checker: TypeChecker, program: Program): Info | undefined { + // The identifier of the missing property. eg: + // this.missing = 1; + // ^^^^^^^ + const token = getTokenAtPosition(sourceFile, tokenPos); + const parent = token.parent; + + if (errorCode === Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code) { + if (!(token.kind === SyntaxKind.OpenBraceToken && isObjectLiteralExpression(parent) && isCallExpression(parent.parent))) return undefined; + + const argIndex = findIndex(parent.parent.arguments, arg => arg === parent); + if (argIndex < 0) return undefined; + + const signature = checker.getResolvedSignature(parent.parent); + if (!(signature && signature.declaration && signature.parameters[argIndex])) return undefined; + + const param = signature.parameters[argIndex].valueDeclaration; + if (!(param && isParameter(param) && isIdentifier(param.name))) return undefined; + + const properties = arrayFrom(checker.getUnmatchedProperties(checker.getTypeAtLocation(parent), checker.getParameterType(signature, argIndex).getNonNullableType(), /*requireOptionalProperties*/ false, /*matchDiscriminantProperties*/ false)); + if (!length(properties)) return undefined; + return { kind: InfoKind.ObjectLiteral, token: param.name, identifier: param.name.text, properties, parentDeclaration: parent }; + } + + if (token.kind === SyntaxKind.OpenBraceToken || isSatisfiesExpression(parent) || isReturnStatement(parent)) { + const expression = (isSatisfiesExpression(parent) || isReturnStatement(parent)) && parent.expression ? parent.expression : parent; + if (isObjectLiteralExpression(expression)) { + const targetType = isSatisfiesExpression(parent) ? checker.getTypeFromTypeNode(parent.type) : + checker.getContextualType(expression) || checker.getTypeAtLocation(expression); + const properties = arrayFrom(checker.getUnmatchedProperties(checker.getTypeAtLocation(parent), targetType.getNonNullableType(), /*requireOptionalProperties*/ false, /*matchDiscriminantProperties*/ false)); + if (!length(properties)) return undefined; + + return { kind: InfoKind.ObjectLiteral, token: parent, identifier: undefined, properties, parentDeclaration: expression, indentation: isReturnStatement(expression.parent) || isYieldExpression(expression.parent) ? 0 : undefined }; + } + } + + if (!isMemberName(token)) return undefined; + + if (isIdentifier(token) && hasInitializer(parent) && parent.initializer && isObjectLiteralExpression(parent.initializer)) { + const targetType = (checker.getContextualType(token) || checker.getTypeAtLocation(token))?.getNonNullableType(); + const properties = arrayFrom(checker.getUnmatchedProperties(checker.getTypeAtLocation(parent.initializer), targetType, /*requireOptionalProperties*/ false, /*matchDiscriminantProperties*/ false)); + if (!length(properties)) return undefined; + + return { kind: InfoKind.ObjectLiteral, token, identifier: token.text, properties, parentDeclaration: parent.initializer }; + } + + if (isIdentifier(token) && isJsxOpeningLikeElement(token.parent)) { + const target = getEmitScriptTarget(program.getCompilerOptions()); + const attributes = getUnmatchedAttributes(checker, target, token.parent); + if (!length(attributes)) return undefined; + return { kind: InfoKind.JsxAttributes, token, attributes, parentDeclaration: token.parent }; + } + + if (isIdentifier(token)) { + const type = checker.getContextualType(token)?.getNonNullableType(); + if (type && getObjectFlags(type) & ObjectFlags.Anonymous) { + const signature = firstOrUndefined(checker.getSignaturesOfType(type, SignatureKind.Call)); + if (signature === undefined) return undefined; + return { kind: InfoKind.Signature, token, signature, sourceFile, parentDeclaration: findScope(token) }; + } + if (isCallExpression(parent) && parent.expression === token) { + return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: findScope(token) }; + } + } + + if (!isPropertyAccessExpression(parent)) return undefined; + + const leftExpressionType = skipConstraint(checker.getTypeAtLocation(parent.expression)); + const symbol = leftExpressionType.symbol; + if (!symbol || !symbol.declarations) return undefined; + + if (isIdentifier(token) && isCallExpression(parent.parent)) { + const moduleDeclaration = find(symbol.declarations, isModuleDeclaration); + const moduleDeclarationSourceFile = moduleDeclaration?.getSourceFile(); + if (moduleDeclaration && moduleDeclarationSourceFile && !isSourceFileFromLibrary(program, moduleDeclarationSourceFile)) { + return { kind: InfoKind.Function, token, call: parent.parent, sourceFile: moduleDeclarationSourceFile, modifierFlags: ModifierFlags.Export, parentDeclaration: moduleDeclaration }; + } + + const moduleSourceFile = find(symbol.declarations, isSourceFile); + if (sourceFile.commonJsModuleIndicator) return undefined; + + if (moduleSourceFile && !isSourceFileFromLibrary(program, moduleSourceFile)) { + return { kind: InfoKind.Function, token, call: parent.parent, sourceFile: moduleSourceFile, modifierFlags: ModifierFlags.Export, parentDeclaration: moduleSourceFile }; + } + } + + const classDeclaration = find(symbol.declarations, isClassLike); + // Don't suggest adding private identifiers to anything other than a class. + if (!classDeclaration && isPrivateIdentifier(token)) return undefined; + + // Prefer to change the class instead of the interface if they are merged + const declaration = classDeclaration || find(symbol.declarations, d => isInterfaceDeclaration(d) || isTypeLiteralNode(d)); + 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.TypeLikeDeclaration, token, call, modifierFlags, parentDeclaration: declaration, declSourceFile, isJSFile }; + } + + const enumDeclaration = find(symbol.declarations, isEnumDeclaration); + if (enumDeclaration && !(leftExpressionType.flags & TypeFlags.EnumLike) && !isPrivateIdentifier(token) && !isSourceFileFromLibrary(program, enumDeclaration.getSourceFile())) { + return { kind: InfoKind.Enum, token, parentDeclaration: enumDeclaration }; + } + + return 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 }: TypeLikeDeclarationInfo): CodeFixAction | undefined { + if (isInterfaceDeclaration(parentDeclaration) || isTypeLiteralNode(parentDeclaration)) { + return undefined; + } + + const changes = textChanges.ChangeTracker.with(context, t => addMissingMemberInJs(t, declSourceFile, parentDeclaration, token, !!(modifierFlags & ModifierFlags.Static))); + if (changes.length === 0) { + return undefined; + } + + const diagnostic = modifierFlags & ModifierFlags.Static ? Diagnostics.Initialize_static_property_0 : + isPrivateIdentifier(token) ? Diagnostics.Declare_a_private_field_named_0 : Diagnostics.Initialize_property_0_in_the_constructor; + + return createCodeFixAction(fixMissingMember, changes, [diagnostic, token.text], fixMissingMember, Diagnostics.Add_all_missing_members); +} + +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) { + return; + } + const className = classDeclaration.name!.getText(); + const staticInitialization = initializePropertyToUndefined(factory.createIdentifier(className), tokenName); + changeTracker.insertNodeAfter(sourceFile, classDeclaration, staticInitialization); + } + else if (isPrivateIdentifier(token)) { + const property = factory.createPropertyDeclaration( + /*modifiers*/ undefined, + tokenName, + /*questionOrExclamationToken*/ undefined, + /*type*/ undefined, + /*initializer*/ undefined, + ); + + const lastProp = getNodeToInsertPropertyAfter(classDeclaration); + if (lastProp) { + changeTracker.insertNodeAfter(sourceFile, lastProp, property); + } + else { + changeTracker.insertMemberAtStart(sourceFile, classDeclaration, property); + } + } + else { + const classConstructor = getFirstConstructorWithBody(classDeclaration); + if (!classConstructor) { + return; + } + const propertyInitialization = initializePropertyToUndefined(factory.createThis(), tokenName); + changeTracker.insertNodeAtConstructorEnd(sourceFile, classConstructor, propertyInitialization); + } +} + +function initializePropertyToUndefined(obj: Expression, propertyName: string) { + return factory.createExpressionStatement(factory.createAssignment(factory.createPropertyAccessExpression(obj, propertyName), createUndefined())); +} + +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); + const addPropertyDeclarationChanges = (modifierFlags: ModifierFlags) => textChanges.ChangeTracker.with(context, t => addPropertyDeclaration(t, declSourceFile, parentDeclaration, memberName, typeNode, modifierFlags)); + + const actions = [createCodeFixAction(fixMissingMember, addPropertyDeclarationChanges(modifierFlags & ModifierFlags.Static), [isStatic ? Diagnostics.Declare_static_property_0 : Diagnostics.Declare_property_0, memberName], fixMissingMember, Diagnostics.Add_all_missing_members)]; + if (isStatic || isPrivateIdentifier(token)) { + return actions; + } + + if (modifierFlags & ModifierFlags.Private) { + actions.unshift(createCodeFixActionWithoutFixAll(fixMissingMember, addPropertyDeclarationChanges(ModifierFlags.Private), [Diagnostics.Declare_private_property_0, memberName])); + } + + actions.push(createAddIndexSignatureAction(context, declSourceFile, parentDeclaration, token.text, typeNode)); + return actions; +} + +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, node, NodeBuilderFlags.NoTruncation, InternalNodeBuilderFlags.AllowUnresolvedNames); + } + else { + const contextualType = checker.getContextualType(token.parent as Expression); + typeNode = contextualType ? checker.typeToTypeNode(contextualType, /*enclosingDeclaration*/ undefined, NodeBuilderFlags.NoTruncation, InternalNodeBuilderFlags.AllowUnresolvedNames) : undefined; + } + return typeNode || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword); +} + +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(modifiers, tokenName, /*questionOrExclamationToken*/ undefined, typeNode, /*initializer*/ undefined) + : factory.createPropertySignature(/*modifiers*/ undefined, tokenName, /*questionToken*/ undefined, typeNode); + + const lastProp = getNodeToInsertPropertyAfter(node); + if (lastProp) { + changeTracker.insertNodeAfter(sourceFile, lastProp, property); + } + else { + 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(node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode): PropertyDeclaration | undefined { + let res: PropertyDeclaration | undefined; + for (const member of node.members) { + if (!isPropertyDeclaration(member)) break; + res = member; + } + return res; +} + +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( + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, + "x", + /*questionToken*/ undefined, + stringTypeNode, + /*initializer*/ undefined, + ); + const indexSignature = factory.createIndexSignature( + /*modifiers*/ undefined, + [indexingParameter], + typeNode, + ); + + 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: TypeLikeDeclarationInfo): CodeFixAction[] | undefined { + const { parentDeclaration, declSourceFile, modifierFlags, token, call } = info; + if (call === undefined) { + return undefined; + } + + const methodName = token.text; + const addMethodDeclarationChanges = (modifierFlags: ModifierFlags) => textChanges.ChangeTracker.with(context, t => addMethodDeclaration(context, t, call, token, modifierFlags, parentDeclaration, declSourceFile)); + const actions = [createCodeFixAction(fixMissingMember, addMethodDeclarationChanges(modifierFlags & ModifierFlags.Static), [modifierFlags & ModifierFlags.Static ? Diagnostics.Declare_static_method_0 : Diagnostics.Declare_method_0, methodName], fixMissingMember, Diagnostics.Add_all_missing_members)]; + if (modifierFlags & ModifierFlags.Private) { + actions.unshift(createCodeFixActionWithoutFixAll(fixMissingMember, addMethodDeclarationChanges(ModifierFlags.Private), [Diagnostics.Declare_private_method_0, methodName])); + } + return actions; +} + +function addMethodDeclaration( + context: CodeFixContextBase, + changes: textChanges.ChangeTracker, + callExpression: CallExpression, + name: Identifier | PrivateIdentifier, + modifierFlags: ModifierFlags, + parentDeclaration: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, + sourceFile: SourceFile, +): void { + const importAdder = createImportAdder(sourceFile, context.program, context.preferences, context.host); + 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.insertMemberAtStart(sourceFile, parentDeclaration, signatureDeclaration); + } + importAdder.writeFixes(changes); +} + +function addEnumMemberDeclaration(changes: textChanges.ChangeTracker, checker: TypeChecker, { token, parentDeclaration }: EnumInfo) { + /** + * create initializer only literal enum that has string initializer. + * value of initializer is a string literal that equal to name of enum member. + * numeric enum or empty enum will not create initializer. + */ + const hasStringInitializer = some(parentDeclaration.members, member => { + const type = checker.getTypeAtLocation(member); + return !!(type && type.flags & TypeFlags.StringLike); + }); + const sourceFile = parentDeclaration.getSourceFile(); + const enumMember = factory.createEnumMember(token, hasStringInitializer ? factory.createStringLiteral(token.text) : undefined); + const last = lastOrUndefined(parentDeclaration.members); + if (last) { + changes.insertNodeInListAfter(sourceFile, last, enumMember, parentDeclaration.members); + } + else { + changes.insertMemberAtStart(sourceFile, parentDeclaration, enumMember); + } +} + +function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo | SignatureInfo) { + const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); + const functionDeclaration = info.kind === InfoKind.Function + ? createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) + : createSignatureDeclarationFromSignature(SyntaxKind.FunctionDeclaration, context, quotePreference, info.signature, createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference), info.token, /*modifiers*/ undefined, /*optional*/ undefined, /*enclosingDeclaration*/ undefined, importAdder); + if (functionDeclaration === undefined) { + Debug.fail("fixMissingFunctionDeclaration codefix got unexpected error."); + } + + isReturnStatement(info.parentDeclaration) + ? changes.insertNodeBefore(info.sourceFile, info.parentDeclaration, functionDeclaration, /*blankLineBetween*/ true) + : changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration); + importAdder.writeFixes(changes); +} + +function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: JsxAttributesInfo) { + const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); + const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const checker = context.program.getTypeChecker(); + const jsxAttributesNode = info.parentDeclaration.attributes; + const hasSpreadAttribute = some(jsxAttributesNode.properties, isJsxSpreadAttribute); + const attrs = map(info.attributes, attr => { + const value = tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeOfSymbol(attr), info.parentDeclaration); + const name = factory.createIdentifier(attr.name); + const jsxAttribute = factory.createJsxAttribute(name, factory.createJsxExpression(/*dotDotDotToken*/ undefined, value)); + // formattingScanner requires the Identifier to have a context for scanning attributes with "-" (data-foo). + setParent(name, jsxAttribute); + return jsxAttribute; + }); + const jsxAttributes = factory.createJsxAttributes(hasSpreadAttribute ? [...attrs, ...jsxAttributesNode.properties] : [...jsxAttributesNode.properties, ...attrs]); + const options = { prefix: jsxAttributesNode.pos === jsxAttributesNode.end ? " " : undefined }; + changes.replaceNode(context.sourceFile, jsxAttributesNode, jsxAttributes, options); + importAdder.writeFixes(changes); +} + +function addObjectLiteralProperties(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: ObjectLiteralInfo) { + const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); + const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const target = getEmitScriptTarget(context.program.getCompilerOptions()); + const checker = context.program.getTypeChecker(); + const props = map(info.properties, prop => { + const initializer = tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeOfSymbol(prop), info.parentDeclaration); + return factory.createPropertyAssignment(createPropertyNameFromSymbol(prop, target, quotePreference, checker), initializer); + }); + const options = { + leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude, + trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude, + indentation: info.indentation, + }; + changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true), options); + importAdder.writeFixes(changes); +} + +function tryGetValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type, enclosingDeclaration: Node | undefined): Expression { + if (type.flags & TypeFlags.AnyOrUnknown) { + return createUndefined(); + } + if (type.flags & (TypeFlags.String | TypeFlags.TemplateLiteral)) { + return factory.createStringLiteral("", /* isSingleQuote */ quotePreference === QuotePreference.Single); + } + if (type.flags & TypeFlags.Number) { + return factory.createNumericLiteral(0); + } + if (type.flags & TypeFlags.BigInt) { + return factory.createBigIntLiteral("0n"); + } + if (type.flags & TypeFlags.Boolean) { + return factory.createFalse(); + } + if (type.flags & TypeFlags.EnumLike) { + const enumMember = type.symbol.exports ? firstOrUndefinedIterator(type.symbol.exports.values()) : type.symbol; + const symbol = type.symbol.parent && type.symbol.parent.flags & SymbolFlags.RegularEnum ? type.symbol.parent : type.symbol; + const name = checker.symbolToExpression(symbol, SymbolFlags.Value, /*enclosingDeclaration*/ undefined, /*flags*/ NodeBuilderFlags.UseFullyQualifiedType); + return enumMember === undefined || name === undefined ? factory.createNumericLiteral(0) : factory.createPropertyAccessExpression(name, checker.symbolToString(enumMember)); + } + if (type.flags & TypeFlags.NumberLiteral) { + return factory.createNumericLiteral((type as NumberLiteralType).value); + } + if (type.flags & TypeFlags.BigIntLiteral) { + return factory.createBigIntLiteral((type as BigIntLiteralType).value); + } + if (type.flags & TypeFlags.StringLiteral) { + return factory.createStringLiteral((type as StringLiteralType).value, /* isSingleQuote */ quotePreference === QuotePreference.Single); + } + if (type.flags & TypeFlags.BooleanLiteral) { + return (type === checker.getFalseType() || type === checker.getFalseType(/*fresh*/ true)) ? factory.createFalse() : factory.createTrue(); + } + if (type.flags & TypeFlags.Null) { + return factory.createNull(); + } + if (type.flags & TypeFlags.Union) { + const expression = firstDefined((type as UnionType).types, t => tryGetValueFromType(context, checker, importAdder, quotePreference, t, enclosingDeclaration)); + return expression ?? createUndefined(); + } + if (checker.isArrayLikeType(type)) { + return factory.createArrayLiteralExpression(); + } + if (isObjectLiteralType(type)) { + const props = map(checker.getPropertiesOfType(type), prop => { + const initializer = tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeOfSymbol(prop), enclosingDeclaration); + return factory.createPropertyAssignment(prop.name, initializer); + }); + return factory.createObjectLiteralExpression(props, /*multiLine*/ true); + } + if (getObjectFlags(type) & ObjectFlags.Anonymous) { + const decl = find(type.symbol.declarations || emptyArray, or(isFunctionTypeNode, isMethodSignature, isMethodDeclaration)); + if (decl === undefined) return createUndefined(); + + const signature = checker.getSignaturesOfType(type, SignatureKind.Call); + if (signature === undefined) return createUndefined(); + + const func = createSignatureDeclarationFromSignature(SyntaxKind.FunctionExpression, context, quotePreference, signature[0], createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference), /*name*/ undefined, /*modifiers*/ undefined, /*optional*/ undefined, /*enclosingDeclaration*/ enclosingDeclaration, importAdder) as FunctionExpression | undefined; + return func ?? createUndefined(); + } + if (getObjectFlags(type) & ObjectFlags.Class) { + const classDeclaration = getClassLikeDeclarationOfSymbol(type.symbol); + if (classDeclaration === undefined || hasAbstractModifier(classDeclaration)) return createUndefined(); + + const constructorDeclaration = getFirstConstructorWithBody(classDeclaration); + if (constructorDeclaration && length(constructorDeclaration.parameters)) return createUndefined(); + + return factory.createNewExpression(factory.createIdentifier(type.symbol.name), /*typeArguments*/ undefined, /*argumentsArray*/ undefined); + } + return createUndefined(); +} + +function createUndefined() { + return factory.createIdentifier("undefined"); +} + +function isObjectLiteralType(type: Type) { + return (type.flags & TypeFlags.Object) && + ((getObjectFlags(type) & ObjectFlags.ObjectLiteral) || (type.symbol && tryCast(singleOrUndefined(type.symbol.declarations), isTypeLiteralNode))); +} + +function getUnmatchedAttributes(checker: TypeChecker, target: ScriptTarget, source: JsxOpeningLikeElement) { + const attrsType = checker.getContextualType(source.attributes); + if (attrsType === undefined) return emptyArray; + + const targetProps = attrsType.getProperties(); + if (!length(targetProps)) return emptyArray; + + const seenNames = new Set<__String>(); + for (const sourceProp of source.attributes.properties) { + if (isJsxAttribute(sourceProp)) { + seenNames.add(getEscapedTextOfJsxAttributeName(sourceProp.name)); + } + if (isJsxSpreadAttribute(sourceProp)) { + const type = checker.getTypeAtLocation(sourceProp.expression); + for (const prop of type.getProperties()) { + seenNames.add(prop.escapedName); + } + } + } + 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; +} + +function createPropertyNameFromSymbol(symbol: Symbol, target: ScriptTarget, quotePreference: QuotePreference, checker: TypeChecker) { + if (isTransientSymbol(symbol)) { + const prop = checker.symbolToNode(symbol, SymbolFlags.Value, /*enclosingDeclaration*/ undefined, /*flags*/ undefined, InternalNodeBuilderFlags.WriteComputedProps); + if (prop && isComputedPropertyName(prop)) return prop; + } + // We're using these nodes as property names in an object literal; no need to quote names when not needed. + return createPropertyNameNodeForIdentifierOrLiteral(symbol.name, target, quotePreference === QuotePreference.Single, /*stringNamed*/ false, /*isMethod*/ false); +} + +function findScope(node: Node) { + if (findAncestor(node, isJsxExpression)) { + const returnStatement = findAncestor(node.parent, isReturnStatement); + if (returnStatement) return returnStatement; + } + return getSourceFileOfNode(node); +} + +function getAllSupers(decl: ClassLikeDeclaration | InterfaceDeclaration | undefined, checker: TypeChecker): readonly ClassLikeDeclaration[] { + const res: ClassLikeDeclaration[] = []; + while (decl) { + const superElement = getClassExtendsHeritageElement(decl); + const superSymbol = superElement && checker.getSymbolAtLocation(superElement.expression); + if (!superSymbol) break; + const symbol = superSymbol.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(superSymbol) : superSymbol; + const superDecl = symbol.declarations && find(symbol.declarations, isClassLike); + if (!superDecl) break; + res.push(superDecl); + decl = superDecl; + } + return res; +} diff --git a/src/services/codefixes/addMissingDeclareMethod.ts.bak b/src/services/codefixes/addMissingDeclareMethod.ts.bak new file mode 100644 index 0000000000000..598195c23d192 --- /dev/null +++ b/src/services/codefixes/addMissingDeclareMethod.ts.bak @@ -0,0 +1,49 @@ +import { + codeFixAll, + createCodeFixAction, + registerCodeFix, +} from "../_namespaces/ts.codefix.js"; +import { + Diagnostics, + getTokenAtPosition, + isIdentifier, + Node, + SourceFile, + SyntaxKind, + textChanges, + tryAddToSet, +} from "../_namespaces/ts.js"; + +const fixId = "addMissingDeclareProperty"; +const errorCodes = [ + Diagnostics.Property_0_will_overwrite_the_base_property_in_1_If_this_is_intentional_add_an_initializer_Otherwise_add_a_declare_modifier_or_remove_the_redundant_declaration.code, +]; + +registerCodeFix({ + errorCodes, + getCodeActions: function getCodeActionsToAddMissingDeclareOnProperty(context) { + const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span.start)); + if (changes.length > 0) { + return [createCodeFixAction(fixId, changes, Diagnostics.Prefix_with_declare, fixId, Diagnostics.Prefix_all_incorrect_property_declarations_with_declare)]; + } + }, + fixIds: [fixId], + getAllCodeActions: context => { + const fixedNodes = new Set(); + return codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag.start, fixedNodes)); + }, +}); + +function makeChange(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, pos: number, fixedNodes?: Set) { + const token = getTokenAtPosition(sourceFile, pos); + if (!isIdentifier(token)) { + return; + } + const declaration = token.parent; + if ( + declaration.kind === SyntaxKind.PropertyDeclaration && + (!fixedNodes || tryAddToSet(fixedNodes, declaration)) + ) { + changeTracker.insertModifierBefore(sourceFile, SyntaxKind.DeclareKeyword, declaration); + } +} diff --git a/tests/cases/fourslash/codeFixAddMissingMethod.ts b/tests/cases/fourslash/codeFixAddMissingMethod.ts new file mode 100644 index 0000000000000..f5bdf567b2063 --- /dev/null +++ b/tests/cases/fourslash/codeFixAddMissingMethod.ts @@ -0,0 +1,18 @@ +/// + +////class C { +//// method() { +//// this.foo = 10; +//// } +////} + +verify.codeFix({ + description: "Declare property 'foo'", + index: 0, + newFileContent: `class C { + foo: number; + method() { + this.foo = 10; + } +}` +});