feat(compiler): recover expression parsing in more malformed pipe cases (#39437)
This commit handles the following cases: - incomplete pipes in a pipe chain - incomplete arguments in a pipe chain - incomplete arguments provided to a pipe - nested pipes The idea is to unconditionally recover on the presence of a pipe, which should be okay because expression parsing can be independently between pipes. PR Close #39437
This commit is contained in:
parent
8d324ec314
commit
e3365724f2
|
@ -1113,20 +1113,25 @@ export class _ParseAST {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error recovery should skip tokens until it encounters a recovery point. skip() treats
|
* Error recovery should skip tokens until it encounters a recovery point.
|
||||||
* the end of input and a ';' as unconditionally a recovery point. It also treats ')',
|
|
||||||
* '}' and ']' as conditional recovery points if one of calling productions is expecting
|
|
||||||
* one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
|
|
||||||
* more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
|
|
||||||
* of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
|
|
||||||
* must be conditional as they must be skipped if none of the calling productions are not
|
|
||||||
* expecting the closing token else we will never make progress in the case of an
|
|
||||||
* extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
|
|
||||||
* parseChain() is always the root production and it expects a ';'.
|
|
||||||
*
|
*
|
||||||
* Furthermore, the presence of a stateful context can add more recovery points.
|
* The following are treated as unconditional recovery points:
|
||||||
* - in a `Writable` context, we are able to recover after seeing the `=` operator, which
|
* - end of input
|
||||||
* signals the presence of an independent rvalue expression following the `=` operator.
|
* - ';' (parseChain() is always the root production, and it expects a ';')
|
||||||
|
* - '|' (since pipes may be chained and each pipe expression may be treated independently)
|
||||||
|
*
|
||||||
|
* The following are conditional recovery points:
|
||||||
|
* - ')', '}', ']' if one of calling productions is expecting one of these symbols
|
||||||
|
* - This allows skip() to recover from errors such as '(a.) + 1' allowing more of the AST to
|
||||||
|
* be retained (it doesn't skip any tokens as the ')' is retained because of the '(' begins
|
||||||
|
* an '(' <expr> ')' production).
|
||||||
|
* The recovery points of grouping symbols must be conditional as they must be skipped if
|
||||||
|
* none of the calling productions are not expecting the closing token else we will never
|
||||||
|
* make progress in the case of an extraneous group closing symbol (such as a stray ')').
|
||||||
|
* That is, we skip a closing symbol if we are not in a grouping production.
|
||||||
|
* - '=' in a `Writable` context
|
||||||
|
* - In this context, we are able to recover after seeing the `=` operator, which
|
||||||
|
* signals the presence of an independent rvalue expression following the `=` operator.
|
||||||
*
|
*
|
||||||
* If a production expects one of these token it increments the corresponding nesting count,
|
* If a production expects one of these token it increments the corresponding nesting count,
|
||||||
* and then decrements it just prior to checking if the token is in the input.
|
* and then decrements it just prior to checking if the token is in the input.
|
||||||
|
@ -1134,7 +1139,7 @@ export class _ParseAST {
|
||||||
private skip() {
|
private skip() {
|
||||||
let n = this.next;
|
let n = this.next;
|
||||||
while (this.index < this.tokens.length && !n.isCharacter(chars.$SEMICOLON) &&
|
while (this.index < this.tokens.length && !n.isCharacter(chars.$SEMICOLON) &&
|
||||||
(this.rparensExpected <= 0 || !n.isCharacter(chars.$RPAREN)) &&
|
!n.isOperator('|') && (this.rparensExpected <= 0 || !n.isCharacter(chars.$RPAREN)) &&
|
||||||
(this.rbracesExpected <= 0 || !n.isCharacter(chars.$RBRACE)) &&
|
(this.rbracesExpected <= 0 || !n.isCharacter(chars.$RBRACE)) &&
|
||||||
(this.rbracketsExpected <= 0 || !n.isCharacter(chars.$RBRACKET)) &&
|
(this.rbracketsExpected <= 0 || !n.isCharacter(chars.$RBRACKET)) &&
|
||||||
(!(this.context & ParseContextFlags.Writable) || !n.isOperator('='))) {
|
(!(this.context & ParseContextFlags.Writable) || !n.isOperator('='))) {
|
||||||
|
|
|
@ -393,9 +393,52 @@ describe('parser', () => {
|
||||||
checkBinding('a | b:(c | d)', '(a | b:(c | d))');
|
checkBinding('a | b:(c | d)', '(a | b:(c | d))');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse incomplete pipes', () => {
|
describe('should parse incomplete pipes', () => {
|
||||||
checkBinding('a | b | ', '((a | b) | )');
|
const cases: Array<[string, string, string, string]> = [
|
||||||
expectBindingError('a | b | ', 'Unexpected end of input, expected identifier or keyword');
|
[
|
||||||
|
'should parse missing pipe names: end',
|
||||||
|
'a | b | ',
|
||||||
|
'((a | b) | )',
|
||||||
|
'Unexpected end of input, expected identifier or keyword',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'should parse missing pipe names: middle',
|
||||||
|
'a | | b',
|
||||||
|
'((a | ) | b)',
|
||||||
|
'Unexpected token |, expected identifier or keyword',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'should parse missing pipe names: start',
|
||||||
|
' | a | b',
|
||||||
|
'(( | a) | b)',
|
||||||
|
'Unexpected token |',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'should parse missing pipe args: end',
|
||||||
|
'a | b | c: ',
|
||||||
|
'((a | b) | c:)',
|
||||||
|
'Unexpected end of expression',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'should parse missing pipe args: middle',
|
||||||
|
'a | b: | c',
|
||||||
|
'((a | b:) | c)',
|
||||||
|
'Unexpected token |',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'should parse incomplete pipe args',
|
||||||
|
'a | b: (a | ) + | c',
|
||||||
|
'((a | b:(a | ) + ) | c)',
|
||||||
|
'Unexpected token |',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [name, input, output, err] of cases) {
|
||||||
|
it(name, () => {
|
||||||
|
checkBinding(input, output);
|
||||||
|
expectBindingError(input, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only allow identifier or keyword as formatter names', () => {
|
it('should only allow identifier or keyword as formatter names', () => {
|
||||||
|
|
Loading…
Reference in New Issue