1import { 2 addRange, AnyValidImportOrReExport, ArrowFunction, AssignmentDeclarationKind, Block, CallExpression, 3 CancellationToken, codefix, compilerOptionsIndicateEsModules, createDiagnosticForNode, Diagnostics, 4 DiagnosticWithLocation, Expression, ExpressionStatement, Extension, fileExtensionIsOneOf, forEachReturnStatement, 5 FunctionDeclaration, FunctionExpression, FunctionFlags, FunctionLikeDeclaration, getAllowSyntheticDefaultImports, 6 getAssignmentDeclarationKind, getFunctionFlags, getModeForUsageLocation, getResolvedModule, hasInitializer, 7 hasPropertyAccessExpressionWithName, Identifier, importFromModuleSpecifier, isAsyncFunction, isBinaryExpression, 8 isBlock, isCallExpression, isExportAssignment, isFunctionLike, isIdentifier, isPropertyAccessExpression, 9 isRequireCall, isReturnStatement, isSourceFileJS, isStringLiteral, isVariableDeclaration, isVariableStatement, Map, 10 MethodDeclaration, ModuleKind, Node, NodeFlags, Program, programContainsEsModules, PropertyAccessExpression, Push, 11 ReturnStatement, skipAlias, some, SourceFile, SyntaxKind, TypeChecker, VariableStatement, 12} from "./_namespaces/ts"; 13 14const visitedNestedConvertibleFunctions = new Map<string, true>(); 15 16/** @internal */ 17export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] { 18 program.getSemanticDiagnostics(sourceFile, cancellationToken); 19 const diags: DiagnosticWithLocation[] = []; 20 const checker = program.getTypeChecker(); 21 const isCommonJSFile = sourceFile.impliedNodeFormat === ModuleKind.CommonJS || fileExtensionIsOneOf(sourceFile.fileName, [Extension.Cts, Extension.Cjs]) ; 22 23 if (!isCommonJSFile && 24 sourceFile.commonJsModuleIndicator && 25 (programContainsEsModules(program) || compilerOptionsIndicateEsModules(program.getCompilerOptions())) && 26 containsTopLevelCommonjs(sourceFile)) { 27 diags.push(createDiagnosticForNode(getErrorNodeFromCommonJsIndicator(sourceFile.commonJsModuleIndicator), Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module)); 28 } 29 30 const isJsFile = isSourceFileJS(sourceFile); 31 32 visitedNestedConvertibleFunctions.clear(); 33 check(sourceFile); 34 35 if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) { 36 for (const moduleSpecifier of sourceFile.imports) { 37 const importNode = importFromModuleSpecifier(moduleSpecifier); 38 const name = importNameForConvertToDefaultImport(importNode); 39 if (!name) continue; 40 const module = getResolvedModule(sourceFile, moduleSpecifier.text, getModeForUsageLocation(sourceFile, moduleSpecifier)); 41 const resolvedFile = module && program.getSourceFile(module.resolvedFileName); 42 if (resolvedFile && resolvedFile.externalModuleIndicator && resolvedFile.externalModuleIndicator !== true && isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals) { 43 diags.push(createDiagnosticForNode(name, Diagnostics.Import_may_be_converted_to_a_default_import)); 44 } 45 } 46 } 47 48 addRange(diags, sourceFile.bindSuggestionDiagnostics); 49 addRange(diags, program.getSuggestionDiagnostics(sourceFile, cancellationToken)); 50 return diags.sort((d1, d2) => d1.start - d2.start); 51 52 function check(node: Node) { 53 if (isJsFile) { 54 if (canBeConvertedToClass(node, checker)) { 55 diags.push(createDiagnosticForNode(isVariableDeclaration(node.parent) ? node.parent.name : node, Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration)); 56 } 57 } 58 else { 59 if (isVariableStatement(node) && 60 node.parent === sourceFile && 61 node.declarationList.flags & NodeFlags.Const && 62 node.declarationList.declarations.length === 1) { 63 const init = node.declarationList.declarations[0].initializer; 64 if (init && isRequireCall(init, /*checkArgumentIsStringLiteralLike*/ true)) { 65 diags.push(createDiagnosticForNode(init, Diagnostics.require_call_may_be_converted_to_an_import)); 66 } 67 } 68 69 if (codefix.parameterShouldGetTypeFromJSDoc(node)) { 70 diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types)); 71 } 72 } 73 74 if (canBeConvertedToAsync(node)) { 75 addConvertToAsyncFunctionDiagnostics(node, checker, diags); 76 } 77 node.forEachChild(check); 78 } 79} 80 81// convertToEsModule only works on top-level, so don't trigger it if commonjs code only appears in nested scopes. 82function containsTopLevelCommonjs(sourceFile: SourceFile): boolean { 83 return sourceFile.statements.some(statement => { 84 switch (statement.kind) { 85 case SyntaxKind.VariableStatement: 86 return (statement as VariableStatement).declarationList.declarations.some(decl => 87 !!decl.initializer && isRequireCall(propertyAccessLeftHandSide(decl.initializer), /*checkArgumentIsStringLiteralLike*/ true)); 88 case SyntaxKind.ExpressionStatement: { 89 const { expression } = statement as ExpressionStatement; 90 if (!isBinaryExpression(expression)) return isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true); 91 const kind = getAssignmentDeclarationKind(expression); 92 return kind === AssignmentDeclarationKind.ExportsProperty || kind === AssignmentDeclarationKind.ModuleExports; 93 } 94 default: 95 return false; 96 } 97 }); 98} 99 100function propertyAccessLeftHandSide(node: Expression): Expression { 101 return isPropertyAccessExpression(node) ? propertyAccessLeftHandSide(node.expression) : node; 102} 103 104function importNameForConvertToDefaultImport(node: AnyValidImportOrReExport): Identifier | undefined { 105 switch (node.kind) { 106 case SyntaxKind.ImportDeclaration: 107 const { importClause, moduleSpecifier } = node; 108 return importClause && !importClause.name && importClause.namedBindings && importClause.namedBindings.kind === SyntaxKind.NamespaceImport && isStringLiteral(moduleSpecifier) 109 ? importClause.namedBindings.name 110 : undefined; 111 case SyntaxKind.ImportEqualsDeclaration: 112 return node.name; 113 default: 114 return undefined; 115 } 116} 117 118function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: Push<DiagnosticWithLocation>): void { 119 // need to check function before checking map so that deeper levels of nested callbacks are checked 120 if (isConvertibleFunction(node, checker) && !visitedNestedConvertibleFunctions.has(getKeyFromNode(node))) { 121 diags.push(createDiagnosticForNode( 122 !node.name && isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ? node.parent.name : node, 123 Diagnostics.This_may_be_converted_to_an_async_function)); 124 } 125} 126 127function isConvertibleFunction(node: FunctionLikeDeclaration, checker: TypeChecker) { 128 return !isAsyncFunction(node) && 129 node.body && 130 isBlock(node.body) && 131 hasReturnStatementWithPromiseHandler(node.body, checker) && 132 returnsPromise(node, checker); 133} 134 135/** @internal */ 136export function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean { 137 const signature = checker.getSignatureFromDeclaration(node); 138 const returnType = signature ? checker.getReturnTypeOfSignature(signature) : undefined; 139 return !!returnType && !!checker.getPromisedTypeOfPromise(returnType); 140} 141 142function getErrorNodeFromCommonJsIndicator(commonJsModuleIndicator: Node): Node { 143 return isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator; 144} 145 146function hasReturnStatementWithPromiseHandler(body: Block, checker: TypeChecker): boolean { 147 return !!forEachReturnStatement(body, statement => isReturnStatementWithFixablePromiseHandler(statement, checker)); 148} 149 150/** @internal */ 151export function isReturnStatementWithFixablePromiseHandler(node: Node, checker: TypeChecker): node is ReturnStatement & { expression: CallExpression } { 152 return isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression, checker); 153} 154 155// Should be kept up to date with transformExpression in convertToAsyncFunction.ts 156/** @internal */ 157export function isFixablePromiseHandler(node: Node, checker: TypeChecker): boolean { 158 // ensure outermost call exists and is a promise handler 159 if (!isPromiseHandler(node) || !hasSupportedNumberOfArguments(node) || !node.arguments.every(arg => isFixablePromiseArgument(arg, checker))) { 160 return false; 161 } 162 163 // ensure all chained calls are valid 164 let currentNode = node.expression.expression; 165 while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) { 166 if (isCallExpression(currentNode)) { 167 if (!hasSupportedNumberOfArguments(currentNode) || !currentNode.arguments.every(arg => isFixablePromiseArgument(arg, checker))) { 168 return false; 169 } 170 currentNode = currentNode.expression.expression; 171 } 172 else { 173 currentNode = currentNode.expression; 174 } 175 } 176 return true; 177} 178 179function isPromiseHandler(node: Node): node is CallExpression & { readonly expression: PropertyAccessExpression } { 180 return isCallExpression(node) && ( 181 hasPropertyAccessExpressionWithName(node, "then") || 182 hasPropertyAccessExpressionWithName(node, "catch") || 183 hasPropertyAccessExpressionWithName(node, "finally")); 184} 185 186function hasSupportedNumberOfArguments(node: CallExpression & { readonly expression: PropertyAccessExpression }) { 187 const name = node.expression.name.text; 188 const maxArguments = name === "then" ? 2 : name === "catch" ? 1 : name === "finally" ? 1 : 0; 189 if (node.arguments.length > maxArguments) return false; 190 if (node.arguments.length < maxArguments) return true; 191 return maxArguments === 1 || some(node.arguments, arg => { 192 return arg.kind === SyntaxKind.NullKeyword || isIdentifier(arg) && arg.text === "undefined"; 193 }); 194} 195 196// should be kept up to date with getTransformationBody in convertToAsyncFunction.ts 197function isFixablePromiseArgument(arg: Expression, checker: TypeChecker): boolean { 198 switch (arg.kind) { 199 case SyntaxKind.FunctionDeclaration: 200 case SyntaxKind.FunctionExpression: 201 const functionFlags = getFunctionFlags(arg as FunctionDeclaration | FunctionExpression); 202 if (functionFlags & FunctionFlags.Generator) { 203 return false; 204 } 205 // falls through 206 case SyntaxKind.ArrowFunction: 207 visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as FunctionLikeDeclaration), true); 208 // falls through 209 case SyntaxKind.NullKeyword: 210 return true; 211 case SyntaxKind.Identifier: 212 case SyntaxKind.PropertyAccessExpression: { 213 const symbol = checker.getSymbolAtLocation(arg); 214 if (!symbol) { 215 return false; 216 } 217 return checker.isUndefinedSymbol(symbol) || 218 some(skipAlias(symbol, checker).declarations, d => isFunctionLike(d) || hasInitializer(d) && !!d.initializer && isFunctionLike(d.initializer)); 219 } 220 default: 221 return false; 222 } 223} 224 225function getKeyFromNode(exp: FunctionLikeDeclaration) { 226 return `${exp.pos.toString()}:${exp.end.toString()}`; 227} 228 229function canBeConvertedToClass(node: Node, checker: TypeChecker): boolean { 230 if (node.kind === SyntaxKind.FunctionExpression) { 231 if (isVariableDeclaration(node.parent) && node.symbol.members?.size) { 232 return true; 233 } 234 235 const symbol = checker.getSymbolOfExpando(node, /*allowDeclaration*/ false); 236 return !!(symbol && (symbol.exports?.size || symbol.members?.size)); 237 } 238 239 if (node.kind === SyntaxKind.FunctionDeclaration) { 240 return !!node.symbol.members?.size; 241 } 242 243 return false; 244} 245 246/** @internal */ 247export function canBeConvertedToAsync(node: Node): node is FunctionDeclaration | MethodDeclaration | FunctionExpression | ArrowFunction { 248 switch (node.kind) { 249 case SyntaxKind.FunctionDeclaration: 250 case SyntaxKind.MethodDeclaration: 251 case SyntaxKind.FunctionExpression: 252 case SyntaxKind.ArrowFunction: 253 return true; 254 default: 255 return false; 256 } 257} 258