Add preliminary support for simple conditions in data-driven fixes files

Change-Id: Id65d4cf8daebfa01bf6d59c90f755b9a8827c458
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/171043
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Brian Wilkerson 2020-11-10 18:07:34 +00:00 committed by commit-bot@chromium.org
parent 03bf11e1d9
commit f3f053e328
7 changed files with 357 additions and 23 deletions

View file

@ -3,8 +3,10 @@
// BSD-style license that can be found in the LICENSE file.
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.
@ -12,18 +14,37 @@ 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.
List<_Token> tokens;
/* late */ List<_Token> tokens;
/// The index in the [tokens] of the next token to be consumed.
int currentIndex = 0;
/// The accessors that have been parsed.
List<Accessor> accessors = [];
/// Initialize a newly created parser to report errors to the [errorReporter].
CodeFragmentParser(this.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.
@ -51,6 +72,29 @@ class CodeFragmentParser {
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 token at the given [index] if it exists and if it has one of
/// the [validKinds]. Report an error and return `null` if those conditions
/// aren't met.
@ -155,25 +199,113 @@ class CodeFragmentParser {
return tokens.length;
}
}
/// 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);
}
errorReporter.reportErrorForOffset(TransformSetErrorCode.expectedPrimary,
token.offset + delta, token.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.
@ -195,8 +327,15 @@ class _CodeFragmentScanner {
/// 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() {
if (content.isEmpty) {}
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) {
@ -210,6 +349,33 @@ class _CodeFragmentScanner {
} 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++;
@ -227,12 +393,7 @@ class _CodeFragmentScanner {
tokens.add(_Token(
start, _TokenKind.integer, content.substring(start, offset)));
} else {
errorReporter.reportErrorForOffset(
TransformSetErrorCode.invalidCharacter,
offset + delta,
1,
[content.substring(offset, offset + 1)]);
return null;
return _reportInvalidCharacter(offset);
}
offset = _skipWhitespace(offset);
}
@ -250,6 +411,13 @@ class _CodeFragmentScanner {
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) {
@ -284,26 +452,38 @@ class _Token {
/// 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 '';
}

View file

@ -0,0 +1,50 @@
// 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.
import 'package:analysis_server/src/services/correction/fix/data_driven/value_generator.dart';
/// A binary expression.
class BinaryExpression extends Expression {
/// The left operand.
final Expression leftOperand;
/// The operator.
final Operator operator;
/// The right operand.
final Expression rightOperand;
/// Initialize a newly created binary expression consisting of the
/// [leftOperand], [operator], and [rightOperand].
BinaryExpression(this.leftOperand, this.operator, this.rightOperand);
}
/// An expression.
abstract class Expression {}
/// A literal string.
class LiteralString extends Expression {
/// The value of the literal string.
final String value;
/// Initialize a newly created literal string to have the given [value].
LiteralString(this.value);
}
/// An operator used in a binary expression.
enum Operator {
and,
equal,
notEqual,
}
/// A reference to a variable.
class VariableReference extends Expression {
/// The generator used to generate the value of the variable.
final ValueGenerator generator;
/// Initialize a newly created variable reference to reference the variable
/// whose value is computed by the [generator].
VariableReference(this.generator);
}

View file

@ -17,6 +17,12 @@ class TransformSetErrorCode extends ErrorCode {
'conflicting_key',
"The key '{0}' can't be used when '{1}' is also used.");
/**
* No parameters.
*/
static const TransformSetErrorCode expectedPrimary = TransformSetErrorCode(
'expected_primary', "Expected either an identifier or a string literal.");
/**
* Parameters:
* 0: the character that is invalid
@ -97,7 +103,14 @@ class TransformSetErrorCode extends ErrorCode {
* 0: the missing key
*/
static const TransformSetErrorCode undefinedVariable = TransformSetErrorCode(
'undefined_variable', "The variable '{0}' is not defined.");
'undefined_variable', "The variable '{0}' isn't defined.");
/**
* Parameters:
* 0: the token that was unexpectedly found
*/
static const TransformSetErrorCode unexpectedToken =
TransformSetErrorCode('unexpected_token', "Didn't expect to find {0}.");
/**
* Parameters:
@ -122,7 +135,7 @@ class TransformSetErrorCode extends ErrorCode {
/**
* Parameters:
* 0: a description of the expected kind of token
* 1: a description of the actial kind of token
* 1: a description of the actual kind of token
*/
static const TransformSetErrorCode wrongToken = TransformSetErrorCode(
'wrong_token', "Expected to find {0}, but found {1}.");

View file

@ -0,0 +1,25 @@
// 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.
import 'package:analysis_server/src/services/correction/fix/data_driven/value_generator.dart';
/// A scope in which the generators associated with variables can be looked up.
class VariableScope {
/// The outer scope in which this scope is nested.
final VariableScope outerScope;
/// A table mapping variable names to generators.
final Map<String, ValueGenerator> _generators;
/// Initialize a newly created variable scope defining the variables in the
/// [_generators] map. Any variables not defined locally will be looked up in
/// the [outerScope].
VariableScope(this.outerScope, this._generators);
/// Return the generator used to generate the value of the variable with the
/// given [variableName], or `null` if the variable is not defined.
ValueGenerator lookup(String variableName) {
return _generators[variableName] ?? outerScope?.lookup(variableName);
}
}

View file

@ -5,6 +5,9 @@
import 'package:_fe_analyzer_shared/src/base/errors.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/accessor.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/code_fragment_parser.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/expression.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/value_generator.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/variable_scope.dart';
import 'package:analyzer/error/listener.dart';
import 'package:matcher/matcher.dart';
import 'package:test/test.dart';
@ -15,7 +18,8 @@ import '../../../../../utils/test_support.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(CodeFragmentParserTest);
defineReflectiveTests(AccessorsTest);
defineReflectiveTests(ConditionTest);
});
}
@ -23,20 +27,36 @@ abstract class AbstractCodeFragmentParserTest {
List<Accessor> assertErrors(
String content, List<ExpectedError> expectedErrors) {
var errorListener = GatheringErrorListener();
var errorReporter = ErrorReporter(errorListener, MockSource());
var accessors = CodeFragmentParser(errorReporter).parse(content, 0);
var accessors = _parser(errorListener).parse(content, 0);
errorListener.assertErrors(expectedErrors);
return accessors;
}
Expression assertErrorsInCondition(String content, List<String> variables,
List<ExpectedError> expectedErrors) {
var errorListener = GatheringErrorListener();
var expression =
_parser(errorListener, variables: variables).parseCondition(content, 0);
errorListener.assertErrors(expectedErrors);
return expression;
}
List<Accessor> assertNoErrors(String content) {
var errorListener = GatheringErrorListener();
var errorReporter = ErrorReporter(errorListener, MockSource());
var accessors = CodeFragmentParser(errorReporter).parse(content, 0);
var accessors = _parser(errorListener).parse(content, 0);
errorListener.assertNoErrors();
return accessors;
}
Expression assertNoErrorsInCondition(String content,
{List<String> variables}) {
var errorListener = GatheringErrorListener();
var expression =
_parser(errorListener, variables: variables).parseCondition(content, 0);
errorListener.assertNoErrors();
return expression;
}
ExpectedError error(ErrorCode code, int offset, int length,
{String message,
Pattern messageContains,
@ -46,10 +66,23 @@ abstract class AbstractCodeFragmentParserTest {
message: message,
messageContains: messageContains,
expectedContextMessages: contextMessages);
CodeFragmentParser _parser(GatheringErrorListener listener,
{List<String> variables}) {
var errorReporter = ErrorReporter(listener, MockSource());
var map = <String, ValueGenerator>{};
if (variables != null) {
for (var variableName in variables) {
map[variableName] = CodeFragment([]);
}
}
var scope = VariableScope(null, map);
return CodeFragmentParser(errorReporter, scope: scope);
}
}
@reflectiveTest
class CodeFragmentParserTest extends AbstractCodeFragmentParserTest {
class AccessorsTest extends AbstractCodeFragmentParserTest {
void test_arguments_arguments_arguments() {
var accessors = assertNoErrors('arguments[0].arguments[1].arguments[2]');
expect(accessors, hasLength(3));
@ -83,3 +116,30 @@ class CodeFragmentParserTest extends AbstractCodeFragmentParserTest {
expect(accessors[0], isA<TypeArgumentAccessor>());
}
}
@reflectiveTest
class ConditionTest extends AbstractCodeFragmentParserTest {
void test_and() {
var expression = assertNoErrorsInCondition("'a' != 'b' && 'c' != 'd'")
as BinaryExpression;
expect(expression.leftOperand, isA<BinaryExpression>());
expect(expression.operator, Operator.and);
expect(expression.rightOperand, isA<BinaryExpression>());
}
void test_equal() {
var expression = assertNoErrorsInCondition('a == b', variables: ['a', 'b'])
as BinaryExpression;
expect(expression.leftOperand, isA<VariableReference>());
expect(expression.operator, Operator.equal);
expect(expression.rightOperand, isA<VariableReference>());
}
void test_notEqual() {
var expression = assertNoErrorsInCondition("a != 'b'", variables: ['a'])
as BinaryExpression;
expect(expression.leftOperand, isA<VariableReference>());
expect(expression.operator, Operator.notEqual);
expect(expression.rightOperand, isA<LiteralString>());
}
}

View file

@ -21,6 +21,12 @@ class MissingTokenTest extends AbstractCodeFragmentParserTest {
]);
}
void test_empty() {
assertErrors('', [
error(TransformSetErrorCode.missingToken, 0, 0),
]);
}
void test_identifier_afterPeriod() {
assertErrors('arguments[2].', [
error(TransformSetErrorCode.missingToken, 12, 1),

View file

@ -9,12 +9,12 @@ import '../code_fragment_parser_test.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(MissingTokenTest);
defineReflectiveTests(WrongTokenTest);
});
}
@reflectiveTest
class MissingTokenTest extends AbstractCodeFragmentParserTest {
class WrongTokenTest extends AbstractCodeFragmentParserTest {
void test_closeBracket() {
assertErrors('arguments[2 3', [
error(TransformSetErrorCode.wrongToken, 12, 1),