blob: b81936e04a099197c3cc8fd9ca2be2a17d9d62bf [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.
import 'package:analysis_server/src/services/correction/fix/data_driven/add_type_parameter.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/change.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/element_descriptor.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/modify_parameters.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/parameter_reference.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/rename.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/transform.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/transform_set.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/value_extractor.dart';
import 'package:analysis_server/src/utilities/extensions/yaml.dart';
import 'package:analyzer/error/listener.dart';
import 'package:yaml/yaml.dart';
/// A parser used to read a transform set from a file.
class TransformSetParser {
static const String _argumentValueKey = 'argumentValue';
static const String _changesKey = 'changes';
static const String _classKey = 'class';
static const String _constantKey = 'constant';
static const String _constructorKey = 'constructor';
static const String _defaultValueKey = 'defaultValue';
static const String _elementKey = 'element';
static const String _enumKey = 'enum';
static const String _extensionKey = 'extension';
static const String _fieldKey = 'field';
static const String _functionKey = 'function';
static const String _getterKey = 'getter';
static const String _inClassKey = 'inClass';
static const String _inEnumKey = 'inEnum';
static const String _inExtensionKey = 'inExtension';
static const String _indexKey = 'index';
static const String _inMixinKey = 'inMixin';
static const String _kindKey = 'kind';
static const String _methodKey = 'method';
static const String _mixinKey = 'mixin';
static const String _nameKey = 'name';
static const String _newNameKey = 'newName';
static const String _setterKey = 'setter';
static const String _styleKey = 'style';
static const String _titleKey = 'title';
static const String _transformsKey = 'transforms';
static const String _typedefKey = 'typedef';
static const String _urisKey = 'uris';
static const String _valueKey = 'value';
static const String _versionKey = 'version';
/// A table mapping top-level keys for member elements to the list of keys for
/// the possible containers of that element.
static const Map<String, List<String>> _containerKeyMap = {
_constructorKey: [_inClassKey],
_constantKey: [_inEnumKey],
_fieldKey: [_inClassKey, _inExtensionKey, _inMixinKey],
_getterKey: [_inClassKey, _inExtensionKey, _inMixinKey],
_methodKey: [_inClassKey, _inExtensionKey, _inMixinKey],
_setterKey: [_inClassKey, _inExtensionKey, _inMixinKey],
};
static const String _addParameterKind = 'addParameter';
static const String _addTypeParameterKind = 'addTypeParameter';
static const String _argumentKind = 'argument';
static const String _removeParameterKind = 'removeParameter';
static const String _renameKind = 'rename';
static const int currentVersion = 1;
/// The error reporter to which diagnostics will be reported.
final ErrorReporter errorReporter;
/// The parameter modifications associated with the current transform, or
/// `null` if the current transform does not yet have any such modifications.
List<ParameterModification> _parameterModifications;
/// Initialize a newly created parser to report diagnostics to the
/// [errorReporter].
TransformSetParser(this.errorReporter);
/// Return the result of parsing the file [content] into a transform set, or
/// `null` if the content does not represent a valid transform set.
TransformSet parse(String content) {
assert(content != null);
var map = _parseYaml(content);
if (map == null) {
// The error has already been reported.
return null;
}
return _translateTransformSet(map);
}
/// Return a textual description of the type of value represented by the
/// [node].
String _nodeType(YamlNode node) {
if (node is YamlScalar) {
return node.value.runtimeType.toString();
} else if (node is YamlList) {
return 'List';
} else if (node is YamlMap) {
return 'Map';
}
// We shouldn't get here.
return node.runtimeType.toString();
}
/// Return the result of parsing the file [content] into a YAML node.
YamlNode _parseYaml(String content) {
try {
return loadYamlNode(content);
} on YamlException catch (e) {
var span = e.span;
errorReporter.reportErrorForOffset(TransformSetErrorCode.yamlSyntaxError,
span.start.offset, span.length, [e.message]);
}
return null;
}
/// Report a diagnostic with the given [code] associated with the given
/// [node]. A list of [arguments] should be provided if the diagnostic message
/// has parameters.
void _reportError(TransformSetErrorCode code, YamlNode node,
[List<String> arguments]) {
var span = node.span;
errorReporter.reportErrorForOffset(
code, span.start.offset, span.length, arguments);
}
/// Report any keys in the [map] whose values are not in [validKeys].
void _reportUnsupportedKeys(YamlMap map, Set<String> validKeys) {
for (var keyNode in map.nodes.keys) {
if (keyNode is YamlScalar && keyNode.value is String) {
var key = keyNode.value as String;
if (key != null && !validKeys.contains(key)) {
_reportError(TransformSetErrorCode.unsupportedKey, keyNode, [key]);
}
} else {
// TODO(brianwilkerson) Report the invalidKey.
// "Keys must be of type 'String' but found the type '{0}'."
}
}
}
/// Given a [map] and a set of [validKeys], ensure that only one of those keys
/// is in the map and return it. If more than one of the keys is in the map,
/// report a diagnostic.
String _singleKey(YamlMap map, List<String> validKeys) {
if (validKeys == null) {
return null;
}
var foundKeys = <String>[];
var keyToNodeMap = <String, YamlNode>{};
for (var keyNode in map.nodes.keys) {
if (keyNode is YamlScalar && keyNode.value is String) {
var key = keyNode.value as String;
if (key != null && validKeys.contains(key)) {
foundKeys.add(key);
keyToNodeMap[key] = keyNode;
}
}
}
if (foundKeys.isEmpty) {
return null;
}
for (var i = 1; i < foundKeys.length; i++) {
// var invalidNode = keyToNodeMap[foundKeys[i]];
// TODO(brianwilkerson) Report the problem.
}
return foundKeys[0];
}
/// Translate the [node] into a add-parameter modification.
void _translateAddParameterChange(YamlMap node) {
_singleKey(node, [_indexKey, _nameKey]);
_reportUnsupportedKeys(node, const {_indexKey, _kindKey, _nameKey});
var index = _translateInteger(node.valueAt(_indexKey), _indexKey);
if (index == null) {
return;
}
var name = _translateString(node.valueAt(_nameKey), _nameKey);
if (name == null) {
return;
}
var style = _translateString(node.valueAt(_styleKey), _styleKey);
if (style == null) {
return;
}
var isRequired = style.startsWith('required_');
var isPositional = style.endsWith('_positional');
// TODO(brianwilkerson) I originally thought we'd need a default value, but
// it seems like we ought to be able to get it from the overridden method,
// so investigate removing this field.
var defaultValue = _translateValueExtractor(
node.valueAt(_defaultValueKey), _defaultValueKey);
if (isRequired && defaultValue != null) {
// TODO(brianwilkerson) Report that required parameters can't have a
// default value.
return;
}
var argumentValue = _translateValueExtractor(
node.valueAt(_argumentValueKey), _argumentValueKey);
// TODO(brianwilkerson) We really ought to require an argument value for
// optional positional parameters too for the case where the added
// parameter is being added before the end of the list and call sites might
// already be providing a value for subsequent parameters. Unfortunately we
// can't know at this point whether there are subsequent parameters in
// order to require it only when it's potentially necessary.
if (isRequired && argumentValue == null) {
// TODO(brianwilkerson) Report that required parameters must have an
// argument value.
return;
}
_parameterModifications ??= [];
_parameterModifications.add(AddParameter(
index, name, isRequired, isPositional, defaultValue, argumentValue));
}
/// Translate the [node] into an add-type-parameter change. Return the
/// resulting change, or `null` if the [node] does not represent a valid
/// add-type-parameter change.
AddTypeParameter _translateAddTypeParameterChange(YamlMap node) {
_reportUnsupportedKeys(
node, const {_indexKey, _kindKey, _nameKey, _valueKey});
var index = _translateInteger(node.valueAt(_indexKey), _indexKey);
if (index == null) {
return null;
}
var name = _translateString(node.valueAt(_nameKey), _nameKey);
if (name == null) {
return null;
}
var value = _translateValueExtractor(node.valueAt(_valueKey), _valueKey);
if (value == null) {
return null;
}
return AddTypeParameter(index: index, name: name, value: value);
}
/// Translate the [node] into a value extractor. Return the resulting
/// extractor, or `null` if the [node] does not represent a valid value
/// extractor.
ValueExtractor _translateArgumentExtractor(YamlMap node) {
var indexNode = node.valueAt(_indexKey);
if (indexNode != null) {
_reportUnsupportedKeys(node, const {_indexKey, _kindKey});
var index = _translateInteger(indexNode, _indexKey);
if (index == null) {
// The error has already been reported.
return null;
}
return ArgumentExtractor(PositionalParameterReference(index));
}
var nameNode = node.valueAt(_nameKey);
if (nameNode != null) {
_reportUnsupportedKeys(node, const {_nameKey, _kindKey});
var name = _translateString(nameNode, _nameKey);
if (name == null) {
// The error has already been reported.
return null;
}
return ArgumentExtractor(NamedParameterReference(name));
}
// TODO(brianwilkerson) Report the missing YAML.
return null;
}
/// Translate the [node] into a change. Return the resulting change, or `null`
/// if the [node] does not represent a valid change. If the [node] is not
/// valid, use the name of the associated [key] to report the error.
Change _translateChange(YamlNode node, String key) {
if (node is YamlMap) {
var kind = _translateString(node.valueAt(_kindKey), _kindKey);
if (kind == _addTypeParameterKind) {
return _translateAddTypeParameterChange(node);
} else if (kind == _renameKind) {
return _translateRenameChange(node);
} else if (kind == _addParameterKind) {
_translateAddParameterChange(node);
return null;
} else if (kind == _removeParameterKind) {
_translateRemoveParameterChange(node);
return null;
}
// TODO(brianwilkerson) Report the invalid change kind.
return null;
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML.
return null;
} else {
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'Map', _nodeType(node)]);
return null;
}
}
/// Translate the [node] into an element descriptor. Return the resulting
/// descriptor, or `null` if the [node] does not represent a valid element
/// descriptor. If the [node] is not valid, use the name of the associated
/// [key] to report the error.
ElementDescriptor _translateElement(YamlNode node, String key) {
if (node is YamlMap) {
var uris =
_translateList(node.valueAt(_urisKey), _urisKey, _translateString);
if (uris == null) {
// The error has already been reported.
// TODO(brianwilkerson) Returning here prevents other errors from being
// reported.
return null;
}
var elementKey = _singleKey(node, [
_classKey,
_constantKey,
_constructorKey,
_enumKey,
_extensionKey,
_fieldKey,
_functionKey,
_getterKey,
_methodKey,
_mixinKey,
_setterKey,
_typedefKey
]);
var elementName = _translateString(node.valueAt(elementKey), elementKey);
if (elementName == null) {
// The error has already been reported.
return null;
}
var components = [elementName];
var containerKey = _singleKey(node, _containerKeyMap[elementKey]);
var containerName =
_translateString(node.valueAt(containerKey), containerKey);
if (containerName == null) {
if ([_constructorKey, _constantKey, _methodKey, _fieldKey]
.contains(elementKey)) {
// TODO(brianwilkerson) Report that no container was found.
return null;
}
} else {
components.insert(0, containerName);
}
return ElementDescriptor(libraryUris: uris, components: components);
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML.
return null;
} else {
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'Map', _nodeType(node)]);
return null;
}
}
/// Translate the [node] into an integer. Return the resulting integer, or
/// `null` if the [node] does not represent a valid integer. If the [node] is
/// not valid, use the name of the associated [key] to report the error.
int _translateInteger(YamlNode node, String key) {
if (node is YamlScalar) {
var value = node.value;
if (value is int) {
return value;
}
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'int', _nodeType(node)]);
return null;
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML. For the best UX we
// probably need to pass in the code to report.
return null;
} else {
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'int', _nodeType(node)]);
return null;
}
}
/// Translate the [node] into a list of objects using the [elementTranslator].
/// Return the resulting list, or `null` if the [node] does not represent a
/// valid list. If any of the elements of the list can't be translated, they
/// will be omitted from the list, the name of the associated [key] will be
/// used to report the error, and the valid elements will be returned.
List<R> _translateList<R>(YamlNode node, String key,
R Function(YamlNode, String) elementTranslator) {
if (node is YamlList) {
var translatedList = <R>[];
for (var element in node.nodes) {
var result = elementTranslator(element, key);
if (result != null) {
translatedList.add(result);
}
}
return translatedList;
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML.
return null;
} else {
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'List', _nodeType(node)]);
return null;
}
}
/// Translate the [node] into a remove-parameter modification.
void _translateRemoveParameterChange(YamlMap node) {
_singleKey(node, [_indexKey, _nameKey]);
_reportUnsupportedKeys(node, const {_indexKey, _kindKey, _nameKey});
ParameterReference reference;
var index = _translateInteger(node.valueAt(_indexKey), _indexKey);
if (index != null) {
reference = PositionalParameterReference(index);
} else {
var name = _translateString(node.valueAt(_nameKey), _nameKey);
if (name == null) {
return;
}
reference = NamedParameterReference(name);
}
_parameterModifications ??= [];
_parameterModifications.add(RemoveParameter(reference));
}
/// Translate the [node] into a rename change. Return the resulting change, or
/// `null` if the [node] does not represent a valid rename change.
Rename _translateRenameChange(YamlMap node) {
_reportUnsupportedKeys(node, const {_kindKey, _newNameKey});
var newName = _translateString(node.valueAt(_newNameKey), _newNameKey);
if (newName == null) {
return null;
}
return Rename(newName: newName);
}
/// Translate the [node] into a string. Return the resulting string, or `null`
/// if the [node] does not represent a valid string. If the [node] is not
/// valid, use the name of the associated [key] to report the error.
String _translateString(YamlNode node, String key) {
if (node is YamlScalar) {
var value = node.value;
if (value is String) {
return value;
}
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'String', _nodeType(node)]);
return null;
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML. For the best UX we
// probably need to pass in the code to report.
return null;
} else {
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'String', _nodeType(node)]);
return null;
}
}
/// Translate the [node] into a transform. Return the resulting transform, or
/// `null` if the [node] does not represent a valid transform. If the [node]
/// is not valid, use the name of the associated [key] to report the error.
Transform _translateTransform(YamlNode node, String key) {
if (node is YamlMap) {
_reportUnsupportedKeys(node, const {_changesKey, _elementKey, _titleKey});
var title = _translateString(node.valueAt(_titleKey), _titleKey);
var element = _translateElement(node.valueAt(_elementKey), _elementKey);
var changes = _translateList(
node.valueAt(_changesKey), _changesKey, _translateChange);
if (changes == null) {
// The error has already been reported.
return null;
}
if (_parameterModifications != null) {
changes.add(ModifyParameters(modifications: _parameterModifications));
_parameterModifications = null;
}
return Transform(title: title, element: element, changes: changes);
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML.
return null;
} else {
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'Map', _nodeType(node)]);
return null;
}
}
/// Translate the [node] into a transform set. Return the resulting transform
/// set, or `null` if the [node] does not represent a valid transform set.
TransformSet _translateTransformSet(YamlNode node) {
if (node is YamlMap) {
_reportUnsupportedKeys(node, const {_transformsKey, _versionKey});
var set = TransformSet();
// TODO(brianwilkerson) Version information is currently being ignored,
// but needs to be used to select a translator.
var version = _translateInteger(node.valueAt(_versionKey), _versionKey);
if (version != currentVersion) {
// TODO(brianwilkerson) Report the unsupported version.
}
var transformations = _translateList(
node.valueAt(_transformsKey), _transformsKey, _translateTransform);
if (transformations == null) {
// The error has already been reported.
return null;
}
for (var transform in transformations) {
set.addTransform(transform);
}
return set;
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML.
return null;
} else {
_reportError(TransformSetErrorCode.invalidValue, node,
['file', 'Map', _nodeType(node)]);
return null;
}
}
/// Translate the [node] into a value extractor. Return the resulting
/// extractor, or `null` if the [node] does not represent a valid value
/// extractor. If the [node] is not valid, use the name of the associated
/// [key] to report the error.
ValueExtractor _translateValueExtractor(YamlNode node, String key) {
if (node is YamlMap) {
var kind = _translateString(node.valueAt(_kindKey), _kindKey);
if (kind == _argumentKind) {
return _translateArgumentExtractor(node);
}
// TODO(brianwilkerson) Report the invalid extractor kind.
return null;
} else if (node == null) {
// TODO(brianwilkerson) Report the missing YAML.
return null;
} else {
_reportError(TransformSetErrorCode.invalidValue, node,
[key, 'Map', _nodeType(node)]);
return null;
}
}
}