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