Skip to content

Commit 617251f

Browse files
authored
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`
1 parent 07fd7bc commit 617251f

File tree

6 files changed

+68
-8
lines changed

6 files changed

+68
-8
lines changed

src/compiler/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2598,14 +2598,14 @@ namespace ts {
25982598

25992599
export interface JsxExpression extends Expression {
26002600
readonly kind: SyntaxKind.JsxExpression;
2601-
readonly parent: JsxElement | JsxAttributeLike;
2601+
readonly parent: JsxElement | JsxFragment | JsxAttributeLike;
26022602
readonly dotDotDotToken?: Token<SyntaxKind.DotDotDotToken>;
26032603
readonly expression?: Expression;
26042604
}
26052605

26062606
export interface JsxText extends LiteralLikeNode {
26072607
readonly kind: SyntaxKind.JsxText;
2608-
readonly parent: JsxElement;
2608+
readonly parent: JsxElement | JsxFragment;
26092609
readonly containsOnlyTriviaWhiteSpaces: boolean;
26102610
}
26112611

src/services/completions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1180,7 +1180,7 @@ namespace ts.Completions {
11801180
case SyntaxKind.CaseKeyword:
11811181
return getSwitchedType(cast(parent, isCaseClause), checker);
11821182
case SyntaxKind.OpenBraceToken:
1183-
return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
1183+
return isJsxExpression(parent) && !isJsxElement(parent.parent) && !isJsxFragment(parent.parent) ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
11841184
default:
11851185
const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile);
11861186
return argInfo ?

src/services/services.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2088,10 +2088,15 @@ namespace ts {
20882088
const token = findPrecedingToken(position, sourceFile);
20892089
if (!token) return undefined;
20902090
const element = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningElement(token.parent) ? token.parent.parent
2091-
: isJsxText(token) ? token.parent : undefined;
2091+
: isJsxText(token) && isJsxElement(token.parent) ? token.parent : undefined;
20922092
if (element && isUnclosedTag(element)) {
20932093
return { newText: `</${element.openingElement.tagName.getText(sourceFile)}>` };
20942094
}
2095+
const fragment = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningFragment(token.parent) ? token.parent.parent
2096+
: isJsxText(token) && isJsxFragment(token.parent) ? token.parent : undefined;
2097+
if (fragment && isUnclosedFragment(fragment)) {
2098+
return { newText: "</>" };
2099+
}
20952100
}
20962101

20972102
function getLinesForRange(sourceFile: SourceFile, textRange: TextRange) {
@@ -2334,6 +2339,10 @@ namespace ts {
23342339
isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent);
23352340
}
23362341

2342+
function isUnclosedFragment({ closingFragment, parent }: JsxFragment): boolean {
2343+
return !!(closingFragment.flags & NodeFlags.ThisNodeHasError) || (isJsxFragment(parent) && isUnclosedFragment(parent));
2344+
}
2345+
23372346
function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined {
23382347
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
23392348
const range = formatting.getRangeOfEnclosingComment(sourceFile, position);

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,13 +1381,13 @@ declare namespace ts {
13811381
}
13821382
export interface JsxExpression extends Expression {
13831383
readonly kind: SyntaxKind.JsxExpression;
1384-
readonly parent: JsxElement | JsxAttributeLike;
1384+
readonly parent: JsxElement | JsxFragment | JsxAttributeLike;
13851385
readonly dotDotDotToken?: Token<SyntaxKind.DotDotDotToken>;
13861386
readonly expression?: Expression;
13871387
}
13881388
export interface JsxText extends LiteralLikeNode {
13891389
readonly kind: SyntaxKind.JsxText;
1390-
readonly parent: JsxElement;
1390+
readonly parent: JsxElement | JsxFragment;
13911391
readonly containsOnlyTriviaWhiteSpaces: boolean;
13921392
}
13931393
export type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,13 +1381,13 @@ declare namespace ts {
13811381
}
13821382
export interface JsxExpression extends Expression {
13831383
readonly kind: SyntaxKind.JsxExpression;
1384-
readonly parent: JsxElement | JsxAttributeLike;
1384+
readonly parent: JsxElement | JsxFragment | JsxAttributeLike;
13851385
readonly dotDotDotToken?: Token<SyntaxKind.DotDotDotToken>;
13861386
readonly expression?: Expression;
13871387
}
13881388
export interface JsxText extends LiteralLikeNode {
13891389
readonly kind: SyntaxKind.JsxText;
1390-
readonly parent: JsxElement;
1390+
readonly parent: JsxElement | JsxFragment;
13911391
readonly containsOnlyTriviaWhiteSpaces: boolean;
13921392
}
13931393
export type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// Using separate files for each example to avoid unclosed JSX tags affecting other tests.
4+
5+
// @Filename: /0.tsx
6+
////const x = <>/*0*/;
7+
8+
// @Filename: /1.tsx
9+
////const x = <> foo/*1*/ </>;
10+
11+
// @Filename: /2.tsx
12+
////const x = <></>/*2*/;
13+
14+
// @Filename: /3.tsx
15+
////const x = </>/*3*/;
16+
17+
// @Filename: /4.tsx
18+
////const x = <div>
19+
//// <>/*4*/
20+
//// </div>
21+
////</>;
22+
23+
// @Filename: /5.tsx
24+
////const x = <> text /*5*/;
25+
26+
// @Filename: /6.tsx
27+
////const x = <>
28+
//// <>/*6*/
29+
////</>;
30+
31+
// @Filename: /7.tsx
32+
////const x = <div>
33+
//// <>/*7*/
34+
////</div>;
35+
36+
// @Filename: /8.tsx
37+
////const x = <div>
38+
//// <>/*8*/</>
39+
////</div>;
40+
41+
verify.jsxClosingTag({
42+
0: { newText: "</>" },
43+
1: undefined,
44+
2: undefined,
45+
3: undefined,
46+
4: { newText: "</>" },
47+
5: { newText: "</>" },
48+
6: { newText: "</>" },
49+
7: { newText: "</>" },
50+
8: undefined,
51+
});

0 commit comments

Comments
 (0)