blob: bebdc17ff9b31cc92857b77cb2e0863c9e5373ae [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
import 'package:analysis_server/src/services/correction/fix/data_driven/accessor.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/expression.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/parameter_reference.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set_error_code.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/variable_scope.dart';
import 'package:analyzer/error/listener.dart';
/// A parser for the textual representation of a code fragment.
class CodeFragmentParser {
/// The error reporter to which diagnostics will be reported.
final ErrorReporter errorReporter;
/// The scope in which variables can be looked up.
VariableScope variableScope;
/// The amount to be added to translate from offsets within the content to
/// offsets within the file.
int delta;
/// The tokens being parsed.
/* late */ List<_Token> tokens;
/// The index in the [tokens] of the next token to be consumed.
int currentIndex = 0;
/// Initialize a newly created parser to report errors to the [errorReporter].
CodeFragmentParser(this.errorReporter, {VariableScope scope})
: variableScope = scope ?? VariableScope(null, {});
/// Return the current token, or `null` if the end of the tokens has been
/// reached.
_Token get currentToken =>
currentIndex < tokens.length ? tokens[currentIndex] : null;
/// Advance to the next token.
void advance() {
if (currentIndex < tokens.length) {
currentIndex++;
}
}
/// Parse the [content] into a list of accessors. Add the [delta] to translate
/// from offsets within the content to offsets within the file.
///
/// <content> ::=
/// <accessor> ('.' <accessor>)*
List<Accessor> parseAccessors(String content, int delta) {
this.delta = delta;
tokens = _CodeFragmentScanner(content, delta, errorReporter).scan();
if (tokens == null) {
// The error has already been reported.
return null;
}
currentIndex = 0;
var accessors = <Accessor>[];
var accessor = _parseAccessor();
if (accessor == null) {
return accessors;
}
accessors.add(accessor);
while (currentIndex < tokens.length) {
var token = currentToken;
if (token.kind == _TokenKind.period) {
advance();
accessor = _parseAccessor();
if (accessor == null) {
return accessors;
}
accessors.add(accessor);
} else {
errorReporter.reportErrorForOffset(TransformSetErrorCode.wrongToken,
token.offset + delta, token.length, ['.', token.kind.displayName]);
return null;
}
}
return accessors;
}
/// Parse the [content] into a condition. Add the [delta] to translate
/// from offsets within the content to offsets within the file.
///
/// <content> ::=
/// <logicalExpression>
Expression parseCondition(String content, int delta) {
this.delta = delta;
tokens = _CodeFragmentScanner(content, delta, errorReporter).scan();
if (tokens == null) {
// The error has already been reported.
return null;
}
currentIndex = 0;
var expression = _parseLogicalAndExpression();
if (currentIndex < tokens.length) {
var token = tokens[currentIndex];
errorReporter.reportErrorForOffset(TransformSetErrorCode.unexpectedToken,
token.offset + delta, token.length, [token.kind.displayName]);
return null;
}
return expression;
}
/// Return the current token if it exists and has one of the [validKinds].
/// Report an error and return `null` if those conditions aren't met.
_Token _expect(List<_TokenKind> validKinds) {
String validKindsDisplayString() {
var buffer = StringBuffer();
for (var i = 0; i < validKinds.length; i++) {
if (i > 0) {
if (i == validKinds.length - 1) {
buffer.write(' or ');
} else {
buffer.write(', ');
}
}
buffer.write(validKinds[i].displayName);
}
return buffer.toString();
}
var token = currentToken;
if (token == null) {
var offset = 0;
var length = 0;
if (tokens.isNotEmpty) {
var last = tokens.last;
offset = last.offset;
length = last.length;
}
errorReporter.reportErrorForOffset(TransformSetErrorCode.missingToken,
offset + delta, length, [validKindsDisplayString()]);
return null;
}
if (!validKinds.contains(token.kind)) {
errorReporter.reportErrorForOffset(
TransformSetErrorCode.wrongToken,
token.offset + delta,
token.length,
[validKindsDisplayString(), token.kind.displayName]);
return null;
}
return token;
}
/// Parse an accessor.
///
/// <accessor> ::=
/// <identifier> '[' (<integer> | <identifier>) ']'
Accessor _parseAccessor() {
var token = _expect(const [_TokenKind.identifier]);
if (token == null) {
// The error has already been reported.
return null;
}
var identifier = token.lexeme;
if (identifier == 'arguments') {
advance();
token = _expect(const [_TokenKind.openSquareBracket]);
if (token == null) {
// The error has already been reported.
return null;
}
advance();
token = _expect(const [_TokenKind.identifier, _TokenKind.integer]);
if (token == null) {
// The error has already been reported.
return null;
}
ParameterReference reference;
if (token.kind == _TokenKind.identifier) {
reference = NamedParameterReference(token.lexeme);
} else {
var argumentIndex = int.parse(token.lexeme);
reference = PositionalParameterReference(argumentIndex);
}
advance();
token = _expect(const [_TokenKind.closeSquareBracket]);
if (token == null) {
// The error has already been reported.
return null;
}
advance();
return ArgumentAccessor(reference);
} else if (identifier == 'typeArguments') {
advance();
token = _expect(const [_TokenKind.openSquareBracket]);
if (token == null) {
// The error has already been reported.
return null;
}
advance();
token = _expect(const [_TokenKind.integer]);
if (token == null) {
// The error has already been reported.
return null;
}
advance();
var argumentIndex = int.parse(token.lexeme);
token = _expect(const [_TokenKind.closeSquareBracket]);
if (token == null) {
// The error has already been reported.
return null;
}
advance();
return TypeArgumentAccessor(argumentIndex);
} else {
errorReporter.reportErrorForOffset(TransformSetErrorCode.unknownAccessor,
token.offset + delta, token.length, [identifier]);
return null;
}
}
/// Parse a logical expression.
///
/// <equalityExpression> ::=
/// <primaryExpression> (<comparisonOperator> <primaryExpression>)?
/// <comparisonOperator> ::=
/// '==' | '!='
Expression _parseEqualityExpression() {
var expression = _parsePrimaryExpression();
if (expression == null) {
return null;
}
if (currentIndex >= tokens.length) {
return expression;
}
var kind = currentToken.kind;
if (kind == _TokenKind.equal || kind == _TokenKind.notEqual) {
advance();
var operator =
kind == _TokenKind.equal ? Operator.equal : Operator.notEqual;
var rightOperand = _parsePrimaryExpression();
if (rightOperand == null) {
return null;
}
expression = BinaryExpression(expression, operator, rightOperand);
}
return expression;
}
/// Parse a logical expression.
///
/// <logicalExpression> ::=
/// <equalityExpression> ('&&' <equalityExpression>)*
Expression _parseLogicalAndExpression() {
var expression = _parseEqualityExpression();
if (expression == null) {
return null;
}
if (currentIndex >= tokens.length) {
return expression;
}
var kind = currentToken.kind;
while (kind == _TokenKind.and) {
advance();
var rightOperand = _parseEqualityExpression();
if (rightOperand == null) {
return null;
}
expression = BinaryExpression(expression, Operator.and, rightOperand);
if (currentIndex >= tokens.length) {
return expression;
}
kind = currentToken.kind;
}
return expression;
}
/// Parse a logical expression.
///
/// <primaryExpression> ::=
/// <identifier> | <string>
Expression _parsePrimaryExpression() {
var token = currentToken;
var kind = token?.kind;
if (kind == _TokenKind.identifier) {
advance();
var variableName = token.lexeme;
var generator = variableScope.lookup(variableName);
if (generator == null) {
errorReporter.reportErrorForOffset(
TransformSetErrorCode.undefinedVariable,
token.offset + delta,
token.length,
[variableName]);
return null;
}
return VariableReference(generator);
} else if (kind == _TokenKind.string) {
advance();
var lexeme = token.lexeme;
var value = lexeme.substring(1, lexeme.length - 1);
return LiteralString(value);
}
int offset;
int length;
if (token == null) {
if (tokens.isNotEmpty) {
token = tokens[tokens.length - 1];
offset = token.offset + delta;
length = token.length;
} else {
offset = delta;
length = 0;
}
} else {
offset = token.offset + delta;
length = token.length;
}
errorReporter.reportErrorForOffset(
TransformSetErrorCode.expectedPrimary, offset, length);
return null;
}
}
/// A scanner for the textual representation of a code fragment.
class _CodeFragmentScanner {
static final int $0 = '0'.codeUnitAt(0);
static final int $9 = '9'.codeUnitAt(0);
static final int $a = 'a'.codeUnitAt(0);
static final int $z = 'z'.codeUnitAt(0);
static final int $A = 'A'.codeUnitAt(0);
static final int $Z = 'Z'.codeUnitAt(0);
static final int ampersand = '&'.codeUnitAt(0);
static final int bang = '!'.codeUnitAt(0);
static final int closeSquareBracket = ']'.codeUnitAt(0);
static final int carriageReturn = '\r'.codeUnitAt(0);
static final int equal = '='.codeUnitAt(0);
static final int newline = '\n'.codeUnitAt(0);
static final int openSquareBracket = '['.codeUnitAt(0);
static final int period = '.'.codeUnitAt(0);
static final int singleQuote = "'".codeUnitAt(0);
static final int space = ' '.codeUnitAt(0);
/// The string being scanned.
final String content;
/// The length of the string being scanned.
final int length;
/// The offset in the file of the first character in the string being scanned.
final int delta;
/// The error reporter to which diagnostics will be reported.
final ErrorReporter errorReporter;
/// Initialize a newly created scanner to scan the given [content].
_CodeFragmentScanner(this.content, this.delta, this.errorReporter)
: length = content.length;
/// Return the tokens in the content, or `null` if there is an error in the
/// content that prevents it from being scanned.
List<_Token> scan() {
var length = content.length;
int peekAt(int offset) {
if (offset > length) {
return -1;
}
return content.codeUnitAt(offset);
}
var offset = _skipWhitespace(0);
var tokens = <_Token>[];
while (offset < length) {
var char = content.codeUnitAt(offset);
if (char == closeSquareBracket) {
tokens.add(_Token(offset, _TokenKind.closeSquareBracket, ']'));
offset++;
} else if (char == openSquareBracket) {
tokens.add(_Token(offset, _TokenKind.openSquareBracket, '['));
offset++;
} else if (char == period) {
tokens.add(_Token(offset, _TokenKind.period, '.'));
offset++;
} else if (char == ampersand) {
if (peekAt(offset + 1) != ampersand) {
return _reportInvalidCharacter(offset);
}
tokens.add(_Token(offset, _TokenKind.and, '&&'));
offset += 2;
} else if (char == bang) {
if (peekAt(offset + 1) != equal) {
return _reportInvalidCharacter(offset);
}
tokens.add(_Token(offset, _TokenKind.notEqual, '!='));
offset += 2;
} else if (char == equal) {
if (peekAt(offset + 1) != equal) {
return _reportInvalidCharacter(offset);
}
tokens.add(_Token(offset, _TokenKind.equal, '=='));
offset += 2;
} else if (char == singleQuote) {
var start = offset;
offset++;
while (offset < length && content.codeUnitAt(offset) != singleQuote) {
offset++;
}
offset++;
tokens.add(
_Token(start, _TokenKind.string, content.substring(start, offset)));
} else if (_isLetter(char)) {
var start = offset;
offset++;
while (offset < length && _isLetter(content.codeUnitAt(offset))) {
offset++;
}
tokens.add(_Token(
start, _TokenKind.identifier, content.substring(start, offset)));
} else if (_isDigit(char)) {
var start = offset;
offset++;
while (offset < length && _isDigit(content.codeUnitAt(offset))) {
offset++;
}
tokens.add(_Token(
start, _TokenKind.integer, content.substring(start, offset)));
} else {
return _reportInvalidCharacter(offset);
}
offset = _skipWhitespace(offset);
}
return tokens;
}
/// Return `true` if the [char] is a digit.
bool _isDigit(int char) => char >= $0 && char <= $9;
/// Return `true` if the [char] is a letter.
bool _isLetter(int char) =>
(char >= $a && char <= $z) || (char >= $A && char <= $Z);
/// Return `true` if the [char] is a whitespace character.
bool _isWhitespace(int char) =>
char == space || char == newline || char == carriageReturn;
/// Report the presence of an invalid character at the given [offset].
Null _reportInvalidCharacter(int offset) {
errorReporter.reportErrorForOffset(TransformSetErrorCode.invalidCharacter,
offset + delta, 1, [content.substring(offset, offset + 1)]);
return null;
}
/// Return the index of the first character at or after the given [offset]
/// that isn't a whitespace character.
int _skipWhitespace(int offset) {
while (offset < length) {
var char = content.codeUnitAt(offset);
if (!_isWhitespace(char)) {
return offset;
}
offset++;
}
return offset;
}
}
/// A token in a code fragment's string representation.
class _Token {
/// The offset of the token.
final int offset;
/// The kind of the token.
final _TokenKind kind;
/// The lexeme of the token.
final String lexeme;
/// Initialize a newly created token.
_Token(this.offset, this.kind, this.lexeme);
/// Return the length of this token.
int get length => lexeme.length;
}
/// An indication of the kind of a token.
enum _TokenKind {
and,
closeSquareBracket,
equal,
identifier,
integer,
notEqual,
openSquareBracket,
period,
string,
}
extension on _TokenKind {
String get displayName {
switch (this) {
case _TokenKind.and:
return "'&&'";
case _TokenKind.closeSquareBracket:
return "']'";
case _TokenKind.equal:
return "'=='";
case _TokenKind.identifier:
return 'an identifier';
case _TokenKind.integer:
return 'an integer';
case _TokenKind.notEqual:
return "'!='";
case _TokenKind.openSquareBracket:
return "'['";
case _TokenKind.period:
return "'.'";
case _TokenKind.string:
return 'a string';
}
return '';
}
}