blob: 29e814f8f5389f905f93c9687d0144d046628b0d [file] [log] [blame]
// Copyright (c) 2021, 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/transform_override.dart';
import 'package:analysis_server/src/services/correction/fix/data_driven/transform_override_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/transform_set_parser.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/util/yaml.dart';
import 'package:yaml/yaml.dart';
/// A parser used to parse the content of a configuration file.
class TransformOverrideSetParser {
// TODO(brianwilkerson) Create a class or mixin that would allow this class
// and `TransformSetParser` to share code.
static const String _bulkApplyKey = 'bulkApply';
/// The error reporter to which diagnostics will be reported.
final ErrorReporter errorReporter;
/// Initialize a newly created parser to report errors to the [errorReporter].
TransformOverrideSetParser(this.errorReporter);
/// Return the result of parsing the file [content] into a transform override,
/// or `null` if the content does not represent a valid transform override.
TransformOverrideSet parse(String content) {
assert(content != null);
var map = _parseYaml(content);
if (map == null) {
// The error has already been reported.
return null;
}
return _translateTransformOverrideSet(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 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 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]);
}
}
}
/// 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 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 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 transform override. Return the resulting
/// transform override, or `null` if the [node] does not represent a valid
/// transform override.
TransformOverride _translateTransformOverride(
YamlNode node, ErrorContext context, String title) {
assert(node != null);
if (node is YamlMap) {
_reportUnsupportedKeys(node, const {_bulkApplyKey});
var bulkApplyNode = node.valueAt(_bulkApplyKey);
if (bulkApplyNode != null) {
var bulkApplyValue = _translateBool(
bulkApplyNode, ErrorContext(key: _bulkApplyKey, parentNode: node),
required: true);
if (bulkApplyValue != null) {
return TransformOverride(title: title, bulkApply: bulkApplyValue);
}
}
return null;
} else {
return _reportInvalidValue(node, context, 'Map');
}
}
/// Translate the [node] into a transform override. Return the resulting
/// transform override, or `null` if the [node] does not represent a valid
/// transform override.
TransformOverrideSet _translateTransformOverrideSet(YamlNode node) {
assert(node != null);
if (node is YamlMap) {
var overrides = <TransformOverride>[];
for (var entry in node.nodes.entries) {
var keyNode = entry.key as YamlNode;
var errorContext = ErrorContext(key: 'file', parentNode: node);
var key = _translateString(keyNode, errorContext);
if (key != null) {
var valueNode = entry.value;
var override =
_translateTransformOverride(valueNode, errorContext, key);
if (override != null) {
overrides.add(override);
}
}
}
return TransformOverrideSet(overrides);
} 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;
}
}
}