blob: 017c666b836c2485f3941668423e483e42588d9d [file] [log] [blame]
// Copyright (c) 2018, 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.
// TODO(dantup): Regex seemed like a good choice when parsing the first few...
// maybe it's not so great now. We should parse this properly if it turns out
// there are issues with what we have here.
const String _blockBody = r'\{([\s\S]*?)\s*\n\s*\}';
const String _comment = r'(?:\/\*\*((?:[\S\s](?!\*\/))+?)\s\*\/)?\s*';
List<ApiItem> extractAllTypes(List<String> code) {
return extractTypes(code.join('\n'));
}
List<ApiItem> extractTypes(String code) {
final types = ApiItem.extractFrom(code);
_removeUnwantedTypes(types);
return types;
}
String _cleanComment(String comment) {
if (comment == null) {
return null;
}
final _commentLinePrefixes = new RegExp(r'\n\s*\* ?');
final _nonConcurrentNewlines = new RegExp(r'\n(?![\n\s\-*])');
final _newLinesThatRequireReinserting = new RegExp(r'\n (\w)');
// Remove any Windows newlines from the source.
comment = comment.replaceAll('\r', '');
// Remove the * prefixes.
comment = comment.replaceAll(_commentLinePrefixes, '\n');
// Remove and newlines that look like wrapped text.
comment = comment.replaceAll(_nonConcurrentNewlines, ' ');
// The above will remove one of the newlines when there are two, so we need
// to re-insert newlines for any block that starts immediately after a newline.
comment = comment.replaceAllMapped(
_newLinesThatRequireReinserting, (m) => '\n\n${m.group(1)}');
return comment.trim();
}
/// Fixes up some enum types that are not as specific as they could be in the
/// spec. For example, Diagnostic.severity is typed "number" but can be mapped
/// to the DiagnosticSeverity enum class.
String _getImprovedType(String interfaceName, String fieldName) {
const Map<String, Map<String, String>> _improvedTypeMappings = {
"Diagnostic": {
"severity": "DiagnosticSeverity",
},
"TextDocumentSyncOptions": {
"change": "TextDocumentSyncKind",
},
"FileSystemWatcher": {
"kind": "WatchKind",
},
"CompletionItem": {
"kind": "CompletionItemKind",
},
"DocumentHighlight": {
"kind": "DocumentHighlightKind",
},
"FoldingRange": {
"kind": "FoldingRangeKind",
},
};
final interface = _improvedTypeMappings[interfaceName];
return interface != null ? interface[fieldName] : null;
}
List<String> _getSpecialBaseClasses(String name) {
const fileOperationTypes = [
'TextDocumentEdit',
'CreateFile',
'RenameFile',
'DeleteFile'
];
if (fileOperationTypes.contains(name)) {
return ['FileOperation'];
} else {
return [];
}
}
List<String> _parseTypes(String baseTypes, String sep) {
// Special case for a single complicated type we can't parse easily...
if (baseTypes ==
'(TextDocumentEdit[] | (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[])') {
return ['FileOperation[]'];
}
return baseTypes?.split(sep)?.map((t) => t.trim())?.toList() ?? [];
}
/// Removes types that are in the spec that we don't want.
void _removeUnwantedTypes(List<ApiItem> types) {
// These types are not used for v3.0 (Feb 2017) and by dropping them we don't
// have to handle any cases where both a namespace and interfaces are declared
// with the same name.
types.removeWhere((item) => item.name == 'InitializeError');
}
/// Base class for Interface, Field, Constant, etc. parsed from the LSP spec.
abstract class ApiItem {
String name, comment;
bool isDeprecated;
ApiItem(this.name, String comment)
: comment = _cleanComment(comment),
isDeprecated = comment?.contains('@deprecated') ?? false;
static List<ApiItem> extractFrom(String code) {
List<ApiItem> types = [];
types.addAll(Interface.extractFrom(code));
types.addAll(Namespace.extractFrom(code));
types.addAll(TypeAlias.extractFrom(code));
return types;
}
}
/// A Constant parsed from the LSP spec.
class Const extends Member {
final String type, value;
Const(String name, String comment, this.type, this.value)
: super(name, comment);
static List<Const> extractFrom(String code) {
final RegExp _constPattern = new RegExp(_comment +
r'''(?:export\s+)?const\s+(\w+)(?::\s+([\w\[\]'".-]+?))?\s*=\s*([\w\[\]'".-]+)\s*(?:;|$)''');
final consts = _constPattern.allMatches(code).map((m) {
final String comment = m.group(1);
final String name = m.group(2);
final String type = m.group(3);
final String value = m.group(4);
return new Const(name, comment, type, value);
}).toList();
return consts;
}
static List<Const> extractFromEnumValue(String code) {
final RegExp _constPattern =
new RegExp(_comment + r'''(\w+)\s*=\s*([\w\[\]'".-]+)\s*(?:,|$)''');
final consts = _constPattern.allMatches(code).map((m) {
final String comment = m.group(1);
final String name = m.group(2);
final String value = m.group(3);
return new Const(name, comment, null, value);
}).toList();
return consts;
}
}
/// A Field for an Interface parsed from the LSP spec.
class Field extends Member {
final List<String> types;
final bool allowsNull, allowsUndefined;
Field(String name, String comment, this.types, this.allowsNull,
this.allowsUndefined)
: super(name, comment);
static List<Field> extractFrom(String interfaceName, String code) {
final RegExp _fieldPattern = new RegExp(_comment +
r'([\w\[\]]+\??)\s*:\s*([\w\[\] \|\{\}\(\)<>:;]+)\s*(?:;|$)');
final fields = _fieldPattern.allMatches(code).where((m) {
// Skip over the indexer in FormattingOptions since we don't need this
// (for now) and it's complicated to represent.
if (m.group(0).contains('[key: string]: boolean | number | string;')) {
return false;
}
return true;
}).map((m) {
String comment = m.group(1);
String name = m.group(2);
String typesString = m.group(3).trim();
// Our regex may result in semicolons on the end...
// TODO(dantup): Fix this, or make a simple parser.
if (typesString.endsWith(';')) {
typesString = typesString.substring(0, typesString.length - 1);
}
// Some fields have weird comments like this in the spec:
// {@link MessageType}
// These seem to be the correct type of the field, while the field is
// marked with number.
if (comment != null) {
final RegExp _linkTypePattern = new RegExp(r'See \{@link (\w+)\}\.?');
final linkTypeMatch = _linkTypePattern.firstMatch(comment);
if (linkTypeMatch != null) {
typesString = linkTypeMatch.group(1);
comment = comment.replaceAll(_linkTypePattern, '');
}
}
List<String> types = _parseTypes(typesString, '|');
final bool allowsNull = types.contains('null');
if (allowsNull) {
types.remove('null');
}
final bool allowsUndefined = name.endsWith('?');
if (allowsUndefined) {
name = name.substring(0, name.length - 1);
}
// Perform simple type improvements for enums values that are typed as
// num/string in the spec but are enums.
// the spec.
if (types.length == 1) {
types[0] = _getImprovedType(interfaceName, name) ?? types[0];
}
return new Field(name, comment, types, allowsNull, allowsUndefined);
}).toList();
return fields;
}
}
/// An Interface parsed from the LSP spec.
class Interface extends ApiItem {
final List<String> baseTypes;
final List<Member> members;
Interface(String name, String comment, this.baseTypes, this.members)
: super(name, comment);
static List<Interface> extractFrom(String code) {
final RegExp _interfacePattern = new RegExp(_comment +
r'(?:export\s+)?(?:interface|class)\s+([\w<>]+)(?:\s+extends\s+([\w, ]+?))?\s*' +
_blockBody);
final interfaces = _interfacePattern.allMatches(code).map((match) {
final String comment = match.group(1);
final String name = match.group(2);
final List<String> baseTypes = _parseTypes(match.group(3), ',');
final String body = match.group(4);
final List<Member> members = Member.extractFrom(name, body);
// Add any special base classes we've added to simplify types.
baseTypes.addAll(_getSpecialBaseClasses(name));
return new Interface(name, comment, baseTypes, members);
}).toList();
return interfaces;
}
}
/// A Field or Constant parsed from the LSP type.
abstract class Member extends ApiItem {
Member(String name, String comment) : super(name, comment);
static List<Member> extractFrom(String interfaceName, String code) {
List<Member> members = [];
members.addAll(Field.extractFrom(interfaceName, code));
members.addAll(Const.extractFrom(code));
return members;
}
}
/// An Enum or Namsepace containing constants parsed from the LSP spec.
class Namespace extends ApiItem {
final List<Member> members;
Namespace(String name, String comment, this.members) : super(name, comment);
static List<Namespace> extractFrom(String code) {
final enums = <Namespace>[];
enums.addAll(_extractNamespacesFrom(code));
enums.addAll(_extractEnumsFrom(code));
return enums;
}
static List<Namespace> _extractEnumsFrom(String code) {
final RegExp _namespacePattern =
new RegExp(_comment + r'(?:export\s+)?enum\s+(\w+)\s*' + _blockBody);
final namespaces = _namespacePattern.allMatches(code).map((match) {
final String comment = match.group(1);
final String name = match.group(2);
final String body = match.group(3);
final List<Member> members = Const.extractFromEnumValue(body);
return new Namespace(name, comment, members);
}).toList();
return namespaces;
}
static List<Namespace> _extractNamespacesFrom(String code) {
final RegExp _namespacePattern = new RegExp(
_comment + r'(?:export\s+)?namespace\s+(\w+)\s*' + _blockBody);
final namespaces = _namespacePattern.allMatches(code).map((match) {
final String comment = match.group(1);
final String name = match.group(2);
final String body = match.group(3);
final List<Member> members = Member.extractFrom(name, body);
return new Namespace(name, comment, members);
}).toList();
return namespaces;
}
}
/// A type alias parsed from the LSP spec.
class TypeAlias extends ApiItem {
final String baseType;
TypeAlias(name, comment, this.baseType) : super(name, comment);
static List<TypeAlias> extractFrom(String code) {
final RegExp _typeAliasPattern =
new RegExp(_comment + r'type\s+([\w]+)\s+=\s+([\w\[\]]+)\s*;');
final typeAliases = _typeAliasPattern.allMatches(code).map((match) {
final String comment = match.group(1);
final String name = match.group(2);
final String baseType = match.group(3);
return new TypeAlias(name, comment, baseType);
}).toList();
return typeAliases;
}
}