| // Copyright (c) 2020, 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. |
| |
| /// Code for managing comment reference lookups in dartdoc. |
| library; |
| |
| import 'dart:core'; |
| |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/element/element2.dart'; |
| import 'package:analyzer/dart/element/scope.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:dartdoc/src/model/library.dart'; |
| import 'package:dartdoc/src/model/model_element.dart'; |
| import 'package:dartdoc/src/model/nameable.dart'; |
| import 'package:dartdoc/src/model/prefix.dart'; |
| import 'package:meta/meta.dart'; |
| |
| class _ReferenceChildrenLookup { |
| final String lookup; |
| final List<String> remaining; |
| |
| _ReferenceChildrenLookup(this.lookup, this.remaining); |
| |
| @override |
| String toString() => |
| '$lookup ($lookup${remaining.isNotEmpty ? ".${remaining.join(".")}" : ''})'; |
| } |
| |
| /// Support comment reference lookups on a Nameable object. |
| mixin CommentReferable implements Nameable { |
| /// For any [CommentReferable] where an analyzer [Scope] exists (or can |
| /// be constructed), implement this. This will take priority over |
| /// lookups via [referenceChildren]. Can be cached. |
| 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 |
| CommentReferable? referenceBy( |
| List<String> reference, { |
| required bool Function(CommentReferable?) filter, |
| bool tryParents = true, |
| Iterable<CommentReferable>? 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 |
| /// [CommentReferable], but you still want to have an implementation of |
| /// [scope]. |
| CommentReferable? _lookupViaScope( |
| _ReferenceChildrenLookup referenceLookup, { |
| required bool Function(CommentReferable?) filter, |
| }) { |
| Element2? 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; |
| } |
| |
| ModelElement result; |
| if (resultElement is PropertyAccessorElement2) { |
| final variable = resultElement.variable3!; |
| if (variable.isSynthetic) { |
| // 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. |
| CommentReferable? _recurseChildrenAndFilter( |
| _ReferenceChildrenLookup referenceLookup, |
| CommentReferable result, { |
| required bool Function(CommentReferable?) filter, |
| }) { |
| CommentReferable? 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, CommentReferable> 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<CommentReferable> get referenceParents; |
| |
| /// Replace the parents of parents. [referenceBy] ignores whatever might |
| /// otherwise be implied by the [referenceParents] of [referenceParents], |
| /// replacing them with this. |
| Iterable<CommentReferable>? 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 comment referable from where this |
| /// `ElementType` was defined. Override where an [Element2] is available. |
| @internal |
| CommentReferable get definingCommentReferable => this; |
| } |
| |
| extension on Scope { |
| /// Prefer the getter for a bundled lookup if both exist. |
| Element2? lookupPreferGetter(String id) { |
| var result = lookup(id); |
| return result.getter2 ?? result.setter2; |
| } |
| } |
| |
| /// A set of utility methods for helping build |
| /// [CommentReferable.referenceChildren] out of collections of other |
| /// [CommentReferable]s. |
| extension CommentReferableEntryGenerators<T extends CommentReferable> |
| on Iterable<T> { |
| /// Creates reference entries for this Iterable. |
| /// |
| /// If there is a conflict with [referable], the included [MapEntry] uses |
| /// [referable]'s [CommentReferable.referenceName] as a prefix. |
| Map<String, T> explicitOnCollisionWith(CommentReferable referable) => { |
| for (var r in this) |
| if (r.referenceName == referable.referenceName) |
| '${referable.referenceName}.${r.referenceName}': r |
| else |
| r.referenceName: r, |
| }; |
| |
| /// A mapping from each [CommentReferable]'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, |
| ]; |
| } |