233 lines
12 KiB
TypeScript
233 lines
12 KiB
TypeScript
/* @internal */
|
|
namespace ts {
|
|
const visitedNestedConvertibleFunctions = new Map<string, true>();
|
|
|
|
export function computeSuggestionDiagnostics(sourceFile: SourceFile, program: Program, cancellationToken: CancellationToken): DiagnosticWithLocation[] {
|
|
program.getSemanticDiagnostics(sourceFile, cancellationToken);
|
|
const diags: DiagnosticWithLocation[] = [];
|
|
const checker = program.getTypeChecker();
|
|
|
|
if (sourceFile.commonJsModuleIndicator &&
|
|
(programContainsEs6Modules(program) || compilerOptionsIndicateEs6Modules(program.getCompilerOptions())) &&
|
|
containsTopLevelCommonjs(sourceFile)) {
|
|
diags.push(createDiagnosticForNode(getErrorNodeFromCommonJsIndicator(sourceFile.commonJsModuleIndicator), Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES6_module));
|
|
}
|
|
|
|
const isJsFile = isSourceFileJS(sourceFile);
|
|
|
|
visitedNestedConvertibleFunctions.clear();
|
|
check(sourceFile);
|
|
|
|
if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) {
|
|
for (const moduleSpecifier of sourceFile.imports) {
|
|
const importNode = importFromModuleSpecifier(moduleSpecifier);
|
|
const name = importNameForConvertToDefaultImport(importNode);
|
|
if (!name) continue;
|
|
const module = getResolvedModule(sourceFile, moduleSpecifier.text);
|
|
const resolvedFile = module && program.getSourceFile(module.resolvedFileName);
|
|
if (resolvedFile && resolvedFile.externalModuleIndicator && isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals) {
|
|
diags.push(createDiagnosticForNode(name, Diagnostics.Import_may_be_converted_to_a_default_import));
|
|
}
|
|
}
|
|
}
|
|
|
|
addRange(diags, sourceFile.bindSuggestionDiagnostics);
|
|
addRange(diags, program.getSuggestionDiagnostics(sourceFile, cancellationToken));
|
|
return diags.sort((d1, d2) => d1.start - d2.start);
|
|
|
|
function check(node: Node) {
|
|
if (isJsFile) {
|
|
if (canBeConvertedToClass(node, checker)) {
|
|
diags.push(createDiagnosticForNode(isVariableDeclaration(node.parent) ? node.parent.name : node, Diagnostics.This_constructor_function_may_be_converted_to_a_class_declaration));
|
|
}
|
|
}
|
|
else {
|
|
if (isVariableStatement(node) &&
|
|
node.parent === sourceFile &&
|
|
node.declarationList.flags & NodeFlags.Const &&
|
|
node.declarationList.declarations.length === 1) {
|
|
const init = node.declarationList.declarations[0].initializer;
|
|
if (init && isRequireCall(init, /*checkArgumentIsStringLiteralLike*/ true)) {
|
|
diags.push(createDiagnosticForNode(init, Diagnostics.require_call_may_be_converted_to_an_import));
|
|
}
|
|
}
|
|
|
|
if (codefix.parameterShouldGetTypeFromJSDoc(node)) {
|
|
diags.push(createDiagnosticForNode(node.name || node, Diagnostics.JSDoc_types_may_be_moved_to_TypeScript_types));
|
|
}
|
|
}
|
|
|
|
if (canBeConvertedToAsync(node)) {
|
|
addConvertToAsyncFunctionDiagnostics(node, checker, diags);
|
|
}
|
|
node.forEachChild(check);
|
|
}
|
|
}
|
|
|
|
// convertToEs6Module only works on top-level, so don't trigger it if commonjs code only appears in nested scopes.
|
|
function containsTopLevelCommonjs(sourceFile: SourceFile): boolean {
|
|
return sourceFile.statements.some(statement => {
|
|
switch (statement.kind) {
|
|
case SyntaxKind.VariableStatement:
|
|
return (statement as VariableStatement).declarationList.declarations.some(decl =>
|
|
!!decl.initializer && isRequireCall(propertyAccessLeftHandSide(decl.initializer), /*checkArgumentIsStringLiteralLike*/ true));
|
|
case SyntaxKind.ExpressionStatement: {
|
|
const { expression } = statement as ExpressionStatement;
|
|
if (!isBinaryExpression(expression)) return isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true);
|
|
const kind = getAssignmentDeclarationKind(expression);
|
|
return kind === AssignmentDeclarationKind.ExportsProperty || kind === AssignmentDeclarationKind.ModuleExports;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
function propertyAccessLeftHandSide(node: Expression): Expression {
|
|
return isPropertyAccessExpression(node) ? propertyAccessLeftHandSide(node.expression) : node;
|
|
}
|
|
|
|
function importNameForConvertToDefaultImport(node: AnyValidImportOrReExport): Identifier | undefined {
|
|
switch (node.kind) {
|
|
case SyntaxKind.ImportDeclaration:
|
|
const { importClause, moduleSpecifier } = node;
|
|
return importClause && !importClause.name && importClause.namedBindings && importClause.namedBindings.kind === SyntaxKind.NamespaceImport && isStringLiteral(moduleSpecifier)
|
|
? importClause.namedBindings.name
|
|
: undefined;
|
|
case SyntaxKind.ImportEqualsDeclaration:
|
|
return node.name;
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function addConvertToAsyncFunctionDiagnostics(node: FunctionLikeDeclaration, checker: TypeChecker, diags: Push<DiagnosticWithLocation>): void {
|
|
// need to check function before checking map so that deeper levels of nested callbacks are checked
|
|
if (isConvertibleFunction(node, checker) && !visitedNestedConvertibleFunctions.has(getKeyFromNode(node))) {
|
|
diags.push(createDiagnosticForNode(
|
|
!node.name && isVariableDeclaration(node.parent) && isIdentifier(node.parent.name) ? node.parent.name : node,
|
|
Diagnostics.This_may_be_converted_to_an_async_function));
|
|
}
|
|
}
|
|
|
|
function isConvertibleFunction(node: FunctionLikeDeclaration, checker: TypeChecker) {
|
|
return !isAsyncFunction(node) &&
|
|
node.body &&
|
|
isBlock(node.body) &&
|
|
hasReturnStatementWithPromiseHandler(node.body, checker) &&
|
|
returnsPromise(node, checker);
|
|
}
|
|
|
|
export function returnsPromise(node: FunctionLikeDeclaration, checker: TypeChecker): boolean {
|
|
const signature = checker.getSignatureFromDeclaration(node);
|
|
const returnType = signature ? checker.getReturnTypeOfSignature(signature) : undefined;
|
|
return !!returnType && !!checker.getPromisedTypeOfPromise(returnType);
|
|
}
|
|
|
|
function getErrorNodeFromCommonJsIndicator(commonJsModuleIndicator: Node): Node {
|
|
return isBinaryExpression(commonJsModuleIndicator) ? commonJsModuleIndicator.left : commonJsModuleIndicator;
|
|
}
|
|
|
|
function hasReturnStatementWithPromiseHandler(body: Block, checker: TypeChecker): boolean {
|
|
return !!forEachReturnStatement(body, statement => isReturnStatementWithFixablePromiseHandler(statement, checker));
|
|
}
|
|
|
|
export function isReturnStatementWithFixablePromiseHandler(node: Node, checker: TypeChecker): node is ReturnStatement & { expression: CallExpression } {
|
|
return isReturnStatement(node) && !!node.expression && isFixablePromiseHandler(node.expression, checker);
|
|
}
|
|
|
|
// Should be kept up to date with transformExpression in convertToAsyncFunction.ts
|
|
export function isFixablePromiseHandler(node: Node, checker: TypeChecker): boolean {
|
|
// ensure outermost call exists and is a promise handler
|
|
if (!isPromiseHandler(node) || !node.arguments.every(arg => isFixablePromiseArgument(arg, checker))) {
|
|
return false;
|
|
}
|
|
|
|
// ensure all chained calls are valid
|
|
let currentNode = node.expression;
|
|
while (isPromiseHandler(currentNode) || isPropertyAccessExpression(currentNode)) {
|
|
if (isCallExpression(currentNode) && !currentNode.arguments.every(arg => isFixablePromiseArgument(arg, checker))) {
|
|
return false;
|
|
}
|
|
currentNode = currentNode.expression;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isPromiseHandler(node: Node): node is CallExpression {
|
|
return isCallExpression(node) && (
|
|
hasPropertyAccessExpressionWithName(node, "then") && hasSupportedNumberOfArguments(node) ||
|
|
hasPropertyAccessExpressionWithName(node, "catch"));
|
|
}
|
|
|
|
function hasSupportedNumberOfArguments(node: CallExpression) {
|
|
if (node.arguments.length > 2) return false;
|
|
if (node.arguments.length < 2) return true;
|
|
return some(node.arguments, arg => {
|
|
return arg.kind === SyntaxKind.NullKeyword ||
|
|
isIdentifier(arg) && arg.text === "undefined";
|
|
});
|
|
}
|
|
|
|
// should be kept up to date with getTransformationBody in convertToAsyncFunction.ts
|
|
function isFixablePromiseArgument(arg: Expression, checker: TypeChecker): boolean {
|
|
switch (arg.kind) {
|
|
case SyntaxKind.FunctionDeclaration:
|
|
case SyntaxKind.FunctionExpression:
|
|
const functionFlags = getFunctionFlags(arg as FunctionDeclaration | FunctionExpression);
|
|
if (functionFlags & FunctionFlags.Generator) {
|
|
return false;
|
|
}
|
|
// falls through
|
|
case SyntaxKind.ArrowFunction:
|
|
visitedNestedConvertibleFunctions.set(getKeyFromNode(arg as FunctionLikeDeclaration), true);
|
|
// falls through
|
|
case SyntaxKind.NullKeyword:
|
|
return true;
|
|
case SyntaxKind.Identifier:
|
|
case SyntaxKind.PropertyAccessExpression: {
|
|
const symbol = checker.getSymbolAtLocation(arg);
|
|
if (!symbol) {
|
|
return false;
|
|
}
|
|
return checker.isUndefinedSymbol(symbol) ||
|
|
some(skipAlias(symbol, checker).declarations, d => isFunctionLike(d) || hasInitializer(d) && !!d.initializer && isFunctionLike(d.initializer));
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getKeyFromNode(exp: FunctionLikeDeclaration) {
|
|
return `${exp.pos.toString()}:${exp.end.toString()}`;
|
|
}
|
|
|
|
function canBeConvertedToClass(node: Node, checker: TypeChecker): boolean {
|
|
if (node.kind === SyntaxKind.FunctionExpression) {
|
|
if (isVariableDeclaration(node.parent) && node.symbol.members?.size) {
|
|
return true;
|
|
}
|
|
|
|
const symbol = checker.getSymbolOfExpando(node, /*allowDeclaration*/ false);
|
|
return !!(symbol && (symbol.exports?.size || symbol.members?.size));
|
|
}
|
|
|
|
if (node.kind === SyntaxKind.FunctionDeclaration) {
|
|
return !!node.symbol.members?.size;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function canBeConvertedToAsync(node: Node): node is FunctionDeclaration | MethodDeclaration | FunctionExpression | ArrowFunction {
|
|
switch (node.kind) {
|
|
case SyntaxKind.FunctionDeclaration:
|
|
case SyntaxKind.MethodDeclaration:
|
|
case SyntaxKind.FunctionExpression:
|
|
case SyntaxKind.ArrowFunction:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
}
|