-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathmeta_model_reader.dart
375 lines (326 loc) · 12.4 KB
/
meta_model_reader.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
// Copyright (c) 2022, 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 'dart:io';
import 'package:analysis_server/src/utilities/strings.dart';
import 'package:collection/collection.dart';
import 'meta_model.dart';
/// Reads the LSP 'meta_model.json' file and returns its types.
class LspMetaModelReader {
final _types = <LspEntity>[];
/// A set of names already used (or reserved) by types that have been read.
final Set<String> _typeNames = {};
/// Characters to strip from member names.
final _memberNameInvalidCharPattern = RegExp(r'\$_?');
/// Patterns to replace with '_' in member names.
final _memberNameSeparatorPattern = RegExp(r'/');
/// Gets all types that have been read from the model JSON.
List<LspEntity> get types => _types.toList();
/// Reads all spec types from [file].
LspMetaModel readFile(File file) {
var modelJson = file.readAsStringSync();
var model = jsonDecode(modelJson) as Map<String, Object?>;
return readMap(model);
}
/// Reads all spec types from [model].
LspMetaModel readMap(Map<String, dynamic> model) {
var requests = model['requests'] as List?;
var notifications = model['notifications'] as List?;
var structures = model['structures'] as List?;
var enums = model['enumerations'] as List?;
var typeAliases = model['typeAliases'] as List?;
var methods = [...?requests, ...?notifications].toList();
[
...?structures?.map(_readStructure),
...?enums?.map((e) => _readEnum(e)),
...?typeAliases?.map(_readTypeAlias),
].forEach(_addType);
// Requests and notifications may have inline union types as their
// params/result. We can create TypeAliases for those using sensible
// names to simplify their use in the handlers.
requests?.forEach(_readRequest);
notifications?.forEach(_readNotification);
var methodsEnum = _createMethodsEnum(methods);
if (methodsEnum != null) {
_addType(methodsEnum);
}
return LspMetaModel(
types: types,
methods: methodsEnum?.members.cast<Constant>().toList() ?? [],
);
}
/// Adds [type] to the current list and prevents its name from being used
/// by generated interfaces.
void _addType(LspEntity type) {
_typeNames.add(type.name);
_types.add(type);
}
String _camelCase(String str) =>
str.substring(0, 1).toLowerCase() + str.substring(1);
/// Creates an enum for all LSP method names.
LspEnum? _createMethodsEnum(List<Object?> methods) {
Constant toConstant(Map<String, Object?> item) {
var name = item['method'] as String;
// We use documentation from the request/notification for things like
// proposed check, but we don't put the full request/notification docs
// on the method enum member.
var documentation = item['documentation'] as String?;
var comment = '''Constant for the '$name' method.''';
return Constant(
name: _generateMemberName(name, camelCase: true),
comment: comment,
isProposed: _isProposed(documentation),
type: TypeReference.string,
value: name,
);
}
var methodConstants =
methods.cast<Map<String, Object?>>().map(toConstant).toList();
if (methodConstants.isEmpty) {
return null;
}
var comment = 'All standard LSP Methods read from the JSON spec.';
return LspEnum(
name: 'Method',
comment: comment,
typeOfValues: TypeReference.string,
members: methodConstants,
);
}
/// Creates a type alias for a top-level union, such as those used for
/// request parameters/results that don't have named types in the spec.
void _createUnionAlias(String name, dynamic model, String? documentation) {
if (model == null) {
return;
}
// We don't currently support reading the two top-level intersection types.
// These can just be skipped because the types we're generating here are
// just for convenience (to produce better names for use in handlers rather
// than referencing `EitherX<Y>` everywhere).
if (model['kind'] == 'and') {
return;
}
var type = _extractType(name, '', model);
if (type is UnionType) {
_addType(
TypeAlias(
name: name,
comment: documentation,
isProposed: _isProposed(documentation),
baseType: type,
renameReferences: false,
),
);
}
}
Constant _extractEnumValue(TypeBase parentType, dynamic model) {
var name = model['name'] as String;
var documentation = model['documentation'] as String?;
return Constant(
name: _generateMemberName(name),
comment: documentation,
isProposed: _isProposed(documentation),
type: parentType,
value: model['value'].toString(),
);
}
Member _extractMember(String parentName, dynamic model) {
var name = model['name'] as String;
var documentation = model['documentation'] as String?;
var type = _extractType(parentName, name, model['type']);
// Unions may contain `null` types which we promote up to the field.
var allowsNull = false;
if (type is UnionType) {
var types = type.types;
// Extract and strip `null`s from the union.
if (types.any(isNullType)) {
allowsNull = true;
type = UnionType(types.whereNot(isNullType).toList());
}
}
return Field(
name: _generateMemberName(name),
comment: documentation,
isProposed: _isProposed(documentation),
type: type,
allowsNull: allowsNull,
allowsUndefined: model['optional'] == true,
);
}
/// Reads the type of [model].
TypeBase _extractType(String parentName, String? fieldName, dynamic model) {
if (model['kind'] == 'reference' || model['kind'] == 'base') {
// Reference kinds are other named interfaces defined in the spec, base are
// other named types defined elsewhere.
return TypeReference(model['name'] as String);
} else if (model['kind'] == 'array') {
return ArrayType(_extractType(parentName, fieldName, model['element']!));
} else if (model['kind'] == 'map') {
var name = fieldName ?? '';
return MapType(
_extractType(parentName, '${name}Key', model['key']!),
_extractType(parentName, '${name}Value', model['value']!),
);
} else if (model['kind'] == 'literal') {
// "Literal" here means an inline/anonymous type.
var inlineTypeName = _generateTypeName(parentName, fieldName ?? '');
// First record the definition of the anonymous type itself.
var members =
(model['value']['properties'] as List)
.map((p) => _extractMember(inlineTypeName, p))
.toList();
_addType(Interface.inline(inlineTypeName, members));
// Then return its name.
return TypeReference(inlineTypeName);
} else if (model['kind'] == 'stringLiteral') {
return LiteralType(TypeReference.string, model['value'] as String);
} else if (model['kind'] == 'or') {
// Ensure the parent name is reserved so we don't try to reuse its name
// if we're parsing something without a field name.
_typeNames.add(parentName);
var itemTypes = model['items'] as List;
var types =
itemTypes.map((item) {
var generatedName = _generateAvailableTypeName(
parentName,
fieldName,
);
return _extractType(generatedName, null, item);
}).toList();
return UnionType(types);
} else if (model['kind'] == 'tuple') {
// We currently just map tuples to an array of any of the types. The
// LSP 3.17 spec only has one tuple which is `[number, number]`.
var itemTypes = model['items'] as List;
var types =
itemTypes.mapIndexed((index, item) {
var suffix = index + 1;
var name = fieldName ?? '';
var thisName = '$name$suffix';
return _extractType(parentName, thisName, item);
}).toList();
return ArrayType(UnionType(types));
} else {
throw 'Unable to extract type from $model';
}
}
/// Generates an available name for a node.
///
/// If the computed name is already used, a number will be appended to the
/// end.
String _generateAvailableTypeName(String containerName, String? fieldName) {
var name = _generateTypeName(containerName, fieldName ?? '');
var requiresSuffix = fieldName == null;
// If the name has already been taken, try appending a number and try
// again.
String generatedName;
var suffixIndex = 1;
do {
if (suffixIndex > 20) {
throw 'Failed to generate an available name for $name';
}
generatedName =
requiresSuffix || suffixIndex > 1 ? '$name$suffixIndex' : name;
suffixIndex++;
} while (_typeNames.contains(generatedName));
return generatedName;
}
/// Generates a valid name for a member.
String _generateMemberName(String name, {bool camelCase = false}) {
// Replace any separators like `/` with `_`.
name = name.replaceAll(_memberNameSeparatorPattern, '_');
// Replace out any characters we don't want in member names.
name = name.replaceAll(_memberNameInvalidCharPattern, '');
// TODO(dantup): Remove this condition and always do camelCase in a future
// CL to reduce the migration diff.
if (camelCase) {
name = _camelCase(name);
}
return name;
}
/// Generates a valid name for a type.
String _generateTypeName(String parent, String child) {
// Some classes are private (`_InitializeParams`) but still exposed via
// other classes (`InitializeParams`) but the child types still need to be
// exposed, so remove any leading underscores.
if (parent.startsWith('_')) {
parent = parent.substring(1);
}
return '${capitalize(parent)}${capitalize(child)}';
}
bool _isProposed(String? documentation) {
return documentation?.contains('@proposed') ?? false;
}
LspEnum _readEnum(dynamic model) {
var name = model['name'] as String;
var type = TypeReference(name);
var baseType = _extractType(name, null, model['type']);
var documentation = model['documentation'] as String?;
return LspEnum(
name: name,
comment: documentation,
isProposed: _isProposed(documentation),
typeOfValues: baseType,
members: [
...?(model['values'] as List?)?.map((p) => _extractEnumValue(type, p)),
],
);
}
void _readNotification(dynamic model) {
var method = model['method'] as String;
var namePrefix = method.split('/').map(capitalize).join();
var documentation = model['documentation'] as String?;
var paramsDoc =
documentation != null
? 'Parameters for ${_camelCase(documentation)}'
: null;
_createUnionAlias('${namePrefix}Params', model['params'], paramsDoc);
}
void _readRequest(dynamic model) {
var method = model['method'] as String;
var namePrefix = method.split('/').map(capitalize).join();
var documentation = model['documentation'] as String?;
var paramsDoc =
documentation != null
? 'Parameters for ${_camelCase(documentation)}'
: null;
var resultDoc =
documentation != null
? 'Result for ${_camelCase(documentation)}'
: null;
_createUnionAlias('${namePrefix}Params', model['params'], paramsDoc);
_createUnionAlias('${namePrefix}Result', model['result'], resultDoc);
}
LspEntity _readStructure(dynamic model) {
var name = model['name'] as String;
var documentation = model['documentation'] as String?;
return Interface(
name: name,
comment: documentation,
isProposed: _isProposed(documentation),
baseTypes: [
...?(model['extends'] as List?)?.map(
(e) => TypeReference(e['name'] as String),
),
...?(model['mixins'] as List?)?.map(
(e) => TypeReference(e['name'] as String),
),
],
members: [
...?(model['properties'] as List?)?.map((p) => _extractMember(name, p)),
],
);
}
TypeAlias _readTypeAlias(dynamic model) {
var name = model['name'] as String;
var documentation = model['documentation'] as String?;
return TypeAlias(
name: name,
comment: documentation,
isProposed: _isProposed(documentation),
baseType: _extractType(name, null, model['type']),
renameReferences: false,
);
}
}