• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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