diff --git a/pkg/analyzer/api.txt b/pkg/analyzer/api.txt index 5a8168ea6d8..d0c32578925 100644 --- a/pkg/analyzer/api.txt +++ b/pkg/analyzer/api.txt @@ -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) diff --git a/pkg/analyzer/lib/dart/element/element.dart b/pkg/analyzer/lib/dart/element/element.dart index 7dffcdef2a5..60d500d234f 100644 --- a/pkg/analyzer/lib/dart/element/element.dart +++ b/pkg/analyzer/lib/dart/element/element.dart @@ -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; diff --git a/pkg/analyzer/lib/src/dart/element/element.dart b/pkg/analyzer/lib/src/dart/element/element.dart index cc91bfe9ff0..99a6bf10882 100644 --- a/pkg/analyzer/lib/src/dart/element/element.dart +++ b/pkg/analyzer/lib/src/dart/element/element.dart @@ -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; diff --git a/pkg/analyzer/tool/api/generate.dart b/pkg/analyzer/tool/api/generate.dart index aa99d5b7e57..d27cdeef57d 100644 --- a/pkg/analyzer/tool/api/generate.dart +++ b/pkg/analyzer/tool/api/generate.dart @@ -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 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.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>( - 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(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 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 = >[]; - switch (element) { - case TypeAliasElement(:var aliasedType, :var typeParameters2): - List 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 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 = ['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(); - 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 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(LibraryElement 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 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 { - 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> { - /// 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(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 { - 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 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, - }; -} diff --git a/pkg/analyzer/tool/api/generate_test.dart b/pkg/analyzer/tool/api/generate_test.dart index 180c11e0a6f..b1275cfc985 100644 --- a/pkg/analyzer/tool/api/generate_test.dart +++ b/pkg/analyzer/tool/api/generate_test.dart @@ -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'; diff --git a/pkg/analyzer_testing/api.txt b/pkg/analyzer_testing/api.txt new file mode 100644 index 00000000000..3f5445ec3f6 --- /dev/null +++ b/pkg/analyzer_testing/api.txt @@ -0,0 +1,4 @@ +package:analyzer_testing/package_root.dart: + packageRoot (static getter: String) +dart:core: + String (referenced) diff --git a/pkg/analyzer_testing/pubspec.yaml b/pkg/analyzer_testing/pubspec.yaml index d3d6c293da0..fac1f0a8d07 100644 --- a/pkg/analyzer_testing/pubspec.yaml +++ b/pkg/analyzer_testing/pubspec.yaml @@ -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 diff --git a/pkg/analyzer_testing/tool/api/generate.dart b/pkg/analyzer_testing/tool/api/generate.dart new file mode 100644 index 00000000000..c5ef7ddb4d0 --- /dev/null +++ b/pkg/analyzer_testing/tool/api/generate.dart @@ -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 main() async { + await GeneratedContent.generateAll(analyzerTestingPkgPath, allTargets); +} + +/// The path to the `analyzer_testing` package. +final String analyzerTestingPkgPath = normalize( + join(pkg_root.packageRoot, 'analyzer_testing'), +); diff --git a/pkg/analyzer_testing/tool/api/generate_test.dart b/pkg/analyzer_testing/tool/api/generate_test.dart new file mode 100644 index 00000000000..f873279ebd1 --- /dev/null +++ b/pkg/analyzer_testing/tool/api/generate_test.dart @@ -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 main() async { + await GeneratedContent.checkAll( + analyzerTestingPkgPath, + join(analyzerTestingPkgPath, 'tool', 'api', 'generate.dart'), + allTargets, + ); +} diff --git a/pkg/analyzer_utilities/lib/tool/README.md b/pkg/analyzer_utilities/lib/tool/README.md new file mode 100644 index 00000000000..2d28ece5127 --- /dev/null +++ b/pkg/analyzer_utilities/lib/tool/README.md @@ -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'. diff --git a/pkg/analyzer_utilities/lib/tool/api.dart b/pkg/analyzer_utilities/lib/tool/api.dart new file mode 100644 index 00000000000..9ce1bfc51e1 --- /dev/null +++ b/pkg/analyzer_utilities/lib/tool/api.dart @@ -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 allTargets = [ + GeneratedFile('api.txt', computeApiTxtContents), +]; + +/// Computes what should be written to the `api.txt` file. +Future 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>( + 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 = {}; + + /// 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 = + >>{}; + + 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> 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.isInPublicLibOf(_pkgName)) { + 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, _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 _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(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 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 = >[]; + switch (element) { + case TypeAliasElement(:var aliasedType, :var typeParameters2): + List 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 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 = ['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(); + 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 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(LibraryElement 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 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 { + 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> { + /// 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(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 { + 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> { + /// 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 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'; +} diff --git a/pkg/analyzer_utilities/pubspec.yaml b/pkg/analyzer_utilities/pubspec.yaml index 38453699076..b20868acdd1 100644 --- a/pkg/analyzer_utilities/pubspec.yaml +++ b/pkg/analyzer_utilities/pubspec.yaml @@ -11,6 +11,7 @@ resolution: workspace dependencies: analyzer_testing: any analyzer: any + collection: any dart_style: any package_config: any path: any