// 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 '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/element2.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_utilities/package_root.dart' as pkg_root; import 'package:analyzer_utilities/package_root.dart'; import 'package:analyzer_utilities/tools.dart'; import 'package:collection/collection.dart'; import 'package:path/path.dart'; Future main() async { await GeneratedContent.generateAll(analyzerPkgPath, allTargets); } /// A list of all targets generated by this code generator. final List 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 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.single; 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>( 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 = {}; /// 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(); final _uniqueNamer = UniqueNamer(); /// Cache of values returned by [_getOrComputeImmediateSubinterfaceMap], to /// avoid unnecessary recomputation. final _immediateSubinterfaceCache = >>{}; /// 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> build(AnalysisContext context) async { var nodes = >{}; 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(); _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() ..text.add(_uniqueNamer.name(element)); _dumpElement(element, childNode); (nodes[containingLibraryUri] ??= Node() ..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 _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 = >[]; var optionalParams = >[]; var namedParams = >{}; 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 [ ..._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 _describeTypeParameter(TypeParameterElement2 typeParameter) { return [ typeParameter.name3!, if (typeParameter.bound case var bound?) ...[ ' extends ', ..._describeType(bound) ] ]; } /// Appends information to [node] describing [element]. void _dumpElement(Element2 element, Node node) { var enclosingElement = element.enclosingElement2; if (enclosingElement is LibraryElement2 && !element.isInAnalyzerPublicApi) { if (!enclosingElement.uri.isInAnalyzer) { node.text.add(' (referenced)'); } else { node.text.add(' (non-public)'); } return; } var parentheticals = >[]; switch (element) { case InstanceElement2(): switch (element) { case InterfaceElement2( :var typeParameters2, :var supertype, :var interfaces ): List instanceDescription = [ switch (element) { ClassElement2() => 'class', EnumElement2() => 'enum', MixinElement2() => 'mixin', dynamic(:var runtimeType) => 'TODO: $runtimeType', } ]; if (typeParameters2.isNotEmpty) { instanceDescription.addAll(typeParameters2 .map(_describeTypeParameter) .separate(prefix: '<', suffix: '>')); } if (element is! EnumElement2 && supertype != null) { instanceDescription .addAll([' extends ', ..._describeType(supertype)]); } if (element is MixinElement2 && 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 ClassElement2 && element.isSealed) { var parenthetical = ['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 ExtensionElement2(: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 FieldElement2) { // Ignore fields; we care about the getters and setters they induce. continue; } if (member is ConstructorElement2 && element is ClassElement2 && 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 ConstructorElement2 && element is EnumElement2) { // Enum constructors can't be called from outside the enum itself, // so they aren't part of the public API. continue; } var childNode = Node(); childNode.text.add(member.apiName); _dumpElement(member, childNode); node.childNodes.add((MemberSortKey(member), childNode)); } case TopLevelFunctionElement(:var type): parentheticals.add(['function: ', ..._describeType(type)]); case ExecutableElement2(: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 MethodElement2(:var type): parentheticals .add(['${maybeStatic}method: ', ..._describeType(type)]); case ConstructorElement2(: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(LibraryElement2 library, Node 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() ..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> _getOrComputeImmediateSubinterfaceMap(LibraryElement2 library) { if (_immediateSubinterfaceCache[library] case var m?) return m; var result = >{}; 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 MixinElement2) ...interface.superclassConstraints ]) { if (superinterface == null) continue; var superinterfaceElement = superinterface.element3; if (superinterfaceElement is ClassElement2 && 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 } /// Sort key used to sort elements in the output. class MemberSortKey implements Comparable { final bool _isInstanceMember; final MemberCategory _category; final String _name; MemberSortKey(Element2 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(Element2 element) => switch (element) { ConstructorElement2() => MemberCategory.constructor, PropertyAccessorElement2() => MemberCategory.propertyAccessor, TopLevelFunctionElement() => MemberCategory.topLevelFunctionOrMethod, MethodElement2() => MemberCategory.topLevelFunctionOrMethod, InterfaceElement2() => MemberCategory.interface, ExtensionElement2() => MemberCategory.extension, dynamic(:var runtimeType) => throw UnimplementedError('Unexpected element: $runtimeType') }; static bool _computeIsInstanceMember(Element2 element) => element.enclosingElement2 is InstanceElement2 && switch (element) { ExecutableElement2(:var isStatic) => !isStatic, dynamic(:var runtimeType) => throw UnimplementedError('Unexpected element: $runtimeType') }; } /// A node to be printed to the output. class Node> { /// 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 = []; /// 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 = {}; final _conflicts = >{}; /// Returns a [UniqueName] object whose [toString] method will produce a /// unique name for [element]. UniqueName name(Element2 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 { 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> { /// Forms a list containing [prefix], followed by the elements of `this` /// (separated by [separator]), followed by [suffix]. List separate( {String separator = ', ', String prefix = '', String suffix = ''}) { var result = [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 Element2 { /// 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 PropertyAccessorElement2( 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: InterfaceElement2(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, }; }