Skip to content

Commit b0acaea

Browse files
jensjohacommit-bot@chromium.org
authored andcommitted
[parser] Recover written out binary operators
* Add mechanisms to try out a recovery and only perform it if is successful. * Recover written out binary operators, e.g. "a xor b", "a or b" etc. This fixes #26810 and is also how these operators are written in e.g. Kotlin. This might be somewhat controversial though. * Adds a mechanism to _replace_ a token by another token. This might be controversial. It is done because just inserting the operator causes the same rewrite to be attempted on the same token stream several times (at least with the way the token stream is parsed via the CFE) in turn causing it to be recovered *sometimes*. This is now avoided by actually replacing the token. Change-Id: Icfa806045575d2aa2e5f35126708651b275bcf84 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/162003 Commit-Queue: Jens Johansen <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Johnni Winther <[email protected]>
1 parent 0e93845 commit b0acaea

35 files changed

+12986
-9
lines changed

pkg/_fe_analyzer_shared/lib/src/messages/codes_generated.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,36 @@ const MessageCode messageAwaitNotAsync = const MessageCode("AwaitNotAsync",
292292
analyzerCodes: <String>["AWAIT_IN_WRONG_CONTEXT"],
293293
message: r"""'await' can only be used in 'async' or 'async*' methods.""");
294294

