| // 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; |
| } |
| } |