1/* @internal */ 2namespace ts.codefix { 3 const fixMissingMember = "fixMissingMember"; 4 const fixMissingFunctionDeclaration = "fixMissingFunctionDeclaration"; 5 const errorCodes = [ 6 Diagnostics.Property_0_does_not_exist_on_type_1.code, 7 Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code, 8 Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2.code, 9 Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2.code, 10 Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more.code, 11 Diagnostics.Cannot_find_name_0.code 12 ]; 13 14 registerCodeFix({ 15 errorCodes, 16 getCodeActions(context) { 17 const typeChecker = context.program.getTypeChecker(); 18 const info = getInfo(context.sourceFile, context.span.start, typeChecker, context.program); 19 if (!info) { 20 return undefined; 21 } 22 if (info.kind === InfoKind.Function) { 23 const changes = textChanges.ChangeTracker.with(context, t => addFunctionDeclaration(t, context, info)); 24 return [createCodeFixAction(fixMissingFunctionDeclaration, changes, [Diagnostics.Add_missing_function_declaration_0, info.token.text], fixMissingFunctionDeclaration, Diagnostics.Add_all_missing_function_declarations)]; 25 } 26 if (info.kind === InfoKind.Enum) { 27 const changes = textChanges.ChangeTracker.with(context, t => addEnumMemberDeclaration(t, context.program.getTypeChecker(), info)); 28 return [createCodeFixAction(fixMissingMember, changes, [Diagnostics.Add_missing_enum_member_0, info.token.text], fixMissingMember, Diagnostics.Add_all_missing_members)]; 29 } 30 return concatenate(getActionsForMissingMethodDeclaration(context, info), getActionsForMissingMemberDeclaration(context, info)); 31 }, 32 fixIds: [fixMissingMember, fixMissingFunctionDeclaration], 33 getAllCodeActions: context => { 34 const { program, fixId } = context; 35 const checker = program.getTypeChecker(); 36 const seen = new Map<string, true>(); 37 const typeDeclToMembers = new Map<ClassOrInterface, ClassOrInterfaceInfo[]>(); 38 39 return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { 40 eachDiagnostic(context, errorCodes, diag => { 41 const info = getInfo(diag.file, diag.start, checker, context.program); 42 if (!info || !addToSeen(seen, getNodeId(info.parentDeclaration) + "#" + info.token.text)) { 43 return; 44 } 45 46 if (fixId === fixMissingFunctionDeclaration) { 47 if (info.kind === InfoKind.Function) { 48 addFunctionDeclaration(changes, context, info); 49 } 50 } 51 else { 52 if (info.kind === InfoKind.Enum) { 53 addEnumMemberDeclaration(changes, checker, info); 54 } 55 56 if (info.kind === InfoKind.ClassOrInterface) { 57 const { parentDeclaration, token } = info; 58 const infos = getOrUpdate(typeDeclToMembers, parentDeclaration, () => []); 59 if (!infos.some(i => i.token.text === token.text)) { 60 infos.push(info); 61 } 62 } 63 } 64 }); 65 66 typeDeclToMembers.forEach((infos, classDeclaration) => { 67 const supers = getAllSupers(classDeclaration, checker); 68 for (const info of infos) { 69 // If some superclass added this property, don't add it again. 70 if (supers.some(superClassOrInterface => { 71 const superInfos = typeDeclToMembers.get(superClassOrInterface); 72 return !!superInfos && superInfos.some(({ token }) => token.text === info.token.text); 73 })) continue; 74 75 const { parentDeclaration, declSourceFile, modifierFlags, token, call, isJSFile } = info; 76 // Always prefer to add a method declaration if possible. 77 if (call && !isPrivateIdentifier(token)) { 78 addMethodDeclaration(context, changes, call, token, modifierFlags & ModifierFlags.Static, parentDeclaration, declSourceFile); 79 } 80 else { 81 if (isJSFile && !isInterfaceDeclaration(parentDeclaration)) { 82 addMissingMemberInJs(changes, declSourceFile, parentDeclaration, token, !!(modifierFlags & ModifierFlags.Static)); 83 } 84 else { 85 const typeNode = getTypeNode(program.getTypeChecker(), parentDeclaration, token); 86 addPropertyDeclaration(changes, declSourceFile, parentDeclaration, token.text, typeNode, modifierFlags & ModifierFlags.Static); 87 } 88 } 89 } 90 }); 91 })); 92 }, 93 }); 94 95 const enum InfoKind { Enum, ClassOrInterface, Function } 96 type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo; 97 98 interface EnumInfo { 99 readonly kind: InfoKind.Enum; 100 readonly token: Identifier; 101 readonly parentDeclaration: EnumDeclaration; 102 } 103 104 interface ClassOrInterfaceInfo { 105 readonly kind: InfoKind.ClassOrInterface; 106 readonly call: CallExpression | undefined; 107 readonly token: Identifier | PrivateIdentifier; 108 readonly modifierFlags: ModifierFlags; 109 readonly parentDeclaration: ClassOrInterface; 110 readonly declSourceFile: SourceFile; 111 readonly isJSFile: boolean; 112 } 113 114 interface FunctionInfo { 115 readonly kind: InfoKind.Function; 116 readonly call: CallExpression; 117 readonly token: Identifier; 118 readonly sourceFile: SourceFile; 119 readonly modifierFlags: ModifierFlags; 120 readonly parentDeclaration: SourceFile | ModuleDeclaration; 121 } 122 123 function getInfo(sourceFile: SourceFile, tokenPos: number, checker: TypeChecker, program: Program): Info | undefined { 124 // The identifier of the missing property. eg: 125 // this.missing = 1; 126 // ^^^^^^^ 127 const token = getTokenAtPosition(sourceFile, tokenPos); 128 if (!isIdentifier(token) && !isPrivateIdentifier(token)) { 129 return undefined; 130 } 131 132 const { parent } = token; 133 if (isIdentifier(token) && isCallExpression(parent)) { 134 return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile }; 135 } 136 137 if (!isPropertyAccessExpression(parent)) { 138 return undefined; 139 } 140 141 const leftExpressionType = skipConstraint(checker.getTypeAtLocation(parent.expression)); 142 const { symbol } = leftExpressionType; 143 if (!symbol || !symbol.declarations) { 144 return undefined; 145 } 146 147 if (isIdentifier(token) && isCallExpression(parent.parent)) { 148 const moduleDeclaration = find(symbol.declarations, isModuleDeclaration); 149 const moduleDeclarationSourceFile = moduleDeclaration?.getSourceFile(); 150 if (moduleDeclaration && moduleDeclarationSourceFile && !program.isSourceFileFromExternalLibrary(moduleDeclarationSourceFile)) { 151 return { kind: InfoKind.Function, token, call: parent.parent, sourceFile, modifierFlags: ModifierFlags.Export, parentDeclaration: moduleDeclaration }; 152 } 153 154 const moduleSourceFile = find(symbol.declarations, isSourceFile); 155 if (sourceFile.commonJsModuleIndicator) { 156 return; 157 } 158 159 if (moduleSourceFile && !program.isSourceFileFromExternalLibrary(moduleSourceFile)) { 160 return { kind: InfoKind.Function, token, call: parent.parent, sourceFile: moduleSourceFile, modifierFlags: ModifierFlags.Export, parentDeclaration: moduleSourceFile }; 161 } 162 } 163 164 const classDeclaration = find(symbol.declarations, isClassLike); 165 // Don't suggest adding private identifiers to anything other than a class. 166 if (!classDeclaration && isPrivateIdentifier(token)) { 167 return undefined; 168 } 169 170 // Prefer to change the class instead of the interface if they are merged 171 const classOrInterface = classDeclaration || find(symbol.declarations, isInterfaceDeclaration); 172 if (classOrInterface && !program.isSourceFileFromExternalLibrary(classOrInterface.getSourceFile())) { 173 const makeStatic = ((leftExpressionType as TypeReference).target || leftExpressionType) !== checker.getDeclaredTypeOfSymbol(symbol); 174 if (makeStatic && (isPrivateIdentifier(token) || isInterfaceDeclaration(classOrInterface))) { 175 return undefined; 176 } 177 178 const declSourceFile = classOrInterface.getSourceFile(); 179 const modifierFlags = (makeStatic ? ModifierFlags.Static : 0) | (startsWithUnderscore(token.text) ? ModifierFlags.Private : 0); 180 const isJSFile = isSourceFileJS(declSourceFile); 181 const call = tryCast(parent.parent, isCallExpression); 182 return { kind: InfoKind.ClassOrInterface, token, call, modifierFlags, parentDeclaration: classOrInterface, declSourceFile, isJSFile }; 183 } 184 185 const enumDeclaration = find(symbol.declarations, isEnumDeclaration); 186 if (enumDeclaration && !isPrivateIdentifier(token) && !program.isSourceFileFromExternalLibrary(enumDeclaration.getSourceFile())) { 187 return { kind: InfoKind.Enum, token, parentDeclaration: enumDeclaration }; 188 } 189 return undefined; 190 } 191 192 function getActionsForMissingMemberDeclaration(context: CodeFixContext, info: ClassOrInterfaceInfo): CodeFixAction[] | undefined { 193 return info.isJSFile ? singleElementArray(createActionForAddMissingMemberInJavascriptFile(context, info)) : 194 createActionsForAddMissingMemberInTypeScriptFile(context, info); 195 } 196 197 function createActionForAddMissingMemberInJavascriptFile(context: CodeFixContext, { parentDeclaration, declSourceFile, modifierFlags, token }: ClassOrInterfaceInfo): CodeFixAction | undefined { 198 if (isInterfaceDeclaration(parentDeclaration)) { 199 return undefined; 200 } 201 202 const changes = textChanges.ChangeTracker.with(context, t => addMissingMemberInJs(t, declSourceFile, parentDeclaration, token, !!(modifierFlags & ModifierFlags.Static))); 203 if (changes.length === 0) { 204 return undefined; 205 } 206 207 const diagnostic = modifierFlags & ModifierFlags.Static ? Diagnostics.Initialize_static_property_0 : 208 isPrivateIdentifier(token) ? Diagnostics.Declare_a_private_field_named_0 : Diagnostics.Initialize_property_0_in_the_constructor; 209 210 return createCodeFixAction(fixMissingMember, changes, [diagnostic, token.text], fixMissingMember, Diagnostics.Add_all_missing_members); 211 } 212 213 function addMissingMemberInJs(changeTracker: textChanges.ChangeTracker, declSourceFile: SourceFile, classDeclaration: ClassLikeDeclaration, token: Identifier | PrivateIdentifier, makeStatic: boolean): void { 214 const tokenName = token.text; 215 if (makeStatic) { 216 if (classDeclaration.kind === SyntaxKind.ClassExpression) { 217 return; 218 } 219 const className = classDeclaration.name!.getText(); 220 const staticInitialization = initializePropertyToUndefined(factory.createIdentifier(className), tokenName); 221 changeTracker.insertNodeAfter(declSourceFile, classDeclaration, staticInitialization); 222 } 223 else if (isPrivateIdentifier(token)) { 224 const property = factory.createPropertyDeclaration( 225 /*decorators*/ undefined, 226 /*modifiers*/ undefined, 227 tokenName, 228 /*questionToken*/ undefined, 229 /*type*/ undefined, 230 /*initializer*/ undefined); 231 232 const lastProp = getNodeToInsertPropertyAfter(classDeclaration); 233 if (lastProp) { 234 changeTracker.insertNodeAfter(declSourceFile, lastProp, property); 235 } 236 else { 237 changeTracker.insertNodeAtClassStart(declSourceFile, classDeclaration, property); 238 } 239 } 240 else { 241 const classConstructor = getFirstConstructorWithBody(classDeclaration); 242 if (!classConstructor) { 243 return; 244 } 245 const propertyInitialization = initializePropertyToUndefined(factory.createThis(), tokenName); 246 changeTracker.insertNodeAtConstructorEnd(declSourceFile, classConstructor, propertyInitialization); 247 } 248 } 249 250 function initializePropertyToUndefined(obj: Expression, propertyName: string) { 251 return factory.createExpressionStatement(factory.createAssignment(factory.createPropertyAccessExpression(obj, propertyName), factory.createIdentifier("undefined"))); 252 } 253 254 function createActionsForAddMissingMemberInTypeScriptFile(context: CodeFixContext, { parentDeclaration, declSourceFile, modifierFlags, token }: ClassOrInterfaceInfo): CodeFixAction[] | undefined { 255 const memberName = token.text; 256 const isStatic = modifierFlags & ModifierFlags.Static; 257 const typeNode = getTypeNode(context.program.getTypeChecker(), parentDeclaration, token); 258 const addPropertyDeclarationChanges = (modifierFlags: ModifierFlags) => textChanges.ChangeTracker.with(context, t => addPropertyDeclaration(t, declSourceFile, parentDeclaration, memberName, typeNode, modifierFlags)); 259 260 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)]; 261 if (isStatic || isPrivateIdentifier(token)) { 262 return actions; 263 } 264 265 if (modifierFlags & ModifierFlags.Private) { 266 actions.unshift(createCodeFixActionWithoutFixAll(fixMissingMember, addPropertyDeclarationChanges(ModifierFlags.Private), [Diagnostics.Declare_private_property_0, memberName])); 267 } 268 269 actions.push(createAddIndexSignatureAction(context, declSourceFile, parentDeclaration, token.text, typeNode)); 270 return actions; 271 } 272 273 function getTypeNode(checker: TypeChecker, classDeclaration: ClassOrInterface, token: Node) { 274 let typeNode: TypeNode | undefined; 275 if (token.parent.parent.kind === SyntaxKind.BinaryExpression) { 276 const binaryExpression = token.parent.parent as BinaryExpression; 277 const otherExpression = token.parent === binaryExpression.left ? binaryExpression.right : binaryExpression.left; 278 const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(otherExpression))); 279 typeNode = checker.typeToTypeNode(widenedType, classDeclaration, /*flags*/ undefined); 280 } 281 else { 282 const contextualType = checker.getContextualType(token.parent as Expression); 283 typeNode = contextualType ? checker.typeToTypeNode(contextualType, /*enclosingDeclaration*/ undefined, /*flags*/ undefined) : undefined; 284 } 285 return typeNode || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword); 286 } 287 288 function addPropertyDeclaration(changeTracker: textChanges.ChangeTracker, declSourceFile: SourceFile, classDeclaration: ClassOrInterface, tokenName: string, typeNode: TypeNode, modifierFlags: ModifierFlags): void { 289 const property = factory.createPropertyDeclaration( 290 /*decorators*/ undefined, 291 /*modifiers*/ modifierFlags ? factory.createNodeArray(factory.createModifiersFromModifierFlags(modifierFlags)) : undefined, 292 tokenName, 293 /*questionToken*/ undefined, 294 typeNode, 295 /*initializer*/ undefined); 296 297 const lastProp = getNodeToInsertPropertyAfter(classDeclaration); 298 if (lastProp) { 299 changeTracker.insertNodeAfter(declSourceFile, lastProp, property); 300 } 301 else { 302 changeTracker.insertNodeAtClassStart(declSourceFile, classDeclaration, property); 303 } 304 } 305 306 // Gets the last of the first run of PropertyDeclarations, or undefined if the class does not start with a PropertyDeclaration. 307 function getNodeToInsertPropertyAfter(cls: ClassOrInterface): PropertyDeclaration | undefined { 308 let res: PropertyDeclaration | undefined; 309 for (const member of cls.members) { 310 if (!isPropertyDeclaration(member)) break; 311 res = member; 312 } 313 return res; 314 } 315 316 function createAddIndexSignatureAction(context: CodeFixContext, declSourceFile: SourceFile, classDeclaration: ClassOrInterface, tokenName: string, typeNode: TypeNode): CodeFixAction { 317 // Index signatures cannot have the static modifier. 318 const stringTypeNode = factory.createKeywordTypeNode(SyntaxKind.StringKeyword); 319 const indexingParameter = factory.createParameterDeclaration( 320 /*decorators*/ undefined, 321 /*modifiers*/ undefined, 322 /*dotDotDotToken*/ undefined, 323 "x", 324 /*questionToken*/ undefined, 325 stringTypeNode, 326 /*initializer*/ undefined); 327 const indexSignature = factory.createIndexSignature( 328 /*decorators*/ undefined, 329 /*modifiers*/ undefined, 330 [indexingParameter], 331 typeNode); 332 333 const changes = textChanges.ChangeTracker.with(context, t => t.insertNodeAtClassStart(declSourceFile, classDeclaration, indexSignature)); 334 // No fixId here because code-fix-all currently only works on adding individual named properties. 335 return createCodeFixActionWithoutFixAll(fixMissingMember, changes, [Diagnostics.Add_index_signature_for_property_0, tokenName]); 336 } 337 338 function getActionsForMissingMethodDeclaration(context: CodeFixContext, info: ClassOrInterfaceInfo): CodeFixAction[] | undefined { 339 const { parentDeclaration, declSourceFile, modifierFlags, token, call } = info; 340 if (call === undefined) { 341 return undefined; 342 } 343 344 // Private methods are not implemented yet. 345 if (isPrivateIdentifier(token)) { 346 return undefined; 347 } 348 349 const methodName = token.text; 350 const addMethodDeclarationChanges = (modifierFlags: ModifierFlags) => textChanges.ChangeTracker.with(context, t => addMethodDeclaration(context, t, call, token, modifierFlags, parentDeclaration, declSourceFile)); 351 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)]; 352 if (modifierFlags & ModifierFlags.Private) { 353 actions.unshift(createCodeFixActionWithoutFixAll(fixMissingMember, addMethodDeclarationChanges(ModifierFlags.Private), [Diagnostics.Declare_private_method_0, methodName])); 354 } 355 return actions; 356 } 357 358 function addMethodDeclaration( 359 context: CodeFixContextBase, 360 changes: textChanges.ChangeTracker, 361 callExpression: CallExpression, 362 name: Identifier, 363 modifierFlags: ModifierFlags, 364 parentDeclaration: ClassOrInterface, 365 sourceFile: SourceFile, 366 ): void { 367 const importAdder = createImportAdder(sourceFile, context.program, context.preferences, context.host); 368 const methodDeclaration = createSignatureDeclarationFromCallExpression(SyntaxKind.MethodDeclaration, context, importAdder, callExpression, name, modifierFlags, parentDeclaration) as MethodDeclaration; 369 const containingMethodDeclaration = findAncestor(callExpression, n => isMethodDeclaration(n) || isConstructorDeclaration(n)); 370 if (containingMethodDeclaration && containingMethodDeclaration.parent === parentDeclaration) { 371 changes.insertNodeAfter(sourceFile, containingMethodDeclaration, methodDeclaration); 372 } 373 else { 374 changes.insertNodeAtClassStart(sourceFile, parentDeclaration, methodDeclaration); 375 } 376 importAdder.writeFixes(changes); 377 } 378 379 function addEnumMemberDeclaration(changes: textChanges.ChangeTracker, checker: TypeChecker, { token, parentDeclaration }: EnumInfo) { 380 /** 381 * create initializer only literal enum that has string initializer. 382 * value of initializer is a string literal that equal to name of enum member. 383 * numeric enum or empty enum will not create initializer. 384 */ 385 const hasStringInitializer = some(parentDeclaration.members, member => { 386 const type = checker.getTypeAtLocation(member); 387 return !!(type && type.flags & TypeFlags.StringLike); 388 }); 389 390 const enumMember = factory.createEnumMember(token, hasStringInitializer ? factory.createStringLiteral(token.text) : undefined); 391 changes.replaceNode(parentDeclaration.getSourceFile(), parentDeclaration, factory.updateEnumDeclaration( 392 parentDeclaration, 393 parentDeclaration.decorators, 394 parentDeclaration.modifiers, 395 parentDeclaration.name, 396 concatenate(parentDeclaration.members, singleElementArray(enumMember)) 397 ), { 398 leadingTriviaOption: textChanges.LeadingTriviaOption.IncludeAll, 399 trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude 400 }); 401 } 402 403 function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo) { 404 const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); 405 const functionDeclaration = createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) as FunctionDeclaration; 406 changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration); 407 } 408} 409