• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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