blob: dc8184c86c64045e3c7f8324a19ec09f05615e66 [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/changes_selector.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/code_template.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/element_descriptor.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/element_kind.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/expression.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/rename_parameter.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/replaced_by.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_generator.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/variable_scope.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/util/yaml.dart';
import 'package:yaml/yaml.dart';
/// Information used to report errors when translating a node.
class ErrorContext {
/// The name of the key used to identify the node that has an error associated
/// with it.
final String key;
/// The node that should be used to compute the highlight region for the
/// diagnostic.
final YamlNode parentNode;
/// Initialize a newly created error context.
ErrorContext({required this.key, required this.parentNode});
}
/// A parser used to read a transform set from a file.
class TransformSetParser {
static const String _argumentValueKey = 'argumentValue';
static const String _bulkApplyKey = 'bulkApply';
static const String _changesKey = 'changes';
static const String _classKey = 'class';
static const String _constantKey = 'constant';
static const String _constructorKey = 'constructor';
static const String _dateKey = 'date';
static const String _elementKey = 'element';
static const String _enumKey = 'enum';
static const String _expressionKey = 'expression';
static const String _extendsKey = 'extends';
static const String _extensionKey = 'extension';
static const String _fieldKey = 'field';
static const String _functionKey = 'function';
static const String _getterKey = 'getter';
static const String _ifKey = 'if';
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 _newElementKey = 'newElement';
static const String _newNameKey = 'newName';
static const String _oldNameKey = 'oldName';
static const String _oneOfKey = 'oneOf';
static const String _requiredIfKey = 'requiredIf';
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 _variableKey = 'variable';
static const String _variablesKey = 'variables';
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 _fragmentKind = 'fragment';
static const String _importKind = 'import';
static const String _removeParameterKind = 'removeParameter';
static const String _renameKind = 'rename';
static const String _renameParameterKind = 'renameParameter';
static const String _replacedByKind = 'replacedBy';
/// The valid values for the [_styleKey] in an [_addParameterKind] change.
static const List<String> validStyles = [
'optional_named',
'optional_positional',
'required_named',
'required_positional'
];
/// A table mapping the kinds of elements that can be replaced by a different
/// element to a set of the kinds of elements with which they can be replaced.
static final Map<ElementKind, Set<ElementKind>> compatibleReplacementTypes =
_createCompatibleReplacementTypes();
static const String _openComponent = '{%';
static const String _closeComponent = '%}';
/// The lowest file version supported by this parser.
static const int oldestVersion = 1;
/// The highest file version supported by this parser. The version needs to be
/// incremented any time the parser is updated to disallow input that would
/// have been valid in the most recently published version of server. This
/// includes removing support for keys and adding a new required key.
static const int currentVersion = 1;
/// The error reporter to which diagnostics will be reported.
final ErrorReporter errorReporter;
/// The name of the package from which the data file being translated was
/// found.
final String packageName;
/// The description of the element that is being transformed by the current
/// transformation, or `null` if we are not in the process of parsing a
/// transformation or if the element associated with the transformation is not
/// valid.
ElementDescriptor? elementBeingTransformed;
/// The variable scope defined for the current transform.
VariableScope transformVariableScope = VariableScope.empty;
/// 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, this.packageName);
/// 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) {
var map = _parseYaml(content);
if (map == null) {
// The error has already been reported.
return null;
}
return _translateTransformSet(map);
}
bool _equalUris(List<Uri> oldUris, List<Uri> newUris) {
var oldSet = oldUris.toSet();
var newSet = newUris.toSet();
return oldSet.difference(newSet).isEmpty &&
newSet.difference(oldSet).isEmpty;
}
/// Convert the given [template] into a list of components. Variable
/// references in the template are looked up in the map of [generators].
List<TemplateComponent> _extractTemplateComponents(
String template, VariableScope variableScope, int templateOffset) {
var components = <TemplateComponent>[];
var textStart = 0;
var variableStart = template.indexOf(_openComponent);
while (variableStart >= 0) {
if (textStart < variableStart) {
// TODO(brianwilkerson) Check for an end brace without a start brace.
components
.add(TemplateText(template.substring(textStart, variableStart)));
}
var endIndex = template.indexOf(_closeComponent, variableStart + 2);
if (endIndex < 0) {
errorReporter.reportErrorForOffset(
TransformSetErrorCode.missingTemplateEnd,
templateOffset + variableStart,
2);
// Ignore the invalid component, treating it as if it extended to the
// end of the template.
return components;
} else {
var name = template.substring(variableStart + 2, endIndex).trim();
var generator = variableScope.lookup(name);
if (generator == null) {
errorReporter.reportErrorForOffset(
TransformSetErrorCode.undefinedVariable,
templateOffset + template.indexOf(name, variableStart),
name.length,
[name]);
// Ignore the invalid component.
} else {
components.add(TemplateVariable(generator));
}
}
textStart = endIndex + 2;
variableStart = template.indexOf(_openComponent, textStart);
}
if (textStart < template.length) {
// TODO(brianwilkerson) Check for an end brace without a start brace.
components.add(TemplateText(template.substring(textStart)));
}
// TODO(brianwilkerson) If there are no other errors, then report
// unreferenced variables.
return components;
}
/// 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 offset of the first character in the [string], exclusive of any
/// surrounding quotes.
int _offsetOfString(YamlScalar string) {
if (string.style == ScalarStyle.PLAIN) {
return string.span.start.offset;
}
return string.span.start.offset + 1;
}
/// 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;
var offset = span?.start.offset ?? 0;
var length = span?.length ?? 0;
errorReporter.reportErrorForOffset(
TransformSetErrorCode.yamlSyntaxError, offset, 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 that the value represented by the [node] does not have the
/// [expectedType], using the [context] to get the key to use in the message.
Null _reportInvalidValue(
YamlNode node, ErrorContext context, String expectedType) {
_reportError(TransformSetErrorCode.invalidValue, node,
[context.key, expectedType, _nodeType(node)]);
return null;
}
/// Report that the value represented by the [node] does not have the
/// [expectedType], using the [context] to get the key to use in the message.
Null _reportInvalidValueOneOf(
YamlNode node, ErrorContext context, List<String> allowedValues) {
_reportError(TransformSetErrorCode.invalidValueOneOf, node,
[context.key, allowedValues.join(', ')]);
return null;
}
/// Report that a required key is missing, using the [context] to locate the
/// node associated with the diagnostic and the key to use in the message.
Null _reportMissingKey(ErrorContext context) {
_reportError(
TransformSetErrorCode.missingKey, context.parentNode, [context.key]);
return null;
}
/// 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) {
var key = _translateKey(keyNode);
if (key != null && !validKeys.contains(key)) {
_reportError(TransformSetErrorCode.unsupportedKey, keyNode, [key]);
}
}
}
/// Given a [map] and a set of [validKeys], ensure that exactly 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 for each extra key. If [required] is `true` and
/// none of the keys is in the map, report a diagnostic at the [errorNode].
String? _singleKey(YamlMap map, List<String> validKeys, YamlNode errorNode,
{bool required = true}) {
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;
if (key is String && validKeys.contains(key)) {
foundKeys.add(key);
keyToNodeMap[key] = keyNode;
}
}
}
if (foundKeys.isEmpty) {
if (required) {
var validKeysList = validKeys.map((key) => "'$key'").join(', ');
_reportError(TransformSetErrorCode.missingOneOfMultipleKeys, errorNode,
[validKeysList]);
}
return null;
}
var firstKey = foundKeys[0];
for (var i = 1; i < foundKeys.length; i++) {
var foundKey = foundKeys[i];
var invalidNode = keyToNodeMap[foundKey]!;
_reportError(TransformSetErrorCode.conflictingKey, invalidNode,
[foundKey, firstKey]);
}
return firstKey;
}
/// Translate the [node] into a add-parameter modification.
void _translateAddParameterChange(YamlMap node) {
_reportUnsupportedKeys(node,
const {_argumentValueKey, _indexKey, _kindKey, _nameKey, _styleKey});
var index = _translateInteger(node.valueAt(_indexKey),
ErrorContext(key: _indexKey, parentNode: node));
if (index == null) {
// The error has already been reported.
return;
}
var name = _translateString(
node.valueAt(_nameKey), ErrorContext(key: _nameKey, parentNode: node));
if (name == null) {
// The error has already been reported.
return;
}
var styleNode = node.valueAt(_styleKey);
var style = _translateString(
styleNode, ErrorContext(key: _styleKey, parentNode: node));
if (styleNode == null || style == null) {
// The error has already been reported.
return;
}
if (!validStyles.contains(style)) {
var validStylesList = validStyles.map((style) => "'$style'").join(', ');
_reportError(TransformSetErrorCode.invalidParameterStyle, styleNode,
[validStylesList]);
return;
}
var isRequired = style.startsWith('required_');
var isPositional = style.endsWith('_positional');
var argumentValueNode = node.valueAt(_argumentValueKey);
var argumentValue = _translateCodeTemplate(argumentValueNode,
ErrorContext(key: _argumentValueKey, parentNode: node),
canBeConditionallyRequired: true);
// 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;
} else if (argumentValue != null &&
argumentValue.requiredIfCondition != null) {
if (style != 'optional_named') {
var valueNode = argumentValueNode as YamlMap;
_reportError(TransformSetErrorCode.invalidRequiredIf,
valueNode.keyAtValue(valueNode.valueAt(_requiredIfKey)!)!);
return;
}
}
_parameterModifications ??= [];
_parameterModifications!.add(
AddParameter(index, name, isRequired, isPositional, 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 {_extendsKey, _indexKey, _kindKey, _nameKey, _argumentValueKey});
var index = _translateInteger(node.valueAt(_indexKey),
ErrorContext(key: _indexKey, parentNode: node));
var name = _translateString(
node.valueAt(_nameKey), ErrorContext(key: _nameKey, parentNode: node));
var extendedType = _translateCodeTemplate(node.valueAt(_extendsKey),
ErrorContext(key: _extendsKey, parentNode: node),
required: false);
var argumentValue = _translateCodeTemplate(node.valueAt(_argumentValueKey),
ErrorContext(key: _argumentValueKey, parentNode: node));
if (index == null || name == null || argumentValue == null) {
// The error has already been reported.
return null;
}
// In order to support adding multiple type parameters we might need to
// introduce a `TypeParameterModification` change, similar to
// `ParameterModification`. That becomes more likely if we add support for
// removing type parameters.
return AddTypeParameter(
index: index,
name: name,
extendedType: extendedType,
argumentValue: argumentValue);
}
/// Translate the [node] into a bool. Return the resulting bool, or `null`
/// if the [node] doesn't represent a valid bool. If the [node] isn't valid,
/// use the [context] to report the error. If the [node] doesn't exist and
/// [required] is `true`, then report an error.
bool? _translateBool(YamlNode? node, ErrorContext context,
{bool required = true}) {
if (node is YamlScalar) {
var value = node.value;
if (value is bool) {
return value;
}
return _reportInvalidValue(node, context, 'boolean');
} else if (node == null) {
if (required) {
return _reportMissingKey(context);
}
return null;
} else {
return _reportInvalidValue(node, context, 'boolean');
}
}
/// 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 [context] to report the error.
Change? _translateChange(YamlNode node, ErrorContext context) {
if (node is YamlMap) {
var kindNode = node.valueAt(_kindKey);
var kindContext = ErrorContext(key: _kindKey, parentNode: node);
var kind = _translateString(kindNode, kindContext);
if (kindNode == null || kind == null) {
return null;
} else if (kind == _addParameterKind) {
_translateAddParameterChange(node);
return null;
} else if (kind == _addTypeParameterKind) {
return _translateAddTypeParameterChange(node);
} else if (kind == _removeParameterKind) {
_translateRemoveParameterChange(node);
return null;
} else if (kind == _renameKind) {
return _translateRenameChange(node);
} else if (kind == _renameParameterKind) {
return _translateRenameParameterChange(node);
} else if (kind == _replacedByKind) {
return _translateReplacedByChange(node);
}
return _reportInvalidValueOneOf(kindNode, kindContext, [
_addParameterKind,
_addTypeParameterKind,
_removeParameterKind,
_renameKind,
_renameParameterKind,
_replacedByKind,
]);
} else {
return _reportInvalidValue(node, context, 'Map');
}
}
/// Translate the [node] into a value generator. Return the resulting
/// generator, or `null` if the [node] does not represent a valid value
/// extractor.
ValueGenerator? _translateCodeFragment(YamlMap node) {
_reportUnsupportedKeys(node, const {_kindKey, _valueKey});
var valueNode = node.valueAt(_valueKey);
var value = _translateString(
valueNode, ErrorContext(key: _valueKey, parentNode: node));
if (valueNode is! YamlScalar || value == null) {
// The error has already been reported.
return null;
}
var accessors = CodeFragmentParser(errorReporter)
.parseAccessors(value, _offsetOfString(valueNode));
if (accessors == null) {
// The error has already been reported.
return null;
}
return CodeFragment(accessors);
}
/// Translate the [node] into a code template. Return the resulting template,
/// or `null` if the [node] doesn't represent a valid code template. If the
/// [node] isn't valid, use the [context] to report the error. If the [node]
/// doesn't exist and [required] is `true`, then report an error.
CodeTemplate? _translateCodeTemplate(YamlNode? node, ErrorContext context,
{bool canBeConditionallyRequired = false, bool required = true}) {
if (node is YamlMap) {
if (canBeConditionallyRequired) {
_reportUnsupportedKeys(
node, const {_expressionKey, _requiredIfKey, _variablesKey});
} else {
_reportUnsupportedKeys(node, const {_expressionKey, _variablesKey});
}
var expressionNode = node.valueAt(_expressionKey);
var template = _translateString(
expressionNode, ErrorContext(key: _expressionKey, parentNode: node));
var variableScope = _translateTemplateVariables(
node.valueAt(_variablesKey),
ErrorContext(key: _variablesKey, parentNode: node));
Expression? requiredIfCondition;
if (canBeConditionallyRequired) {
var requiredIfNode = node.valueAt(_requiredIfKey);
var requiredIfText = _translateString(
requiredIfNode, ErrorContext(key: _requiredIfKey, parentNode: node),
required: false);
if (requiredIfNode is YamlScalar && requiredIfText != null) {
requiredIfCondition = CodeFragmentParser(errorReporter,
scope: variableScope)
.parseCondition(requiredIfText, _offsetOfString(requiredIfNode));
if (requiredIfCondition == null) {
// The error has already been reported.
return null;
}
}
}
if (expressionNode is! YamlScalar || template == null) {
// The error has already been reported.
return null;
}
var templateOffset = _offsetOfString(expressionNode);
var components =
_extractTemplateComponents(template, variableScope, templateOffset);
return CodeTemplate(
CodeTemplateKind.expression, components, requiredIfCondition);
} else if (node == null) {
if (required) {
_reportMissingKey(context);
}
return null;
} else {
return _reportInvalidValue(node, context, 'Map');
}
}
void _translateConditionalChange(YamlNode node, ErrorContext context,
Map<Expression, List<Change>> changeMap) {
if (node is YamlMap) {
_reportUnsupportedKeys(node, const {_ifKey, _changesKey});
var expressionNode = node.valueAt(_ifKey);
var expressionText = _translateString(
expressionNode, ErrorContext(key: _ifKey, parentNode: node));
var changes = _translateList(node.valueAt(_changesKey),
ErrorContext(key: _changesKey, parentNode: node), _translateChange);
var parameterModifications = _parameterModifications;
if (parameterModifications != null) {
if (changes != null) {
changes.add(ModifyParameters(modifications: parameterModifications));
}
_parameterModifications = null;
}
if (expressionNode is YamlScalar &&
expressionText != null &&
changes != null) {
var expression = CodeFragmentParser(errorReporter,
scope: transformVariableScope)
.parseCondition(expressionText, _offsetOfString(expressionNode));
if (expression != null) {
changeMap[expression] = changes;
}
}
} else {
return _reportInvalidValue(node, context, 'Map');
}
}
ChangesSelector? _translateConditionalChanges(
YamlNode node, ErrorContext context) {
if (node is YamlList) {
var changeMap = <Expression, List<Change>>{};
for (var element in node.nodes) {
_translateConditionalChange(element, context, changeMap);
}
return ConditionalChangesSelector(changeMap);
} else {
return _reportInvalidValue(node, context, 'List');
}
}
/// Translate the [node] into a date. Return the resulting date, or `null`
/// if the [node] does not represent a valid date. If the [node] is not
/// valid, use the [context] to report the error.
DateTime? _translateDate(YamlNode? node, ErrorContext context) {
if (node is YamlScalar) {
var value = node.value;
if (value is String) {
try {
return DateTime.parse(value);
} on FormatException {
// Fall through to report the invalid value.
}
}
return _reportInvalidValue(node, context, 'Date');
} else if (node == null) {
return _reportMissingKey(context);
} else {
return _reportInvalidValue(node, context, 'Date');
}
}
/// 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 [context] to report the
/// error.
ElementDescriptor? _translateElement(YamlNode? node, ErrorContext context) {
if (node is YamlMap) {
var urisNode = node.valueAt(_urisKey);
var uris = _translateList(urisNode,
ErrorContext(key: _urisKey, parentNode: node), _translateUri);
var elementKey = _singleKey(
node,
const [
_classKey,
_constantKey,
_constructorKey,
_enumKey,
_extensionKey,
_fieldKey,
_functionKey,
_getterKey,
_methodKey,
_mixinKey,
_setterKey,
_typedefKey,
_variableKey
],
node);
if (elementKey == null) {
// The error has already been reported.
return null;
}
var elementName = _translateString(node.valueAt(elementKey),
ErrorContext(key: elementKey, parentNode: node));
if (elementName == null) {
// The error has already been reported.
return null;
}
var components = [elementName];
if (_containerKeyMap.containsKey(elementKey)) {
var validContainerKeys = _containerKeyMap[elementKey]!;
var containerKey =
_singleKey(node, validContainerKeys, node, required: false);
String? containerName;
if (containerKey != null) {
containerName = _translateString(node.valueAt(containerKey),
ErrorContext(key: containerKey, parentNode: node),
required: false);
}
if (containerName == null) {
if ([_constructorKey, _constantKey, _methodKey, _fieldKey]
.contains(elementKey)) {
var validKeysList =
validContainerKeys.map((key) => "'$key'").join(', ');
_reportError(TransformSetErrorCode.missingOneOfMultipleKeys, node,
[validKeysList]);
return null;
}
} else {
components.add(containerName);
}
}
if (uris == null) {
// The error has already been reported.
return null;
}
if (uris.isEmpty) {
if ((urisNode as YamlList).isEmpty) {
_reportError(TransformSetErrorCode.missingUri, urisNode);
}
return null;
}
return ElementDescriptor(
libraryUris: uris,
kind: ElementKindUtilities.fromName(elementKey)!,
components: components);
} else if (node == null) {
return _reportMissingKey(context);
} else {
return _reportInvalidValue(node, context, 'Map');
}
}
/// Translate the [node] into a value generator. Return the resulting
/// generator, or `null` if the [node] does not represent a valid value
/// extractor.
ValueGenerator? _translateImportValue(YamlMap node) {
_reportUnsupportedKeys(node, const {_kindKey, _nameKey, _urisKey});
var urisNode = node.valueAt(_urisKey);
var uris = _translateList(
urisNode, ErrorContext(key: _urisKey, parentNode: node), _translateUri);
var name = _translateString(
node.valueAt(_nameKey), ErrorContext(key: _nameKey, parentNode: node));
if (uris == null || name == null) {
// The error has already been reported.
return null;
}
if (uris.isEmpty) {
if ((urisNode as YamlList).isEmpty) {
_reportError(TransformSetErrorCode.missingUri, urisNode);
}
return null;
}
return ImportedName(uris, name);
}
/// 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 [context] to report the error.
int? _translateInteger(YamlNode? node, ErrorContext context) {
if (node is YamlScalar) {
var value = node.value;
if (value is int) {
return value;
}
return _reportInvalidValue(node, context, 'int');
} else if (node == null) {
return _reportMissingKey(context);
} else {
return _reportInvalidValue(node, context, 'int');
}
}
/// Translate the given [node] as a key.
String? _translateKey(YamlNode node) {
String type;
if (node is YamlScalar) {
if (node.value is String) {
return node.value as String;
}
type = node.value.runtimeType.toString();
} else if (node is YamlList) {
type = 'List';
} else if (node is YamlMap) {
type = 'Map';
} else {
type = node.runtimeType.toString();
}
_reportError(TransformSetErrorCode.invalidKey, node, [type]);
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 [context] will be used to report the
/// error, and the valid elements will be returned.
List<R>? _translateList<R>(YamlNode? node, ErrorContext context,
R? Function(YamlNode, ErrorContext) elementTranslator) {
if (node is YamlList) {
var translatedList = <R>[];
for (var element in node.nodes) {
var result = elementTranslator(element, context);
if (result != null) {
translatedList.add(result);
}
}
return translatedList;
} else if (node == null) {
return _reportMissingKey(context);
} else {
return _reportInvalidValue(node, context, 'List');
}
}
/// Translate the [node] into a remove-parameter modification.
void _translateRemoveParameterChange(YamlMap node) {
_reportUnsupportedKeys(node, const {_indexKey, _kindKey, _nameKey});
var parameterSpecKey = _singleKey(node, const [_nameKey, _indexKey], node);
if (parameterSpecKey == null) {
// The error has already been reported.
return null;
}
ParameterReference reference;
if (parameterSpecKey == _indexKey) {
var index = _translateInteger(node.valueAt(_indexKey),
ErrorContext(key: _indexKey, parentNode: node));
if (index == null) {
// The error has already been reported.
return null;
}
reference = PositionalParameterReference(index);
} else {
var name = _translateString(node.valueAt(_nameKey),
ErrorContext(key: _nameKey, parentNode: node));
if (name == null) {
// The error has already been reported.
return;
}
reference = NamedParameterReference(name);
}
var parameterModifications = _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),
ErrorContext(key: _newNameKey, parentNode: node));
if (newName == null) {
// The error has already been reported.
return null;
}
return Rename(newName: newName);
}
/// Translate the [node] into a rename parameter change. Return the resulting
/// change, or `null` if the [node] does not represent a valid rename change.
RenameParameter? _translateRenameParameterChange(YamlMap node) {
_reportUnsupportedKeys(node, const {_kindKey, _newNameKey, _oldNameKey});
var oldName = _translateString(node.valueAt(_oldNameKey),
ErrorContext(key: _oldNameKey, parentNode: node));
var newName = _translateString(node.valueAt(_newNameKey),
ErrorContext(key: _newNameKey, parentNode: node));
if (oldName == null || newName == null) {
// The error has already been reported.
return null;
}
return RenameParameter(newName: newName, oldName: oldName);
}
/// Translate the [node] into a replaced_by change. Return the resulting
/// change, or `null` if the [node] does not represent a valid replaced_by
/// change.
ReplacedBy? _translateReplacedByChange(YamlMap node) {
_reportUnsupportedKeys(node, const {_kindKey, _newElementKey});
var newElement = _translateElement(node.valueAt(_newElementKey),
ErrorContext(key: _newElementKey, parentNode: node));
if (newElement == null) {
// The error has already been reported.
return null;
}
var oldElement = elementBeingTransformed;
if (oldElement != null) {
if (!_equalUris(oldElement.libraryUris, newElement.libraryUris)) {
_reportError(TransformSetErrorCode.unsupportedUriChange,
(node.valueAt(_newElementKey) as YamlMap).valueAt(_urisKey)!);
}
var compatibleTypes = compatibleReplacementTypes[oldElement.kind];
if (compatibleTypes == null) {
_reportError(
TransformSetErrorCode.invalidChangeForKind,
node.valueAt(_newElementKey)!,
[_replacedByKind, oldElement.kind.displayName]);
return null;
} else if (!compatibleTypes.contains(newElement.kind)) {
_reportError(
TransformSetErrorCode.incompatibleElementKind,
node.valueAt(_newElementKey)!,
[oldElement.kind.displayName, newElement.kind.displayName]);
return null;
}
}
return ReplacedBy(newElement: newElement);
}
/// Translate the [node] into a string. Return the resulting string, or `null`
/// if the [node] doesn't represent a valid string. If the [node] isn't valid,
/// use the [context] to report the error. If the [node] doesn't exist and
/// [required] is `true`, then report an error.
String? _translateString(YamlNode? node, ErrorContext context,
{bool required = true}) {
if (node is YamlScalar) {
var value = node.value;
if (value is String) {
return value;
}
return _reportInvalidValue(node, context, 'String');
} else if (node == null) {
if (required) {
return _reportMissingKey(context);
}
return null;
} else {
return _reportInvalidValue(node, context, 'String');
}
}
/// Translate the [node] into a variable scope. Return the resulting scope, or
/// the enclosing scope if the [node] does not represent a valid variables
/// map. If the [node] is not valid, use the [context] to report the error.
VariableScope _translateTemplateVariables(
YamlNode? node, ErrorContext context) {
if (node is YamlMap) {
var generators = <String, ValueGenerator>{};
for (var entry in node.nodes.entries) {
var name = _translateKey(entry.key);
if (name != null) {
var value = _translateValueGenerator(
entry.value, ErrorContext(key: name, parentNode: node));
if (value != null) {
generators[name] = value;
}
}
}
return VariableScope(transformVariableScope, generators);
} else if (node == null) {
return transformVariableScope;
} else {
_reportInvalidValue(node, context, 'Map');
return transformVariableScope;
}
}
/// 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 [context] to report the error.
Transform? _translateTransform(YamlNode node, ErrorContext context) {
if (node is YamlMap) {
_reportUnsupportedKeys(node, const {
_bulkApplyKey,
_changesKey,
_dateKey,
_elementKey,
_oneOfKey,
_titleKey,
_variablesKey
});
var title = _translateString(node.valueAt(_titleKey),
ErrorContext(key: _titleKey, parentNode: node));
var date = _translateDate(node.valueAt(_dateKey),
ErrorContext(key: _dateKey, parentNode: node));
var bulkApply = _translateBool(node.valueAt(_bulkApplyKey),
ErrorContext(key: _bulkApplyKey, parentNode: node),
required: false) ??
true;
var element = _translateElement(node.valueAt(_elementKey),
ErrorContext(key: _elementKey, parentNode: node));
elementBeingTransformed = element;
transformVariableScope = _translateTemplateVariables(
node.valueAt(_variablesKey),
ErrorContext(key: _variablesKey, parentNode: node));
ChangesSelector? selector;
var key = _singleKey(
node, const [_changesKey, _oneOfKey], context.parentNode,
required: true);
if (key == _oneOfKey) {
selector = _translateConditionalChanges(node.valueAt(_oneOfKey)!,
ErrorContext(key: _oneOfKey, parentNode: node));
} else if (key == _changesKey) {
var changes = _translateList(node.valueAt(_changesKey),
ErrorContext(key: _changesKey, parentNode: node), _translateChange);
if (changes == null) {
// The error has already been reported.
_parameterModifications = null;
return null;
}
var parameterModifications = _parameterModifications;
if (parameterModifications != null) {
changes.add(ModifyParameters(modifications: parameterModifications));
_parameterModifications = null;
}
selector = UnconditionalChangesSelector(changes);
} else {
// The error has already been reported.
return null;
}
transformVariableScope = VariableScope.empty;
if (title == null ||
date == null ||
element == null ||
selector == null) {
// The error has already been reported.
return null;
}
return Transform(
title: title,
date: date,
bulkApply: bulkApply,
element: element,
changesSelector: selector);
} else {
return _reportInvalidValue(node, context, 'Map');
}
}
/// 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 versionNode = node.valueAt(_versionKey);
var version = _translateInteger(
versionNode, ErrorContext(key: _versionKey, parentNode: node));
if (versionNode == null || version == null) {
// The error has already been reported.
return null;
} else if (version < 1 || version > currentVersion) {
_reportError(TransformSetErrorCode.unsupportedVersion, versionNode);
return null;
}
// TODO(brianwilkerson) Version information is currently being ignored,
// but needs to be used to select a translator.
var transforms = _translateList(
node.valueAt(_transformsKey),
ErrorContext(key: _transformsKey, parentNode: node),
_translateTransform);
if (transforms == null) {
// The error has already been reported.
return null;
}
transforms.sort((first, second) => first.date.compareTo(second.date));
var set = TransformSet();
for (var transform in transforms) {
set.addTransform(transform);
}
return set;
} else {
// TODO(brianwilkerson) Consider having a different error code for the
// top-level node (instead of using 'file' as the "key").
_reportError(TransformSetErrorCode.invalidValue, node,
['file', 'Map', _nodeType(node)]);
return null;
}
}
/// Translate the [node] into a URI. Return the resulting URI, or `null` if
/// the [node] doesn't represent a valid URI. If the [node] isn't valid, use
/// the [context] to report the error. If the [node] doesn't exist and
/// [required] is `true`, then report an error.
Uri? _translateUri(YamlNode? node, ErrorContext context,
{bool required = true}) {
if (node is YamlScalar) {
var value = node.value;
if (value is String) {
if (!(value.startsWith('dart:') || value.startsWith('package:'))) {
value = 'package:$packageName/$value';
}
return Uri.parse(value);
}
return _reportInvalidValue(node, context, 'URI');
} else if (node == null) {
if (required) {
return _reportMissingKey(context);
}
return null;
} else {
return _reportInvalidValue(node, context, 'URI');
}
}
/// 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 [context] to report the
/// error.
ValueGenerator? _translateValueGenerator(
YamlNode? node, ErrorContext context) {
if (node is YamlMap) {
var kindNode = node.valueAt(_kindKey);
var kindContext = ErrorContext(key: _kindKey, parentNode: node);
var kind = _translateString(kindNode, kindContext);
if (kindNode == null || kind == null) {
return null;
} else if (kind == _fragmentKind) {
return _translateCodeFragment(node);
} else if (kind == _importKind) {
return _translateImportValue(node);
}
return _reportInvalidValueOneOf(kindNode, kindContext, [
_fragmentKind,
_importKind,
]);
} else if (node == null) {
return _reportMissingKey(context);
} else {
return _reportInvalidValue(node, context, 'Map');
}
}
static Map<ElementKind, Set<ElementKind>>
_createCompatibleReplacementTypes() {
var types = <ElementKind, Set<ElementKind>>{};
void addSet(Set<ElementKind> set) {
for (var kind in set) {
types.putIfAbsent(kind, () => {}).addAll(set);
}
}
// Constructors can replace constructors.
addSet({
ElementKind.constructorKind,
});
// Static methods and top-level functions can replace each other.
addSet({
ElementKind.functionKind,
ElementKind.methodKind,
});
// Static getters and getter-inducing elements can replace each other.
addSet({
ElementKind.constantKind,
ElementKind.fieldKind,
ElementKind.getterKind,
ElementKind.variableKind,
});
// Static setters and setter-inducing elements can replace each other.
// TODO(brianwilkerson) We can't currently distinguish between final and
// non-final elements, but we don't support replacing setters with final
// elements, nor vice versa. We need a way to distinguish these cases if we
// want to be able to report an error.
addSet({
ElementKind.fieldKind,
ElementKind.setterKind,
ElementKind.variableKind,
});
return types;
}
}