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