295+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
296+
const Template<
297+
Message Function(
298+
String string,
299+
String
300+
string2)> templateBinaryOperatorWrittenOut = const Template<
301+
Message Function(String string, String string2)>(
302+
messageTemplate:
303+
r"""Binary operator '#string' is written as '#string2' instead of the written out word.""",
304+
tipTemplate: r"""Try replacing '#string' with '#string2'.""",
305+
withArguments: _withArgumentsBinaryOperatorWrittenOut);
306+
307+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
308+
const Code<Message Function(String string, String string2)>
309+
codeBinaryOperatorWrittenOut =
310+
const Code<Message Function(String string, String string2)>(
311+
"BinaryOperatorWrittenOut", templateBinaryOperatorWrittenOut,
312+
index: 112);
313+
314+
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
315+
Message _withArgumentsBinaryOperatorWrittenOut(String string, String string2) {
316+
if (string.isEmpty) throw 'No string provided';
317+
if (string2.isEmpty) throw 'No string provided';
318+
return new Message(codeBinaryOperatorWrittenOut,
319+
message:
320+
"""Binary operator '${string}' is written as '${string2}' instead of the written out word.""",
321+
tip: """Try replacing '${string}' with '${string2}'.""",
322+
arguments: {'string': string, 'string2': string2});
323+
}
324+
295325
// DO NOT EDIT. THIS FILE IS GENERATED. SEE TOP OF FILE.
296326
const Template<
297327
Message Function(

pkg/_fe_analyzer_shared/lib/src/parser/parser_impl.dart

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4615,13 +4615,22 @@ class Parser {
46154615
token = typeArg.parseArguments(bangToken, this);
46164616
assert(optional('(', token.next));
46174617
}
4618+
4619+
return _parsePrecedenceExpressionLoop(
4620+
precedence, allowCascades, typeArg, token);
4621+
}
4622+
4623+
Token _parsePrecedenceExpressionLoop(int precedence, bool allowCascades,
4624+
TypeParamOrArgInfo typeArg, Token token) {
46184625
Token next = token.next;
46194626
TokenType type = next.type;
46204627
int tokenLevel = _computePrecedence(next);
4628+
bool enteredLoop = false;
46214629
for (int level = tokenLevel; level >= precedence; --level) {
46224630
int lastBinaryExpressionLevel = -1;
46234631
Token lastCascade;
46244632
while (identical(tokenLevel, level)) {
4633+
enteredLoop = true;
46254634
Token operator = next;
46264635
if (identical(tokenLevel, CASCADE_PRECEDENCE)) {
46274636
if (!allowCascades) {
@@ -4725,10 +4734,103 @@ class Parser {
47254734
type = next.type;
47264735
tokenLevel = _computePrecedence(next);
47274736
}
4737+
if (_recoverAtPrecedenceLevel && !_currentlyRecovering) {
4738+
// Attempt recovery
4739+
if (_attemptPrecedenceLevelRecovery(
4740+
token, precedence, level, allowCascades, typeArg)) {
4741+
// Recovered - try again at same level with the replacement token.
4742+
level++;
4743+
next = token.next;
4744+
type = next.type;
4745+
tokenLevel = _computePrecedence(next);
4746+
}
4747+
}
4748+
}
4749+
4750+
if (!enteredLoop && _recoverAtPrecedenceLevel && !_currentlyRecovering) {
4751+
// Attempt recovery
4752+
if (_attemptPrecedenceLevelRecovery(
4753+
token, precedence, /*currentLevel = */ -1, allowCascades, typeArg)) {
4754+
return _parsePrecedenceExpressionLoop(
4755+
precedence, allowCascades, typeArg, token);
4756+
}
47284757
}
47294758
return token;
47304759
}
47314760

4761+
/// Attempt a recovery where [token.next] is replaced.
4762+
bool _attemptPrecedenceLevelRecovery(Token token, int precedence,
4763+
int currentLevel, bool allowCascades, TypeParamOrArgInfo typeArg) {
4764+
// Attempt recovery.
4765+
assert(_token_recovery_replacements.containsKey(token.next.lexeme));
4766+
TokenType replacement = _token_recovery_replacements[token.next.lexeme];
4767+
if (currentLevel >= 0) {
4768+
// Check that the new precedence and currentLevel would have accepted this
4769+
// replacement here.
4770+
int newLevel = replacement.precedence;
4771+
// The loop it would normally have gone through is something like
4772+
// for (; ; --level) {
4773+
// while (identical(tokenLevel, level)) {
4774+
// }
4775+
// }
4776+
// So if the new tokens level <= the "old" (current) level, [level] (in
4777+
// the above code snippet) would get down to it and accept it.
4778+
// But if the new tokens level > the "old" (current) level, normally we
4779+
// would never get to it - so we shouldn't here either. As the loop starts
4780+
// by taking the first tokens tokenLevel as level, recursing below won't
4781+
// weed that out so we need to do it here.
4782+
if (newLevel > currentLevel) return false;
4783+
}
4784+
4785+
_currentlyRecovering = true;
4786+
_recoverAtPrecedenceLevel = false;
4787+
Listener originalListener = listener;
4788+
TokenStreamRewriter originalRewriter = cachedRewriter;
4789+
NullListener nullListener = listener = new NullListener();
4790+
UndoableTokenStreamRewriter undoableTokenStreamRewriter =
4791+
new UndoableTokenStreamRewriter();
4792+
cachedRewriter = undoableTokenStreamRewriter;
4793+
rewriter.replaceNextTokenWithSyntheticToken(token, replacement);
4794+
bool acceptRecovery = false;
4795+
Token afterExpression = _parsePrecedenceExpressionLoop(
4796+
precedence, allowCascades, typeArg, token);
4797+
4798+
if (!nullListener.hasErrors &&
4799+
isOneOfOrEof(afterExpression.next, const [';', ',', ')', '{', '}'])) {
4800+
// Seems good!
4801+
acceptRecovery = true;
4802+
}
4803+
4804+
// Undo all changes and reset.
4805+
_currentlyRecovering = false;
4806+
undoableTokenStreamRewriter.undo();
4807+
listener = originalListener;
4808+
cachedRewriter = originalRewriter;
4809+
4810+
if (acceptRecovery) {
4811+
// Report and redo recovery.
4812+
reportRecoverableError(
4813+
token.next,
4814+
codes.templateBinaryOperatorWrittenOut
4815+
.withArguments(token.next.lexeme, replacement.lexeme));
4816+
rewriter.replaceNextTokenWithSyntheticToken(token, replacement);
4817+
return true;
4818+
}
4819+
return false;
4820+
}
4821+
4822+
bool _recoverAtPrecedenceLevel = false;
4823+
bool _currentlyRecovering = false;
4824+
static const Map<String, TokenType> _token_recovery_replacements = const {
4825+
// E.g. in Kotlin these are written out, see.
4826+
// https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-int/.
4827+
"xor": TokenType.CARET,
4828+
"and": TokenType.AMPERSAND,
4829+
"or": TokenType.BAR,
4830+
"shl": TokenType.LT_LT,
4831+
"shr": TokenType.GT_GT,
4832+
};
4833+
47324834
int _computePrecedence(Token token) {
47334835
TokenType type = token.type;
47344836
if (identical(type, TokenType.BANG)) {
@@ -4751,7 +4853,15 @@ class Parser {
47514853
if (!isConditional) {
47524854
return SELECTOR_PRECEDENCE;
47534855
}
4856+
} else if (identical(type, TokenType.IDENTIFIER)) {
4857+
// An identifier at this point is not right. So some recovery is going to
4858+
// happen soon. The question is, if we can do a better recovery here.
4859+
if (!_currentlyRecovering &&
4860+
_token_recovery_replacements.containsKey(token.lexeme)) {
4861+
_recoverAtPrecedenceLevel = true;
4862+
}
47544863
}
4864+
47554865
return type.precedence;
47564866
}
47574867

pkg/_fe_analyzer_shared/lib/src/parser/token_stream_rewriter.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import '../scanner/token.dart'
99
BeginToken,
1010
CommentToken,
1111
Keyword,
12+
ReplacementToken,
1213
SimpleToken,
1314
SyntheticBeginToken,
1415
SyntheticKeywordToken,
@@ -113,6 +114,27 @@ abstract class TokenStreamRewriter with _TokenStreamMixin {
113114
return current;
114115
}
115116

117+
/// Insert a new simple synthetic token of [newTokenType] after
118+
/// [previousToken] instead of the token actually coming after it and return
119+
/// the new token.
120+
/// The old token will be linked from the new one though, so it's not totally
121+
/// gone.
122+
ReplacementToken replaceNextTokenWithSyntheticToken(
123+
Token previousToken, TokenType newTokenType) {
124+
assert(newTokenType is! Keyword,
125+
'use an unwritten variation of insertSyntheticKeyword instead');
126+
127+
// [token] <--> [a] <--> [b]
128+
ReplacementToken replacement =
129+
new ReplacementToken(newTokenType, previousToken.next);
130+
insertToken(previousToken, replacement);
131+
// [token] <--> [replacement] <--> [a] <--> [b]
132+
_setNext(replacement, replacement.next.next);
133+
// [token] <--> [replacement] <--> [b]
134+
135+
return replacement;
136+
}
137+
116138
Token _setNext(Token setOn, Token nextToken);
117139
void _setEndGroup(BeginToken setOn, Token endGroup);
118140
void _setOffset(Token setOn, int offset);
@@ -248,6 +270,12 @@ class TokenStreamGhostWriter
248270
void _setPrevious(Token setOn, Token previous) {
249271
throw new UnimplementedError("_setPrevious");
250272
}
273+
274+
@override
275+
ReplacementToken replaceNextTokenWithSyntheticToken(
276+
Token previousToken, TokenType newTokenType) {
277+
throw new UnimplementedError("replaceWithSyntheticToken");
278+
}
251279
}
252280

253281
mixin _TokenStreamMixin {

pkg/_fe_analyzer_shared/lib/src/scanner/token.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,32 @@ class SyntheticToken extends SimpleToken {
804804
Token copy() => new SyntheticToken(type, offset);
805805
}
806806

807+
/// A token used to replace another token in the stream, while still keeping the
808+
/// old token around (in [replacedToken]). Automatically sets the offset and
809+
/// precedingComments from the data available on [replacedToken].
810+
class ReplacementToken extends SyntheticToken {
811+
/// The token that this token replaces. This will normally be the token
812+
/// representing what the user actually wrote.
813+
final Token replacedToken;
814+
815+
ReplacementToken(TokenType type, this.replacedToken)
816+
: super(type, replacedToken.offset) {
817+
precedingComments = replacedToken.precedingComments;
818+
}
819+
820+
@override
821+
Token beforeSynthetic;
822+
823+
@override
824+
bool get isSynthetic => true;
825+
826+
@override
827+
int get length => 0;
828+
829+
@override
830+
Token copy() => new ReplacementToken(type, replacedToken);
831+
}
832+
807833
/**
808834
* A token that was scanned from the input. Each token knows which tokens
809835
* precede and follow it, acting as a link in a doubly linked list of tokens.

pkg/analyzer/lib/error/error.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ const List<ErrorCode> errorCodeValues = [
597597
ParserErrorCode.ANNOTATION_ON_TYPE_ARGUMENT,
598598
ParserErrorCode.ANNOTATION_WITH_TYPE_ARGUMENTS,
599599
ParserErrorCode.ASYNC_KEYWORD_USED_AS_IDENTIFIER,
600+
ParserErrorCode.BINARY_OPERATOR_WRITTEN_OUT,
600601
ParserErrorCode.BREAK_OUTSIDE_OF_LOOP,
601602
ParserErrorCode.CATCH_SYNTAX,
602603
ParserErrorCode.CATCH_SYNTAX_EXTRA_PARAMETERS,

pkg/analyzer/lib/src/dart/error/syntactic_errors.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ class ParserErrorCode extends ErrorCode {
7272
"The keywords 'await' and 'yield' can't be used as "
7373
"identifiers in an asynchronous or generator function.");
7474

75+
static const ParserErrorCode BINARY_OPERATOR_WRITTEN_OUT =
76+
_BINARY_OPERATOR_WRITTEN_OUT;
77+
7578
static const ParserErrorCode BREAK_OUTSIDE_OF_LOOP = _BREAK_OUTSIDE_OF_LOOP;
7679

7780
static const ParserErrorCode CATCH_SYNTAX = _CATCH_SYNTAX;

pkg/analyzer/lib/src/dart/error/syntactic_errors.g.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ final fastaAnalyzerErrorCodes = <ErrorCode>[
119119
_EXTERNAL_LATE_FIELD,
120120
_ABSTRACT_EXTERNAL_FIELD,
121121
_ANNOTATION_ON_TYPE_ARGUMENT,
122+
_BINARY_OPERATOR_WRITTEN_OUT,
122123
];
123124

124125
const ParserErrorCode _ABSTRACT_CLASS_MEMBER = ParserErrorCode(
@@ -148,6 +149,11 @@ const ParserErrorCode _ANNOTATION_WITH_TYPE_ARGUMENTS = ParserErrorCode(
148149
'ANNOTATION_WITH_TYPE_ARGUMENTS',
149150
r"An annotation (metadata) can't use type arguments.");
150151

152+
const ParserErrorCode _BINARY_OPERATOR_WRITTEN_OUT = ParserErrorCode(
153+
'BINARY_OPERATOR_WRITTEN_OUT',
154+
r"Binary operator '#string' is written as '#string2' instead of the written out word.",
155+
correction: "Try replacing '#string' with '#string2'.");
156+
151157
const ParserErrorCode _BREAK_OUTSIDE_OF_LOOP = ParserErrorCode(
152158
'BREAK_OUTSIDE_OF_LOOP',
153159
r"A break statement can't be used outside of a loop or switch statement.",

pkg/analyzer/test/generated/parser_fasta_test.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,17 @@ class CollectionLiteralParserTest extends FastaParserTestCase {
11141114
@reflectiveTest
11151115
class ComplexParserTest_Fasta extends FastaParserTestCase
11161116
with ComplexParserTestMixin {
1117+
void test_binary_operator_written_out_expression() {
1118+
BinaryExpression expression = parseExpression('x xor y', errors: [
1119+
expectedError(ParserErrorCode.BINARY_OPERATOR_WRITTEN_OUT, 2, 3),
1120+
]);
1121+
SimpleIdentifier lhs = expression.leftOperand;
1122+
expect(lhs.name, 'x');
1123+
expect(expression.operator.lexeme, '^');
1124+
SimpleIdentifier rhs = expression.rightOperand;
1125+
expect(rhs.name, 'y');
1126+
}
1127+
11171128
void test_conditionalExpression_precedence_nullableType_as2() {
11181129
ExpressionStatement statement = parseStatement('x as bool? ? (x + y) : z;');
11191130
ConditionalExpression expression = statement.expression;

0 commit comments

Comments
 (0)