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