77466f2b83
Change-Id: I52c7d16bdaf9f68148a8d7a956330bc54608ec43 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/195980 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
360 lines
12 KiB
Dart
360 lines
12 KiB
Dart
// 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.
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:analysis_server/src/utilities/strings.dart';
|
|
import 'package:args/args.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'codegen_dart.dart';
|
|
import 'markdown.dart';
|
|
import 'typescript.dart';
|
|
import 'typescript_parser.dart';
|
|
|
|
Future<void> main(List<String> arguments) async {
|
|
final args = argParser.parse(arguments);
|
|
if (args[argHelp]) {
|
|
print(argParser.usage);
|
|
return;
|
|
}
|
|
|
|
final script = Platform.script.toFilePath();
|
|
// 3x parent = file -> lsp_spec -> tool -> analysis_server.
|
|
final packageFolder = File(script).parent.parent.parent.path;
|
|
final outFolder = path.join(packageFolder, 'lib', 'lsp_protocol');
|
|
Directory(outFolder).createSync();
|
|
|
|
// Collect definitions for types in the spec and our custom extensions.
|
|
final specTypes = await getSpecClasses(args);
|
|
final customTypes = getCustomClasses();
|
|
|
|
// Record both sets of types in dictionaries for faster lookups, but also so
|
|
// they can reference each other and we can find the definitions during
|
|
// codegen.
|
|
recordTypes(specTypes);
|
|
recordTypes(customTypes);
|
|
|
|
// Generate formatted Dart code (as a string) for each set of types.
|
|
final specTypesOutput = generateDartForTypes(specTypes);
|
|
final customTypesOutput = generateDartForTypes(customTypes);
|
|
|
|
File(path.join(outFolder, 'protocol_generated.dart')).writeAsStringSync(
|
|
generatedFileHeader(2018, importCustom: true) + specTypesOutput);
|
|
File(path.join(outFolder, 'protocol_custom_generated.dart'))
|
|
.writeAsStringSync(generatedFileHeader(2019) + customTypesOutput);
|
|
}
|
|
|
|
const argDownload = 'download';
|
|
|
|
const argHelp = 'help';
|
|
|
|
final argParser = ArgParser()
|
|
..addFlag(argHelp, hide: true)
|
|
..addFlag(argDownload,
|
|
negatable: false,
|
|
abbr: 'd',
|
|
help:
|
|
'Download the latest version of the LSP spec before generating types');
|
|
|
|
final String localSpecPath = path.join(
|
|
path.dirname(Platform.script.toFilePath()), 'lsp_specification.md');
|
|
|
|
final Uri specLicenseUri = Uri.parse(
|
|
'https://raw.githubusercontent.com/Microsoft/language-server-protocol/gh-pages/License.txt');
|
|
|
|
/// The URI of the version of the spec to generate from. This should be periodically updated as
|
|
/// there's no longer a stable URI for the latest published version.
|
|
final Uri specUri = Uri.parse(
|
|
'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/specification-3-16.md');
|
|
|
|
/// Pattern to extract inline types from the `result: {xx, yy }` notes in the spec.
|
|
/// Doesn't parse past full stops as some of these have english sentences tagged on
|
|
/// the end that we don't want to parse.
|
|
final _resultsInlineTypesPattern = RegExp(r'''\* result:[^\.{}]*({[^\.`]*})''');
|
|
|
|
Future<void> downloadSpec() async {
|
|
final specResp = await http.get(specUri);
|
|
final licenseResp = await http.get(specLicenseUri);
|
|
final text = [
|
|
'''
|
|
This is an unmodified copy of the Language Server Protocol Specification,
|
|
downloaded from $specUri. It is the version of the specification that was
|
|
used to generate a portion of the Dart code used to support the protocol.
|
|
|
|
To regenerate the generated code, run the script in
|
|
"analysis_server/tool/lsp_spec/generate_all.dart" with no arguments. To
|
|
download the latest version of the specification before regenerating the
|
|
code, run the same script with an argument of "--download".''',
|
|
licenseResp.body,
|
|
specResp.body
|
|
];
|
|
await File(localSpecPath).writeAsString(text.join('\n\n---\n\n'));
|
|
}
|
|
|
|
Namespace extractMethodsEnum(String spec) {
|
|
Const toConstant(String value) {
|
|
final comment = Comment(
|
|
Token(TokenType.COMMENT, '''Constant for the '$value' method.'''));
|
|
|
|
// Generate a safe name for the member from the string. Those that start with
|
|
// $/ will have the prefix removed and all slashes should be replaced with
|
|
// underscores.
|
|
final safeMemberName = value.replaceAll(r'$/', '').replaceAll('/', '_');
|
|
|
|
return Const(
|
|
comment,
|
|
Token.identifier(safeMemberName),
|
|
Type.identifier('string'),
|
|
Token(TokenType.STRING, "'$value'"),
|
|
);
|
|
}
|
|
|
|
final comment = Comment(Token(TokenType.COMMENT,
|
|
'Valid LSP methods known at the time of code generation from the spec.'));
|
|
final methodConstants = extractMethodNames(spec).map(toConstant).toList();
|
|
|
|
return Namespace(comment, Token.identifier('Method'), methodConstants);
|
|
}
|
|
|
|
/// Extract inline types found directly in the `results:` sections of the spec
|
|
/// that are not declared with their own names elsewhere.
|
|
List<AstNode> extractResultsInlineTypes(String spec) {
|
|
InlineInterface toInterface(String typeDef) {
|
|
// The definition passed here will be a bare inline type, such as:
|
|
//
|
|
// { range: Range, placeholder: string }
|
|
//
|
|
// In order to parse this, we'll just format it as a type alias and then
|
|
// run it through the standard parsing code.
|
|
final typeAlias = 'type temp = ${typeDef.replaceAll(',', ';')};';
|
|
|
|
final parsed = parseString(typeAlias);
|
|
|
|
// Extract the InlineInterface that was created.
|
|
final interface =
|
|
parsed.firstWhere((t) => t is InlineInterface) as InlineInterface;
|
|
|
|
// Create a new name based on the fields.
|
|
var newName = interface.members.map((m) => capitalize(m.name)).join('And');
|
|
|
|
return InlineInterface(newName, interface.members);
|
|
}
|
|
|
|
return _resultsInlineTypesPattern
|
|
.allMatches(spec)
|
|
.map((m) => m.group(1)!.trim())
|
|
.toList()
|
|
.map(toInterface)
|
|
.toList();
|
|
}
|
|
|
|
String generatedFileHeader(int year, {bool importCustom = false}) => '''
|
|
// Copyright (c) $year, 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.
|
|
|
|
// This file has been automatically generated. Please do not edit it manually.
|
|
// To regenerate the file, use the script
|
|
// "pkg/analysis_server/tool/lsp_spec/generate_all.dart".
|
|
|
|
// ignore_for_file: annotate_overrides
|
|
// ignore_for_file: unnecessary_parenthesis
|
|
|
|
import 'dart:core' hide deprecated;
|
|
import 'dart:core' as core show deprecated;
|
|
import 'dart:convert' show JsonEncoder;
|
|
import 'package:analysis_server/lsp_protocol/protocol${importCustom ? '_custom' : ''}_generated.dart';
|
|
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
|
|
import 'package:analysis_server/src/lsp/json_parsing.dart';
|
|
import 'package:analysis_server/src/protocol/protocol_internal.dart';
|
|
import 'package:analyzer/src/generated/utilities_general.dart';
|
|
|
|
const jsonEncoder = JsonEncoder.withIndent(' ');
|
|
|
|
''';
|
|
|
|
List<AstNode> getCustomClasses() {
|
|
Interface interface(String name, List<Member> fields, {String? baseType}) {
|
|
return Interface(
|
|
null,
|
|
Token.identifier(name),
|
|
[],
|
|
[if (baseType != null) Type.identifier(baseType)],
|
|
fields,
|
|
);
|
|
}
|
|
|
|
Field field(
|
|
String name, {
|
|
required String type,
|
|
array = false,
|
|
canBeNull = false,
|
|
canBeUndefined = false,
|
|
}) {
|
|
var fieldType =
|
|
array ? ArrayType(Type.identifier(type)) : Type.identifier(type);
|
|
|
|
return Field(
|
|
null, Token.identifier(name), fieldType, canBeNull, canBeUndefined);
|
|
}
|
|
|
|
final customTypes = <AstNode>[
|
|
interface('DartDiagnosticServer', [field('port', type: 'int')]),
|
|
interface('AnalyzerStatusParams', [field('isAnalyzing', type: 'boolean')]),
|
|
interface('PublishClosingLabelsParams', [
|
|
field('uri', type: 'string'),
|
|
field('labels', type: 'ClosingLabel', array: true)
|
|
]),
|
|
interface('ClosingLabel',
|
|
[field('range', type: 'Range'), field('label', type: 'string')]),
|
|
interface('Element', [
|
|
field('range', type: 'Range', canBeUndefined: true),
|
|
field('name', type: 'string'),
|
|
field('kind', type: 'string'),
|
|
field('parameters', type: 'string', canBeUndefined: true),
|
|
field('typeParameters', type: 'string', canBeUndefined: true),
|
|
field('returnType', type: 'string', canBeUndefined: true),
|
|
]),
|
|
interface('PublishOutlineParams',
|
|
[field('uri', type: 'string'), field('outline', type: 'Outline')]),
|
|
interface('Outline', [
|
|
field('element', type: 'Element'),
|
|
field('range', type: 'Range'),
|
|
field('codeRange', type: 'Range'),
|
|
field('children', type: 'Outline', array: true, canBeUndefined: true)
|
|
]),
|
|
interface('PublishFlutterOutlineParams', [
|
|
field('uri', type: 'string'),
|
|
field('outline', type: 'FlutterOutline')
|
|
]),
|
|
interface('FlutterOutline', [
|
|
field('kind', type: 'string'),
|
|
field('label', type: 'string', canBeUndefined: true),
|
|
field('className', type: 'string', canBeUndefined: true),
|
|
field('variableName', type: 'string', canBeUndefined: true),
|
|
field('attributes',
|
|
type: 'FlutterOutlineAttribute', array: true, canBeUndefined: true),
|
|
field('dartElement', type: 'Element', canBeUndefined: true),
|
|
field('range', type: 'Range'),
|
|
field('codeRange', type: 'Range'),
|
|
field('children',
|
|
type: 'FlutterOutline', array: true, canBeUndefined: true)
|
|
]),
|
|
interface(
|
|
'FlutterOutlineAttribute',
|
|
[
|
|
field('name', type: 'string'),
|
|
field('label', type: 'string'),
|
|
field('valueRange', type: 'Range', canBeUndefined: true),
|
|
],
|
|
),
|
|
interface(
|
|
'CompletionItemResolutionInfo',
|
|
[
|
|
field('file', type: 'string'),
|
|
field('offset', type: 'int'),
|
|
],
|
|
),
|
|
interface(
|
|
'DartCompletionItemResolutionInfo',
|
|
[
|
|
// These fields have short-ish names because they're on the payload
|
|
// for all suggestion-set backed completions.
|
|
field('libId', type: 'int'),
|
|
field('displayUri', type: 'string'),
|
|
field('rOffset', type: 'int'), // replacementOffset
|
|
field('iLength', type: 'int'), // insertLength
|
|
field('rLength', type: 'int'), // replacementLength
|
|
],
|
|
baseType: 'CompletionItemResolutionInfo',
|
|
),
|
|
interface(
|
|
'PubPackageCompletionItemResolutionInfo',
|
|
[
|
|
field('packageName', type: 'string'),
|
|
],
|
|
baseType: 'CompletionItemResolutionInfo',
|
|
),
|
|
// Custom types for experimental SnippetTextEdits
|
|
// https://github.com/rust-analyzer/rust-analyzer/blob/b35559a2460e7f0b2b79a7029db0c5d4e0acdb44/docs/dev/lsp-extensions.md#snippet-textedit
|
|
interface(
|
|
'SnippetTextEdit',
|
|
[
|
|
field('insertTextFormat', type: 'InsertTextFormat'),
|
|
],
|
|
baseType: 'TextEdit',
|
|
),
|
|
TypeAlias(
|
|
null,
|
|
Token.identifier('TextDocumentEditEdits'),
|
|
ArrayType(
|
|
UnionType([
|
|
Type.identifier('SnippetTextEdit'),
|
|
Type.identifier('AnnotatedTextEdit'),
|
|
Type.identifier('TextEdit'),
|
|
]),
|
|
),
|
|
)
|
|
];
|
|
return customTypes;
|
|
}
|
|
|
|
Future<List<AstNode>> getSpecClasses(ArgResults args) async {
|
|
if (args[argDownload]) {
|
|
await downloadSpec();
|
|
}
|
|
final spec = await readSpec();
|
|
|
|
final types = extractTypeScriptBlocks(spec)
|
|
.where(shouldIncludeScriptBlock)
|
|
.map(parseString)
|
|
.expand((f) => f)
|
|
.where(includeTypeDefinitionInOutput)
|
|
.toList();
|
|
|
|
// Generate an enum for all of the request methods to avoid strings.
|
|
types.add(extractMethodsEnum(spec));
|
|
|
|
// Extract additional inline types that are specificed online in the `results`
|
|
// section of the doc.
|
|
types.addAll(extractResultsInlineTypes(spec));
|
|
return types;
|
|
}
|
|
|
|
Future<String> readSpec() => File(localSpecPath).readAsString();
|
|
|
|
/// Returns whether a script block should be parsed or not.
|
|
bool shouldIncludeScriptBlock(String input) {
|
|
// Skip over some typescript blocks that are known sample code and not part
|
|
// of the LSP spec.
|
|
if (input.trim() == r"export const EOL: string[] = ['\n', '\r\n', '\r'];" ||
|
|
input.startsWith('textDocument.codeAction.resolveSupport =')) {
|
|
return false;
|
|
}
|
|
|
|
// There are some code blocks that just have example JSON in them.
|
|
if (input.startsWith('{') && input.endsWith('}')) {
|
|
return false;
|
|
}
|
|
|
|
// There are some example blocks that just contain arrays with no definitions.
|
|
// They're most easily noted by ending with `]` which no valid TypeScript blocks
|
|
// do.
|
|
if (input.trim().endsWith(']')) {
|
|
return false;
|
|
}
|
|
|
|
// There's a chunk of typescript that is just a partial snippet from a real
|
|
// interface declared elsewhere that we can only detect by the leading comment.
|
|
if (input
|
|
.replaceAll('\r', '')
|
|
.startsWith('/**\n\t * Window specific client capabilities.')) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|