cb7c932f7b
Also bump min SDK for pkg:analyzer Towards https://github.com/dart-lang/sdk/issues/27698 Change-Id: Ic32c839c02f18afd99fdb98eb382540aae7da88a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/214126 Auto-Submit: Kevin Moore <kevmoo@google.com> Commit-Queue: Kevin Moore <kevmoo@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
1215 lines
40 KiB
Dart
1215 lines
40 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/tools.dart';
|
|
import 'package:html/dom.dart' as dom;
|
|
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 clientTarget(bool responseRequiresRequestTime) {
|
|
return GeneratedFile(
|
|
'../analysis_server_client/lib/src/protocol/protocol_generated.dart',
|
|
(String pkgPath) async {
|
|
var visitor = CodegenProtocolVisitor('analysis_server_client',
|
|
responseRequiresRequestTime, false, readApi(pkgPath));
|
|
return visitor.collectCode(visitor.visitApi);
|
|
});
|
|
}
|
|
|
|
GeneratedFile serverTarget(bool responseRequiresRequestTime) {
|
|
return GeneratedFile('lib/protocol/protocol_generated.dart',
|
|
(String pkgPath) async {
|
|
var visitor = CodegenProtocolVisitor(path.basename(pkgPath),
|
|
responseRequiresRequestTime, true, 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;
|
|
|
|
/// A flag indicating whether this generated code is for the server
|
|
/// (analysis_server) or for the client (analysis_server_client).
|
|
final bool isServer;
|
|
|
|
/// 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.isServer, 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 args = <String>['$inputType $inputName'];
|
|
args.addAll(extraArgs);
|
|
writeln('factory $className.$constructorName(${args.join(', ')}) {');
|
|
indent(() {
|
|
var fieldNameString =
|
|
literalString(fieldName.replaceFirst(RegExp('^_'), ''));
|
|
if (className == 'EditGetRefactoringParams') {
|
|
writeln('var params = $className.fromJson(');
|
|
writeln(' $makeDecoder, $fieldNameString, $inputName.$fieldName);');
|
|
writeln('REQUEST_ID_REFACTORING_KINDS[request.id] = params.kind;');
|
|
writeln('return params;');
|
|
} else {
|
|
writeln('return $className.fromJson(');
|
|
writeln(' $makeDecoder, $fieldNameString, $inputName.$fieldName);');
|
|
}
|
|
});
|
|
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() {
|
|
writeln('@override');
|
|
writeln('Map<String, Object> toJson() => <String, Object>{};');
|
|
}
|
|
|
|
/// Emit a class to encapsulate an enum.
|
|
void emitEnumClass(String className, TypeEnum type, ImpliedType impliedType) {
|
|
docComment(toHtmlVisitor.collectHtml(() {
|
|
toHtmlVisitor.p(() {
|
|
toHtmlVisitor.write(impliedType.humanReadableName);
|
|
});
|
|
var impliedTypeType = impliedType.type;
|
|
if (impliedTypeType != null) {
|
|
toHtmlVisitor.showType(null, impliedTypeType);
|
|
}
|
|
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() => 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) {
|
|
writeln(
|
|
'factory $className.fromJson(JsonDecoder jsonDecoder, String jsonPath, Object? json) {');
|
|
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();
|
|
if (isServer) {
|
|
writeln("import 'package:$packageName/protocol/protocol.dart';");
|
|
writeln(
|
|
"import 'package:$packageName/src/protocol/protocol_internal.dart';");
|
|
for (var uri in api.types.importUris) {
|
|
write("import '");
|
|
write(uri);
|
|
writeln("';");
|
|
}
|
|
} else {
|
|
writeln("import 'package:$packageName/src/protocol/protocol_base.dart';");
|
|
writeln(
|
|
"import 'package:$packageName/src/protocol/protocol_common.dart';");
|
|
writeln(
|
|
"import 'package:$packageName/src/protocol/protocol_internal.dart';");
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
});
|
|
var impliedTypeType = impliedType.type;
|
|
if (impliedTypeType != null) {
|
|
toHtmlVisitor.showType(null, impliedTypeType);
|
|
}
|
|
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();
|
|
}
|
|
writeln('@override');
|
|
writeln('String toString() => json.encode(toJson());');
|
|
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');
|
|
writeln('bool operator ==(other) {');
|
|
indent(() {
|
|
writeln('if (other is $className) {');
|
|
indent(() {
|
|
var comparisons = <String>[];
|
|
if (type != null) {
|
|
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 humanReadableNameString = literalString(impliedType.humanReadableName);
|
|
if (className == 'RefactoringFeedback') {
|
|
writeln('static RefactoringFeedback? fromJson(JsonDecoder jsonDecoder, '
|
|
'String jsonPath, Object? json, Map responseJson) {');
|
|
indent(() {
|
|
writeln('return refactoringFeedbackFromJson(jsonDecoder, jsonPath, '
|
|
'json, responseJson);');
|
|
});
|
|
writeln('}');
|
|
return;
|
|
}
|
|
if (className == 'RefactoringOptions') {
|
|
writeln('static RefactoringOptions? fromJson(JsonDecoder jsonDecoder, '
|
|
'String jsonPath, Object? json, RefactoringKind kind) {');
|
|
indent(() {
|
|
writeln('return refactoringOptionsFromJson(jsonDecoder, jsonPath, '
|
|
'json, kind);');
|
|
});
|
|
writeln('}');
|
|
return;
|
|
}
|
|
writeln(
|
|
'factory $className.fromJson(JsonDecoder jsonDecoder, String jsonPath, Object? json) {');
|
|
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 = 'jsonPath + ${literalString('.${field.name}')}';
|
|
var fieldValue = field.value;
|
|
if (fieldValue is String) {
|
|
var valueString = literalString(fieldValue);
|
|
writeln('if ($fieldAccessor != $valueString) {');
|
|
indent(() {
|
|
writeln(
|
|
"throw jsonDecoder.mismatch(jsonPath, 'equal $fieldValue', 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 => ');
|
|
indent(() {
|
|
if (type == null) {
|
|
writeln(' ${className.hashCode}');
|
|
} else {
|
|
final items = type.fields.map((field) {
|
|
if (field.value != null) {
|
|
return field.value.hashCode.toString();
|
|
} else {
|
|
return '${field.name}.hashCode';
|
|
}
|
|
}).toList();
|
|
|
|
if (items.isEmpty) {
|
|
writeln('0');
|
|
} else if (items.length == 1) {
|
|
writeln(items.single);
|
|
} else {
|
|
writeln('Object.hash(');
|
|
for (var field in items) {
|
|
writeln('$field,');
|
|
}
|
|
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.Text('Adds [edit] to the [FileEdit] for the given [file].')]);
|
|
writeln('void addEdit(String file, int fileStamp, SourceEdit edit) =>');
|
|
writeln(' addEditToSourceChange(this, file, fileStamp, edit);');
|
|
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.Text('Adds the given [Edit] to the list.')]);
|
|
writeln('void add(SourceEdit edit) => addEditForSource(this, edit);');
|
|
writeln();
|
|
docComment([dom.Text('Adds the given [Edit]s.')]);
|
|
writeln('void addAll(Iterable<SourceEdit> edits) =>');
|
|
writeln(' addAllEditsForSource(this, edits);');
|
|
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('$camelName = false');
|
|
makeFlagsStatements.add('if ($camelName) flags |= $flag;');
|
|
});
|
|
writeln();
|
|
writeln('static int makeFlags({${makeFlagsArgs.join(', ')}}) {');
|
|
indent(() {
|
|
writeln('int 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) {
|
|
writeln('@override');
|
|
writeln('Map<String, Object> toJson() {');
|
|
indent(() {
|
|
writeln('var result = <String, Object>{};');
|
|
for (var field in type.fields) {
|
|
var fieldNameString = literalString(field.name);
|
|
var fieldValue = field.value;
|
|
if (fieldValue is String) {
|
|
var valueString = literalString(fieldValue);
|
|
writeln('result[$fieldNameString] = $valueString;');
|
|
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) {
|
|
if (impliedType.kind == 'notificationParams') {
|
|
writeln('Notification toNotification() {');
|
|
indent(() {
|
|
var eventString =
|
|
literalString((impliedType.apiNode as Notification).longEvent);
|
|
var jsonPart = impliedType.type != null ? 'toJson()' : '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) {
|
|
if (impliedType.kind == 'requestParams') {
|
|
writeln('@override');
|
|
writeln('Request toRequest(String id) {');
|
|
indent(() {
|
|
var methodString =
|
|
literalString((impliedType.apiNode as Request).longMethod);
|
|
var jsonPart = impliedType.type != null ? 'toJson()' : 'null';
|
|
writeln('return Request(id, $methodString, $jsonPart);');
|
|
});
|
|
writeln('}');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Emit the toResponse() code for a class, if appropriate. Returns true if
|
|
/// code was emitted.
|
|
bool emitToResponseMember(ImpliedType impliedType) {
|
|
if (impliedType.kind == 'requestResult') {
|
|
writeln('@override');
|
|
if (responseRequiresRequestTime) {
|
|
writeln('Response toResponse(String id, int requestTime) {');
|
|
} else {
|
|
writeln('Response toResponse(String id) {');
|
|
}
|
|
indent(() {
|
|
var jsonPart = impliedType.type != null ? 'toJson()' : 'null';
|
|
if (responseRequiresRequestTime) {
|
|
writeln('return Response(id, requestTime, result: $jsonPart);');
|
|
} else {
|
|
writeln('return Response(id, result: $jsonPart);');
|
|
}
|
|
});
|
|
writeln('}');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Compute the code necessary to translate [type] from JSON.
|
|
FromJsonCode fromJsonCode(TypeDecl type) {
|
|
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)';
|
|
} else if (typeName == 'RefactoringOptions') {
|
|
return '$typeName.fromJson(jsonDecoder, $jsonPath, $json, kind)';
|
|
} else {
|
|
return '$typeName.fromJson(jsonDecoder, $jsonPath, $json)';
|
|
}
|
|
});
|
|
} else {
|
|
return fromJsonCode(referencedType);
|
|
}
|
|
} else {
|
|
switch (type.typeName) {
|
|
case 'String':
|
|
return FromJsonFunction('jsonDecoder.decodeString');
|
|
case 'bool':
|
|
return FromJsonFunction('jsonDecoder.decodeBool');
|
|
case 'double':
|
|
return FromJsonFunction('jsonDecoder.decodeDouble');
|
|
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;
|
|
if (dartType(type.keyType) != 'String') {
|
|
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 resolvedType = resolveTypeReferenceChain(type);
|
|
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;
|
|
if (dartType(resolvedType.keyType) != 'String') {
|
|
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()');
|
|
} else {
|
|
throw Exception("Can't convert $resolvedType from JSON");
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitApi() {
|
|
outputHeader(year: '2017');
|
|
writeln();
|
|
emitImports();
|
|
emitClasses(getClassesToEmit());
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
}
|