TypeScript/src/services/suggestionDiagnostics.ts

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;
}
}
}