analyzer: Move the API tool so that it can generate API for other packages
Also, add some scripts to generate the analyzer_testing API, and checkin the api.txt file for analyzer_testing. In order to avoid a private analyzer import, I add `isExperimental` and `hasExperimental` support for `@experimental` annotation, and switch to using public APIs in `_dumpElement`. Change-Id: I44319270d4e6083b80973b45933268c472876232 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/426282 Commit-Queue: Samuel Rawlins <srawlins@google.com> Reviewed-by: Paul Berry <paulberry@google.com>
This commit is contained in:
committed by
Commit Queue
parent
86e8e24017
commit
fafad4e03d
@@ -3087,6 +3087,7 @@ package:analyzer/dart/element/element.dart:
|
||||
isDeprecated (getter: bool)
|
||||
isDoNotStore (getter: bool)
|
||||
isDoNotSubmit (getter: bool)
|
||||
isExperimental (getter: bool)
|
||||
isFactory (getter: bool)
|
||||
isImmutable (getter: bool)
|
||||
isInternal (getter: bool)
|
||||
@@ -3555,6 +3556,7 @@ package:analyzer/dart/element/element.dart:
|
||||
hasDeprecated (getter: bool)
|
||||
hasDoNotStore (getter: bool)
|
||||
hasDoNotSubmit (getter: bool)
|
||||
hasExperimental (getter: bool)
|
||||
hasFactory (getter: bool)
|
||||
hasImmutable (getter: bool)
|
||||
hasInternal (getter: bool)
|
||||
|
||||
@@ -733,6 +733,9 @@ abstract class ElementAnnotation implements ConstantEvaluationTarget {
|
||||
/// Whether the annotation marks the associated member as not to be used.
|
||||
bool get isDoNotSubmit;
|
||||
|
||||
/// Whether the annotation marks the associated element as experimental.
|
||||
bool get isExperimental;
|
||||
|
||||
/// Whether the annotation marks the associated member as a factory.
|
||||
bool get isFactory;
|
||||
|
||||
@@ -2637,6 +2640,9 @@ abstract class Metadata {
|
||||
/// Whether the receiver has an annotation of the form `@doNotSubmit`.
|
||||
bool get hasDoNotSubmit;
|
||||
|
||||
/// Whether the receiver has an annotation of the form `@experimental`.
|
||||
bool get hasExperimental;
|
||||
|
||||
/// Whether the receiver has an annotation of the form `@factory`.
|
||||
bool get hasFactory;
|
||||
|
||||
|
||||
@@ -1511,6 +1511,9 @@ class ElementAnnotationImpl implements ElementAnnotation {
|
||||
/// used (for ephemeral testing and debugging only).
|
||||
static const String _doNotSubmitVariableName = 'doNotSubmit';
|
||||
|
||||
/// The name of the top-level variable used to mark a declaration as experimental.
|
||||
static const String _experimentalVariableName = 'experimental';
|
||||
|
||||
/// The name of the top-level variable used to mark a method as being a
|
||||
/// factory.
|
||||
static const String _factoryVariableName = 'factory';
|
||||
@@ -1723,6 +1726,9 @@ class ElementAnnotationImpl implements ElementAnnotation {
|
||||
@override
|
||||
bool get isDoNotSubmit => _isPackageMetaGetter(_doNotSubmitVariableName);
|
||||
|
||||
@override
|
||||
bool get isExperimental => _isPackageMetaGetter(_experimentalVariableName);
|
||||
|
||||
@override
|
||||
bool get isFactory => _isPackageMetaGetter(_factoryVariableName);
|
||||
|
||||
@@ -6924,6 +6930,18 @@ final class MetadataImpl implements Metadata {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get hasExperimental {
|
||||
var annotations = this.annotations;
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var annotation = annotations[i];
|
||||
if (annotation.isExperimental) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get hasFactory {
|
||||
var annotations = this.annotations;
|
||||
|
||||
@@ -5,724 +5,16 @@
|
||||
/// Generates the file `api.txt`, which describes the analyzer public API.
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:analyzer/dart/analysis/analysis_context.dart';
|
||||
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
|
||||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:analyzer/file_system/physical_file_system.dart';
|
||||
import 'package:analyzer/src/utilities/extensions/element.dart';
|
||||
import 'package:analyzer_testing/package_root.dart' as pkg_root;
|
||||
import 'package:analyzer_testing/package_root.dart';
|
||||
import 'package:analyzer_utilities/tool/api.dart';
|
||||
import 'package:analyzer_utilities/tools.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await GeneratedContent.generateAll(analyzerPkgPath, allTargets);
|
||||
}
|
||||
|
||||
/// A list of all targets generated by this code generator.
|
||||
final List<GeneratedContent> allTargets = [
|
||||
GeneratedFile('api.txt', computeApiTxtContents),
|
||||
];
|
||||
|
||||
/// The path to the `analyzer` package.
|
||||
final String analyzerPkgPath = normalize(
|
||||
join(pkg_root.packageRoot, 'analyzer'),
|
||||
);
|
||||
|
||||
/// Computes what should be written to the `api.txt` file.
|
||||
Future<String> computeApiTxtContents(String pkgPath) async {
|
||||
var provider = PhysicalResourceProvider.INSTANCE;
|
||||
var pathContext = provider.pathContext;
|
||||
var pkgRootPath = provider.pathContext.normalize(packageRoot);
|
||||
var packagePath = pathContext.join(pkgRootPath, 'analyzer');
|
||||
var collection = AnalysisContextCollection(
|
||||
includedPaths: [packagePath],
|
||||
resourceProvider: provider,
|
||||
);
|
||||
var context = collection.contexts.first;
|
||||
var publicApi = ApiDescription();
|
||||
var stringBuffer = StringBuffer();
|
||||
printNodes(stringBuffer, await publicApi.build(context));
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
|
||||
/// Outputs the contents of [nodes] to [sink], prepending [prefix] to every
|
||||
/// line.
|
||||
void printNodes<SortKey extends Comparable<SortKey>>(
|
||||
StringSink sink,
|
||||
List<(SortKey, Node)> nodes, {
|
||||
String prefix = '',
|
||||
}) {
|
||||
for (var entry in nodes.sortedBy((n) => n.$1)) {
|
||||
var node = entry.$2;
|
||||
sink.writeln('$prefix${node.text.join()}');
|
||||
node.printChildren(sink, prefix: '$prefix ');
|
||||
}
|
||||
}
|
||||
|
||||
/// Data structure keeping track of the analyzer API while walking it to produce
|
||||
/// `api.txt`.
|
||||
class ApiDescription {
|
||||
/// Top level elements that have already had their child elements dumped.
|
||||
///
|
||||
/// If an element is seen again in a different library, it will be followed
|
||||
/// with `(see above)` (rather than having its child elements dumped twice).
|
||||
final _dumpedTopLevelElements = <Element>{};
|
||||
|
||||
/// Top level elements that have been referenced so far and haven't yet been
|
||||
/// processed by [build].
|
||||
///
|
||||
/// This is used to ensure that all elements referred to by the public API
|
||||
/// (e.g., by being mentioned in the type of an API element) also show up in
|
||||
/// the output.
|
||||
final _potentiallyDanglingReferences = Queue<Element>();
|
||||
|
||||
final _uniqueNamer = UniqueNamer();
|
||||
|
||||
/// Cache of values returned by [_getOrComputeImmediateSubinterfaceMap], to
|
||||
/// avoid unnecessary recomputation.
|
||||
final _immediateSubinterfaceCache =
|
||||
<LibraryElement, Map<ClassElement, Set<InterfaceElement>>>{};
|
||||
|
||||
/// Builds a list of [Node] objects representing all the libraries that are
|
||||
/// relevant to the analyzer public API.
|
||||
///
|
||||
/// This includes libraries that are in the analyzer public API as well as
|
||||
/// libraries that are referenced by the analyzer public API (either by being
|
||||
/// re-exported as part of the analyzer public API, or by being used as part
|
||||
/// of the type of something in the public API).
|
||||
///
|
||||
/// Each library node is pared with a [UriSortKey] indicating the order in
|
||||
/// which the nodes should be output.
|
||||
Future<List<(UriSortKey, Node)>> build(AnalysisContext context) async {
|
||||
var nodes = <Uri, Node<MemberSortKey>>{};
|
||||
for (var file in context.contextRoot.analyzedFiles().sorted()) {
|
||||
if (!file.endsWith('.dart')) continue;
|
||||
var fileResult = context.currentSession.getFile(file) as FileResult;
|
||||
var uri = fileResult.uri;
|
||||
if (fileResult.isLibrary && uri.isInAnalyzerPublicLib) {
|
||||
var resolvedLibraryResult =
|
||||
(await context.currentSession.getResolvedLibrary(file))
|
||||
as ResolvedLibraryResult;
|
||||
var node = nodes[uri] = Node<MemberSortKey>();
|
||||
_dumpLibrary(resolvedLibraryResult.element2, node);
|
||||
}
|
||||
}
|
||||
// Then dump anything referenced by public libraries.
|
||||
while (_potentiallyDanglingReferences.isNotEmpty) {
|
||||
var element = _potentiallyDanglingReferences.removeFirst();
|
||||
if (!_dumpedTopLevelElements.add(element)) continue;
|
||||
var containingLibraryUri = element.library2!.uri;
|
||||
var childNode =
|
||||
Node<MemberSortKey>()..text.add(_uniqueNamer.name(element));
|
||||
_dumpElement(element, childNode);
|
||||
(nodes[containingLibraryUri] ??=
|
||||
Node<MemberSortKey>()..text.add('$containingLibraryUri:'))
|
||||
.childNodes
|
||||
.add((MemberSortKey(element), childNode));
|
||||
}
|
||||
return [
|
||||
for (var entry in nodes.entries) (UriSortKey(entry.key), entry.value),
|
||||
];
|
||||
}
|
||||
|
||||
/// Creates a list of objects which, when their string representations are
|
||||
/// concatenated, describes [type].
|
||||
///
|
||||
/// The reason we use this method rather than [DartType.toString] is to make
|
||||
/// sure that (a) every element mentioned by the type is added to
|
||||
/// [_potentiallyDanglingReferences], and (b) if an ambiguous name is used,
|
||||
/// the ambiguity will be taken care of by [_uniqueNamer].
|
||||
List<Object?> _describeType(DartType type) {
|
||||
var suffix = switch (type.nullabilitySuffix) {
|
||||
NullabilitySuffix.none => '',
|
||||
NullabilitySuffix.star => '*',
|
||||
NullabilitySuffix.question => '?',
|
||||
};
|
||||
switch (type) {
|
||||
case DynamicType():
|
||||
return ['dynamic'];
|
||||
case FunctionType(
|
||||
:var returnType,
|
||||
:var typeParameters,
|
||||
:var formalParameters,
|
||||
):
|
||||
var params = <List<Object?>>[];
|
||||
var optionalParams = <List<Object?>>[];
|
||||
var namedParams = <String, List<Object?>>{};
|
||||
for (var formalParameter in formalParameters) {
|
||||
if (formalParameter.isNamed) {
|
||||
namedParams[formalParameter.name3!] = [
|
||||
if (formalParameter.isRequired) 'required ',
|
||||
..._describeType(formalParameter.type),
|
||||
];
|
||||
} else if (formalParameter.isOptional) {
|
||||
optionalParams.add(_describeType(formalParameter.type));
|
||||
} else {
|
||||
params.add(_describeType(formalParameter.type));
|
||||
}
|
||||
}
|
||||
if (optionalParams.isNotEmpty) {
|
||||
params.add(optionalParams.separate(prefix: '[', suffix: ']'));
|
||||
}
|
||||
if (namedParams.isNotEmpty) {
|
||||
params.add(
|
||||
namedParams.entries
|
||||
.sortedBy((e) => e.key)
|
||||
.map((e) => [...e.value, ' ${e.key}'])
|
||||
.separate(prefix: '{', suffix: '}'),
|
||||
);
|
||||
}
|
||||
return <Object?>[
|
||||
..._describeType(returnType),
|
||||
' Function',
|
||||
if (typeParameters.isNotEmpty)
|
||||
...typeParameters
|
||||
.map(_describeTypeParameter)
|
||||
.separate(prefix: '<', suffix: '>'),
|
||||
'(',
|
||||
...params.separate(),
|
||||
')',
|
||||
suffix,
|
||||
];
|
||||
case InterfaceType(:var element3, :var typeArguments):
|
||||
_potentiallyDanglingReferences.addLast(element3);
|
||||
return [
|
||||
_uniqueNamer.name(element3),
|
||||
if (typeArguments.isNotEmpty)
|
||||
...typeArguments
|
||||
.map(_describeType)
|
||||
.separate(prefix: '<', suffix: '>'),
|
||||
suffix,
|
||||
];
|
||||
case RecordType(:var positionalFields, :var namedFields):
|
||||
if (positionalFields.length == 1 && namedFields.isEmpty) {
|
||||
return [
|
||||
'(',
|
||||
..._describeType(positionalFields[0].type),
|
||||
',)',
|
||||
suffix,
|
||||
];
|
||||
}
|
||||
return [
|
||||
...[
|
||||
for (var positionalField in positionalFields)
|
||||
_describeType(positionalField.type),
|
||||
if (namedFields.isNotEmpty)
|
||||
namedFields
|
||||
.sortedBy((f) => f.name)
|
||||
.map((f) => [..._describeType(f.type), ' ', f.name])
|
||||
.separate(prefix: '{', suffix: '}'),
|
||||
].separate(prefix: '(', suffix: ')'),
|
||||
suffix,
|
||||
];
|
||||
case TypeParameterType(:var element3):
|
||||
return [element3.name3!, suffix];
|
||||
case VoidType():
|
||||
return ['void'];
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('Unexpected type: $runtimeType');
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a list of objects which, when their string representations are
|
||||
/// concatenated, describes [typeParameter].
|
||||
List<Object?> _describeTypeParameter(TypeParameterElement typeParameter) {
|
||||
return [
|
||||
typeParameter.name3!,
|
||||
if (typeParameter.bound case var bound?) ...[
|
||||
' extends ',
|
||||
..._describeType(bound),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/// Appends information to [node] describing [element].
|
||||
void _dumpElement(Element element, Node<MemberSortKey> node) {
|
||||
var enclosingElement = element.enclosingElement2;
|
||||
if (enclosingElement is LibraryElement && !element.isInAnalyzerPublicApi) {
|
||||
if (!enclosingElement.uri.isInAnalyzer) {
|
||||
node.text.add(' (referenced)');
|
||||
} else {
|
||||
node.text.add(' (non-public)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
var parentheticals = <List<Object?>>[];
|
||||
switch (element) {
|
||||
case TypeAliasElement(:var aliasedType, :var typeParameters2):
|
||||
List<Object?> description = ['type alias'];
|
||||
if (typeParameters2.isNotEmpty) {
|
||||
description.addAll(
|
||||
typeParameters2
|
||||
.map(_describeTypeParameter)
|
||||
.separate(prefix: '<', suffix: '>'),
|
||||
);
|
||||
}
|
||||
description.addAll([' for ', ..._describeType(aliasedType)]);
|
||||
parentheticals.add(description);
|
||||
case InstanceElement():
|
||||
switch (element) {
|
||||
case InterfaceElement(
|
||||
:var typeParameters2,
|
||||
:var supertype,
|
||||
:var interfaces,
|
||||
):
|
||||
List<Object?> instanceDescription = [
|
||||
switch (element) {
|
||||
ClassElement() => 'class',
|
||||
EnumElement() => 'enum',
|
||||
MixinElement() => 'mixin',
|
||||
dynamic(:var runtimeType) => 'TODO: $runtimeType',
|
||||
},
|
||||
];
|
||||
if (typeParameters2.isNotEmpty) {
|
||||
instanceDescription.addAll(
|
||||
typeParameters2
|
||||
.map(_describeTypeParameter)
|
||||
.separate(prefix: '<', suffix: '>'),
|
||||
);
|
||||
}
|
||||
if (element is! EnumElement && supertype != null) {
|
||||
instanceDescription.addAll([
|
||||
' extends ',
|
||||
..._describeType(supertype),
|
||||
]);
|
||||
}
|
||||
if (element is MixinElement &&
|
||||
element.superclassConstraints.isNotEmpty) {
|
||||
instanceDescription.addAll(
|
||||
element.superclassConstraints
|
||||
.map(_describeType)
|
||||
.separate(prefix: ' on '),
|
||||
);
|
||||
}
|
||||
if (interfaces.isNotEmpty) {
|
||||
instanceDescription.addAll(
|
||||
interfaces.map(_describeType).separate(prefix: ' implements '),
|
||||
);
|
||||
}
|
||||
parentheticals.add(instanceDescription);
|
||||
if (element is ClassElement && element.isSealed) {
|
||||
var parenthetical = <Object>['sealed'];
|
||||
parentheticals.add(parenthetical);
|
||||
if (_getOrComputeImmediateSubinterfaceMap(
|
||||
element.library2,
|
||||
)[element]
|
||||
case var subinterfaces?) {
|
||||
parenthetical.add(' (immediate subtypes: ');
|
||||
// Note: it's tempting to just do
|
||||
// `subinterfaces.map(_uniqueNamer.name).join(', ')`, but that
|
||||
// won't work, because the names returned by
|
||||
// `UniqueName.toString()` aren't finalized until we've visited
|
||||
// the entire API and seen if there are class names that need to
|
||||
// be disambiguated. So we accumulate the `UniqueName` objects
|
||||
// into the `parenthetical` list and rely on `printNodes`
|
||||
// converting everything to a string when the final API
|
||||
// description is being output.
|
||||
var commaNeeded = false;
|
||||
for (var subinterface in subinterfaces) {
|
||||
if (commaNeeded) {
|
||||
parenthetical.add(', ');
|
||||
} else {
|
||||
commaNeeded = true;
|
||||
}
|
||||
parenthetical.add(_uniqueNamer.name(subinterface));
|
||||
}
|
||||
parenthetical.add(')');
|
||||
}
|
||||
}
|
||||
case ExtensionElement(:var extendedType):
|
||||
parentheticals.add([
|
||||
'extension on ',
|
||||
..._describeType(extendedType),
|
||||
]);
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('Unexpected element: $runtimeType');
|
||||
}
|
||||
for (var member in element.children2.sortedBy((m) => m.name3 ?? '')) {
|
||||
if (member.name3 case var name? when name.startsWith('_')) {
|
||||
// Ignore private members
|
||||
continue;
|
||||
}
|
||||
if (member is FieldElement) {
|
||||
// Ignore fields; we care about the getters and setters they induce.
|
||||
continue;
|
||||
}
|
||||
if (member is ConstructorElement &&
|
||||
element is ClassElement &&
|
||||
element.isAbstract &&
|
||||
(element.isFinal || element.isInterface || element.isSealed)) {
|
||||
// The class can't be constructed from outside of the library that
|
||||
// declares it, so its constructors aren't part of the public API.
|
||||
continue;
|
||||
}
|
||||
if (member is ConstructorElement && element is EnumElement) {
|
||||
// Enum constructors can't be called from outside the enum itself,
|
||||
// so they aren't part of the public API.
|
||||
continue;
|
||||
}
|
||||
var childNode = Node<MemberSortKey>();
|
||||
childNode.text.add(member.apiName);
|
||||
_dumpElement(member, childNode);
|
||||
node.childNodes.add((MemberSortKey(member), childNode));
|
||||
}
|
||||
case TopLevelFunctionElement(:var type):
|
||||
parentheticals.add(['function: ', ..._describeType(type)]);
|
||||
case ExecutableElement(:var isStatic):
|
||||
String maybeStatic = isStatic ? 'static ' : '';
|
||||
switch (element) {
|
||||
case GetterElement(:var type):
|
||||
parentheticals.add([
|
||||
'${maybeStatic}getter: ',
|
||||
..._describeType(type.returnType),
|
||||
]);
|
||||
case SetterElement(:var type):
|
||||
parentheticals.add([
|
||||
'${maybeStatic}setter: ',
|
||||
..._describeType(type.formalParameters.single.type),
|
||||
]);
|
||||
case MethodElement(:var type):
|
||||
parentheticals.add([
|
||||
'${maybeStatic}method: ',
|
||||
..._describeType(type),
|
||||
]);
|
||||
case ConstructorElement(:var type):
|
||||
parentheticals.add(['constructor: ', ..._describeType(type)]);
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('Unexpected element: $runtimeType');
|
||||
}
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('Unexpected element: $runtimeType');
|
||||
}
|
||||
|
||||
if (element.metadata.any(_isDeprecated)) {
|
||||
parentheticals.add(['deprecated']);
|
||||
}
|
||||
if (element.metadata.any(_isExperimental)) {
|
||||
parentheticals.add(['experimental']);
|
||||
}
|
||||
|
||||
if (parentheticals.isNotEmpty) {
|
||||
node.text.addAll(parentheticals.separate(prefix: ' (', suffix: ')'));
|
||||
}
|
||||
if (node.childNodes.isNotEmpty) {
|
||||
node.text.add(':');
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends information to [node] describing [element].
|
||||
void _dumpLibrary(LibraryElement library, Node<MemberSortKey> node) {
|
||||
var uri = library.uri;
|
||||
node.text.addAll([uri, ':']);
|
||||
var definedNames = library.exportNamespace.definedNames2;
|
||||
for (var key in definedNames.keys.sorted()) {
|
||||
var element = definedNames[key]!;
|
||||
var childNode =
|
||||
Node<MemberSortKey>()..text.add(_uniqueNamer.name(element));
|
||||
if (!_dumpedTopLevelElements.add(element)) {
|
||||
childNode.text.add(' (see above)');
|
||||
} else {
|
||||
_dumpElement(element, childNode);
|
||||
}
|
||||
node.childNodes.add((MemberSortKey(element), childNode));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a map from each sealed class in [library] to the set of its
|
||||
/// immediate sub-interfaces.
|
||||
///
|
||||
/// If this method has been called before with the same [library], a cached
|
||||
/// map is returned from [_immediateSubinterfaceCache]. Otherwise a fresh map
|
||||
/// is computed.
|
||||
Map<ClassElement, Set<InterfaceElement>>
|
||||
_getOrComputeImmediateSubinterfaceMap(LibraryElement library) {
|
||||
if (_immediateSubinterfaceCache[library] case var m?) return m;
|
||||
var result = <ClassElement, Set<InterfaceElement>>{};
|
||||
for (var interface in [
|
||||
...library.classes,
|
||||
...library.mixins,
|
||||
...library.enums,
|
||||
...library.extensionTypes,
|
||||
]..sortBy((e) => e.name3!)) {
|
||||
for (var superinterface in [
|
||||
interface.supertype,
|
||||
...interface.interfaces,
|
||||
...interface.mixins,
|
||||
if (interface is MixinElement) ...interface.superclassConstraints,
|
||||
]) {
|
||||
if (superinterface == null) continue;
|
||||
var superinterfaceElement = superinterface.element3;
|
||||
if (superinterfaceElement is ClassElement &&
|
||||
superinterfaceElement.isSealed) {
|
||||
(result[superinterfaceElement] ??= {}).add(interface);
|
||||
}
|
||||
}
|
||||
}
|
||||
_immediateSubinterfaceCache[library] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool _isDeprecated(ElementAnnotation annotation) {
|
||||
if (annotation.computeConstantValue()!.type case InterfaceType(
|
||||
:var element3,
|
||||
) when element3.name3 == 'Deprecated') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isExperimental(ElementAnnotation annotation) {
|
||||
if (annotation.computeConstantValue()!.type case InterfaceType(
|
||||
:var element3,
|
||||
) when element3.name3 == '_Experimental') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Element categorization used by [MemberSortKey].
|
||||
enum MemberCategory {
|
||||
constructor,
|
||||
propertyAccessor,
|
||||
topLevelFunctionOrMethod,
|
||||
interface,
|
||||
extension,
|
||||
typeAlias,
|
||||
}
|
||||
|
||||
/// Sort key used to sort elements in the output.
|
||||
class MemberSortKey implements Comparable<MemberSortKey> {
|
||||
final bool _isInstanceMember;
|
||||
final MemberCategory _category;
|
||||
final String _name;
|
||||
|
||||
MemberSortKey(Element element)
|
||||
: _isInstanceMember = _computeIsInstanceMember(element),
|
||||
_category = _computeCategory(element),
|
||||
_name = element.name3!;
|
||||
|
||||
@override
|
||||
int compareTo(MemberSortKey other) {
|
||||
if ((_isInstanceMember ? 1 : 0).compareTo(other._isInstanceMember ? 1 : 0)
|
||||
case var value when value != 0) {
|
||||
return value;
|
||||
}
|
||||
if (_category.index.compareTo(other._category.index) case var value
|
||||
when value != 0) {
|
||||
return value;
|
||||
}
|
||||
return _name.compareTo(other._name);
|
||||
}
|
||||
|
||||
static MemberCategory _computeCategory(Element element) => switch (element) {
|
||||
ConstructorElement() => MemberCategory.constructor,
|
||||
PropertyAccessorElement() => MemberCategory.propertyAccessor,
|
||||
TopLevelFunctionElement() => MemberCategory.topLevelFunctionOrMethod,
|
||||
MethodElement() => MemberCategory.topLevelFunctionOrMethod,
|
||||
InterfaceElement() => MemberCategory.interface,
|
||||
ExtensionElement() => MemberCategory.extension,
|
||||
TypeAliasElement() => MemberCategory.typeAlias,
|
||||
dynamic(:var runtimeType) =>
|
||||
throw UnimplementedError('Unexpected element: $runtimeType'),
|
||||
};
|
||||
|
||||
static bool _computeIsInstanceMember(Element element) =>
|
||||
element.enclosingElement2 is InstanceElement &&
|
||||
switch (element) {
|
||||
ExecutableElement(:var isStatic) => !isStatic,
|
||||
dynamic(:var runtimeType) =>
|
||||
throw UnimplementedError('Unexpected element: $runtimeType'),
|
||||
};
|
||||
}
|
||||
|
||||
/// A node to be printed to the output.
|
||||
class Node<ChildSortKey extends Comparable<ChildSortKey>> {
|
||||
/// A list of objects which, when their string representations are
|
||||
/// concatenated, is the text that should be displayed on the first line of
|
||||
/// the node.
|
||||
///
|
||||
/// The reason this is a list rather than a single string is to allow elements
|
||||
/// of the list to be [UniqueName] objects, which may acquire a disambiguation
|
||||
/// suffix at a later time.
|
||||
final text = <Object?>[];
|
||||
|
||||
/// A list of child nodes, paired with a sort key indicating the order in
|
||||
/// which they should be output.
|
||||
final childNodes = <(ChildSortKey, Node)>[];
|
||||
|
||||
/// Outputs [childNodes], prepending [prefix] to every line.
|
||||
void printChildren(StringSink sink, {required String prefix}) {
|
||||
printNodes(sink, childNodes, prefix: prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/// Object that will have a unique string representation within the context of a
|
||||
/// given [UniqueNamer] instance.
|
||||
///
|
||||
/// If two or more [UniqueName] objects are constructed with reference to the
|
||||
/// same [UniqueNamer], and they have the same [_nameHint], then all such
|
||||
/// objects' [toString] methods will append a unique suffix of the form
|
||||
/// `@INTEGER`, so that the resulting strings are unique.
|
||||
class UniqueName {
|
||||
/// The name that will be returned by [toString] if no disambiguation is
|
||||
/// needed.
|
||||
final String _nameHint;
|
||||
|
||||
/// If not `Null`, the integer that [toString] will use to disambiguate this
|
||||
/// [UniqueName] from other ones with the same [_nameHint].
|
||||
int? _disambiguator;
|
||||
|
||||
UniqueName(UniqueNamer uniqueNamer, this._nameHint) {
|
||||
// The uniqueness guarantee depends on `_nameHint` not containing an `@`.
|
||||
assert(!_nameHint.contains('@'));
|
||||
var conflicts = uniqueNamer._conflicts[_nameHint] ??= [];
|
||||
if (conflicts.length == 1) {
|
||||
conflicts[0]._disambiguator = 1;
|
||||
}
|
||||
conflicts.add(this);
|
||||
if (conflicts.length > 1) {
|
||||
_disambiguator = conflicts.length;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
[
|
||||
_nameHint,
|
||||
if (_disambiguator case var disambiguator?) '@$disambiguator',
|
||||
].join();
|
||||
}
|
||||
|
||||
/// Manager of unique names for elements.
|
||||
class UniqueNamer {
|
||||
final _names = <Element, UniqueName>{};
|
||||
final _conflicts = <String, List<UniqueName>>{};
|
||||
|
||||
/// Returns a [UniqueName] object whose [toString] method will produce a
|
||||
/// unique name for [element].
|
||||
UniqueName name(Element element) =>
|
||||
_names[element] ??= UniqueName(this, element.apiName);
|
||||
}
|
||||
|
||||
/// URI categorization used by [UriSortKey].
|
||||
enum UriCategory { inAnalyzer, notInAnalyzer }
|
||||
|
||||
/// Sort key used to sort libraries in the output.
|
||||
///
|
||||
/// Libraries in `package:analyzer` will be output first (sorted by URI),
|
||||
/// followed by libraries not in `package:analyzer`.
|
||||
class UriSortKey implements Comparable<UriSortKey> {
|
||||
final UriCategory _category;
|
||||
final String _uriString;
|
||||
|
||||
UriSortKey(Uri uri)
|
||||
: _category =
|
||||
uri.isInAnalyzer ? UriCategory.inAnalyzer : UriCategory.notInAnalyzer,
|
||||
_uriString = uri.toString();
|
||||
|
||||
@override
|
||||
int compareTo(UriSortKey other) {
|
||||
if (_category.index.compareTo(other._category.index) case var value
|
||||
when value != 0) {
|
||||
return value;
|
||||
}
|
||||
return _uriString.compareTo(other._uriString);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Iterable<Iterable<Object?>> {
|
||||
/// Forms a list containing [prefix], followed by the elements of `this`
|
||||
/// (separated by [separator]), followed by [suffix].
|
||||
List<Object?> separate({
|
||||
String separator = ', ',
|
||||
String prefix = '',
|
||||
String suffix = '',
|
||||
}) {
|
||||
var result = <Object?>[prefix];
|
||||
var first = true;
|
||||
for (var item in this) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
result.add(separator);
|
||||
}
|
||||
result.addAll(item);
|
||||
}
|
||||
result.add(suffix);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
extension on Element {
|
||||
/// Returns the appropriate name for describing the element in `api.txt`.
|
||||
///
|
||||
/// The name is the same as [name3], but with `=` appended for setters.
|
||||
String get apiName {
|
||||
var apiName = name3!;
|
||||
if (this is SetterElement) {
|
||||
apiName += '=';
|
||||
}
|
||||
return apiName;
|
||||
}
|
||||
|
||||
bool get isInAnalyzerPublicApi {
|
||||
if (this case PropertyAccessorElement(
|
||||
isSynthetic: true,
|
||||
:var variable3?,
|
||||
) when variable3.isInAnalyzerPublicApi) {
|
||||
return true;
|
||||
}
|
||||
if (this case Annotatable(
|
||||
metadata2: Metadata(:var annotations),
|
||||
) when annotations.any(_isPublicApiAnnotation)) {
|
||||
return true;
|
||||
}
|
||||
if (name3 case var name? when !name.isPublic) return false;
|
||||
if (library2!.uri.isInAnalyzerPublicLib) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _isPublicApiAnnotation(ElementAnnotation annotation) {
|
||||
if (annotation.computeConstantValue() case DartObject(
|
||||
type: InterfaceType(
|
||||
element3: InterfaceElement(name3: 'AnalyzerPublicApi'),
|
||||
),
|
||||
)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
bool get isPublic => !startsWith('_');
|
||||
}
|
||||
|
||||
extension on Uri {
|
||||
bool get isInAnalyzer =>
|
||||
scheme == 'package' &&
|
||||
switch (pathSegments) {
|
||||
['analyzer', ...] => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
bool get isInAnalyzerPublicLib =>
|
||||
scheme == 'package' &&
|
||||
switch (pathSegments) {
|
||||
['analyzer', 'src', ...] => false,
|
||||
['analyzer', ...] => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// This test verifies that all files generated by `generate.dart` are up to
|
||||
// date.
|
||||
|
||||
import 'package:analyzer_utilities/tool/api.dart';
|
||||
import 'package:analyzer_utilities/tools.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package:analyzer_testing/package_root.dart:
|
||||
packageRoot (static getter: String)
|
||||
dart:core:
|
||||
String (referenced)
|
||||
@@ -13,4 +13,5 @@ dependencies:
|
||||
# best practice for packages is to specify their compatible version ranges.
|
||||
# See also https://dart.dev/tools/pub/dependencies.
|
||||
dev_dependencies:
|
||||
analyzer_utilities: any
|
||||
lints: any
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2025, 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.
|
||||
|
||||
/// Generates the file `api.txt`, which describes the analyzer public API.
|
||||
library;
|
||||
|
||||
import 'package:analyzer_testing/package_root.dart' as pkg_root;
|
||||
import 'package:analyzer_utilities/tool/api.dart';
|
||||
import 'package:analyzer_utilities/tools.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await GeneratedContent.generateAll(analyzerTestingPkgPath, allTargets);
|
||||
}
|
||||
|
||||
/// The path to the `analyzer_testing` package.
|
||||
final String analyzerTestingPkgPath = normalize(
|
||||
join(pkg_root.packageRoot, 'analyzer_testing'),
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2025, 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 test verifies that all files generated by `generate.dart` are up to
|
||||
// date.
|
||||
|
||||
import 'package:analyzer_utilities/tool/api.dart';
|
||||
import 'package:analyzer_utilities/tools.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import 'generate.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await GeneratedContent.checkAll(
|
||||
analyzerTestingPkgPath,
|
||||
join(analyzerTestingPkgPath, 'tool', 'api', 'generate.dart'),
|
||||
allTargets,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
This 'tool' directory is similar to the top-level 'tool' directory in other
|
||||
packages, except that the source must be importable by tools in other packages
|
||||
found in the Dart SDK, so it lives at 'lib/tool'.
|
||||
@@ -0,0 +1,698 @@
|
||||
// Copyright (c) 2025, 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.
|
||||
|
||||
/// Generates the file `api.txt`, which describes a package's public API.
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:analyzer/dart/analysis/analysis_context.dart';
|
||||
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
|
||||
import 'package:analyzer/dart/analysis/results.dart';
|
||||
import 'package:analyzer/dart/constant/value.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:analyzer/file_system/physical_file_system.dart';
|
||||
import 'package:analyzer_utilities/tools.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
/// A list of all targets generated by this code generator.
|
||||
final List<GeneratedContent> allTargets = [
|
||||
GeneratedFile('api.txt', computeApiTxtContents),
|
||||
];
|
||||
|
||||
/// Computes what should be written to the `api.txt` file.
|
||||
Future<String> computeApiTxtContents(String pkgPath) async {
|
||||
var provider = PhysicalResourceProvider.INSTANCE;
|
||||
var pathContext = provider.pathContext;
|
||||
var pkgName = pathContext.basename(pkgPath);
|
||||
var collection = AnalysisContextCollection(
|
||||
includedPaths: [pkgPath],
|
||||
resourceProvider: provider,
|
||||
);
|
||||
var context = collection.contexts.first;
|
||||
var publicApi = ApiDescription(pkgName);
|
||||
var stringBuffer = StringBuffer();
|
||||
_printNodes(stringBuffer, await publicApi.build(context));
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
|
||||
/// Outputs the contents of [nodes] to [sink], prepending [prefix] to every
|
||||
/// line.
|
||||
void _printNodes<SortKey extends Comparable<SortKey>>(
|
||||
StringSink sink,
|
||||
List<(SortKey, Node)> nodes, {
|
||||
String prefix = '',
|
||||
}) {
|
||||
for (var entry in nodes.sortedBy((n) => n.$1)) {
|
||||
var node = entry.$2;
|
||||
sink.writeln('$prefix${node.text.join()}');
|
||||
node.printChildren(sink, prefix: '$prefix ');
|
||||
}
|
||||
}
|
||||
|
||||
/// Data structure keeping track of a package's API while walking it to produce
|
||||
/// `api.txt`.
|
||||
class ApiDescription {
|
||||
final String _pkgName;
|
||||
|
||||
/// Top level elements that have already had their child elements dumped.
|
||||
///
|
||||
/// If an element is seen again in a different library, it will be followed
|
||||
/// with `(see above)` (rather than having its child elements dumped twice).
|
||||
final _dumpedTopLevelElements = <Element>{};
|
||||
|
||||
/// Top level elements that have been referenced so far and haven't yet been
|
||||
/// processed by [build].
|
||||
///
|
||||
/// This is used to ensure that all elements referred to by the public API
|
||||
/// (e.g., by being mentioned in the type of an API element) also show up in
|
||||
/// the output.
|
||||
final _potentiallyDanglingReferences = Queue<Element>();
|
||||
|
||||
final _uniqueNamer = UniqueNamer();
|
||||
|
||||
/// Cache of values returned by [_getOrComputeImmediateSubinterfaceMap], to
|
||||
/// avoid unnecessary recomputation.
|
||||
final _immediateSubinterfaceCache =
|
||||
<LibraryElement, Map<ClassElement, Set<InterfaceElement>>>{};
|
||||
|
||||
ApiDescription(String pkgName) : _pkgName = pkgName;
|
||||
|
||||
/// Builds a list of [Node] objects representing all the libraries that are
|
||||
/// relevant to the package's public API.
|
||||
///
|
||||
/// This includes libraries that are in the package's public API as well as
|
||||
/// libraries that are referenced by the package's public API (either by being
|
||||
/// re-exported as part of the package's public API, or by being used as part
|
||||
/// of the type of something in the public API).
|
||||
///
|
||||
/// Each library node is pared with a [UriSortKey] indicating the order in
|
||||
/// which the nodes should be output.
|
||||
Future<List<(UriSortKey, Node)>> build(AnalysisContext context) async {
|
||||
var nodes = <Uri, Node<MemberSortKey>>{};
|
||||
for (var file in context.contextRoot.analyzedFiles().sorted()) {
|
||||
if (!file.endsWith('.dart')) continue;
|
||||
var fileResult = context.currentSession.getFile(file) as FileResult;
|
||||
var uri = fileResult.uri;
|
||||
if (fileResult.isLibrary && uri.isInPublicLibOf(_pkgName)) {
|
||||
var resolvedLibraryResult =
|
||||
(await context.currentSession.getResolvedLibrary(file))
|
||||
as ResolvedLibraryResult;
|
||||
var node = nodes[uri] = Node<MemberSortKey>();
|
||||
_dumpLibrary(resolvedLibraryResult.element2, node);
|
||||
}
|
||||
}
|
||||
// Then dump anything referenced by public libraries.
|
||||
while (_potentiallyDanglingReferences.isNotEmpty) {
|
||||
var element = _potentiallyDanglingReferences.removeFirst();
|
||||
if (!_dumpedTopLevelElements.add(element)) continue;
|
||||
var containingLibraryUri = element.library2!.uri;
|
||||
var childNode =
|
||||
Node<MemberSortKey>()..text.add(_uniqueNamer.name(element));
|
||||
_dumpElement(element, childNode);
|
||||
(nodes[containingLibraryUri] ??=
|
||||
Node<MemberSortKey>()..text.add('$containingLibraryUri:'))
|
||||
.childNodes
|
||||
.add((MemberSortKey(element), childNode));
|
||||
}
|
||||
return [
|
||||
for (var entry in nodes.entries)
|
||||
(UriSortKey(entry.key, _pkgName), entry.value),
|
||||
];
|
||||
}
|
||||
|
||||
/// Creates a list of objects which, when their string representations are
|
||||
/// concatenated, describes [type].
|
||||
///
|
||||
/// The reason we use this method rather than [DartType.toString] is to make
|
||||
/// sure that (a) every element mentioned by the type is added to
|
||||
/// [_potentiallyDanglingReferences], and (b) if an ambiguous name is used,
|
||||
/// the ambiguity will be taken care of by [_uniqueNamer].
|
||||
List<Object?> _describeType(DartType type) {
|
||||
var suffix = switch (type.nullabilitySuffix) {
|
||||
NullabilitySuffix.none => '',
|
||||
NullabilitySuffix.star => '*',
|
||||
NullabilitySuffix.question => '?',
|
||||
};
|
||||
switch (type) {
|
||||
case DynamicType():
|
||||
return ['dynamic'];
|
||||
case FunctionType(
|
||||
:var returnType,
|
||||
:var typeParameters,
|
||||
:var formalParameters,
|
||||
):
|
||||
var params = <List<Object?>>[];
|
||||
var optionalParams = <List<Object?>>[];
|
||||
var namedParams = <String, List<Object?>>{};
|
||||
for (var formalParameter in formalParameters) {
|
||||
if (formalParameter.isNamed) {
|
||||
namedParams[formalParameter.name3!] = [
|
||||
if (formalParameter.isRequired) 'required ',
|
||||
..._describeType(formalParameter.type),
|
||||
];
|
||||
} else if (formalParameter.isOptional) {
|
||||
optionalParams.add(_describeType(formalParameter.type));
|
||||
} else {
|
||||
params.add(_describeType(formalParameter.type));
|
||||
}
|
||||
}
|
||||
if (optionalParams.isNotEmpty) {
|
||||
params.add(optionalParams.separate(prefix: '[', suffix: ']'));
|
||||
}
|
||||
if (namedParams.isNotEmpty) {
|
||||
params.add(
|
||||
namedParams.entries
|
||||
.sortedBy((e) => e.key)
|
||||
.map((e) => [...e.value, ' ${e.key}'])
|
||||
.separate(prefix: '{', suffix: '}'),
|
||||
);
|
||||
}
|
||||
return <Object?>[
|
||||
..._describeType(returnType),
|
||||
' Function',
|
||||
if (typeParameters.isNotEmpty)
|
||||
...typeParameters
|
||||
.map(_describeTypeParameter)
|
||||
.separate(prefix: '<', suffix: '>'),
|
||||
'(',
|
||||
...params.separate(),
|
||||
')',
|
||||
suffix,
|
||||
];
|
||||
case InterfaceType(:var element3, :var typeArguments):
|
||||
_potentiallyDanglingReferences.addLast(element3);
|
||||
return [
|
||||
_uniqueNamer.name(element3),
|
||||
if (typeArguments.isNotEmpty)
|
||||
...typeArguments
|
||||
.map(_describeType)
|
||||
.separate(prefix: '<', suffix: '>'),
|
||||
suffix,
|
||||
];
|
||||
case RecordType(:var positionalFields, :var namedFields):
|
||||
if (positionalFields.length == 1 && namedFields.isEmpty) {
|
||||
return [
|
||||
'(',
|
||||
..._describeType(positionalFields[0].type),
|
||||
',)',
|
||||
suffix,
|
||||
];
|
||||
}
|
||||
return [
|
||||
...[
|
||||
for (var positionalField in positionalFields)
|
||||
_describeType(positionalField.type),
|
||||
if (namedFields.isNotEmpty)
|
||||
namedFields
|
||||
.sortedBy((f) => f.name)
|
||||
.map((f) => [..._describeType(f.type), ' ', f.name])
|
||||
.separate(prefix: '{', suffix: '}'),
|
||||
].separate(prefix: '(', suffix: ')'),
|
||||
suffix,
|
||||
];
|
||||
case TypeParameterType(:var element3):
|
||||
return [element3.name3!, suffix];
|
||||
case VoidType():
|
||||
return ['void'];
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('Unexpected type: $runtimeType');
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a list of objects which, when their string representations are
|
||||
/// concatenated, describes [typeParameter].
|
||||
List<Object?> _describeTypeParameter(TypeParameterElement typeParameter) {
|
||||
return [
|
||||
typeParameter.name3!,
|
||||
if (typeParameter.bound case var bound?) ...[
|
||||
' extends ',
|
||||
..._describeType(bound),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/// Appends information to [node] describing [element].
|
||||
void _dumpElement(Element element, Node<MemberSortKey> node) {
|
||||
var enclosingElement = element.enclosingElement2;
|
||||
if (enclosingElement is LibraryElement &&
|
||||
!element.isInPublicApiOf(_pkgName)) {
|
||||
if (!enclosingElement.uri.isIn(_pkgName)) {
|
||||
node.text.add(' (referenced)');
|
||||
} else {
|
||||
node.text.add(' (non-public)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
var parentheticals = <List<Object?>>[];
|
||||
switch (element) {
|
||||
case TypeAliasElement(:var aliasedType, :var typeParameters2):
|
||||
List<Object?> description = ['type alias'];
|
||||
if (typeParameters2.isNotEmpty) {
|
||||
description.addAll(
|
||||
typeParameters2
|
||||
.map(_describeTypeParameter)
|
||||
.separate(prefix: '<', suffix: '>'),
|
||||
);
|
||||
}
|
||||
description.addAll([' for ', ..._describeType(aliasedType)]);
|
||||
parentheticals.add(description);
|
||||
case InstanceElement():
|
||||
switch (element) {
|
||||
case InterfaceElement(
|
||||
:var typeParameters2,
|
||||
:var supertype,
|
||||
:var interfaces,
|
||||
):
|
||||
List<Object?> instanceDescription = [
|
||||
switch (element) {
|
||||
ClassElement() => 'class',
|
||||
EnumElement() => 'enum',
|
||||
MixinElement() => 'mixin',
|
||||
dynamic(:var runtimeType) => 'TODO: $runtimeType',
|
||||
},
|
||||
];
|
||||
if (typeParameters2.isNotEmpty) {
|
||||
instanceDescription.addAll(
|
||||
typeParameters2
|
||||
.map(_describeTypeParameter)
|
||||
.separate(prefix: '<', suffix: '>'),
|
||||
);
|
||||
}
|
||||
if (element is! EnumElement && supertype != null) {
|
||||
instanceDescription.addAll([
|
||||
' extends ',
|
||||
..._describeType(supertype),
|
||||
]);
|
||||
}
|
||||
if (element is MixinElement &&
|
||||
element.superclassConstraints.isNotEmpty) {
|
||||
instanceDescription.addAll(
|
||||
element.superclassConstraints
|
||||
.map(_describeType)
|
||||
.separate(prefix: ' on '),
|
||||
);
|
||||
}
|
||||
if (interfaces.isNotEmpty) {
|
||||
instanceDescription.addAll(
|
||||
interfaces.map(_describeType).separate(prefix: ' implements '),
|
||||
);
|
||||
}
|
||||
parentheticals.add(instanceDescription);
|
||||
if (element is ClassElement && element.isSealed) {
|
||||
var parenthetical = <Object>['sealed'];
|
||||
parentheticals.add(parenthetical);
|
||||
if (_getOrComputeImmediateSubinterfaceMap(
|
||||
element.library2,
|
||||
)[element]
|
||||
case var subinterfaces?) {
|
||||
parenthetical.add(' (immediate subtypes: ');
|
||||
// Note: it's tempting to just do
|
||||
// `subinterfaces.map(_uniqueNamer.name).join(', ')`, but that
|
||||
// won't work, because the names returned by
|
||||
// `UniqueName.toString()` aren't finalized until we've visited
|
||||
// the entire API and seen if there are class names that need to
|
||||
// be disambiguated. So we accumulate the `UniqueName` objects
|
||||
// into the `parenthetical` list and rely on `printNodes`
|
||||
// converting everything to a string when the final API
|
||||
// description is being output.
|
||||
var commaNeeded = false;
|
||||
for (var subinterface in subinterfaces) {
|
||||
if (commaNeeded) {
|
||||
parenthetical.add(', ');
|
||||
} else {
|
||||
commaNeeded = true;
|
||||
}
|
||||
parenthetical.add(_uniqueNamer.name(subinterface));
|
||||
}
|
||||
parenthetical.add(')');
|
||||
}
|
||||
}
|
||||
case ExtensionElement(:var extendedType):
|
||||
parentheticals.add([
|
||||
'extension on ',
|
||||
..._describeType(extendedType),
|
||||
]);
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('Unexpected element: $runtimeType');
|
||||
}
|
||||
for (var member in element.children2.sortedBy((m) => m.name3 ?? '')) {
|
||||
if (member.name3 case var name? when name.startsWith('_')) {
|
||||
// Ignore private members
|
||||
continue;
|
||||
}
|
||||
if (member is FieldElement) {
|
||||
// Ignore fields; we care about the getters and setters they induce.
|
||||
continue;
|
||||
}
|
||||
if (member is ConstructorElement &&
|
||||
element is ClassElement &&
|
||||
element.isAbstract &&
|
||||
(element.isFinal || element.isInterface || element.isSealed)) {
|
||||
// The class can't be constructed from outside of the library that
|
||||
// declares it, so its constructors aren't part of the public API.
|
||||
continue;
|
||||
}
|
||||
if (member is ConstructorElement && element is EnumElement) {
|
||||
// Enum constructors can't be called from outside the enum itself,
|
||||
// so they aren't part of the public API.
|
||||
continue;
|
||||
}
|
||||
var childNode = Node<MemberSortKey>();
|
||||
childNode.text.add(member.apiName);
|
||||
_dumpElement(member, childNode);
|
||||
node.childNodes.add((MemberSortKey(member), childNode));
|
||||
}
|
||||
case TopLevelFunctionElement(:var type):
|
||||
parentheticals.add(['function: ', ..._describeType(type)]);
|
||||
case ExecutableElement(:var isStatic):
|
||||
String maybeStatic = isStatic ? 'static ' : '';
|
||||
switch (element) {
|
||||
case GetterElement(:var type):
|
||||
parentheticals.add([
|
||||
'${maybeStatic}getter: ',
|
||||
..._describeType(type.returnType),
|
||||
]);
|
||||
case SetterElement(:var type):
|
||||
parentheticals.add([
|
||||
'${maybeStatic}setter: ',
|
||||
..._describeType(type.formalParameters.single.type),
|
||||
]);
|
||||
case MethodElement(:var type):
|
||||
parentheticals.add([
|
||||
'${maybeStatic}method: ',
|
||||
..._describeType(type),
|
||||
]);
|
||||
case ConstructorElement(:var type):
|
||||
parentheticals.add(['constructor: ', ..._describeType(type)]);
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('Unexpected element: $runtimeType');
|
||||
}
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('Unexpected element: $runtimeType');
|
||||
}
|
||||
|
||||
if (element case Annotatable element) {
|
||||
if (element.metadata2.hasDeprecated) {
|
||||
parentheticals.add(['deprecated']);
|
||||
}
|
||||
if (element.metadata2.hasExperimental) {
|
||||
parentheticals.add(['experimental']);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentheticals.isNotEmpty) {
|
||||
node.text.addAll(parentheticals.separate(prefix: ' (', suffix: ')'));
|
||||
}
|
||||
if (node.childNodes.isNotEmpty) {
|
||||
node.text.add(':');
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends information to [node] describing [element].
|
||||
void _dumpLibrary(LibraryElement library, Node<MemberSortKey> node) {
|
||||
var uri = library.uri;
|
||||
node.text.addAll([uri, ':']);
|
||||
var definedNames = library.exportNamespace.definedNames2;
|
||||
for (var key in definedNames.keys.sorted()) {
|
||||
var element = definedNames[key]!;
|
||||
var childNode =
|
||||
Node<MemberSortKey>()..text.add(_uniqueNamer.name(element));
|
||||
if (!_dumpedTopLevelElements.add(element)) {
|
||||
childNode.text.add(' (see above)');
|
||||
} else {
|
||||
_dumpElement(element, childNode);
|
||||
}
|
||||
node.childNodes.add((MemberSortKey(element), childNode));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a map from each sealed class in [library] to the set of its
|
||||
/// immediate sub-interfaces.
|
||||
///
|
||||
/// If this method has been called before with the same [library], a cached
|
||||
/// map is returned from [_immediateSubinterfaceCache]. Otherwise a fresh map
|
||||
/// is computed.
|
||||
Map<ClassElement, Set<InterfaceElement>>
|
||||
_getOrComputeImmediateSubinterfaceMap(LibraryElement library) {
|
||||
if (_immediateSubinterfaceCache[library] case var m?) return m;
|
||||
var result = <ClassElement, Set<InterfaceElement>>{};
|
||||
for (var interface in [
|
||||
...library.classes,
|
||||
...library.mixins,
|
||||
...library.enums,
|
||||
...library.extensionTypes,
|
||||
]..sortBy((e) => e.name3!)) {
|
||||
for (var superinterface in [
|
||||
interface.supertype,
|
||||
...interface.interfaces,
|
||||
...interface.mixins,
|
||||
if (interface is MixinElement) ...interface.superclassConstraints,
|
||||
]) {
|
||||
if (superinterface == null) continue;
|
||||
var superinterfaceElement = superinterface.element3;
|
||||
if (superinterfaceElement is ClassElement &&
|
||||
superinterfaceElement.isSealed) {
|
||||
(result[superinterfaceElement] ??= {}).add(interface);
|
||||
}
|
||||
}
|
||||
}
|
||||
_immediateSubinterfaceCache[library] = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Element categorization used by [MemberSortKey].
|
||||
enum MemberCategory {
|
||||
constructor,
|
||||
propertyAccessor,
|
||||
topLevelFunctionOrMethod,
|
||||
interface,
|
||||
extension,
|
||||
typeAlias,
|
||||
}
|
||||
|
||||
/// Sort key used to sort elements in the output.
|
||||
class MemberSortKey implements Comparable<MemberSortKey> {
|
||||
final bool _isInstanceMember;
|
||||
final MemberCategory _category;
|
||||
final String _name;
|
||||
|
||||
MemberSortKey(Element element)
|
||||
: _isInstanceMember = _computeIsInstanceMember(element),
|
||||
_category = _computeCategory(element),
|
||||
_name = element.name3!;
|
||||
|
||||
@override
|
||||
int compareTo(MemberSortKey other) {
|
||||
if ((_isInstanceMember ? 1 : 0).compareTo(other._isInstanceMember ? 1 : 0)
|
||||
case var value when value != 0) {
|
||||
return value;
|
||||
}
|
||||
if (_category.index.compareTo(other._category.index) case var value
|
||||
when value != 0) {
|
||||
return value;
|
||||
}
|
||||
return _name.compareTo(other._name);
|
||||
}
|
||||
|
||||
static MemberCategory _computeCategory(Element element) => switch (element) {
|
||||
ConstructorElement() => MemberCategory.constructor,
|
||||
PropertyAccessorElement() => MemberCategory.propertyAccessor,
|
||||
TopLevelFunctionElement() => MemberCategory.topLevelFunctionOrMethod,
|
||||
MethodElement() => MemberCategory.topLevelFunctionOrMethod,
|
||||
InterfaceElement() => MemberCategory.interface,
|
||||
ExtensionElement() => MemberCategory.extension,
|
||||
TypeAliasElement() => MemberCategory.typeAlias,
|
||||
dynamic(:var runtimeType) =>
|
||||
throw UnimplementedError('Unexpected element: $runtimeType'),
|
||||
};
|
||||
|
||||
static bool _computeIsInstanceMember(Element element) =>
|
||||
element.enclosingElement2 is InstanceElement &&
|
||||
switch (element) {
|
||||
ExecutableElement(:var isStatic) => !isStatic,
|
||||
dynamic(:var runtimeType) =>
|
||||
throw UnimplementedError('Unexpected element: $runtimeType'),
|
||||
};
|
||||
}
|
||||
|
||||
/// A node to be printed to the output.
|
||||
class Node<ChildSortKey extends Comparable<ChildSortKey>> {
|
||||
/// A list of objects which, when their string representations are
|
||||
/// concatenated, is the text that should be displayed on the first line of
|
||||
/// the node.
|
||||
///
|
||||
/// The reason this is a list rather than a single string is to allow elements
|
||||
/// of the list to be [UniqueName] objects, which may acquire a disambiguation
|
||||
/// suffix at a later time.
|
||||
final text = <Object?>[];
|
||||
|
||||
/// A list of child nodes, paired with a sort key indicating the order in
|
||||
/// which they should be output.
|
||||
final childNodes = <(ChildSortKey, Node)>[];
|
||||
|
||||
/// Outputs [childNodes], prepending [prefix] to every line.
|
||||
void printChildren(StringSink sink, {required String prefix}) {
|
||||
_printNodes(sink, childNodes, prefix: prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/// Object that will have a unique string representation within the context of a
|
||||
/// given [UniqueNamer] instance.
|
||||
///
|
||||
/// If two or more [UniqueName] objects are constructed with reference to the
|
||||
/// same [UniqueNamer], and they have the same [_nameHint], then all such
|
||||
/// objects' [toString] methods will append a unique suffix of the form
|
||||
/// `@INTEGER`, so that the resulting strings are unique.
|
||||
class UniqueName {
|
||||
/// The name that will be returned by [toString] if no disambiguation is
|
||||
/// needed.
|
||||
final String _nameHint;
|
||||
|
||||
/// If not `Null`, the integer that [toString] will use to disambiguate this
|
||||
/// [UniqueName] from other ones with the same [_nameHint].
|
||||
int? _disambiguator;
|
||||
|
||||
UniqueName(UniqueNamer uniqueNamer, this._nameHint) {
|
||||
// The uniqueness guarantee depends on `_nameHint` not containing an `@`.
|
||||
assert(!_nameHint.contains('@'));
|
||||
var conflicts = uniqueNamer._conflicts[_nameHint] ??= [];
|
||||
if (conflicts.length == 1) {
|
||||
conflicts[0]._disambiguator = 1;
|
||||
}
|
||||
conflicts.add(this);
|
||||
if (conflicts.length > 1) {
|
||||
_disambiguator = conflicts.length;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
[
|
||||
_nameHint,
|
||||
if (_disambiguator case var disambiguator?) '@$disambiguator',
|
||||
].join();
|
||||
}
|
||||
|
||||
/// Manager of unique names for elements.
|
||||
class UniqueNamer {
|
||||
final _names = <Element, UniqueName>{};
|
||||
final _conflicts = <String, List<UniqueName>>{};
|
||||
|
||||
/// Returns a [UniqueName] object whose [toString] method will produce a
|
||||
/// unique name for [element].
|
||||
UniqueName name(Element element) =>
|
||||
_names[element] ??= UniqueName(this, element.apiName);
|
||||
}
|
||||
|
||||
/// URI categorization used by [UriSortKey].
|
||||
enum UriCategory { inPackage, notInPackage }
|
||||
|
||||
/// Sort key used to sort libraries in the output.
|
||||
///
|
||||
/// Libraries in the specified package will be output first (sorted by URI),
|
||||
/// followed by libraries not in the package.
|
||||
class UriSortKey implements Comparable<UriSortKey> {
|
||||
final UriCategory _category;
|
||||
final String _uriString;
|
||||
|
||||
UriSortKey(Uri uri, String pkgName)
|
||||
: _category =
|
||||
uri.isIn(pkgName) ? UriCategory.inPackage : UriCategory.notInPackage,
|
||||
_uriString = uri.toString();
|
||||
|
||||
@override
|
||||
int compareTo(UriSortKey other) {
|
||||
if (_category.index.compareTo(other._category.index) case var value
|
||||
when value != 0) {
|
||||
return value;
|
||||
}
|
||||
return _uriString.compareTo(other._uriString);
|
||||
}
|
||||
}
|
||||
|
||||
extension on Iterable<Iterable<Object?>> {
|
||||
/// Forms a list containing [prefix], followed by the elements of `this`
|
||||
/// (separated by [separator]), followed by [suffix].
|
||||
List<Object?> separate({
|
||||
String separator = ', ',
|
||||
String prefix = '',
|
||||
String suffix = '',
|
||||
}) {
|
||||
var result = <Object?>[prefix];
|
||||
var first = true;
|
||||
for (var item in this) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
result.add(separator);
|
||||
}
|
||||
result.addAll(item);
|
||||
}
|
||||
result.add(suffix);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
extension on Element {
|
||||
/// Returns the appropriate name for describing the element in `api.txt`.
|
||||
///
|
||||
/// The name is the same as [name3], but with `=` appended for setters.
|
||||
String get apiName {
|
||||
var apiName = name3!;
|
||||
if (this is SetterElement) {
|
||||
apiName += '=';
|
||||
}
|
||||
return apiName;
|
||||
}
|
||||
|
||||
bool isInPublicApiOf(String packageName) {
|
||||
if (this case PropertyAccessorElement(
|
||||
isSynthetic: true,
|
||||
:var variable3?,
|
||||
) when variable3.isInPublicApiOf(packageName)) {
|
||||
return true;
|
||||
}
|
||||
if (this case Annotatable(
|
||||
metadata2: Metadata(:var annotations),
|
||||
) when annotations.any(_isPublicApiAnnotation)) {
|
||||
return true;
|
||||
}
|
||||
if (name3 case var name? when !name.isPublic) return false;
|
||||
if (library2!.uri.isInPublicLibOf(packageName)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _isPublicApiAnnotation(ElementAnnotation annotation) {
|
||||
if (annotation.computeConstantValue() case DartObject(
|
||||
type: InterfaceType(
|
||||
element3: InterfaceElement(name3: 'AnalyzerPublicApi'),
|
||||
),
|
||||
)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on String {
|
||||
bool get isPublic => !startsWith('_');
|
||||
}
|
||||
|
||||
extension on Uri {
|
||||
bool isIn(String packageName) =>
|
||||
scheme == 'package' &&
|
||||
pathSegments.isNotEmpty &&
|
||||
pathSegments[0] == packageName;
|
||||
|
||||
bool isInPublicLibOf(String packageName) =>
|
||||
scheme == 'package' &&
|
||||
pathSegments.length > 1 &&
|
||||
pathSegments[0] == packageName &&
|
||||
pathSegments[1] != 'src';
|
||||
}
|
||||
@@ -11,6 +11,7 @@ resolution: workspace
|
||||
dependencies:
|
||||
analyzer_testing: any
|
||||
analyzer: any
|
||||
collection: any
|
||||
dart_style: any
|
||||
package_config: any
|
||||
path: any
|
||||
|
||||
Reference in New Issue
Block a user