• 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
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