| // 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'; |
| } |