blob: b3840db1d8bbf349772b0ee3e4463698f2d03019 [file] [log] [blame]
// Copyright (c) 2014, 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.
/// The models used to represent Dart code.
library dartdoc.models;
import 'dart:convert';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart' show FunctionType;
import 'package:analyzer/source/line_info.dart';
// ignore: implementation_imports
import 'package:analyzer/src/dart/element/member.dart'
show ExecutableMember, Member, ParameterMember;
import 'package:collection/collection.dart';
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/generator/file_structure.dart';
import 'package:dartdoc/src/model/annotation.dart';
import 'package:dartdoc/src/model/attribute.dart';
import 'package:dartdoc/src/model/comment_referable.dart';
import 'package:dartdoc/src/model/feature_set.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/model/model_object_builder.dart';
import 'package:dartdoc/src/model/prefix.dart';
import 'package:dartdoc/src/model_utils.dart' as utils;
import 'package:dartdoc/src/render/model_element_renderer.dart';
import 'package:dartdoc/src/render/parameter_renderer.dart';
import 'package:dartdoc/src/render/source_code_renderer.dart';
import 'package:dartdoc/src/source_linker.dart';
import 'package:dartdoc/src/special_elements.dart';
import 'package:dartdoc/src/type_utils.dart';
import 'package:dartdoc/src/warnings.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p show Context;
// TODO(jcollins-g): Implement resolution per ECMA-408 4th edition, page 39 #22.
/// Resolves this very rare case incorrectly by picking the closest element in
/// the inheritance and interface chains from the analyzer.
ModelElement _resolveMultiplyInheritedElement(
MultiplyInheritedExecutableElement e,
Library library,
PackageGraph packageGraph,
Class enclosingClass) {
var inheritables = e.inheritedElements
.map((ee) => ModelElement._fromElement(ee, packageGraph) as Inheritable);
late Inheritable foundInheritable;
var lowIndex = enclosingClass.inheritanceChain.length;
for (var inheritable in inheritables) {
var index = enclosingClass.inheritanceChain
.indexOf(inheritable.enclosingElement as InheritingContainer);
if (index < lowIndex) {
foundInheritable = inheritable;
lowIndex = index;
return ModelElement._from(foundInheritable.element, library, packageGraph,
enclosingContainer: enclosingClass);
mixin ModelElementBuilderImpl implements ModelElementBuilder {
PackageGraph get packageGraph;
ModelElement from(Element e, Library library,
{Container? enclosingContainer}) =>
ModelElement._from(e, library, packageGraph,
enclosingContainer: enclosingContainer);
ModelElement fromElement(Element e) =>
ModelElement._fromElement(e, packageGraph);
ModelElement fromPropertyInducingElement(Element e, Library l,
{Container? enclosingContainer,
Accessor? getter,
Accessor? setter}) =>
e as PropertyInducingElement, l, packageGraph,
enclosingContainer: enclosingContainer,
getter: getter,
setter: setter);
/// This class is the foundation of Dartdoc's model for source code.
/// All ModelElements are contained within a [PackageGraph], and laid out in a
/// structure that mirrors the availability of identifiers in the various
/// namespaces within that package. For example, multiple [Class] objects for a
/// particular identifier ([ModelElement.element]) may show up in different
/// [Library]s as the identifier is reexported.
/// However, ModelElements have an additional concept vital to generating
/// documentation: canonicalization.
/// A ModelElement is canonical if it is the element in the namespace where that
/// element 'comes from' in the public interface to this [PackageGraph]. That
/// often means the [ModelElement.library] is contained in
/// [PackageGraph.libraries], but there are many exceptions and ambiguities the
/// code tries to address here.
/// Non-canonical elements should refer to their canonical counterparts, making
/// it easy to calculate links via [ModelElement.href] without having to know in
/// a particular namespace which elements are canonical or not. A number of
/// [PackageGraph] methods, such as [PackageGraph.findCanonicalModelElementFor]
/// can help with this.
/// When documenting, Dartdoc should only write out files corresponding to
/// canonical instances of ModelElement ([ModelElement.isCanonical]). This
/// helps prevent subtle bugs as generated output for a non-canonical
/// ModelElement will reference itself as part of the "wrong" [Library] from the
/// public interface perspective.
abstract class ModelElement extends Canonicalization
implements Comparable<ModelElement>, Documentable, Privacy {
// TODO(jcollins-g): This really wants a "member that has a type" class.
final Member? _originalMember;
final Library _library;
final PackageGraph _packageGraph;
ModelElement(this._library, this._packageGraph, [this._originalMember]);
/// Creates a [ModelElement] from [e].
factory ModelElement._fromElement(Element e, PackageGraph p) {
if (e is MultiplyDefinedElement) {
// The code-to-document has static errors. We can pick the first
// conflicting element and move on.
e = e.conflictingElements.first;
var library = p.findButDoNotCreateLibraryFor(e) ?? Library.sentinel;
if (e is PropertyInducingElement) {
var elementGetter = e.getter;
var getter = elementGetter != null
? ModelElement._from(elementGetter, library, p)
: null;
var elementSetter = e.setter;
var setter = elementSetter != null
? ModelElement._from(elementSetter, library, p)
: null;
return ModelElement._fromPropertyInducingElement(e, library, p,
getter: getter as Accessor?, setter: setter as Accessor?);
return ModelElement._from(e, library, p);
/// Creates a [ModelElement] from [PropertyInducingElement] [e].
/// Do not construct any ModelElements except from this constructor or
/// [ModelElement._from]. Specify [enclosingContainer]
/// if and only if this is to be an inherited or extended object.
factory ModelElement._fromPropertyInducingElement(
PropertyInducingElement e, Library library, PackageGraph packageGraph,
{required Accessor? getter,
required Accessor? setter,
Container? enclosingContainer}) {
// TODO(jcollins-g): Refactor object model to instantiate 'ModelMembers'
// for members?
if (e is Member) {
e = e.declaration as PropertyInducingElement;
// Return the cached ModelElement if it exists.
var cachedModelElement = packageGraph
.allConstructedModelElements[(e, library, enclosingContainer)];
if (cachedModelElement != null) {
return cachedModelElement;
ModelElement newModelElement;
if (e is FieldElement) {
if (enclosingContainer == null) {
if (e.isEnumConstant) {
var constantValue = e.computeConstantValue();
if (constantValue == null) {
throw StateError(
'Enum $e (${e.runtimeType}) does not have a constant value.');
var constantIndex = constantValue.getField('index');
if (constantIndex == null) {
throw StateError(
'Enum $e (${e.runtimeType}) does not have a constant value.');
var index = constantIndex.toIntValue()!;
newModelElement =
EnumField.forConstant(index, e, library, packageGraph, getter);
} else if (e.enclosingElement is ExtensionElement) {
newModelElement = Field(e, library, packageGraph,
getter as ContainerAccessor?, setter as ContainerAccessor?);
} else {
newModelElement = Field(e, library, packageGraph,
getter as ContainerAccessor?, setter as ContainerAccessor?);
} else {
// EnumFields can't be inherited, so this case is simpler.
// TODO(srawlins): Correct this? Is this dead?
newModelElement = Field.inherited(
e, enclosingContainer, library, packageGraph, getter, setter);
} else if (e is TopLevelVariableElement) {
assert(getter != null || setter != null);
newModelElement =
TopLevelVariable(e, library, packageGraph, getter, setter);
} else {
throw UnimplementedError(
'Unrecognized property inducing element: $e (${e.runtimeType})');
if (enclosingContainer != null) assert(newModelElement is Inheritable);
_cacheNewModelElement(e, newModelElement, library,
enclosingContainer: enclosingContainer);
assert(newModelElement.element is! MultiplyInheritedExecutableElement);
return newModelElement;
/// Creates a [ModelElement] from a non-property-inducing [e].
/// Do not construct any ModelElements except from this constructor or
/// [ModelElement._fromPropertyInducingElement]. Specify [enclosingContainer]
/// if and only if this is to be an inherited or extended object.
// TODO(jcollins-g): this way of using the optional parameter is messy,
// clean that up.
// TODO(jcollins-g): Enforce construction restraint.
// TODO(jcollins-g): Allow e to be null and drop extraneous null checks.
// TODO(jcollins-g): Auto-vivify element's defining library for library
// parameter when given a null.
factory ModelElement._from(
Element e, Library library, PackageGraph packageGraph,
{Container? enclosingContainer}) {
assert(library != Library.sentinel ||
e is ParameterElement ||
e is TypeParameterElement ||
e is GenericFunctionTypeElement ||
e.kind == ElementKind.DYNAMIC ||
e.kind == ElementKind.NEVER);
if (e.kind == ElementKind.DYNAMIC) {
return Dynamic(e, packageGraph);
if (e.kind == ElementKind.NEVER) {
return NeverType(e, packageGraph);
Member? originalMember;
// TODO(jcollins-g): Refactor object model to instantiate 'ModelMembers'
// for members?
if (e is Member) {
originalMember = e;
e = e.declaration;
// Return the cached ModelElement if it exists.
var cachedModelElement = packageGraph
.allConstructedModelElements[(e, library, enclosingContainer)];
if (cachedModelElement != null) {
return cachedModelElement;
var newModelElement = ModelElement._constructFromElementDeclaration(
enclosingContainer: enclosingContainer,
originalMember: originalMember,
if (enclosingContainer != null) assert(newModelElement is Inheritable);
_cacheNewModelElement(e, newModelElement, library,
enclosingContainer: enclosingContainer);
assert(newModelElement.element is! MultiplyInheritedExecutableElement);
return newModelElement;
/// Caches a newly-created [ModelElement] from [ModelElement._from] or
/// [ModelElement._fromPropertyInducingElement].
static void _cacheNewModelElement(
Element e, ModelElement? newModelElement, Library library,
{Container? enclosingContainer}) {
// TODO(jcollins-g): Reenable Parameter caching when dart-lang/sdk#30146
// is fixed?
if (library != Library.sentinel && newModelElement is! Parameter) {
var key = (e, library, enclosingContainer);
library.packageGraph.allConstructedModelElements[key] = newModelElement;
if (newModelElement is Inheritable) {
.putIfAbsent((e, library), () => {}).add(newModelElement);
static ModelElement _constructFromElementDeclaration(
Element e,
Library library,
PackageGraph packageGraph, {
Container? enclosingContainer,
Member? originalMember,
}) {
return switch (e) {
MultiplyInheritedExecutableElement() => _resolveMultiplyInheritedElement(
e, library, packageGraph, enclosingContainer as Class),
LibraryElement() => packageGraph.findButDoNotCreateLibraryFor(e)!,
PrefixElement() => Prefix(e, library, packageGraph),
EnumElement() => Enum(e, library, packageGraph),
MixinElement() => Mixin(e, library, packageGraph),
ClassElement() => Class(e, library, packageGraph),
ExtensionElement() => Extension(e, library, packageGraph),
ExtensionTypeElement() => ExtensionType(e, library, packageGraph),
FunctionElement() => ModelFunction(e, library, packageGraph),
ConstructorElement() => Constructor(e, library, packageGraph),
GenericFunctionTypeElement() =>
ModelFunctionTypedef(e, library, packageGraph),
TypeAliasElement(aliasedType: FunctionType()) =>
FunctionTypedef(e, library, packageGraph),
when e.aliasedType.documentableElement is InterfaceElement =>
ClassTypedef(e, library, packageGraph),
TypeAliasElement() => GeneralizedTypedef(e, library, packageGraph),
MethodElement(isOperator: true) => enclosingContainer == null
? Operator(e, library, packageGraph)
: Operator.inherited(e, enclosingContainer, library, packageGraph,
originalMember: originalMember),
MethodElement(isOperator: false) => enclosingContainer == null
? Method(e, library, packageGraph)
: Method.inherited(e, enclosingContainer, library, packageGraph,
originalMember: originalMember as ExecutableMember?),
ParameterElement() => Parameter(e, library, packageGraph,
originalMember: originalMember as ParameterMember?),
PropertyAccessorElement() => _constructFromPropertyAccessor(
enclosingContainer: enclosingContainer,
originalMember: originalMember,
TypeParameterElement() => TypeParameter(e, library, packageGraph),
_ => throw UnimplementedError('Unknown type ${e.runtimeType}'),
/// Constructs a [ModelElement] from a [PropertyAccessorElement].
static ModelElement _constructFromPropertyAccessor(
PropertyAccessorElement e,
Library library,
PackageGraph packageGraph, {
Container? enclosingContainer,
Member? originalMember,
}) {
// Accessors can be part of a [Container], or a part of a [Library].
if (e.enclosingElement is ExtensionElement ||
e.enclosingElement is InterfaceElement ||
e is MultiplyInheritedExecutableElement) {
if (enclosingContainer == null) {
return ContainerAccessor(e, library, packageGraph);
assert(e.enclosingElement is! ExtensionElement);
return ContainerAccessor.inherited(
e, library, packageGraph, enclosingContainer,
originalMember: originalMember as ExecutableMember?);
return Accessor(e, library, packageGraph);
ModelElement? get enclosingElement;
// Stub for mustache, which would otherwise search enclosing elements to find
// names for members.
bool get hasCategoryNames => false;
// Stub for mustache.
Iterable<Category?> get displayedCategories => const [];
ModelNode? get modelNode => packageGraph.getModelNodeFor(element);
/// This element's [Annotation]s.
/// Does not include annotations with `null` elements or that are otherwise
/// supposed to be invisible (like `@pragma`). While `null` elements indicate
/// invalid code from analyzer's perspective, some are present in `sky_engine`
/// (`@Native`) so we don't want to crash here.
late final List<Annotation> annotations = element.metadata
.whereNot((m) =>
m.element == null ||
.map((m) => Annotation(m, library, packageGraph))
.toList(growable: false);
late final bool isPublic = () {
if (name.isEmpty) {
return false;
if (this is! Library &&
(library == Library.sentinel || !library.isPublic)) {
return false;
if (enclosingElement is Class && !(enclosingElement as Class).isPublic) {
return false;
// TODO(srawlins): Er, mixin? enum?
if (enclosingElement is Extension &&
!(enclosingElement as Extension).isPublic) {
return false;
return utils.hasPublicName(element) && !hasNodoc;
late final DartdocOptionContext config =
packageGraph.config, library.element, packageGraph.resourceProvider);
late final Set<String> locationPieces = element.location
.where((s) => s.isNotEmpty)
bool get hasAttributes => attributes.isNotEmpty;
/// This element's attributes.
/// This includes tags applied by Dartdoc for various attributes that should
/// be called out. See [Attribute] for a list.
Set<Attribute> get attributes {
return {
// 'const' and 'static' are not needed here because 'const' and 'static'
// elements get their own sections in the doc.
if (isFinal) Attribute.final_,
if (isLate) Attribute.late_,
String get attributesAsString => modelElementRenderer.renderAttributes(this);
// True if this is a function, or if it is an type alias to a function.
bool get isCallable =>
element is FunctionTypedElement ||
(element is TypeAliasElement &&
(element as TypeAliasElement).aliasedType is FunctionType);
// The canonical ModelElement for this ModelElement,
// or null if there isn't one.
late final ModelElement? canonicalModelElement = () {
Container? preferredClass;
// TODO(srawlins): Add mixin.
if (enclosingElement is Class ||
enclosingElement is Enum ||
enclosingElement is Extension) {
preferredClass = enclosingElement as Container?;
return packageGraph.findCanonicalModelElementFor(element,
preferredClass: preferredClass);
bool get hasSourceHref => sourceHref.isNotEmpty;
late final String sourceHref = SourceLinker.fromElement(this).href();
Library get definingLibrary =>
modelBuilder.fromElement(element.library!) as Library;
late final Library? canonicalLibrary = () {
if (!utils.hasPublicName(element)) {
// Privately named elements can never have a canonical library.
return null;
// This is not accurate if we are still constructing the Package.
var definingLibraryIsLocalPublic =
var possibleCanonicalLibrary = definingLibraryIsLocalPublic
? definingLibrary
: _searchForCanonicalLibrary();
if (possibleCanonicalLibrary != null) return possibleCanonicalLibrary;
if (this case Inheritable(isInherited: true)) {
if (!config.linkToRemote &&
packageGraph.publicLibraries.contains(library)) {
// If this is an element inherited from a container that isn't directly
// reexported, and we're not linking to remote, we can pretend that
// [library] is canonical.
return library;
return null;
Library? _searchForCanonicalLibrary() {
var thisAndExported = packageGraph.libraryExports[definingLibrary.element];
if (thisAndExported == null) {
return null;
// Since we're looking for a library, find the [Element] immediately
// contained by a [CompilationUnitElement] in the tree.
var topLevelElement = element;
while (topLevelElement.enclosingElement is! LibraryElement &&
topLevelElement.enclosingElement is! CompilationUnitElement &&
topLevelElement.enclosingElement != null) {
topLevelElement = topLevelElement.enclosingElement!;
final candidateLibraries = thisAndExported
.where((l) =>
l.isPublic && l.package.documentedWhere != DocumentLocation.missing)
.where((l) {
var lookup =
return switch (lookup) {
PropertyAccessorElement() => topLevelElement == lookup.variable,
_ => topLevelElement == lookup,
}).toList(growable: true);
// Avoid claiming canonicalization for elements outside of this element's
// defining package.
// TODO(jcollins-g): Make the else block unconditional.
if (candidateLibraries.isNotEmpty &&
!candidateLibraries.any((l) => l.package == definingLibrary.package)) {
message: definingLibrary.package.fullyQualifiedName,
referredFrom: candidateLibraries);
} else {
.removeWhere((l) => l.package != definingLibrary.package);
if (candidateLibraries.isEmpty) {
return null;
if (candidateLibraries.length == 1) {
return candidateLibraries.single;
var topLevelModelElement =
ModelElement._fromElement(topLevelElement, packageGraph);
return topLevelModelElement.calculateCanonicalCandidate(candidateLibraries);
bool get isCanonical {
if (!isPublic) return false;
if (library != canonicalLibrary) return false;
// If there's no inheritance to deal with, we're done.
if (this is! Inheritable) return true;
final self = this as Inheritable;
// If we're the defining element, or if the defining element is not in the
// set of libraries being documented, then this element should be treated as
// canonical (given `library == canonicalLibrary`).
return self.enclosingElement == self.canonicalEnclosingContainer;
/// The documentaion, stripped of its comment syntax, like `///` characters.
String get documentation => injectMacros( => e.documentationLocal).join('<p>'));
Element get element;
String get location {
// Call nothing from here that can emit warnings or you'll cause stack
// overflows.
var sourceUri = pathContext.toUri(sourceFileName);
if (characterLocation != null) {
return '($sourceUri:${characterLocation.toString()})';
return '($sourceUri)';
/// The name of the output file in which this element will be primarily
/// documented.
@Deprecated('replace with fileStructure.fileName')
String get fileName => fileStructure.fileName;
@Deprecated('replace with fileStructure.fileType')
String get fileType => fileStructure.fileType;
/// The full path of the output file in which this element will be primarily
/// documented.
String get filePath;
/// Returns the fully qualified name.
/// For example: 'libraryName.className.methodName'
late final String fullyQualifiedName = _buildFullyQualifiedName(this, name);
late final String _fullyQualifiedNameWithoutLibrary =
fullyQualifiedName.replaceFirst('${library.fullyQualifiedName}.', '');
String get fullyQualifiedNameWithoutLibrary =>
String get sourceFileName => element.source!.fullName;
late final CharacterLocation? characterLocation = () {
final lineInfo = compilationUnitElement.lineInfo;
late final element = this.element;
assert(element.nameOffset >= 0,
'Invalid location data for element: $fullyQualifiedName');
var nameOffset = element.nameOffset;
if (nameOffset >= 0) {
return lineInfo.getLocation(nameOffset);
return null;
CompilationUnitElement get compilationUnitElement =>
bool get hasAnnotations => annotations.isNotEmpty;
bool get hasDocumentation => documentation.isNotEmpty == true;
bool get hasParameters => parameters.isNotEmpty;
/// If [canonicalLibrary] (or [canonicalEnclosingElement], for [Inheritable]
/// subclasses) is null, this is null.
String? get href {
if (!identical(canonicalModelElement, this)) {
return canonicalModelElement?.href;
assert(canonicalLibrary == library);
var packageBaseHref = package.baseHref;
return '$packageBaseHref$filePath';
String get htmlId => name;
bool get isConst => false;
bool get isDeprecated {
// If element.metadata is empty, it might be because this is a property
// where the metadata belongs to the individual getter/setter
if (element.metadata.isEmpty && element is PropertyInducingElement) {
var pie = element as PropertyInducingElement;
// The getter or the setter might be null – so the stored value may be
// `true`, `false`, or `null`
var getterDeprecated = pie.getter?.metadata.any((a) => a.isDeprecated);
var setterDeprecated = pie.setter?.metadata.any((a) => a.isDeprecated);
var deprecatedValues =
[getterDeprecated, setterDeprecated].whereNotNull();
// At least one of these should be non-null. Otherwise things are weird
// If there are both a setter and getter, only show the property as
// deprecated if both are deprecated.
return deprecatedValues.every((d) => d);
return element.metadata.any((a) => a.isDeprecated);
bool get isDocumented => isCanonical && isPublic;
/// Whether this element is an enum value.
bool get isEnumValue => false;
bool get isFinal => false;
bool get isLate => false;
/// A human-friendly name for the kind of element this is.
Kind get kind;
Library get library => _library;
late final String linkedName = () {
// If we're calling this with an empty name, we probably have the wrong
// element associated with a ModelElement or there's an analysis bug.
assert(name.isNotEmpty ||
element.kind == ElementKind.DYNAMIC ||
element.kind == ElementKind.NEVER ||
this is ModelFunction);
if (href == null) {
if (isPublicAndPackageDocumented) {
return htmlEscape.convert(name);
return modelElementRenderer.renderLinkedName(this);
ModelElementRenderer get modelElementRenderer =>
ParameterRenderer get _parameterRenderer =>
ParameterRenderer get _parameterRendererDetailed =>
SourceCodeRenderer get _sourceCodeRenderer =>
String get linkedParams => _parameterRenderer.renderLinkedParams(parameters);
String get linkedParamsLines =>
String? get linkedParamsNoMetadata =>
_parameterRenderer.renderLinkedParams(parameters, showMetadata: false);
String get name =>!;
String get oneLineDoc => elementDocumentation.asOneLiner;
Member? get originalMember => _originalMember;
PackageGraph get packageGraph => _packageGraph;
Package get package => library.package;
bool get isPublicAndPackageDocumented => isPublic && package.isDocumented;
p.Context get pathContext => packageGraph.resourceProvider.pathContext;
// TODO(srawlins): This really smells like it should just be implemented in
// the subclasses.
late final List<Parameter> parameters = () {
final element = this.element;
if (!isCallable) {
throw StateError(
'$element (${element.runtimeType}) cannot have parameters');
final List<ParameterElement> params;
if (element is TypeAliasElement) {
final aliasedType = element.aliasedType;
if (aliasedType is FunctionType) {
params = aliasedType.parameters;
} else {
return const <Parameter>[];
} else if (element is ExecutableElement) {
if (_originalMember != null) {
assert(_originalMember is ExecutableMember);
params = (_originalMember as ExecutableMember).parameters;
} else {
params = element.parameters;
} else if (element is FunctionTypedElement) {
if (_originalMember != null) {
params = (_originalMember as FunctionTypedElement).parameters;
} else {
params = element.parameters;
} else {
return const <Parameter>[];
return List.of(
(p) => ModelElement._from(p, library, packageGraph) as Parameter),
growable: false,
late final String sourceCode =
int compareTo(Object other) {
if (other is ModelElement) {
return name.toLowerCase().compareTo(;
} else {
return 0;
String toString() => '$runtimeType $name';
String _buildFullyQualifiedName(ModelElement e, String fullyQualifiedName) {
final enclosingElement = e.enclosingElement;
return enclosingElement == null
? fullyQualifiedName
: _buildFullyQualifiedName(
enclosingElement, '${}.$fullyQualifiedName');
CommentReferable get definingCommentReferable {
var element = this.element;
return modelBuilder.fromElement(element);
String get linkedObjectType => _packageGraph.dartCoreObject;
late final FileStructure fileStructure = FileStructure.fromDocumentable(this);