blob: 6bd0c6b179e293ef0d920c648d505125fc464b26 [file] [log] [blame]
// 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 dartdoc.src.model.comment_reference;
import 'dart:core';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/scope.dart';
import 'package:collection/collection.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/model_object_builder.dart';
import 'package:dartdoc/src/model/nameable.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, ModelBuilderInterface {
/// 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;
/// Look 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. Will skip over entire subtrees whose parent node does not
/// pass [allowTree].
@nonVirtual
CommentReferable? referenceBy(
List<String> reference, {
required bool Function(CommentReferable?) filter,
required bool Function(CommentReferable?) allowTree,
bool tryParents = true,
Iterable<CommentReferable>? parentOverrides,
}) {
parentOverrides ??= referenceParents;
if (reference.isEmpty) {
return tryParents ? null : this;
}
for (var referenceLookup in _childLookups(reference)) {
if (scope != null) {
var result = _lookupViaScope(referenceLookup,
filter: filter, allowTree: allowTree);
if (result != null) {
return result;
}
}
final referenceChildren = this.referenceChildren;
final childrenResult = referenceChildren[referenceLookup.lookup];
if (childrenResult != null) {
var result = _recurseChildrenAndFilter(referenceLookup, childrenResult,
allowTree: allowTree, 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,
allowTree: allowTree,
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,
required bool Function(CommentReferable?) allowTree,
}) {
final resultElement = scope!.lookupPreferGetter(referenceLookup.lookup);
if (resultElement == null) return null;
ModelElement result;
if (resultElement is PropertyAccessorElement) {
final variable = resultElement.variable;
if (variable.isSynthetic) {
result = modelBuilder.fromElement(resultElement);
} else {
result = modelBuilder.fromElement(variable);
}
} else {
result = modelBuilder.fromElement(resultElement);
}
if (result.enclosingElement is Container) {
assert(
false,
'[Container] member detected, support not implemented for analyzer '
'scope inside containers',
);
return null;
}
if (!allowTree(result)) return null;
return _recurseChildrenAndFilter(referenceLookup, result,
allowTree: allowTree, 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,
required bool Function(CommentReferable?) allowTree,
}) {
CommentReferable? returnValue = result;
if (referenceLookup.remaining.isNotEmpty) {
if (allowTree(result)) {
returnValue = result.referenceBy(referenceLookup.remaining,
tryParents: false, allowTree: allowTree, filter: filter);
} else {
returnValue = null;
}
} else if (!filter(result)) {
returnValue = result.referenceBy([referenceLookup.lookup],
tryParents: false, allowTree: allowTree, 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 [Element] is available.
@internal
CommentReferable get definingCommentReferable => this;
}
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;
}
}
/// A set of utility methods for helping build
/// [CommentReferable.referenceChildren] out of collections of other
/// [CommentReferable]s.
extension CommentReferableEntryGenerators on Iterable<CommentReferable> {
/// 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, CommentReferable> explicitOnCollisionWith(
CommentReferable referable) =>
{
for (var r in this)
if (r.referenceName == referable.referenceName)
'${referable.referenceName}.${r.referenceName}': r
else
r.referenceName: r,
};
/// Generates entries from this Iterable.
Iterable<MapEntry<String, CommentReferable>> generateEntries() =>
map((r) => MapEntry(r.referenceName, r));
/// Returns all values not of this type.
List<CommentReferable> whereNotType<T>() => [
for (var referable in this)
if (referable is! T) referable,
];
}
/// A set of utility methods to add entries to
/// [CommentReferable.referenceChildren].
extension CommentReferableEntryBuilder on Map<String, CommentReferable> {
/// Like [Map.putIfAbsent] except works on an iterable of entries.
void addEntriesIfAbsent(
Iterable<MapEntry<String, CommentReferable>> entries) {
for (var entry in entries) {
if (!containsKey(entry.key)) this[entry.key] = entry.value;
}
}
}