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