Files
sdk/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart
T
Danny Tuppeny 916272331b [analysis_server] Remove the singleton uriConverter and pass explicitly to all toJson/fromJson methods
This avoids accidentally using the converter when talking to plugins, (at least for now) URIs are never expected, and file paths should always be used regardless of which mode the server is in.

This unfortunately touches a _lot_ of code, so I've pushed in many separate patch sets to Gerrit.

Fixes https://github.com/Dart-Code/Dart-Code/issues/5156

Change-Id: I312c3e2cbc35a05a078aaa0138aec7288b3c7dd6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/373745
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
2024-07-02 14:15:11 +00:00

1400 lines
48 KiB
Dart

// Copyright (c) 2014, 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 'dart:convert';
import 'package:analyzer_utilities/html_dom.dart' as dom;
import 'package:analyzer_utilities/tools.dart';
import 'package:path/path.dart' as path;
import 'api.dart';
import 'codegen_dart.dart';
import 'from_html.dart';
import 'implied_types.dart';
import 'to_html.dart';
/// Special flags that need to be inserted into the declaration of the Element
/// class.
const Map<String, String> specialElementFlags = {
'abstract': '0x01',
'const': '0x02',
'final': '0x04',
'static': '0x08',
'private': '0x10',
'deprecated': '0x20'
};
GeneratedFile target(bool responseRequiresRequestTime,
CodegenUriConverterKind clientUriConverterKind) {
return GeneratedFile('lib/protocol/protocol_generated.dart',
(String pkgPath) async {
var visitor = CodegenProtocolVisitor(path.basename(pkgPath),
responseRequiresRequestTime, clientUriConverterKind, readApi(pkgPath));
return visitor.collectCode(visitor.visitApi);
});
}
/// Callback type used to represent arbitrary code generation.
typedef CodegenCallback = void Function();
typedef FromJsonSnippetCallback = String Function(String jsonPath, String json);
typedef ToJsonSnippetCallback = String Function(String value);
/// Visitor which produces Dart code representing the API.
class CodegenProtocolVisitor extends DartCodegenVisitor with CodeGenerator {
/// Class members for which the constructor argument should be optional, even
/// if the member is not an optional part of the protocol. For list types,
/// the constructor will default the member to the empty list.
static const Map<String, List<String>> _optionalConstructorArguments = {
'AnalysisErrorFixes': ['fixes'],
'SourceChange': ['edits', 'linkedEditGroups'],
'SourceFileEdit': ['edits'],
'TypeHierarchyItem': ['interfaces', 'mixins', 'subclasses'],
};
/// The disclaimer added to the documentation comment for each of the classes
/// that are generated.
static const String disclaimer =
'Clients may not extend, implement or mix-in this class.';
/// The name of the package into which code is being generated.
final String packageName;
/// A flag indicating whether the class [Response] requires a `requestTime`
/// parameter.
final bool responseRequiresRequestTime;
/// The kind or URI Conversion to be used in generated code.
final CodegenUriConverterKind clientUriConverterKind;
/// Visitor used to produce doc comments.
final ToHtmlVisitor toHtmlVisitor;
/// Types implied by the API. This includes types explicitly named in the
/// API as well as those implied by the definitions of requests, responses,
/// notifications, etc.
final Map<String, ImpliedType> impliedTypes;
CodegenProtocolVisitor(this.packageName, this.responseRequiresRequestTime,
this.clientUriConverterKind, Api api)
: toHtmlVisitor = ToHtmlVisitor(api),
impliedTypes = computeImpliedTypes(api),
super(api) {
codeGeneratorSettings.commentLineLength = 79;
codeGeneratorSettings.docCommentStartMarker = null;
codeGeneratorSettings.docCommentLineLeader = '/// ';
codeGeneratorSettings.docCommentEndMarker = null;
codeGeneratorSettings.languageName = 'dart';
}
/// Compute the code necessary to compare two objects for equality.
String compareEqualsCode(TypeDecl type, String thisVar, String otherVar) {
var resolvedType = resolveTypeReferenceChain(type);
if (resolvedType is TypeReference ||
resolvedType is TypeEnum ||
resolvedType is TypeObject ||
resolvedType is TypeUnion) {
return '$thisVar == $otherVar';
} else if (resolvedType is TypeList) {
var itemTypeName = dartType(resolvedType.itemType);
var subComparison = compareEqualsCode(resolvedType.itemType, 'a', 'b');
var closure = '($itemTypeName a, $itemTypeName b) => $subComparison';
return 'listEqual($thisVar, $otherVar, $closure)';
} else if (resolvedType is TypeMap) {
var valueTypeName = dartType(resolvedType.valueType);
var subComparison = compareEqualsCode(resolvedType.valueType, 'a', 'b');
var closure = '($valueTypeName a, $valueTypeName b) => $subComparison';
return 'mapEqual($thisVar, $otherVar, $closure)';
}
throw Exception("Don't know how to compare for equality: $resolvedType");
}
/// Translate each of the given [types] implied by the API to a class.
void emitClasses(List<ImpliedType> types) {
for (var impliedType in types) {
var type = impliedType.type;
var dartTypeName = capitalize(impliedType.camelName);
if (type == null) {
writeln();
emitEmptyObjectClass(dartTypeName, impliedType);
} else if (type is TypeObject) {
writeln();
emitObjectClass(dartTypeName, type, impliedType);
} else if (type is TypeEnum) {
writeln();
emitEnumClass(dartTypeName, type, impliedType);
}
}
}
/// Emit a convenience constructor for decoding a piece of protocol, if
/// appropriate. Return true if a constructor was emitted.
bool emitConvenienceConstructor(String className, ImpliedType impliedType) {
// The type of object from which this piece of protocol should be decoded.
String inputType;
// The name of the input object.
String inputName;
// The field within the input object to decode.
String fieldName;
// Constructor call to create the JsonDecoder object.
String makeDecoder;
// Name of the constructor to create.
String constructorName;
// Extra arguments for the constructor.
var extraArgs = <String>[];
switch (impliedType.kind) {
case 'requestParams':
inputType = 'Request';
inputName = 'request';
fieldName = 'params';
makeDecoder = 'RequestDecoder(request)';
constructorName = 'fromRequest';
break;
case 'requestResult':
inputType = 'Response';
inputName = 'response';
fieldName = 'result';
makeDecoder =
'ResponseDecoder(REQUEST_ID_REFACTORING_KINDS.remove(response.id))';
constructorName = 'fromResponse';
break;
case 'notificationParams':
inputType = 'Notification';
inputName = 'notification';
fieldName = 'params';
makeDecoder = 'ResponseDecoder(null)';
constructorName = 'fromNotification';
break;
case 'refactoringOptions':
inputType = 'EditGetRefactoringParams';
inputName = 'refactoringParams';
fieldName = 'options';
makeDecoder = 'RequestDecoder(request)';
constructorName = 'fromRefactoringParams';
extraArgs.add('Request request');
break;
default:
return false;
}
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? ', { ${clientUriConverterKind.namedParameterString} }'
: '';
var namedArguments = clientUriConverterKind != CodegenUriConverterKind.none
? ', ${clientUriConverterKind.namedArgumentString}'
: '';
var args = <String>['$inputType $inputName'];
args.addAll(extraArgs);
writeln(
'factory $className.$constructorName(${args.join(', ')}$namedParameters) {');
indent(() {
var fieldNameString =
literalString(fieldName.replaceFirst(RegExp('^_'), ''));
if (className == 'EditGetRefactoringParams') {
writeln('var params = $className.fromJson(');
writeln(
' $makeDecoder, $fieldNameString, $inputName.$fieldName$namedArguments);');
writeln('REQUEST_ID_REFACTORING_KINDS[request.id] = params.kind;');
writeln('return params;');
} else {
writeln('return $className.fromJson(');
writeln(
' $makeDecoder, $fieldNameString, $inputName.$fieldName$namedArguments);');
}
});
writeln('}');
return true;
}
/// Emit a class representing an data structure that doesn't exist in the
/// protocol because it is empty (e.g. the "params" object for a request that
/// doesn't have any parameters).
void emitEmptyObjectClass(String className, ImpliedType impliedType) {
docComment(toHtmlVisitor.collectHtml(() {
toHtmlVisitor.p(() {
toHtmlVisitor.write(impliedType.humanReadableName);
});
toHtmlVisitor.p(() {
toHtmlVisitor.write(disclaimer);
});
}));
write('class $className');
if (impliedType.kind == 'refactoringFeedback') {
writeln(' extends RefactoringFeedback implements HasToJson {');
} else if (impliedType.kind == 'refactoringOptions') {
writeln(' extends RefactoringOptions implements HasToJson {');
} else if (impliedType.kind == 'requestParams') {
writeln(' implements RequestParams {');
} else if (impliedType.kind == 'requestResult') {
writeln(' implements ResponseResult {');
} else {
writeln(' {');
}
indent(() {
if (impliedType.kind == 'requestResult' ||
impliedType.kind == 'requestParams') {
emitEmptyToJsonMember();
writeln();
}
if (emitToRequestMember(impliedType)) {
writeln();
}
if (emitToResponseMember(impliedType)) {
writeln();
}
if (emitToNotificationMember(impliedType)) {
writeln();
}
emitObjectEqualsMember(null, className);
writeln();
emitObjectHashCode(null, className);
});
writeln('}');
}
/// Emit the toJson() code for an empty class.
void emitEmptyToJsonMember() {
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? '{ ${clientUriConverterKind.namedParameterString} }'
: '';
writeln('@override');
writeln('Map<String, Object> toJson($namedParameters) => {};');
}
/// Emit a class to encapsulate an enum.
void emitEnumClass(String className, TypeEnum type, ImpliedType impliedType) {
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? '{ ${clientUriConverterKind.namedParameterString} }'
: '';
docComment(toHtmlVisitor.collectHtml(() {
toHtmlVisitor.p(() {
toHtmlVisitor.write(impliedType.humanReadableName);
});
if (impliedType.type != null) {
toHtmlVisitor.showType(null, impliedType.type!);
}
toHtmlVisitor.p(() {
toHtmlVisitor.write(disclaimer);
});
}));
writeln('class $className implements Enum {');
indent(() {
if (emitSpecialStaticMembers(className)) {
writeln();
}
for (var value in type.values) {
docComment(toHtmlVisitor.collectHtml(() {
toHtmlVisitor.translateHtml(value.html);
}));
var valueString = literalString(value.value);
writeln(
'static const $className ${value.value} = $className._($valueString);');
writeln();
}
writeln('/// A list containing all of the enum values that are defined.');
write('static const List<');
write(className);
write('> VALUES = <');
write(className);
write('>[');
var first = true;
for (var value in type.values) {
if (first) {
first = false;
} else {
write(', ');
}
write(value.value);
}
writeln('];');
writeln();
writeln('@override');
writeln('final String name;');
writeln();
writeln('const $className._(this.name);');
writeln();
emitEnumClassConstructor(className, type);
writeln();
emitEnumFromJsonConstructor(className, type, impliedType);
writeln();
if (emitSpecialConstructors(className)) {
writeln();
}
if (emitSpecialGetters(className)) {
writeln();
}
if (emitSpecialMethods(className)) {
writeln();
}
writeln('@override');
writeln("String toString() => '$className.\$name';");
writeln();
writeln('String toJson($namedParameters) => name;');
});
writeln('}');
}
/// Emit the constructor for an enum class.
void emitEnumClassConstructor(String className, TypeEnum type) {
writeln('factory $className(String name) {');
indent(() {
writeln('switch (name) {');
indent(() {
for (var value in type.values) {
var valueString = literalString(value.value);
writeln('case $valueString:');
indent(() {
writeln('return ${value.value};');
});
}
});
writeln('}');
writeln(r"throw Exception('Illegal enum value: $name');");
});
writeln('}');
}
/// Emit the method for decoding an enum from JSON.
void emitEnumFromJsonConstructor(
String className, TypeEnum type, ImpliedType impliedType) {
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? ', { ${clientUriConverterKind.namedParameterString} }'
: '';
writeln(
'factory $className.fromJson(JsonDecoder jsonDecoder, String jsonPath, Object? json$namedParameters) {');
indent(() {
writeln('if (json is String) {');
indent(() {
writeln('try {');
indent(() {
writeln('return $className(json);');
});
writeln('} catch(_) {');
indent(() {
writeln('// Fall through');
});
writeln('}');
});
writeln('}');
var humanReadableNameString =
literalString(impliedType.humanReadableName);
writeln(
'throw jsonDecoder.mismatch(jsonPath, $humanReadableNameString, json);');
});
writeln('}');
}
void emitImports() {
writeln("import 'dart:convert' hide JsonDecoder;");
writeln();
writeln("import 'package:$packageName/protocol/protocol.dart';");
writeln(
"import 'package:$packageName/src/protocol/protocol_internal.dart';");
writeln(
"import 'package:$packageName/src/utilities/client_uri_converter.dart';");
for (var uri in api.types.importUris) {
write("import '");
write(uri);
writeln("';");
}
}
/// Emit the class to encapsulate an object type.
void emitObjectClass(
String className, TypeObject type, ImpliedType impliedType) {
docComment(toHtmlVisitor.collectHtml(() {
toHtmlVisitor.p(() {
toHtmlVisitor.write(impliedType.humanReadableName);
});
if (impliedType.type != null) {
toHtmlVisitor.showType(null, impliedType.type!);
}
toHtmlVisitor.p(() {
toHtmlVisitor.write(disclaimer);
});
}));
write('class $className');
if (impliedType.kind == 'refactoringFeedback') {
writeln(' extends RefactoringFeedback {');
} else if (impliedType.kind == 'refactoringOptions') {
writeln(' extends RefactoringOptions {');
} else if (impliedType.kind == 'requestParams') {
writeln(' implements RequestParams {');
} else if (impliedType.kind == 'requestResult') {
writeln(' implements ResponseResult {');
} else {
writeln(' implements HasToJson {');
}
indent(() {
if (emitSpecialStaticMembers(className)) {
writeln();
}
for (var field in type.fields) {
if (field.value != null) {
continue;
}
docComment(toHtmlVisitor.collectHtml(() {
toHtmlVisitor.translateHtml(field.html);
}));
writeln('${fieldDartType(field)} ${field.name};');
writeln();
}
emitObjectConstructor(type, className);
writeln();
emitObjectFromJsonConstructor(className, type, impliedType);
writeln();
if (emitConvenienceConstructor(className, impliedType)) {
writeln();
}
if (emitSpecialConstructors(className)) {
writeln();
}
if (emitSpecialGetters(className)) {
writeln();
}
emitToJsonMember(type);
writeln();
if (emitToRequestMember(impliedType)) {
writeln();
}
if (emitToResponseMember(impliedType)) {
writeln();
}
if (emitToNotificationMember(impliedType)) {
writeln();
}
if (emitSpecialMethods(className)) {
writeln();
}
var namedArguments =
clientUriConverterKind != CodegenUriConverterKind.none
? 'clientUriConverter: null'
: '';
writeln('@override');
writeln('String toString() => json.encode(toJson($namedArguments));');
writeln();
emitObjectEqualsMember(type, className);
writeln();
emitObjectHashCode(type, className);
});
writeln('}');
}
/// Emit the constructor for an object class.
void emitObjectConstructor(TypeObject type, String className) {
var args = <String>[];
var optionalArgs = <String>[];
var initializers = <String>[];
for (var field in type.fields) {
if (field.value != null) {
continue;
}
if (isOptionalConstructorArg(className, field)) {
if (!field.optional) {
optionalArgs.add('${dartType(field.type)}? ${field.name}');
// Optional constructor arg, but non-optional field. If no arg is
// given, the constructor should populate with the empty list.
var fieldType = field.type;
if (fieldType is TypeList) {
var defaultValue = '<${dartType(fieldType.itemType)}>[]';
initializers.add('${field.name} = ${field.name} ?? $defaultValue');
} else {
throw Exception("Don't know how to create default field value.");
}
} else {
optionalArgs.add('this.${field.name}');
}
} else {
args.add('this.${field.name}');
}
}
if (optionalArgs.isNotEmpty) {
args.add('{${optionalArgs.join(', ')}}');
}
write('$className(${args.join(', ')})');
if (initializers.isEmpty) {
writeln(';');
} else {
writeln(' : ${initializers.join(', ')}');
writeln(';');
}
}
/// Emit the operator== code for an object class.
void emitObjectEqualsMember(TypeObject? type, String className) {
writeln('@override');
write('bool operator ==(other) ');
if (type == null) {
writeln('=> other is $className;');
return;
}
writeln('{');
indent(() {
writeln('if (other is $className) {');
indent(() {
var comparisons = <String>[];
for (var field in type.fields) {
if (field.value != null) {
continue;
}
comparisons.add(
compareEqualsCode(field.type, field.name, 'other.${field.name}'));
}
if (comparisons.isEmpty) {
writeln('return true;');
} else {
var concatenated = comparisons.join(' &&\n ');
writeln('return $concatenated;');
}
});
writeln('}');
writeln('return false;');
});
writeln('}');
}
/// Emit the method for decoding an object from JSON.
void emitObjectFromJsonConstructor(
String className, TypeObject type, ImpliedType impliedType) {
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? ', { ${clientUriConverterKind.namedParameterString} }'
: '';
var namedArguments = clientUriConverterKind != CodegenUriConverterKind.none
? ', ${clientUriConverterKind.namedArgumentString}'
: '';
var humanReadableNameString = literalString(impliedType.humanReadableName);
if (className == 'RefactoringFeedback') {
writeln('factory RefactoringFeedback.fromJson(JsonDecoder jsonDecoder, '
'String jsonPath, '
'Object? json, '
'Map<Object?, Object?> responseJson$namedParameters) {');
indent(() {
writeln('return refactoringFeedbackFromJson(jsonDecoder, jsonPath, '
'json, responseJson$namedArguments);');
});
writeln('}');
return;
}
if (className == 'RefactoringOptions') {
writeln('factory RefactoringOptions.fromJson(JsonDecoder jsonDecoder, '
'String jsonPath, Object? json, RefactoringKind kind$namedParameters) {');
indent(() {
writeln('return refactoringOptionsFromJson(jsonDecoder, jsonPath, '
'json, kind$namedArguments);');
});
writeln('}');
return;
}
writeln(
'factory $className.fromJson(JsonDecoder jsonDecoder, String jsonPath, Object? json$namedParameters) {');
indent(() {
writeln('json ??= {};');
writeln('if (json is Map) {');
indent(() {
var args = <String>[];
var optionalArgs = <String>[];
for (var field in type.fields) {
var fieldNameString = literalString(field.name);
var fieldAccessor = 'json[$fieldNameString]';
var jsonPath = literalString('\$jsonPath.${field.name}');
if (field.value != null) {
var valueString = literalString(field.value as String);
writeln('if ($fieldAccessor != $valueString) {');
indent(() {
writeln(
"throw jsonDecoder.mismatch(jsonPath, 'equal ${field.value}', json);");
});
writeln('}');
continue;
}
if (isOptionalConstructorArg(className, field)) {
optionalArgs.add('${field.name}: ${field.name}');
} else {
args.add(field.name);
}
var typeStr = fieldDartType(field);
writeln('$typeStr ${field.name};');
writeln('if (json.containsKey($fieldNameString)) {');
indent(() {
var fieldType = field.type;
var fromJson =
fromJsonCode(fieldType).asSnippet(jsonPath, fieldAccessor);
writeln('${field.name} = $fromJson;');
});
write('}');
if (!field.optional) {
writeln(' else {');
indent(() {
writeln(
'throw jsonDecoder.mismatch(jsonPath, $fieldNameString);');
});
writeln('}');
} else {
writeln();
}
}
args.addAll(optionalArgs);
writeln('return $className(${args.join(', ')});');
});
writeln('} else {');
indent(() {
writeln(
'throw jsonDecoder.mismatch(jsonPath, $humanReadableNameString, json);');
});
writeln('}');
});
writeln('}');
}
/// Emit the hashCode getter for an object class.
void emitObjectHashCode(TypeObject? type, String className) {
writeln('@override');
writeln('int get hashCode => ');
String hashAll(String value) => 'Object.hashAll($value)';
String fieldValue(TypeObjectField field, {required bool single}) {
if (field.value != null) {
return field.value.hashCode.toString();
} else {
var name = field.name;
var type = field.type;
if (type is TypeList) {
var nullableString = field.optional ? ' ?? []' : '';
return hashAll(name + nullableString);
} else if (type is TypeMap) {
var nullable = field.optional ? '?' : '';
return hashAll(
'[...$nullable$name$nullable.keys,'
' ...$nullable$name$nullable.values]',
);
}
return single ? '$name.hashCode' : name;
}
}
indent(() {
if (type == null) {
writeln(' ${className.hashCode}');
} else {
var fields = type.fields;
if (fields.isEmpty) {
writeln('0');
} else if (fields.length == 1) {
var field = fields.single;
write(fieldValue(field, single: true));
} else if (fields.length <= 20) {
writeln('Object.hash(');
for (var field in fields) {
write(fieldValue(field, single: false));
writeln(',');
}
writeln(')');
} else {
writeln('Object.hashAll([');
for (var field in fields) {
write(fieldValue(field, single: false));
writeln(',');
}
writeln('])');
}
}
writeln(';');
});
}
/// If the class named [className] requires special constructors, emit them
/// and return true.
bool emitSpecialConstructors(String className) {
switch (className) {
case 'LinkedEditGroup':
docComment([dom.Text('Construct an empty LinkedEditGroup.')]);
writeln(
'LinkedEditGroup.empty() : this(<Position>[], 0, <LinkedEditSuggestion>[]);');
return true;
case 'RefactoringProblemSeverity':
docComment([
dom.Text(
'Returns the [RefactoringProblemSeverity] with the maximal severity.')
]);
writeln(
'static RefactoringProblemSeverity? max(RefactoringProblemSeverity? a, RefactoringProblemSeverity? b) =>');
writeln(' maxRefactoringProblemSeverity(a, b);');
return true;
default:
return false;
}
}
/// If the class named [className] requires special getters, emit them and
/// return true.
bool emitSpecialGetters(String className) {
switch (className) {
case 'Element':
for (var name in specialElementFlags.keys) {
var flag = 'FLAG_${name.toUpperCase()}';
writeln(
'bool get ${camelJoin(['is', name])} => (flags & $flag) != 0;');
}
return true;
case 'SourceEdit':
docComment([dom.Text('The end of the region to be modified.')]);
writeln('int get end => offset + length;');
return true;
default:
return false;
}
}
/// If the class named [className] requires special methods, emit them and
/// return true.
bool emitSpecialMethods(String className) {
switch (className) {
case 'LinkedEditGroup':
docComment([dom.Text('Add a new position and change the length.')]);
writeln('void addPosition(Position position, int length) {');
indent(() {
writeln('positions.add(position);');
writeln('this.length = length;');
});
writeln('}');
writeln();
docComment([dom.Text('Add a new suggestion.')]);
writeln('void addSuggestion(LinkedEditSuggestion suggestion) {');
indent(() {
writeln('suggestions.add(suggestion);');
});
writeln('}');
return true;
case 'SourceChange':
docComment([
dom.Element.tag('p')
..append(dom.Text(
'Adds [edit] to the [FileEdit] for the given [file].')),
dom.Element.tag('p')
..append(dom.Text(
'If [insertBeforeExisting] is `true`, inserts made at the '
'same offset as other edits will be inserted such that they '
'appear before them in the resulting document.')),
]);
writeln('void addEdit(String file, int fileStamp, SourceEdit edit, '
'{bool insertBeforeExisting = false}) =>');
writeln(' addEditToSourceChange(this, file, fileStamp, edit, '
'insertBeforeExisting: insertBeforeExisting);');
writeln();
docComment([dom.Text('Adds the given [FileEdit].')]);
writeln('void addFileEdit(SourceFileEdit edit) {');
indent(() {
writeln('edits.add(edit);');
});
writeln('}');
writeln();
docComment([dom.Text('Adds the given [LinkedEditGroup].')]);
writeln('void addLinkedEditGroup(LinkedEditGroup linkedEditGroup) {');
indent(() {
writeln('linkedEditGroups.add(linkedEditGroup);');
});
writeln('}');
writeln();
docComment([
dom.Text('Returns the [FileEdit] for the given [file], maybe `null`.')
]);
writeln('SourceFileEdit? getFileEdit(String file) =>');
writeln(' getChangeFileEdit(this, file);');
return true;
case 'SourceEdit':
docComment([
dom.Text('Get the result of applying the edit to the given [code].')
]);
writeln('String apply(String code) => applyEdit(code, this);');
return true;
case 'SourceFileEdit':
docComment([
dom.Element.tag('p')
..append(dom.Text('Adds the given [Edit] to the list.')),
dom.Element.tag('p')
..append(dom.Text(
'If [insertBeforeExisting] is `true`, inserts made at the '
'same offset as other edits will be inserted such that they '
'appear before them in the resulting document.')),
]);
writeln('void add(SourceEdit edit, '
'{bool insertBeforeExisting = false}) =>');
writeln(' addEditForSource(this, edit, '
'insertBeforeExisting: insertBeforeExisting);');
writeln();
docComment([
dom.Element.tag('p')..append(dom.Text('Adds the given [Edit]s.')),
dom.Element.tag('p')
..append(dom.Text(
'If [insertBeforeExisting] is `true`, inserts made at the '
'same offset as other edits will be inserted such that they '
'appear before them in the resulting document.')),
]);
writeln('void addAll(Iterable<SourceEdit> edits, '
'{bool insertBeforeExisting = false}) =>');
writeln(' addAllEditsForSource(this, edits, '
'insertBeforeExisting: insertBeforeExisting);');
return true;
default:
return false;
}
}
/// If the class named [className] requires special static members, emit them
/// and return true.
bool emitSpecialStaticMembers(String className) {
switch (className) {
case 'Element':
var makeFlagsArgs = <String>[];
var makeFlagsStatements = <String>[];
specialElementFlags.forEach((String name, String value) {
var flag = 'FLAG_${name.toUpperCase()}';
var camelName = camelJoin(['is', name]);
writeln('static const int $flag = $value;');
makeFlagsArgs.add('bool $camelName = false');
makeFlagsStatements.add('if ($camelName) flags |= $flag;');
});
writeln();
writeln('static int makeFlags({${makeFlagsArgs.join(', ')}}) {');
indent(() {
writeln('var flags = 0;');
for (var statement in makeFlagsStatements) {
writeln(statement);
}
writeln('return flags;');
});
writeln('}');
return true;
case 'SourceEdit':
docComment([
dom.Text('Get the result of applying a set of [edits] to the given '
'[code]. Edits are applied in the order they appear in [edits].')
]);
writeln(
'static String applySequence(String code, Iterable<SourceEdit> edits) =>');
writeln(' applySequenceOfEdits(code, edits);');
return true;
default:
return false;
}
}
/// Emit the toJson() code for an object class.
void emitToJsonMember(TypeObject type) {
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? '{ ${clientUriConverterKind.namedParameterString} }'
: '';
writeln('@override');
writeln('Map<String, Object> toJson($namedParameters) {');
indent(() {
writeln('var result = <String, Object>{};');
for (var field in type.fields) {
var fieldNameString = literalString(field.name);
if (field.value != null) {
writeln(
'result[$fieldNameString] = ${literalString(field.value as String)};');
continue;
}
var fieldToJson = toJsonCode(field.type).asSnippet(field.name);
var populateField = 'result[$fieldNameString] = $fieldToJson;';
if (field.optional) {
var name = field.name;
writeln('var $name = this.$name;');
writeln('if ($name != null) {');
indent(() {
writeln(populateField);
});
writeln('}');
} else {
writeln(populateField);
}
}
writeln('return result;');
});
writeln('}');
}
/// Emit the toNotification() code for a class, if appropriate. Returns true
/// if code was emitted.
bool emitToNotificationMember(ImpliedType impliedType) {
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? '{ ${clientUriConverterKind.namedParameterString} }'
: '';
var namedArguments = clientUriConverterKind != CodegenUriConverterKind.none
? clientUriConverterKind.namedArgumentString
: '';
if (impliedType.kind == 'notificationParams') {
writeln('Notification toNotification($namedParameters) {');
indent(() {
var eventString =
literalString((impliedType.apiNode as Notification).longEvent);
var jsonPart =
impliedType.type != null ? 'toJson($namedArguments)' : 'null';
writeln('return Notification($eventString, $jsonPart);');
});
writeln('}');
return true;
}
return false;
}
/// Emit the toRequest() code for a class, if appropriate. Returns true if
/// code was emitted.
bool emitToRequestMember(ImpliedType impliedType) {
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? ', { ${clientUriConverterKind.namedParameterString} }'
: '';
var namedArguments = clientUriConverterKind != CodegenUriConverterKind.none
? clientUriConverterKind.namedArgumentString
: '';
if (impliedType.kind == 'requestParams') {
writeln('@override');
writeln('Request toRequest(String id$namedParameters) {');
indent(() {
var methodString =
literalString((impliedType.apiNode as Request).longMethod);
if (impliedType.type != null) {
writeln(
'return Request(id, $methodString, toJson($namedArguments));');
} else {
writeln('return Request(id, $methodString);');
}
});
writeln('}');
return true;
}
return false;
}
/// Emit the toResponse() code for a class, if appropriate. Returns true if
/// code was emitted.
bool emitToResponseMember(ImpliedType impliedType) {
var namedParameters = clientUriConverterKind != CodegenUriConverterKind.none
? ', { ${clientUriConverterKind.namedParameterString} }'
: '';
var namedArguments = clientUriConverterKind != CodegenUriConverterKind.none
? clientUriConverterKind.namedArgumentString
: '';
if (impliedType.kind == 'requestResult') {
writeln('@override');
if (responseRequiresRequestTime) {
writeln(
'Response toResponse(String id, int requestTime$namedParameters) {');
} else {
writeln('Response toResponse(String id$namedParameters) {');
}
indent(() {
if (impliedType.type != null) {
if (responseRequiresRequestTime) {
writeln(
'return Response(id, requestTime, result: toJson($namedArguments));');
} else {
writeln('return Response(id, result: toJson($namedArguments));');
}
} else {
if (responseRequiresRequestTime) {
writeln('return Response(id, requestTime);');
} else {
writeln('return Response(id);');
}
}
});
writeln('}');
return true;
}
return false;
}
/// Compute the code necessary to translate [type] from JSON.
FromJsonCode fromJsonCode(TypeDecl type) {
var namedArguments = clientUriConverterKind != CodegenUriConverterKind.none
? ', ${clientUriConverterKind.namedArgumentString}'
: '';
if (type is TypeReference) {
var referencedDefinition = api.types[type.typeName];
if (referencedDefinition != null) {
var referencedType = referencedDefinition.type;
if (referencedType is TypeObject || referencedType is TypeEnum) {
return FromJsonSnippet((String jsonPath, String json) {
var typeName = dartType(type);
if (typeName == 'RefactoringFeedback') {
return '$typeName.fromJson(jsonDecoder, $jsonPath, $json, json$namedArguments)';
} else if (typeName == 'RefactoringOptions') {
return '$typeName.fromJson(jsonDecoder, $jsonPath, $json, kind$namedArguments)';
} else {
return '$typeName.fromJson(jsonDecoder, $jsonPath, $json$namedArguments)';
}
});
} else if (clientUriConverterKind != CodegenUriConverterKind.none &&
referencedDefinition.name == 'FilePath') {
// TODO(dantup): Ensure if the client sends us filepaths instead of
// URIs that we generate good error responses.
return FromJsonSnippet((jsonPath, json) =>
'clientUriConverter?.fromClientFilePath(jsonDecoder.decodeString($jsonPath, $json))'
' ?? jsonDecoder.decodeString($jsonPath, $json)');
} else {
return fromJsonCode(referencedType);
}
} else {
switch (type.typeName) {
case 'String':
return FromJsonFunction('jsonDecoder.decodeString');
case 'bool':
return FromJsonFunction('jsonDecoder.decodeBool');
case 'int':
case 'long':
return FromJsonFunction('jsonDecoder.decodeInt');
case 'object':
return FromJsonIdentity();
default:
throw Exception('Unexpected type name ${type.typeName}');
}
}
} else if (type is TypeMap) {
FromJsonCode keyCode;
var referencedDefinition = api.types[type.keyType.typeName];
if (dartType(type.keyType) != 'String' ||
(clientUriConverterKind != CodegenUriConverterKind.none &&
referencedDefinition?.name == 'FilePath')) {
keyCode = fromJsonCode(type.keyType);
} else {
keyCode = FromJsonIdentity();
}
var valueCode = fromJsonCode(type.valueType);
if (keyCode.isIdentity && valueCode.isIdentity) {
return FromJsonFunction('jsonDecoder.decodeMap');
} else {
return FromJsonSnippet((String jsonPath, String json) {
var result = StringBuffer();
result.write('jsonDecoder.decodeMap($jsonPath, $json');
if (!keyCode.isIdentity) {
result.write(', keyDecoder: ${keyCode.asClosure}');
}
if (!valueCode.isIdentity) {
result.write(', valueDecoder: ${valueCode.asClosure}');
}
result.write(')');
return result.toString();
});
}
} else if (type is TypeList) {
var itemCode = fromJsonCode(type.itemType);
if (itemCode.isIdentity) {
return FromJsonFunction('jsonDecoder.decodeList');
} else {
return FromJsonSnippet((String jsonPath, String json) =>
'jsonDecoder.decodeList($jsonPath, $json, ${itemCode.asClosure})');
}
} else if (type is TypeUnion) {
var decoders = <String>[];
for (var choice in type.choices) {
var resolvedChoice = resolveTypeReferenceChain(choice);
if (resolvedChoice is TypeObject) {
var field = resolvedChoice.getField(type.field);
if (field == null) {
throw Exception(
'Each choice in the union needs a field named ${type.field}');
}
if (field.value == null) {
throw Exception(
'Each choice in the union needs a constant value for the field ${type.field}');
}
var closure = fromJsonCode(choice).asClosure;
decoders.add('${literalString(field.value as String)}: $closure');
} else {
throw Exception('Union types must be unions of objects.');
}
}
return FromJsonSnippet((String jsonPath, String json) =>
'jsonDecoder.decodeUnion($jsonPath, $json, ${literalString(type.field)}, {${decoders.join(', ')}})');
} else {
throw Exception("Can't convert $type from JSON");
}
}
/// Return a list of the classes to be emitted.
List<ImpliedType> getClassesToEmit() {
var types = impliedTypes.values.where((ImpliedType type) {
var node = type.apiNode;
return !(node is TypeDefinition && node.isExternal);
}).toList();
types.sort((first, second) =>
capitalize(first.camelName).compareTo(capitalize(second.camelName)));
return types;
}
/// True if the constructor argument for the given field should be optional.
bool isOptionalConstructorArg(String className, TypeObjectField field) {
if (field.optional) {
return true;
}
var forceOptional = _optionalConstructorArguments[className];
if (forceOptional != null && forceOptional.contains(field.name)) {
return true;
}
return false;
}
/// Create a string literal that evaluates to [s].
String literalString(String s) {
if (s.contains("'")) {
if (s.contains('"')) {
return json.encode(s);
}
return '"$s"';
}
return "'$s'";
}
/// Compute the code necessary to convert [type] to JSON.
ToJsonCode toJsonCode(TypeDecl type) {
var namedArguments = clientUriConverterKind != CodegenUriConverterKind.none
? clientUriConverterKind.namedArgumentString
: '';
var resolvedType = resolveTypeReferenceChain(type);
if (type is TypeReference &&
clientUriConverterKind != CodegenUriConverterKind.none &&
type.typeName == 'FilePath') {
return ToJsonSnippet(
dartType(type),
(String value) =>
'clientUriConverter?.toClientFilePath($value) ?? $value',
);
} else if (resolvedType is TypeReference) {
return ToJsonIdentity(dartType(type));
} else if (resolvedType is TypeList) {
var itemCode = toJsonCode(resolvedType.itemType);
if (itemCode.isIdentity) {
return ToJsonIdentity(dartType(type));
} else {
return ToJsonSnippet(dartType(type),
(String value) => '$value.map(${itemCode.asClosure}).toList()');
}
} else if (resolvedType is TypeMap) {
ToJsonCode keyCode;
var referencedDefinition = api.types[resolvedType.keyType.typeName];
if (dartType(resolvedType.keyType) != 'String' ||
(clientUriConverterKind != CodegenUriConverterKind.none &&
referencedDefinition?.name == 'FilePath')) {
keyCode = toJsonCode(resolvedType.keyType);
} else {
keyCode = ToJsonIdentity(dartType(resolvedType.keyType));
}
var valueCode = toJsonCode(resolvedType.valueType);
if (keyCode.isIdentity && valueCode.isIdentity) {
return ToJsonIdentity(dartType(resolvedType));
} else {
return ToJsonSnippet(dartType(type), (String value) {
var result = StringBuffer();
result.write('mapMap($value');
if (!keyCode.isIdentity) {
result.write(', keyCallback: ${keyCode.asClosure}');
}
if (!valueCode.isIdentity) {
result.write(', valueCallback: ${valueCode.asClosure}');
}
result.write(')');
return result.toString();
});
}
} else if (resolvedType is TypeUnion) {
for (var choice in resolvedType.choices) {
if (resolveTypeReferenceChain(choice) is! TypeObject) {
throw Exception('Union types must be unions of objects');
}
}
return ToJsonSnippet(
dartType(type),
(String value) => '($value as dynamic).toJson()',
);
} else if (resolvedType is TypeObject || resolvedType is TypeEnum) {
return ToJsonSnippet(
dartType(type), (String value) => '$value.toJson($namedArguments)');
} else {
throw Exception("Can't convert $resolvedType from JSON");
}
}
@override
void visitApi() {
outputHeader(year: '2017');
writeln();
emitImports();
emitClasses(getClassesToEmit());
}
}
/// The kind of URI converter that should be used in generated toJson/fromJson
/// methods.
enum CodegenUriConverterKind {
/// No URI/Path conversion is done.
///
/// This is used in analysis_server_client where the client code should handle
/// getting/setting the raw values based on server parameters and no
/// conversion is ever done.
none,
/// URI/Path conversion is done optionally with a UriConverter parameter.
///
/// This is used in analyzer_plugin common protocol classes that are used by
/// both server and plugins. Plugin protocol classes will not pass a converter
/// but server classes will.
optionalParameter,
/// URI/Path conversion is done with a required converter.
///
/// This is used in analysis_server where all calls should use the
/// server-configured converter.
requiredParameter;
/// Dart code for a named argument to pass the converter to another
/// `toJson`/`fromJson` method.
///
/// Throws if this value is [none].
String get namedArgumentString => switch (this) {
optionalParameter ||
requiredParameter =>
'clientUriConverter: clientUriConverter',
_ => throw "'namedArgumentString' cannot be used for 'none'",
};
/// Dart code for a named parameter for the converter.
///
/// Throws if this value is [none].
String get namedParameterString => switch (this) {
optionalParameter => 'ClientUriConverter? clientUriConverter',
requiredParameter => 'required ClientUriConverter? clientUriConverter',
_ => throw "'namedParameterString' can only be used for 'parameter'",
};
}
/// Container for code that can be used to translate a data type from JSON.
abstract class FromJsonCode {
/// Get the translation code in the form of a closure.
String get asClosure;
/// True if the data type is already in JSON form, so the translation is the
/// identity function.
bool get isIdentity;
/// Get the translation code in the form of a code snippet, where [jsonPath]
/// is the variable holding the JSON path, and [json] is the variable holding
/// the raw JSON.
String asSnippet(String jsonPath, String json);
}
/// Representation of FromJsonCode for a function defined elsewhere.
class FromJsonFunction extends FromJsonCode {
@override
final String asClosure;
FromJsonFunction(this.asClosure);
@override
bool get isIdentity => false;
@override
String asSnippet(String jsonPath, String json) =>
'$asClosure($jsonPath, $json)';
}
/// Representation of FromJsonCode for the identity transformation.
class FromJsonIdentity extends FromJsonSnippet {
FromJsonIdentity() : super((String jsonPath, String json) => json);
@override
bool get isIdentity => true;
}
/// Representation of FromJsonCode for a snippet of inline code.
class FromJsonSnippet extends FromJsonCode {
/// Callback that can be used to generate the code snippet, once the names
/// of the [jsonPath] and [json] variables are known.
final FromJsonSnippetCallback callback;
FromJsonSnippet(this.callback);
@override
String get asClosure =>
'(String jsonPath, Object? json) => ${callback('jsonPath', 'json')}';
@override
bool get isIdentity => false;
@override
String asSnippet(String jsonPath, String json) => callback(jsonPath, json);
}
/// Container for code that can be used to translate a data type to JSON.
abstract class ToJsonCode {
/// Get the translation code in the form of a closure.
String get asClosure;
/// True if the data type is already in JSON form, so the translation is the
/// identity function.
bool get isIdentity;
/// Get the translation code in the form of a code snippet, where [value]
/// is the variable holding the object to be translated.
String asSnippet(String value);
}
/// Representation of ToJsonCode for a function defined elsewhere.
class ToJsonFunction extends ToJsonCode {
@override
final String asClosure;
ToJsonFunction(this.asClosure);
@override
bool get isIdentity => false;
@override
String asSnippet(String value) => '$asClosure($value)';
}
/// Representation of FromJsonCode for the identity transformation.
class ToJsonIdentity extends ToJsonSnippet {
ToJsonIdentity(String type) : super(type, (String value) => value);
@override
bool get isIdentity => true;
}
/// Representation of ToJsonCode for a snippet of inline code.
class ToJsonSnippet extends ToJsonCode {
/// Callback that can be used to generate the code snippet, once the name
/// of the [value] variable is known.
final ToJsonSnippetCallback callback;
/// Dart type of the [value] variable.
final String type;
ToJsonSnippet(this.type, this.callback);
@override
String get asClosure => '($type value) => ${callback('value')}';
@override
bool get isIdentity => false;
@override
String asSnippet(String value) => callback(value);
}