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`
This commit is contained in:
Hiroshi Ogawa 2021-09-09 01:22:38 +09:00 committed by GitHub
parent 07fd7bce64
commit 617251f2e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 68 additions and 8 deletions

View file

@ -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<SyntaxKind.DotDotDotToken>;
readonly expression?: Expression;
}
export interface JsxText extends LiteralLikeNode {
readonly kind: SyntaxKind.JsxText;
readonly parent: JsxElement;
readonly parent: JsxElement | JsxFragment;
readonly containsOnlyTriviaWhiteSpaces: boolean;
}

View file

@ -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 ?

View file

@ -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: `</${element.openingElement.tagName.getText(sourceFile)}>` };
}
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);

View file

@ -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<SyntaxKind.DotDotDotToken>;
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;

View file

@ -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<SyntaxKind.DotDotDotToken>;
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;

View file

@ -0,0 +1,51 @@
/// <reference path='fourslash.ts' />
// 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 = <div>
//// <>/*4*/
//// </div>
////</>;
// @Filename: /5.tsx
////const x = <> text /*5*/;
// @Filename: /6.tsx
////const x = <>
//// <>/*6*/
////</>;
// @Filename: /7.tsx
////const x = <div>
//// <>/*7*/
////</div>;
// @Filename: /8.tsx
////const x = <div>
//// <>/*8*/</>
////</div>;
verify.jsxClosingTag({
0: { newText: "</>" },
1: undefined,
2: undefined,
3: undefined,
4: { newText: "</>" },
5: { newText: "</>" },
6: { newText: "</>" },
7: { newText: "</>" },
8: undefined,
});