| // Copyright (c) 2017, Devon Carew. 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:collection' show LinkedHashMap; |
| import 'dart:io'; |
| |
| import 'package:html/dom.dart'; |
| import 'package:html/dom_parsing.dart' show TreeVisitor; |
| import 'package:html/parser.dart' show parse; |
| |
| import 'src/src_gen.dart'; |
| |
| Api api; |
| |
| main(List<String> args) { |
| // Parse spec_input.html into a model. |
| File file = new File('tool/spec_input.html'); |
| Document document = parse(file.readAsStringSync()); |
| print('Parsed ${file.path}.'); |
| Element ver = document.body.querySelector('version'); |
| List<Element> domains = document.body.getElementsByTagName('domain'); |
| List<Element> typedefs = document.body |
| .getElementsByTagName('types') |
| .first |
| .getElementsByTagName('type'); |
| List<Element> refactorings = document.body |
| .getElementsByTagName('refactorings') |
| .first |
| .getElementsByTagName('refactoring'); |
| |
| // Common common_types_spec.html. |
| File commonTypesFile = new File('tool/common_types_spec.html'); |
| Document commonTypesDoc = parse(commonTypesFile.readAsStringSync()); |
| print('Parsed ${commonTypesFile.path}.'); |
| List<Element> commonTypedefs = commonTypesDoc.body |
| .getElementsByTagName('types') |
| .first |
| .getElementsByTagName('type'); |
| |
| List<Element> combinedTypeDefs = new List() |
| ..addAll(typedefs) |
| ..addAll(commonTypedefs); |
| combinedTypeDefs.sort((Element a, Element b) { |
| return a.attributes['name'].compareTo(b.attributes['name']); |
| }); |
| |
| api = new Api(ver.text); |
| api.parse(domains, combinedTypeDefs, refactorings); |
| |
| // Generate code from the model. |
| File outputFile = new File('lib/analysis_server_lib.dart'); |
| DartGenerator generator = new DartGenerator(); |
| api.generate(generator); |
| outputFile.writeAsStringSync(generator.toString()); |
| Process.runSync( |
| 'dartfmt${Platform.isWindows ? ".bat" : ""}', ['-w', outputFile.path]); |
| print('Wrote ${outputFile.path}.'); |
| } |
| |
| class Api { |
| final String version; |
| |
| List<Domain> domains; |
| List<TypeDef> typedefs; |
| List<Refactoring> refactorings; |
| |
| Api(this.version); |
| |
| void parse(List<Element> domainElements, List<Element> typeElements, |
| List<Element> refactoringElements) { |
| typedefs = |
| new List.from(typeElements.map((element) => new TypeDef(element))); |
| domains = |
| new List.from(domainElements.map((element) => new Domain(element))); |
| refactorings = |
| new List.from(refactoringElements.map((e) => new Refactoring(e))); |
| |
| // Mark some types as jsonable - we can send them back over the wire. |
| findRef('SourceEdit').setCallParam(); |
| findRef('CompletionSuggestion').setCallParam(); |
| findRef('Element').setCallParam(); |
| findRef('Location').setCallParam(); |
| typedefs |
| .where((def) => def.name.endsWith('ContentOverlay')) |
| .forEach((def) => def.setCallParam()); |
| } |
| |
| TypeDef findRef(String name) => |
| typedefs.firstWhere((TypeDef t) => t.name == name); |
| |
| void generate(DartGenerator gen) { |
| gen.out(_headerCode); |
| gen.writeln("const String generatedProtocolVersion = '${version}';"); |
| gen.writeln(); |
| gen.writeln("typedef void MethodSend(String methodName);"); |
| gen.writeln(); |
| gen.writeDocs(''' |
| A class to communicate with an analysis server instance. |
| |
| Here's a simple example of starting and communicating with the server: |
| |
| ```dart |
| import 'package:analysis_server_lib/analysis_server_lib.dart'; |
| |
| main() async { |
| AnalysisServer server = await AnalysisServer.create(); |
| await server.server.onConnected.first; |
| |
| VersionResult version = await server.server.getVersion(); |
| print(version.version); |
| |
| server.dispose(); |
| } |
| ``` |
| '''); |
| gen.writeStatement('class AnalysisServer {'); |
| gen.writeln(_staticFactory); |
| gen.writeStatement('final Completer<int> processCompleter;'); |
| gen.writeStatement('final Function _processKillHandler;'); |
| gen.writeln(); |
| gen.writeStatement('StreamSubscription _streamSub;'); |
| gen.writeStatement('Function _writeMessage;'); |
| gen.writeStatement('int _id = 0;'); |
| gen.writeStatement('Map<String, Completer> _completers = {};'); |
| gen.writeStatement('Map<String, String> _methodNames = {};'); |
| gen.writeln( |
| 'JsonCodec _jsonEncoder = new JsonCodec(toEncodable: _toEncodable);'); |
| gen.writeStatement('Map<String, Domain> _domains = {};'); |
| gen.writeln( |
| "StreamController<String> _onSend = new StreamController.broadcast();"); |
| gen.writeln( |
| "StreamController<String> _onReceive = new StreamController.broadcast();"); |
| gen.writeln("MethodSend _willSend;"); |
| gen.writeln(); |
| domains.forEach( |
| (Domain domain) => gen.writeln('${domain.className} _${domain.name};')); |
| gen.writeln(); |
| gen.writeDocs('Connect to an existing analysis server instance.'); |
| gen.writeStatement( |
| 'AnalysisServer(Stream<String> inStream, void writeMessage(String message), \n' |
| 'this.processCompleter, [this._processKillHandler]) {'); |
| gen.writeStatement('configure(inStream, writeMessage);'); |
| gen.writeln(); |
| domains.forEach((Domain domain) => |
| gen.writeln('_${domain.name} = new ${domain.className}(this);')); |
| gen.writeln('}'); |
| gen.writeln(); |
| domains.forEach((Domain domain) => gen |
| .writeln('${domain.className} get ${domain.name} => _${domain.name};')); |
| gen.writeln(); |
| gen.out(_serverCode); |
| gen.writeln('}'); |
| gen.writeln(); |
| |
| // abstract Domain |
| gen.out(_domainCode); |
| |
| // individual domains |
| domains.forEach((Domain domain) => domain.generate(gen)); |
| |
| // Object definitions. |
| gen.writeln(); |
| gen.writeln('// type definitions'); |
| gen.writeln(); |
| typedefs |
| .where((t) => t.isObject) |
| .forEach((TypeDef def) => def.generate(gen)); |
| |
| // Handle the refactorings items. |
| gen.writeln(); |
| gen.writeln('// refactorings'); |
| gen.writeln(); |
| gen.writeStatement('class Refactorings {'); |
| refactorings.forEach((Refactoring refactor) { |
| gen.writeStatement( |
| "static const String ${refactor.kind} = '${refactor.kind}';"); |
| }); |
| gen.writeStatement('}'); |
| |
| refactorings.forEach((Refactoring refactor) => refactor.generate(gen)); |
| |
| // Refactoring feedback. |
| gen.writeln(); |
| |
| gen.writeStatement('abstract class RefactoringFeedback {'); |
| gen.writeStatement( |
| 'static RefactoringFeedback parse(String kind, Map m) {'); |
| gen.writeStatement('if (m == null) return null;'); |
| gen.writeln(); |
| gen.writeStatement('switch (kind) {'); |
| refactorings.forEach((Refactoring refactor) { |
| if (refactor.feedbackFields.isEmpty) return; |
| gen.writeStatement("case Refactorings.${refactor.kind}: " |
| "return ${refactor.className}Feedback.parse(m);"); |
| }); |
| gen.writeStatement('}'); |
| gen.writeln(); |
| gen.writeStatement('return null;'); |
| gen.writeStatement('}'); |
| gen.writeStatement('}'); |
| |
| // Refactoring feedback classes. |
| for (Refactoring refactor in refactorings) { |
| if (refactor.feedbackFields.isEmpty) continue; |
| |
| List<Field> fields = refactor.feedbackFields; |
| String name = '${refactor.className}Feedback'; |
| |
| gen.writeln(); |
| gen.writeDocs('Feedback class for the `${refactor.kind}` refactoring.'); |
| gen.writeStatement('class ${name} extends RefactoringFeedback {'); |
| gen.write('static ${name} parse(Map m) => '); |
| gen.write('new ${name}('); |
| gen.write(fields.map((Field field) { |
| String val = "m['${field.name}']"; |
| if (field.type.isMap) { |
| val = 'new Map.from($val)'; |
| } |
| |
| if (field.optional) { |
| return "${field.name}: ${field.type.jsonConvert(val)}"; |
| } else { |
| return field.type.jsonConvert(val); |
| } |
| }).join(', ')); |
| gen.writeln(');'); |
| gen.writeln(); |
| fields.forEach((field) { |
| if (field.deprecated) { |
| gen.write('@deprecated '); |
| } else { |
| gen.writeDocs(field.docs); |
| } |
| if (field.optional) gen.write('@optional '); |
| gen.writeln('final ${field.type} ${field.name};'); |
| }); |
| gen.writeln(); |
| gen.write('${name}('); |
| gen.write(fields.map((field) { |
| StringBuffer buf = new StringBuffer(); |
| if (field.optional && fields.firstWhere((a) => a.optional) == field) |
| buf.write('{'); |
| buf.write('this.${field.name}'); |
| if (field.optional && fields.lastWhere((a) => a.optional) == field) |
| buf.write('}'); |
| return buf.toString(); |
| }).join(', ')); |
| gen.writeln(');'); |
| gen.writeln('}'); |
| } |
| } |
| |
| String toString() => domains.toString(); |
| } |
| |
| class Domain { |
| bool experimental = false; |
| String name; |
| String docs; |
| |
| List<Request> requests; |
| List<Notification> notifications; |
| Map<String, List<Field>> resultClasses = new LinkedHashMap(); |
| |
| Domain(Element element) { |
| name = element.attributes['name']; |
| experimental = element.attributes.containsKey('experimental'); |
| docs = _collectDocs(element); |
| requests = element |
| .getElementsByTagName('request') |
| .map((element) => new Request(this, element)) |
| .toList(); |
| notifications = element |
| .getElementsByTagName('notification') |
| .map((element) => new Notification(this, element)) |
| .toList(); |
| } |
| |
| String get className => '${titleCase(name)}Domain'; |
| |
| void generate(DartGenerator gen) { |
| resultClasses.clear(); |
| gen.writeln(); |
| gen.writeln('// ${name} domain'); |
| gen.writeln(); |
| gen.writeDocs(docs); |
| if (experimental) gen.writeln('@experimental'); |
| gen.writeStatement('class ${className} extends Domain {'); |
| gen.writeStatement( |
| "${className}(AnalysisServer server) : super(server, '${name}');"); |
| if (notifications.isNotEmpty) { |
| gen.writeln(); |
| notifications |
| .forEach((Notification notification) => notification.generate(gen)); |
| } |
| requests.forEach((Request request) => request.generate(gen)); |
| gen.writeln('}'); |
| |
| notifications.forEach( |
| (Notification notification) => notification.generateClass(gen)); |
| |
| // Result classes |
| for (String name in resultClasses.keys) { |
| List<Field> fields = resultClasses[name]; |
| |
| gen.writeln(); |
| gen.writeStatement('class ${name} {'); |
| if (name == 'RefactoringResult') { |
| gen.write('static ${name} parse(String kind, Map m) => '); |
| } else { |
| gen.write('static ${name} parse(Map m) => '); |
| } |
| gen.write('new ${name}('); |
| gen.write(fields.map((Field field) { |
| String val = "m['${field.name}']"; |
| if (field.type.isMap) { |
| val = 'new Map.from($val)'; |
| } |
| |
| if (field.optional) { |
| return "${field.name}: ${field.type.jsonConvert(val)}"; |
| } else { |
| return field.type.jsonConvert(val); |
| } |
| }).join(', ')); |
| gen.writeln(');'); |
| gen.writeln(); |
| fields.forEach((field) { |
| if (field.deprecated) { |
| gen.write('@deprecated '); |
| } else { |
| gen.writeDocs(field.docs); |
| } |
| if (field.optional) gen.write('@optional '); |
| gen.writeln('final ${field.type} ${field.name};'); |
| }); |
| gen.writeln(); |
| gen.write('${name}('); |
| gen.write(fields.map((field) { |
| StringBuffer buf = new StringBuffer(); |
| if (field.optional && fields.firstWhere((a) => a.optional) == field) |
| buf.write('{'); |
| buf.write('this.${field.name}'); |
| if (field.optional && fields.lastWhere((a) => a.optional) == field) |
| buf.write('}'); |
| return buf.toString(); |
| }).join(', ')); |
| gen.writeln(');'); |
| gen.writeln('}'); |
| } |
| } |
| |
| String toString() => "Domain '${name}': ${requests}"; |
| } |
| |
| class Request { |
| final Domain domain; |
| |
| bool experimental; |
| bool deprecated; |
| String method; |
| String docs; |
| |
| List<Field> args = []; |
| List<Field> results = []; |
| |
| Request(this.domain, Element element) { |
| experimental = element.attributes.containsKey('experimental'); |
| deprecated = element.attributes.containsKey('deprecated'); |
| method = element.attributes['method']; |
| docs = _collectDocs(element); |
| |
| List paramsList = element.getElementsByTagName('params'); |
| if (paramsList.isNotEmpty) { |
| args = new List.from(paramsList.first |
| .getElementsByTagName('field') |
| .map((field) => new Field(field))); |
| } |
| |
| List resultsList = element.getElementsByTagName('result'); |
| if (resultsList.isNotEmpty) { |
| results = new List.from(resultsList.first |
| .getElementsByTagName('field') |
| .map((field) => new Field(field))); |
| } |
| } |
| |
| void generate(DartGenerator gen) { |
| gen.writeln(); |
| |
| args.forEach((Field field) => field.setCallParam()); |
| if (results.isNotEmpty) { |
| domain.resultClasses[resultName] = results; |
| } |
| |
| if (deprecated) { |
| gen.writeln('@deprecated'); |
| } else { |
| gen.writeDocs(docs); |
| } |
| if (experimental) gen.writeln('@experimental'); |
| |
| String qName = '${domain.name}.${method}'; |
| |
| if (results.isEmpty) { |
| if (args.isEmpty) { |
| gen.writeln("Future ${method}() => _call('$qName');"); |
| return; |
| } |
| |
| if (args.length == 1 && !args.first.optional) { |
| Field arg = args.first; |
| String type = arg.type.toString(); |
| |
| if (method == 'updateContent') { |
| type = 'Map<String, ContentOverlayType>'; |
| } |
| gen.write("Future ${method}(${type} ${arg.name}) => "); |
| gen.writeln("_call('$qName', {'${arg.name}': ${arg.name}});"); |
| return; |
| } |
| } |
| |
| if (args.isEmpty) { |
| gen.writeln("Future<${resultName}> ${method}() => _call('$qName').then(" |
| "${resultName}.parse);"); |
| return; |
| } |
| |
| if (results.isEmpty) { |
| gen.write('Future ${method}('); |
| } else { |
| gen.write('Future<${resultName}> ${method}('); |
| } |
| gen.write(args.map((arg) { |
| StringBuffer buf = new StringBuffer(); |
| if (arg.optional && args.firstWhere((a) => a.optional) == arg) |
| buf.write('{'); |
| buf.write('${arg.type} ${arg.name}'); |
| if (arg.optional && args.lastWhere((a) => a.optional) == arg) |
| buf.write('}'); |
| return buf.toString(); |
| }).join(', ')); |
| gen.writeStatement(') {'); |
| if (args.isEmpty) { |
| gen.write("return _call('$qName')"); |
| if (results.isNotEmpty) { |
| gen.write(".then(${resultName}.parse)"); |
| } |
| gen.writeln(';'); |
| } else { |
| String mapStr = args |
| .where((arg) => !arg.optional) |
| .map((arg) => "'${arg.name}': ${arg.name}") |
| .join(', '); |
| gen.writeStatement('Map m = {${mapStr}};'); |
| for (Field arg in args.where((arg) => arg.optional)) { |
| gen.writeStatement( |
| "if (${arg.name} != null) m['${arg.name}'] = ${arg.name};"); |
| } |
| gen.write("return _call('$qName', m)"); |
| if (results.isNotEmpty) { |
| if (qName == 'edit.getRefactoring') { |
| gen.write(".then((m) => ${resultName}.parse(kind, m))"); |
| } else { |
| gen.write(".then(${resultName}.parse)"); |
| } |
| } |
| gen.writeln(';'); |
| } |
| gen.writeStatement('}'); |
| } |
| |
| String get resultName { |
| if (results.isEmpty) return 'dynamic'; |
| if (method.startsWith('get')) return '${method.substring(3)}Result'; |
| return '${titleCase(method)}Result'; |
| } |
| |
| String toString() => 'Request ${method}()'; |
| } |
| |
| class Notification { |
| static Set<String> disambiguateEvents = new Set.from(['FlutterOutline']); |
| |
| final Domain domain; |
| String event; |
| String docs; |
| List<Field> fields; |
| |
| Notification(this.domain, Element element) { |
| event = element.attributes['event']; |
| docs = _collectDocs(element); |
| fields = new List.from( |
| element.getElementsByTagName('field').map((field) => new Field(field))); |
| fields.sort(); |
| } |
| |
| String get title => '${domain.name}.${event}'; |
| |
| String get onName => 'on${titleCase(event)}'; |
| |
| String get className { |
| String name = '${titleCase(domain.name)}${titleCase(event)}'; |
| if (disambiguateEvents.contains(name)) { |
| name = name + 'Event'; |
| } |
| return name; |
| } |
| |
| void generate(DartGenerator gen) { |
| gen.writeDocs(docs); |
| gen.writeln("Stream<${className}> get ${onName} {"); |
| gen.writeln("return _listen('${title}', ${className}.parse);"); |
| gen.writeln("}"); |
| } |
| |
| void generateClass(DartGenerator gen) { |
| gen.writeln(); |
| gen.writeln('class ${className} {'); |
| gen.write('static ${className} parse(Map m) => '); |
| gen.write('new ${className}('); |
| gen.write(fields.map((Field field) { |
| String val = "m['${field.name}']"; |
| if (field.optional) { |
| return "${field.name}: ${field.type.jsonConvert(val)}"; |
| } else { |
| return field.type.jsonConvert(val); |
| } |
| }).join(', ')); |
| gen.writeln(');'); |
| if (fields.isNotEmpty) { |
| gen.writeln(); |
| fields.forEach((field) { |
| if (field.deprecated) { |
| gen.write('@deprecated '); |
| } else { |
| gen.writeDocs(field.docs); |
| } |
| if (field.optional) gen.write('@optional '); |
| gen.writeln('final ${field.type} ${field.name};'); |
| }); |
| } |
| gen.writeln(); |
| gen.write('${className}('); |
| gen.write(fields.map((field) { |
| StringBuffer buf = new StringBuffer(); |
| if (field.optional && fields.firstWhere((a) => a.optional) == field) |
| buf.write('{'); |
| buf.write('this.${field.name}'); |
| if (field.optional && fields.lastWhere((a) => a.optional) == field) |
| buf.write('}'); |
| return buf.toString(); |
| }).join(', ')); |
| gen.writeln(');'); |
| gen.writeln('}'); |
| } |
| } |
| |
| class Field implements Comparable { |
| String name; |
| String docs; |
| bool optional; |
| bool deprecated; |
| Type type; |
| |
| Field(Element element) { |
| name = element.attributes['name']; |
| docs = _collectDocs(element); |
| optional = element.attributes['optional'] == 'true'; |
| deprecated = element.attributes.containsKey('deprecated'); |
| type = Type.create(element.children.first); |
| } |
| |
| void setCallParam() => type.setCallParam(); |
| |
| bool get isJsonable => type.isCallParam(); |
| |
| String toString() => name; |
| |
| int compareTo(other) { |
| if (other is! Field) return 0; |
| if (!optional && other.optional) return -1; |
| if (optional && !other.optional) return 1; |
| return 0; |
| } |
| |
| void generate(DartGenerator gen) { |
| if (deprecated) { |
| gen.writeln('@deprecated'); |
| } else { |
| gen.writeDocs(docs); |
| } |
| if (optional) gen.write('@optional '); |
| gen.writeStatement('final ${type} ${name};'); |
| } |
| } |
| |
| class Refactoring { |
| String kind; |
| String docs; |
| List<Field> optionsFields = []; |
| List<Field> feedbackFields = []; |
| |
| Refactoring(Element element) { |
| kind = element.attributes['kind']; |
| docs = _collectDocs(element); |
| |
| // Parse <options> |
| // <field name="deleteSource"><ref>bool</ref></field> |
| Element options = element.querySelector('options'); |
| if (options != null) { |
| optionsFields = new List.from(options |
| .getElementsByTagName('field') |
| .map((field) => new Field(field))); |
| } |
| |
| // Parse <feedback> |
| // <field name="className" optional="true"><ref>String</ref></field> |
| Element feedback = element.querySelector('feedback'); |
| if (feedback != null) { |
| feedbackFields = new List.from(feedback |
| .getElementsByTagName('field') |
| .map((field) => new Field(field))); |
| feedbackFields.sort(); |
| } |
| } |
| |
| String get className { |
| // MOVE_FILE ==> MoveFile |
| return kind.split('_').map((s) => forceTitleCase(s)).join(''); |
| } |
| |
| void generate(DartGenerator gen) { |
| // Generate the refactoring options. |
| if (optionsFields.isNotEmpty) { |
| gen.writeln(); |
| gen.writeDocs(docs); |
| gen.writeStatement( |
| 'class ${className}RefactoringOptions extends RefactoringOptions {'); |
| // fields |
| for (Field field in optionsFields) { |
| field.generate(gen); |
| } |
| |
| gen.writeln(); |
| gen.writeStatement('${className}RefactoringOptions({' |
| '${optionsFields.map((f) => 'this.${f.name}').join(', ')}' |
| '});'); |
| gen.writeln(); |
| |
| // toMap |
| gen.write("Map toMap() => _stripNullValues({"); |
| gen.write(optionsFields.map((f) => "'${f.name}': ${f.name}").join(', ')); |
| gen.writeStatement("});"); |
| gen.writeStatement('}'); |
| } |
| } |
| } |
| |
| class TypeDef { |
| static final Set<String> _shouldHaveToString = new Set.from([ |
| 'SourceEdit', |
| 'PubStatus', |
| 'Location', |
| 'AnalysisStatus', |
| 'AnalysisError', |
| 'SourceChange', |
| 'SourceFileEdit', |
| 'LinkedEditGroup', |
| 'Position', |
| 'NavigationRegion', |
| 'NavigationTarget', |
| 'CompletionSuggestion', |
| 'Element', |
| 'SearchResult' |
| ]); |
| |
| static final Set<String> _shouldHaveEquals = |
| new Set.from(['Location', 'AnalysisError']); |
| |
| String name; |
| bool experimental; |
| bool deprecated; |
| String docs; |
| bool isString = false; |
| List<Field> fields; |
| bool _callParam = false; |
| |
| TypeDef(Element element) { |
| name = element.attributes['name']; |
| experimental = element.attributes.containsKey('experimental'); |
| deprecated = element.attributes.containsKey('deprecated'); |
| docs = _collectDocs(element); |
| |
| // object, enum, ref |
| Set<String> tags = new Set.from(element.children.map((c) => c.localName)); |
| |
| if (tags.contains('object')) { |
| Element object = element.getElementsByTagName('object').first; |
| fields = new List.from( |
| object.getElementsByTagName('field').map((f) => new Field(f))); |
| fields.sort(); |
| } else if (tags.contains('enum')) { |
| isString = true; |
| } else if (tags.contains('ref')) { |
| Element tag = element.getElementsByTagName('ref').first; |
| String type = tag.text; |
| if (type == 'String') { |
| isString = true; |
| } else { |
| throw 'unknown ref type: ${type}'; |
| } |
| } else { |
| throw 'unknown tag: ${tags}'; |
| } |
| } |
| |
| bool get isObject => fields != null; |
| |
| bool get callParam => _callParam; |
| |
| void setCallParam() { |
| _callParam = true; |
| } |
| |
| bool isCallParam() => _callParam; |
| |
| void generate(DartGenerator gen) { |
| if (name == 'RefactoringOptions' || |
| name == 'RefactoringFeedback' || |
| name == 'RequestError') { |
| return; |
| } |
| |
| bool isContentOverlay = name.endsWith('ContentOverlay'); |
| List<Field> _fields = fields; |
| if (isContentOverlay) { |
| _fields = _fields.toList()..removeAt(0); |
| } |
| |
| gen.writeln(); |
| if (deprecated) { |
| gen.writeln('@deprecated'); |
| } else { |
| gen.writeDocs(docs); |
| } |
| if (experimental) gen.writeln('@experimental'); |
| gen.write('class ${name}'); |
| if (isContentOverlay) gen.write(' extends ContentOverlayType'); |
| if (callParam) gen.write(' implements Jsonable'); |
| gen.writeln(' {'); |
| gen.writeln('static ${name} parse(Map m) {'); |
| gen.writeln('if (m == null) return null;'); |
| gen.write('return new ${name}('); |
| gen.write(_fields.map((Field field) { |
| String val = "m['${field.name}']"; |
| if (field.optional) { |
| return "${field.name}: ${field.type.jsonConvert(val)}"; |
| } else { |
| return field.type.jsonConvert(val); |
| } |
| }).join(', ')); |
| gen.writeln(');'); |
| gen.writeln('}'); |
| |
| if (_fields.isNotEmpty) { |
| gen.writeln(); |
| _fields.forEach((field) { |
| gen.writeln(); |
| gen.writeDocs(field.docs); |
| if (field.deprecated) { |
| gen.write('@deprecated '); |
| } |
| |
| if (field.optional) gen.write('@optional '); |
| gen.writeln('final ${field.type} ${field.name};'); |
| }); |
| } |
| |
| gen.writeln(); |
| gen.write('${name}('); |
| gen.write(_fields.map((field) { |
| StringBuffer buf = new StringBuffer(); |
| if (field.optional && fields.firstWhere((a) => a.optional) == field) |
| buf.write('{'); |
| buf.write('this.${field.name}'); |
| if (field.optional && fields.lastWhere((a) => a.optional) == field) |
| buf.write('}'); |
| return buf.toString(); |
| }).join(', ')); |
| if (isContentOverlay) { |
| String type = name |
| .substring(0, name.length - 'ContentOverlay'.length) |
| .toLowerCase(); |
| gen.writeln(") : super('$type');"); |
| } else { |
| gen.writeln(');'); |
| } |
| |
| if (callParam) { |
| gen.writeln(); |
| String map = fields.map((f) { |
| if (f.isJsonable && f.type.typeName != 'String') { |
| return "'${f.name}': ${f.name}?.toMap()"; |
| } |
| return "'${f.name}': ${f.name}"; |
| }).join(', '); |
| gen.writeln("Map toMap() => _stripNullValues({${map}});"); |
| } |
| |
| if (hasEquals) { |
| gen.writeln(); |
| String str = fields.map((f) => "${f.name} == o.${f.name}").join(' && '); |
| gen.writeln("bool operator==(o) => o is ${name} && ${str};"); |
| gen.writeln(); |
| String str2 = fields |
| .where((f) => !f.optional) |
| .map((f) => "${f.name}.hashCode") |
| .join(' ^ '); |
| gen.writeln("int get hashCode => ${str2};"); |
| } |
| |
| if (hasToString) { |
| gen.writeln(); |
| String str = fields |
| .where((f) => (!f.optional && !f.deprecated)) |
| .map((f) => "${f.name}: \${${f.name}}") |
| .join(', '); |
| gen.writeln("String toString() => '[${name} ${str}]';"); |
| } |
| |
| gen.writeln('}'); |
| } |
| |
| bool get hasEquals => _shouldHaveEquals.contains(name); |
| |
| bool get hasToString => _shouldHaveToString.contains(name); |
| |
| String toString() => 'TypeDef ${name}'; |
| } |
| |
| abstract class Type { |
| String get typeName; |
| |
| static Type create(Element element) { |
| // <ref>String</ref>, or list, or map |
| if (element.localName == 'ref') { |
| String text = element.text; |
| if (text == 'int' || |
| text == 'bool' || |
| text == 'String' || |
| text == 'long') { |
| return new PrimitiveType(text); |
| } else { |
| return new RefType(text); |
| } |
| } else if (element.localName == 'list') { |
| return new ListType(element.children.first); |
| } else if (element.localName == 'map') { |
| return new MapType(element.children[0].children.first, |
| element.children[1].children.first); |
| } else if (element.localName == 'union') { |
| return new PrimitiveType('dynamic'); |
| } else { |
| throw 'unknown type: ${element}'; |
| } |
| } |
| |
| String jsonConvert(String ref); |
| |
| void setCallParam(); |
| |
| bool isCallParam() => false; |
| |
| bool get isMap => typeName == 'Map' || typeName.startsWith('Map<'); |
| |
| String toString() => typeName; |
| } |
| |
| class ListType extends Type { |
| Type subType; |
| |
| ListType(Element element) : subType = Type.create(element); |
| |
| String get typeName => 'List<${subType.typeName}>'; |
| |
| String jsonConvert(String ref) { |
| if (subType is PrimitiveType) { |
| return "${ref} == null ? null : new List.from(${ref})"; |
| } |
| |
| if (subType is RefType && (subType as RefType).isString) { |
| return "${ref} == null ? null : new List.from(${ref})"; |
| } |
| |
| return "${ref} == null ? null : new List.from(${ref}.map((obj) => ${subType.jsonConvert('obj')}))"; |
| } |
| |
| void setCallParam() => subType.setCallParam(); |
| } |
| |
| class MapType extends Type { |
| Type key; |
| Type value; |
| |
| MapType(Element keyElement, Element valueElement) { |
| key = Type.create(keyElement); |
| value = Type.create(valueElement); |
| } |
| |
| String get typeName => 'Map<${key.typeName}, ${value.typeName}>'; |
| |
| String jsonConvert(String ref) => ref; |
| |
| void setCallParam() { |
| key.setCallParam(); |
| value.setCallParam(); |
| } |
| } |
| |
| class RefType extends Type { |
| String text; |
| TypeDef ref; |
| |
| RefType(this.text); |
| |
| bool get isString { |
| if (ref == null) _resolve(); |
| return ref.isString; |
| } |
| |
| String get typeName { |
| if (ref == null) _resolve(); |
| return ref.isString ? 'String' : ref.name; |
| } |
| |
| String jsonConvert(String r) { |
| if (ref == null) _resolve(); |
| if (ref.name == 'RefactoringFeedback') { |
| return '${ref.name}.parse(kind, ${r})'; |
| } |
| return ref.isString ? r : '${ref.name}.parse(${r})'; |
| } |
| |
| void setCallParam() { |
| if (ref == null) _resolve(); |
| ref.setCallParam(); |
| } |
| |
| bool isCallParam() => ref.isCallParam(); |
| |
| void _resolve() { |
| try { |
| ref = api.findRef(text); |
| } catch (e) { |
| print("can't resolve ${text}"); |
| rethrow; |
| } |
| } |
| } |
| |
| class PrimitiveType extends Type { |
| final String type; |
| |
| PrimitiveType(this.type); |
| |
| String get typeName => type == 'long' ? 'int' : type; |
| |
| String jsonConvert(String ref) => ref; |
| |
| void setCallParam() {} |
| } |
| |
| class _ConcatTextVisitor extends TreeVisitor { |
| final StringBuffer buffer = new StringBuffer(); |
| |
| String toString() => buffer.toString(); |
| |
| visitText(Text node) { |
| buffer.write(node.data); |
| } |
| |
| visitElement(Element node) { |
| if (node.localName == 'b') { |
| buffer.write('**${node.text}**'); |
| } else if (node.localName == 'a') { |
| buffer.write('(${node.text})[${node.attributes['href']}]'); |
| } else if (node.localName == 'tt') { |
| buffer.write('`${node.text}`'); |
| } else { |
| visitChildren(node); |
| } |
| } |
| } |
| |
| final RegExp _wsRegexp = new RegExp(r'\s+', multiLine: true); |
| |
| String _collectDocs(Element element) { |
| String str = |
| element.children.where((e) => e.localName == 'p').map((Element e) { |
| _ConcatTextVisitor visitor = new _ConcatTextVisitor(); |
| visitor.visit(e); |
| return visitor.toString().trim().replaceAll(_wsRegexp, ' '); |
| }).join('\n\n'); |
| return str.isEmpty ? null : str; |
| } |
| |
| final String _headerCode = r''' |
| // Copyright (c) 2017, Devon Carew. 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. |
| |
| // This is a generated file. |
| |
| /// A library to access the analysis server API. |
| /// |
| /// [AnalysisServer] is the main entry-point to this library. |
| library analysis_server_lib; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:logging/logging.dart'; |
| import 'package:path/path.dart' as path; |
| |
| /// @optional |
| const String optional = 'optional'; |
| |
| /// @experimental |
| const String experimental = 'experimental'; |
| |
| final Logger _logger = new Logger('analysis_server'); |
| |
| '''; |
| |
| final String _staticFactory = r''' |
| /// Create and connect to a new analysis server instance. |
| /// |
| /// - [sdkPath] override the default sdk path |
| /// - [scriptPath] override the default entry-point script to use for the |
| /// analysis server |
| /// - [onRead] called every time data is read from the server |
| /// - [onWrite] called every time data is written to the server |
| static Future<AnalysisServer> create( |
| {String sdkPath, String scriptPath, |
| void onRead(String str), void onWrite(String str), |
| List<String> vmArgs, List<String> serverArgs, |
| String clientId, String clientVersion, |
| Map<String, String> processEnvironment}) async { |
| Completer<int> processCompleter = new Completer(); |
| |
| String vmPath; |
| if (sdkPath != null) { |
| vmPath = path.join(sdkPath, 'bin', Platform.isWindows ? 'dart.exe' : 'dart'); |
| } else { |
| sdkPath = path.dirname(path.dirname(Platform.resolvedExecutable)); |
| vmPath = Platform.resolvedExecutable; |
| } |
| scriptPath ??= '$sdkPath/bin/snapshots/analysis_server.dart.snapshot'; |
| |
| List<String> args = [scriptPath, '--sdk', sdkPath]; |
| if (vmArgs != null) args.insertAll(0, vmArgs); |
| if (serverArgs != null) args.addAll(serverArgs); |
| if (clientId != null) args.add('--client-id=$clientId'); |
| if (clientVersion != null) args.add('--client-version=$clientVersion'); |
| |
| Process process = await Process.start(vmPath, args, environment: processEnvironment); |
| process.exitCode.then((code) => processCompleter.complete(code)); |
| |
| Stream<String> inStream = process.stdout |
| .transform(utf8.decoder) |
| .transform(const LineSplitter()) |
| .map((String message) { |
| if (onRead != null) onRead(message); |
| return message; |
| }); |
| |
| AnalysisServer server = new AnalysisServer(inStream, (String message) { |
| if (onWrite != null) onWrite(message); |
| process.stdin.writeln(message); |
| }, processCompleter, process.kill); |
| |
| return server; |
| } |
| |
| '''; |
| |
| final String _serverCode = r''' |
| Stream<String> get onSend => _onSend.stream; |
| Stream<String> get onReceive => _onReceive.stream; |
| |
| set willSend(MethodSend fn) { |
| _willSend = fn; |
| } |
| |
| void configure(Stream<String> inStream, void writeMessage(String message)) { |
| _streamSub = inStream.listen(_processMessage); |
| _writeMessage = writeMessage; |
| } |
| |
| void dispose() { |
| if (_streamSub != null) _streamSub.cancel(); |
| //_completers.values.forEach((c) => c.completeError('disposed')); |
| _completers.clear(); |
| |
| if (_processKillHandler != null) { |
| _processKillHandler(); |
| } |
| } |
| |
| void _processMessage(String message) { |
| _onReceive.add(message); |
| |
| if (!message.startsWith('{')) { |
| _logger.warning('unknown message: ${message}'); |
| return; |
| } |
| |
| try { |
| var json = jsonDecode(message); |
| |
| if (json['id'] == null) { |
| // Handle a notification. |
| String event = json['event']; |
| if (event == null) { |
| _logger.severe('invalid message: ${message}'); |
| } else { |
| String prefix = event.substring(0, event.indexOf('.')); |
| if (_domains[prefix] == null) { |
| _logger.severe('no domain for notification: ${message}'); |
| } else { |
| _domains[prefix]._handleEvent(event, json['params']); |
| } |
| } |
| } else { |
| Completer completer = _completers.remove(json['id']); |
| String methodName = _methodNames.remove(json['id']); |
| |
| if (completer == null) { |
| _logger.severe('unmatched request response: ${message}'); |
| } else if (json['error'] != null) { |
| completer.completeError(RequestError.parse(methodName, json['error'])); |
| } else { |
| completer.complete(json['result']); |
| } |
| } |
| } catch (e) { |
| _logger.severe('unable to decode message: ${message}, ${e}'); |
| } |
| } |
| |
| Future<Map> _call(String method, [Map args]) { |
| String id = '${++_id}'; |
| _completers[id] = new Completer<Map>(); |
| _methodNames[id] = method; |
| Map m = {'id': id, 'method': method}; |
| if (args != null) m['params'] = args; |
| String message = _jsonEncoder.encode(m); |
| if (_willSend != null) _willSend(method); |
| _onSend.add(message); |
| _writeMessage(message); |
| return _completers[id].future; |
| } |
| |
| static dynamic _toEncodable(obj) => obj is Jsonable ? obj.toMap() : obj; |
| '''; |
| |
| final String _domainCode = r''' |
| abstract class Domain { |
| final AnalysisServer server; |
| final String name; |
| |
| Map<String, StreamController<Map>> _controllers = {}; |
| Map<String, Stream> _streams = {}; |
| |
| Domain(this.server, this.name) { |
| server._domains[name] = this; |
| } |
| |
| Future<Map> _call(String method, [Map args]) => server._call(method, args); |
| |
| Stream<E> _listen<E>(String name, E cvt(Map m)) { |
| if (_streams[name] == null) { |
| _controllers[name] = new StreamController<Map>.broadcast(); |
| _streams[name] = _controllers[name].stream.map<E>(cvt); |
| } |
| |
| return _streams[name]; |
| } |
| |
| void _handleEvent(String name, dynamic event) { |
| if (_controllers[name] != null) { |
| _controllers[name].add(event); |
| } |
| } |
| |
| String toString() => 'Domain ${name}'; |
| } |
| |
| abstract class Jsonable { |
| Map toMap(); |
| } |
| |
| abstract class RefactoringOptions implements Jsonable { |
| } |
| |
| abstract class ContentOverlayType { |
| final String type; |
| |
| ContentOverlayType(this.type); |
| } |
| |
| class RequestError { |
| static RequestError parse(String method, Map m) { |
| if (m == null) return null; |
| return new RequestError(method, m['code'], m['message'], stackTrace: m['stackTrace']); |
| } |
| |
| final String method; |
| final String code; |
| final String message; |
| @optional final String stackTrace; |
| |
| RequestError(this.method, this.code, this.message, {this.stackTrace}); |
| |
| String toString() => '[Analyzer RequestError method: ${method}, code: ${code}, message: ${message}]'; |
| } |
| |
| Map _stripNullValues(Map m) { |
| Map copy = {}; |
| |
| for (var key in m.keys) { |
| var value = m[key]; |
| if (value != null) copy[key] = value; |
| } |
| |
| return copy; |
| } |
| '''; |