• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.codefix {
3    /**
4     * Finds members of the resolved type that are missing in the class pointed to by class decl
5     * and generates source code for the missing members.
6     * @param possiblyMissingSymbols The collection of symbols to filter and then get insertions for.
7     * @param importAdder If provided, type annotations will use identifier type references instead of ImportTypeNodes, and the missing imports will be added to the importAdder.
8     * @returns Empty string iff there are no member insertions.
9     */
10    export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: readonly Symbol[], sourceFile: SourceFile, context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: ClassElement) => void): void {
11        const classMembers = classDeclaration.symbol.members!;
12        for (const symbol of possiblyMissingSymbols) {
13            if (!classMembers.has(symbol.escapedName)) {
14                addNewNodeForMemberSymbol(symbol, classDeclaration, sourceFile, context, preferences, importAdder, addClassElement);
15            }
16        }
17    }
18
19    export function getNoopSymbolTrackerWithResolver(context: TypeConstructionContext): SymbolTracker {
20        return {
21            trackSymbol: noop,
22            moduleResolverHost: getModuleSpecifierResolverHost(context.program, context.host),
23        };
24    }
25
26    export interface TypeConstructionContext {
27        program: Program;
28        host: LanguageServiceHost;
29    }
30
31    /**
32     * @returns Empty string iff there we can't figure out a representation for `symbol` in `enclosingDeclaration`.
33     */
34    function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, sourceFile: SourceFile, context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: Node) => void): void {
35        const declarations = symbol.getDeclarations();
36        if (!(declarations && declarations.length)) {
37            return undefined;
38        }
39        const checker = context.program.getTypeChecker();
40        const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions());
41        const declaration = declarations[0];
42        const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
43        const visibilityModifier = createVisibilityModifier(getEffectiveModifierFlags(declaration));
44        const modifiers = visibilityModifier ? factory.createNodeArray([visibilityModifier]) : undefined;
45        const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration));
46        const optional = !!(symbol.flags & SymbolFlags.Optional);
47        const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient);
48        const quotePreference = getQuotePreference(sourceFile, preferences);
49
50        switch (declaration.kind) {
51            case SyntaxKind.PropertySignature:
52            case SyntaxKind.PropertyDeclaration:
53                const flags = quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined;
54                let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context));
55                if (importAdder) {
56                    const importableReference = tryGetAutoImportableReferenceFromTypeNode(typeNode, scriptTarget);
57                    if (importableReference) {
58                        typeNode = importableReference.typeNode;
59                        importSymbols(importAdder, importableReference.symbols);
60                    }
61                }
62                addClassElement(factory.createPropertyDeclaration(
63                    /*decorators*/ undefined,
64                    modifiers,
65                    name,
66                    optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
67                    typeNode,
68                    /*initializer*/ undefined));
69                break;
70            case SyntaxKind.GetAccessor:
71            case SyntaxKind.SetAccessor: {
72                let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context));
73                const allAccessors = getAllAccessorDeclarations(declarations, declaration as AccessorDeclaration);
74                const orderedAccessors = allAccessors.secondAccessor
75                    ? [allAccessors.firstAccessor, allAccessors.secondAccessor]
76                    : [allAccessors.firstAccessor];
77                if (importAdder) {
78                    const importableReference = tryGetAutoImportableReferenceFromTypeNode(typeNode, scriptTarget);
79                    if (importableReference) {
80                        typeNode = importableReference.typeNode;
81                        importSymbols(importAdder, importableReference.symbols);
82                    }
83                }
84                for (const accessor of orderedAccessors) {
85                    if (isGetAccessorDeclaration(accessor)) {
86                        addClassElement(factory.createGetAccessorDeclaration(
87                            /*decorators*/ undefined,
88                            modifiers,
89                            name,
90                            emptyArray,
91                            typeNode,
92                            ambient ? undefined : createStubbedMethodBody(quotePreference)));
93                    }
94                    else {
95                        Debug.assertNode(accessor, isSetAccessorDeclaration, "The counterpart to a getter should be a setter");
96                        const parameter = getSetAccessorValueParameter(accessor);
97                        const parameterName = parameter && isIdentifier(parameter.name) ? idText(parameter.name) : undefined;
98                        addClassElement(factory.createSetAccessorDeclaration(
99                            /*decorators*/ undefined,
100                            modifiers,
101                            name,
102                            createDummyParameters(1, [parameterName], [typeNode], 1, /*inJs*/ false),
103                            ambient ? undefined : createStubbedMethodBody(quotePreference)));
104                    }
105                }
106                break;
107            }
108            case SyntaxKind.MethodSignature:
109            case SyntaxKind.MethodDeclaration:
110                // The signature for the implementation appears as an entry in `signatures` iff
111                // there is only one signature.
112                // If there are overloads and an implementation signature, it appears as an
113                // extra declaration that isn't a signature for `type`.
114                // If there is more than one overload but no implementation signature
115                // (eg: an abstract method or interface declaration), there is a 1-1
116                // correspondence of declarations and signatures.
117                const signatures = checker.getSignaturesOfType(type, SignatureKind.Call);
118                if (!some(signatures)) {
119                    break;
120                }
121
122                if (declarations.length === 1) {
123                    Debug.assert(signatures.length === 1, "One declaration implies one signature");
124                    const signature = signatures[0];
125                    outputMethod(quotePreference, signature, modifiers, name, ambient ? undefined : createStubbedMethodBody(quotePreference));
126                    break;
127                }
128
129                for (const signature of signatures) {
130                    // Need to ensure nodes are fresh each time so they can have different positions.
131                    outputMethod(quotePreference, signature, getSynthesizedDeepClones(modifiers, /*includeTrivia*/ false), getSynthesizedDeepClone(name, /*includeTrivia*/ false));
132                }
133
134                if (!ambient) {
135                    if (declarations.length > signatures.length) {
136                        const signature = checker.getSignatureFromDeclaration(declarations[declarations.length - 1] as SignatureDeclaration)!;
137                        outputMethod(quotePreference, signature, modifiers, name, createStubbedMethodBody(quotePreference));
138                    }
139                    else {
140                        Debug.assert(declarations.length === signatures.length, "Declarations and signatures should match count");
141                        addClassElement(createMethodImplementingSignatures(signatures, name, optional, modifiers, quotePreference));
142                    }
143                }
144                break;
145        }
146
147        function outputMethod(quotePreference: QuotePreference, signature: Signature, modifiers: NodeArray<Modifier> | undefined, name: PropertyName, body?: Block): void {
148            const method = signatureToMethodDeclaration(context, quotePreference, signature, enclosingDeclaration, modifiers, name, optional, body, importAdder);
149            if (method) addClassElement(method);
150        }
151    }
152
153    function signatureToMethodDeclaration(
154        context: TypeConstructionContext,
155        quotePreference: QuotePreference,
156        signature: Signature,
157        enclosingDeclaration: ClassLikeDeclaration,
158        modifiers: NodeArray<Modifier> | undefined,
159        name: PropertyName,
160        optional: boolean,
161        body: Block | undefined,
162        importAdder: ImportAdder | undefined,
163    ): MethodDeclaration | undefined {
164        const program = context.program;
165        const checker = program.getTypeChecker();
166        const scriptTarget = getEmitScriptTarget(program.getCompilerOptions());
167        const flags = NodeBuilderFlags.NoTruncation | NodeBuilderFlags.NoUndefinedOptionalParameterType | NodeBuilderFlags.SuppressAnyReturnType | (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : 0);
168        const signatureDeclaration = <MethodDeclaration>checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context));
169        if (!signatureDeclaration) {
170            return undefined;
171        }
172
173        let typeParameters = signatureDeclaration.typeParameters;
174        let parameters = signatureDeclaration.parameters;
175        let type = signatureDeclaration.type;
176        if (importAdder) {
177            if (typeParameters) {
178                const newTypeParameters = sameMap(typeParameters, typeParameterDecl => {
179                    let constraint = typeParameterDecl.constraint;
180                    let defaultType = typeParameterDecl.default;
181                    if (constraint) {
182                        const importableReference = tryGetAutoImportableReferenceFromTypeNode(constraint, scriptTarget);
183                        if (importableReference) {
184                            constraint = importableReference.typeNode;
185                            importSymbols(importAdder, importableReference.symbols);
186                        }
187                    }
188                    if (defaultType) {
189                        const importableReference = tryGetAutoImportableReferenceFromTypeNode(defaultType, scriptTarget);
190                        if (importableReference) {
191                            defaultType = importableReference.typeNode;
192                            importSymbols(importAdder, importableReference.symbols);
193                        }
194                    }
195                    return factory.updateTypeParameterDeclaration(
196                        typeParameterDecl,
197                        typeParameterDecl.name,
198                        constraint,
199                        defaultType
200                    );
201                });
202                if (typeParameters !== newTypeParameters) {
203                    typeParameters = setTextRange(factory.createNodeArray(newTypeParameters, typeParameters.hasTrailingComma), typeParameters);
204                }
205            }
206            const newParameters = sameMap(parameters, parameterDecl => {
207                const importableReference = tryGetAutoImportableReferenceFromTypeNode(parameterDecl.type, scriptTarget);
208                let type = parameterDecl.type;
209                if (importableReference) {
210                    type = importableReference.typeNode;
211                    importSymbols(importAdder, importableReference.symbols);
212                }
213                return factory.updateParameterDeclaration(
214                    parameterDecl,
215                    parameterDecl.decorators,
216                    parameterDecl.modifiers,
217                    parameterDecl.dotDotDotToken,
218                    parameterDecl.name,
219                    parameterDecl.questionToken,
220                    type,
221                    parameterDecl.initializer
222                );
223            });
224            if (parameters !== newParameters) {
225                parameters = setTextRange(factory.createNodeArray(newParameters, parameters.hasTrailingComma), parameters);
226            }
227            if (type) {
228                const importableReference = tryGetAutoImportableReferenceFromTypeNode(type, scriptTarget);
229                if (importableReference) {
230                    type = importableReference.typeNode;
231                    importSymbols(importAdder, importableReference.symbols);
232                }
233            }
234        }
235
236        return factory.updateMethodDeclaration(
237            signatureDeclaration,
238            /*decorators*/ undefined,
239            modifiers,
240            signatureDeclaration.asteriskToken,
241            name,
242            optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
243            typeParameters,
244            parameters,
245            type,
246            body
247        );
248    }
249
250    export function createSignatureDeclarationFromCallExpression(
251        kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionDeclaration,
252        context: CodeFixContextBase,
253        importAdder: ImportAdder,
254        call: CallExpression,
255        name: Identifier | string,
256        modifierFlags: ModifierFlags,
257        contextNode: Node
258    ) {
259        const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
260        const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions());
261        const tracker = getNoopSymbolTrackerWithResolver(context);
262        const checker = context.program.getTypeChecker();
263        const isJs = isInJSFile(contextNode);
264        const { typeArguments, arguments: args, parent } = call;
265
266        const contextualType = isJs ? undefined : checker.getContextualType(call);
267        const names = map(args, arg =>
268            isIdentifier(arg) ? arg.text : isPropertyAccessExpression(arg) && isIdentifier(arg.name) ? arg.name.text : undefined);
269        const types = isJs ? [] : map(args, arg =>
270            typeToAutoImportableTypeNode(checker, importAdder, checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(arg)), contextNode, scriptTarget, /*flags*/ undefined, tracker));
271
272        const modifiers = modifierFlags
273            ? factory.createNodeArray(factory.createModifiersFromModifierFlags(modifierFlags))
274            : undefined;
275        const asteriskToken = isYieldExpression(parent)
276            ? factory.createToken(SyntaxKind.AsteriskToken)
277            : undefined;
278        const typeParameters = isJs || typeArguments === undefined
279            ? undefined
280            : map(typeArguments, (_, i) =>
281                factory.createTypeParameterDeclaration(CharacterCodes.T + typeArguments.length - 1 <= CharacterCodes.Z ? String.fromCharCode(CharacterCodes.T + i) : `T${i}`));
282        const parameters = createDummyParameters(args.length, names, types, /*minArgumentCount*/ undefined, isJs);
283        const type = isJs || contextualType === undefined
284            ? undefined
285            : checker.typeToTypeNode(contextualType, contextNode, /*flags*/ undefined, tracker);
286
287        if (kind === SyntaxKind.MethodDeclaration) {
288            return factory.createMethodDeclaration(
289                /*decorators*/ undefined,
290                modifiers,
291                asteriskToken,
292                name,
293                /*questionToken*/ undefined,
294                typeParameters,
295                parameters,
296                type,
297                isInterfaceDeclaration(contextNode) ? undefined : createStubbedMethodBody(quotePreference)
298            );
299        }
300        return factory.createFunctionDeclaration(
301            /*decorators*/ undefined,
302            modifiers,
303            asteriskToken,
304            name,
305            typeParameters,
306            parameters,
307            type,
308            createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference)
309        );
310    }
311
312    export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder: ImportAdder, type: Type, contextNode: Node | undefined, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined {
313        const typeNode = checker.typeToTypeNode(type, contextNode, flags, tracker);
314        if (typeNode && isImportTypeNode(typeNode)) {
315            const importableReference = tryGetAutoImportableReferenceFromTypeNode(typeNode, scriptTarget);
316            if (importableReference) {
317                importSymbols(importAdder, importableReference.symbols);
318                return importableReference.typeNode;
319            }
320        }
321        return typeNode;
322    }
323
324    function createDummyParameters(argCount: number, names: (string | undefined)[] | undefined, types: (TypeNode | undefined)[] | undefined, minArgumentCount: number | undefined, inJs: boolean): ParameterDeclaration[] {
325        const parameters: ParameterDeclaration[] = [];
326        for (let i = 0; i < argCount; i++) {
327            const newParameter = factory.createParameterDeclaration(
328                /*decorators*/ undefined,
329                /*modifiers*/ undefined,
330                /*dotDotDotToken*/ undefined,
331                /*name*/ names && names[i] || `arg${i}`,
332                /*questionToken*/ minArgumentCount !== undefined && i >= minArgumentCount ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
333                /*type*/ inJs ? undefined : types && types[i] || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword),
334                /*initializer*/ undefined);
335            parameters.push(newParameter);
336        }
337        return parameters;
338    }
339
340    function createMethodImplementingSignatures(
341        signatures: readonly Signature[],
342        name: PropertyName,
343        optional: boolean,
344        modifiers: readonly Modifier[] | undefined,
345        quotePreference: QuotePreference,
346    ): MethodDeclaration {
347        /** This is *a* signature with the maximal number of arguments,
348         * such that if there is a "maximal" signature without rest arguments,
349         * this is one of them.
350         */
351        let maxArgsSignature = signatures[0];
352        let minArgumentCount = signatures[0].minArgumentCount;
353        let someSigHasRestParameter = false;
354        for (const sig of signatures) {
355            minArgumentCount = Math.min(sig.minArgumentCount, minArgumentCount);
356            if (signatureHasRestParameter(sig)) {
357                someSigHasRestParameter = true;
358            }
359            if (sig.parameters.length >= maxArgsSignature.parameters.length && (!signatureHasRestParameter(sig) || signatureHasRestParameter(maxArgsSignature))) {
360                maxArgsSignature = sig;
361            }
362        }
363        const maxNonRestArgs = maxArgsSignature.parameters.length - (signatureHasRestParameter(maxArgsSignature) ? 1 : 0);
364        const maxArgsParameterSymbolNames = maxArgsSignature.parameters.map(symbol => symbol.name);
365
366        const parameters = createDummyParameters(maxNonRestArgs, maxArgsParameterSymbolNames, /* types */ undefined, minArgumentCount, /*inJs*/ false);
367
368        if (someSigHasRestParameter) {
369            const anyArrayType = factory.createArrayTypeNode(factory.createKeywordTypeNode(SyntaxKind.AnyKeyword));
370            const restParameter = factory.createParameterDeclaration(
371                /*decorators*/ undefined,
372                /*modifiers*/ undefined,
373                factory.createToken(SyntaxKind.DotDotDotToken),
374                maxArgsParameterSymbolNames[maxNonRestArgs] || "rest",
375                /*questionToken*/ maxNonRestArgs >= minArgumentCount ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
376                anyArrayType,
377                /*initializer*/ undefined);
378            parameters.push(restParameter);
379        }
380
381        return createStubbedMethod(
382            modifiers,
383            name,
384            optional,
385            /*typeParameters*/ undefined,
386            parameters,
387            /*returnType*/ undefined,
388            quotePreference);
389    }
390
391    function createStubbedMethod(
392        modifiers: readonly Modifier[] | undefined,
393        name: PropertyName,
394        optional: boolean,
395        typeParameters: readonly TypeParameterDeclaration[] | undefined,
396        parameters: readonly ParameterDeclaration[],
397        returnType: TypeNode | undefined,
398        quotePreference: QuotePreference
399    ): MethodDeclaration {
400        return factory.createMethodDeclaration(
401            /*decorators*/ undefined,
402            modifiers,
403            /*asteriskToken*/ undefined,
404            name,
405            optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
406            typeParameters,
407            parameters,
408            returnType,
409            createStubbedMethodBody(quotePreference));
410    }
411
412    function createStubbedMethodBody(quotePreference: QuotePreference) {
413        return createStubbedBody(Diagnostics.Method_not_implemented.message, quotePreference);
414    }
415
416    export function createStubbedBody(text: string, quotePreference: QuotePreference): Block {
417        return factory.createBlock(
418            [factory.createThrowStatement(
419                factory.createNewExpression(
420                    factory.createIdentifier("Error"),
421                    /*typeArguments*/ undefined,
422                    // TODO Handle auto quote preference.
423                    [factory.createStringLiteral(text, /*isSingleQuote*/ quotePreference === QuotePreference.Single)]))],
424            /*multiline*/ true);
425    }
426
427    function createVisibilityModifier(flags: ModifierFlags): Modifier | undefined {
428        if (flags & ModifierFlags.Public) {
429            return factory.createToken(SyntaxKind.PublicKeyword);
430        }
431        else if (flags & ModifierFlags.Protected) {
432            return factory.createToken(SyntaxKind.ProtectedKeyword);
433        }
434        return undefined;
435    }
436
437    export function setJsonCompilerOptionValues(
438        changeTracker: textChanges.ChangeTracker,
439        configFile: TsConfigSourceFile,
440        options: [string, Expression][]
441    ) {
442        const tsconfigObjectLiteral = getTsConfigObjectLiteralExpression(configFile);
443        if (!tsconfigObjectLiteral) return undefined;
444
445        const compilerOptionsProperty = findJsonProperty(tsconfigObjectLiteral, "compilerOptions");
446        if (compilerOptionsProperty === undefined) {
447            changeTracker.insertNodeAtObjectStart(configFile, tsconfigObjectLiteral, createJsonPropertyAssignment(
448                "compilerOptions",
449                factory.createObjectLiteralExpression(options.map(([optionName, optionValue]) => createJsonPropertyAssignment(optionName, optionValue)), /*multiLine*/ true)));
450            return;
451        }
452
453        const compilerOptions = compilerOptionsProperty.initializer;
454        if (!isObjectLiteralExpression(compilerOptions)) {
455            return;
456        }
457
458        for (const [optionName, optionValue] of options) {
459            const optionProperty = findJsonProperty(compilerOptions, optionName);
460            if (optionProperty === undefined) {
461                changeTracker.insertNodeAtObjectStart(configFile, compilerOptions, createJsonPropertyAssignment(optionName, optionValue));
462            }
463            else {
464                changeTracker.replaceNode(configFile, optionProperty.initializer, optionValue);
465            }
466        }
467    }
468
469    export function setJsonCompilerOptionValue(
470        changeTracker: textChanges.ChangeTracker,
471        configFile: TsConfigSourceFile,
472        optionName: string,
473        optionValue: Expression,
474    ) {
475        setJsonCompilerOptionValues(changeTracker, configFile, [[optionName, optionValue]]);
476    }
477
478    export function createJsonPropertyAssignment(name: string, initializer: Expression) {
479        return factory.createPropertyAssignment(factory.createStringLiteral(name), initializer);
480    }
481
482    export function findJsonProperty(obj: ObjectLiteralExpression, name: string): PropertyAssignment | undefined {
483        return find(obj.properties, (p): p is PropertyAssignment => isPropertyAssignment(p) && !!p.name && isStringLiteral(p.name) && p.name.text === name);
484    }
485
486    /**
487     * Given a type node containing 'import("./a").SomeType<import("./b").OtherType<...>>',
488     * returns an equivalent type reference node with any nested ImportTypeNodes also replaced
489     * with type references, and a list of symbols that must be imported to use the type reference.
490     */
491    export function tryGetAutoImportableReferenceFromTypeNode(importTypeNode: TypeNode | undefined, scriptTarget: ScriptTarget) {
492        let symbols: Symbol[] | undefined;
493        const typeNode = visitNode(importTypeNode, visit);
494        if (symbols && typeNode) {
495            return { typeNode, symbols };
496        }
497
498        function visit(node: TypeNode): TypeNode;
499        function visit(node: Node): Node {
500            if (isLiteralImportTypeNode(node) && node.qualifier) {
501                // Symbol for the left-most thing after the dot
502                const firstIdentifier = getFirstIdentifier(node.qualifier);
503                const name = getNameForExportedSymbol(firstIdentifier.symbol, scriptTarget);
504                const qualifier = name !== firstIdentifier.text
505                    ? replaceFirstIdentifierOfEntityName(node.qualifier, factory.createIdentifier(name))
506                    : node.qualifier;
507
508                symbols = append(symbols, firstIdentifier.symbol);
509                const typeArguments = node.typeArguments?.map(visit);
510                return factory.createTypeReferenceNode(qualifier, typeArguments);
511            }
512            return visitEachChild(node, visit, nullTransformationContext);
513        }
514    }
515
516    function replaceFirstIdentifierOfEntityName(name: EntityName, newIdentifier: Identifier): EntityName {
517        if (name.kind === SyntaxKind.Identifier) {
518            return newIdentifier;
519        }
520        return factory.createQualifiedName(replaceFirstIdentifierOfEntityName(name.left, newIdentifier), name.right);
521    }
522
523    export function importSymbols(importAdder: ImportAdder, symbols: readonly Symbol[]) {
524        symbols.forEach(s => importAdder.addImportFromExportedSymbol(s, /*usageIsTypeOnly*/ true));
525    }
526}
527