blob: f1783c251a823a4640ff74c79bc2028db92fbadb [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.
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);
var help = args[argHelp] as bool;
if (help) {
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');
// 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.
// 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)
negatable: false,
abbr: 'd',
'Download the latest version of the LSP spec before generating types');
final String localSpecPath = path.join(
path.dirname(Platform.script.toFilePath()), '');
final Uri specLicenseUri = Uri.parse(
/// 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(
/// 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".''',
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(
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 = => capitalize('And');
return InlineInterface(newName, interface.members);
return _resultsInlineTypesPattern
.map((m) =>!.trim())
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';
const jsonEncoder = JsonEncoder.withIndent(' ');
List<AstNode> getCustomClasses() {
Interface interface(String name, List<Member> fields, {String? baseType}) {
return Interface(
[if (baseType != null) Type.identifier(baseType)],
Field field(
String name, {
required String type,
bool array = false,
bool canBeUndefined = false,
}) {
var fieldType =
array ? ArrayType(Type.identifier(type)) : Type.identifier(type);
return Field(
null, Token.identifier(name), fieldType, false, 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)
[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),
[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),
type: 'FlutterOutlineAttribute', array: true, canBeUndefined: true),
field('dartElement', type: 'Element', canBeUndefined: true),
field('range', type: 'Range'),
field('codeRange', type: 'Range'),
type: 'FlutterOutline', array: true, canBeUndefined: true)
field('name', type: 'string'),
field('label', type: 'string'),
field('valueRange', type: 'Range', canBeUndefined: true),
field('file', type: 'string'),
field('offset', type: 'int'),
// 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',
field('packageName', type: 'string'),
baseType: 'CompletionItemResolutionInfo',
// Custom types for experimental SnippetTextEdits
field('insertTextFormat', type: 'InsertTextFormat'),
baseType: 'TextEdit',
return customTypes;
Future<List<AstNode>> getSpecClasses(ArgResults args) async {
var download = args[argDownload] as bool;
if (download) {
await downloadSpec();
final spec = await readSpec();
final types = extractTypeScriptBlocks(spec)
.expand((f) => f)
// Generate an enum for all of the request methods to avoid strings.
// Extract additional inline types that are specificed online in the `results`
// section of the doc.
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;