| // Copyright (c) 2019, 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. |
| |
| import 'dart:collection'; |
| |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/scope.dart'; |
| import 'package:analyzer/dart/element/type_system.dart'; |
| import 'package:analyzer/source/line_info.dart'; |
| // ignore: implementation_imports |
| import 'package:analyzer/src/generated/sdk.dart' show SdkLibrary; |
| import 'package:dartdoc/src/model/comment_referable.dart'; |
| import 'package:dartdoc/src/model/model.dart'; |
| import 'package:dartdoc/src/package_meta.dart' show PackageMeta; |
| import 'package:dartdoc/src/warnings.dart'; |
| |
| class _LibrarySentinel implements Library { |
| @override |
| dynamic noSuchMethod(Invocation invocation) => |
| throw UnimplementedError('No members on Library.sentinel are accessible'); |
| } |
| |
| class Library extends ModelElement |
| with |
| Categorization, |
| TopLevelContainer, |
| CanonicalFor, |
| HideConstantImplementations { |
| @override |
| final LibraryElement element; |
| |
| final Set<Element> _exportedAndLocalElements; |
| final String _restoredUri; |
| |
| @override |
| final Package package; |
| |
| /// A [Library] value used as a sentinel in three cases: |
| /// |
| /// * the library for `dynamic` and `Never` |
| /// * the library for type parameters |
| /// * the library passed up to [ModelElement.library] when constructing a |
| /// `Library`, via the super constructor. |
| /// |
| /// TODO(srawlins): I think this last case demonstrates that |
| /// [ModelElement.library] should not be a field, and instead should be an |
| /// abstract getter. |
| static final Library sentinel = _LibrarySentinel(); |
| |
| Library._(this.element, PackageGraph packageGraph, this.package, |
| this._restoredUri, this._exportedAndLocalElements) |
| : super(sentinel, packageGraph); |
| |
| factory Library.fromLibraryResult(DartDocResolvedLibrary resolvedLibrary, |
| PackageGraph packageGraph, Package package) { |
| packageGraph.gatherModelNodes(resolvedLibrary); |
| |
| var element = resolvedLibrary.element; |
| |
| var exportedAndLocalElements = { |
| // Initialize the list of elements defined in this library and |
| // exported via its export directives. |
| ...element.exportNamespace.definedNames.values, |
| // TODO(jcollins-g): Consider switch to [_libraryElement.topLevelElements]. |
| ..._getDefinedElements(element.definingCompilationUnit), |
| ...element.parts |
| .map((e) => e.uri) |
| .whereType<DirectiveUriWithUnit>() |
| .map((part) => part.unit) |
| }; |
| var library = Library._( |
| element, |
| packageGraph, |
| package, |
| resolvedLibrary.element.source.uri.toString(), |
| exportedAndLocalElements); |
| |
| package.allLibraries.add(library); |
| return library; |
| } |
| |
| static Iterable<Element> _getDefinedElements( |
| CompilationUnitElement compilationUnit) => |
| [ |
| ...compilationUnit.accessors, |
| ...compilationUnit.classes, |
| ...compilationUnit.enums, |
| ...compilationUnit.extensions, |
| ...compilationUnit.extensionTypes, |
| ...compilationUnit.functions, |
| ...compilationUnit.mixins, |
| ...compilationUnit.topLevelVariables, |
| ...compilationUnit.typeAliases, |
| ]; |
| |
| /// Allow scope for Libraries. |
| @override |
| Scope get scope => element.scope; |
| |
| bool get isInSdk => element.isInSdk; |
| |
| /// [allModelElements] resolved to their original names. |
| /// |
| /// A collection of [ModelElement.fullyQualifiedName]s for [ModelElement]s |
| /// documented with this library, but these ModelElements and names correspond |
| /// to the defining library where each originally came from with respect |
| /// to inheritance and reexporting. Most useful for error reporting. |
| late final Iterable<String> allOriginalModelElementNames = |
| allModelElements.map((e) { |
| if (e is GetterSetterCombo) { |
| Accessor? getter; |
| Accessor? setter; |
| var elementGetter = e.getter; |
| if (elementGetter != null) { |
| getter = modelBuilder.fromElement(elementGetter.element) as Accessor; |
| } |
| var elementSetter = e.setter; |
| if (elementSetter != null) { |
| setter = modelBuilder.fromElement(elementSetter.element) as Accessor; |
| } |
| return modelBuilder |
| .fromPropertyInducingElement(e.element, |
| modelBuilder.fromElement(e.element.library!) as Library, |
| getter: getter, setter: setter) |
| .fullyQualifiedName; |
| } |
| return modelBuilder.fromElement(e.element).fullyQualifiedName; |
| }).toList(growable: false); |
| |
| @override |
| CharacterLocation? get characterLocation { |
| if (element.nameOffset == -1) { |
| assert(isAnonymous, |
| 'Only anonymous libraries are allowed to have no declared location'); |
| return CharacterLocation(1, 1); |
| } |
| return super.characterLocation; |
| } |
| |
| @override |
| CompilationUnitElement get compilationUnitElement => |
| element.definingCompilationUnit; |
| |
| @override |
| Iterable<Class> get classes => allClasses.where((c) => !c.isErrorOrException); |
| |
| @override |
| late final Iterable<Extension> extensions = _exportedAndLocalElements |
| .whereType<ExtensionElement>() |
| .map((e) => modelBuilder.from(e, this) as Extension) |
| .toList(growable: false); |
| |
| @override |
| late final Iterable<ExtensionType> extensionTypes = _exportedAndLocalElements |
| .whereType<ExtensionTypeElement>() |
| .map((e) => modelBuilder.from(e, this) as ExtensionType) |
| .toList(growable: false); |
| |
| SdkLibrary? get _sdkLib => |
| packageGraph.sdkLibrarySources[element.librarySource]; |
| |
| @override |
| bool get isPublic { |
| if (!super.isPublic) return false; |
| final sdkLib = _sdkLib; |
| if (sdkLib != null && (sdkLib.isInternal || !sdkLib.isDocumented)) { |
| return false; |
| } |
| if ( |
| // TODO(srawlins): Stop supporting a 'name' here. |
| config.isLibraryExcluded(name) || |
| config.isLibraryExcluded(element.librarySource.uri.toString())) { |
| return false; |
| } |
| return true; |
| } |
| |
| @override |
| Iterable<TopLevelVariable> get constants => |
| _variables.where((v) => v.isConst); |
| |
| /// Map of each import prefix ('import "foo" as prefix;') to the set of |
| /// libraries which are imported via that prefix. |
| Map<String, Set<Library>> get prefixToLibrary { |
| var prefixToLibrary = <String, Set<Library>>{}; |
| // It is possible to have overlapping prefixes. |
| for (var i in element.libraryImports) { |
| var prefixName = i.prefix?.element.name; |
| // Ignore invalid imports. |
| if (prefixName != null && i.importedLibrary != null) { |
| prefixToLibrary |
| .putIfAbsent(prefixName, () => {}) |
| .add(modelBuilder.from(i.importedLibrary!, library) as Library); |
| } |
| } |
| return prefixToLibrary; |
| } |
| |
| late final String dirName = (isAnonymous ? nameFromPath : name) |
| .replaceAll(':', '-') |
| .replaceAll('/', '_'); |
| |
| /// Libraries are not enclosed by anything. |
| @override |
| ModelElement? get enclosingElement => null; |
| |
| @override |
| late final List<Enum> enums = _exportedAndLocalElements |
| .whereType<EnumElement>() |
| .map((e) => modelBuilder.from(e, this) as Enum) |
| .toList(growable: false); |
| |
| @override |
| late final List<Mixin> mixins = _exportedAndLocalElements |
| .whereType<MixinElement>() |
| .map((e) => modelBuilder.from(e, this) as Mixin) |
| .toList(growable: false); |
| |
| @override |
| late final List<Class> exceptions = |
| allClasses.where((c) => c.isErrorOrException).toList(growable: false); |
| |
| @override |
| String get filePath => '${library.dirName}/${fileStructure.fileName}'; |
| |
| String get sidebarPath => |
| '${library.dirName}/$dirName-library-sidebar.${fileStructure.fileType}'; |
| |
| /// The library template manually includes 'packages' in the left/above |
| /// sidebar. |
| @override |
| String? get aboveSidebarPath => null; |
| |
| @override |
| String get belowSidebarPath => sidebarPath; |
| |
| @override |
| late final List<ModelFunction> functions = _exportedAndLocalElements |
| .whereType<FunctionElement>() |
| .map((e) => modelBuilder.from(e, this) as ModelFunction) |
| .toList(growable: false); |
| |
| @override |
| String? get href { |
| if (!identical(canonicalModelElement, this)) { |
| return canonicalModelElement?.href; |
| } |
| return '${package.baseHref}$filePath'; |
| } |
| |
| bool get isAnonymous => element.name.isEmpty; |
| |
| @override |
| Kind get kind => Kind.library; |
| |
| @override |
| Library get library => this; |
| |
| @override |
| String get name { |
| var source = element.source; |
| if (source.uri.isScheme('dart')) { |
| // There are inconsistencies in library naming + URIs for the Dart |
| // SDK libraries; we rationalize them here. |
| if (source.uri.toString().contains('/')) { |
| return element.name.replaceFirst('dart.', 'dart:'); |
| } |
| return source.uri.toString(); |
| } else if (element.name.isNotEmpty) { |
| // An empty name indicates that the library is "implicitly named" with the |
| // empty string. That is, it either has no `library` directive, or it has |
| // a `library` directive with no name. |
| return element.name; |
| } |
| var baseName = pathContext.basename(source.fullName); |
| if (baseName.endsWith('.dart')) { |
| const dartExtensionLength = '.dart'.length; |
| return baseName.substring(0, baseName.length - dartExtensionLength); |
| } |
| return baseName; |
| } |
| |
| /// Generate a name for this library based on its location. |
| /// |
| /// nameFromPath provides filename collision-proofing for anonymous libraries |
| /// by incorporating more from the location of the anonymous library into |
| /// the name calculation. Simple cases (such as an anonymous library in |
| /// 'lib') are the same, but this will include slashes and possibly colons |
| /// for anonymous libraries in subdirectories or other packages. |
| late final String nameFromPath = |
| _getNameFromPath(element, package, _restoredUri); |
| |
| /// The name of the package we were defined in. |
| String get packageName => packageMeta?.name ?? ''; |
| |
| /// The real packageMeta, as opposed to the package we are documenting with. |
| late final PackageMeta? packageMeta = |
| packageGraph.packageMetaProvider.fromElement(element, config.sdkDir); |
| |
| /// All variables ("properties") except constants. |
| @override |
| late final Iterable<TopLevelVariable> properties = |
| _variables.where((v) => !v.isConst).toList(growable: false); |
| |
| @override |
| late final List<Typedef> typedefs = _exportedAndLocalElements |
| .whereType<TypeAliasElement>() |
| .map((e) => modelBuilder.from(e, this) as Typedef) |
| .toList(growable: false); |
| |
| TypeSystem get typeSystem => element.typeSystem; |
| |
| late final List<Class> allClasses = _exportedAndLocalElements |
| .whereType<ClassElement>() |
| .where((e) => e is! EnumElement && e is! MixinElement) |
| .map((e) => modelBuilder.from(e, this) as Class) |
| .toList(growable: false); |
| |
| List<TopLevelVariable> get _variables { |
| var elements = |
| _exportedAndLocalElements.whereType<TopLevelVariableElement>().toSet(); |
| elements.addAll(_exportedAndLocalElements |
| .whereType<PropertyAccessorElement>() |
| .map((a) => a.variable as TopLevelVariableElement)); |
| var variables = <TopLevelVariable>[]; |
| for (var element in elements) { |
| Accessor? getter; |
| var elementGetter = element.getter; |
| if (elementGetter != null) { |
| getter = modelBuilder.from(elementGetter, this) as Accessor; |
| } |
| Accessor? setter; |
| var elementSetter = element.setter; |
| if (elementSetter != null) { |
| setter = modelBuilder.from(elementSetter, this) as Accessor; |
| } |
| var me = modelBuilder.fromPropertyInducingElement(element, this, |
| getter: getter, setter: setter); |
| variables.add(me as TopLevelVariable); |
| } |
| return variables; |
| } |
| |
| /// Reverses URIs if needed to get a package URI. |
| /// |
| /// Not the same as [PackageGraph.name] because there we always strip all |
| /// path components; this function only strips the package prefix if the |
| /// library is part of the default package or if it is being documented |
| /// remotely. |
| static String _getNameFromPath( |
| LibraryElement element, Package package, String restoredUri) { |
| assert(!restoredUri.startsWith('file:'), |
| '"$restoredUri" must not start with "file:"'); |
| var hidePackage = package.documentedWhere == DocumentLocation.remote |
| ? package.packageMeta |
| : package.packageGraph.packageMeta; |
| var defaultPackagePrefix = 'package:$hidePackage/'; |
| |
| var name = restoredUri; |
| if (name.startsWith(defaultPackagePrefix)) { |
| name = name.substring(defaultPackagePrefix.length, name.length); |
| } |
| if (name.endsWith('.dart')) { |
| name = name.substring(0, name.length - '.dart'.length); |
| } |
| assert(!name.startsWith('file:')); |
| return name; |
| } |
| |
| /// A mapping of all [Element]s in this library to the [ModelElement]s which |
| /// represent them in dartdoc. |
| // Note: Keep this a late final field; converting to a getter (without further |
| // investigation) causes dartdoc to hang. |
| late final HashMap<Element, Set<ModelElement>> modelElementsMap = () { |
| var modelElements = HashMap<Element, Set<ModelElement>>(); |
| for (var modelElement in <ModelElement>[ |
| ...library.constants, |
| ...library.functions, |
| ...library.properties, |
| ...library.typedefs, |
| ...library.extensions.expand((e) => [e, ...e.allModelElements]), |
| ...library.extensionTypes.expand((e) => [e, ...e.allModelElements]), |
| ...library.allClasses.expand((c) => [c, ...c.allModelElements]), |
| ...library.enums.expand((e) => [e, ...e.allModelElements]), |
| ...library.mixins.expand((m) => [m, ...m.allModelElements]), |
| ]) { |
| modelElements |
| .putIfAbsent(modelElement.element, () => {}) |
| .add(modelElement); |
| } |
| modelElements.putIfAbsent(element, () => {}).add(this); |
| return modelElements; |
| }(); |
| |
| Iterable<ModelElement> get allModelElements => [ |
| for (var modelElements in modelElementsMap.values) ...modelElements, |
| ]; |
| |
| @override |
| Map<String, CommentReferable> get referenceChildren { |
| var referenceChildrenBuilder = <String, CommentReferable>{}; |
| var definedNamesModelElements = element.exportNamespace.definedNames.values |
| .map(modelBuilder.fromElement); |
| referenceChildrenBuilder.addEntries( |
| definedNamesModelElements.whereNotType<Accessor>().generateEntries()); |
| // TODO(jcollins-g): warn and get rid of this case where it shows up. |
| // If a user is hiding parts of a prefix import, the user should not |
| // refer to hidden members via the prefix, because that can be |
| // ambiguous. dart-lang/dartdoc#2683. |
| for (var MapEntry(key: prefix, value: libraries) |
| in prefixToLibrary.entries) { |
| referenceChildrenBuilder.putIfAbsent(prefix, () => libraries.first); |
| } |
| return referenceChildrenBuilder; |
| } |
| |
| @override |
| Iterable<CommentReferable> get referenceParents => [package]; |
| |
| /// Check [canonicalFor] for correctness and warn if it refers to |
| /// non-existent elements (or those that this Library can not be canonical |
| /// for). |
| @override |
| String buildDocumentationAddition(String rawDocs) { |
| rawDocs = super.buildDocumentationAddition(rawDocs); |
| var notFoundInAllModelElements = <String>{}; |
| for (var elementName in canonicalFor) { |
| if (!allOriginalModelElementNames.contains(elementName)) { |
| notFoundInAllModelElements.add(elementName); |
| } |
| } |
| for (var notFound in notFoundInAllModelElements) { |
| warn(PackageWarning.ignoredCanonicalFor, message: notFound); |
| } |
| // TODO(jcollins-g): warn if a macro/tool generates an unexpected |
| // canonicalFor? |
| return rawDocs; |
| } |
| } |