| // 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 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/scope.dart'; |
| import 'package:analyzer/dart/element/type.dart' show DartType; |
| import 'package:collection/collection.dart'; |
| import 'package:dartdoc/src/element_type.dart'; |
| import 'package:dartdoc/src/model/accessor.dart'; |
| import 'package:dartdoc/src/model/constructor.dart'; |
| import 'package:dartdoc/src/model/container.dart'; |
| import 'package:dartdoc/src/model/library.dart'; |
| import 'package:dartdoc/src/model/model_element.dart'; |
| import 'package:dartdoc/src/model/package_graph.dart'; |
| import 'package:dartdoc/src/model/prefix.dart'; |
| import 'package:dartdoc/src/warnings.dart'; |
| import 'package:meta/meta.dart'; |
| |
| /// Something that has a name, and can be referenced in a doc comment. |
| mixin Referable { |
| String get name; |
| |
| /// A "fully" qualified name, used for things like for warnings printed in the |
| /// terminal; not for display use in rendered HTML. |
| /// |
| /// "Fully" means the name is qualified through the library. For example, a |
| /// method named 'baz' in a class named 'Bar' in a library named 'foo' has a |
| /// fully qualified name of 'foo.Bar.baz'. |
| /// |
| /// As dartdoc can document multiple packages at once, note that such |
| /// qualifying names may not be unique across all documented packages. |
| String get fullyQualifiedName => name; |
| |
| /// The name to use as text in the rendered documentation. |
| String get displayName => name; |
| |
| /// The name to use in breadcrumbs in the rendered documentation. |
| String get breadcrumbName => name; |
| |
| /// The name that should be used in documentation to refer to this object if |
| /// not the name that was actually used in the reference. |
| /// |
| /// This is `null` for most objects in which case the reference's original |
| /// text is used, but returns the public name of a private named parameter: |
| /// |
| /// class C { |
| /// /// Make with [_x]. |
| /// C({this._x}); |
| /// } |
| /// |
| /// Here, the generated doc comment will use "x" as the name for the parameter |
| /// reference, not "_x". |
| String? get documentedName => null; |
| |
| /// Whether this is "package-public." |
| /// |
| /// A "package-public" element satisfies the following requirements: |
| /// * is not documented with the `@nodoc` directive, |
| /// * for a library, adheres to the documentation for [Library.isPublic], |
| /// * for a library member, is in a _public_ library's exported namespace, and |
| /// is not privately named, nor an unnamed extension, |
| /// * for a container (class, enum, extension, extension type, mixin) member, |
| /// is in a _public_ container, and is not privately named. |
| bool get isPublic => name.isNotEmpty && !name.startsWith('_'); |
| |
| @override |
| String toString() => name; |
| |
| PackageGraph get packageGraph; |
| |
| /// Returns the [ModelElement] for [element], instantiating it if needed. |
| /// |
| /// A convenience method for [ModelElement.for_], see its documentation. |
| ModelElement getModelFor( |
| Element element, |
| Library? library, { |
| Container? enclosingContainer, |
| }) => |
| ModelElement.for_( |
| element, |
| library, |
| packageGraph, |
| enclosingContainer: enclosingContainer, |
| ); |
| |
| /// Returns the [ModelElement] for [element], instantiating it if needed. |
| /// |
| /// A convenience method for [ModelElement.forElement], see its |
| /// documentation. |
| ModelElement getModelForElement(Element element) => |
| ModelElement.forElement(element, packageGraph); |
| |
| /// Returns the [ModelElement] for [element], instantiating it if needed. |
| /// |
| /// A convenience method for [ModelElement.forPropertyInducingElement], see |
| /// its documentation. |
| // TODO(srawlins): Most callers seem to determine `getter` and `setter` |
| // immediately before calling this method, and I imagine could instead just |
| // call `getModelFor`. |
| ModelElement getModelForPropertyInducingElement( |
| PropertyInducingElement element, |
| Library library, { |
| required Accessor? getter, |
| required Accessor? setter, |
| Container? enclosingContainer, |
| }) => |
| ModelElement.forPropertyInducingElement( |
| element, |
| library, |
| packageGraph, |
| getter: getter, |
| setter: setter, |
| enclosingContainer: enclosingContainer, |
| ); |
| |
| /// Returns the [ElementType] for [type], instantiating it if needed. |
| ElementType getTypeFor(DartType type, Library? library) => |
| ElementType.for_(type, library, packageGraph); |
| |
| /// For any [Referable] where an analyzer [Scope] exists (or can be |
| /// constructed), implement this. This will take priority over lookups via |
| /// [referenceChildren]. Can be cached. |
| @visibleForOverriding |
| Scope? get scope => null; |
| |
| String? get href => null; |
| |
| /// Looks up a comment reference by its component parts. |
| /// |
| /// If [tryParents] is true, try looking up the same reference in any parents |
| /// of `this`. Will skip over results that do not pass a given [filter] and |
| /// keep searching. |
| @nonVirtual |
| Referable? referenceBy( |
| List<String> reference, { |
| required bool Function(Referable?) filter, |
| bool tryParents = true, |
| Iterable<Referable>? parentOverrides, |
| }) { |
| parentOverrides ??= referenceParents; |
| if (reference.isEmpty) { |
| return tryParents ? null : this; |
| } |
| for (var referenceLookup in _childLookups(reference)) { |
| // First attempt: Ask analyzer's `Scope.lookup` API. |
| var result = _lookupViaScope(referenceLookup, filter: filter); |
| if (result != null) { |
| if (result is Prefix && |
| result.name == '_' && |
| library!.element.featureSet.isEnabled(Feature.wildcard_variables)) { |
| // A wildcard import prefix is non-binding. |
| continue; |
| } |
| return result; |
| } |
| |
| // Second attempt: Look through `referenceChildren`. |
| final referenceChildren = this.referenceChildren; |
| final childrenResult = referenceChildren[referenceLookup.lookup]; |
| if (childrenResult != null) { |
| var result = _recurseChildrenAndFilter( |
| referenceLookup, |
| childrenResult, |
| filter: filter, |
| ); |
| if (result != null) { |
| return result; |
| } |
| } |
| } |
| // If we can't find it in children, try searching parents if allowed. |
| if (tryParents) { |
| for (var parent in parentOverrides) { |
| var result = parent.referenceBy( |
| reference, |
| tryParents: true, |
| parentOverrides: referenceGrandparentOverrides, |
| filter: filter, |
| ); |
| if (result != null) return result; |
| } |
| } |
| return null; |
| } |
| |
| /// Looks up references by [scope], skipping over results that do not match |
| /// [filter]. |
| /// |
| /// Override if [Scope.lookup] may return elements not corresponding to a |
| /// [Referable], but you still want to have an implementation of |
| /// [scope]. |
| Referable? _lookupViaScope( |
| _ReferenceChildrenLookup referenceLookup, { |
| required bool Function(Referable?) filter, |
| }) { |
| Element? resultElement; |
| final scope = this.scope; |
| if (scope != null) { |
| resultElement = scope.lookupPreferGetter(referenceLookup.lookup); |
| if (resultElement == null) return null; |
| } |
| |
| if (resultElement == null) { |
| if (this case ModelElement(:var modelNode?)) { |
| var references = modelNode.commentData?.references; |
| if (references != null) { |
| resultElement = references[referenceLookup.lookup]?.element; |
| } |
| } |
| } |
| |
| if (resultElement == null) { |
| return null; |
| } |
| |
| // If the element was brought in by a @docImport or has an unlinked library |
| // fragment, we must drop it gracefully instead of crashing on `library!`. |
| // See: https://github.com/dart-lang/sdk/issues/62812 |
| // TODO(zarah): Investigate if this bailout is still necessary after the |
| // issue in the analyzer is resolved. We need to determine whether there are |
| // other edge cases that could lead to an element's library not being found. |
| if (packageGraph.findButDoNotCreateLibraryFor(resultElement) == null && |
| resultElement.kind != ElementKind.DYNAMIC && |
| resultElement.kind != ElementKind.NEVER) { |
| packageGraph.warnOnElement( |
| switch (this) { Warnable w => w, _ => null }, |
| PackageWarning.internalError, |
| message: 'Unable to find library for element $resultElement', |
| ); |
| return null; |
| } |
| |
| ModelElement result; |
| if (resultElement is PropertyAccessorElement) { |
| final variable = resultElement.variable; |
| if (!variable.isOriginDeclaration) { |
| // First, cache the synthetic variable, so that the |
| // PropertyAccessorElement getter and/or setter are set (see |
| // `Field.new` regarding `enclosingCombo`). |
| packageGraph.getModelForElement(variable); |
| // Then, use the result for the PropertyAccessorElement. |
| result = packageGraph.getModelForElement(resultElement); |
| } else { |
| result = packageGraph.getModelForElement(variable); |
| } |
| } else { |
| result = packageGraph.getModelForElement(resultElement); |
| } |
| return _recurseChildrenAndFilter(referenceLookup, result, filter: filter); |
| } |
| |
| /// Given a [result] found in an implementation of [_lookupViaScope] or |
| /// [_ReferenceChildrenLookup], recurse through children, skipping over |
| /// results that do not match the filter. |
| Referable? _recurseChildrenAndFilter( |
| _ReferenceChildrenLookup referenceLookup, |
| Referable result, { |
| required bool Function(Referable?) filter, |
| }) { |
| Referable? returnValue = result; |
| if (referenceLookup.remaining.isNotEmpty) { |
| returnValue = result.referenceBy(referenceLookup.remaining, |
| tryParents: false, filter: filter); |
| } else if (!filter(result)) { |
| returnValue = result.referenceBy([referenceLookup.lookup], |
| tryParents: false, filter: filter); |
| } |
| if (!filter(returnValue)) { |
| returnValue = null; |
| } |
| return returnValue; |
| } |
| |
| /// A list of lookups that should be attempted on children based on |
| /// [reference]. |
| /// |
| /// This allows us to deal with libraries that may have separators in them. |
| /// [referenceBy] stops at the first one found. |
| Iterable<_ReferenceChildrenLookup> _childLookups(List<String> reference) => |
| reference |
| .mapIndexed((index, _) => _ReferenceChildrenLookup( |
| reference.sublist(0, index + 1).join('.'), |
| reference.sublist(index + 1))) |
| .toList(growable: false); |
| |
| /// Map of [referenceName] to the elements that are a member of `this`, but |
| /// not this model element itself. Can be cached. |
| /// |
| /// There is no need to duplicate references here that can be found via |
| /// [scope]. |
| Map<String, Referable> get referenceChildren; |
| |
| /// Iterable of immediate "parents" to try resolving component parts. |
| /// [referenceBy] stops at the first parent where a part is found. |
| /// Can be cached. |
| // TODO(jcollins-g): Rationalize the different "enclosing" types so that |
| // this doesn't duplicate `[enclosingElement]` in many cases. |
| // TODO(jcollins-g): Implement comment reference resolution via categories, |
| // making the iterable make sense here. |
| Iterable<Referable> get referenceParents; |
| |
| /// Replace the parents of parents. [referenceBy] ignores whatever might |
| /// otherwise be implied by the [referenceParents] of [referenceParents], |
| /// replacing them with this. |
| Iterable<Referable>? get referenceGrandparentOverrides => null; |
| |
| // TODO(jcollins-g): Enforce that reference name is always the same |
| // as [ModelElement.name]. Easier/possible after old lookup code is gone. |
| String get referenceName => name; |
| |
| // TODO(jcollins-g): Eliminate need for this in markdown_processor. |
| Library? get library => null; |
| |
| /// For testing / comparison only, get the [Referable] from where this |
| /// `ElementType` was defined. Override where an [Element] is available. |
| @internal |
| Referable get definingReferable => this; |
| } |
| |
| /// Compares [a] with [b] by name. |
| int byName(Referable a, Referable b) { |
| if (a is Library && b is Library) { |
| return compareAsciiLowerCaseNatural(a.displayName, b.displayName); |
| } |
| |
| if (a is Constructor && b is Constructor) { |
| var aName = a.name.replaceFirst('.new', ''); |
| var bName = b.name.replaceFirst('.new', ''); |
| return compareAsciiLowerCaseNatural(aName, bName); |
| } |
| |
| var stringCompare = compareAsciiLowerCaseNatural(a.name, b.name); |
| if (stringCompare != 0) { |
| return stringCompare; |
| } |
| |
| return a.hashCode.compareTo(b.hashCode); |
| } |
| |
| /// A set of utility methods for helping build [Referable.referenceChildren] out |
| /// of collections of other [Referable]s. |
| extension ReferableEntryGenerators<T extends Referable> on Iterable<T> { |
| /// Creates reference entries for this Iterable. |
| /// |
| /// If there is a conflict with [referable], the included [MapEntry] uses |
| /// [referable]'s [Referable.referenceName] as a prefix. |
| Map<String, T> explicitOnCollisionWith(Referable referable) => { |
| for (var r in this) |
| if (r.referenceName == referable.referenceName) |
| '${referable.referenceName}.${r.referenceName}': r |
| else |
| r.referenceName: r, |
| }; |
| |
| /// A mapping from each [Referable]'s name to itself. |
| Map<String, T> get asMapByName => { |
| for (var r in this) r.referenceName: r, |
| }; |
| |
| /// Returns all values not of this type. |
| List<T> whereNotType<U>() => [ |
| for (var referable in this) |
| if (referable is! U) referable, |
| ]; |
| } |
| |
| class _ReferenceChildrenLookup { |
| final String lookup; |
| final List<String> remaining; |
| |
| _ReferenceChildrenLookup(this.lookup, this.remaining); |
| |
| @override |
| String toString() => |
| '$lookup ($lookup${remaining.isNotEmpty ? ".${remaining.join(".")}" : ''})'; |
| } |
| |
| extension on Scope { |
| /// Prefer the getter for a bundled lookup if both exist. |
| Element? lookupPreferGetter(String id) { |
| var result = lookup(id); |
| return result.getter ?? result.setter; |
| } |
| } |