diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 5d3300ba19b..35d7307d4bd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -117,6 +117,7 @@ namespace ts { }, getParameterType: getTypeAtPosition, getReturnTypeOfSignature, + getNullableType, getNonNullableType, typeToTypeNode: nodeBuilder.typeToTypeNode, indexInfoToIndexSignatureDeclaration: nodeBuilder.indexInfoToIndexSignatureDeclaration, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6ba1591d54f..c3c57058284 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2520,6 +2520,7 @@ namespace ts { * Returns `any` if the index is not valid. */ /* @internal */ getParameterType(signature: Signature, parameterIndex: number): Type; + getNullableType(type: Type, flags: TypeFlags): Type; getNonNullableType(type: Type): Type; /** Note that the resulting nodes cannot be checked. */ diff --git a/src/services/codefixes/fixJSDocTypes.ts b/src/services/codefixes/fixJSDocTypes.ts index 706fdd6c1be..d0adb9d98c3 100644 --- a/src/services/codefixes/fixJSDocTypes.ts +++ b/src/services/codefixes/fixJSDocTypes.ts @@ -10,65 +10,31 @@ namespace ts.codefix { const node = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false); const decl = ts.findAncestor(node, n => n.kind === SyntaxKind.VariableDeclaration); if (!decl) return; + const checker = context.program.getTypeChecker(); + const jsdocType = (decl as VariableDeclaration).type; - let cheesyHacks = false; - - // TODO: Only if get(jsdoctype) !== jsdoctype - // TODO: Create cheesy hacks to support | null | undefined -- just flip a boolean and rerun with that boolean set - - const trk = textChanges.ChangeTracker.fromCodeFixContext(context); - trk.replaceNode(sourceFile, jsdocType, getTypeFromJSDocType(jsdocType)); - const changes = [{ - // TODO: This seems like the LEAST SAFE way to get the new text - description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Change_0_to_1), [getTextOfNode(jsdocType), trk.getChanges()[0].textChanges[0].newText]), - changes: trk.getChanges(), - }]; - - if (cheesyHacks) { - const trk = textChanges.ChangeTracker.fromCodeFixContext(context); - trk.replaceNode(sourceFile, jsdocType, getTypeFromJSDocType(jsdocType)); - changes.push({ - // TODO: This seems like the LEAST SAFE way to get the new text - description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Change_0_to_1), [getTextOfNode(jsdocType), trk.getChanges()[0].textChanges[0].newText]), - changes: trk.getChanges(), - }); + const original = getTextOfNode(jsdocType); + const type = checker.getTypeFromTypeNode(jsdocType); + const actions = [createAction(jsdocType, sourceFile.fileName, original, checker.typeToString(type))]; + if (jsdocType.kind === SyntaxKind.JSDocNullableType) { + // for nullable types, suggest the flow-compatible `T | null | undefined` + // in addition to the jsdoc/closure-compatible `T | null` + const replacementWithUndefined = checker.typeToString(checker.getNullableType(type, TypeFlags.Undefined)); + actions.push(createAction(jsdocType, sourceFile.fileName, original, replacementWithUndefined)); } - return changes; + return actions; + } - function getTypeFromJSDocType(type: TypeNode): TypeNode { - switch (type.kind) { - case SyntaxKind.JSDocUnknownType: - case SyntaxKind.JSDocAllType: - return createToken(SyntaxKind.AnyKeyword) as TypeNode; - case SyntaxKind.JSDocVariadicType: - return createArrayTypeNode(getTypeFromJSDocType((type as JSDocVariadicType).type)); - case SyntaxKind.JSDocNullableType: - if (cheesyHacks) { - return createUnionTypeNode([getTypeFromJSDocType((type as JSDocNullableType).type), createNull(), createToken(SyntaxKind.UndefinedKeyword) as TypeNode]); - } - else { - cheesyHacks = true; - return createUnionTypeNode([getTypeFromJSDocType((type as JSDocNullableType).type), createNull()]); - } - case SyntaxKind.JSDocNonNullableType: - return getTypeFromJSDocType((type as JSDocNullableType).type); - case SyntaxKind.ArrayType: - // TODO: Only create an error if the get(type.type) !== type.type. - return createArrayTypeNode(getTypeFromJSDocType((type as ArrayTypeNode).elementType)); - case SyntaxKind.TypeReference: - return getTypeReferenceFromJSDocType(type as TypeReferenceNode); - case SyntaxKind.Identifier: - return type; - } - // TODO: Need to recur on all relevant nodes. Is a call to visit enough? - return type; - } - - function getTypeReferenceFromJSDocType(type: TypeReferenceNode) { - if (type.typeArguments && type.typeName.jsdocDotPos) { - return createTypeReferenceNode(type.typeName, map(type.typeArguments, getTypeFromJSDocType)); - } - return getTypeFromJSDocType(type); - } + function createAction(declaration: TypeNode, fileName: string, original: string, replacement: string): CodeAction { + return { + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Change_0_to_1), [original, replacement]), + changes: [{ + fileName, + textChanges: [{ + span: { start: declaration.getStart(), length: declaration.getWidth() }, + newText: replacement + }] + }], + }; } }