blob: d28e4827b8e06e5436ea0cd812ce39a7c9e78bad [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:dartdoc/src/model/accessor.dart';
import 'package:dartdoc/src/model/container.dart';
import 'package:dartdoc/src/model/library.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(".") : ''})';
}
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
/// [CommmentReferable]s.
extension CommentReferableEntryGenerators on Iterable<CommentReferable> {
/// Creates ordinary references except if there is a conflict with
/// [referable], it will generate a [MapEntry] using [referable]'s
/// [CommentReferable.referenceName] as a prefix for the conflicting item.
Iterable<MapEntry<String, CommentReferable>> explicitOnCollisionWith(
CommentReferable referable) =>
map((r) {
if (r.referenceName == referable.referenceName) {
return MapEntry('${referable.referenceName}.${r.referenceName}', r);
}
return MapEntry(r.referenceName, r);
});
/// Just generate entries from this iterable.
Iterable<MapEntry<String, CommentReferable>> generateEntries() =>
map((r) => MapEntry(r.referenceName, r));
/// Return all values not of this type.
Iterable<CommentReferable> whereNotType<T>() sync* {
for (var r in this) {
if (r is! T) yield r;
}
}
}
/// 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;
}
}
}
/// 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,
{bool tryParents = true,
bool Function(CommentReferable) filter,
bool Function(CommentReferable) allowTree,
Iterable<CommentReferable> parentOverrides}) {
filter ??= (r) => true;
allowTree ??= (r) => true;
parentOverrides ??= referenceParents;
if (reference.isEmpty) {
if (tryParents == false) return this;
return null;
}
CommentReferable result;
/// Search for the reference.
for (var referenceLookup in childLookups(reference)) {
if (scope != null) {
result = lookupViaScope(referenceLookup, filter, allowTree);
if (result != null) break;
}
if (referenceChildren.containsKey(referenceLookup.lookup)) {
result = recurseChildrenAndFilter(
referenceLookup, referenceChildren[referenceLookup.lookup],
allowTree: allowTree, filter: filter);
if (result != null) break;
}
}
// If we can't find it in children, try searching parents if allowed.
if (result == null && tryParents) {
for (var parent in parentOverrides) {
result = parent.referenceBy(reference,
tryParents: true,
parentOverrides: referenceGrandparentOverrides,
allowTree: allowTree,
filter: filter);
if (result != null) break;
}
}
return result;
}
/// Looks up references by [scope], skipping over results that do not match
/// the given 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,
bool Function(CommentReferable) allowTree,
bool Function(CommentReferable) filter) {
var resultElement = scope.lookupPreferGetter(referenceLookup.lookup);
if (resultElement == null) return null;
var result = modelBuilder.fromElement(resultElement);
if (result is Accessor) {
result = (result as Accessor).enclosingCombo;
}
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
/// [_lookupViaReferenceChildren], recurse through children, skipping over
/// results that do not match the filter.
CommentReferable recurseChildrenAndFilter(
ReferenceChildrenLookup referenceLookup, CommentReferable result,
{bool Function(CommentReferable) allowTree,
bool Function(CommentReferable) filter}) {
assert(result != null);
if (referenceLookup.remaining.isNotEmpty) {
if (allowTree(result)) {
result = result.referenceBy(referenceLookup.remaining,
tryParents: false, allowTree: allowTree, filter: filter);
} else {
result = null;
}
} else if (!filter(result)) {
result = result.referenceBy([referenceLookup.lookup],
tryParents: false, allowTree: allowTree, filter: filter);
}
if (!filter(result)) {
result = null;
}
return result;
}
/// 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.
// TODO(jcollins-g): Convert to generator after dart-lang/sdk#46419
Iterable<ReferenceChildrenLookup> childLookups(List<String> reference) {
var retval = <ReferenceChildrenLookup>[];
for (var index = 1; index <= reference.length; index++) {
retval.add(ReferenceChildrenLookup(
reference.sublist(0, index).join('.'), reference.sublist(index)));
}
return retval;
}
/// 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;
// TODO(jcollins-g): Eliminate need for this in markdown_processor.
Element get element;
}