From 617251f2e0c9bb4980c87ef157cbafa2e1e39e48 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 9 Sep 2021 01:22:38 +0900 Subject: [PATCH] feat(45010): handle unclosed fragment in `getJsxClosingTagAtPosition` (#45532) * feat(45010): handle unclosed fragment in `getJsxClosingTagAtPosition` * Update tests * Fix types of `JsxText.parent` and `JsxExpression.parent` --- src/compiler/types.ts | 4 +- src/services/completions.ts | 2 +- src/services/services.ts | 11 +++- .../reference/api/tsserverlibrary.d.ts | 4 +- tests/baselines/reference/api/typescript.d.ts | 4 +- tests/cases/fourslash/autoCloseFragment.ts | 51 +++++++++++++++++++ 6 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 tests/cases/fourslash/autoCloseFragment.ts diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 08efc5d5457..651c9b0ef6d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2598,14 +2598,14 @@ namespace ts { export interface JsxExpression extends Expression { readonly kind: SyntaxKind.JsxExpression; - readonly parent: JsxElement | JsxAttributeLike; + readonly parent: JsxElement | JsxFragment | JsxAttributeLike; readonly dotDotDotToken?: Token; readonly expression?: Expression; } export interface JsxText extends LiteralLikeNode { readonly kind: SyntaxKind.JsxText; - readonly parent: JsxElement; + readonly parent: JsxElement | JsxFragment; readonly containsOnlyTriviaWhiteSpaces: boolean; } diff --git a/src/services/completions.ts b/src/services/completions.ts index 2bb3e8f27d2..ad5e24ea3eb 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1180,7 +1180,7 @@ namespace ts.Completions { case SyntaxKind.CaseKeyword: return getSwitchedType(cast(parent, isCaseClause), checker); case SyntaxKind.OpenBraceToken: - return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; + return isJsxExpression(parent) && !isJsxElement(parent.parent) && !isJsxFragment(parent.parent) ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; default: const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile); return argInfo ? diff --git a/src/services/services.ts b/src/services/services.ts index 3f4096093a0..426fe7a5269 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2088,10 +2088,15 @@ namespace ts { const token = findPrecedingToken(position, sourceFile); if (!token) return undefined; const element = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningElement(token.parent) ? token.parent.parent - : isJsxText(token) ? token.parent : undefined; + : isJsxText(token) && isJsxElement(token.parent) ? token.parent : undefined; if (element && isUnclosedTag(element)) { return { newText: `` }; } + const fragment = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningFragment(token.parent) ? token.parent.parent + : isJsxText(token) && isJsxFragment(token.parent) ? token.parent : undefined; + if (fragment && isUnclosedFragment(fragment)) { + return { newText: "" }; + } } function getLinesForRange(sourceFile: SourceFile, textRange: TextRange) { @@ -2334,6 +2339,10 @@ namespace ts { isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent); } + function isUnclosedFragment({ closingFragment, parent }: JsxFragment): boolean { + return !!(closingFragment.flags & NodeFlags.ThisNodeHasError) || (isJsxFragment(parent) && isUnclosedFragment(parent)); + } + function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); const range = formatting.getRangeOfEnclosingComment(sourceFile, position); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 16f89a78423..06aeadfde34 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1381,13 +1381,13 @@ declare namespace ts { } export interface JsxExpression extends Expression { readonly kind: SyntaxKind.JsxExpression; - readonly parent: JsxElement | JsxAttributeLike; + readonly parent: JsxElement | JsxFragment | JsxAttributeLike; readonly dotDotDotToken?: Token; readonly expression?: Expression; } export interface JsxText extends LiteralLikeNode { readonly kind: SyntaxKind.JsxText; - readonly parent: JsxElement; + readonly parent: JsxElement | JsxFragment; readonly containsOnlyTriviaWhiteSpaces: boolean; } export type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 52725cfb153..538f1702665 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1381,13 +1381,13 @@ declare namespace ts { } export interface JsxExpression extends Expression { readonly kind: SyntaxKind.JsxExpression; - readonly parent: JsxElement | JsxAttributeLike; + readonly parent: JsxElement | JsxFragment | JsxAttributeLike; readonly dotDotDotToken?: Token; readonly expression?: Expression; } export interface JsxText extends LiteralLikeNode { readonly kind: SyntaxKind.JsxText; - readonly parent: JsxElement; + readonly parent: JsxElement | JsxFragment; readonly containsOnlyTriviaWhiteSpaces: boolean; } export type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment; diff --git a/tests/cases/fourslash/autoCloseFragment.ts b/tests/cases/fourslash/autoCloseFragment.ts new file mode 100644 index 00000000000..baa80a8aa62 --- /dev/null +++ b/tests/cases/fourslash/autoCloseFragment.ts @@ -0,0 +1,51 @@ +/// + +// Using separate files for each example to avoid unclosed JSX tags affecting other tests. + +// @Filename: /0.tsx +////const x = <>/*0*/; + +// @Filename: /1.tsx +////const x = <> foo/*1*/ ; + +// @Filename: /2.tsx +////const x = <>/*2*/; + +// @Filename: /3.tsx +////const x = /*3*/; + +// @Filename: /4.tsx +////const x =
+//// <>/*4*/ +////
+////; + +// @Filename: /5.tsx +////const x = <> text /*5*/; + +// @Filename: /6.tsx +////const x = <> +//// <>/*6*/ +////; + +// @Filename: /7.tsx +////const x =
+//// <>/*7*/ +////
; + +// @Filename: /8.tsx +////const x =
+//// <>/*8*/ +////
; + +verify.jsxClosingTag({ + 0: { newText: "" }, + 1: undefined, + 2: undefined, + 3: undefined, + 4: { newText: "" }, + 5: { newText: "" }, + 6: { newText: "" }, + 7: { newText: "" }, + 8: undefined, +});