Files
Paul Berry afcfbbeba8 Migrate developer experience packages to new constructor decl syntax.
(Part of https://github.com/dart-lang/sdk/issues/63288)

This change migrates the packages owned by the developer experience
team to use the new constructor declaration syntax, described in
https://github.com/dart-lang/language/blob/main/accepted/future-releases/primary-constructors/feature-specification.md#abbreviations-of-in-body-constructor-declarations.

This change was performed in an automated fashion, by (a) bumping the
packages' SDK constraints to `3.13.0-0`, (b) enabling the lints
`unnecessary_type_name_in_constructor` and
`unnecessary_const_in_enum_constructor`, (c) fixing the resulting lint
failures using `dart fix`, and then (d) reformatting the affected
files.

To ease code review, I've reverted unrelated formatting changes.

Since this change requires bumping SDK constaints to `3.13.0-0`, it
was only performed on packages that are *not* published on
pub. (Packages that *are* published on pub should remain on lower
language versions until at least after the stable version of 3.13 is
released, so that we don't block users on the stable channel from
receiving updates to those packages.)

Change-Id: Ibb4daebafd239da58251e838ea6a3f336a6a6964
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505046
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
2026-05-27 14:52:58 -07:00

688 lines
20 KiB
Dart

// Copyright (c) 2014, 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.
/// Code for reading an HTML API description.
library;
import 'dart:io';
import 'package:analyzer_utilities/html_dom.dart' as dom;
import 'package:analyzer_utilities/html_generator.dart';
import 'package:analyzer_utilities/html_parser.dart' as parser;
import 'package:path/path.dart';
import 'api.dart';
/// Read the API description from the file 'plugin_spec.html'. [pkgRoot] is the
/// path to the SDK's `pkg` directory.
Api readApi(String pkgRoot) {
var reader = ApiReader(
join(pkgRoot, 'analysis_server', 'tool', 'spec', 'spec_input.html'),
);
return reader.readApi();
}
typedef ElementProcessor = void Function(dom.Element element);
class ApiReader {
static const List<String> specialElements = [
'domain',
'feedback',
'object',
'refactorings',
'refactoring',
'type',
'types',
'request',
'notification',
'params',
'result',
'field',
'list',
'map',
'enum',
'key',
'value',
'options',
'ref',
'code',
'version',
'union',
'index',
'include',
];
/// The absolute and normalized path to the file being read.
final String filePath;
/// Initialize a newly created API reader to read from the file with the given
/// [filePath].
new(this.filePath);
/// Create an [Api] object from an HTML representation such as:
///
/// ```html
/// <html>
/// ...
/// <body>
/// ... <version>1.0</version> ...
/// <domain name="...">...</domain> <!-- zero or more -->
/// <types>...</types>
/// <refactorings>...</refactorings>
/// </body>
/// </html>
/// ```
///
/// Child elements of API can occur in any order.
Api apiFromHtml(dom.Element html) {
Api api;
var versions = <String>[];
var domains = <Domain>[];
var types = Types({}, null);
var refactorings = Refactorings([], null);
recurse(html, 'api', {
'domain': (dom.Element element) {
domains.add(domainFromHtml(element));
},
'refactorings': (dom.Element element) {
refactorings = refactoringsFromHtml(element);
},
'types': (dom.Element element) {
types = typesFromHtml(element);
},
'version': (dom.Element element) {
versions.add(innerText(element));
},
'index': (dom.Element element) {
/* Ignore; generated dynamically. */
},
});
if (versions.length != 1) {
throw Exception('The API must contain exactly one <version> element');
}
api = Api(versions[0], domains, types, refactorings, html);
return api;
}
/// Check that the given [element] has all of the attributes in
/// [requiredAttributes], possibly some of the attributes in
/// [optionalAttributes], and no others.
void checkAttributes(
dom.Element element,
List<String> requiredAttributes,
String context, {
List<String> optionalAttributes = const [],
}) {
var attributesFound = <String>{};
element.attributes.forEach((name, value) {
if (!requiredAttributes.contains(name) &&
!optionalAttributes.contains(name)) {
throw Exception(
'$context: Unexpected attribute in ${element.name}: $name',
);
}
attributesFound.add(name);
});
for (var expectedAttribute in requiredAttributes) {
if (!attributesFound.contains(expectedAttribute)) {
throw Exception(
'$context: ${element.name} must contain attribute $expectedAttribute',
);
}
}
}
/// Check that the given [element] has the given [expectedName].
void checkName(dom.Element element, String expectedName, [String? context]) {
if (element.name != expectedName) {
context ??= element.name;
throw Exception(
'$context: Expected $expectedName, found ${element.name}',
);
}
}
/// Create a [Domain] object from an HTML representation such as:
///
/// ```html
/// <domain name="domainName">
/// <request method="...">...</request> <!-- zero or more -->
/// <notification event="...">...</notification> <!-- zero or more -->
/// </domain>
/// ```
///
/// Child elements can occur in any order.
Domain domainFromHtml(dom.Element html) {
checkName(html, 'domain');
var name = html.attributes['name'];
if (name == null) {
throw Exception('domains: name not specified');
}
var experimental = html.attributes['experimental'] == 'true';
checkAttributes(html, ['name'], name, optionalAttributes: ['experimental']);
var requests = <Request>[];
var notifications = <Notification>[];
recurse(html, name, {
'request': (dom.Element child) {
requests.add(requestFromHtml(child, name));
},
'notification': (dom.Element child) {
notifications.add(notificationFromHtml(child, name));
},
});
return Domain(
name,
requests,
notifications,
html,
experimental: experimental,
);
}
dom.Element getAncestor(dom.Element html, String name, String context) {
var ancestor = html.parent;
while (ancestor != null) {
if (ancestor.name == name) {
return ancestor;
}
ancestor = ancestor.parent;
}
throw Exception('$context: <${html.name}> must be nested within <$name>');
}
/// Create a [Notification] object from an HTML representation such as:
///
/// ```html
/// <notification event="methodName">
/// <params>...</params> <!-- optional -->
/// </notification>
/// ```
///
/// Note that the event name should not include the domain name.
///
/// `<params>` has the same form as `<object>`, as described in
/// [typeObjectFromHtml].
///
/// Child elements can occur in any order.
Notification notificationFromHtml(dom.Element html, String context) {
var domainName = getAncestor(html, 'domain', context).attributes['name'];
if (domainName == null) {
throw Exception('$context: domain not specified');
}
checkName(html, 'notification', context);
var event = html.attributes['event'];
if (event == null) {
throw Exception('$context: event not specified');
}
context = '$context.$event';
TypeObject? params;
recurse(html, context, {
'params': (dom.Element child) {
params = typeObjectFromHtml(child, '$context.params');
},
});
checkAttributes(
html,
['event'],
context,
optionalAttributes: ['experimental'],
);
var experimental = html.attributes['experimental'] == 'true';
return Notification(
domainName,
event,
params,
html,
experimental: experimental,
);
}
/// Create a single of [TypeDecl] corresponding to the type defined inside the
/// given HTML element.
TypeDecl processContentsAsType(dom.Element html, String context) {
var types = processContentsAsTypes(html, context);
if (types.length != 1) {
throw Exception('$context: Exactly one type must be specified');
}
return types[0];
}
/// Create a list of [TypeDecl]s corresponding to the types defined inside the
/// given HTML element. The following forms are supported.
///
/// To refer to a type declared elsewhere (or a built-in type):
/// `<ref>typeName</ref>`
///
/// For a list: `<list>ItemType</list>`
///
/// For a map: `<map><key>KeyType</key><value>ValueType</value></map>`
///
/// For a JSON object:
/// ```html
/// <object>
/// <field name="...">...</field> <!-- zero or more -->
/// </object>
/// ```
///
/// For an enum:
/// ```html
/// <enum>
/// <value>...</value> <!-- zero or more -->
/// </enum>
/// ```
///
/// For a union type:
/// ```html
/// <union>
/// TYPE <!-- zero or more -->
/// </union>
/// ```
List<TypeDecl> processContentsAsTypes(dom.Element html, String context) {
var types = <TypeDecl>[];
recurse(html, context, {
'object': (dom.Element child) {
types.add(typeObjectFromHtml(child, context));
},
'list': (dom.Element child) {
checkAttributes(child, [], context);
types.add(TypeList(processContentsAsType(child, context), child));
},
'map': (dom.Element child) {
checkAttributes(child, [], context);
TypeDecl? keyTypeNullable;
TypeDecl? valueTypeNullable;
recurse(child, context, {
'key': (dom.Element child) {
if (keyTypeNullable != null) {
throw Exception('$context: Key type already specified');
}
keyTypeNullable = processContentsAsType(child, '$context.key');
},
'value': (dom.Element child) {
if (valueTypeNullable != null) {
throw Exception('$context: Value type already specified');
}
valueTypeNullable = processContentsAsType(child, '$context.value');
},
});
var keyType = keyTypeNullable;
if (keyType is! TypeReference) {
throw Exception(
'$context: Key type not specified, or not a reference',
);
}
var valueType = valueTypeNullable;
if (valueType == null) {
throw Exception('$context: Value type not specified');
}
types.add(TypeMap(keyType, valueType, child));
},
'enum': (dom.Element child) {
types.add(typeEnumFromHtml(child, context));
},
'ref': (dom.Element child) {
checkAttributes(child, [], context);
types.add(TypeReference(innerText(child), child));
},
'union': (dom.Element child) {
checkAttributes(child, ['field'], context);
var field = child.attributes['field']!;
types.add(
TypeUnion(processContentsAsTypes(child, context), field, child),
);
},
});
return types;
}
/// Read the API description from file with the given [filePath].
Api readApi() {
var file = File(filePath);
var htmlContents = file.readAsStringSync();
var document = parser.parse(htmlContents, file.uri);
var htmlElement = document.children.singleWhere(
(element) => element.name.toLowerCase() == 'html',
);
return apiFromHtml(htmlElement);
}
void recurse(
dom.Element parent,
String context,
Map<String, ElementProcessor> elementProcessors,
) {
for (var key in elementProcessors.keys) {
if (!specialElements.contains(key)) {
throw Exception('$context: $key is not a special element');
}
}
for (var node in parent.nodes) {
if (node is dom.Element) {
var processor = elementProcessors[node.name];
if (processor != null) {
processor(node);
} else if (specialElements.contains(node.name)) {
throw Exception('$context: Unexpected use of <${node.name}>');
} else {
recurse(node, context, elementProcessors);
}
}
}
}
/// Create a [Refactoring] object from an HTML representation such as:
///
/// ```html
/// <refactoring kind="refactoringKind">
/// <feedback>...</feedback> <!-- optional -->
/// <options>...</options> <!-- optional -->
/// </refactoring>
/// ```
///
/// `<feedback>` and `<options>` have the same form as `<object>`, as
/// described in [typeObjectFromHtml].
///
/// Child elements can occur in any order.
Refactoring refactoringFromHtml(dom.Element html) {
checkName(html, 'refactoring');
var kind = html.attributes['kind'];
if (kind == null) {
throw Exception('refactorings: kind not specified');
}
checkAttributes(html, ['kind'], kind);
TypeObject? feedback;
TypeObject? options;
recurse(html, kind, {
'feedback': (dom.Element child) {
feedback = typeObjectFromHtml(child, '$kind.feedback');
},
'options': (dom.Element child) {
options = typeObjectFromHtml(child, '$kind.options');
},
});
return Refactoring(kind, feedback, options, html);
}
/// Create a [Refactorings] object from an HTML representation such as:
///
/// ```html
/// <refactorings>
/// <refactoring kind="...">...</refactoring> <!-- zero or more -->
/// </refactorings>
/// ```
Refactorings refactoringsFromHtml(dom.Element html) {
checkName(html, 'refactorings');
var context = 'refactorings';
checkAttributes(html, [], context);
var refactorings = <Refactoring>[];
recurse(html, context, {
'refactoring': (dom.Element child) {
refactorings.add(refactoringFromHtml(child));
},
});
return Refactorings(refactorings, html);
}
/// Create a [Request] object from an HTML representation such as:
///
/// ```html
/// <request method="methodName">
/// <params>...</params> <!-- optional -->
/// <result>...</result> <!-- optional -->
/// </request>
/// ```
///
/// Note that the method name should not include the domain name.
///
/// `<params>` and `<result>` have the same form as `<object>`, as described
/// in [typeObjectFromHtml].
///
/// Child elements can occur in any order.
Request requestFromHtml(dom.Element html, String context) {
var domainName = getAncestor(html, 'domain', context).attributes['name'];
if (domainName == null) {
throw Exception('$context: domain not specified');
}
checkName(html, 'request', context);
var method = html.attributes['method'];
if (method == null) {
throw Exception('$context: method not specified');
}
context = '$context.$method}';
checkAttributes(
html,
['method'],
context,
optionalAttributes: ['experimental', 'deprecated'],
);
var experimental = html.attributes['experimental'] == 'true';
var deprecated = html.attributes['deprecated'] == 'true';
TypeObject? params;
TypeObject? result;
recurse(html, context, {
'params': (dom.Element child) {
params = typeObjectFromHtml(child, '$context.params');
},
'result': (dom.Element child) {
result = typeObjectFromHtml(child, '$context.result');
},
});
return Request(
domainName,
method,
params,
result,
html,
experimental: experimental,
deprecated: deprecated,
);
}
/// Create a [TypeDefinition] object from an HTML representation such as:
///
/// ```html
/// <type name="typeName">
/// TYPE
/// </type>
/// ```
///
/// Where TYPE is any HTML that can be parsed by [typeObjectFromHtml].
///
/// Child elements can occur in any order.
TypeDefinition typeDefinitionFromHtml(dom.Element html) {
checkName(html, 'type');
var name = html.attributes['name'];
if (name == null) {
throw Exception('types: name not specified');
}
checkAttributes(
html,
['name'],
name,
optionalAttributes: ['experimental', 'deprecated'],
);
var type = processContentsAsType(html, name);
var experimental = html.attributes['experimental'] == 'true';
var deprecated = html.attributes['deprecated'] == 'true';
return TypeDefinition(
name,
type,
html,
experimental: experimental,
deprecated: deprecated,
);
}
/// Create a [TypeEnum] from an HTML description.
TypeEnum typeEnumFromHtml(dom.Element html, String context) {
checkName(html, 'enum', context);
checkAttributes(html, [], context);
var values = <TypeEnumValue>[];
recurse(html, context, {
'value': (dom.Element child) {
values.add(typeEnumValueFromHtml(child, context));
},
});
return TypeEnum(values, html);
}
/// Create a [TypeEnumValue] from an HTML description such as:
///
/// ```html
/// <enum>
/// <code>VALUE</code>
/// </enum>
/// ```
///
/// Where VALUE is the text of the enumerated value.
///
/// Child elements can occur in any order.
TypeEnumValue typeEnumValueFromHtml(dom.Element html, String context) {
checkName(html, 'value', context);
checkAttributes(html, [], context, optionalAttributes: ['deprecated']);
var deprecated = html.attributes['deprecated'] == 'true';
var values = <String>[];
recurse(html, context, {
'code': (dom.Element child) {
var text = innerText(child).trim();
values.add(text);
},
});
if (values.length != 1) {
throw Exception('$context: Exactly one value must be specified');
}
return TypeEnumValue(values[0], html, deprecated: deprecated);
}
/// Create a [TypeObjectField] from an HTML description such as:
///
/// ```html
/// <field name="fieldName">
/// TYPE
/// </field>
/// ```
///
/// Where TYPE is any HTML that can be parsed by [typeObjectFromHtml].
///
/// In addition, the attribute `optional="true"` may be used to specify that
/// the field is optional, and the attribute `value="..."` may be used to
/// specify that the field is required to have a certain value.
///
/// Child elements can occur in any order.
TypeObjectField typeObjectFieldFromHtml(dom.Element html, String context) {
checkName(html, 'field', context);
var name = html.attributes['name'];
if (name == null) {
throw Exception('$context: name not specified');
}
context = '$context.$name';
checkAttributes(
html,
['name'],
context,
optionalAttributes: ['optional', 'value', 'deprecated', 'experimental'],
);
var deprecated = html.attributes['deprecated'] == 'true';
var experimental = html.attributes['experimental'] == 'true';
var optional = false;
var optionalString = html.attributes['optional'];
if (optionalString != null) {
switch (optionalString) {
case 'true':
optional = true;
case 'false':
optional = false;
default:
throw Exception(
'$context: field contains invalid "optional" attribute: "$optionalString"',
);
}
}
var value = html.attributes['value'];
var type = processContentsAsType(html, context);
return TypeObjectField(
name,
type,
html,
optional: optional,
value: value,
deprecated: deprecated,
experimental: experimental,
);
}
/// Create a [TypeObject] from an HTML description.
TypeObject typeObjectFromHtml(dom.Element html, String context) {
checkAttributes(html, [], context, optionalAttributes: ['experimental']);
var fields = <TypeObjectField>[];
recurse(html, context, {
'field': (dom.Element child) {
fields.add(typeObjectFieldFromHtml(child, context));
},
});
var experimental = html.attributes['experimental'] == 'true';
return TypeObject(fields, html, experimental: experimental);
}
/// Create a [Types] object from an HTML representation such as:
///
/// ```html
/// <types>
/// <type name="...">...</type> <!-- zero or more -->
/// </types>
/// ```
Types typesFromHtml(dom.Element html) {
checkName(html, 'types');
var context = 'types';
checkAttributes(html, [], context);
var importUris = <String>[];
var typeMap = <String, TypeDefinition>{};
var childElements = <dom.Element>[];
recurse(html, context, {
'include': (dom.Element child) {
var importUri = child.attributes['import'];
if (importUri != null) {
importUris.add(importUri);
}
var relativePath = child.attributes['path'];
var path = normalize(join(dirname(filePath), relativePath));
var reader = ApiReader(path);
var api = reader.readApi();
for (var typeDefinition in api.types) {
typeDefinition.isExternal = true;
childElements.add(typeDefinition.html!);
typeMap[typeDefinition.name] = typeDefinition;
}
},
'type': (dom.Element child) {
var typeDefinition = typeDefinitionFromHtml(child);
typeMap[typeDefinition.name] = typeDefinition;
},
});
for (var element in childElements) {
html.append(element);
}
var types = Types(typeMap, html);
types.importUris.addAll(importUris);
return types;
}
}