Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

parser and language service fixes for unclosed element tags #42554

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/compiler/src/ml_parser/parser.ts
Expand Up @@ -313,6 +313,7 @@ class _TreeBuilder {
* opening tag is recovered).
*/
private _popElement(fullName: string, endSourceSpan: ParseSourceSpan|null): boolean {
let unexpectedCloseTagDetected = false;
for (let stackIndex = this._elementStack.length - 1; stackIndex >= 0; stackIndex--) {
const el = this._elementStack[stackIndex];
if (el.name == fullName) {
Expand All @@ -323,11 +324,14 @@ class _TreeBuilder {
el.sourceSpan.end = endSourceSpan !== null ? endSourceSpan.end : el.sourceSpan.end;

this._elementStack.splice(stackIndex, this._elementStack.length - stackIndex);
return true;
return !unexpectedCloseTagDetected;
}

if (!this.getTagDefinition(el.name).closedByParent) {
return false;
// Note that we encountered an unexpected close tag but continue processing the element
// stack so we can assign an `endSourceSpan` if there is a corresponding start tag for this
// end tag in the stack.
unexpectedCloseTagDetected = true;
}
}
return false;
Expand Down
14 changes: 14 additions & 0 deletions packages/compiler/test/ml_parser/html_parser_spec.ts
Expand Up @@ -857,6 +857,20 @@ import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn, humanizeNodes}
]]);
});

it('gets correct close tag for parent when a child is not closed', () => {
const {errors, rootNodes} = parser.parse('<div><span></div>', 'TestComp');
expect(errors.length).toEqual(1);
expect(humanizeErrors(errors)).toEqual([[
'div',
'Unexpected closing tag "div". It may happen when the tag has already been closed by another tag. For more info see https://www.w3.org/TR/html5/syntax.html#closing-elements-that-have-implied-end-tags',
'0:11'
]]);
expect(humanizeNodes(rootNodes, true)).toEqual([
[html.Element, 'div', 0, '<div><span></div>', '<div>', '</div>'],
[html.Element, 'span', 1, '<span>', '<span>', null],
]);
});

describe('incomplete element tag', () => {
it('should parse and report incomplete tags after the tag name', () => {
const {errors, rootNodes} = parser.parse('<div<span><div </span>', 'TestComp');
Expand Down
14 changes: 11 additions & 3 deletions packages/language-service/ivy/template_target.ts
Expand Up @@ -286,7 +286,7 @@ class TemplateTargetVisitor implements t.Visitor {

visit(node: t.Node) {
const {start, end} = getSpanIncludingEndTag(node);
if (!isWithin(this.position, {start, end})) {
if (end !== null && !isWithin(this.position, {start, end})) {
return;
}

Expand Down Expand Up @@ -441,8 +441,16 @@ function getSpanIncludingEndTag(ast: t.Node) {
// the end of the closing tag. Otherwise, for situation like
// <my-component></my-comp¦onent> where the cursor is in the closing tag
// we will not be able to return any information.
if ((ast instanceof t.Element || ast instanceof t.Template) && ast.endSourceSpan) {
result.end = ast.endSourceSpan.end.offset;
if (ast instanceof t.Element || ast instanceof t.Template) {
if (ast.endSourceSpan) {
result.end = ast.endSourceSpan.end.offset;
} else if (ast.children.length > 0) {
// If the AST has children but no end source span, then it is an unclosed element with an end
// that should be the end of the last child.
result.end = getSpanIncludingEndTag(ast.children[ast.children.length - 1]).end;
} else {
// This is likely a self-closing tag with no children so the `sourceSpan.end` is correct.
}
}
return result;
}
36 changes: 36 additions & 0 deletions packages/language-service/ivy/test/legacy/template_target_spec.ts
Expand Up @@ -797,3 +797,39 @@ describe('findNodeAtPosition for microsyntax expression', () => {
expect((context as SingleNodeTarget).node).toBeInstanceOf(t.Element);
});
});

describe('unclosed elements', () => {
it('should locate children of unclosed elements', () => {
const {errors, nodes, position} = parse(`<div> {{b¦ar}}`);
expect(errors).toBe(null);
const {context} = getTargetAtPosition(nodes, position)!;
const {node} = context as SingleNodeTarget;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});

it('should locate children of outside of unclosed when parent is closed elements', () => {
const {nodes, position} = parse(`<li><div></li> {{b¦ar}}`);
const {context} = getTargetAtPosition(nodes, position)!;
const {node} = context as SingleNodeTarget;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});

it('should locate nodes before unclosed element', () => {
const {nodes, position} = parse(`<li>{{b¦ar}}<div></li>`);
const {context} = getTargetAtPosition(nodes, position)!;
const {node} = context as SingleNodeTarget;
expect(isExpressionNode(node!)).toBe(true);
expect(node).toBeInstanceOf(e.PropertyRead);
});

it('should be correct for end tag of parent node with unclosed child', () => {
const {nodes, position} = parse(`<li><div><div>{{bar}}</l¦i>`);
const {context} = getTargetAtPosition(nodes, position)!;
const {node} = context as SingleNodeTarget;
expect(isTemplateNode(node!)).toBe(true);
expect(node).toBeInstanceOf(t.Element);
expect((node as t.Element).name).toBe('li');
});
});