From cef078d3db68fadc35de5b2415b69c3523ddb704 Mon Sep 17 00:00:00 2001 From: Jonathan Koren Date: Thu, 7 Nov 2019 15:40:37 -0800 Subject: [PATCH] Split up model.dart and move to a subpackage --- lib/dartdoc.dart | 4 +- lib/src/element_type.dart | 2 +- lib/src/empty_generator.dart | 2 +- lib/src/generator.dart | 2 +- lib/src/html/html_generator.dart | 2 +- lib/src/html/html_generator_instance.dart | 2 +- lib/src/html/template_data.dart | 2 +- lib/src/markdown_processor.dart | 2 +- lib/src/model.dart | 7317 --------------------- lib/src/model/accessor.dart | 209 + lib/src/model/canonicalization.dart | 101 + lib/src/model/categorization.dart | 134 + lib/src/model/category.dart | 208 + lib/src/model/class.dart | 594 ++ lib/src/model/constructor.dart | 112 + lib/src/model/container.dart | 135 + lib/src/model/container_member.dart | 71 + lib/src/model/documentable.dart | 81 + lib/src/model/dynamic.dart | 30 + lib/src/model/enclosed_element.dart | 12 + lib/src/model/enum.dart | 104 + lib/src/model/extendable.dart | 15 + lib/src/model/extension.dart | 155 + lib/src/model/field.dart | 189 + lib/src/model/getter_setter_combo.dart | 231 + lib/src/model/indexable.dart | 14 + lib/src/model/inheritable.dart | 161 + lib/src/model/library.dart | 661 ++ lib/src/model/library_container.dart | 63 + lib/src/model/locatable.dart | 22 + lib/src/model/method.dart | 121 + lib/src/model/mixin.dart | 71 + lib/src/model/model.dart | 42 + lib/src/model/model_element.dart | 1777 +++++ lib/src/model/model_function.dart | 119 + lib/src/model/model_node.dart | 77 + lib/src/model/nameable.dart | 42 + lib/src/model/operator.dart | 62 + lib/src/model/package.dart | 326 + lib/src/model/package_builder.dart | 462 ++ lib/src/model/package_graph.dart | 916 +++ lib/src/model/parameter.dart | 75 + lib/src/model/privacy.dart | 8 + lib/src/model/source_code_mixin.dart | 21 + lib/src/model/top_level_container.dart | 72 + lib/src/model/top_level_variable.dart | 88 + lib/src/model/type_parameter.dart | 100 + lib/src/model/typedef.dart | 65 + lib/src/model_utils.dart | 2 +- lib/src/source_linker.dart | 2 +- lib/src/special_elements.dart | 2 +- lib/src/warnings.dart | 18 +- test/dartdoc_test.dart | 2 +- test/model_test.dart | 2 +- test/src/utils.dart | 2 +- 55 files changed, 7762 insertions(+), 7349 deletions(-) delete mode 100644 lib/src/model.dart create mode 100644 lib/src/model/accessor.dart create mode 100644 lib/src/model/canonicalization.dart create mode 100644 lib/src/model/categorization.dart create mode 100644 lib/src/model/category.dart create mode 100644 lib/src/model/class.dart create mode 100644 lib/src/model/constructor.dart create mode 100644 lib/src/model/container.dart create mode 100644 lib/src/model/container_member.dart create mode 100644 lib/src/model/documentable.dart create mode 100644 lib/src/model/dynamic.dart create mode 100644 lib/src/model/enclosed_element.dart create mode 100644 lib/src/model/enum.dart create mode 100644 lib/src/model/extendable.dart create mode 100644 lib/src/model/extension.dart create mode 100644 lib/src/model/field.dart create mode 100644 lib/src/model/getter_setter_combo.dart create mode 100644 lib/src/model/indexable.dart create mode 100644 lib/src/model/inheritable.dart create mode 100644 lib/src/model/library.dart create mode 100644 lib/src/model/library_container.dart create mode 100644 lib/src/model/locatable.dart create mode 100644 lib/src/model/method.dart create mode 100644 lib/src/model/mixin.dart create mode 100644 lib/src/model/model.dart create mode 100644 lib/src/model/model_element.dart create mode 100644 lib/src/model/model_function.dart create mode 100644 lib/src/model/model_node.dart create mode 100644 lib/src/model/nameable.dart create mode 100644 lib/src/model/operator.dart create mode 100644 lib/src/model/package.dart create mode 100644 lib/src/model/package_builder.dart create mode 100644 lib/src/model/package_graph.dart create mode 100644 lib/src/model/parameter.dart create mode 100644 lib/src/model/privacy.dart create mode 100644 lib/src/model/source_code_mixin.dart create mode 100644 lib/src/model/top_level_container.dart create mode 100644 lib/src/model/top_level_variable.dart create mode 100644 lib/src/model/type_parameter.dart create mode 100644 lib/src/model/typedef.dart diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart index 78eb3ea6fb..0a8e252668 100644 --- a/lib/dartdoc.dart +++ b/lib/dartdoc.dart @@ -16,7 +16,7 @@ import 'package:dartdoc/src/dartdoc_options.dart'; import 'package:dartdoc/src/generator.dart'; import 'package:dartdoc/src/html/html_generator.dart'; import 'package:dartdoc/src/logging.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/package_meta.dart'; import 'package:dartdoc/src/tuple.dart'; import 'package:dartdoc/src/utils.dart'; @@ -29,7 +29,7 @@ import 'package:path/path.dart' as path; export 'package:dartdoc/src/dartdoc_options.dart'; export 'package:dartdoc/src/element_type.dart'; export 'package:dartdoc/src/generator.dart'; -export 'package:dartdoc/src/model.dart'; +export 'package:dartdoc/src/model/model.dart'; export 'package:dartdoc/src/package_meta.dart'; const String programName = 'dartdoc'; diff --git a/lib/src/element_type.dart b/lib/src/element_type.dart index 19a230e936..f753c6d65b 100644 --- a/lib/src/element_type.dart +++ b/lib/src/element_type.dart @@ -10,7 +10,7 @@ import 'dart:collection'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/model_utils.dart'; /// Base class representing a type in Dartdoc. It wraps a [DartType], and diff --git a/lib/src/empty_generator.dart b/lib/src/empty_generator.dart index 92a96b84d8..016f2d1062 100644 --- a/lib/src/empty_generator.dart +++ b/lib/src/empty_generator.dart @@ -3,7 +3,7 @@ library dartdoc.empty_generator; import 'dart:async'; import 'package:dartdoc/src/generator.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/model_utils.dart'; /// A generator that does not generate files, but does traverse the [PackageGraph] diff --git a/lib/src/generator.dart b/lib/src/generator.dart index bd359885b8..758659a022 100644 --- a/lib/src/generator.dart +++ b/lib/src/generator.dart @@ -7,7 +7,7 @@ library dartdoc.generator; import 'dart:async' show Stream, Future; -import 'package:dartdoc/src/model.dart' show PackageGraph; +import 'package:dartdoc/src/model/model.dart' show PackageGraph; /// An abstract class that defines a generator that generates documentation for /// a given package. diff --git a/lib/src/html/html_generator.dart b/lib/src/html/html_generator.dart index a0ecb0058d..93fe22151b 100644 --- a/lib/src/html/html_generator.dart +++ b/lib/src/html/html_generator.dart @@ -14,7 +14,7 @@ import 'package:dartdoc/src/generator.dart'; import 'package:dartdoc/src/html/html_generator_instance.dart'; import 'package:dartdoc/src/html/template_data.dart'; import 'package:dartdoc/src/html/templates.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:path/path.dart' as path; typedef Renderer = String Function(String input); diff --git a/lib/src/html/html_generator_instance.dart b/lib/src/html/html_generator_instance.dart index 037ec674a8..1eb7a968c5 100644 --- a/lib/src/html/html_generator_instance.dart +++ b/lib/src/html/html_generator_instance.dart @@ -13,7 +13,7 @@ import 'package:dartdoc/src/html/resources.g.dart' as resources; import 'package:dartdoc/src/html/template_data.dart'; import 'package:dartdoc/src/html/templates.dart'; import 'package:dartdoc/src/logging.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/model_utils.dart'; import 'package:dartdoc/src/warnings.dart'; import 'package:mustache/mustache.dart'; diff --git a/lib/src/html/template_data.dart b/lib/src/html/template_data.dart index 7f25e2a4b3..8e1095b3f6 100644 --- a/lib/src/html/template_data.dart +++ b/lib/src/html/template_data.dart @@ -2,7 +2,7 @@ // 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:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; abstract class HtmlOptions { String get relCanonicalPrefix; diff --git a/lib/src/markdown_processor.dart b/lib/src/markdown_processor.dart index cfa3014f2e..5fab0eece2 100644 --- a/lib/src/markdown_processor.dart +++ b/lib/src/markdown_processor.dart @@ -10,7 +10,7 @@ import 'dart:math'; import 'package:analyzer/dart/element/element.dart'; import 'package:dartdoc/src/element_type.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/tuple.dart'; import 'package:dartdoc/src/warnings.dart'; import 'package:html/parser.dart' show parse; diff --git a/lib/src/model.dart b/lib/src/model.dart deleted file mode 100644 index b6d4e72d8e..0000000000 --- a/lib/src/model.dart +++ /dev/null @@ -1,7317 +0,0 @@ -// 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:async'; -import 'dart:collection' show HashSet, UnmodifiableListView; -import 'dart:convert'; -import 'dart:io'; - -import 'package:analyzer/dart/analysis/results.dart'; -import 'package:analyzer/dart/analysis/session.dart'; -import 'package:analyzer/dart/ast/ast.dart' - show - AnnotatedNode, - AstNode, - CommentReference, - CompilationUnit, - Expression, - InstanceCreationExpression; -import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/dart/element/visitor.dart'; -import 'package:analyzer/file_system/file_system.dart' as file_system; -import 'package:analyzer/file_system/physical_file_system.dart'; -import 'package:analyzer/source/line_info.dart'; -import 'package:analyzer/src/context/builder.dart'; -import 'package:analyzer/src/dart/analysis/byte_store.dart'; -import 'package:analyzer/src/dart/analysis/driver.dart'; -import 'package:analyzer/src/dart/analysis/file_state.dart'; -import 'package:analyzer/src/dart/analysis/performance_logger.dart'; -import 'package:analyzer/src/dart/element/element.dart'; -import 'package:analyzer/src/dart/element/inheritance_manager3.dart'; -import 'package:analyzer/src/dart/element/member.dart' - show ExecutableMember, Member, ParameterMember; -import 'package:analyzer/src/dart/sdk/sdk.dart'; -import 'package:analyzer/src/generated/engine.dart'; -import 'package:analyzer/src/generated/java_io.dart'; -import 'package:analyzer/src/generated/sdk.dart'; -import 'package:analyzer/src/generated/source.dart'; -import 'package:analyzer/src/generated/source_io.dart'; -import 'package:analyzer/src/generated/type_system.dart' show Dart2TypeSystem; -import 'package:analyzer/src/source/package_map_resolver.dart'; -import 'package:analyzer/src/source/sdk_ext.dart'; -import 'package:args/args.dart'; -import 'package:collection/collection.dart'; -import 'package:crypto/crypto.dart'; -import 'package:dartdoc/src/dartdoc_options.dart'; -import 'package:dartdoc/src/element_type.dart'; -import 'package:dartdoc/src/io_utils.dart'; -import 'package:dartdoc/src/logging.dart'; -import 'package:dartdoc/src/markdown_processor.dart' show Documentation; -import 'package:dartdoc/src/model_utils.dart' as utils; -import 'package:dartdoc/src/package_meta.dart' show PackageMeta, FileContents; -import 'package:dartdoc/src/source_linker.dart'; -import 'package:dartdoc/src/special_elements.dart'; -import 'package:dartdoc/src/tuple.dart'; -import 'package:dartdoc/src/utils.dart'; -import 'package:dartdoc/src/warnings.dart'; -import 'package:package_config/discovery.dart' as package_config; -import 'package:path/path.dart' as path; -import 'package:pub_semver/pub_semver.dart'; -import 'package:quiver/iterables.dart' as quiver; - -int byName(Nameable a, Nameable b) => - compareAsciiLowerCaseNatural(a.name, b.name); - -/// Items mapped less than zero will sort before custom annotations. -/// Items mapped above zero are sorted after custom annotations. -/// Items mapped to zero will sort alphabetically among custom annotations. -/// Custom annotations are assumed to be any annotation or feature not in this -/// map. -const Map featureOrder = { - 'read-only': 1, - 'write-only': 1, - 'read / write': 1, - 'covariant': 2, - 'final': 2, - 'inherited': 3, - 'inherited-getter': 3, - 'inherited-setter': 3, - 'override': 3, - 'override-getter': 3, - 'override-setter': 3, - 'extended': 3, -}; - -int byFeatureOrdering(String a, String b) { - int scoreA = 0; - int scoreB = 0; - - if (featureOrder.containsKey(a)) scoreA = featureOrder[a]; - if (featureOrder.containsKey(b)) scoreB = featureOrder[b]; - - if (scoreA < scoreB) return -1; - if (scoreA > scoreB) return 1; - return compareAsciiLowerCaseNatural(a, b); -} - -final RegExp locationSplitter = RegExp(r'(package:|[\\/;.])'); -final RegExp substituteNameVersion = RegExp(r'%([bnv])%'); - -/// This doc may need to be processed in case it has a template or html -/// fragment. -final needsPrecacheRegExp = RegExp(r'{@(template|tool|inject-html)'); - -final templateRegExp = RegExp( - r'[ ]*{@template\s+(.+?)}([\s\S]+?){@endtemplate}[ ]*\n?', - multiLine: true); -final htmlRegExp = RegExp( - r'[ ]*{@inject-html\s*}([\s\S]+?){@end-inject-html}[ ]*\n?', - multiLine: true); -final htmlInjectRegExp = RegExp(r'([a-f0-9]+)'); - -// Matches all tool directives (even some invalid ones). This is so -// we can give good error messages if the directive is malformed, instead of -// just silently emitting it as-is. -final basicToolRegExp = RegExp( - r'[ ]*{@tool\s+([^}]+)}\n?([\s\S]+?)\n?{@end-tool}[ ]*\n?', - multiLine: true); - -/// Regexp to take care of splitting arguments, and handling the quotes -/// around arguments, if any. -/// -/// Match group 1 is the "foo=" (or "--foo=") part of the option, if any. -/// Match group 2 contains the quote character used (which is discarded). -/// Match group 3 is a quoted arg, if any, without the quotes. -/// Match group 4 is the unquoted arg, if any. -final RegExp argMatcher = RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name - r'(?:' // Start a new non-capture group for the two possibilities. - r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes. - r'([^ ]+))'); // without quotes. - -final categoryRegexp = RegExp( - r'[ ]*{@(api|category|subCategory|image|samples) (.+?)}[ ]*\n?', - multiLine: true); -final macroRegExp = RegExp(r'{@macro\s+([^}]+)}'); - -/// Mixin for subclasses of [ModelElement] representing Elements that can be -/// extension methods. -mixin Extendable on ContainerMember { - /// Returns this Extendable from the [Extension] in which it was declared. - Extendable get definingExtension => throw UnimplementedError; - - @override - Container get canonicalEnclosingContainer => throw UnimplementedError; -} - -/// A [ModelElement] that is a [Container] member. -mixin ContainerMember on ModelElement implements EnclosedElement { - /// True if this [ContainerMember] is inherited from a different class. - bool get isInherited; - - /// True if this [ContainerMember] is overriding a superclass. - bool get isOverride; - - /// True if this [ContainerMember] has a parameter whose type is overridden - /// by a subtype. - bool get isCovariant; - - /// True if this [ContainerMember] is from an applicable [Extension]. - /// False otherwise, including if this [ContainerMember]'s [enclosingElement] - /// is the extension it was declared in. - // TODO(jcollins-g): This semantic is a little confusing, because a declared - // extension member element returns false. The rationale is an - // extension member is not extending itself. - // FIXME(jcollins-g): Remove concrete implementation after [Extendable] is - // implemented. - bool get isExtended => false; - - Container _definingEnclosingContainer; - Container get definingEnclosingContainer { - if (_definingEnclosingContainer == null) { - _definingEnclosingContainer = - ModelElement.fromElement(element.enclosingElement, packageGraph); - } - return _definingEnclosingContainer; - } - - @override - Set get features { - Set _features = super.features; - if (isOverride) _features.add('override'); - if (isInherited) _features.add('inherited'); - if (isCovariant) _features.add('covariant'); - if (isExtended) _features.add('extended'); - return _features; - } - - bool _canonicalEnclosingContainerIsSet = false; - Container _canonicalEnclosingContainer; - Container get canonicalEnclosingContainer { - if (!_canonicalEnclosingContainerIsSet) { - // TODO(jcollins-g): move Extension specific code to [Extendable] - if (enclosingElement is! Extension || - (enclosingElement is Extension && enclosingElement.isDocumented)) { - _canonicalEnclosingContainer = - packageGraph.findCanonicalModelElementFor(enclosingElement.element); - } - _canonicalEnclosingContainerIsSet = true; - assert(_canonicalEnclosingContainer == null || - _canonicalEnclosingContainer.isDocumented); - } - assert(_canonicalEnclosingContainer == null || - (_canonicalEnclosingContainer.isDocumented)); - return _canonicalEnclosingContainer; - } -} - -/// Mixin for subclasses of ModelElement representing Elements that can be -/// inherited from one class to another. -/// -/// We can search the inheritance chain between this instance and -/// [definingEnclosingContainer] in [Inheritable.canonicalEnclosingContainer], -/// for the canonical [Class] closest to where this member was defined. We -/// can then know that when we find [Inheritable.element] inside that [Class]'s -/// namespace, that's the one we should treat as canonical and implementors -/// of this class can use that knowledge to determine canonicalization. -/// -/// We pick the class closest to the [definingEnclosingElement] so that all -/// children of that class inheriting the same member will point to the same -/// place in the documentation, and we pick a canonical class because that's -/// the one in the public namespace that will be documented. -mixin Inheritable on ContainerMember { - @override - ModelElement _buildCanonicalModelElement() { - if (canonicalEnclosingContainer is Extension) { - return this; - } - if (canonicalEnclosingContainer is Class) { - return (canonicalEnclosingContainer as Class) - ?.allCanonicalModelElements - ?.firstWhere( - (m) => - m.name == name && m.isPropertyAccessor == isPropertyAccessor, - orElse: () => null); - } - return null; - } - - @override - Container get canonicalEnclosingContainer { - Element searchElement = element; - if (!_canonicalEnclosingContainerIsSet) { - if (isInherited) { - searchElement = searchElement is Member - ? PackageGraph.getBasestElement(searchElement) - : searchElement; - // TODO(jcollins-g): generate warning if an inherited element's definition - // is in an intermediate non-canonical class in the inheritance chain? - Class previous; - Class previousNonSkippable; - for (Class c in inheritance.reversed) { - // Filter out mixins. - if (c.contains(searchElement)) { - if ((packageGraph.inheritThrough.contains(previous) && - c != definingEnclosingContainer) || - (packageGraph.inheritThrough.contains(c) && - c == definingEnclosingContainer)) { - return (previousNonSkippable.memberByExample(this) as Inheritable) - .canonicalEnclosingContainer; - } - Class canonicalC = - packageGraph.findCanonicalModelElementFor(c.element); - // TODO(jcollins-g): invert this lookup so traversal is recursive - // starting from the ModelElement. - if (canonicalC != null) { - assert(canonicalC.isCanonical); - assert(canonicalC.contains(searchElement)); - _canonicalEnclosingContainer = canonicalC; - break; - } - } - previous = c; - if (!packageGraph.inheritThrough.contains(c)) { - previousNonSkippable = c; - } - } - // This is still OK because we're never supposed to cloak public - // classes. - if (definingEnclosingContainer.isCanonical && - definingEnclosingContainer.isPublic) { - assert(definingEnclosingContainer == _canonicalEnclosingContainer); - } - _canonicalEnclosingContainerIsSet = true; - assert(_canonicalEnclosingContainer == null || - _canonicalEnclosingContainer.isDocumented); - } - } - return super.canonicalEnclosingContainer; - } - - List get inheritance { - List inheritance = []; - inheritance.addAll((enclosingElement as Class).inheritanceChain); - Class object = packageGraph.specialClasses[SpecialClass.object]; - if (!inheritance.contains(definingEnclosingContainer) && - definingEnclosingContainer != null) { - assert(definingEnclosingContainer == object); - } - // Unless the code explicitly extends dart-core's Object, we won't get - // an entry here. So add it. - if (inheritance.last != object && object != null) { - inheritance.add(object); - } - assert(inheritance.where((e) => e == object).length == 1); - return inheritance; - } - - Inheritable get overriddenElement; - - bool _isOverride; - - @override - bool get isOverride { - if (_isOverride == null) { - // The canonical version of the enclosing element -- not canonicalEnclosingElement, - // as that is the element enclosing the canonical version of this element, - // two different things. Defaults to the enclosing element. - // - // We use canonical elements here where possible to deal with reexports - // as seen in Flutter. - if (enclosingElement is Extension) { - _isOverride = false; - return _isOverride; - } - Class enclosingCanonical = enclosingElement.canonicalModelElement; - // The container in which this element was defined, canonical if available. - Container definingCanonical = - definingEnclosingContainer.canonicalModelElement ?? - definingEnclosingContainer; - // The canonical version of the element we're overriding, if available. - ModelElement overriddenCanonical = - overriddenElement?.canonicalModelElement ?? overriddenElement; - - // We have to have an overridden element for it to be possible for this - // element to be an override. - _isOverride = overriddenElement != null && - // The defining class and the enclosing class for this element - // must be the same (element is defined here). - enclosingCanonical == definingCanonical && - // If the overridden element isn't public, we shouldn't be an - // override in most cases. Approximation until #1623 is fixed. - overriddenCanonical.isPublic; - assert(!(_isOverride && isInherited)); - } - return _isOverride; - } - - int _overriddenDepth; - @override - int get overriddenDepth { - if (_overriddenDepth == null) { - _overriddenDepth = 0; - Inheritable e = this; - while (e.overriddenElement != null) { - _overriddenDepth += 1; - e = e.overriddenElement; - } - } - return _overriddenDepth; - } -} - -/// A getter or setter that is a member of a [Container]. -class ContainerAccessor extends Accessor with ContainerMember, Inheritable { - /// Factory will return an [ContainerAccessor] with isInherited = true - /// if [element] is in [inheritedAccessors]. - factory ContainerAccessor.from(PropertyAccessorElement element, - Set inheritedAccessors, Class enclosingClass) { - ContainerAccessor accessor; - if (element == null) return null; - if (inheritedAccessors.contains(element)) { - accessor = ModelElement.from( - element, enclosingClass.library, enclosingClass.packageGraph, - enclosingContainer: enclosingClass); - } else { - accessor = ModelElement.from( - element, enclosingClass.library, enclosingClass.packageGraph); - } - return accessor; - } - - ModelElement _enclosingElement; - bool _isInherited = false; - - @override - bool get isCovariant => isSetter && parameters.first.isCovariant; - - ContainerAccessor(PropertyAccessorElement element, Library library, - PackageGraph packageGraph) - : super(element, library, packageGraph, null); - - ContainerAccessor.inherited(PropertyAccessorElement element, Library library, - PackageGraph packageGraph, this._enclosingElement, - {Member originalMember}) - : super(element, library, packageGraph, originalMember) { - _isInherited = true; - } - - @override - bool get isInherited => _isInherited; - - @override - Container get enclosingElement { - if (_enclosingElement == null) { - _enclosingElement = super.enclosingElement; - } - return _enclosingElement; - } - - bool _overriddenElementIsSet = false; - ModelElement _overriddenElement; - - @override - ContainerAccessor get overriddenElement { - assert(packageGraph.allLibrariesAdded); - if (!_overriddenElementIsSet) { - _overriddenElementIsSet = true; - Element parent = element.enclosingElement; - if (parent is ClassElement) { - for (InterfaceType t in parent.allSupertypes) { - Element accessor = this.isGetter - ? t.getGetter(element.name) - : t.getSetter(element.name); - if (accessor != null) { - if (accessor is Member) { - accessor = PackageGraph.getBasestElement(accessor); - } - Class parentClass = - ModelElement.fromElement(t.element, packageGraph); - List possibleFields = []; - possibleFields.addAll(parentClass.allInstanceFields); - possibleFields.addAll(parentClass.staticProperties); - String fieldName = accessor.name.replaceFirst('=', ''); - Field foundField = possibleFields.firstWhere( - (f) => f.element.name == fieldName, - orElse: () => null); - if (foundField != null) { - if (this.isGetter) { - _overriddenElement = foundField.getter; - } else { - _overriddenElement = foundField.setter; - } - assert(!(_overriddenElement as ContainerAccessor).isInherited); - break; - } - } - } - } - } - return _overriddenElement; - } -} - -/// Getters and setters. -class Accessor extends ModelElement implements EnclosedElement { - GetterSetterCombo _enclosingCombo; - - Accessor(PropertyAccessorElement element, Library library, - PackageGraph packageGraph, Member originalMember) - : super(element, library, packageGraph, originalMember); - - String get linkedReturnType { - assert(isGetter); - return modelType.createLinkedReturnTypeName(); - } - - GetterSetterCombo get enclosingCombo { - assert(_enclosingCombo != null); - return _enclosingCombo; - } - - bool get isSynthetic => element.isSynthetic; - - @override - String get sourceCode { - if (_sourceCode == null) { - if (isSynthetic) { - _sourceCode = packageGraph - ._getModelNodeFor((element as PropertyAccessorElement).variable) - .sourceCode; - } else { - _sourceCode = super.sourceCode; - } - } - return _sourceCode; - } - - @override - List get computeDocumentationFrom { - return super.computeDocumentationFrom; - } - - @override - String _computeDocumentationComment() { - if (isSynthetic) { - String docComment = - (element as PropertyAccessorElement).variable.documentationComment; - // If we're a setter, only display something if we have something different than the getter. - // TODO(jcollins-g): modify analyzer to do this itself? - if (isGetter || - // TODO(jcollins-g): @nodoc reading from comments is at the wrong abstraction level here. - (docComment != null && - (docComment.contains('') || - docComment.contains('@nodoc'))) || - (isSetter && - enclosingCombo.hasGetter && - enclosingCombo.getter.documentationComment != docComment)) { - return stripComments(docComment); - } else { - return ''; - } - } - return stripComments(super._computeDocumentationComment()); - } - - @override - void warn(PackageWarning kind, - {String message, - Iterable referredFrom, - Iterable extendedDebug}) { - enclosingCombo.warn(kind, - message: message, - referredFrom: referredFrom, - extendedDebug: extendedDebug); - } - - @override - ModelElement get enclosingElement { - if (_accessor.enclosingElement is CompilationUnitElement) { - return packageGraph.findButDoNotCreateLibraryFor( - _accessor.enclosingElement.enclosingElement); - } - - return ModelElement.from(_accessor.enclosingElement, library, packageGraph); - } - - @override - bool get isCanonical => enclosingCombo.isCanonical; - - @override - String get href { - return enclosingCombo.href; - } - - bool get isGetter => _accessor.isGetter; - - bool get isSetter => _accessor.isSetter; - - @override - String get kind => 'accessor'; - - @override - String get namePart { - if (_namePart == null) { - _namePart = super.namePart.split('=').first; - } - return _namePart; - } - - PropertyAccessorElement get _accessor => (element as PropertyAccessorElement); -} - -/// Implements the Dart 2.1 "mixin" style of mixin declarations. -class Mixin extends Class { - Mixin(ClassElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph); - - @override - bool get isAbstract => false; - - @override - List get inheritanceChain { - if (_inheritanceChain == null) { - _inheritanceChain = []; - _inheritanceChain.add(this); - - // Mix-in interfaces come before other interfaces. - _inheritanceChain.addAll(superclassConstraints - .expand((ParameterizedElementType i) => - (i.element as Class).inheritanceChain) - .where((Class c) => - c != packageGraph.specialClasses[SpecialClass.object])); - - // Interfaces need to come last, because classes in the superChain might - // implement them even when they aren't mentioned. - _inheritanceChain.addAll( - interfaces.expand((e) => (e.element as Class).inheritanceChain)); - } - return _inheritanceChain.toList(growable: false); - } - - List _superclassConstraints; - - /// Returns a list of superclass constraints for this mixin. - Iterable get superclassConstraints { - if (_superclassConstraints == null) { - _superclassConstraints = (element as ClassElement) - .superclassConstraints - .map( - (InterfaceType i) => ElementType.from(i, library, packageGraph)) - .toList(); - } - return _superclassConstraints; - } - - bool get hasPublicSuperclassConstraints => - publicSuperclassConstraints.isNotEmpty; - - Iterable get publicSuperclassConstraints => - utils.filterNonPublic(superclassConstraints); - - @override - bool get hasModifiers => super.hasModifiers || hasPublicSuperclassConstraints; - - @override - String get fileName => "${name}-mixin.html"; - - @override - String get kind => 'mixin'; -} - -// Can be either a Class or Extension, used in the package graph and template data. -// Aggregates some of the common getters. -abstract class Container extends ModelElement { - List _allMethods; - List _constants; - List _operators; - List _staticMethods; - List _instanceMethods; - List _fields; - List _staticFields; - List _instanceFields; - List _typeParameters; - - Container(Element element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph, null); - - bool get isClass => element is ClassElement; - - bool get isExtension => element is ExtensionElement; - - List get _methods => _allMethods; - - List get instanceMethods { - if (_instanceMethods != null) return _instanceMethods; - - _instanceMethods = _methods - .where((m) => !m.isStatic && !m.isOperator) - .toList(growable: false) - ..sort(byName); - return _instanceMethods; - } - - bool get hasPublicMethods => - utils.filterNonPublic(instanceMethods).isNotEmpty; - - Iterable get allPublicInstanceMethods => - utils.filterNonPublic(instanceMethods); - - List get staticMethods { - if (_staticMethods != null) return _staticMethods; - - _staticMethods = _methods.where((m) => m.isStatic).toList(growable: false) - ..sort(byName); - - return _staticMethods; - } - - bool get hasPublicStaticMethods => - utils.filterNonPublic(staticMethods).isNotEmpty; - - Iterable get publicStaticMethods => - utils.filterNonPublic(staticMethods); - - List get operators { - if (_operators != null) return _operators; - _operators = _methods - .where((m) => m.isOperator) - .cast() - .toList(growable: false) - ..sort(byName); - return _operators; - } - - Iterable get allOperators => operators; - - bool get hasPublicOperators => publicOperators.isNotEmpty; - - Iterable get allPublicOperators => - utils.filterNonPublic(allOperators); - - Iterable get publicOperators => utils.filterNonPublic(operators); - - List get _allFields => []; - - List get staticProperties { - if (_staticFields != null) return _staticFields; - _staticFields = _allFields - .where((f) => f.isStatic && !f.isConst) - .toList(growable: false) - ..sort(byName); - - return _staticFields; - } - - Iterable get publicStaticProperties => - utils.filterNonPublic(staticProperties); - - bool get hasPublicStaticProperties => publicStaticProperties.isNotEmpty; - - List get instanceProperties { - if (_instanceFields != null) return _instanceFields; - _instanceFields = _allFields - .where((f) => !f.isStatic && !f.isInherited && !f.isConst) - .toList(growable: false) - ..sort(byName); - - return _instanceFields; - } - - Iterable get publicInstanceProperties => - utils.filterNonPublic(instanceProperties); - - bool get hasPublicProperties => publicInstanceProperties.isNotEmpty; - - Iterable get allInstanceFields => instanceProperties; - - Iterable get allPublicInstanceProperties => - utils.filterNonPublic(allInstanceFields); - - bool isInheritingFrom(Container other) => false; - - List get constants { - if (_constants != null) return _constants; - _constants = _allFields.where((f) => f.isConst).toList(growable: false) - ..sort(byName); - - return _constants; - } - - Iterable get publicConstants => utils.filterNonPublic(constants); - - bool get hasPublicConstants => publicConstants.isNotEmpty; - - Iterable get allAccessors => quiver.concat([ - allInstanceFields.expand((f) => f.allAccessors), - constants.map((c) => c.getter) - ]); -} - -class Class extends Container - with TypeParameters, Categorization - implements EnclosedElement { - List _mixins; - DefinedElementType supertype; - List _interfaces; - List _constructors; - List _inheritedOperators; - List _inheritedMethods; - List _inheritedProperties; - - Class(ClassElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph) { - packageGraph.specialClasses.addSpecial(this); - _mixins = _cls.mixins - .map((f) { - DefinedElementType t = ElementType.from(f, library, packageGraph); - return t; - }) - .where((mixin) => mixin != null) - .toList(growable: false); - - if (_cls.supertype != null && _cls.supertype.element.supertype != null) { - supertype = ElementType.from(_cls.supertype, library, packageGraph); - } - - _interfaces = _cls.interfaces - .map((f) => - ElementType.from(f, library, packageGraph) as DefinedElementType) - .toList(growable: false); - } - - Constructor _defaultConstructor; - - Constructor get defaultConstructor { - if (_defaultConstructor == null) { - _defaultConstructor = constructors - .firstWhere((c) => c.isDefaultConstructor, orElse: () => null); - } - return _defaultConstructor; - } - - bool get hasPotentiallyApplicableExtensions => - potentiallyApplicableExtensions.isNotEmpty; - - List _potentiallyApplicableExtensions; - Iterable get potentiallyApplicableExtensions { - if (_potentiallyApplicableExtensions == null) { - _potentiallyApplicableExtensions = utils - .filterNonDocumented(packageGraph.extensions) - .where((e) => e.couldApplyTo(this)) - .toList(growable: false) - ..sort(byName); - } - return _potentiallyApplicableExtensions; - } - - Iterable get allInstanceMethods => - quiver.concat([instanceMethods, inheritedMethods]); - - @override - Iterable get allPublicInstanceMethods => - utils.filterNonPublic(allInstanceMethods); - - bool get allPublicInstanceMethodsInherited => - instanceMethods.every((f) => f.isInherited); - - @override - Iterable get allInstanceFields => - quiver.concat([instanceProperties, inheritedProperties]); - - bool get allPublicInstancePropertiesInherited => - allPublicInstanceProperties.every((f) => f.isInherited); - - @override - Iterable get allOperators => - quiver.concat([operators, inheritedOperators]); - - bool get allPublicOperatorsInherited => - allPublicOperators.every((f) => f.isInherited); - - Map _allElements; - - Map get allElements { - if (_allElements == null) { - _allElements = Map(); - for (ModelElement me in allModelElements) { - assert(!_allElements.containsKey(me.element)); - _allElements[me.element] = me; - } - } - return _allElements; - } - - Map> _allModelElementsByNamePart; - - /// Helper for [_MarkdownCommentReference._getResultsForClass]. - Map> get allModelElementsByNamePart { - if (_allModelElementsByNamePart == null) { - _allModelElementsByNamePart = {}; - for (ModelElement me in allModelElements) { - _allModelElementsByNamePart.update( - me.namePart, (List v) => v..add(me), - ifAbsent: () => [me]); - } - } - return _allModelElementsByNamePart; - } - - /// This class might be canonical for elements it does not contain. - /// See [Inheritable.canonicalEnclosingContainer]. - bool contains(Element element) => allElements.containsKey(element); - - Map> _membersByName; - - /// Given a ModelElement that is a member of some other class, return - /// a member of this class that has the same name and return type. - /// - /// This enables object substitution for canonicalization, such as Interceptor - /// for Object. - ModelElement memberByExample(ModelElement example) { - if (_membersByName == null) { - _membersByName = Map(); - for (ModelElement me in allModelElements) { - if (!_membersByName.containsKey(me.name)) { - _membersByName[me.name] = List(); - } - _membersByName[me.name].add(me); - } - } - ModelElement member; - Iterable possibleMembers = _membersByName[example.name] - .where((e) => e.runtimeType == example.runtimeType); - if (example.runtimeType == Accessor) { - possibleMembers = possibleMembers.where( - (e) => (example as Accessor).isGetter == (e as Accessor).isGetter); - } - member = possibleMembers.first; - assert(possibleMembers.length == 1); - return member; - } - - List _allModelElements; - - List get allModelElements { - if (_allModelElements == null) { - _allModelElements = List.from( - quiver.concat([ - allInstanceMethods, - allInstanceFields, - allAccessors, - allOperators, - constants, - constructors, - staticMethods, - staticProperties, - typeParameters, - ]), - growable: false); - } - return _allModelElements; - } - - List _allCanonicalModelElements; - - List get allCanonicalModelElements { - return (_allCanonicalModelElements ??= - allModelElements.where((e) => e.isCanonical).toList()); - } - - List get constructors { - if (_constructors != null) return _constructors; - - _constructors = _cls.constructors.map((e) { - return ModelElement.from(e, library, packageGraph) as Constructor; - }).toList(growable: true) - ..sort(byName); - - return _constructors; - } - - Iterable get publicConstructors => - utils.filterNonPublic(constructors); - - /// Returns the library that encloses this element. - @override - ModelElement get enclosingElement => library; - - @override - String get fileName => "${name}-class.html"; - - String get fullkind { - if (isAbstract) return 'abstract $kind'; - return kind; - } - - bool get hasPublicConstructors => publicConstructors.isNotEmpty; - - bool get hasPublicImplementors => publicImplementors.isNotEmpty; - - bool get hasInstanceProperties => instanceProperties.isNotEmpty; - - bool get hasPublicInterfaces => publicInterfaces.isNotEmpty; - - @override - bool get hasPublicMethods => - publicInstanceMethods.isNotEmpty || publicInheritedMethods.isNotEmpty; - - bool get hasPublicMixins => publicMixins.isNotEmpty; - - bool get hasModifiers => - hasPublicMixins || - hasAnnotations || - hasPublicInterfaces || - hasPublicSuperChainReversed || - hasPublicImplementors || - hasPotentiallyApplicableExtensions; - - @override - bool get hasPublicOperators => - publicOperators.isNotEmpty || publicInheritedOperators.isNotEmpty; - - @override - bool get hasPublicProperties => - publicInheritedProperties.isNotEmpty || - publicInstanceProperties.isNotEmpty; - - @override - bool get hasPublicStaticMethods => publicStaticMethods.isNotEmpty; - - bool get hasPublicSuperChainReversed => publicSuperChainReversed.isNotEmpty; - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(canonicalLibrary != null); - assert(canonicalLibrary == library); - return '${package.baseHref}${library.dirName}/$fileName'; - } - - /// Returns all the implementors of this class. - Iterable get publicImplementors { - return utils.filterNonPublic(utils.findCanonicalFor( - packageGraph.implementors[href] != null - ? packageGraph.implementors[href] - : [])); - } - - List get inheritedMethods { - if (_inheritedMethods == null) { - _inheritedMethods = []; - Set methodNames = _methods.map((m) => m.element.name).toSet(); - - Set inheritedMethodElements = - _inheritedElements.where((e) { - return (e is MethodElement && - !e.isOperator && - e is! PropertyAccessorElement && - !methodNames.contains(e.name)); - }).toSet(); - - for (ExecutableElement e in inheritedMethodElements) { - Method m = ModelElement.from(e, library, packageGraph, - enclosingContainer: this); - _inheritedMethods.add(m); - } - _inheritedMethods.sort(byName); - } - return _inheritedMethods; - } - - Iterable get publicInheritedMethods => - utils.filterNonPublic(inheritedMethods); - - bool get hasPublicInheritedMethods => publicInheritedMethods.isNotEmpty; - - List get inheritedOperators { - if (_inheritedOperators == null) { - _inheritedOperators = []; - Set operatorNames = _operators.map((o) => o.element.name).toSet(); - - Set inheritedOperatorElements = - _inheritedElements.where((e) { - return (e is MethodElement && - e.isOperator && - !operatorNames.contains(e.name)); - }).toSet(); - for (ExecutableElement e in inheritedOperatorElements) { - Operator o = ModelElement.from(e, library, packageGraph, - enclosingContainer: this); - _inheritedOperators.add(o); - } - _inheritedOperators.sort(byName); - } - return _inheritedOperators; - } - - Iterable get publicInheritedOperators => - utils.filterNonPublic(inheritedOperators); - - List get inheritedProperties { - if (_inheritedProperties == null) { - _inheritedProperties = _allFields.where((f) => f.isInherited).toList() - ..sort(byName); - } - return _inheritedProperties; - } - - Iterable get publicInheritedProperties => - utils.filterNonPublic(inheritedProperties); - - Iterable get publicInstanceMethods => instanceMethods; - - List get interfaces => _interfaces; - - Iterable get publicInterfaces => - utils.filterNonPublic(interfaces); - - bool get isAbstract => _cls.isAbstract; - - @override - bool get isCanonical => super.isCanonical && isPublic; - - bool get isErrorOrException { - bool _doCheck(ClassElement element) { - return (element.library.isDartCore && - (element.name == 'Exception' || element.name == 'Error')); - } - - // if this class is itself Error or Exception, return true - if (_doCheck(_cls)) return true; - - return _cls.allSupertypes.map((t) => t.element).any(_doCheck); - } - - /// Returns true if [other] is a parent class for this class. - @override - bool isInheritingFrom(covariant Class other) => - superChain.map((et) => (et.element as Class)).contains(other); - - @override - String get kind => 'class'; - - List get mixins => _mixins; - - Iterable get publicMixins => - utils.filterNonPublic(mixins); - - @override - DefinedElementType get modelType => super.modelType; - - /// Not the same as superChain as it may include mixins. - /// It's really not even the same as ordinary Dart inheritance, either, - /// because we pretend that interfaces are part of the inheritance chain - /// to include them in the set of things we might link to for documentation - /// purposes in abstract classes. - List _inheritanceChain; - - List get inheritanceChain { - if (_inheritanceChain == null) { - _inheritanceChain = []; - _inheritanceChain.add(this); - - /// Caching should make this recursion a little less painful. - for (Class c in mixins.reversed.map((e) => (e.element as Class))) { - _inheritanceChain.addAll(c.inheritanceChain); - } - - for (Class c in superChain.map((e) => (e.element as Class))) { - _inheritanceChain.addAll(c.inheritanceChain); - } - - /// Interfaces need to come last, because classes in the superChain might - /// implement them even when they aren't mentioned. - _inheritanceChain.addAll( - interfaces.expand((e) => (e.element as Class).inheritanceChain)); - } - return _inheritanceChain.toList(growable: false); - } - - List get superChain { - List typeChain = []; - DefinedElementType parent = supertype; - while (parent != null) { - typeChain.add(parent); - if (parent.type is InterfaceType) { - // Avoid adding [Object] to the superChain (_supertype already has this - // check) - if ((parent.type as InterfaceType)?.superclass?.superclass == null) { - parent = null; - } else { - parent = ElementType.from( - (parent.type as InterfaceType).superclass, library, packageGraph); - } - } else { - parent = (parent.element as Class).supertype; - } - } - return typeChain; - } - - Iterable get publicSuperChain => - utils.filterNonPublic(superChain); - - Iterable get publicSuperChainReversed => - publicSuperChain.toList().reversed; - - List __inheritedElements; - - List get _inheritedElements { - if (__inheritedElements == null) { - var classElement = element as ClassElement; - if (classElement.isDartCoreObject) { - return __inheritedElements = []; - } - - var inheritance = definingLibrary.inheritanceManager; - var classType = classElement.thisType; - var cmap = inheritance.getInheritedConcreteMap(classType); - var imap = inheritance.getInheritedMap(classType); - - var combinedMap = {}; - for (var nameObj in cmap.keys) { - combinedMap[nameObj.name] = cmap[nameObj]; - } - for (var nameObj in imap.keys) { - combinedMap[nameObj.name] ??= imap[nameObj]; - } - - __inheritedElements = combinedMap.values.toList(); - } - return __inheritedElements; - } - - /// Internal only because subclasses are allowed to override how - /// these are mapped to [allInheritedFields] and so forth. - @override - List get _allFields { - if (_fields != null) return _fields; - _fields = []; - Set inheritedAccessors = Set() - ..addAll(_inheritedElements.whereType()); - - // This structure keeps track of inherited accessors, allowing lookup - // by field name (stripping the '=' from setters). - Map> accessorMap = Map(); - for (PropertyAccessorElement accessorElement in inheritedAccessors) { - String name = accessorElement.name.replaceFirst('=', ''); - accessorMap.putIfAbsent(name, () => []); - accessorMap[name].add(accessorElement); - } - - // For half-inherited fields, the analyzer only links the non-inherited - // to the [FieldElement]. Compose our [Field] class by hand by looking up - // inherited accessors that may be related. - for (FieldElement f in _cls.fields) { - PropertyAccessorElement getterElement = f.getter; - if (getterElement == null && accessorMap.containsKey(f.name)) { - getterElement = accessorMap[f.name] - .firstWhere((e) => e.isGetter, orElse: () => null); - } - PropertyAccessorElement setterElement = f.setter; - if (setterElement == null && accessorMap.containsKey(f.name)) { - setterElement = accessorMap[f.name] - .firstWhere((e) => e.isSetter, orElse: () => null); - } - _addSingleField(getterElement, setterElement, inheritedAccessors, f); - accessorMap.remove(f.name); - } - - // Now we only have inherited accessors who aren't associated with - // anything in cls._fields. - for (String fieldName in accessorMap.keys) { - List elements = accessorMap[fieldName].toList(); - PropertyAccessorElement getterElement = - elements.firstWhere((e) => e.isGetter, orElse: () => null); - PropertyAccessorElement setterElement = - elements.firstWhere((e) => e.isSetter, orElse: () => null); - _addSingleField(getterElement, setterElement, inheritedAccessors); - } - - _fields.sort(byName); - return _fields; - } - - /// Add a single Field to _fields. - /// - /// If [f] is not specified, pick the FieldElement from the PropertyAccessorElement - /// whose enclosing class inherits from the other (defaulting to the getter) - /// and construct a Field using that. - void _addSingleField( - PropertyAccessorElement getterElement, - PropertyAccessorElement setterElement, - Set inheritedAccessors, - [FieldElement f]) { - ContainerAccessor getter = - ContainerAccessor.from(getterElement, inheritedAccessors, this); - ContainerAccessor setter = - ContainerAccessor.from(setterElement, inheritedAccessors, this); - // Rebind getterElement/setterElement as ModelElement.from can resolve - // MultiplyInheritedExecutableElements or resolve Members. - getterElement = getter?.element; - setterElement = setter?.element; - assert(!(getter == null && setter == null)); - if (f == null) { - // Pick an appropriate FieldElement to represent this element. - // Only hard when dealing with a synthetic Field. - if (getter != null && setter == null) { - f = getterElement.variable; - } else if (getter == null && setter != null) { - f = setterElement.variable; - } else - /* getter != null && setter != null */ { - // In cases where a Field is composed of two Accessors defined in - // different places in the inheritance chain, there are two FieldElements - // for this single Field we're trying to compose. Pick the one closest - // to this class on the inheritance chain. - if ((setter.enclosingElement) - .isInheritingFrom(getter.enclosingElement)) { - f = setterElement.variable; - } else { - f = getterElement.variable; - } - } - } - Field field; - if ((getter == null || getter.isInherited) && - (setter == null || setter.isInherited)) { - // Field is 100% inherited. - field = ModelElement.from(f, library, packageGraph, - enclosingContainer: this, getter: getter, setter: setter); - } else { - // Field is <100% inherited (could be half-inherited). - // TODO(jcollins-g): Navigation is probably still confusing for - // half-inherited fields when traversing the inheritance tree. Make - // this better, somehow. - field = ModelElement.from(f, library, packageGraph, - getter: getter, setter: setter); - } - _fields.add(field); - } - - ClassElement get _cls => (element as ClassElement); - - @override - List get _methods { - if (_allMethods != null) return _allMethods; - - _allMethods = _cls.methods.map((e) { - return ModelElement.from(e, library, packageGraph) as Method; - }).toList(growable: false) - ..sort(byName); - - return _allMethods; - } - - // a stronger hash? - @override - List get typeParameters { - if (_typeParameters == null) { - _typeParameters = _cls.typeParameters.map((f) { - var lib = Library(f.enclosingElement.library, packageGraph); - return ModelElement.from(f, lib, packageGraph) as TypeParameter; - }).toList(); - } - return _typeParameters; - } - - @override - bool operator ==(o) => - o is Class && - name == o.name && - o.library.name == library.name && - o.library.package.name == library.package.name; -} - -/// Extension methods -class Extension extends Container - with TypeParameters, Categorization - implements EnclosedElement { - DefinedElementType extendedType; - - Extension( - ExtensionElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph) { - extendedType = - ElementType.from(_extension.extendedType, library, packageGraph); - } - - bool couldApplyTo(Class c) => _couldApplyTo(c.modelType); - - /// Return true if this extension could apply to [t]. - bool _couldApplyTo(DefinedElementType t) { - return t.instantiatedType == extendedType.instantiatedType || - (t.instantiatedType.element == extendedType.instantiatedType.element && - isSubtypeOf(t)) || - isBoundSupertypeTo(t); - } - - /// The instantiated to bounds [extendedType] of this extension is a subtype of - /// [t]. - bool isSubtypeOf(DefinedElementType t) => packageGraph.typeSystem - .isSubtypeOf(extendedType.instantiatedType, t.instantiatedType); - - bool isBoundSupertypeTo(DefinedElementType t) => - _isBoundSupertypeTo(t.type, HashSet()); - - /// Returns true if at least one supertype (including via mixins and - /// interfaces) is equivalent to or a subtype of [extendedType] when - /// instantiated to bounds. - bool _isBoundSupertypeTo( - InterfaceType superType, HashSet visited) { - ClassElement superClass = superType?.element; - if (visited.contains(superType)) return false; - visited.add(superType); - if (superClass == extendedType.type.element && - (superType == extendedType.instantiatedType || - packageGraph.typeSystem - .isSubtypeOf(superType, extendedType.instantiatedType))) { - return true; - } - List supertypes = []; - ClassElementImpl.collectAllSupertypes(supertypes, superType, null); - for (InterfaceType toVisit in supertypes) { - if (_isBoundSupertypeTo(toVisit, visited)) return true; - } - return false; - } - - @override - ModelElement get enclosingElement => library; - - ExtensionElement get _extension => (element as ExtensionElement); - - @override - String get kind => 'extension'; - - @override - List get _methods { - if (_allMethods != null) return _allMethods; - - _allMethods = _extension.methods.map((e) { - return ModelElement.from(e, library, packageGraph) as Method; - }).toList(growable: false) - ..sort(byName); - - return _allMethods; - } - - @override - List get _allFields { - if (_fields != null) return _fields; - _fields = _extension.fields.map((f) { - Accessor getter, setter; - if (f.getter != null) { - getter = ContainerAccessor(f.getter, library, packageGraph); - } - if (f.setter != null) { - setter = ContainerAccessor(f.setter, library, packageGraph); - } - return ModelElement.from(f, library, packageGraph, - getter: getter, setter: setter) as Field; - }).toList(growable: false) - ..sort(byName); - - return _fields; - } - - // a stronger hash? - @override - List get typeParameters { - if (_typeParameters == null) { - _typeParameters = _extension.typeParameters.map((f) { - var lib = Library(f.enclosingElement.library, packageGraph); - return ModelElement.from(f, lib, packageGraph) as TypeParameter; - }).toList(); - } - return _typeParameters; - } - - @override - ParameterizedElementType get modelType => super.modelType; - - List _allModelElements; - - List get allModelElements { - if (_allModelElements == null) { - _allModelElements = List.from( - quiver.concat([ - instanceMethods, - allInstanceFields, - allAccessors, - allOperators, - constants, - staticMethods, - staticProperties, - typeParameters, - ]), - growable: false); - } - return _allModelElements; - } - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(canonicalLibrary != null); - assert(canonicalLibrary == library); - return '${package.baseHref}${library.dirName}/$fileName'; - } -} - -class Constructor extends ModelElement - with TypeParameters - implements EnclosedElement { - Constructor( - ConstructorElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph, null); - - @override - CharacterLocation get characterLocation { - if (element.isSynthetic) { - // Make warnings for a synthetic constructor refer to somewhere reasonable - // since a synthetic constructor has no definition independent of the - // parent class. - return enclosingElement.characterLocation; - } - return super.characterLocation; - } - - @override - // TODO(jcollins-g): Revisit this when dart-lang/sdk#31517 is implemented. - List get typeParameters => - (enclosingElement as Class).typeParameters; - - @override - ModelElement get enclosingElement => - ModelElement.from(_constructor.enclosingElement, library, packageGraph); - - String get fullKind { - if (isConst) return 'const $kind'; - if (isFactory) return 'factory $kind'; - return kind; - } - - @override - String get fullyQualifiedName { - if (isDefaultConstructor) return super.fullyQualifiedName; - return '${library.name}.$name'; - } - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(canonicalLibrary != null); - assert(canonicalLibrary == library); - return '${package.baseHref}${enclosingElement.library.dirName}/${enclosingElement.name}/$name.html'; - } - - @override - bool get isConst => _constructor.isConst; - - bool get isDefaultConstructor => name == enclosingElement.name; - - bool get isFactory => _constructor.isFactory; - - @override - String get kind => 'constructor'; - - @override - DefinedElementType get modelType => super.modelType; - - String _name; - - @override - String get name { - if (_name == null) { - String constructorName = element.name; - if (constructorName.isEmpty) { - _name = enclosingElement.name; - } else { - _name = '${enclosingElement.name}.$constructorName'; - } - } - return _name; - } - - String _nameWithGenerics; - - @override - String get nameWithGenerics { - if (_nameWithGenerics == null) { - String constructorName = element.name; - if (constructorName.isEmpty) { - _nameWithGenerics = '${enclosingElement.name}${genericParameters}'; - } else { - _nameWithGenerics = - '${enclosingElement.name}${genericParameters}.$constructorName'; - } - } - return _nameWithGenerics; - } - - String get shortName { - if (name.contains('.')) { - return name.substring(_constructor.enclosingElement.name.length + 1); - } else { - return name; - } - } - - ConstructorElement get _constructor => (element as ConstructorElement); -} - -/// Bridges the gap between model elements and packages, -/// both of which have documentation. -abstract class Documentable extends Nameable { - String get documentation; - - String get documentationAsHtml; - - bool get hasDocumentation; - - bool get hasExtendedDocumentation; - - String get oneLineDoc; - - PackageGraph get packageGraph; - - bool get isDocumented; - - DartdocOptionContext get config; -} - -/// Mixin implementing dartdoc categorization for ModelElements. -abstract class Categorization implements ModelElement { - @override - String _buildDocumentationAddition(String rawDocs) => - _stripAndSetDartdocCategories(rawDocs ??= ''); - - /// Parse {@category ...} and related information in API comments, stripping - /// out that information from the given comments and returning the stripped - /// version. - String _stripAndSetDartdocCategories(String rawDocs) { - Set _categorySet = Set(); - Set _subCategorySet = Set(); - _hasCategorization = false; - - rawDocs = rawDocs.replaceAllMapped(categoryRegexp, (match) { - _hasCategorization = true; - switch (match[1]) { - case 'category': - case 'api': - _categorySet.add(match[2].trim()); - break; - case 'subCategory': - _subCategorySet.add(match[2].trim()); - break; - case 'image': - _image = match[2].trim(); - break; - case 'samples': - _samples = match[2].trim(); - break; - } - return ''; - }); - - if (_categorySet.isEmpty) { - // All objects are in the default category if not specified. - _categorySet.add(null); - } - if (_subCategorySet.isEmpty) { - // All objects are in the default subcategory if not specified. - _subCategorySet.add(null); - } - _categoryNames = _categorySet.toList()..sort(); - _subCategoryNames = _subCategorySet.toList()..sort(); - _image ??= ''; - _samples ??= ''; - return rawDocs; - } - - bool get hasSubCategoryNames => - subCategoryNames.length > 1 || subCategoryNames.first != null; - List _subCategoryNames; - - /// Either a set of strings containing all declared subcategories for this symbol, - /// or a set containing Null if none were declared. - List get subCategoryNames { - // TODO(jcollins-g): avoid side-effect dependency - if (_subCategoryNames == null) documentationLocal; - return _subCategoryNames; - } - - @override - bool get hasCategoryNames => - categoryNames.length > 1 || categoryNames.first != null; - List _categoryNames; - - /// Either a set of strings containing all declared categories for this symbol, - /// or a set containing Null if none were declared. - List get categoryNames { - // TODO(jcollins-g): avoid side-effect dependency - if (_categoryNames == null) documentationLocal; - return _categoryNames; - } - - bool get hasImage => image.isNotEmpty; - String _image; - - /// Either a URI to a defined image, or the empty string if none - /// was declared. - String get image { - // TODO(jcollins-g): avoid side-effect dependency - if (_image == null) documentationLocal; - return _image; - } - - bool get hasSamples => samples.isNotEmpty; - String _samples; - - /// Either a URI to documentation with samples, or the empty string if none - /// was declared. - String get samples { - // TODO(jcollins-g): avoid side-effect dependency - if (_samples == null) documentationLocal; - return _samples; - } - - bool _hasCategorization; - - Iterable _categories; - - Iterable get categories { - if (_categories == null) { - _categories = categoryNames - .map((n) => package.nameToCategory[n]) - .where((c) => c != null) - .toList() - ..sort(); - } - return _categories; - } - - Iterable get displayedCategories { - if (config.showUndocumentedCategories) return categories; - return categories.where((c) => c.isDocumented); - } - - /// True if categories, subcategories, a documentation icon, or samples were - /// declared. - bool get hasCategorization { - if (_hasCategorization == null) documentationLocal; - return _hasCategorization; - } -} - -/// A stripped down [CommentReference] containing only that information needed -/// for Dartdoc. Drops link to the [CommentReference] after construction. -class ModelCommentReference { - final String name; - final Element staticElement; - - ModelCommentReference(CommentReference ref) - : name = ref.identifier.name, - staticElement = ref.identifier.staticElement; -} - -/// Stripped down information derived from [AstNode] containing only information -/// needed for Dartdoc. Drops link to the [AstNode] after construction. -class ModelNode { - final List commentRefs; - final Element element; - - final int _sourceOffset; - final int _sourceEnd; - - ModelNode(AstNode sourceNode, this.element) - : _sourceOffset = sourceNode?.offset, - _sourceEnd = sourceNode?.end, - commentRefs = _commentRefsFor(sourceNode); - - static List _commentRefsFor(AstNode node) { - if (node is AnnotatedNode && - node?.documentationComment?.references != null) { - return node.documentationComment.references - .map((c) => ModelCommentReference(c)) - .toList(growable: false); - } - return null; - } - - String get sourceCode { - String contents = utils.getFileContentsFor(element); - if (_sourceOffset != null) { - // Find the start of the line, so that we can line up all the indents. - int i = _sourceOffset; - while (i > 0) { - i -= 1; - if (contents[i] == '\n' || contents[i] == '\r') { - i += 1; - break; - } - } - - // Trim the common indent from the source snippet. - var start = _sourceOffset - (_sourceOffset - i); - String source = contents.substring(start, _sourceEnd); - - source = const HtmlEscape().convert(source); - source = utils.stripIndentFromSource(source); - source = utils.stripDartdocCommentsFromSource(source); - - return source.trim(); - } else { - return ''; - } - } -} - -/// Classes extending this class have canonicalization support in Dartdoc. -abstract class Canonicalization implements Locatable, Documentable { - bool get isCanonical; - - Library get canonicalLibrary; - - List _commentRefs; - - List get commentRefs => _commentRefs; - - /// Pieces of the location split by [locationSplitter] (removing package: and - /// slashes). - Set get locationPieces; - - List scoreCanonicalCandidates(List libraries) { - return libraries.map((l) => scoreElementWithLibrary(l)).toList()..sort(); - } - - ScoredCandidate scoreElementWithLibrary(Library lib) { - ScoredCandidate scoredCandidate = ScoredCandidate(this, lib); - Iterable resplit(Set items) sync* { - for (String item in items) { - for (String subItem in item.split('_')) { - yield subItem; - } - } - } - - // Large boost for @canonicalFor, essentially overriding all other concerns. - if (lib.canonicalFor.contains(fullyQualifiedName)) { - scoredCandidate.alterScore(5.0, 'marked @canonicalFor'); - } - // Penalty for deprecated libraries. - if (lib.isDeprecated) scoredCandidate.alterScore(-1.0, 'is deprecated'); - // Give a big boost if the library has the package name embedded in it. - if (lib.package.namePieces.intersection(lib.namePieces).isEmpty) { - scoredCandidate.alterScore(1.0, 'embeds package name'); - } - // Give a tiny boost for libraries with long names, assuming they're - // more specific (and therefore more likely to be the owner of this symbol). - scoredCandidate.alterScore(.01 * lib.namePieces.length, 'name is long'); - // If we don't know the location of this element, return our best guess. - // TODO(jcollins-g): is that even possible? - assert(locationPieces.isNotEmpty); - if (locationPieces.isEmpty) return scoredCandidate; - // The more pieces we have of the location in our library name, the more we should boost our score. - scoredCandidate.alterScore( - lib.namePieces.intersection(locationPieces).length.toDouble() / - locationPieces.length.toDouble(), - 'element location shares parts with name'); - // If pieces of location at least start with elements of our library name, boost the score a little bit. - double scoreBoost = 0.0; - for (String piece in resplit(locationPieces)) { - for (String namePiece in lib.namePieces) { - if (piece.startsWith(namePiece)) { - scoreBoost += 0.001; - } - } - } - scoredCandidate.alterScore( - scoreBoost, 'element location parts start with parts of name'); - return scoredCandidate; - } -} - -class Dynamic extends ModelElement { - Dynamic(Element element, PackageGraph packageGraph) - : super(element, null, packageGraph, null); - - /// [dynamic] is not a real object, and so we can't document it, so there - /// can be nothing canonical for it. - @override - ModelElement get canonicalModelElement => null; - - @override - ModelElement get enclosingElement => throw UnsupportedError(''); - - /// And similiarly, even if someone references it directly it can have - /// no hyperlink. - @override - String get href => null; - - @override - String get kind => 'dynamic'; - - @override - String get linkedName => 'dynamic'; -} - -/// An element that is enclosed by some other element. -/// -/// Libraries are not enclosed. -abstract class EnclosedElement { - ModelElement get enclosingElement; -} - -// TODO(jcollins-g): Consider Enum as subclass of Container? -class Enum extends Class { - Enum(ClassElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph); - - List _instanceProperties; - - @override - List get instanceProperties { - if (_instanceProperties == null) { - _instanceProperties = super - .instanceProperties - .map((Field p) => ModelElement.from( - p.element, p.library, p.packageGraph, - getter: p.getter, setter: p.setter) as EnumField) - .toList(growable: false); - } - return _instanceProperties; - } - - @override - String get kind => 'enum'; -} - -/// Enum's fields are virtual, so we do a little work to create -/// usable values for the docs. -class EnumField extends Field { - int _index; - - EnumField(FieldElement element, Library library, PackageGraph packageGraph, - Accessor getter, Accessor setter) - : super(element, library, packageGraph, getter, setter); - - EnumField.forConstant(this._index, FieldElement element, Library library, - PackageGraph packageGraph, Accessor getter) - : super(element, library, packageGraph, getter, null); - - @override - String get constantValueBase { - if (name == 'values') { - return 'const List<${_field.enclosingElement.name}>'; - } else { - return 'const ${_field.enclosingElement.name}($_index)'; - } - } - - @override - List get documentationFrom { - if (name == 'values' || name == 'index') return [this]; - return super.documentationFrom; - } - - @override - String get documentation { - if (name == 'values') { - return 'A constant List of the values in this enum, in order of their declaration.'; - } else if (name == 'index') { - return 'The integer index of this enum.'; - } else { - return super.documentation; - } - } - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(!(canonicalLibrary == null || canonicalEnclosingContainer == null)); - assert(canonicalLibrary == library); - assert(canonicalEnclosingContainer == enclosingElement); - return '${package.baseHref}${enclosingElement.library.dirName}/${(enclosingElement as Class).fileName}'; - } - - @override - String get linkedName => name; - - @override - bool get isCanonical { - if (name == 'index') return false; - // If this is something inherited from Object, e.g. hashCode, let the - // normal rules apply. - if (_index == null) { - return super.isCanonical; - } - // TODO(jcollins-g): We don't actually document this as a separate entity; - // do that or change this to false and deal with the - // consequences. - return true; - } - - @override - String get oneLineDoc => documentationAsHtml; - - @override - Inheritable get overriddenElement => null; -} - -class Field extends ModelElement - with GetterSetterCombo, ContainerMember, Inheritable - implements EnclosedElement { - bool _isInherited = false; - Container _enclosingClass; - @override - final ContainerAccessor getter; - @override - final ContainerAccessor setter; - - Field(FieldElement element, Library library, PackageGraph packageGraph, - this.getter, this.setter) - : super(element, library, packageGraph, null) { - assert(getter != null || setter != null); - if (getter != null) getter._enclosingCombo = this; - if (setter != null) setter._enclosingCombo = this; - _setModelType(); - } - - factory Field.inherited( - FieldElement element, - Class enclosingClass, - Library library, - PackageGraph packageGraph, - Accessor getter, - Accessor setter) { - Field newField = Field(element, library, packageGraph, getter, setter); - newField._isInherited = true; - newField._enclosingClass = enclosingClass; - // Can't set _isInherited to true if this is the defining element, because - // that would mean it isn't inherited. - assert(newField.enclosingElement != newField.definingEnclosingContainer); - return newField; - } - - @override - String get documentation { - // Verify that hasSetter and hasGetterNoSetter are mutually exclusive, - // to prevent displaying more or less than one summary. - if (isPublic) { - Set assertCheck = Set() - ..addAll([hasPublicSetter, hasPublicGetterNoSetter]); - assert(assertCheck.containsAll([true, false])); - } - documentationFrom; - return super.documentation; - } - - @override - ModelElement get enclosingElement { - if (_enclosingClass == null) { - _enclosingClass = - ModelElement.from(_field.enclosingElement, library, packageGraph); - } - return _enclosingClass; - } - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(canonicalLibrary != null); - assert(canonicalEnclosingContainer == enclosingElement); - assert(canonicalLibrary == library); - return '${package.baseHref}${enclosingElement.library.dirName}/${enclosingElement.name}/$fileName'; - } - - @override - bool get isConst => _field.isConst; - - /// Returns true if the FieldElement is covariant, or if the first parameter - /// for the setter is covariant. - @override - bool get isCovariant => - setter?.isCovariant == true || (_field as FieldElementImpl).isCovariant; - - @override - bool get isFinal { - /// isFinal returns true for the field even if it has an explicit getter - /// (which means we should not document it as "final"). - if (hasExplicitGetter) return false; - return _field.isFinal; - } - - @override - bool get isInherited => _isInherited; - - @override - String get kind => isConst ? 'constant' : 'property'; - - @override - List get annotations { - List all_annotations = List(); - all_annotations.addAll(super.annotations); - - if (element is PropertyInducingElement) { - var pie = element as PropertyInducingElement; - all_annotations.addAll(annotationsFromMetadata(pie.getter?.metadata)); - all_annotations.addAll(annotationsFromMetadata(pie.setter?.metadata)); - } - return all_annotations.toList(growable: false); - } - - @override - Set get features { - Set allFeatures = super.features..addAll(comboFeatures); - // Combo features can indicate 'inherited' and 'override' if - // either the getter or setter has one of those properties, but that's not - // really specific enough for [Field]s that have public getter/setters. - if (hasPublicGetter && hasPublicSetter) { - if (getter.isInherited && setter.isInherited) { - allFeatures.add('inherited'); - } else { - allFeatures.remove('inherited'); - if (getter.isInherited) allFeatures.add('inherited-getter'); - if (setter.isInherited) allFeatures.add('inherited-setter'); - } - if (getter.isOverride && setter.isOverride) { - allFeatures.add('override'); - } else { - allFeatures.remove('override'); - if (getter.isOverride) allFeatures.add('override-getter'); - if (setter.isOverride) allFeatures.add('override-setter'); - } - } else { - if (isInherited) allFeatures.add('inherited'); - if (isOverride) allFeatures.add('override'); - } - return allFeatures; - } - - @override - String _computeDocumentationComment() { - String docs = getterSetterDocumentationComment; - if (docs.isEmpty) return _field.documentationComment; - return docs; - } - - FieldElement get _field => (element as FieldElement); - - @override - String get fileName => isConst ? '$name-constant.html' : '$name.html'; - - @override - String get sourceCode { - if (_sourceCode == null) { - // We could use a set to figure the dupes out, but that would lose ordering. - String fieldSourceCode = modelNode.sourceCode ?? ''; - String getterSourceCode = getter?.sourceCode ?? ''; - String setterSourceCode = setter?.sourceCode ?? ''; - StringBuffer buffer = StringBuffer(); - if (fieldSourceCode.isNotEmpty) { - buffer.write(fieldSourceCode); - } - if (buffer.isNotEmpty) buffer.write('\n\n'); - if (fieldSourceCode != getterSourceCode) { - if (getterSourceCode != setterSourceCode) { - buffer.write(getterSourceCode); - if (buffer.isNotEmpty) buffer.write('\n\n'); - } - } - if (fieldSourceCode != setterSourceCode) { - buffer.write(setterSourceCode); - } - _sourceCode = buffer.toString(); - } - return _sourceCode; - } - - void _setModelType() { - if (hasGetter) { - _modelType = getter.modelType; - } - } - - @override - Inheritable get overriddenElement => null; -} - -/// Mixin for top-level variables and fields (aka properties) -mixin GetterSetterCombo on ModelElement { - Accessor get getter; - - Iterable get allAccessors sync* { - for (Accessor a in [getter, setter]) { - if (a != null) yield a; - } - } - - Set get comboFeatures { - Set allFeatures = Set(); - if (hasExplicitGetter && hasPublicGetter) { - allFeatures.addAll(getter.features); - } - if (hasExplicitSetter && hasPublicSetter) { - allFeatures.addAll(setter.features); - } - if (readOnly && !isFinal && !isConst) allFeatures.add('read-only'); - if (writeOnly) allFeatures.add('write-only'); - if (readWrite) allFeatures.add('read / write'); - return allFeatures; - } - - @override - ModelElement enclosingElement; - - bool get isInherited; - - Expression get constantInitializer => - (element as ConstVariableElement).constantInitializer; - - String linkifyConstantValue(String original) { - if (constantInitializer is! InstanceCreationExpression) return original; - String constructorName = (constantInitializer as InstanceCreationExpression) - .constructorName - .toString(); - Element staticElement = - (constantInitializer as InstanceCreationExpression).staticElement; - Constructor target = ModelElement.fromElement(staticElement, packageGraph); - Class targetClass = target.enclosingElement; - // TODO(jcollins-g): this logic really should be integrated into Constructor, - // but that's not trivial because of linkedName's usage. - if (targetClass.name == target.name) { - return original.replaceAll(constructorName, "${target.linkedName}"); - } - return original.replaceAll("${targetClass.name}.${target.name}", - "${targetClass.linkedName}.${target.linkedName}"); - } - - String _buildConstantValueBase() { - String result = constantInitializer?.toString() ?? ''; - return const HtmlEscape(HtmlEscapeMode.unknown).convert(result); - } - - @override - CharacterLocation get characterLocation { - // Handle all synthetic possibilities. Ordinarily, warnings for - // explicit setters/getters will be handled by those objects, but - // if a warning comes up for an enclosing synthetic field we have to - // put it somewhere. So pick an accessor. - if (element.isSynthetic) { - if (hasExplicitGetter) return getter.characterLocation; - if (hasExplicitSetter) return setter.characterLocation; - assert(false, 'Field and accessors can not all be synthetic'); - } - return super.characterLocation; - } - - String get constantValue => linkifyConstantValue(constantValueBase); - - String get constantValueTruncated => - linkifyConstantValue(truncateString(constantValueBase, 200)); - String _constantValueBase; - - String get constantValueBase => - _constantValueBase ??= _buildConstantValueBase(); - - bool get hasPublicGetter => hasGetter && getter.isPublic; - - bool get hasPublicSetter => hasSetter && setter.isPublic; - - @override - bool get isPublic => hasPublicGetter || hasPublicSetter; - - @override - List get documentationFrom { - if (_documentationFrom == null) { - _documentationFrom = []; - if (hasPublicGetter) { - _documentationFrom.addAll(getter.documentationFrom); - } else if (hasPublicSetter) { - _documentationFrom.addAll(setter.documentationFrom); - } - if (_documentationFrom.isEmpty || - _documentationFrom.every((e) => e.documentationComment == '')) { - _documentationFrom = computeDocumentationFrom; - } - } - return _documentationFrom; - } - - bool get hasAccessorsWithDocs => (hasPublicGetter && - !getter.isSynthetic && - getter.documentation.isNotEmpty || - hasPublicSetter && - !setter.isSynthetic && - setter.documentation.isNotEmpty); - - bool get getterSetterBothAvailable => (hasPublicGetter && - getter.documentation.isNotEmpty && - hasPublicSetter && - setter.documentation.isNotEmpty); - - @override - String get oneLineDoc { - if (_oneLineDoc == null) { - if (!hasAccessorsWithDocs) { - _oneLineDoc = computeOneLineDoc(); - } else { - StringBuffer buffer = StringBuffer(); - if (hasPublicGetter && getter.oneLineDoc.isNotEmpty) { - buffer.write('${getter.oneLineDoc}'); - } - if (hasPublicSetter && setter.oneLineDoc.isNotEmpty) { - buffer.write('${getterSetterBothAvailable ? "" : setter.oneLineDoc}'); - } - _oneLineDoc = buffer.toString(); - } - } - return _oneLineDoc; - } - - String get getterSetterDocumentationComment { - var buffer = StringBuffer(); - - if (hasPublicGetter && !getter.isSynthetic) { - assert(getter.documentationFrom.length == 1); - // We have to check against dropTextFrom here since documentationFrom - // doesn't yield the real elements for GetterSetterCombos. - if (!config.dropTextFrom - .contains(getter.documentationFrom.first.element.library.name)) { - String docs = getter.documentationFrom.first.documentationComment; - if (docs != null) buffer.write(docs); - } - } - - if (hasPublicSetter && !setter.isSynthetic) { - assert(setter.documentationFrom.length == 1); - if (!config.dropTextFrom - .contains(setter.documentationFrom.first.element.library.name)) { - String docs = setter.documentationFrom.first.documentationComment; - if (docs != null) { - if (buffer.isNotEmpty) buffer.write('\n\n'); - buffer.write(docs); - } - } - } - return buffer.toString(); - } - - String get linkedReturnType { - if (hasGetter) { - return getter.linkedReturnType; - } else { - return setter.linkedParamsNoMetadataOrNames; - } - } - - @override - bool get canHaveParameters => hasSetter; - - @override - List get parameters => setter.parameters; - - @override - String get linkedParamsNoMetadata { - if (hasSetter) return setter.linkedParamsNoMetadata; - return null; - } - - bool get hasExplicitGetter => hasPublicGetter && !getter.isSynthetic; - - bool get hasExplicitSetter => hasPublicSetter && !setter.isSynthetic; - - bool get hasGetter => getter != null; - - bool get hasNoGetterSetter => !hasGetterOrSetter; - - bool get hasGetterOrSetter => hasExplicitGetter || hasExplicitSetter; - - bool get hasSetter => setter != null; - - bool get hasPublicGetterNoSetter => (hasPublicGetter && !hasPublicSetter); - - String get arrow { - // → - if (readOnly) return r'→'; - // ← - if (writeOnly) return r'←'; - // ↔ - if (readWrite) return r'↔'; - throw UnsupportedError( - 'GetterSetterCombo must be one of readOnly, writeOnly, or readWrite'); - } - - bool get readOnly => hasPublicGetter && !hasPublicSetter; - - bool get readWrite => hasPublicGetter && hasPublicSetter; - - bool get writeOnly => hasPublicSetter && !hasPublicGetter; - - Accessor get setter; -} - -/// Find all hashable children of a given element that are defined in the -/// [LibraryElement] given at initialization. -class _HashableChildLibraryElementVisitor - extends GeneralizingElementVisitor { - final void Function(Element) libraryProcessor; - - _HashableChildLibraryElementVisitor(this.libraryProcessor); - - @override - void visitElement(Element element) { - libraryProcessor(element); - super.visitElement(element); - return null; - } - - @override - void visitExportElement(ExportElement element) { - // [ExportElement]s are not always hashable; skip them. - return null; - } - - @override - void visitImportElement(ImportElement element) { - // [ImportElement]s are not always hashable; skip them. - return null; - } - - @override - void visitParameterElement(ParameterElement element) { - // [ParameterElement]s without names do not provide sufficiently distinct - // hashes / comparison, so just skip them all. (dart-lang/sdk#30146) - return null; - } -} - -class Library extends ModelElement with Categorization, TopLevelContainer { - List _variables; - List _exportedAndLocalElements; - String _name; - - factory Library(LibraryElement element, PackageGraph packageGraph) { - return packageGraph.findButDoNotCreateLibraryFor(element); - } - - Library._(ResolvedLibraryResult libraryResult, PackageGraph packageGraph, - this._package) - : super(libraryResult.element, null, packageGraph, null) { - if (element == null) throw ArgumentError.notNull('element'); - - // Initialize [packageGraph]'s cache of ModelNodes for relevant - // elements in this library. - Map _compilationUnitMap = Map(); - _compilationUnitMap.addEntries(libraryResult.units - .map((ResolvedUnitResult u) => MapEntry(u.path, u.unit))); - _HashableChildLibraryElementVisitor((Element e) => - packageGraph._populateModelNodeFor(e, _compilationUnitMap)) - .visitElement(element); - - // Initialize the list of elements defined in this library and - // exported via its export directives. - Set exportedAndLocalElements = - _libraryElement.exportNamespace.definedNames.values.toSet(); - // TODO(jcollins-g): Consider switch to [_libraryElement.topLevelElements]. - exportedAndLocalElements - .addAll(getDefinedElements(_libraryElement.definingCompilationUnit)); - for (CompilationUnitElement cu in _libraryElement.parts) { - exportedAndLocalElements.addAll(getDefinedElements(cu)); - } - _exportedAndLocalElements = exportedAndLocalElements.toList(); - - _package._allLibraries.add(this); - } - - static Iterable getDefinedElements( - CompilationUnitElement compilationUnit) { - return quiver.concat([ - compilationUnit.accessors, - compilationUnit.enums, - compilationUnit.extensions, - compilationUnit.functions, - compilationUnit.functionTypeAliases, - compilationUnit.mixins, - compilationUnit.topLevelVariables, - compilationUnit.types, - ]); - } - - List _allOriginalModelElementNames; - - bool get isInSdk => _libraryElement.isInSdk; - - final Package _package; - - @override - Package get package { - // Everything must be in a package. TODO(jcollins-g): Support other things - // that look like packages. - assert(_package != null); - return _package; - } - - /// [allModelElements] resolved to their original names. - /// - /// A collection of [ModelElement.fullyQualifiedName]s for [ModelElement]s - /// documented with this library, but these ModelElements and names correspond - /// to the defining library where each originally came from with respect - /// to inheritance and reexporting. Most useful for error reporting. - Iterable get allOriginalModelElementNames { - if (_allOriginalModelElementNames == null) { - _allOriginalModelElementNames = allModelElements.map((e) { - Accessor getter; - Accessor setter; - if (e is GetterSetterCombo) { - if (e.hasGetter) { - getter = ModelElement.fromElement(e.getter.element, packageGraph); - } - if (e.hasSetter) { - setter = ModelElement.fromElement(e.setter.element, packageGraph); - } - } - return ModelElement.from( - e.element, - packageGraph.findButDoNotCreateLibraryFor(e.element), - packageGraph, - getter: getter, - setter: setter) - .fullyQualifiedName; - }).toList(); - } - return _allOriginalModelElementNames; - } - - @override - CharacterLocation get characterLocation { - if (element.nameOffset == -1) { - assert(isAnonymous, - 'Only anonymous libraries are allowed to have no declared location'); - return CharacterLocation(1, 1); - } - return super.characterLocation; - } - - @override - CompilationUnitElement get compilationUnitElement => - (element as LibraryElement).definingCompilationUnit; - - @override - Iterable get classes => allClasses.where((c) => !c.isErrorOrException); - - @override - Iterable get extensions { - if (_extensions == null) { - _extensions = _exportedAndLocalElements - .whereType() - .map((e) => ModelElement.from(e, this, packageGraph) as Extension) - .toList(growable: false) - ..sort(byName); - } - return _extensions; - } - - SdkLibrary get sdkLib { - if (packageGraph.sdkLibrarySources.containsKey(element.librarySource)) { - return packageGraph.sdkLibrarySources[element.librarySource]; - } - return null; - } - - @override - bool get isPublic { - if (!super.isPublic) return false; - if (sdkLib != null && (sdkLib.isInternal || !sdkLib.isDocumented)) { - return false; - } - if (config.isLibraryExcluded(name) || - config.isLibraryExcluded(element.librarySource.uri.toString())) { - return false; - } - return true; - } - - @override - Iterable get constants { - if (_constants == null) { - // _getVariables() is already sorted. - _constants = - _getVariables().where((v) => v.isConst).toList(growable: false); - } - return _constants; - } - - Set _packageImportedExportedLibraries; - - /// Returns all libraries either imported by or exported by any public library - /// this library's package. (Not [PackageGraph], but sharing a package name). - /// - /// Note: will still contain non-public libraries because those can be - /// imported or exported. - // TODO(jcollins-g): move this to [Package] once it really knows about - // more than one package. - Set get packageImportedExportedLibraries { - if (_packageImportedExportedLibraries == null) { - _packageImportedExportedLibraries = Set(); - packageGraph.publicLibraries - .where((l) => l.packageName == packageName) - .forEach((l) { - _packageImportedExportedLibraries.addAll(l.importedExportedLibraries); - }); - } - return _packageImportedExportedLibraries; - } - - Set _importedExportedLibraries; - - /// Returns all libraries either imported by or exported by this library, - /// recursively. - Set get importedExportedLibraries { - if (_importedExportedLibraries == null) { - _importedExportedLibraries = Set(); - Set importedExportedLibraryElements = Set(); - importedExportedLibraryElements - .addAll((element as LibraryElement).importedLibraries); - importedExportedLibraryElements - .addAll((element as LibraryElement).exportedLibraries); - for (LibraryElement l in importedExportedLibraryElements) { - Library lib = ModelElement.from(l, library, packageGraph); - _importedExportedLibraries.add(lib); - _importedExportedLibraries.addAll(lib.importedExportedLibraries); - } - } - return _importedExportedLibraries; - } - - Map> _prefixToLibrary; - - /// Map of import prefixes ('import "foo" as prefix;') to [Library]. - Map> get prefixToLibrary { - if (_prefixToLibrary == null) { - _prefixToLibrary = {}; - // It is possible to have overlapping prefixes. - for (ImportElement i in (element as LibraryElement).imports) { - // Ignore invalid imports. - if (i.prefix?.name != null && i.importedLibrary != null) { - _prefixToLibrary.putIfAbsent(i.prefix?.name, () => Set()); - _prefixToLibrary[i.prefix?.name] - .add(ModelElement.from(i.importedLibrary, library, packageGraph)); - } - } - } - return _prefixToLibrary; - } - - String _dirName; - - String get dirName { - if (_dirName == null) { - _dirName = name; - if (isAnonymous) { - _dirName = nameFromPath; - } - _dirName = _dirName.replaceAll(':', '-').replaceAll('/', '_'); - } - return _dirName; - } - - Set _canonicalFor; - - Set get canonicalFor { - if (_canonicalFor == null) { - // TODO(jcollins-g): restructure to avoid using side effects. - _buildDocumentationAddition(documentationComment); - } - return _canonicalFor; - } - - /// Hide canonicalFor from doc while leaving a note to ourselves to - /// help with ambiguous canonicalization determination. - /// - /// Example: - /// {@canonicalFor libname.ClassName} - @override - String _buildDocumentationAddition(String rawDocs) { - rawDocs = super._buildDocumentationAddition(rawDocs); - Set newCanonicalFor = Set(); - Set notFoundInAllModelElements = Set(); - final canonicalRegExp = RegExp(r'{@canonicalFor\s([^}]+)}'); - rawDocs = rawDocs.replaceAllMapped(canonicalRegExp, (Match match) { - newCanonicalFor.add(match.group(1)); - notFoundInAllModelElements.add(match.group(1)); - return ''; - }); - if (notFoundInAllModelElements.isNotEmpty) { - notFoundInAllModelElements.removeAll(allOriginalModelElementNames); - } - for (String notFound in notFoundInAllModelElements) { - warn(PackageWarning.ignoredCanonicalFor, message: notFound); - } - // TODO(jcollins-g): warn if a macro/tool _does_ generate an unexpected - // canonicalFor? - if (_canonicalFor == null) { - _canonicalFor = newCanonicalFor; - } - return rawDocs; - } - - /// Libraries are not enclosed by anything. - @override - ModelElement get enclosingElement => null; - - @override - List get enums { - if (_enums == null) { - _enums = _exportedAndLocalElements - .whereType() - .where((element) => element.isEnum) - .map((e) => ModelElement.from(e, this, packageGraph) as Enum) - .toList(growable: false) - ..sort(byName); - } - return _enums; - } - - @override - List get mixins { - if (_mixins == null) { - /// Can not be [MixinElementImpl] because [ClassHandle]s are sometimes - /// returned from _exportedElements. - _mixins = _exportedAndLocalElements - .whereType() - .where((ClassElement c) => c.isMixin) - .map((e) => ModelElement.from(e, this, packageGraph) as Mixin) - .toList(growable: false) - ..sort(byName); - } - return _mixins; - } - - @override - List get exceptions { - if (_exceptions == null) { - _exceptions = - allClasses.where((c) => c.isErrorOrException).toList(growable: false); - } - return _exceptions; - } - - @override - String get fileName => '$dirName-library.html'; - - @override - List get functions { - if (_functions == null) { - _functions = - _exportedAndLocalElements.whereType().map((e) { - return ModelElement.from(e, this, packageGraph) as ModelFunction; - }).toList(growable: false) - ..sort(byName); - } - return _functions; - } - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - return '${package.baseHref}${library.dirName}/$fileName'; - } - - InheritanceManager3 _inheritanceManager; - - InheritanceManager3 get inheritanceManager { - if (_inheritanceManager == null) { - var typeSystem = element.context.typeSystem; - _inheritanceManager = InheritanceManager3(typeSystem); - } - return _inheritanceManager; - } - - bool get isAnonymous => element.name == null || element.name.isEmpty; - - @override - String get kind => 'library'; - - @override - Library get library => this; - - @override - String get name { - if (_name == null) { - _name = getLibraryName(element); - } - return _name; - } - - String _nameFromPath; - - /// Generate a name for this library based on its location. - /// - /// nameFromPath provides filename collision-proofing for anonymous libraries - /// by incorporating more from the location of the anonymous library into - /// the name calculation. Simple cases (such as an anonymous library in - /// 'lib') are the same, but this will include slashes and possibly colons - /// for anonymous libraries in subdirectories or other packages. - String get nameFromPath { - if (_nameFromPath == null) { - _nameFromPath = getNameFromPath(element, packageGraph.driver, package); - } - return _nameFromPath; - } - - /// The real package, as opposed to the package we are documenting it with, - /// [PackageGraph.name] - String get packageName => packageMeta?.name ?? ''; - - /// The real packageMeta, as opposed to the package we are documenting with. - PackageMeta _packageMeta; - - PackageMeta get packageMeta { - if (_packageMeta == null) { - _packageMeta = PackageMeta.fromElement(element, config); - } - return _packageMeta; - } - - /// All variables ("properties") except constants. - @override - Iterable get properties { - if (_properties == null) { - _properties = - _getVariables().where((v) => !v.isConst).toList(growable: false); - } - return _properties; - } - - @override - List get typedefs { - if (_typedefs == null) { - _typedefs = _exportedAndLocalElements - .whereType() - .map((e) => ModelElement.from(e, this, packageGraph) as Typedef) - .toList(growable: false) - ..sort(byName); - } - return _typedefs; - } - - List get allClasses { - if (_classes == null) { - _classes = _exportedAndLocalElements - .whereType() - .where((e) => !e.isMixin && !e.isEnum) - .map((e) => ModelElement.from(e, this, packageGraph) as Class) - .toList(growable: false) - ..sort(byName); - } - return _classes; - } - - LibraryElement get _libraryElement => (element as LibraryElement); - - Class getClassByName(String name) { - return allClasses.firstWhere((it) => it.name == name, orElse: () => null); - } - - List _getVariables() { - if (_variables == null) { - Set elements = _exportedAndLocalElements - .whereType() - .toSet(); - elements.addAll(_exportedAndLocalElements - .whereType() - .map((a) => a.variable)); - _variables = []; - for (TopLevelVariableElement element in elements) { - Accessor getter; - if (element.getter != null) { - getter = ModelElement.from(element.getter, this, packageGraph); - } - Accessor setter; - if (element.setter != null) { - setter = ModelElement.from(element.setter, this, packageGraph); - } - ModelElement me = ModelElement.from(element, this, packageGraph, - getter: getter, setter: setter); - _variables.add(me); - } - _variables.sort(byName); - } - return _variables; - } - - /// Reverses URIs if needed to get a package URI. - /// Not the same as [PackageGraph.name] because there we always strip all - /// path components; this function only strips the package prefix if the - /// library is part of the default package or if it is being documented - /// remotely. - static String getNameFromPath( - LibraryElement element, AnalysisDriver driver, Package package) { - String name; - if (element.source.uri.toString().startsWith('dart:')) { - name = element.source.uri.toString(); - } else { - name = driver.sourceFactory.restoreUri(element.source).toString(); - } - PackageMeta hidePackage; - if (package.documentedWhere == DocumentLocation.remote) { - hidePackage = package.packageMeta; - } else { - hidePackage = package.packageGraph.packageMeta; - } - // restoreUri must not result in another file URI. - assert(!name.startsWith('file:')); - - String defaultPackagePrefix = 'package:$hidePackage/'; - if (name.startsWith(defaultPackagePrefix)) { - name = name.substring(defaultPackagePrefix.length, name.length); - } - if (name.endsWith('.dart')) { - name = name.substring(0, name.length - '.dart'.length); - } - assert(!name.startsWith('file:')); - return name; - } - - static String getLibraryName(LibraryElement element) { - var source = element.source; - - if (source.uri.isScheme('dart')) { - return '${source.uri}'; - } - - var name = element.name; - if (name != null && name.isNotEmpty) { - return name; - } - - name = path.basename(source.fullName); - if (name.endsWith('.dart')) { - name = name.substring(0, name.length - '.dart'.length); - } - return name; - } - - Map> _modelElementsNameMap; - - /// Map of [fullyQualifiedNameWithoutLibrary] to all matching [ModelElement]s - /// in this library. Used for code reference lookups. - Map> get modelElementsNameMap { - if (_modelElementsNameMap == null) { - _modelElementsNameMap = Map>(); - allModelElements.forEach((ModelElement modelElement) { - _modelElementsNameMap.putIfAbsent( - modelElement.fullyQualifiedNameWithoutLibrary, () => Set()); - _modelElementsNameMap[modelElement.fullyQualifiedNameWithoutLibrary] - .add(modelElement); - }); - } - return _modelElementsNameMap; - } - - Map> _modelElementsMap; - - Map> get modelElementsMap { - if (_modelElementsMap == null) { - Iterable results = quiver.concat([ - library.constants, - library.functions, - library.properties, - library.typedefs, - library.extensions.expand((e) { - return quiver.concat([ - [e], - e.allModelElements - ]); - }), - library.allClasses.expand((c) { - return quiver.concat([ - [c], - c.allModelElements - ]); - }), - library.enums.expand((e) { - return quiver.concat([ - [e], - e.allModelElements - ]); - }), - library.mixins.expand((m) { - return quiver.concat([ - [m], - m.allModelElements - ]); - }), - ]); - _modelElementsMap = Map>(); - results.forEach((modelElement) { - _modelElementsMap.putIfAbsent(modelElement.element, () => Set()); - _modelElementsMap[modelElement.element].add(modelElement); - }); - _modelElementsMap.putIfAbsent(element, () => Set()); - _modelElementsMap[element].add(this); - } - return _modelElementsMap; - } - - List _allModelElements; - - Iterable get allModelElements { - if (_allModelElements == null) { - _allModelElements = []; - for (Set modelElements in modelElementsMap.values) { - _allModelElements.addAll(modelElements); - } - } - return _allModelElements; - } - - List _allCanonicalModelElements; - - Iterable get allCanonicalModelElements { - return (_allCanonicalModelElements ??= - allModelElements.where((e) => e.isCanonical).toList()); - } -} - -class Method extends ModelElement - with ContainerMember, Inheritable, TypeParameters - implements EnclosedElement { - bool _isInherited = false; - Container _enclosingContainer; - @override - List typeParameters = []; - - Method(MethodElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph, null) { - _calcTypeParameters(); - } - - Method.inherited(MethodElement element, this._enclosingContainer, - Library library, PackageGraph packageGraph, - {Member originalMember}) - : super(element, library, packageGraph, originalMember) { - _isInherited = true; - _calcTypeParameters(); - } - - void _calcTypeParameters() { - typeParameters = _method.typeParameters.map((f) { - return ModelElement.from(f, library, packageGraph) as TypeParameter; - }).toList(); - } - - @override - CharacterLocation get characterLocation { - if (enclosingElement is Enum && name == 'toString') { - // The toString() method on Enums is special, treated as not having - // a definition location by the analyzer yet not being inherited, either. - // Just directly override our location with the Enum definition -- - // this is OK because Enums can not inherit from each other nor - // have their definitions split between files. - return enclosingElement.characterLocation; - } - return super.characterLocation; - } - - @override - ModelElement get enclosingElement { - if (_enclosingContainer == null) { - _enclosingContainer = - ModelElement.from(_method.enclosingElement, library, packageGraph); - } - return _enclosingContainer; - } - - String get fullkind { - if (_method.isAbstract) return 'abstract $kind'; - return kind; - } - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(!(canonicalLibrary == null || canonicalEnclosingContainer == null)); - assert(canonicalLibrary == library); - assert(canonicalEnclosingContainer == enclosingElement); - return '${package.baseHref}${enclosingElement.library.dirName}/${enclosingElement.name}/${fileName}'; - } - - @override - bool get isInherited => _isInherited; - - bool get isOperator => false; - - @override - Set get features { - Set allFeatures = super.features; - if (isInherited) allFeatures.add('inherited'); - return allFeatures; - } - - @override - bool get isStatic => _method.isStatic; - - @override - String get kind => 'method'; - - String get linkedReturnType => modelType.createLinkedReturnTypeName(); - - @override - DefinedElementType get modelType => super.modelType; - - @override - Method get overriddenElement { - if (_enclosingContainer is Extension) { - return null; - } - ClassElement parent = element.enclosingElement; - for (InterfaceType t in parent.allSupertypes) { - Element e = t.getMethod(element.name); - if (e != null) { - assert(e.enclosingElement is ClassElement); - return ModelElement.fromElement(e, packageGraph); - } - } - return null; - } - - MethodElement get _method => (element as MethodElement); - - /// Methods can not be covariant; always returns false. - @override - bool get isCovariant => false; -} - -/// This class represents the score for a particular element; how likely -/// it is that this is the canonical element. -class ScoredCandidate implements Comparable { - final List reasons = []; - - /// The canonicalization element being scored. - final Canonicalization element; - final Library library; - - /// The score accumulated so far. Higher means it is more likely that this - /// is the intended canonical Library. - double score = 0.0; - - ScoredCandidate(this.element, this.library); - - void alterScore(double scoreDelta, String reason) { - score += scoreDelta; - if (scoreDelta != 0) { - reasons.add( - "${reason} (${scoreDelta >= 0 ? '+' : ''}${scoreDelta.toStringAsPrecision(4)})"); - } - } - - @override - int compareTo(ScoredCandidate other) { - //assert(element == other.element); - return score.compareTo(other.score); - } - - @override - String toString() => - "${library.name}: ${score.toStringAsPrecision(4)} - ${reasons.join(', ')}"; -} - -// 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) { - Iterable inheritables = e.inheritedElements - .map((ee) => ModelElement.fromElement(ee, packageGraph) as Inheritable); - Inheritable foundInheritable; - int lowIndex = enclosingClass.inheritanceChain.length; - for (var inheritable in inheritables) { - int index = - enclosingClass.inheritanceChain.indexOf(inheritable.enclosingElement); - if (index < lowIndex) { - foundInheritable = inheritable; - lowIndex = index; - } - } - return ModelElement.from(foundInheritable.element, library, packageGraph, - enclosingContainer: enclosingClass); -} - -/// Classes implementing this have a public/private distinction. -abstract class Privacy { - bool get isPublic; -} - -/// 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 - with Privacy, Warnable, Locatable, Nameable, SourceCodeMixin, Indexable - implements Comparable, Documentable { - final Element _element; - - // TODO(jcollins-g): This really wants a "member that has a type" class. - final Member _originalMember; - final Library _library; - - ElementType _modelType; - String _rawDocs; - Documentation __documentation; - UnmodifiableListView _parameters; - String _linkedName; - - String _fullyQualifiedName; - String _fullyQualifiedNameWithoutLibrary; - - // TODO(jcollins-g): make _originalMember optional after dart-lang/sdk#15101 - // is fixed. - ModelElement( - this._element, this._library, this._packageGraph, this._originalMember); - - factory ModelElement.fromElement(Element e, PackageGraph p) { - Library lib = p.findButDoNotCreateLibraryFor(e); - Accessor getter; - Accessor setter; - if (e is PropertyInducingElement) { - getter = e.getter != null ? ModelElement.from(e.getter, lib, p) : null; - setter = e.setter != null ? ModelElement.from(e.setter, lib, p) : null; - } - return ModelElement.from(e, lib, p, getter: getter, setter: setter); - } - - // TODO(jcollins-g): this way of using the optional parameter is messy, - // clean that up. - // TODO(jcollins-g): Refactor this into class-specific factories that - // call this one. - // 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. - /// Do not construct any ModelElements unless they are from this constructor. - /// Specify enclosingContainer if and only if this is to be an inherited or - /// extended object. - factory ModelElement.from( - Element e, Library library, PackageGraph packageGraph, - {Container enclosingContainer, Accessor getter, Accessor setter}) { - assert(packageGraph != null && e != null); - assert(library != null || - e is ParameterElement || - e is TypeParameterElement || - e is GenericFunctionTypeElementImpl || - e.kind == ElementKind.DYNAMIC); - - Member originalMember; - // TODO(jcollins-g): Refactor object model to instantiate 'ModelMembers' - // for members? - if (e is Member) { - var basest = PackageGraph.getBasestElement(e); - originalMember = e; - e = basest; - } - Tuple3 key = - Tuple3(e, library, enclosingContainer); - ModelElement newModelElement; - if (e.kind != ElementKind.DYNAMIC && - packageGraph._allConstructedModelElements.containsKey(key)) { - newModelElement = packageGraph._allConstructedModelElements[key]; - assert(newModelElement.element is! MultiplyInheritedExecutableElement); - } else { - if (e.kind == ElementKind.DYNAMIC) { - newModelElement = Dynamic(e, packageGraph); - } - if (e is MultiplyInheritedExecutableElement) { - newModelElement = resolveMultiplyInheritedElement( - e, library, packageGraph, enclosingContainer); - } else { - if (e is LibraryElement) { - newModelElement = Library(e, packageGraph); - } - // Also handles enums - if (e is ClassElement) { - if (e.isMixin) { - newModelElement = Mixin(e, library, packageGraph); - } else if (e.isEnum) { - newModelElement = Enum(e, library, packageGraph); - } else { - newModelElement = Class(e, library, packageGraph); - } - } - if (e is ExtensionElement) { - newModelElement = Extension(e, library, packageGraph); - } - if (e is FunctionElement) { - newModelElement = ModelFunction(e, library, packageGraph); - } else if (e is GenericFunctionTypeElement) { - // TODO(scheglov) "e" cannot be both GenericFunctionTypeElement, - // and FunctionTypeAliasElement or GenericTypeAliasElement. - if (e is FunctionTypeAliasElement) { - assert(e.name != ''); - newModelElement = ModelFunctionTypedef(e, library, packageGraph); - } else { - if (e.enclosingElement is GenericTypeAliasElement) { - assert(e.enclosingElement.name != ''); - newModelElement = ModelFunctionTypedef(e, library, packageGraph); - } else { - // Allowing null here is allowed as a workaround for - // dart-lang/sdk#32005. - assert(e.name == '' || e.name == null); - newModelElement = ModelFunctionAnonymous(e, packageGraph); - } - } - } - if (e is FunctionTypeAliasElement) { - newModelElement = Typedef(e, library, packageGraph); - } - if (e is FieldElement) { - if (enclosingContainer == null) { - if (e.isEnumConstant) { - int index = - e.computeConstantValue().getField(e.name).toIntValue(); - newModelElement = EnumField.forConstant( - index, e, library, packageGraph, getter); - // ignore: unnecessary_cast - } else if (e.enclosingElement is ExtensionElement) { - newModelElement = Field(e, library, packageGraph, getter, setter); - } else if (e.enclosingElement is ClassElement && - (e.enclosingElement as ClassElement).isEnum) { - newModelElement = - EnumField(e, library, packageGraph, getter, setter); - } else { - newModelElement = Field(e, library, packageGraph, getter, setter); - } - } else { - // EnumFields can't be inherited, so this case is simpler. - newModelElement = Field.inherited( - e, enclosingContainer, library, packageGraph, getter, setter); - } - } - if (e is ConstructorElement) { - newModelElement = Constructor(e, library, packageGraph); - } - if (e is MethodElement && e.isOperator) { - if (enclosingContainer == null) { - newModelElement = Operator(e, library, packageGraph); - } else { - newModelElement = Operator.inherited( - e, enclosingContainer, library, packageGraph, - originalMember: originalMember); - } - } - if (e is MethodElement && !e.isOperator) { - if (enclosingContainer == null) { - newModelElement = Method(e, library, packageGraph); - } else { - newModelElement = Method.inherited( - e, enclosingContainer, library, packageGraph, - originalMember: originalMember); - } - } - if (e is TopLevelVariableElement) { - assert(getter != null || setter != null); - newModelElement = - TopLevelVariable(e, library, packageGraph, getter, setter); - } - if (e is PropertyAccessorElement) { - // TODO(jcollins-g): why test for ClassElement in enclosingElement? - if (e.enclosingElement is ClassElement || - e is MultiplyInheritedExecutableElement) { - if (enclosingContainer == null) { - newModelElement = ContainerAccessor(e, library, packageGraph); - } else { - newModelElement = ContainerAccessor.inherited( - e, library, packageGraph, enclosingContainer, - originalMember: originalMember); - } - } else { - newModelElement = Accessor(e, library, packageGraph, null); - } - } - if (e is TypeParameterElement) { - newModelElement = TypeParameter(e, library, packageGraph); - } - if (e is ParameterElement) { - newModelElement = Parameter(e, library, packageGraph, - originalMember: originalMember); - } - } - } - - if (newModelElement == null) throw "Unknown type ${e.runtimeType}"; - if (enclosingContainer != null) assert(newModelElement is Inheritable); - // TODO(jcollins-g): Reenable Parameter caching when dart-lang/sdk#30146 - // is fixed? - if (library != null && newModelElement is! Parameter) { - library.packageGraph._allConstructedModelElements[key] = newModelElement; - if (newModelElement is Inheritable) { - Tuple2 iKey = Tuple2(e, library); - library.packageGraph._allInheritableElements - .putIfAbsent(iKey, () => Set()); - library.packageGraph._allInheritableElements[iKey].add(newModelElement); - } - } - if (newModelElement is GetterSetterCombo) { - assert(getter == null || newModelElement?.getter?.enclosingCombo != null); - assert(setter == null || newModelElement?.setter?.enclosingCombo != null); - } - - assert(newModelElement.element is! MultiplyInheritedExecutableElement); - return newModelElement; - } - - /// Stub for mustache4dart, or it will search enclosing elements to find - /// names for members. - bool get hasCategoryNames => false; - - Set get exportedInLibraries { - return library - .packageGraph.libraryElementReexportedBy[this.element.library]; - } - - ModelNode _modelNode; - - @override - ModelNode get modelNode => - _modelNode ??= packageGraph._getModelNodeFor(element); - - List get annotations => annotationsFromMetadata(element.metadata); - - /// Returns linked annotations from a given metadata set, with escaping. - List annotationsFromMetadata(List md) { - List annotationStrings = []; - if (md == null) return annotationStrings; - for (ElementAnnotation a in md) { - String annotation = (const HtmlEscape()).convert(a.toSource()); - Element annotationElement = a.element; - - ClassElement annotationClassElement; - if (annotationElement is ExecutableElement) { - annotationElement = - (annotationElement as ExecutableElement).returnType.element; - } - if (annotationElement is ClassElement) { - annotationClassElement = annotationElement; - } - ModelElement annotationModelElement = - packageGraph.findCanonicalModelElementFor(annotationElement); - // annotationElement can be null if the element can't be resolved. - Class annotationClass = packageGraph - .findCanonicalModelElementFor(annotationClassElement) as Class; - if (annotationClass == null && - annotationElement != null && - annotationClassElement != null) { - annotationClass = - ModelElement.fromElement(annotationClassElement, packageGraph) - as Class; - } - // Some annotations are intended to be invisible (@pragma) - if (annotationClass == null || - !packageGraph.invisibleAnnotations.contains(annotationClass)) { - if (annotationModelElement != null) { - annotation = annotation.replaceFirst( - annotationModelElement.name, annotationModelElement.linkedName); - } - annotationStrings.add(annotation); - } - } - return annotationStrings; - } - - bool _isPublic; - - @override - bool get isPublic { - if (_isPublic == null) { - if (name == '') { - _isPublic = false; - } else if (this is! Library && (library == null || !library.isPublic)) { - _isPublic = false; - } else if (enclosingElement is Class && - !(enclosingElement as Class).isPublic) { - _isPublic = false; - } else if (enclosingElement is Extension && - !(enclosingElement as Extension).isPublic) { - _isPublic = false; - } else { - String docComment = documentationComment; - if (docComment == null) { - _isPublic = utils.hasPublicName(element); - } else { - _isPublic = utils.hasPublicName(element) && - !(docComment.contains('@nodoc') || - docComment.contains('')); - } - } - } - return _isPublic; - } - - @override - List get commentRefs { - if (_commentRefs == null) { - _commentRefs = []; - for (ModelElement from in documentationFrom) { - List checkReferences = [from]; - if (from is Accessor) { - checkReferences.add(from.enclosingCombo); - } - for (ModelElement e in checkReferences) { - _commentRefs.addAll(e.modelNode.commentRefs ?? []); - } - } - } - return _commentRefs; - } - - DartdocOptionContext _config; - - @override - DartdocOptionContext get config { - if (_config == null) { - _config = - DartdocOptionContext.fromContextElement(packageGraph.config, element); - } - return _config; - } - - @override - Set get locationPieces { - return Set.from(element.location - .toString() - .split(locationSplitter) - .where((s) => s.isNotEmpty)); - } - - Set get features { - Set allFeatures = Set(); - allFeatures.addAll(annotations); - - // Replace the @override annotation with a feature that explicitly - // indicates whether an override has occurred. - allFeatures.remove('@override'); - - // Drop the plain "deprecated" annotation, that's indicated via - // strikethroughs. Custom @Deprecated() will still appear. - allFeatures.remove('@deprecated'); - // const and static are not needed here because const/static elements get - // their own sections in the doc. - if (isFinal) allFeatures.add('final'); - return allFeatures; - } - - String get featuresAsString { - List allFeatures = features.toList()..sort(byFeatureOrdering); - return allFeatures.join(', '); - } - - bool get canHaveParameters => - element is ExecutableElement || - element is FunctionTypedElement || - element is FunctionTypeAliasElement; - - ModelElement _buildCanonicalModelElement() { - Container preferredClass; - if (enclosingElement is Class || enclosingElement is Extension) { - preferredClass = enclosingElement; - } - return packageGraph.findCanonicalModelElementFor(element, - preferredClass: preferredClass); - } - - // Returns the canonical ModelElement for this ModelElement, or null - // if there isn't one. - ModelElement _canonicalModelElement; - - ModelElement get canonicalModelElement => - _canonicalModelElement ??= _buildCanonicalModelElement(); - - List _documentationFrom; - - // TODO(jcollins-g): untangle when mixins can call super - @override - List get documentationFrom { - if (_documentationFrom == null) { - _documentationFrom = computeDocumentationFrom; - } - return _documentationFrom; - } - - bool get hasSourceHref => sourceHref.isNotEmpty; - String _sourceHref; - - String get sourceHref { - _sourceHref ??= SourceLinker.fromElement(this).href(); - return _sourceHref; - } - - /// Returns the ModelElement(s) from which we will get documentation. - /// Can be more than one if this is a Field composing documentation from - /// multiple Accessors. - /// - /// This getter will walk up the inheritance hierarchy - /// to find docs, if the current class doesn't have docs - /// for this element. - List get computeDocumentationFrom { - List docFrom; - - if (documentationComment == null && - canOverride() && - this is Inheritable && - (this as Inheritable).overriddenElement != null) { - docFrom = (this as Inheritable).overriddenElement.documentationFrom; - } else if (this is Inheritable && (this as Inheritable).isInherited) { - Inheritable thisInheritable = (this as Inheritable); - ModelElement fromThis = ModelElement.fromElement( - element, thisInheritable.definingEnclosingContainer.packageGraph); - docFrom = fromThis.documentationFrom; - } else { - docFrom = [this]; - } - return docFrom; - } - - String _buildDocumentationLocal() => _buildDocumentationBaseSync(); - - /// Override this to add more features to the documentation builder in a - /// subclass. - String _buildDocumentationAddition(String docs) => docs ??= ''; - - /// Separate from _buildDocumentationLocal for overriding. - String _buildDocumentationBaseSync() { - assert(_rawDocs == null, - 'reentrant calls to _buildDocumentation* not allowed'); - // Do not use the sync method if we need to evaluate tools or templates. - assert(!isCanonical || - !needsPrecacheRegExp.hasMatch(documentationComment ?? '')); - if (config.dropTextFrom.contains(element.library.name)) { - _rawDocs = ''; - } else { - _rawDocs = documentationComment ?? ''; - _rawDocs = stripComments(_rawDocs) ?? ''; - _rawDocs = _injectExamples(_rawDocs); - _rawDocs = _injectYouTube(_rawDocs); - _rawDocs = _injectAnimations(_rawDocs); - _rawDocs = _stripHtmlAndAddToIndex(_rawDocs); - } - _rawDocs = _buildDocumentationAddition(_rawDocs); - return _rawDocs; - } - - /// Separate from _buildDocumentationLocal for overriding. Can only be - /// used as part of [PackageGraph.setUpPackageGraph]. - Future _buildDocumentationBase() async { - assert(_rawDocs == null, - 'reentrant calls to _buildDocumentation* not allowed'); - // Do not use the sync method if we need to evaluate tools or templates. - if (config.dropTextFrom.contains(element.library.name)) { - _rawDocs = ''; - } else { - _rawDocs = documentationComment ?? ''; - _rawDocs = stripComments(_rawDocs) ?? ''; - // Must evaluate tools first, in case they insert any other directives. - _rawDocs = await _evaluateTools(_rawDocs); - _rawDocs = _injectExamples(_rawDocs); - _rawDocs = _injectYouTube(_rawDocs); - _rawDocs = _injectAnimations(_rawDocs); - _rawDocs = _stripMacroTemplatesAndAddToIndex(_rawDocs); - _rawDocs = _stripHtmlAndAddToIndex(_rawDocs); - } - _rawDocs = _buildDocumentationAddition(_rawDocs); - return _rawDocs; - } - - /// Returns the documentation for this literal element unless - /// [config.dropTextFrom] indicates it should not be returned. Macro - /// definitions are stripped, but macros themselves are not injected. This - /// is a two stage process to avoid ordering problems. - String _documentationLocal; - - String get documentationLocal => - _documentationLocal ??= _buildDocumentationLocal(); - - /// Returns the docs, stripped of their leading comments syntax. - @override - String get documentation { - return _injectMacros( - documentationFrom.map((e) => e.documentationLocal).join('

')); - } - - Library get definingLibrary => - packageGraph.findButDoNotCreateLibraryFor(element); - - Library _canonicalLibrary; - - // _canonicalLibrary can be null so we can't check against null to see whether - // we tried to compute it before. - bool _canonicalLibraryIsSet = false; - - @override - Library get canonicalLibrary { - if (!_canonicalLibraryIsSet) { - // This is not accurate if we are constructing the Package. - assert(packageGraph.allLibrariesAdded); - // Since we're may be looking for a library, find the [Element] immediately - // contained by a [CompilationUnitElement] in the tree. - Element topLevelElement = element; - while (topLevelElement != null && - topLevelElement.enclosingElement is! LibraryElement && - topLevelElement.enclosingElement is! CompilationUnitElement && - topLevelElement.enclosingElement != null) { - topLevelElement = topLevelElement.enclosingElement; - } - - // Privately named elements can never have a canonical library, so - // just shortcut them out. - if (!utils.hasPublicName(element)) { - _canonicalLibrary = null; - } else if (!packageGraph.localPublicLibraries.contains(definingLibrary)) { - List candidateLibraries = definingLibrary.exportedInLibraries - ?.where((l) => - l.isPublic && - l.package.documentedWhere != DocumentLocation.missing) - ?.toList(); - - if (candidateLibraries != null) { - candidateLibraries = candidateLibraries.where((l) { - Element lookup = (l.element as LibraryElement) - .exportNamespace - .definedNames[topLevelElement?.name]; - if (lookup is PropertyAccessorElement) { - lookup = (lookup as PropertyAccessorElement).variable; - } - if (topLevelElement == lookup) return true; - return false; - }).toList(); - - // 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)) { - warn(PackageWarning.reexportedPrivateApiAcrossPackages, - message: definingLibrary.package.fullyQualifiedName, - referredFrom: candidateLibraries); - } else { - candidateLibraries - .removeWhere((l) => l.package != definingLibrary.package); - } - - // Start with our top-level element. - ModelElement warnable = - ModelElement.fromElement(topLevelElement, packageGraph); - if (candidateLibraries.length > 1) { - // Heuristic scoring to determine which library a human likely - // considers this element to be primarily 'from', and therefore, - // canonical. Still warn if the heuristic isn't that confident. - List scoredCandidates = - warnable.scoreCanonicalCandidates(candidateLibraries); - candidateLibraries = - scoredCandidates.map((s) => s.library).toList(); - double secondHighestScore = - scoredCandidates[scoredCandidates.length - 2].score; - double highestScore = scoredCandidates.last.score; - double confidence = highestScore - secondHighestScore; - String message = - "${candidateLibraries.map((l) => l.name)} -> ${candidateLibraries.last.name} (confidence ${confidence.toStringAsPrecision(4)})"; - List debugLines = []; - debugLines.addAll(scoredCandidates.map((s) => '${s.toString()}')); - - if (confidence < config.ambiguousReexportScorerMinConfidence) { - warnable.warn(PackageWarning.ambiguousReexport, - message: message, extendedDebug: debugLines); - } - } - if (candidateLibraries.isNotEmpty) { - _canonicalLibrary = candidateLibraries.last; - } - } - } else { - _canonicalLibrary = definingLibrary; - } - // Only pretend when not linking to remote packages. - if (this is Inheritable && !config.linkToRemote) { - if ((this as Inheritable).isInherited && - _canonicalLibrary == null && - packageGraph.publicLibraries.contains(library)) { - // In the event we've inherited a field from an object that isn't directly reexported, - // we may need to pretend we are canonical for this. - _canonicalLibrary = library; - } - } - _canonicalLibraryIsSet = true; - } - assert(_canonicalLibrary == null || - packageGraph.publicLibraries.contains(_canonicalLibrary)); - return _canonicalLibrary; - } - - @override - bool get isCanonical { - if (library == canonicalLibrary) { - if (this is Inheritable) { - Inheritable i = (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). - if (i.enclosingElement == i.canonicalEnclosingContainer) { - return true; - } else { - return false; - } - } - // If there's no inheritance to deal with, we're done. - return true; - } - return false; - } - - String _htmlDocumentation; - - @override - String get documentationAsHtml { - if (_htmlDocumentation != null) return _htmlDocumentation; - _htmlDocumentation = _injectHtmlFragments(_documentation.asHtml); - return _htmlDocumentation; - } - - @override - Element get element => _element; - - @override - String get location { - // Call nothing from here that can emit warnings or you'll cause stack overflows. - if (characterLocation != null) { - return "(${path.toUri(sourceFileName)}:${characterLocation.toString()})"; - } - return "(${path.toUri(sourceFileName)})"; - } - - /// Returns a link to extended documentation, or the empty string if that - /// does not exist. - String get extendedDocLink { - if (hasExtendedDocumentation) { - return '[...]'; - } - return ''; - } - - String get fileName => "${name}.html"; - - /// Returns the fully qualified name. - /// - /// For example: libraryName.className.methodName - @override - String get fullyQualifiedName { - return (_fullyQualifiedName ??= _buildFullyQualifiedName()); - } - - String get fullyQualifiedNameWithoutLibrary { - // Remember, periods are legal in library names. - if (_fullyQualifiedNameWithoutLibrary == null) { - _fullyQualifiedNameWithoutLibrary = - fullyQualifiedName.replaceFirst("${library.fullyQualifiedName}.", ''); - } - return _fullyQualifiedNameWithoutLibrary; - } - - String get sourceFileName => element.source.fullName; - - CharacterLocation _characterLocation; - bool _characterLocationIsSet = false; - - @override - CharacterLocation get characterLocation { - if (!_characterLocationIsSet) { - LineInfo lineInfo = compilationUnitElement.lineInfo; - _characterLocationIsSet = true; - assert(element.nameOffset >= 0, - 'Invalid location data for element: $fullyQualifiedName'); - assert(lineInfo != null, - 'No lineInfo data available for element: $fullyQualifiedName'); - if (element.nameOffset >= 0) { - _characterLocation = lineInfo?.getLocation(element.nameOffset); - } - } - return _characterLocation; - } - - CompilationUnitElement get compilationUnitElement => - element.getAncestor((e) => e is CompilationUnitElement); - - bool get hasAnnotations => annotations.isNotEmpty; - - @override - bool get hasDocumentation => - documentation != null && documentation.isNotEmpty; - - @override - bool get hasExtendedDocumentation => - href != null && _documentation.hasExtendedDocs; - - bool get hasParameters => parameters.isNotEmpty; - - /// If canonicalLibrary (or canonicalEnclosingElement, for Inheritable - /// subclasses) is null, href should be null. - @override - String get href; - - String get htmlId => name; - - bool get isAsynchronous => - isExecutable && (element as ExecutableElement).isAsynchronous; - - 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].where((a) => a != null).toList(); - - // At least one of these should be non-null. Otherwise things are weird - assert(deprecatedValues.isNotEmpty); - - // 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); - } - - @override - bool get isDocumented => isCanonical && isPublic; - - bool get isExecutable => element is ExecutableElement; - - bool get isFinal => false; - - bool get isLocalElement => element is LocalElement; - - bool get isPropertyAccessor => element is PropertyAccessorElement; - - bool get isPropertyInducer => element is PropertyInducingElement; - - bool get isStatic { - if (isPropertyInducer) { - return (element as PropertyInducingElement).isStatic; - } - return false; - } - - /// A human-friendly name for the kind of element this is. - @override - String get kind; - - @override - Library get library => _library; - - String get linkedName { - if (_linkedName == null) { - _linkedName = _calculateLinkedName(); - } - return _linkedName; - } - - String get linkedParams => utils.linkedParams(parameters); - - String get linkedParamsLines => utils.linkedParams(parameters).trim(); - - String get linkedParamsNoMetadata => - utils.linkedParams(parameters, showMetadata: false); - - String get linkedParamsNoMetadataOrNames { - return utils.linkedParams(parameters, - showMetadata: false, showNames: false); - } - - ElementType get modelType { - if (_modelType == null) { - // TODO(jcollins-g): Need an interface for a "member with a type" (or changed object model). - if (_originalMember != null && - (_originalMember is ExecutableMember || - _originalMember is ParameterMember)) { - if (_originalMember is ExecutableMember) { - _modelType = ElementType.from( - (_originalMember as ExecutableMember).type, - library, - packageGraph); - } else { - // ParameterMember - _modelType = ElementType.from( - (_originalMember as ParameterMember).type, library, packageGraph); - } - } else if (element is ExecutableElement || - element is FunctionTypedElement || - element is ParameterElement || - element is TypeDefiningElement || - element is PropertyInducingElement) { - _modelType = - ElementType.from((element as dynamic).type, library, packageGraph); - } - } - return _modelType; - } - - @override - String get name => element.name; - - // TODO(jcollins-g): refactor once dartdoc will only run in a VM where mixins - // calling super is allowed (SDK constraint >= 2.1.0). - String computeOneLineDoc() => - '${_documentation.asOneLiner}${extendedDocLink.isEmpty ? "" : " $extendedDocLink"}'; - String _oneLineDoc; - - @override - String get oneLineDoc { - if (_oneLineDoc == null) { - _oneLineDoc = computeOneLineDoc(); - } - return _oneLineDoc; - } - - Member get originalMember => _originalMember; - - final PackageGraph _packageGraph; - - @override - PackageGraph get packageGraph => _packageGraph; - - @override - Package get package => library.package; - - bool get isPublicAndPackageDocumented => - isPublic && library.packageGraph.packageDocumentedFor(this); - - List _allParameters; - - // TODO(jcollins-g): This is in the wrong place. Move parts to GetterSetterCombo, - // elsewhere as appropriate? - List get allParameters { - if (_allParameters == null) { - final Set recursedParameters = Set(); - final Set newParameters = Set(); - if (this is GetterSetterCombo && - (this as GetterSetterCombo).setter != null) { - newParameters.addAll((this as GetterSetterCombo).setter.parameters); - } else { - if (canHaveParameters) newParameters.addAll(parameters); - } - while (newParameters.isNotEmpty) { - recursedParameters.addAll(newParameters); - newParameters.clear(); - for (Parameter p in recursedParameters) { - var l = p.modelType.parameters - .where((pm) => !recursedParameters.contains(pm)); - newParameters.addAll(l); - } - } - _allParameters = recursedParameters.toList(); - } - return _allParameters; - } - - List get parameters { - if (!canHaveParameters) { - throw StateError("$element cannot have parameters"); - } - - if (_parameters == null) { - List params; - - if (element is ExecutableElement) { - if (_originalMember != null) { - assert(_originalMember is ExecutableMember); - params = (_originalMember as ExecutableMember).parameters; - } else { - params = (element as ExecutableElement).parameters; - } - } - if (params == null && element is FunctionTypedElement) { - if (_originalMember != null) { - params = (_originalMember as dynamic).parameters; - } else { - params = (element as FunctionTypedElement).parameters; - } - } - if (params == null && element is FunctionTypeAliasElement) { - params = (element as FunctionTypeAliasElement).function.parameters; - } - - _parameters = UnmodifiableListView(params - .map((p) => ModelElement.from(p, library, packageGraph) as Parameter) - .toList()); - } - return _parameters; - } - - @override - void warn(PackageWarning kind, - {String message, - Iterable referredFrom, - Iterable extendedDebug}) { - packageGraph.warnOnElement(this, kind, - message: message, - referredFrom: referredFrom, - extendedDebug: extendedDebug); - } - - String _computeDocumentationComment() => element.documentationComment; - - bool _documentationCommentComputed = false; - String _documentationComment; - - String get documentationComment { - if (_documentationCommentComputed == false) { - _documentationComment = _computeDocumentationComment(); - _documentationCommentComputed = true; - } - return _documentationComment; - } - - /// Unconditionally precache local documentation. - /// - /// Use only in factory for [PackageGraph]. - Future _precacheLocalDocs() async { - _documentationLocal = await _buildDocumentationBase(); - } - - Documentation get _documentation { - if (__documentation != null) return __documentation; - __documentation = Documentation.forElement(this); - return __documentation; - } - - bool canOverride() => - element is ClassMemberElement || element is PropertyAccessorElement; - - @override - int compareTo(dynamic other) { - if (other is ModelElement) { - return name.toLowerCase().compareTo(other.name.toLowerCase()); - } else { - return 0; - } - } - - @override - String toString() => '$runtimeType $name'; - - String _buildFullyQualifiedName([ModelElement e, String fqName]) { - e ??= this; - fqName ??= e.name; - - if (e is! EnclosedElement || e.enclosingElement == null) { - return fqName; - } - - return _buildFullyQualifiedName( - e.enclosingElement, '${e.enclosingElement.name}.$fqName'); - } - - String _calculateLinkedName() { - // 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 || - this.element?.kind == ElementKind.DYNAMIC || - this is ModelFunction); - - if (href == null) { - if (isPublicAndPackageDocumented) { - warn(PackageWarning.noCanonicalFound); - } - return htmlEscape.convert(name); - } - - var classContent = isDeprecated ? ' class="deprecated"' : ''; - return '$name'; - } - - /// Replace {@example ...} in API comments with the content of named file. - /// - /// Syntax: - /// - /// {@example PATH [region=NAME] [lang=NAME]} - /// - /// If PATH is `dir/file.ext` and region is `r` then we'll look for the file - /// named `dir/file-r.ext.md`, relative to the project root directory of the - /// project for which the docs are being generated. - /// - /// Examples: (escaped in this comment to show literal values in dartdoc's - /// dartdoc) - /// - /// {@example examples/angular/quickstart/web/main.dart} - /// {@example abc/def/xyz_component.dart region=template lang=html} - /// - String _injectExamples(String rawdocs) { - final dirPath = package.packageMeta.dir.path; - RegExp exampleRE = RegExp(r'{@example\s+([^}]+)}'); - return rawdocs.replaceAllMapped(exampleRE, (match) { - var args = _getExampleArgs(match[1]); - if (args == null) { - // Already warned about an invalid parameter if this happens. - return ''; - } - var lang = - args['lang'] ?? path.extension(args['src']).replaceFirst('.', ''); - - var replacement = match[0]; // default to fully matched string. - - var fragmentFile = File(path.join(dirPath, args['file'])); - if (fragmentFile.existsSync()) { - replacement = fragmentFile.readAsStringSync(); - if (lang.isNotEmpty) { - replacement = replacement.replaceFirst('```', '```$lang'); - } - } else { - // TODO(jcollins-g): move this to Package.warn system - var filePath = - this.element.source.fullName.substring(dirPath.length + 1); - - logWarning( - 'warning: ${filePath}: @example file not found, ${fragmentFile.path}'); - } - return replacement; - }); - } - - static Future _replaceAllMappedAsync( - String string, Pattern exp, Future replace(Match match)) async { - StringBuffer replaced = StringBuffer(); - int currentIndex = 0; - for (Match match in exp.allMatches(string)) { - String prefix = match.input.substring(currentIndex, match.start); - currentIndex = match.end; - replaced..write(prefix)..write(await replace(match)); - } - replaced.write(string.substring(currentIndex)); - return replaced.toString(); - } - - /// Replace {@tool ...}{@end-tool} in API comments with the - /// output of an external tool. - /// - /// Looks for tools invocations, looks up their bound executables in the - /// options, and executes them with the source comment material as input, - /// returning the output of the tool. If a named tool isn't configured in the - /// options file, then it will not be executed, and dartdoc will quit with an - /// error. - /// - /// Tool command line arguments are passed to the tool, with the token - /// `$INPUT` replaced with the absolute path to a temporary file containing - /// the content for the tool to read and produce output from. If the tool - /// doesn't need any input, then no `$INPUT` is needed. - /// - /// Nested tool directives will not be evaluated, but tools may generate other - /// directives in their output and those will be evaluated. - /// - /// Syntax: - /// - /// {@tool TOOL [Tool arguments]} - /// Content to send to tool. - /// {@end-tool} - /// - /// Examples: - /// - /// In `dart_options.yaml`: - /// - /// ```yaml - /// dartdoc: - /// tools: - /// # Prefixes the given input with "## " - /// # Path is relative to project root. - /// prefix: "bin/prefix.dart" - /// # Prints the date - /// date: "/bin/date" - /// ``` - /// - /// In code: - /// - /// _This:_ - /// - /// {@tool prefix $INPUT} - /// Content to send to tool. - /// {@end-tool} - /// {@tool date --iso-8601=minutes --utc} - /// {@end-tool} - /// - /// _Produces:_ - /// - /// ## Content to send to tool. - /// 2018-09-18T21:15+00:00 - Future _evaluateTools(String rawDocs) async { - if (config.allowTools) { - int invocationIndex = 0; - return await _replaceAllMappedAsync(rawDocs, basicToolRegExp, - (basicMatch) async { - List args = _splitUpQuotedArgs(basicMatch[1]).toList(); - // Tool name must come first. - if (args.isEmpty) { - warn(PackageWarning.toolError, - message: - 'Must specify a tool to execute for the @tool directive.'); - return Future.value(''); - } - // Count the number of invocations of tools in this dartdoc block, - // so that tools can differentiate different blocks from each other. - invocationIndex++; - return await config.tools.runner.run( - args, - (String message) async => - warn(PackageWarning.toolError, message: message), - content: basicMatch[2], - environment: { - 'SOURCE_LINE': characterLocation?.lineNumber.toString(), - 'SOURCE_COLUMN': characterLocation?.columnNumber.toString(), - 'SOURCE_PATH': (sourceFileName == null || - package?.packagePath == null) - ? null - : path.relative(sourceFileName, from: package.packagePath), - 'PACKAGE_PATH': package?.packagePath, - 'PACKAGE_NAME': package?.name, - 'LIBRARY_NAME': library?.fullyQualifiedName, - 'ELEMENT_NAME': fullyQualifiedNameWithoutLibrary, - 'INVOCATION_INDEX': invocationIndex.toString(), - 'PACKAGE_INVOCATION_INDEX': - (package.toolInvocationIndex++).toString(), - }..removeWhere((key, value) => value == null)); - }); - } else { - return rawDocs; - } - } - - /// Replace {@youtube ...} in API comments with some HTML to embed - /// a YouTube video. - /// - /// Syntax: - /// - /// {@youtube WIDTH HEIGHT URL} - /// - /// Example: - /// - /// {@youtube 560 315 https://www.youtube.com/watch?v=oHg5SJYRHA0} - /// - /// Which will embed a YouTube player into the page that plays the specified - /// video. - /// - /// The width and height must be positive integers specifying the dimensions - /// of the video in pixels. The height and width are used to calculate the - /// aspect ratio of the video; the video is always rendered to take up all - /// available horizontal space to accommodate different screen sizes on - /// desktop and mobile. - /// - /// The video URL must have the following format: - /// https://www.youtube.com/watch?v=oHg5SJYRHA0. This format can usually be - /// found in the address bar of the browser when viewing a YouTube video. - String _injectYouTube(String rawDocs) { - // Matches all youtube directives (even some invalid ones). This is so - // we can give good error messages if the directive is malformed, instead of - // just silently emitting it as-is. - final RegExp basicAnimationRegExp = RegExp(r'''{@youtube\s+([^}]+)}'''); - - // Matches YouTube IDs from supported YouTube URLs. - final RegExp validYouTubeUrlRegExp = - RegExp('https://www\.youtube\.com/watch\\?v=([^&]+)\$'); - - return rawDocs.replaceAllMapped(basicAnimationRegExp, (basicMatch) { - final ArgParser parser = ArgParser(); - final ArgResults args = _parseArgs(basicMatch[1], parser, 'youtube'); - if (args == null) { - // Already warned about an invalid parameter if this happens. - return ''; - } - final List positionalArgs = args.rest.sublist(0); - if (positionalArgs.length != 3) { - warn(PackageWarning.invalidParameter, - message: 'Invalid @youtube directive, "${basicMatch[0]}"\n' - 'YouTube directives must be of the form "{@youtube WIDTH ' - 'HEIGHT URL}"'); - return ''; - } - - final int width = int.tryParse(positionalArgs[0]); - if (width == null || width <= 0) { - warn(PackageWarning.invalidParameter, - message: 'A @youtube directive has an invalid width, ' - '"${positionalArgs[0]}". The width must be a positive integer.'); - } - - final int height = int.tryParse(positionalArgs[1]); - if (height == null || height <= 0) { - warn(PackageWarning.invalidParameter, - message: 'A @youtube directive has an invalid height, ' - '"${positionalArgs[1]}". The height must be a positive integer.'); - } - - final Match url = validYouTubeUrlRegExp.firstMatch(positionalArgs[2]); - if (url == null) { - warn(PackageWarning.invalidParameter, - message: 'A @youtube directive has an invalid URL: ' - '"${positionalArgs[2]}". Supported YouTube URLs have the ' - 'follwing format: https://www.youtube.com/watch?v=oHg5SJYRHA0.'); - return ''; - } - final String youTubeId = url.group(url.groupCount); - final String aspectRatio = (height / width * 100).toStringAsFixed(2); - - // Blank lines before and after, and no indenting at the beginning and end - // is needed so that Markdown doesn't confuse this with code, so be - // careful of whitespace here. - return ''' - -

- -

- -'''; // String must end at beginning of line, or following inline text will be - // indented. - }); - } - - /// Replace {@animation ...} in API comments with some HTML to manage an - /// MPEG 4 video as an animation. - /// - /// Syntax: - /// - /// {@animation WIDTH HEIGHT URL [id=ID]} - /// - /// Example: - /// - /// {@animation 300 300 https://example.com/path/to/video.mp4 id="my_video"} - /// - /// Which will render the HTML necessary for embedding a simple click-to-play - /// HTML5 video player with no controls that has an HTML id of "my_video". - /// - /// The optional ID should be a unique id that is a valid JavaScript - /// identifier, and will be used as the id for the video tag. If no ID is - /// supplied, then a unique identifier (starting with "animation_") will be - /// generated. - /// - /// The width and height must be integers specifying the dimensions of the - /// video file in pixels. - String _injectAnimations(String rawDocs) { - // Matches all animation directives (even some invalid ones). This is so - // we can give good error messages if the directive is malformed, instead of - // just silently emitting it as-is. - final RegExp basicAnimationRegExp = RegExp(r'''{@animation\s+([^}]+)}'''); - - // Matches valid javascript identifiers. - final RegExp validIdRegExp = RegExp(r'^[a-zA-Z_]\w*$'); - - // Make sure we have a set to keep track of used IDs for this href. - package.usedAnimationIdsByHref[href] ??= {}; - - String getUniqueId(String base) { - int animationIdCount = 1; - String id = '$base$animationIdCount'; - // We check for duplicate IDs so that we make sure not to collide with - // user-supplied ids on the same page. - while (package.usedAnimationIdsByHref[href].contains(id)) { - animationIdCount++; - id = '$base$animationIdCount'; - } - return id; - } - - return rawDocs.replaceAllMapped(basicAnimationRegExp, (basicMatch) { - final ArgParser parser = ArgParser(); - parser.addOption('id'); - final ArgResults args = _parseArgs(basicMatch[1], parser, 'animation'); - if (args == null) { - // Already warned about an invalid parameter if this happens. - return ''; - } - final List positionalArgs = args.rest.sublist(0); - String uniqueId; - bool wasDeprecated = false; - if (positionalArgs.length == 4) { - // Supports the original form of the animation tag for backward - // compatibility. - uniqueId = positionalArgs.removeAt(0); - wasDeprecated = true; - } else if (positionalArgs.length == 3) { - uniqueId = args['id'] ?? getUniqueId('animation_'); - } else { - warn(PackageWarning.invalidParameter, - message: 'Invalid @animation directive, "${basicMatch[0]}"\n' - 'Animation directives must be of the form "{@animation WIDTH ' - 'HEIGHT URL [id=ID]}"'); - return ''; - } - - if (!validIdRegExp.hasMatch(uniqueId)) { - warn(PackageWarning.invalidParameter, - message: 'An animation has an invalid identifier, "$uniqueId". The ' - 'identifier can only contain letters, numbers and underscores, ' - 'and must not begin with a number.'); - return ''; - } - if (package.usedAnimationIdsByHref[href].contains(uniqueId)) { - warn(PackageWarning.invalidParameter, - message: 'An animation has a non-unique identifier, "$uniqueId". ' - 'Animation identifiers must be unique.'); - return ''; - } - package.usedAnimationIdsByHref[href].add(uniqueId); - - int width; - try { - width = int.parse(positionalArgs[0]); - } on FormatException { - warn(PackageWarning.invalidParameter, - message: 'An animation has an invalid width ($uniqueId), ' - '"${positionalArgs[0]}". The width must be an integer.'); - return ''; - } - - int height; - try { - height = int.parse(positionalArgs[1]); - } on FormatException { - warn(PackageWarning.invalidParameter, - message: 'An animation has an invalid height ($uniqueId), ' - '"${positionalArgs[1]}". The height must be an integer.'); - return ''; - } - - Uri movieUrl; - try { - movieUrl = Uri.parse(positionalArgs[2]); - } on FormatException catch (e) { - warn(PackageWarning.invalidParameter, - message: 'An animation URL could not be parsed ($uniqueId): ' - '${positionalArgs[2]}\n$e'); - return ''; - } - final String overlayId = '${uniqueId}_play_button_'; - - // Only warn about deprecation if some other warning didn't occur. - if (wasDeprecated) { - warn(PackageWarning.deprecated, - message: - 'Deprecated form of @animation directive, "${basicMatch[0]}"\n' - 'Animation directives are now of the form "{@animation ' - 'WIDTH HEIGHT URL [id=ID]}" (id is an optional ' - 'parameter)'); - } - - // Blank lines before and after, and no indenting at the beginning and end - // is needed so that Markdown doesn't confuse this with code, so be - // careful of whitespace here. - return ''' - -
-
-
- -
- -'''; // String must end at beginning of line, or following inline text will be - // indented. - }); - } - - /// Replace <[digest]> in API comments with - /// the contents of the HTML fragment earlier defined by the - /// {@inject-html} directive. The [digest] is a SHA1 of the contents - /// of the HTML fragment, automatically generated upon parsing the - /// {@inject-html} directive. - /// - /// This markup is generated and inserted by [_stripHtmlAndAddToIndex] when it - /// removes the HTML fragment in preparation for markdown processing. It isn't - /// meant to be used at a user level. - /// - /// Example: - /// - /// You place the fragment in a dartdoc comment: - /// - /// Some comments - /// {@inject-html} - /// <p>[HTML contents!]</p> - /// {@endtemplate} - /// More comments - /// - /// and [_stripHtmlAndAddToIndex] will replace your HTML fragment with this: - /// - /// Some comments - /// <dartdoc-html>4cc02f877240bf69855b4c7291aba8a16e5acce0</dartdoc-html> - /// More comments - /// - /// Which will render in the final HTML output as: - /// - /// Some comments - /// <p>[HTML contents!]</p> - /// More comments - /// - /// And the HTML fragment will not have been processed or changed by Markdown, - /// but just injected verbatim. - String _injectHtmlFragments(String rawDocs) { - if (!config.injectHtml) return rawDocs; - - return rawDocs.replaceAllMapped(htmlInjectRegExp, (match) { - String fragment = packageGraph.getHtmlFragment(match[1]); - if (fragment == null) { - warn(PackageWarning.unknownHtmlFragment, message: match[1]); - } - return fragment; - }); - } - - /// Replace {@macro ...} in API comments with the contents of the macro - /// - /// Syntax: - /// - /// {@macro NAME} - /// - /// Example: - /// - /// You define the template in any comment for a documentable entity like: - /// - /// {@template foo} - /// Foo contents! - /// {@endtemplate} - /// - /// and them somewhere use it like this: - /// - /// Some comments - /// {@macro foo} - /// More comments - /// - /// Which will render - /// - /// Some comments - /// Foo contents! - /// More comments - /// - String _injectMacros(String rawDocs) { - return rawDocs.replaceAllMapped(macroRegExp, (match) { - String macro = packageGraph.getMacro(match[1]); - if (macro == null) { - warn(PackageWarning.unknownMacro, message: match[1]); - } - return macro; - }); - } - - /// Parse and remove {@template ...} in API comments and store them - /// in the index on the package. - /// - /// Syntax: - /// - /// {@template NAME} - /// The contents of the macro - /// {@endtemplate} - /// - String _stripMacroTemplatesAndAddToIndex(String rawDocs) { - return rawDocs.replaceAllMapped(templateRegExp, (match) { - packageGraph._addMacro(match[1].trim(), match[2].trim()); - return "{@macro ${match[1].trim()}}"; - }); - } - - /// Parse and remove {@inject-html ...} in API comments and store - /// them in the index on the package, replacing them with a SHA1 hash of the - /// contents, where the HTML will be re-injected after Markdown processing of - /// the rest of the text is complete. - /// - /// Syntax: - /// - /// {@inject-html} - ///

The HTML to inject.

- /// {@end-inject-html} - /// - String _stripHtmlAndAddToIndex(String rawDocs) { - if (!config.injectHtml) return rawDocs; - return rawDocs.replaceAllMapped(htmlRegExp, (match) { - String fragment = match[1]; - String digest = sha1.convert(fragment.codeUnits).toString(); - packageGraph._addHtmlFragment(digest, fragment); - // The newlines are so that Markdown will pass this through without - // touching it. - return '\n$digest\n'; - }); - } - - /// Helper to process arguments given as a (possibly quoted) string. - /// - /// First, this will split the given [argsAsString] into separate arguments, - /// taking any quoting (either ' or " are accepted) into account, including - /// handling backslash-escaped quotes. - /// - /// Then, it will prepend "--" to any args that start with an identifier - /// followed by an equals sign, allowing the argument parser to treat any - /// "foo=bar" argument as "--foo=bar". It does handle quoted args like - /// "foo='bar baz'" too, returning just bar (without quotes) for the foo - /// value. - Iterable _splitUpQuotedArgs(String argsAsString, - {bool convertToArgs = false}) { - final Iterable matches = argMatcher.allMatches(argsAsString); - // Remove quotes around args, and if convertToArgs is true, then for any - // args that look like assignments (start with valid option names followed - // by an equals sign), add a "--" in front so that they parse as options. - return matches.map((Match match) { - var option = ''; - if (convertToArgs && match[1] != null && !match[1].startsWith('-')) { - option = '--'; - } - if (match[2] != null) { - // This arg has quotes, so strip them. - return '$option${match[1] ?? ''}${match[3] ?? ''}${match[4] ?? ''}'; - } - return '$option${match[0]}'; - }); - } - - /// Helper to process arguments given as a (possibly quoted) string. - /// - /// First, this will split the given [argsAsString] into separate arguments - /// with [_splitUpQuotedArgs] it then parses the resulting argument list - /// normally with [argParser] and returns the result. - ArgResults _parseArgs( - String argsAsString, ArgParser argParser, String directiveName) { - var args = _splitUpQuotedArgs(argsAsString, convertToArgs: true); - try { - return argParser.parse(args); - } on ArgParserException catch (e) { - warn(PackageWarning.invalidParameter, - message: 'The {@$directiveName ...} directive was called with ' - 'invalid parameters. $e'); - return null; - } - } - - /// Helper for _injectExamples used to process @example arguments. - /// Returns a map of arguments. The first unnamed argument will have key 'src'. - /// The computed file path, constructed from 'src' and 'region' will have key - /// 'file'. - Map _getExampleArgs(String argsAsString) { - ArgParser parser = ArgParser(); - parser.addOption('lang'); - parser.addOption('region'); - ArgResults results = _parseArgs(argsAsString, parser, 'example'); - if (results == null) { - return null; - } - - // Extract PATH and fix the path separators. - final String src = results.rest.isEmpty - ? '' - : results.rest.first.replaceAll('/', Platform.pathSeparator); - final Map args = { - 'src': src, - 'lang': results['lang'], - 'region': results['region'] ?? '', - }; - - // Compute 'file' from region and src. - final fragExtension = '.md'; - var file = src + fragExtension; - var region = args['region'] ?? ''; - if (region.isNotEmpty) { - var dir = path.dirname(src); - var basename = path.basenameWithoutExtension(src); - var ext = path.extension(src); - file = path.join(dir, '$basename-$region$ext$fragExtension'); - } - args['file'] = config.examplePathPrefix == null - ? file - : path.join(config.examplePathPrefix, file); - return args; - } -} - -/// A [ModelElement] for a [FunctionElement] that isn't part of a type definition. -class ModelFunction extends ModelFunctionTyped with Categorization { - ModelFunction( - FunctionElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph); - - @override - bool get isStatic { - return _func.isStatic; - } - - @override - String get name => element.name ?? ''; - - @override - FunctionElement get _func => (element as FunctionElement); -} - -/// A [ModelElement] for a [FunctionTypedElement] that is an -/// explicit typedef. -/// -/// Distinct from ModelFunctionTypedef in that it doesn't -/// have a name, but we document it as "Function" to match how these are -/// written in declarations. -class ModelFunctionAnonymous extends ModelFunctionTyped { - ModelFunctionAnonymous( - FunctionTypedElement element, PackageGraph packageGraph) - : super(element, null, packageGraph); - - @override - ModelElement get enclosingElement { - // These are not considered to be a part of libraries, so we can simply - // blindly instantiate a ModelElement for their enclosing element. - return ModelElement.fromElement(element.enclosingElement, packageGraph); - } - - @override - String get name => 'Function'; - - @override - String get linkedName => 'Function'; - - @override - bool get isPublic => false; -} - -/// A [ModelElement] for a [FunctionTypedElement] that is part of an -/// explicit typedef. -class ModelFunctionTypedef extends ModelFunctionTyped { - ModelFunctionTypedef( - FunctionTypedElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph); - - @override - String get name { - Element e = element; - while (e != null) { - if (e is FunctionTypeAliasElement || e is GenericTypeAliasElement) { - return e.name; - } - e = e.enclosingElement; - } - assert(false); - return super.name; - } -} - -class ModelFunctionTyped extends ModelElement - with TypeParameters - implements EnclosedElement { - @override - List typeParameters = []; - - ModelFunctionTyped( - FunctionTypedElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph, null) { - _calcTypeParameters(); - } - - void _calcTypeParameters() { - typeParameters = _func.typeParameters.map((f) { - return ModelElement.from(f, library, packageGraph) as TypeParameter; - }).toList(); - } - - @override - ModelElement get enclosingElement => library; - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(canonicalLibrary != null); - assert(canonicalLibrary == library); - return '${package.baseHref}${library.dirName}/$fileName'; - } - - @override - String get kind => 'function'; - - String get linkedReturnType => modelType.createLinkedReturnTypeName(); - - // Food for mustache. TODO(jcollins-g): what about enclosing elements? - bool get isInherited => false; - - @override - DefinedElementType get modelType => super.modelType; - - FunctionTypedElement get _func => (element as FunctionTypedElement); -} - -/// Something that has a name. -abstract class Nameable { - String get name; - - String get fullyQualifiedName => name; - - Set _namePieces; - - Set get namePieces { - if (_namePieces == null) { - _namePieces = Set() - ..addAll(name.split(locationSplitter).where((s) => s.isNotEmpty)); - } - return _namePieces; - } - - String _namePart; - - /// Utility getter/cache for [_MarkdownCommentReference._getResultsForClass]. - String get namePart { - // TODO(jcollins-g): This should really be the same as 'name', but isn't - // because of accessors and operators. - if (_namePart == null) { - _namePart = fullyQualifiedName.split('.').last; - } - return _namePart; - } - - @override - String toString() => name; -} - -/// Something able to be indexed. -abstract class Indexable implements Nameable { - String get href; - - String get kind; - - int get overriddenDepth => 0; -} - -class Operator extends Method { - static const Map friendlyNames = { - "[]": "get", - "[]=": "put", - "~": "bitwise_negate", - "==": "equals", - "-": "minus", - "+": "plus", - "*": "multiply", - "/": "divide", - "<": "less", - ">": "greater", - ">=": "greater_equal", - "<=": "less_equal", - "<<": "shift_left", - ">>": "shift_right", - "^": "bitwise_exclusive_or", - "unary-": "unary_minus", - "|": "bitwise_or", - "&": "bitwise_and", - "~/": "truncate_divide", - "%": "modulo" - }; - - Operator(MethodElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph); - - Operator.inherited(MethodElement element, Class enclosingClass, - Library library, PackageGraph packageGraph, {Member originalMember}) - : super.inherited(element, enclosingClass, library, packageGraph, - originalMember: originalMember) { - _isInherited = true; - } - - @override - String get fileName { - var actualName = super.name; - if (friendlyNames.containsKey(actualName)) { - return "operator_${friendlyNames[actualName]}.html"; - } else { - return '$actualName.html'; - } - } - - @override - String get fullyQualifiedName => - '${library.name}.${enclosingElement.name}.${super.name}'; - - @override - bool get isOperator => true; - - @override - String get name { - return 'operator ${super.name}'; - } -} - -class PackageGraph { - PackageGraph.UninitializedPackageGraph( - this.config, this.driver, this.typeSystem, this.sdk, this.hasEmbedderSdk) - : packageMeta = config.topLevelPackageMeta, - session = driver.currentSession { - _packageWarningCounter = PackageWarningCounter(this); - // Make sure the default package exists, even if it has no libraries. - // This can happen for packages that only contain embedder SDKs. - Package.fromPackageMeta(packageMeta, this); - } - - /// Call during initialization to add a library to this [PackageGraph]. - /// - /// Libraries added in this manner are assumed to be part of documented - /// packages, even if includes or embedder.yaml files cause these to - /// span packages. - void addLibraryToGraph(ResolvedLibraryResult result) { - assert(!allLibrariesAdded); - LibraryElement element = result.element; - var packageMeta = PackageMeta.fromElement(element, config); - var lib = - Library._(result, this, Package.fromPackageMeta(packageMeta, this)); - packageMap[packageMeta.name]._libraries.add(lib); - allLibraries[element] = lib; - } - - /// Call during initialization to add a library possibly containing - /// special/non-documented elements to this [PackageGraph]. Must be called - /// after any normal libraries. - void addSpecialLibraryToGraph(ResolvedLibraryResult result) { - allLibrariesAdded = true; - assert(!_localDocumentationBuilt); - findOrCreateLibraryFor(result); - } - - /// Call after all libraries are added. - Future initializePackageGraph() async { - allLibrariesAdded = true; - assert(!_localDocumentationBuilt); - // From here on in, we might find special objects. Initialize the - // specialClasses handler so when we find them, they get added. - specialClasses = SpecialClasses(); - // Go through docs of every ModelElement in package to pre-build the macros - // index. Uses toList() in order to get all the precaching on the stack. - List precacheFutures = precacheLocalDocs().toList(); - for (Future f in precacheFutures) { - await f; - } - _localDocumentationBuilt = true; - - // Scan all model elements to insure that interceptor and other special - // objects are found. - // After the allModelElements traversal to be sure that all packages - // are picked up. - documentedPackages.toList().forEach((package) { - package._libraries.sort((a, b) => compareNatural(a.name, b.name)); - package._libraries.forEach((library) { - library.allClasses.forEach(_addToImplementors); - _extensions.addAll(library.extensions); - }); - }); - _implementors.values.forEach((l) => l.sort()); - allImplementorsAdded = true; - allExtensionsAdded = true; - - // We should have found all special classes by now. - specialClasses.assertSpecials(); - } - - /// Generate a list of futures for any docs that actually require precaching. - Iterable precacheLocalDocs() sync* { - // Prevent reentrancy. - Set precachedElements = Set(); - - Iterable precacheOneElement(ModelElement m) sync* { - for (ModelElement d - in m.documentationFrom.where((d) => d.documentationComment != null)) { - if (needsPrecacheRegExp.hasMatch(d.documentationComment) && - !precachedElements.contains(d)) { - precachedElements.add(d); - yield d._precacheLocalDocs(); - // TopLevelVariables get their documentation from getters and setters, - // so should be precached if either has a template. - if (m is TopLevelVariable) { - precachedElements.add(m); - yield m._precacheLocalDocs(); - } - } - } - } - - for (ModelElement m in allModelElements) { - // Skip if there is a canonicalModelElement somewhere else we can run this - // for. Not the same as allCanonicalModelElements since we need to run - // for any ModelElement that might not have a canonical ModelElement, - // too. - if (m.canonicalModelElement != null && !m.isCanonical) continue; - yield* precacheOneElement(m); - } - } - - // Many ModelElements have the same ModelNode; don't build/cache this data more - // than once for them. - final Map _modelNodes = Map(); - - void _populateModelNodeFor( - Element element, Map compilationUnitMap) { - _modelNodes.putIfAbsent( - element, - () => - ModelNode(utils.getAstNode(element, compilationUnitMap), element)); - } - - ModelNode _getModelNodeFor(Element element) => _modelNodes[element]; - - SpecialClasses specialClasses; - - /// It is safe to cache values derived from the [_implementors] table if this - /// is true. - bool allImplementorsAdded = false; - - /// It is safe to cache values derived from the [_extensions] table if this - /// is true. - bool allExtensionsAdded = false; - - Map> get implementors { - assert(allImplementorsAdded); - return _implementors; - } - - Iterable get extensions { - assert(allExtensionsAdded); - return _extensions; - } - - Map> _findRefElementCache; - - Map> get findRefElementCache { - if (_findRefElementCache == null) { - assert(packageGraph.allLibrariesAdded); - _findRefElementCache = Map(); - for (final modelElement - in utils.filterNonDocumented(packageGraph.allLocalModelElements)) { - _findRefElementCache.putIfAbsent( - modelElement.fullyQualifiedNameWithoutLibrary, () => Set()); - _findRefElementCache.putIfAbsent( - modelElement.fullyQualifiedName, () => Set()); - _findRefElementCache[modelElement.fullyQualifiedName].add(modelElement); - _findRefElementCache[modelElement.fullyQualifiedNameWithoutLibrary] - .add(modelElement); - } - } - return _findRefElementCache; - } - - // All library objects related to this package; a superset of _libraries. - final Map allLibraries = Map(); - - /// Keep track of warnings - PackageWarningCounter _packageWarningCounter; - - /// All ModelElements constructed for this package; a superset of [allModelElements]. - final Map, ModelElement> - _allConstructedModelElements = Map(); - - /// Anything that might be inheritable, place here for later lookup. - final Map, Set> - _allInheritableElements = Map(); - - /// Map of Class.href to a list of classes implementing that class - final Map> _implementors = Map(); - - /// A list of extensions that exist in the package graph. - final List _extensions = []; - - /// PackageMeta for the default package. - final PackageMeta packageMeta; - - /// Name of the default package. - String get defaultPackageName => packageMeta.name; - - /// Dartdoc's configuration flags. - final DartdocOptionContext config; - - Package _defaultPackage; - - Package get defaultPackage { - if (_defaultPackage == null) { - _defaultPackage = Package.fromPackageMeta(packageMeta, this); - } - return _defaultPackage; - } - - final bool hasEmbedderSdk; - - bool get hasFooterVersion => !config.excludeFooterVersion; - - PackageGraph get packageGraph => this; - - /// Map of package name to Package. - final Map packageMap = {}; - - /// TODO(brianwilkerson) Replace the driver with the session. - final AnalysisDriver driver; - final AnalysisSession session; - final Dart2TypeSystem typeSystem; - final DartSdk sdk; - - Map _sdkLibrarySources; - - Map get sdkLibrarySources { - if (_sdkLibrarySources == null) { - _sdkLibrarySources = Map(); - for (SdkLibrary lib in sdk?.sdkLibraries) { - _sdkLibrarySources[sdk.mapDartUri(lib.shortName)] = lib; - } - } - return _sdkLibrarySources; - } - - final Map _macros = {}; - final Map _htmlFragments = {}; - bool allLibrariesAdded = false; - bool _localDocumentationBuilt = false; - - /// Returns true if there's at least one library documented in the package - /// that has the same package path as the library for the given element. - /// Usable as a cross-check for dartdoc's canonicalization to generate - /// warnings for ModelElement.isPublicAndPackageDocumented. - Set _allRootDirs; - - bool packageDocumentedFor(ModelElement element) { - if (_allRootDirs == null) { - _allRootDirs = Set() - ..addAll(publicLibraries.map((l) => l.packageMeta?.resolvedDir)); - } - return (_allRootDirs.contains(element.library.packageMeta?.resolvedDir)); - } - - PackageWarningCounter get packageWarningCounter => _packageWarningCounter; - - final Set> _warnAlreadySeen = Set(); - - void warnOnElement(Warnable warnable, PackageWarning kind, - {String message, - Iterable referredFrom, - Iterable extendedDebug}) { - var newEntry = Tuple3(warnable?.element, kind, message); - if (_warnAlreadySeen.contains(newEntry)) { - return; - } - // Warnings can cause other warnings. Queue them up via the stack but - // don't allow warnings we're already working on to get in there. - _warnAlreadySeen.add(newEntry); - _warnOnElement(warnable, kind, - message: message, - referredFrom: referredFrom, - extendedDebug: extendedDebug); - _warnAlreadySeen.remove(newEntry); - } - - void _warnOnElement(Warnable warnable, PackageWarning kind, - {String message, - Iterable referredFrom, - Iterable extendedDebug}) { - if (warnable != null) { - // This sort of warning is only applicable to top level elements. - if (kind == PackageWarning.ambiguousReexport) { - while (warnable.enclosingElement is! Library && - warnable.enclosingElement != null) { - warnable = warnable.enclosingElement; - } - } - } else { - // If we don't have an element, we need a message to disambiguate. - assert(message != null); - } - if (_packageWarningCounter.hasWarning(warnable, kind, message)) { - return; - } - // Some kinds of warnings it is OK to drop if we're not documenting them. - // TODO(jcollins-g): drop this and use new flag system instead. - if (warnable != null && - skipWarningIfNotDocumentedFor.contains(kind) && - !warnable.isDocumented) { - return; - } - // Elements that are part of the Dart SDK can have colons in their FQNs. - // This confuses IntelliJ and makes it so it can't link to the location - // of the error in the console window, so separate out the library from - // the path. - // TODO(jcollins-g): What about messages that may include colons? Substituting - // them out doesn't work as well there since it might confuse - // the user, yet we still want IntelliJ to link properly. - final warnableName = _safeWarnableName(warnable); - - String warnablePrefix = 'from'; - String referredFromPrefix = 'referred to by'; - String warningMessage; - switch (kind) { - case PackageWarning.noCanonicalFound: - // Fix these warnings by adding libraries with --include, or by using - // --auto-include-dependencies. - // TODO(jcollins-g): pipeline references through linkedName for error - // messages and warn for non-public canonicalization - // errors. - warningMessage = - "no canonical library found for ${warnableName}, not linking"; - break; - case PackageWarning.ambiguousReexport: - // Fix these warnings by adding the original library exporting the - // symbol with --include, by using --auto-include-dependencies, - // or by using --exclude to hide one of the libraries involved - warningMessage = - "ambiguous reexport of ${warnableName}, canonicalization candidates: ${message}"; - break; - case PackageWarning.noLibraryLevelDocs: - warningMessage = - "${warnable.fullyQualifiedName} has no library level documentation comments"; - break; - case PackageWarning.ambiguousDocReference: - warningMessage = "ambiguous doc reference ${message}"; - break; - case PackageWarning.ignoredCanonicalFor: - warningMessage = - "library says it is {@canonicalFor ${message}} but ${message} can't be canonical there"; - break; - case PackageWarning.packageOrderGivesMissingPackageName: - warningMessage = - "--package-order gives invalid package name: '${message}'"; - break; - case PackageWarning.reexportedPrivateApiAcrossPackages: - warningMessage = - "private API of ${message} is reexported by libraries in other packages: "; - break; - case PackageWarning.notImplemented: - warningMessage = message; - break; - case PackageWarning.unresolvedDocReference: - warningMessage = "unresolved doc reference [${message}]"; - referredFromPrefix = 'in documentation inherited from'; - break; - case PackageWarning.unknownMacro: - warningMessage = "undefined macro [${message}]"; - break; - case PackageWarning.unknownHtmlFragment: - warningMessage = "undefined HTML fragment identifier [${message}]"; - break; - case PackageWarning.brokenLink: - warningMessage = 'dartdoc generated a broken link to: ${message}'; - warnablePrefix = 'to element'; - referredFromPrefix = 'linked to from'; - break; - case PackageWarning.orphanedFile: - warningMessage = 'dartdoc generated a file orphan: ${message}'; - break; - case PackageWarning.unknownFile: - warningMessage = - 'dartdoc detected an unknown file in the doc tree: ${message}'; - break; - case PackageWarning.missingFromSearchIndex: - warningMessage = - 'dartdoc generated a file not in the search index: ${message}'; - break; - case PackageWarning.typeAsHtml: - // The message for this warning can contain many punctuation and other symbols, - // so bracket with a triple quote for defense. - warningMessage = 'generic type handled as HTML: """${message}"""'; - break; - case PackageWarning.invalidParameter: - warningMessage = 'invalid parameter to dartdoc directive: ${message}'; - break; - case PackageWarning.toolError: - warningMessage = 'tool execution failed: ${message}'; - break; - case PackageWarning.deprecated: - warningMessage = 'deprecated dartdoc usage: ${message}'; - break; - case PackageWarning.unresolvedExport: - warningMessage = 'unresolved export uri: ${message}'; - break; - } - - List messageParts = [warningMessage]; - if (warnable != null) { - messageParts.add("$warnablePrefix $warnableName: ${warnable.location}"); - } - if (referredFrom != null) { - for (Locatable referral in referredFrom) { - if (referral != warnable) { - var referredFromStrings = _safeWarnableName(referral); - messageParts.add( - "$referredFromPrefix $referredFromStrings: ${referral.location}"); - } - } - } - if (config.verboseWarnings && extendedDebug != null) { - messageParts.addAll(extendedDebug.map((s) => " $s")); - } - String fullMessage; - if (messageParts.length <= 2) { - fullMessage = messageParts.join(', '); - } else { - fullMessage = messageParts.join('\n '); - } - - packageWarningCounter.addWarning(warnable, kind, message, fullMessage); - } - - String _safeWarnableName(Locatable locatable) { - if (locatable == null) { - return ''; - } - - return locatable.fullyQualifiedName.replaceFirst(':', '-'); - } - - List get packages => packageMap.values.toList(); - - List _publicPackages; - - List get publicPackages { - if (_publicPackages == null) { - assert(allLibrariesAdded); - // Help the user if they pass us a package that doesn't exist. - for (String packageName in config.packageOrder) { - if (!packages.map((p) => p.name).contains(packageName)) { - warnOnElement( - null, PackageWarning.packageOrderGivesMissingPackageName, - message: - "${packageName}, packages: ${packages.map((p) => p.name).join(',')}"); - } - } - _publicPackages = packages.where((p) => p.isPublic).toList()..sort(); - } - return _publicPackages; - } - - /// Local packages are to be documented locally vs. remote or not at all. - List get localPackages => - publicPackages.where((p) => p.isLocal).toList(); - - /// Documented packages are documented somewhere (local or remote). - Iterable get documentedPackages => - packages.where((p) => p.documentedWhere != DocumentLocation.missing); - - Map> _libraryElementReexportedBy = Map(); - - /// Prevent cycles from breaking our stack. - Set> _reexportsTagged = Set(); - - void _tagReexportsFor( - final Library topLevelLibrary, final LibraryElement libraryElement, - [ExportElement lastExportedElement]) { - Tuple2 key = - Tuple2(topLevelLibrary, libraryElement); - if (_reexportsTagged.contains(key)) { - return; - } - _reexportsTagged.add(key); - if (libraryElement == null) { - // The first call to _tagReexportFor should not have a null libraryElement. - assert(lastExportedElement != null); - warnOnElement( - findButDoNotCreateLibraryFor(lastExportedElement.enclosingElement), - PackageWarning.unresolvedExport, - message: '"${lastExportedElement.uri}"', - referredFrom: [topLevelLibrary]); - return; - } - _libraryElementReexportedBy.putIfAbsent(libraryElement, () => Set()); - _libraryElementReexportedBy[libraryElement].add(topLevelLibrary); - for (ExportElement exportedElement in libraryElement.exports) { - _tagReexportsFor( - topLevelLibrary, exportedElement.exportedLibrary, exportedElement); - } - } - - int _lastSizeOfAllLibraries = 0; - - Map> get libraryElementReexportedBy { - // Table must be reset if we're still in the middle of adding libraries. - if (allLibraries.keys.length != _lastSizeOfAllLibraries) { - _lastSizeOfAllLibraries = allLibraries.keys.length; - _libraryElementReexportedBy = Map>(); - _reexportsTagged = Set(); - for (Library library in publicLibraries) { - _tagReexportsFor(library, library.element); - } - } - return _libraryElementReexportedBy; - } - - /// A lookup index for hrefs to allow warnings to indicate where a broken - /// link or orphaned file may have come from. Not cached because - /// [ModelElement]s can be created at any time and we're basing this - /// on more than just [allLocalModelElements] to make the error messages - /// comprehensive. - Map> get allHrefs { - Map> hrefMap = Map(); - // TODO(jcollins-g ): handle calculating hrefs causing new elements better - // than toList(). - for (ModelElement modelElement - in _allConstructedModelElements.values.toList()) { - // Technically speaking we should be able to use canonical model elements - // only here, but since the warnings that depend on this debug - // canonicalization problems, don't limit ourselves in case an href is - // generated for something non-canonical. - if (modelElement is Dynamic) continue; - // TODO: see [Accessor.enclosingCombo] - if (modelElement is Accessor) continue; - if (modelElement.href == null) continue; - hrefMap.putIfAbsent(modelElement.href, () => Set()); - hrefMap[modelElement.href].add(modelElement); - } - for (Package package in packageMap.values) { - for (Library library in package.libraries) { - if (library.href == null) continue; - hrefMap.putIfAbsent(library.href, () => Set()); - hrefMap[library.href].add(library); - } - } - return hrefMap; - } - - void _addToImplementors(Class c) { - assert(!allImplementorsAdded); - _implementors.putIfAbsent(c.href, () => []); - void _checkAndAddClass(Class key, Class implClass) { - _implementors.putIfAbsent(key.href, () => []); - List list = _implementors[key.href]; - - if (!list.any((l) => l.element == c.element)) { - list.add(implClass); - } - } - - if (c._mixins.isNotEmpty) { - c._mixins.forEach((t) { - _checkAndAddClass(t.element, c); - }); - } - if (c.supertype != null) { - _checkAndAddClass(c.supertype.element, c); - } - if (c.interfaces.isNotEmpty) { - c.interfaces.forEach((t) { - _checkAndAddClass(t.element, c); - }); - } - } - - List get libraries => - packages.expand((p) => p.libraries).toList()..sort(); - - List _publicLibraries; - - Iterable get publicLibraries { - if (_publicLibraries == null) { - assert(allLibrariesAdded); - _publicLibraries = utils.filterNonPublic(libraries).toList(); - } - return _publicLibraries; - } - - List _localLibraries; - - Iterable get localLibraries { - if (_localLibraries == null) { - assert(allLibrariesAdded); - _localLibraries = localPackages.expand((p) => p.libraries).toList() - ..sort(); - } - return _localLibraries; - } - - List _localPublicLibraries; - - Iterable get localPublicLibraries { - if (_localPublicLibraries == null) { - assert(allLibrariesAdded); - _localPublicLibraries = utils.filterNonPublic(localLibraries).toList(); - } - return _localPublicLibraries; - } - - Set _inheritThrough; - - /// Return the set of [Class]es objects should inherit through if they - /// show up in the inheritance chain. Do not call before interceptorElement is - /// found. Add classes here if they are similar to Interceptor in that they - /// are to be ignored even when they are the implementors of [Inheritable]s, - /// and the class these inherit from should instead claim implementation. - Set get inheritThrough { - if (_inheritThrough == null) { - _inheritThrough = Set(); - _inheritThrough.add(specialClasses[SpecialClass.interceptor]); - } - return _inheritThrough; - } - - Set _invisibleAnnotations; - - /// Returns the set of [Class] objects that are similar to pragma - /// in that we should never count them as documentable annotations. - Set get invisibleAnnotations { - if (_invisibleAnnotations == null) { - _invisibleAnnotations = Set(); - _invisibleAnnotations.add(specialClasses[SpecialClass.pragma]); - } - return _invisibleAnnotations; - } - - @override - String toString() => 'PackageGraph built from ${defaultPackage.name}'; - - final Map _canonicalLibraryFor = Map(); - - /// Tries to find a top level library that references this element. - Library findCanonicalLibraryFor(Element e) { - assert(allLibrariesAdded); - Element searchElement = e; - if (e is PropertyAccessorElement) { - searchElement = e.variable; - } - if (e is GenericFunctionTypeElement) { - searchElement = e.enclosingElement; - } - - if (_canonicalLibraryFor.containsKey(e)) { - return _canonicalLibraryFor[e]; - } - _canonicalLibraryFor[e] = null; - for (Library library in publicLibraries) { - if (library.modelElementsMap.containsKey(searchElement)) { - for (ModelElement modelElement - in library.modelElementsMap[searchElement]) { - if (modelElement.isCanonical) { - _canonicalLibraryFor[e] = library; - break; - } - } - } - } - return _canonicalLibraryFor[e]; - } - - // TODO(jcollins-g): Revise when dart-lang/sdk#29600 is fixed. - static Element getBasestElement(Element possibleMember) { - Element element = possibleMember; - while (element is Member) { - element = (element as Member).baseElement; - } - return element; - } - - /// Tries to find a canonical ModelElement for this element. If we know - /// this element is related to a particular class, pass preferredClass to - /// disambiguate. - /// - /// This doesn't know anything about [PackageGraph.inheritThrough] and probably - /// shouldn't, so using it with [Inheritable]s without special casing is - /// not advised. - ModelElement findCanonicalModelElementFor(Element e, - {Container preferredClass}) { - assert(allLibrariesAdded); - Library lib = findCanonicalLibraryFor(e); - if (preferredClass != null && preferredClass is Container) { - Container canonicalClass = - findCanonicalModelElementFor(preferredClass.element); - if (canonicalClass != null) preferredClass = canonicalClass; - } - if (lib == null && preferredClass != null) { - lib = findCanonicalLibraryFor(preferredClass.element); - } - ModelElement modelElement; - // For elements defined in extensions, they are canonical. - if (e?.enclosingElement is ExtensionElement) { - lib ??= Library(e.enclosingElement.library, packageGraph); - // (TODO:keertip) Find a better way to exclude members of extensions - // when libraries are specified using the "--include" flag - if (lib?.isDocumented == true) { - return ModelElement.from(e, lib, packageGraph); - } - } - // TODO(jcollins-g): Special cases are pretty large here. Refactor to split - // out into helpers. - // TODO(jcollins-g): The data structures should be changed to eliminate guesswork - // with member elements. - if (e is ClassMemberElement || e is PropertyAccessorElement) { - if (e is Member) e = getBasestElement(e); - Set candidates = Set(); - Tuple2 iKey = Tuple2(e, lib); - Tuple4 key = - Tuple4(e, lib, null, null); - Tuple4 keyWithClass = - Tuple4(e, lib, preferredClass, null); - if (_allConstructedModelElements.containsKey(key)) { - candidates.add(_allConstructedModelElements[key]); - } - if (_allConstructedModelElements.containsKey(keyWithClass)) { - candidates.add(_allConstructedModelElements[keyWithClass]); - } - if (candidates.isEmpty && _allInheritableElements.containsKey(iKey)) { - candidates.addAll( - _allInheritableElements[iKey].where((me) => me.isCanonical)); - } - Class canonicalClass = findCanonicalModelElementFor(e.enclosingElement); - if (canonicalClass != null) { - candidates.addAll(canonicalClass.allCanonicalModelElements.where((m) { - if (m.element == e) return true; - return false; - })); - } - Set matches = Set() - ..addAll(candidates.where((me) => me.isCanonical)); - - // It's possible to find accessors but no combos. Be sure that if we - // have Accessors, we find their combos too. - if (matches.any((me) => me is Accessor)) { - List combos = - matches.whereType().map((a) => a.enclosingCombo).toList(); - matches.addAll(combos); - assert(combos.every((c) => c.isCanonical)); - } - - // This is for situations where multiple classes may actually be canonical - // for an inherited element whose defining Class is not canonical. - if (matches.length > 1 && - preferredClass != null && - preferredClass is Class) { - // Search for matches inside our superchain. - List superChain = preferredClass.superChain - .map((et) => et.element) - .cast() - .toList(); - superChain.add(preferredClass); - matches.removeWhere((me) => - !superChain.contains((me as EnclosedElement).enclosingElement)); - // Assumed all matches are EnclosedElement because we've been told about a - // preferredClass. - Set enclosingElements = Set() - ..addAll(matches - .map((me) => (me as EnclosedElement).enclosingElement as Class)); - for (Class c in superChain.reversed) { - if (enclosingElements.contains(c)) { - matches.removeWhere( - (me) => (me as EnclosedElement).enclosingElement != c); - } - if (matches.length <= 1) break; - } - } - - // Prefer a GetterSetterCombo to Accessors. - if (matches.any((me) => me is GetterSetterCombo)) { - matches.removeWhere((me) => me is Accessor); - } - - assert(matches.length <= 1); - if (matches.isNotEmpty) { - modelElement = matches.first; - } - } else { - if (lib != null) { - Accessor getter; - Accessor setter; - if (e is PropertyInducingElement) { - if (e.getter != null) { - getter = ModelElement.from(e.getter, lib, packageGraph); - } - if (e.setter != null) { - setter = ModelElement.from(e.setter, lib, packageGraph); - } - } - modelElement = ModelElement.from(e, lib, packageGraph, - getter: getter, setter: setter); - } - assert(modelElement is! Inheritable); - if (modelElement != null && !modelElement.isCanonical) { - modelElement = null; - } - } - // Prefer Fields. - if (e is PropertyAccessorElement && modelElement is Accessor) { - modelElement = (modelElement as Accessor).enclosingCombo; - } - return modelElement; - } - - /// This is used when we might need a Library object that isn't actually - /// a documentation entry point (for elements that have no Library within the - /// set of canonical Libraries). - Library findButDoNotCreateLibraryFor(Element e) { - // This is just a cache to avoid creating lots of libraries over and over. - if (allLibraries.containsKey(e.library)) { - return allLibraries[e.library]; - } - return null; - } - - /// This is used when we might need a Library object that isn't actually - /// a documentation entry point (for elements that have no Library within the - /// set of canonical Libraries). - Library findOrCreateLibraryFor(ResolvedLibraryResult result) { - // This is just a cache to avoid creating lots of libraries over and over. - if (allLibraries.containsKey(result.element.library)) { - return allLibraries[result.element.library]; - } - // can be null if e is for dynamic - if (result.element.library == null) { - return null; - } - Library foundLibrary = Library._( - result, - this, - Package.fromPackageMeta( - PackageMeta.fromElement(result.element.library, config), - packageGraph)); - allLibraries[result.element.library] = foundLibrary; - return foundLibrary; - } - - List _allModelElements; - - Iterable get allModelElements { - assert(allLibrariesAdded); - if (_allModelElements == null) { - _allModelElements = []; - Set packagesToDo = packages.toSet(); - Set completedPackages = Set(); - while (packagesToDo.length > completedPackages.length) { - packagesToDo.difference(completedPackages).forEach((Package p) { - Set librariesToDo = p.allLibraries.toSet(); - Set completedLibraries = Set(); - while (librariesToDo.length > completedLibraries.length) { - librariesToDo - .difference(completedLibraries) - .forEach((Library library) { - _allModelElements.addAll(library.allModelElements); - completedLibraries.add(library); - }); - librariesToDo.addAll(p.allLibraries); - } - completedPackages.add(p); - }); - packagesToDo.addAll(packages); - } - } - return _allModelElements; - } - - List _allLocalModelElements; - - Iterable get allLocalModelElements { - assert(allLibrariesAdded); - if (_allLocalModelElements == null) { - _allLocalModelElements = []; - this.localLibraries.forEach((library) { - _allLocalModelElements.addAll(library.allModelElements); - }); - } - return _allLocalModelElements; - } - - List _allCanonicalModelElements; - - Iterable get allCanonicalModelElements { - return (_allCanonicalModelElements ??= - allLocalModelElements.where((e) => e.isCanonical).toList()); - } - - String getMacro(String name) { - assert(_localDocumentationBuilt); - return _macros[name]; - } - - void _addMacro(String name, String content) { - assert(!_localDocumentationBuilt); - _macros[name] = content; - } - - String getHtmlFragment(String name) { - assert(_localDocumentationBuilt); - return _htmlFragments[name]; - } - - void _addHtmlFragment(String name, String content) { - assert(!_localDocumentationBuilt); - _htmlFragments[name] = content; - } -} - -/// A set of [Class]es, [Enum]s, [TopLevelVariable]s, [ModelFunction]s, -/// [Property]s, and [Typedef]s, possibly initialized after construction by -/// accessing private member variables. Do not call any methods or members -/// excepting [name] and the private Lists below before finishing initialization -/// of a [TopLevelContainer]. -abstract class TopLevelContainer implements Nameable { - List _classes; - List _extensions; - List _enums; - List _mixins; - List _exceptions; - List _constants; - List _properties; - List _functions; - List _typedefs; - - Iterable get classes => _classes; - - Iterable get extensions => _extensions; - - Iterable get enums => _enums; - - Iterable get mixins => _mixins; - - Iterable get exceptions => _exceptions; - - Iterable get constants => _constants; - - Iterable get properties => _properties; - - Iterable get functions => _functions; - - Iterable get typedefs => _typedefs; - - bool get hasPublicClasses => publicClasses.isNotEmpty; - - bool get hasPublicExtensions => publicExtensions.isNotEmpty; - - bool get hasPublicConstants => publicConstants.isNotEmpty; - - bool get hasPublicEnums => publicEnums.isNotEmpty; - - bool get hasPublicExceptions => publicExceptions.isNotEmpty; - - bool get hasPublicFunctions => publicFunctions.isNotEmpty; - - bool get hasPublicMixins => publicMixins.isNotEmpty; - - bool get hasPublicProperties => publicProperties.isNotEmpty; - - bool get hasPublicTypedefs => publicTypedefs.isNotEmpty; - - Iterable get publicClasses => utils.filterNonPublic(classes); - - Iterable get publicExtensions => utils.filterNonPublic(extensions); - - Iterable get publicConstants => - utils.filterNonPublic(constants); - - Iterable get publicEnums => utils.filterNonPublic(enums); - - Iterable get publicExceptions => utils.filterNonPublic(exceptions); - - Iterable get publicFunctions => - utils.filterNonPublic(functions); - - Iterable get publicMixins => utils.filterNonPublic(mixins); - - Iterable get publicProperties => - utils.filterNonPublic(properties); - - Iterable get publicTypedefs => utils.filterNonPublic(typedefs); -} - -/// A set of libraries, initialized after construction by accessing [_libraries]. -/// Do not cache return values of any methods or members excepting [_libraries] -/// and [name] before finishing initialization of a [LibraryContainer]. -abstract class LibraryContainer - implements Nameable, Comparable { - final List _libraries = []; - - List get libraries => _libraries; - - PackageGraph get packageGraph; - - Iterable get publicLibraries => utils.filterNonPublic(libraries); - - bool get hasPublicLibraries => publicLibraries.isNotEmpty; - - /// The name of the container or object that this LibraryContainer is a part - /// of. Used for sorting in [containerOrder]. - String get enclosingName; - - /// Order by which this container should be sorted. - List get containerOrder; - - /// Sorting key. [containerOrder] should contain these. - String get sortKey => name; - - /// Does this container represent the SDK? This can be false for containers - /// that only represent a part of the SDK. - bool get isSdk => false; - - /// Returns: - /// -1 if this container is listed in [containerOrder]. - /// 0 if this container is named the same as the [enclosingName]. - /// 1 if this container represents the SDK. - /// 2 if this group has a name that contains the name [enclosingName]. - /// 3 otherwise. - int get _group { - if (containerOrder.contains(sortKey)) return -1; - if (equalsIgnoreAsciiCase(sortKey, enclosingName)) return 0; - if (isSdk) return 1; - if (sortKey.toLowerCase().contains(enclosingName.toLowerCase())) return 2; - return 3; - } - - @override - int compareTo(LibraryContainer other) { - if (_group == other._group) { - if (_group == -1) { - return Comparable.compare(containerOrder.indexOf(sortKey), - containerOrder.indexOf(other.sortKey)); - } else { - return sortKey.toLowerCase().compareTo(other.sortKey.toLowerCase()); - } - } - return Comparable.compare(_group, other._group); - } -} - -abstract class MarkdownFileDocumentation - implements Documentable, Canonicalization { - DocumentLocation get documentedWhere; - - @override - String get documentation => documentationFile?.contents; - - Documentation __documentation; - - Documentation get _documentation { - if (__documentation != null) return __documentation; - __documentation = Documentation.forElement(this); - return __documentation; - } - - @override - String get documentationAsHtml => _documentation.asHtml; - - @override - bool get hasDocumentation => - documentationFile != null && documentationFile.contents.isNotEmpty; - - @override - bool get hasExtendedDocumentation => - documentation != null && documentation.isNotEmpty; - - @override - bool get isDocumented; - - @override - String get oneLineDoc => __documentation.asOneLiner; - - FileContents get documentationFile; - - @override - String get location => path.toUri(documentationFile.file.path).toString(); - - @override - Set get locationPieces => Set.from([location]); -} - -/// A category is a subcategory of a package, containing libraries tagged -/// with a @category identifier. -class Category extends Nameable - with - Warnable, - Locatable, - Canonicalization, - MarkdownFileDocumentation, - LibraryContainer, - TopLevelContainer, - Indexable - implements Documentable { - /// All libraries in [libraries] must come from [package]. - @override - Package package; - final String _name; - @override - DartdocOptionContext config; - final Set _allItems = Set(); - - Category(this._name, this.package, this.config) { - _enums = []; - _exceptions = []; - _classes = []; - _constants = []; - _properties = []; - _functions = []; - _mixins = []; - _typedefs = []; - _extensions = []; - } - - void addItem(Categorization c) { - if (_allItems.contains(c)) return; - _allItems.add(c); - if (c is Library) { - _libraries.add(c); - } else if (c is Mixin) { - _mixins.add(c); - } else if (c is Enum) { - _enums.add(c); - } else if (c is Class) { - if (c.isErrorOrException) { - _exceptions.add(c); - } else { - _classes.add(c); - } - } else if (c is TopLevelVariable) { - if (c.isConst) { - _constants.add(c); - } else { - _properties.add(c); - } - } else if (c is ModelFunction) { - _functions.add(c); - } else if (c is Typedef) { - _typedefs.add(c); - } else if (c is Extension) { - _extensions.add(c); - } else { - throw UnimplementedError("Unrecognized element"); - } - } - - @override - // TODO(jcollins-g): make [Category] a [Warnable]? - Warnable get enclosingElement => null; - - @override - Element get element => null; - - @override - String get name => categoryDefinition?.displayName ?? _name; - - @override - String get sortKey => _name; - - @override - List get containerOrder => config.categoryOrder; - - @override - String get enclosingName => package.name; - - @override - PackageGraph get packageGraph => package.packageGraph; - - @override - Library get canonicalLibrary => null; - - @override - List get documentationFrom => [this]; - - @override - DocumentLocation get documentedWhere => package.documentedWhere; - - bool _isDocumented; - - @override - bool get isDocumented { - if (_isDocumented == null) { - _isDocumented = documentedWhere != DocumentLocation.missing && - documentationFile != null; - } - return _isDocumented; - } - - @override - String get fullyQualifiedName => name; - - @override - String get href => - isCanonical ? '${package.baseHref}topics/${name}-topic.html' : null; - - String get linkedName { - String unbrokenCategoryName = name.replaceAll(' ', ' '); - if (isDocumented) { - return '$unbrokenCategoryName'; - } else { - return unbrokenCategoryName; - } - } - - String _categoryNumberClass; - - /// The position in the container order for this category. - String get categoryNumberClass { - if (_categoryNumberClass == null) { - _categoryNumberClass = "cp-${package.categories.indexOf(this)}"; - } - return _categoryNumberClass; - } - - /// Category name used in template as part of the class. - String get spanClass => name.split(' ').join('-').toLowerCase(); - - CategoryDefinition get categoryDefinition => - config.categories.categoryDefinitions[sortKey]; - - @override - bool get isCanonical => categoryDefinition != null; - - @override - String get kind => 'Topic'; - - FileContents _documentationFile; - - @override - FileContents get documentationFile { - if (_documentationFile == null) { - if (categoryDefinition?.documentationMarkdown != null) { - _documentationFile = - FileContents(File(categoryDefinition.documentationMarkdown)); - } - } - return _documentationFile; - } - - @override - void warn(PackageWarning kind, - {String message, - Iterable referredFrom, - Iterable extendedDebug}) { - packageGraph.warnOnElement(this, kind, - message: message, - referredFrom: referredFrom, - extendedDebug: extendedDebug); - } -} - -/// For a given package, indicate with this enum whether it should be documented -/// [local]ly, whether we should treat the package as [missing] and any references -/// to it made canonical to this package, or [remote], indicating that -/// we can build hrefs to an external source. -enum DocumentLocation { - local, - missing, - remote, -} - -/// A [LibraryContainer] that contains [Library] objects related to a particular -/// package. -class Package extends LibraryContainer - with Nameable, Locatable, Canonicalization, Warnable - implements Privacy, Documentable { - String _name; - PackageGraph _packageGraph; - - final Map _nameToCategory = {}; - - // Creates a package, if necessary, and adds it to the [packageGraph]. - factory Package.fromPackageMeta( - PackageMeta packageMeta, PackageGraph packageGraph) { - String packageName = packageMeta.name; - - bool expectNonLocal = false; - - if (!packageGraph.packageMap.containsKey(packageName) && - packageGraph.allLibrariesAdded) expectNonLocal = true; - packageGraph.packageMap.putIfAbsent( - packageName, () => Package._(packageName, packageGraph, packageMeta)); - // Verify that we don't somehow decide to document locally a package picked - // up after all documented libraries are added, because that breaks the - // assumption that we've picked up all documented libraries and packages - // before allLibrariesAdded is true. - assert( - !(expectNonLocal && - packageGraph.packageMap[packageName].documentedWhere == - DocumentLocation.local), - 'Found more libraries to document after allLibrariesAdded was set to true'); - return packageGraph.packageMap[packageName]; - } - - Package._(this._name, this._packageGraph, this._packageMeta); - - @override - bool get isCanonical => true; - - @override - Library get canonicalLibrary => null; - - /// Number of times we have invoked a tool for this package. - int toolInvocationIndex = 0; - - // The animation IDs that have already been used, indexed by the [href] of the - // object that contains them. - Map> usedAnimationIdsByHref = {}; - - /// Pieces of the location split by [locationSplitter] (removing package: and - /// slashes). - @override - Set get locationPieces => Set(); - - final Set _allLibraries = Set(); - - bool get hasHomepage => - packageMeta.homepage != null && packageMeta.homepage.isNotEmpty; - - String get homepage => packageMeta.homepage; - - String get kind => (isSdk) ? 'SDK' : 'package'; - - @override - List get documentationFrom => [this]; - - /// Returns all libraries added to this package. May include non-documented - /// libraries, but is not guaranteed to include a complete list of - /// non-documented libraries unless they are all referenced by documented ones. - /// Not sorted. - Set get allLibraries => _allLibraries; - - /// Return true if the code has defined non-default categories for libraries - /// in this package. - bool get hasCategories => categories.isNotEmpty; - - LibraryContainer get defaultCategory => nameToCategory[null]; - - String _documentationAsHtml; - - @override - String get documentationAsHtml { - if (_documentationAsHtml != null) return _documentationAsHtml; - _documentationAsHtml = Documentation.forElement(this).asHtml; - - return _documentationAsHtml; - } - - @override - String get documentation { - return hasDocumentationFile ? documentationFile.contents : null; - } - - @override - bool get hasDocumentation => - documentationFile != null && documentationFile.contents.isNotEmpty; - - @override - bool get hasExtendedDocumentation => documentation.isNotEmpty; - - // TODO: Clients should use [documentationFile] so they can act differently on - // plain text or markdown. - bool get hasDocumentationFile => documentationFile != null; - - FileContents get documentationFile => packageMeta.getReadmeContents(); - - @override - String get oneLineDoc => ''; - - @override - bool get isDocumented => - isFirstPackage || documentedWhere != DocumentLocation.missing; - - @override - Warnable get enclosingElement => null; - - bool _isPublic; - - @override - bool get isPublic { - if (_isPublic == null) _isPublic = libraries.any((l) => l.isPublic); - return _isPublic; - } - - bool _isLocal; - - /// Return true if this is the default package, this is part of an embedder SDK, - /// or if [config.autoIncludeDependencies] is true -- but only if the package - /// was not excluded on the command line. - bool get isLocal { - if (_isLocal == null) { - _isLocal = (packageMeta == packageGraph.packageMeta || - packageGraph.hasEmbedderSdk && packageMeta.isSdk || - packageGraph.config.autoIncludeDependencies) && - !packageGraph.config.isPackageExcluded(name); - } - return _isLocal; - } - - DocumentLocation get documentedWhere { - if (isLocal) { - if (isPublic) { - return DocumentLocation.local; - } else { - // Possible if excludes result in a "documented" package not having - // any actual documentation. - return DocumentLocation.missing; - } - } else { - if (config.linkToRemote && config.linkToUrl.isNotEmpty && isPublic) { - return DocumentLocation.remote; - } else { - return DocumentLocation.missing; - } - } - } - - @override - String get enclosingName => packageGraph.defaultPackageName; - - @override - String get fullyQualifiedName => 'package:$name'; - - String _baseHref; - - String get baseHref { - if (_baseHref == null) { - if (documentedWhere == DocumentLocation.remote) { - _baseHref = - config.linkToUrl.replaceAllMapped(substituteNameVersion, (m) { - switch (m.group(1)) { - // Return the prerelease tag of the release if a prerelease, - // or 'stable' otherwise. Mostly coded around - // the Dart SDK's use of dev/stable, but theoretically applicable - // elsewhere. - case 'b': - { - Version version = Version.parse(packageMeta.version); - return version.isPreRelease - ? version.preRelease.first - : 'stable'; - } - case 'n': - return name; - // The full version string of the package. - case 'v': - return packageMeta.version; - default: - assert(false, 'Unsupported case: ${m.group(1)}'); - return null; - } - }); - if (!_baseHref.endsWith('/')) _baseHref = '${_baseHref}/'; - } else { - _baseHref = ''; - } - } - return _baseHref; - } - - @override - String get href => '${baseHref}index.html'; - - @override - String get location => path.toUri(packageMeta.resolvedDir).toString(); - - @override - String get name => _name; - - @override - Package get package => this; - - @override - PackageGraph get packageGraph => _packageGraph; - - // Workaround for mustache4dart issue where templates do not recognize - // inherited properties as being in-context. - @override - Iterable get publicLibraries { - assert(libraries.every((l) => l.packageMeta == _packageMeta)); - return super.publicLibraries; - } - - /// A map of category name to the category itself. - Map get nameToCategory { - if (_nameToCategory.isEmpty) { - Category categoryFor(String category) { - _nameToCategory.putIfAbsent( - category, () => Category(category, this, config)); - return _nameToCategory[category]; - } - - _nameToCategory[null] = Category(null, this, config); - for (Categorization c in libraries.expand( - (l) => l.allCanonicalModelElements.whereType())) { - for (String category in c.categoryNames) { - categoryFor(category).addItem(c); - } - } - } - return _nameToCategory; - } - - List _categories; - - List get categories { - if (_categories == null) { - _categories = nameToCategory.values.where((c) => c.name != null).toList() - ..sort(); - } - return _categories; - } - - Iterable get categoriesWithPublicLibraries => - categories.where((c) => c.publicLibraries.isNotEmpty); - - Iterable get documentedCategories => - categories.where((c) => c.isDocumented); - - bool get hasDocumentedCategories => documentedCategories.isNotEmpty; - - DartdocOptionContext _config; - - @override - DartdocOptionContext get config { - if (_config == null) { - _config = DartdocOptionContext.fromContext( - packageGraph.config, Directory(packagePath)); - } - return _config; - } - - /// Is this the package at the top of the list? We display the first - /// package specially (with "Libraries" rather than the package name). - bool get isFirstPackage => - packageGraph.localPackages.isNotEmpty && - identical(packageGraph.localPackages.first, this); - - @override - bool get isSdk => packageMeta.isSdk; - - String _packagePath; - - String get packagePath { - if (_packagePath == null) { - _packagePath = path.canonicalize(packageMeta.dir.path); - } - return _packagePath; - } - - String get version => packageMeta.version ?? '0.0.0-unknown'; - - @override - void warn(PackageWarning kind, - {String message, - Iterable referredFrom, - Iterable extendedDebug}) { - packageGraph.warnOnElement(this, kind, - message: message, - referredFrom: referredFrom, - extendedDebug: extendedDebug); - } - - final PackageMeta _packageMeta; - - PackageMeta get packageMeta => _packageMeta; - - @override - Element get element => null; - - @override - List get containerOrder => config.packageOrder; -} - -class Parameter extends ModelElement implements EnclosedElement { - Parameter( - ParameterElement element, Library library, PackageGraph packageGraph, - {Member originalMember}) - : super(element, library, packageGraph, originalMember); - - String get defaultValue { - if (!hasDefaultValue) return null; - return _parameter.defaultValueCode; - } - - @override - ModelElement get enclosingElement => (_parameter.enclosingElement != null) - ? ModelElement.from(_parameter.enclosingElement, library, packageGraph) - : null; - - bool get hasDefaultValue { - return _parameter.defaultValueCode != null && - _parameter.defaultValueCode.isNotEmpty; - } - - @override - String get href { - throw StateError('href not implemented for parameters'); - } - - @override - String get htmlId { - if (_parameter.enclosingElement != null) { - String enclosingName = _parameter.enclosingElement.name; - if (_parameter.enclosingElement is GenericFunctionTypeElement) { - // TODO(jcollins-g): Drop when GenericFunctionTypeElement populates name. - // Also, allowing null here is allowed as a workaround for - // dart-lang/sdk#32005. - for (Element e = _parameter.enclosingElement; - e.enclosingElement != null; - e = e.enclosingElement) { - enclosingName = e.name; - if (enclosingName != null && enclosingName.isNotEmpty) break; - } - } - return '${enclosingName}-param-${name}'; - } else { - return 'param-${name}'; - } - } - - @override - int get hashCode => _element == null ? 0 : _element.hashCode; - - @override - bool operator ==(Object object) => - object is Parameter && (_parameter.type == object._parameter.type); - - bool get isCovariant => _parameter.isCovariant; - - bool get isOptional => _parameter.isOptional; - - bool get isOptionalNamed => _parameter.isNamed; - - bool get isOptionalPositional => _parameter.isOptionalPositional; - - @override - String get kind => 'parameter'; - - ParameterElement get _parameter => element as ParameterElement; -} - -abstract class SourceCodeMixin implements Documentable { - ModelNode get modelNode; - - CharacterLocation get characterLocation; - - Element get element; - - bool get hasSourceCode => config.includeSource && sourceCode.isNotEmpty; - - Library get library; - - String _sourceCode; - - String get sourceCode => - _sourceCode ??= modelNode == null ? '' : modelNode.sourceCode; -} - -abstract class TypeParameters implements ModelElement { - String get nameWithGenerics => '$name$genericParameters'; - - String get nameWithLinkedGenerics => '$name$linkedGenericParameters'; - - bool get hasGenericParameters => typeParameters.isNotEmpty; - - String get genericParameters { - if (typeParameters.isEmpty) return ''; - return '<${typeParameters.map((t) => t.name).join(', ')}>'; - } - - String get linkedGenericParameters { - if (typeParameters.isEmpty) return ''; - return '<${typeParameters.map((t) => t.linkedName).join(', ')}>'; - } - - @override - DefinedElementType get modelType; - - List get typeParameters; -} - -/// Top-level variables. But also picks up getters and setters? -class TopLevelVariable extends ModelElement - with Canonicalization, GetterSetterCombo, SourceCodeMixin, Categorization - implements EnclosedElement { - @override - final Accessor getter; - @override - final Accessor setter; - - TopLevelVariable(TopLevelVariableElement element, Library library, - PackageGraph packageGraph, this.getter, this.setter) - : super(element, library, packageGraph, null) { - if (getter != null) { - getter._enclosingCombo = this; - } - if (setter != null) { - setter._enclosingCombo = this; - } - } - - @override - bool get isInherited => false; - - @override - String get documentation { - // Verify that hasSetter and hasGetterNoSetter are mutually exclusive, - // to prevent displaying more or less than one summary. - if (isPublic) { - Set assertCheck = Set() - ..addAll([hasPublicSetter, hasPublicGetterNoSetter]); - assert(assertCheck.containsAll([true, false])); - } - return super.documentation; - } - - @override - ModelElement get enclosingElement => library; - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(canonicalLibrary != null); - assert(canonicalLibrary == library); - return '${package.baseHref}${library.dirName}/$fileName'; - } - - @override - bool get isConst => _variable.isConst; - - @override - bool get isFinal { - /// isFinal returns true for the variable even if it has an explicit getter - /// (which means we should not document it as "final"). - if (hasExplicitGetter) return false; - return _variable.isFinal; - } - - @override - String get kind => isConst ? 'top-level constant' : 'top-level property'; - - @override - Set get features => super.features..addAll(comboFeatures); - - @override - String _computeDocumentationComment() { - String docs = getterSetterDocumentationComment; - if (docs.isEmpty) return _variable.documentationComment; - return docs; - } - - @override - String get fileName => isConst ? '$name-constant.html' : '$name.html'; - - @override - DefinedElementType get modelType => super.modelType; - - TopLevelVariableElement get _variable => (element as TopLevelVariableElement); -} - -class Typedef extends ModelElement - with SourceCodeMixin, TypeParameters, Categorization - implements EnclosedElement { - Typedef(FunctionTypeAliasElement element, Library library, - PackageGraph packageGraph) - : super(element, library, packageGraph, null); - - @override - ModelElement get enclosingElement => library; - - @override - String get nameWithGenerics => '$name${super.genericParameters}'; - - @override - String get genericParameters { - if (element is GenericTypeAliasElement) { - List genericTypeParameters = - (element as GenericTypeAliasElement).function.typeParameters; - if (genericTypeParameters.isNotEmpty) { - return '<${genericTypeParameters.map((t) => t.name).join(', ')}>'; - } - } // else, all types are resolved. - return ''; - } - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(canonicalLibrary != null); - assert(canonicalLibrary == library); - return '${package.baseHref}${library.dirName}/$fileName'; - } - - // Food for mustache. - bool get isInherited => false; - - @override - String get kind => 'typedef'; - - String get linkedReturnType => modelType.createLinkedReturnTypeName(); - - @override - DefinedElementType get modelType => super.modelType; - - FunctionTypeAliasElement get _typedef => - (element as FunctionTypeAliasElement); - - @override - List get typeParameters => _typedef.typeParameters.map((f) { - return ModelElement.from(f, library, packageGraph) as TypeParameter; - }).toList(); -} - -class TypeParameter extends ModelElement { - TypeParameter( - TypeParameterElement element, Library library, PackageGraph packageGraph) - : super(element, library, packageGraph, null); - - @override - ModelElement get enclosingElement => (element.enclosingElement != null) - ? ModelElement.from(element.enclosingElement, library, packageGraph) - : null; - - @override - String get href { - if (!identical(canonicalModelElement, this)) { - return canonicalModelElement?.href; - } - assert(canonicalLibrary != null); - assert(canonicalLibrary == library); - return '${package.baseHref}${enclosingElement.library.dirName}/${enclosingElement.name}/$name'; - } - - @override - String get kind => 'type parameter'; - - ElementType _boundType; - - ElementType get boundType { - if (_boundType == null) { - var bound = _typeParameter.bound; - if (bound != null) { - _boundType = ElementType.from(bound, library, packageGraph); - } - } - return _boundType; - } - - String _name; - - @override - String get name { - if (_name == null) { - _name = _typeParameter.bound != null - ? '${_typeParameter.name} extends ${boundType.nameWithGenerics}' - : _typeParameter.name; - } - return _name; - } - - @override - String get linkedName { - if (_linkedName == null) { - _linkedName = _typeParameter.bound != null - ? '${_typeParameter.name} extends ${boundType.linkedName}' - : _typeParameter.name; - } - return _linkedName; - } - - TypeParameterElement get _typeParameter => element as TypeParameterElement; -} - -/// Everything you need to instantiate a PackageGraph object for documenting. -class PackageBuilder { - final DartdocOptionContext config; - - PackageBuilder(this.config); - - Future buildPackageGraph() async { - if (config.topLevelPackageMeta.needsPubGet) { - config.topLevelPackageMeta.runPubGet(); - } - - PackageGraph newGraph = PackageGraph.UninitializedPackageGraph( - config, - driver, - await driver.currentSession.typeSystem, - sdk, - hasEmbedderSdkFiles); - await getLibraries(newGraph); - await newGraph.initializePackageGraph(); - return newGraph; - } - - DartSdk _sdk; - - DartSdk get sdk { - if (_sdk == null) { - _sdk = FolderBasedDartSdk(PhysicalResourceProvider.INSTANCE, - PhysicalResourceProvider.INSTANCE.getFolder(config.sdkDir)); - } - return _sdk; - } - - EmbedderSdk _embedderSdk; - - EmbedderSdk get embedderSdk { - if (_embedderSdk == null && !config.topLevelPackageMeta.isSdk) { - _embedderSdk = EmbedderSdk(PhysicalResourceProvider.INSTANCE, - EmbedderYamlLocator(packageMap).embedderYamls); - } - return _embedderSdk; - } - - static Map> _calculatePackageMap( - file_system.Folder dir) { - Map> map = Map(); - var info = package_config.findPackagesFromFile(dir.toUri()); - - for (String name in info.packages) { - Uri uri = info.asMap()[name]; - String packagePath = path.normalize(path.fromUri(uri)); - file_system.Resource resource = - PhysicalResourceProvider.INSTANCE.getResource(packagePath); - if (resource is file_system.Folder) { - map[name] = [resource]; - } - } - - return map; - } - - Map> _packageMap; - - Map> get packageMap { - if (_packageMap == null) { - file_system.Folder cwd = - PhysicalResourceProvider.INSTANCE.getResource(config.inputDir); - _packageMap = _calculatePackageMap(cwd); - } - return _packageMap; - } - - DartUriResolver _embedderResolver; - - DartUriResolver get embedderResolver { - if (_embedderResolver == null) { - _embedderResolver = DartUriResolver(embedderSdk); - } - return _embedderResolver; - } - - SourceFactory get sourceFactory { - List resolvers = []; - resolvers.add(SdkExtUriResolver(packageMap)); - final UriResolver packageResolver = - PackageMapUriResolver(PhysicalResourceProvider.INSTANCE, packageMap); - UriResolver sdkResolver; - if (embedderSdk == null || embedderSdk.urlMappings.isEmpty) { - // The embedder uri resolver has no mappings. Use the default Dart SDK - // uri resolver. - sdkResolver = DartUriResolver(sdk); - } else { - // The embedder uri resolver has mappings, use it instead of the default - // Dart SDK uri resolver. - sdkResolver = embedderResolver; - } - - /// [AnalysisDriver] seems to require package resolvers that - /// never resolve to embedded SDK files, and the resolvers list must still - /// contain a DartUriResolver. This hack won't be necessary once analyzer - /// has a clean public API. - resolvers.add(PackageWithoutSdkResolver(packageResolver, sdkResolver)); - resolvers.add(sdkResolver); - resolvers.add( - file_system.ResourceUriResolver(PhysicalResourceProvider.INSTANCE)); - - assert( - resolvers.any((UriResolver resolver) => resolver is DartUriResolver)); - SourceFactory sourceFactory = SourceFactory(resolvers); - return sourceFactory; - } - - AnalysisDriver _driver; - - AnalysisDriver get driver { - if (_driver == null) { - PerformanceLog log = PerformanceLog(null); - AnalysisDriverScheduler scheduler = AnalysisDriverScheduler(log); - AnalysisOptionsImpl options = AnalysisOptionsImpl(); - - // TODO(jcollins-g): pass in an ExperimentStatus instead? - options.enabledExperiments = config.enableExperiment - ..add('extension-methods'); - - // TODO(jcollins-g): Make use of currently not existing API for managing - // many AnalysisDrivers - // TODO(jcollins-g): make use of DartProject isApi() - _driver = AnalysisDriver( - scheduler, - log, - PhysicalResourceProvider.INSTANCE, - MemoryByteStore(), - FileContentOverlay(), - null, - sourceFactory, - options); - driver.results.listen((_) {}); - driver.exceptions.listen((_) {}); - scheduler.start(); - } - return _driver; - } - - /// Return an Iterable with the sdk files we should parse. - /// Filter can be String or RegExp (technically, anything valid for - /// [String.contains]) - Iterable getSdkFilesToDocument() sync* { - for (var sdkLib in sdk.sdkLibraries) { - Source source = sdk.mapDartUri(sdkLib.shortName); - yield source.fullName; - } - } - - /// Parse a single library at [filePath] using the current analysis driver. - /// If [filePath] is not a library, returns null. - Future processLibrary(String filePath) async { - String name = filePath; - - if (name.startsWith(directoryCurrentPath)) { - name = name.substring(directoryCurrentPath.length); - if (name.startsWith(Platform.pathSeparator)) name = name.substring(1); - } - JavaFile javaFile = JavaFile(filePath).getAbsoluteFile(); - Source source = FileBasedSource(javaFile); - - // TODO(jcollins-g): remove the manual reversal using embedderSdk when we - // upgrade to analyzer-0.30 (where DartUriResolver implements - // restoreAbsolute) - Uri uri = embedderSdk?.fromFileUri(source.uri)?.uri; - if (uri != null) { - source = FileBasedSource(javaFile, uri); - } else { - uri = driver.sourceFactory.restoreUri(source); - if (uri != null) { - source = FileBasedSource(javaFile, uri); - } - } - var sourceKind = await driver.getSourceKind(filePath); - // Allow dart source files with inappropriate suffixes (#1897). Those - // do not show up as SourceKind.LIBRARY. - if (sourceKind != SourceKind.PART) { - // Loading libraryElements from part files works, but is painfully slow - // and creates many duplicates. - return await driver.currentSession.getResolvedLibrary(source.fullName); - } - return null; - } - - Set _packageMetasForFiles(Iterable files) { - Set metas = Set(); - for (String filename in files) { - metas.add(PackageMeta.fromFilename(filename)); - } - return metas; - } - - /// Parse libraries with the analyzer and invoke a callback with the - /// result. - /// - /// Uses the [libraries] parameter to prevent calling - /// the callback more than once with the same [LibraryElement]. - /// Adds [LibraryElement]s found to that parameter. - Future _parseLibraries( - void Function(ResolvedLibraryResult) libraryAdder, - Set libraries, - Set files, - [bool Function(LibraryElement) isLibraryIncluded]) async { - isLibraryIncluded ??= (_) => true; - Set lastPass = Set(); - Set current; - do { - lastPass = _packageMetasForFiles(files); - - // Be careful here not to accidentally stack up multiple - // ResolvedLibraryResults, as those eat our heap. - for (String f in files) { - ResolvedLibraryResult r = await processLibrary(f); - if (r != null && - !libraries.contains(r.element) && - isLibraryIncluded(r.element)) { - logInfo('parsing ${f}...'); - libraryAdder(r); - libraries.add(r.element); - } - } - - // Be sure to give the analyzer enough time to find all the files. - await driver.discoverAvailableFiles(); - files.addAll(driver.knownFiles); - files.addAll(_includeExternalsFrom(driver.knownFiles)); - current = _packageMetasForFiles(files); - // To get canonicalization correct for non-locally documented packages - // (so we can generate the right hyperlinks), it's vital that we - // add all libraries in dependent packages. So if the analyzer - // discovers some files in a package we haven't seen yet, add files - // for that package. - for (PackageMeta meta in current.difference(lastPass)) { - if (meta.isSdk) { - files.addAll(getSdkFilesToDocument()); - } else { - files.addAll( - findFilesToDocumentInPackage(meta.dir.path, false, false)); - } - } - } while (!lastPass.containsAll(current)); - } - - /// Given a package name, explore the directory and pull out all top level - /// library files in the "lib" directory to document. - Iterable findFilesToDocumentInPackage( - String basePackageDir, bool autoIncludeDependencies, - [bool filterExcludes = true]) sync* { - final String sep = path.separator; - - Set packageDirs = Set()..add(basePackageDir); - - if (autoIncludeDependencies) { - Map info = package_config - .findPackagesFromFile( - Uri.file(path.join(basePackageDir, 'pubspec.yaml'))) - .asMap(); - for (String packageName in info.keys) { - if (!filterExcludes || !config.exclude.contains(packageName)) { - packageDirs.add(path.dirname(info[packageName].toFilePath())); - } - } - } - - for (String packageDir in packageDirs) { - var packageLibDir = path.join(packageDir, 'lib'); - var packageLibSrcDir = path.join(packageLibDir, 'src'); - // To avoid analyzing package files twice, only files with paths not - // containing '/packages' will be added. The only exception is if the file - // to analyze already has a '/package' in its path. - for (var lib - in listDir(packageDir, recursive: true, listDir: _packageDirList)) { - if (lib.endsWith('.dart') && - (!lib.contains('${sep}packages${sep}') || - packageDir.contains('${sep}packages${sep}'))) { - // Only include libraries within the lib dir that are not in lib/src - if (path.isWithin(packageLibDir, lib) && - !path.isWithin(packageLibSrcDir, lib)) { - // Only add the file if it does not contain 'part of' - var contents = File(lib).readAsStringSync(); - - if (contents.contains(newLinePartOfRegexp) || - contents.startsWith(partOfRegexp)) { - // NOOP: it's a part file - } else { - yield lib; - } - } - } - } - } - } - - /// Calculate includeExternals based on a list of files. Assumes each - /// file might be part of a [DartdocOptionContext], and loads those - /// objects to find any [DartdocOptionContext.includeExternal] configurations - /// therein. - Iterable _includeExternalsFrom(Iterable files) sync* { - for (String file in files) { - DartdocOptionContext fileContext = - DartdocOptionContext.fromContext(config, File(file)); - if (fileContext.includeExternal != null) { - yield* fileContext.includeExternal; - } - } - } - - Set getFiles() { - Iterable files; - if (config.topLevelPackageMeta.isSdk) { - files = getSdkFilesToDocument(); - } else { - files = findFilesToDocumentInPackage( - config.inputDir, config.autoIncludeDependencies); - } - files = quiver.concat([files, _includeExternalsFrom(files)]); - return Set.from(files.map((s) => File(s).absolute.path)); - } - - Iterable getEmbedderSdkFiles() sync* { - if (embedderSdk != null && - embedderSdk.urlMappings.isNotEmpty && - !config.topLevelPackageMeta.isSdk) { - for (String dartUri in embedderSdk.urlMappings.keys) { - Source source = embedderSdk.mapDartUri(dartUri); - yield (File(source.fullName)).absolute.path; - } - } - } - - bool get hasEmbedderSdkFiles => - embedderSdk != null && getEmbedderSdkFiles().isNotEmpty; - - Future getLibraries(PackageGraph uninitializedPackageGraph) async { - DartSdk findSpecialsSdk = sdk; - if (embedderSdk != null && embedderSdk.urlMappings.isNotEmpty) { - findSpecialsSdk = embedderSdk; - } - Set files = getFiles()..addAll(getEmbedderSdkFiles()); - Set specialFiles = specialLibraryFiles(findSpecialsSdk).toSet(); - - /// Returns true if this library element should be included according - /// to the configuration. - bool isLibraryIncluded(LibraryElement libraryElement) { - if (config.include.isNotEmpty && - !config.include.contains(libraryElement.name)) { - return false; - } - return true; - } - - Set foundLibraries = Set(); - await _parseLibraries(uninitializedPackageGraph.addLibraryToGraph, - foundLibraries, files, isLibraryIncluded); - if (config.include.isNotEmpty) { - Iterable knownLibraryNames = foundLibraries.map((l) => l.name); - Set notFound = Set.from(config.include) - .difference(Set.from(knownLibraryNames)) - .difference(Set.from(config.exclude)); - if (notFound.isNotEmpty) { - throw 'Did not find: [${notFound.join(', ')}] in ' - 'known libraries: [${knownLibraryNames.join(', ')}]'; - } - } - // Include directive does not apply to special libraries. - await _parseLibraries(uninitializedPackageGraph.addSpecialLibraryToGraph, - foundLibraries, specialFiles.difference(files)); - } - - /// If [dir] contains both a `lib` directory and a `pubspec.yaml` file treat - /// it like a package and only return the `lib` dir. - /// - /// This ensures that packages don't have non-`lib` content documented. - static Iterable _packageDirList(Directory dir) sync* { - var entities = dir.listSync(); - - var pubspec = entities.firstWhere( - (e) => e is File && path.basename(e.path) == 'pubspec.yaml', - orElse: () => null); - - var libDir = entities.firstWhere( - (e) => e is Directory && path.basename(e.path) == 'lib', - orElse: () => null); - - if (pubspec != null && libDir != null) { - yield libDir; - } else { - yield* entities; - } - } -} - -/// This class resolves package URIs, but only if a given SdkResolver doesn't -/// resolve them. -/// -/// TODO(jcollins-g): remove this hackery when a clean public API to analyzer -/// exists, and port dartdoc to it. -class PackageWithoutSdkResolver extends UriResolver { - final UriResolver _packageResolver; - final UriResolver _sdkResolver; - - PackageWithoutSdkResolver(this._packageResolver, this._sdkResolver); - - @override - Source resolveAbsolute(Uri uri, [Uri actualUri]) { - if (_sdkResolver.resolveAbsolute(uri, actualUri) == null) { - return _packageResolver.resolveAbsolute(uri, actualUri); - } - return null; - } - - @override - Uri restoreAbsolute(Source source) { - Uri resolved; - try { - resolved = _sdkResolver.restoreAbsolute(source); - } catch (ArgumentError) { - // SDK resolvers really don't like being thrown package paths. - } - if (resolved == null) { - return _packageResolver.restoreAbsolute(source); - } - return null; - } -} diff --git a/lib/src/model/accessor.dart b/lib/src/model/accessor.dart new file mode 100644 index 0000000000..8b08f4bd67 --- /dev/null +++ b/lib/src/model/accessor.dart @@ -0,0 +1,209 @@ +// 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/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/src/dart/element/member.dart' show Member; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/utils.dart'; +import 'package:dartdoc/src/warnings.dart'; + +/// Getters and setters. +class Accessor extends ModelElement implements EnclosedElement { + GetterSetterCombo enclosingCombo; + + Accessor(PropertyAccessorElement element, Library library, + PackageGraph packageGraph, Member originalMember) + : super(element, library, packageGraph, originalMember); + + String get linkedReturnType { + assert(isGetter); + return modelType.createLinkedReturnTypeName(); + } + + bool get isSynthetic => element.isSynthetic; + + String _sourceCode; + + @override + String get sourceCode { + if (_sourceCode == null) { + if (isSynthetic) { + _sourceCode = packageGraph + .getModelNodeFor((element as PropertyAccessorElement).variable) + .sourceCode; + } else { + _sourceCode = super.sourceCode; + } + } + return _sourceCode; + } + + @override + List get computeDocumentationFrom { + return super.computeDocumentationFrom; + } + + @override + String computeDocumentationComment() { + if (isSynthetic) { + String docComment = + (element as PropertyAccessorElement).variable.documentationComment; + // If we're a setter, only display something if we have something different than the getter. + // TODO(jcollins-g): modify analyzer to do this itself? + if (isGetter || + // TODO(jcollins-g): @nodoc reading from comments is at the wrong abstraction level here. + (docComment != null && + (docComment.contains('') || + docComment.contains('@nodoc'))) || + (isSetter && + enclosingCombo.hasGetter && + enclosingCombo.getter.documentationComment != docComment)) { + return stripComments(docComment); + } else { + return ''; + } + } + return stripComments(super.computeDocumentationComment()); + } + + @override + void warn(PackageWarning kind, + {String message, + Iterable referredFrom, + Iterable extendedDebug}) { + enclosingCombo.warn(kind, + message: message, + referredFrom: referredFrom, + extendedDebug: extendedDebug); + } + + @override + ModelElement get enclosingElement { + if (_accessor.enclosingElement is CompilationUnitElement) { + return packageGraph.findButDoNotCreateLibraryFor( + _accessor.enclosingElement.enclosingElement); + } + + return ModelElement.from(_accessor.enclosingElement, library, packageGraph); + } + + @override + bool get isCanonical => enclosingCombo.isCanonical; + + @override + String get href { + return enclosingCombo.href; + } + + bool get isGetter => _accessor.isGetter; + + bool get isSetter => _accessor.isSetter; + + @override + String get kind => 'accessor'; + + String _namePart; + + @override + String get namePart { + if (_namePart == null) { + _namePart = super.namePart.split('=').first; + } + return _namePart; + } + + PropertyAccessorElement get _accessor => (element as PropertyAccessorElement); +} + +/// A getter or setter that is a member of a [Container]. +class ContainerAccessor extends Accessor with ContainerMember, Inheritable { + /// Factory will return an [ContainerAccessor] with isInherited = true + /// if [element] is in [inheritedAccessors]. + factory ContainerAccessor.from(PropertyAccessorElement element, + Set inheritedAccessors, Class enclosingClass) { + ContainerAccessor accessor; + if (element == null) return null; + if (inheritedAccessors.contains(element)) { + accessor = ModelElement.from( + element, enclosingClass.library, enclosingClass.packageGraph, + enclosingContainer: enclosingClass); + } else { + accessor = ModelElement.from( + element, enclosingClass.library, enclosingClass.packageGraph); + } + return accessor; + } + + ModelElement _enclosingElement; + bool _isInherited = false; + + @override + bool get isCovariant => isSetter && parameters.first.isCovariant; + + ContainerAccessor(PropertyAccessorElement element, Library library, + PackageGraph packageGraph) + : super(element, library, packageGraph, null); + + ContainerAccessor.inherited(PropertyAccessorElement element, Library library, + PackageGraph packageGraph, this._enclosingElement, + {Member originalMember}) + : super(element, library, packageGraph, originalMember) { + _isInherited = true; + } + + @override + bool get isInherited => _isInherited; + + @override + Container get enclosingElement { + if (_enclosingElement == null) { + _enclosingElement = super.enclosingElement; + } + return _enclosingElement; + } + + bool _overriddenElementIsSet = false; + ModelElement _overriddenElement; + + @override + ContainerAccessor get overriddenElement { + assert(packageGraph.allLibrariesAdded); + if (!_overriddenElementIsSet) { + _overriddenElementIsSet = true; + Element parent = element.enclosingElement; + if (parent is ClassElement) { + for (InterfaceType t in parent.allSupertypes) { + Element accessor = this.isGetter + ? t.getGetter(element.name) + : t.getSetter(element.name); + if (accessor != null) { + if (accessor is Member) { + accessor = PackageGraph.getBasestElement(accessor); + } + Class parentClass = + ModelElement.fromElement(t.element, packageGraph); + List possibleFields = []; + possibleFields.addAll(parentClass.allInstanceFields); + possibleFields.addAll(parentClass.staticProperties); + String fieldName = accessor.name.replaceFirst('=', ''); + Field foundField = possibleFields.firstWhere( + (f) => f.element.name == fieldName, + orElse: () => null); + if (foundField != null) { + if (this.isGetter) { + _overriddenElement = foundField.getter; + } else { + _overriddenElement = foundField.setter; + } + assert(!(_overriddenElement as ContainerAccessor).isInherited); + break; + } + } + } + } + } + return _overriddenElement; + } +} diff --git a/lib/src/model/canonicalization.dart b/lib/src/model/canonicalization.dart new file mode 100644 index 0000000000..0438dbb12f --- /dev/null +++ b/lib/src/model/canonicalization.dart @@ -0,0 +1,101 @@ +// 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:dartdoc/src/model/model.dart'; + +/// Classes extending this class have canonicalization support in Dartdoc. +abstract class Canonicalization implements Locatable, Documentable { + bool get isCanonical; + + Library get canonicalLibrary; + + List get commentRefs => null; + + /// Pieces of the location, split to remove 'package:' and slashes. + Set get locationPieces; + + List scoreCanonicalCandidates(List libraries) { + return libraries.map((l) => scoreElementWithLibrary(l)).toList()..sort(); + } + + ScoredCandidate scoreElementWithLibrary(Library lib) { + ScoredCandidate scoredCandidate = ScoredCandidate(this, lib); + Iterable resplit(Set items) sync* { + for (String item in items) { + for (String subItem in item.split('_')) { + yield subItem; + } + } + } + + // Large boost for @canonicalFor, essentially overriding all other concerns. + if (lib.canonicalFor.contains(fullyQualifiedName)) { + scoredCandidate.alterScore(5.0, 'marked @canonicalFor'); + } + // Penalty for deprecated libraries. + if (lib.isDeprecated) scoredCandidate.alterScore(-1.0, 'is deprecated'); + // Give a big boost if the library has the package name embedded in it. + if (lib.package.namePieces.intersection(lib.namePieces).isEmpty) { + scoredCandidate.alterScore(1.0, 'embeds package name'); + } + // Give a tiny boost for libraries with long names, assuming they're + // more specific (and therefore more likely to be the owner of this symbol). + scoredCandidate.alterScore(.01 * lib.namePieces.length, 'name is long'); + // If we don't know the location of this element, return our best guess. + // TODO(jcollins-g): is that even possible? + assert(locationPieces.isNotEmpty); + if (locationPieces.isEmpty) return scoredCandidate; + // The more pieces we have of the location in our library name, the more we should boost our score. + scoredCandidate.alterScore( + lib.namePieces.intersection(locationPieces).length.toDouble() / + locationPieces.length.toDouble(), + 'element location shares parts with name'); + // If pieces of location at least start with elements of our library name, boost the score a little bit. + double scoreBoost = 0.0; + for (String piece in resplit(locationPieces)) { + for (String namePiece in lib.namePieces) { + if (piece.startsWith(namePiece)) { + scoreBoost += 0.001; + } + } + } + scoredCandidate.alterScore( + scoreBoost, 'element location parts start with parts of name'); + return scoredCandidate; + } +} + +/// This class represents the score for a particular element; how likely +/// it is that this is the canonical element. +class ScoredCandidate implements Comparable { + final List reasons = []; + + /// The canonicalization element being scored. + final Canonicalization element; + final Library library; + + /// The score accumulated so far. Higher means it is more likely that this + /// is the intended canonical Library. + double score = 0.0; + + ScoredCandidate(this.element, this.library); + + void alterScore(double scoreDelta, String reason) { + score += scoreDelta; + if (scoreDelta != 0) { + reasons.add( + "${reason} (${scoreDelta >= 0 ? '+' : ''}${scoreDelta.toStringAsPrecision(4)})"); + } + } + + @override + int compareTo(ScoredCandidate other) { + //assert(element == other.element); + return score.compareTo(other.score); + } + + @override + String toString() => + "${library.name}: ${score.toStringAsPrecision(4)} - ${reasons.join(', ')}"; +} diff --git a/lib/src/model/categorization.dart b/lib/src/model/categorization.dart new file mode 100644 index 0000000000..0f8d9d8343 --- /dev/null +++ b/lib/src/model/categorization.dart @@ -0,0 +1,134 @@ +// 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:dartdoc/src/model/model.dart'; + +final categoryRegexp = RegExp( + r'[ ]*{@(api|category|subCategory|image|samples) (.+?)}[ ]*\n?', + multiLine: true); + +/// Mixin implementing dartdoc categorization for ModelElements. +abstract class Categorization implements ModelElement { + @override + String buildDocumentationAddition(String rawDocs) => + _stripAndSetDartdocCategories(rawDocs ??= ''); + + /// Parse {@category ...} and related information in API comments, stripping + /// out that information from the given comments and returning the stripped + /// version. + String _stripAndSetDartdocCategories(String rawDocs) { + Set _categorySet = Set(); + Set _subCategorySet = Set(); + _hasCategorization = false; + + rawDocs = rawDocs.replaceAllMapped(categoryRegexp, (match) { + _hasCategorization = true; + switch (match[1]) { + case 'category': + case 'api': + _categorySet.add(match[2].trim()); + break; + case 'subCategory': + _subCategorySet.add(match[2].trim()); + break; + case 'image': + _image = match[2].trim(); + break; + case 'samples': + _samples = match[2].trim(); + break; + } + return ''; + }); + + if (_categorySet.isEmpty) { + // All objects are in the default category if not specified. + _categorySet.add(null); + } + if (_subCategorySet.isEmpty) { + // All objects are in the default subcategory if not specified. + _subCategorySet.add(null); + } + _categoryNames = _categorySet.toList()..sort(); + _subCategoryNames = _subCategorySet.toList()..sort(); + _image ??= ''; + _samples ??= ''; + return rawDocs; + } + + bool get hasSubCategoryNames => + subCategoryNames.length > 1 || subCategoryNames.first != null; + List _subCategoryNames; + + /// Either a set of strings containing all declared subcategories for this symbol, + /// or a set containing Null if none were declared. + List get subCategoryNames { + // TODO(jcollins-g): avoid side-effect dependency + if (_subCategoryNames == null) documentationLocal; + return _subCategoryNames; + } + + @override + bool get hasCategoryNames => + categoryNames.length > 1 || categoryNames.first != null; + List _categoryNames; + + /// Either a set of strings containing all declared categories for this symbol, + /// or a set containing Null if none were declared. + List get categoryNames { + // TODO(jcollins-g): avoid side-effect dependency + if (_categoryNames == null) documentationLocal; + return _categoryNames; + } + + bool get hasImage => image.isNotEmpty; + String _image; + + /// Either a URI to a defined image, or the empty string if none + /// was declared. + String get image { + // TODO(jcollins-g): avoid side-effect dependency + if (_image == null) documentationLocal; + return _image; + } + + bool get hasSamples => samples.isNotEmpty; + String _samples; + + /// Either a URI to documentation with samples, or the empty string if none + /// was declared. + String get samples { + // TODO(jcollins-g): avoid side-effect dependency + if (_samples == null) documentationLocal; + return _samples; + } + + bool _hasCategorization; + + Iterable _categories; + + Iterable get categories { + if (_categories == null) { + _categories = categoryNames + .map((n) => package.nameToCategory[n]) + .where((c) => c != null) + .toList() + ..sort(); + } + return _categories; + } + + Iterable get displayedCategories { + if (config.showUndocumentedCategories) return categories; + return categories.where((c) => c.isDocumented); + } + + /// True if categories, subcategories, a documentation icon, or samples were + /// declared. + bool get hasCategorization { + if (_hasCategorization == null) documentationLocal; + return _hasCategorization; + } +} diff --git a/lib/src/model/category.dart b/lib/src/model/category.dart new file mode 100644 index 0000000000..d080ac3fc5 --- /dev/null +++ b/lib/src/model/category.dart @@ -0,0 +1,208 @@ +// 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 'dart:io'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:dartdoc/src/dartdoc_options.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/package_meta.dart'; +import 'package:dartdoc/src/warnings.dart'; + + +/// A category is a subcategory of a package, containing libraries tagged +/// with a @category identifier. +class Category extends Nameable + with + Warnable, + Locatable, + Canonicalization, + MarkdownFileDocumentation, + LibraryContainer, + TopLevelContainer, + Indexable + implements Documentable { + /// All libraries in [libraries] must come from [package]. + @override + Package package; + final String _name; + @override + DartdocOptionContext config; + final Set _allItems = Set(); + + final List _classes = []; + final List _extensions = []; + final List _enums = []; + final List _mixins = []; + final List _exceptions = []; + final List _constants = []; + final List _properties = []; + final List _functions = []; + final List _typedefs = []; + + Category(this._name, this.package, this.config); + + void addItem(Categorization c) { + if (_allItems.contains(c)) return; + _allItems.add(c); + if (c is Library) { + libraries.add(c); + } else if (c is Mixin) { + _mixins.add(c); + } else if (c is Enum) { + _enums.add(c); + } else if (c is Class) { + if (c.isErrorOrException) { + _exceptions.add(c); + } else { + _classes.add(c); + } + } else if (c is TopLevelVariable) { + if (c.isConst) { + _constants.add(c); + } else { + _properties.add(c); + } + } else if (c is ModelFunction) { + _functions.add(c); + } else if (c is Typedef) { + _typedefs.add(c); + } else if (c is Extension) { + _extensions.add(c); + } else { + throw UnimplementedError("Unrecognized element"); + } + } + + @override + // TODO(jcollins-g): make [Category] a [Warnable]? + Warnable get enclosingElement => null; + + @override + Element get element => null; + + @override + String get name => categoryDefinition?.displayName ?? _name; + + @override + String get sortKey => _name; + + @override + List get containerOrder => config.categoryOrder; + + @override + String get enclosingName => package.name; + + @override + PackageGraph get packageGraph => package.packageGraph; + + @override + Library get canonicalLibrary => null; + + @override + List get documentationFrom => [this]; + + @override + DocumentLocation get documentedWhere => package.documentedWhere; + + bool _isDocumented; + + @override + bool get isDocumented { + if (_isDocumented == null) { + _isDocumented = documentedWhere != DocumentLocation.missing && + documentationFile != null; + } + return _isDocumented; + } + + @override + String get fullyQualifiedName => name; + + @override + String get href => + isCanonical ? '${package.baseHref}topics/${name}-topic.html' : null; + + String get linkedName { + String unbrokenCategoryName = name.replaceAll(' ', ' '); + if (isDocumented) { + return '$unbrokenCategoryName'; + } else { + return unbrokenCategoryName; + } + } + + String _categoryNumberClass; + + /// The position in the container order for this category. + String get categoryNumberClass { + if (_categoryNumberClass == null) { + _categoryNumberClass = "cp-${package.categories.indexOf(this)}"; + } + return _categoryNumberClass; + } + + /// Category name used in template as part of the class. + String get spanClass => name.split(' ').join('-').toLowerCase(); + + CategoryDefinition get categoryDefinition => + config.categories.categoryDefinitions[sortKey]; + + @override + bool get isCanonical => categoryDefinition != null; + + @override + String get kind => 'Topic'; + + FileContents _documentationFile; + + @override + FileContents get documentationFile { + if (_documentationFile == null) { + if (categoryDefinition?.documentationMarkdown != null) { + _documentationFile = + FileContents(File(categoryDefinition.documentationMarkdown)); + } + } + return _documentationFile; + } + + @override + void warn(PackageWarning kind, + {String message, + Iterable referredFrom, + Iterable extendedDebug}) { + packageGraph.warnOnElement(this, kind, + message: message, + referredFrom: referredFrom, + extendedDebug: extendedDebug); + } + + @override + Iterable get classes => _classes; + + @override + Iterable get constants => _constants; + + @override + Iterable get enums => _enums; + + @override + Iterable get exceptions => _exceptions; + + @override + Iterable get extensions => _extensions; + + @override + Iterable get functions => _functions; + + @override + Iterable get mixins => _mixins; + + @override + Iterable get properties => _properties; + + @override + Iterable get typedefs => _typedefs; +} diff --git a/lib/src/model/class.dart b/lib/src/model/class.dart new file mode 100644 index 0000000000..69ba5b9821 --- /dev/null +++ b/lib/src/model/class.dart @@ -0,0 +1,594 @@ +// 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/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/model_utils.dart' as model_utils; +import 'package:quiver/iterables.dart' as quiver; + +class Class extends Container + with TypeParameters, Categorization + implements EnclosedElement { + List mixins; + DefinedElementType supertype; + List _interfaces; + List _constructors; + List _inheritedOperators; + List _inheritedMethods; + List _inheritedProperties; + + Class(ClassElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph) { + packageGraph.specialClasses.addSpecial(this); + mixins = _cls.mixins + .map((f) { + DefinedElementType t = ElementType.from(f, library, packageGraph); + return t; + }) + .where((mixin) => mixin != null) + .toList(growable: false); + + if (_cls.supertype != null && _cls.supertype.element.supertype != null) { + supertype = ElementType.from(_cls.supertype, library, packageGraph); + } + + _interfaces = _cls.interfaces + .map((f) => + ElementType.from(f, library, packageGraph) as DefinedElementType) + .toList(growable: false); + } + + Constructor _defaultConstructor; + + Constructor get defaultConstructor { + if (_defaultConstructor == null) { + _defaultConstructor = constructors + .firstWhere((c) => c.isDefaultConstructor, orElse: () => null); + } + return _defaultConstructor; + } + + bool get hasPotentiallyApplicableExtensions => + potentiallyApplicableExtensions.isNotEmpty; + + List _potentiallyApplicableExtensions; + + Iterable get potentiallyApplicableExtensions { + if (_potentiallyApplicableExtensions == null) { + _potentiallyApplicableExtensions = model_utils + .filterNonDocumented(packageGraph.extensions) + .where((e) => e.couldApplyTo(this)) + .toList(growable: false) + ..sort(byName); + } + return _potentiallyApplicableExtensions; + } + + Iterable get allInstanceMethods => + quiver.concat([instanceMethods, inheritedMethods]); + + @override + Iterable get allPublicInstanceMethods => + model_utils.filterNonPublic(allInstanceMethods); + + bool get allPublicInstanceMethodsInherited => + instanceMethods.every((f) => f.isInherited); + + @override + Iterable get allInstanceFields => + quiver.concat([instanceProperties, inheritedProperties]); + + bool get allPublicInstancePropertiesInherited => + allPublicInstanceProperties.every((f) => f.isInherited); + + @override + Iterable get allOperators => + quiver.concat([operators, inheritedOperators]); + + bool get allPublicOperatorsInherited => + allPublicOperators.every((f) => f.isInherited); + + Map _allElements; + + Map get allElements { + if (_allElements == null) { + _allElements = Map(); + for (ModelElement me in allModelElements) { + assert(!_allElements.containsKey(me.element)); + _allElements[me.element] = me; + } + } + return _allElements; + } + + Map> _allModelElementsByNamePart; + + /// Helper for [_MarkdownCommentReference._getResultsForClass]. + Map> get allModelElementsByNamePart { + if (_allModelElementsByNamePart == null) { + _allModelElementsByNamePart = {}; + for (ModelElement me in allModelElements) { + _allModelElementsByNamePart.update( + me.namePart, (List v) => v..add(me), + ifAbsent: () => [me]); + } + } + return _allModelElementsByNamePart; + } + + /// This class might be canonical for elements it does not contain. + /// See [Inheritable.canonicalEnclosingContainer]. + bool contains(Element element) => allElements.containsKey(element); + + Map> _membersByName; + + /// Given a ModelElement that is a member of some other class, return + /// a member of this class that has the same name and return type. + /// + /// This enables object substitution for canonicalization, such as Interceptor + /// for Object. + ModelElement memberByExample(ModelElement example) { + if (_membersByName == null) { + _membersByName = Map(); + for (ModelElement me in allModelElements) { + if (!_membersByName.containsKey(me.name)) { + _membersByName[me.name] = List(); + } + _membersByName[me.name].add(me); + } + } + ModelElement member; + Iterable possibleMembers = _membersByName[example.name] + .where((e) => e.runtimeType == example.runtimeType); + if (example.runtimeType == Accessor) { + possibleMembers = possibleMembers.where( + (e) => (example as Accessor).isGetter == (e as Accessor).isGetter); + } + member = possibleMembers.first; + assert(possibleMembers.length == 1); + return member; + } + + List _allModelElements; + + List get allModelElements { + if (_allModelElements == null) { + _allModelElements = List.from( + quiver.concat([ + allInstanceMethods, + allInstanceFields, + allAccessors, + allOperators, + constants, + constructors, + staticMethods, + staticProperties, + typeParameters, + ]), + growable: false); + } + return _allModelElements; + } + + List _allCanonicalModelElements; + + List get allCanonicalModelElements { + return (_allCanonicalModelElements ??= + allModelElements.where((e) => e.isCanonical).toList()); + } + + List get constructors { + if (_constructors != null) return _constructors; + + _constructors = _cls.constructors.map((e) { + return ModelElement.from(e, library, packageGraph) as Constructor; + }).toList(growable: true) + ..sort(byName); + + return _constructors; + } + + Iterable get publicConstructors => + model_utils.filterNonPublic(constructors); + + /// Returns the library that encloses this element. + @override + ModelElement get enclosingElement => library; + + @override + String get fileName => "${name}-class.html"; + + String get fullkind { + if (isAbstract) return 'abstract $kind'; + return kind; + } + + bool get hasPublicConstructors => publicConstructors.isNotEmpty; + + bool get hasPublicImplementors => publicImplementors.isNotEmpty; + + bool get hasInstanceProperties => instanceProperties.isNotEmpty; + + bool get hasPublicInterfaces => publicInterfaces.isNotEmpty; + + @override + bool get hasPublicMethods => + publicInstanceMethods.isNotEmpty || publicInheritedMethods.isNotEmpty; + + bool get hasPublicMixins => publicMixins.isNotEmpty; + + bool get hasModifiers => + hasPublicMixins || + hasAnnotations || + hasPublicInterfaces || + hasPublicSuperChainReversed || + hasPublicImplementors || + hasPotentiallyApplicableExtensions; + + @override + bool get hasPublicOperators => + publicOperators.isNotEmpty || publicInheritedOperators.isNotEmpty; + + @override + bool get hasPublicProperties => + publicInheritedProperties.isNotEmpty || + publicInstanceProperties.isNotEmpty; + + @override + bool get hasPublicStaticMethods => publicStaticMethods.isNotEmpty; + + bool get hasPublicSuperChainReversed => publicSuperChainReversed.isNotEmpty; + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(canonicalLibrary != null); + assert(canonicalLibrary == library); + return '${package.baseHref}${library.dirName}/$fileName'; + } + + /// Returns all the implementors of this class. + Iterable get publicImplementors { + return model_utils.filterNonPublic(model_utils.findCanonicalFor( + packageGraph.implementors[href] != null + ? packageGraph.implementors[href] + : [])); + } + + List get inheritedMethods { + if (_inheritedMethods == null) { + _inheritedMethods = []; + Set methodNames = methods.map((m) => m.element.name).toSet(); + + Set inheritedMethodElements = + _inheritedElements.where((e) { + return (e is MethodElement && + !e.isOperator && + e is! PropertyAccessorElement && + !methodNames.contains(e.name)); + }).toSet(); + + for (ExecutableElement e in inheritedMethodElements) { + Method m = ModelElement.from(e, library, packageGraph, + enclosingContainer: this); + _inheritedMethods.add(m); + } + _inheritedMethods.sort(byName); + } + return _inheritedMethods; + } + + Iterable get publicInheritedMethods => + model_utils.filterNonPublic(inheritedMethods); + + bool get hasPublicInheritedMethods => publicInheritedMethods.isNotEmpty; + + List get inheritedOperators { + if (_inheritedOperators == null) { + _inheritedOperators = []; + Set operatorNames = operators.map((o) => o.element.name).toSet(); + + Set inheritedOperatorElements = + _inheritedElements.where((e) { + return (e is MethodElement && + e.isOperator && + !operatorNames.contains(e.name)); + }).toSet(); + for (ExecutableElement e in inheritedOperatorElements) { + Operator o = ModelElement.from(e, library, packageGraph, + enclosingContainer: this); + _inheritedOperators.add(o); + } + _inheritedOperators.sort(byName); + } + return _inheritedOperators; + } + + Iterable get publicInheritedOperators => + model_utils.filterNonPublic(inheritedOperators); + + List get inheritedProperties { + if (_inheritedProperties == null) { + _inheritedProperties = allFields.where((f) => f.isInherited).toList() + ..sort(byName); + } + return _inheritedProperties; + } + + Iterable get publicInheritedProperties => + model_utils.filterNonPublic(inheritedProperties); + + Iterable get publicInstanceMethods => instanceMethods; + + List get interfaces => _interfaces; + + Iterable get publicInterfaces => + model_utils.filterNonPublic(interfaces); + + bool get isAbstract => _cls.isAbstract; + + @override + bool get isCanonical => super.isCanonical && isPublic; + + bool get isErrorOrException { + bool _doCheck(ClassElement element) { + return (element.library.isDartCore && + (element.name == 'Exception' || element.name == 'Error')); + } + + // if this class is itself Error or Exception, return true + if (_doCheck(_cls)) return true; + + return _cls.allSupertypes.map((t) => t.element).any(_doCheck); + } + + /// Returns true if [other] is a parent class for this class. + @override + bool isInheritingFrom(covariant Class other) => + superChain.map((et) => (et.element as Class)).contains(other); + + @override + String get kind => 'class'; + + Iterable get publicMixins => + model_utils.filterNonPublic(mixins); + + @override + DefinedElementType get modelType => super.modelType; + + /// Not the same as superChain as it may include mixins. + /// It's really not even the same as ordinary Dart inheritance, either, + /// because we pretend that interfaces are part of the inheritance chain + /// to include them in the set of things we might link to for documentation + /// purposes in abstract classes. + List _inheritanceChain; + + List get inheritanceChain { + if (_inheritanceChain == null) { + _inheritanceChain = []; + _inheritanceChain.add(this); + + /// Caching should make this recursion a little less painful. + for (Class c in mixins.reversed.map((e) => (e.element as Class))) { + _inheritanceChain.addAll(c.inheritanceChain); + } + + for (Class c in superChain.map((e) => (e.element as Class))) { + _inheritanceChain.addAll(c.inheritanceChain); + } + + /// Interfaces need to come last, because classes in the superChain might + /// implement them even when they aren't mentioned. + _inheritanceChain.addAll( + interfaces.expand((e) => (e.element as Class).inheritanceChain)); + } + return _inheritanceChain.toList(growable: false); + } + + List get superChain { + List typeChain = []; + DefinedElementType parent = supertype; + while (parent != null) { + typeChain.add(parent); + if (parent.type is InterfaceType) { + // Avoid adding [Object] to the superChain (_supertype already has this + // check) + if ((parent.type as InterfaceType)?.superclass?.superclass == null) { + parent = null; + } else { + parent = ElementType.from( + (parent.type as InterfaceType).superclass, library, packageGraph); + } + } else { + parent = (parent.element as Class).supertype; + } + } + return typeChain; + } + + Iterable get publicSuperChain => + model_utils.filterNonPublic(superChain); + + Iterable get publicSuperChainReversed => + publicSuperChain.toList().reversed; + + List __inheritedElements; + + List get _inheritedElements { + if (__inheritedElements == null) { + var classElement = element as ClassElement; + if (classElement.isDartCoreObject) { + return __inheritedElements = []; + } + + var inheritance = definingLibrary.inheritanceManager; + var classType = classElement.thisType; + var cmap = inheritance.getInheritedConcreteMap(classType); + var imap = inheritance.getInheritedMap(classType); + + var combinedMap = {}; + for (var nameObj in cmap.keys) { + combinedMap[nameObj.name] = cmap[nameObj]; + } + for (var nameObj in imap.keys) { + combinedMap[nameObj.name] ??= imap[nameObj]; + } + + __inheritedElements = combinedMap.values.toList(); + } + return __inheritedElements; + } + + List _fields; + + /// Internal only because subclasses are allowed to override how + /// these are mapped to [allInheritedFields] and so forth. + @override + List get allFields { + if (_fields == null) { + _fields = []; + Set inheritedAccessors = Set() + ..addAll(_inheritedElements.whereType()); + + // This structure keeps track of inherited accessors, allowing lookup + // by field name (stripping the '=' from setters). + Map> accessorMap = Map(); + for (PropertyAccessorElement accessorElement in inheritedAccessors) { + String name = accessorElement.name.replaceFirst('=', ''); + accessorMap.putIfAbsent(name, () => []); + accessorMap[name].add(accessorElement); + } + + // For half-inherited fields, the analyzer only links the non-inherited + // to the [FieldElement]. Compose our [Field] class by hand by looking up + // inherited accessors that may be related. + for (FieldElement f in _cls.fields) { + PropertyAccessorElement getterElement = f.getter; + if (getterElement == null && accessorMap.containsKey(f.name)) { + getterElement = accessorMap[f.name] + .firstWhere((e) => e.isGetter, orElse: () => null); + } + PropertyAccessorElement setterElement = f.setter; + if (setterElement == null && accessorMap.containsKey(f.name)) { + setterElement = accessorMap[f.name] + .firstWhere((e) => e.isSetter, orElse: () => null); + } + _addSingleField(getterElement, setterElement, inheritedAccessors, f); + accessorMap.remove(f.name); + } + + // Now we only have inherited accessors who aren't associated with + // anything in cls._fields. + for (String fieldName in accessorMap.keys) { + List elements = + accessorMap[fieldName].toList(); + PropertyAccessorElement getterElement = + elements.firstWhere((e) => e.isGetter, orElse: () => null); + PropertyAccessorElement setterElement = + elements.firstWhere((e) => e.isSetter, orElse: () => null); + _addSingleField(getterElement, setterElement, inheritedAccessors); + } + + _fields.sort(byName); + } + return _fields; + } + + /// Add a single Field to _fields. + /// + /// If [f] is not specified, pick the FieldElement from the PropertyAccessorElement + /// whose enclosing class inherits from the other (defaulting to the getter) + /// and construct a Field using that. + void _addSingleField( + PropertyAccessorElement getterElement, + PropertyAccessorElement setterElement, + Set inheritedAccessors, + [FieldElement f]) { + ContainerAccessor getter = + ContainerAccessor.from(getterElement, inheritedAccessors, this); + ContainerAccessor setter = + ContainerAccessor.from(setterElement, inheritedAccessors, this); + // Rebind getterElement/setterElement as ModelElement.from can resolve + // MultiplyInheritedExecutableElements or resolve Members. + getterElement = getter?.element; + setterElement = setter?.element; + assert(!(getter == null && setter == null)); + if (f == null) { + // Pick an appropriate FieldElement to represent this element. + // Only hard when dealing with a synthetic Field. + if (getter != null && setter == null) { + f = getterElement.variable; + } else if (getter == null && setter != null) { + f = setterElement.variable; + } else { + /* getter != null && setter != null */ + // In cases where a Field is composed of two Accessors defined in + // different places in the inheritance chain, there are two FieldElements + // for this single Field we're trying to compose. Pick the one closest + // to this class on the inheritance chain. + if ((setter.enclosingElement) + .isInheritingFrom(getter.enclosingElement)) { + f = setterElement.variable; + } else { + f = getterElement.variable; + } + } + } + Field field; + if ((getter == null || getter.isInherited) && + (setter == null || setter.isInherited)) { + // Field is 100% inherited. + field = ModelElement.from(f, library, packageGraph, + enclosingContainer: this, getter: getter, setter: setter); + } else { + // Field is <100% inherited (could be half-inherited). + // TODO(jcollins-g): Navigation is probably still confusing for + // half-inherited fields when traversing the inheritance tree. Make + // this better, somehow. + field = ModelElement.from(f, library, packageGraph, + getter: getter, setter: setter); + } + _fields.add(field); + } + + ClassElement get _cls => (element as ClassElement); + + List _methods; + + @override + List get methods { + if (_methods == null) { + _methods = _cls.methods.map((e) { + return ModelElement.from(e, library, packageGraph) as Method; + }).toList(growable: false) + ..sort(byName); + } + return _methods; + } + + List _typeParameters; + + // a stronger hash? + @override + List get typeParameters { + if (_typeParameters == null) { + _typeParameters = _cls.typeParameters.map((f) { + var lib = Library(f.enclosingElement.library, packageGraph); + return ModelElement.from(f, lib, packageGraph) as TypeParameter; + }).toList(); + } + return _typeParameters; + } + + @override + bool operator ==(o) => + o is Class && + name == o.name && + o.library.name == library.name && + o.library.package.name == library.package.name; +} diff --git a/lib/src/model/constructor.dart b/lib/src/model/constructor.dart new file mode 100644 index 0000000000..781417b611 --- /dev/null +++ b/lib/src/model/constructor.dart @@ -0,0 +1,112 @@ +// 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/element/element.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; + +class Constructor extends ModelElement + with TypeParameters + implements EnclosedElement { + Constructor( + ConstructorElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph, null); + + @override + CharacterLocation get characterLocation { + if (element.isSynthetic) { + // Make warnings for a synthetic constructor refer to somewhere reasonable + // since a synthetic constructor has no definition independent of the + // parent class. + return enclosingElement.characterLocation; + } + return super.characterLocation; + } + + @override + // TODO(jcollins-g): Revisit this when dart-lang/sdk#31517 is implemented. + List get typeParameters => + (enclosingElement as Class).typeParameters; + + @override + ModelElement get enclosingElement => + ModelElement.from(_constructor.enclosingElement, library, packageGraph); + + String get fullKind { + if (isConst) return 'const $kind'; + if (isFactory) return 'factory $kind'; + return kind; + } + + @override + String get fullyQualifiedName { + if (isDefaultConstructor) return super.fullyQualifiedName; + return '${library.name}.$name'; + } + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(canonicalLibrary != null); + assert(canonicalLibrary == library); + return '${package.baseHref}${enclosingElement.library.dirName}/${enclosingElement.name}/$name.html'; + } + + @override + bool get isConst => _constructor.isConst; + + bool get isDefaultConstructor => name == enclosingElement.name; + + bool get isFactory => _constructor.isFactory; + + @override + String get kind => 'constructor'; + + @override + DefinedElementType get modelType => super.modelType; + + String _name; + + @override + String get name { + if (_name == null) { + String constructorName = element.name; + if (constructorName.isEmpty) { + _name = enclosingElement.name; + } else { + _name = '${enclosingElement.name}.$constructorName'; + } + } + return _name; + } + + String _nameWithGenerics; + + @override + String get nameWithGenerics { + if (_nameWithGenerics == null) { + String constructorName = element.name; + if (constructorName.isEmpty) { + _nameWithGenerics = '${enclosingElement.name}${genericParameters}'; + } else { + _nameWithGenerics = + '${enclosingElement.name}${genericParameters}.$constructorName'; + } + } + return _nameWithGenerics; + } + + String get shortName { + if (name.contains('.')) { + return name.substring(_constructor.enclosingElement.name.length + 1); + } else { + return name; + } + } + + ConstructorElement get _constructor => (element as ConstructorElement); +} diff --git a/lib/src/model/container.dart b/lib/src/model/container.dart new file mode 100644 index 0000000000..9b18110c54 --- /dev/null +++ b/lib/src/model/container.dart @@ -0,0 +1,135 @@ +// 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/element/element.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/model_utils.dart' as model_utils; +import 'package:quiver/iterables.dart' as quiver; + +// Can be either a Class or Extension, used in the package graph and template data. +// Aggregates some of the common getters. +abstract class Container extends ModelElement { + List _constants; + List _operators; + List _staticMethods; + List _instanceMethods; + List _staticFields; + List _instanceFields; + + Container(Element element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph, null); + + bool get isClass => element is ClassElement; + + bool get isExtension => element is ExtensionElement; + + List get methods => []; + + List get instanceMethods { + if (_instanceMethods != null) return _instanceMethods; + + _instanceMethods = methods + .where((m) => !m.isStatic && !m.isOperator) + .toList(growable: false) + ..sort(byName); + return _instanceMethods; + } + + bool get hasPublicMethods => + model_utils.filterNonPublic(instanceMethods).isNotEmpty; + + Iterable get allPublicInstanceMethods => + model_utils.filterNonPublic(instanceMethods); + + List get staticMethods { + if (_staticMethods == null) { + _staticMethods = methods.where((m) => m.isStatic).toList(growable: false) + ..sort(byName); + } + return _staticMethods; + } + + bool get hasPublicStaticMethods => + model_utils.filterNonPublic(staticMethods).isNotEmpty; + + Iterable get publicStaticMethods => + model_utils.filterNonPublic(staticMethods); + + List get operators { + if (_operators == null) { + _operators = methods + .where((m) => m.isOperator) + .cast() + .toList(growable: false) + ..sort(byName); + } + return _operators; + } + + Iterable get allOperators => operators; + + bool get hasPublicOperators => publicOperators.isNotEmpty; + + Iterable get allPublicOperators => + model_utils.filterNonPublic(allOperators); + + Iterable get publicOperators => + model_utils.filterNonPublic(operators); + + List get allFields => []; + + List get staticProperties { + if (_staticFields == null) { + _staticFields = allFields + .where((f) => f.isStatic && !f.isConst) + .toList(growable: false) + ..sort(byName); + } + return _staticFields; + } + + Iterable get publicStaticProperties => + model_utils.filterNonPublic(staticProperties); + + bool get hasPublicStaticProperties => publicStaticProperties.isNotEmpty; + + List get instanceProperties { + if (_instanceFields == null) { + _instanceFields = allFields + .where((f) => !f.isStatic && !f.isInherited && !f.isConst) + .toList(growable: false) + ..sort(byName); + } + return _instanceFields; + } + + Iterable get publicInstanceProperties => + model_utils.filterNonPublic(instanceProperties); + + bool get hasPublicProperties => publicInstanceProperties.isNotEmpty; + + Iterable get allInstanceFields => instanceProperties; + + Iterable get allPublicInstanceProperties => + model_utils.filterNonPublic(allInstanceFields); + + bool isInheritingFrom(Container other) => false; + + List get constants { + if (_constants == null) { + _constants = allFields.where((f) => f.isConst).toList(growable: false) + ..sort(byName); + } + return _constants; + } + + Iterable get publicConstants => model_utils.filterNonPublic(constants); + + bool get hasPublicConstants => publicConstants.isNotEmpty; + + Iterable get allAccessors => quiver.concat([ + allInstanceFields.expand((f) => f.allAccessors), + constants.map((c) => c.getter) + ]); +} diff --git a/lib/src/model/container_member.dart b/lib/src/model/container_member.dart new file mode 100644 index 0000000000..76362f33f2 --- /dev/null +++ b/lib/src/model/container_member.dart @@ -0,0 +1,71 @@ +// 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:dartdoc/src/model/model.dart'; + +/// A [ModelElement] that is a [Container] member. +mixin ContainerMember on ModelElement implements EnclosedElement { + /// True if this [ContainerMember] is inherited from a different class. + bool get isInherited; + + /// True if this [ContainerMember] is overriding a superclass. + bool get isOverride; + + /// True if this [ContainerMember] has a parameter whose type is overridden + /// by a subtype. + bool get isCovariant; + + /// True if this [ContainerMember] is from an applicable [Extension]. + /// False otherwise, including if this [ContainerMember]'s [enclosingElement] + /// is the extension it was declared in. + // TODO(jcollins-g): This semantic is a little confusing, because a declared + // extension member element returns false. The rationale is an + // extension member is not extending itself. + // FIXME(jcollins-g): Remove concrete implementation after [Extendable] is + // implemented. + bool get isExtended => false; + + Container _definingEnclosingContainer; + + Container get definingEnclosingContainer { + if (_definingEnclosingContainer == null) { + _definingEnclosingContainer = + ModelElement.fromElement(element.enclosingElement, packageGraph); + } + return _definingEnclosingContainer; + } + + @override + Set get features { + Set _features = super.features; + if (isOverride) _features.add('override'); + if (isInherited) _features.add('inherited'); + if (isCovariant) _features.add('covariant'); + if (isExtended) _features.add('extended'); + return _features; + } + + bool _canonicalEnclosingContainerIsSet = false; + Container _canonicalEnclosingContainer; + + Container get canonicalEnclosingContainer { + if (!_canonicalEnclosingContainerIsSet) { + _canonicalEnclosingContainer = computeCanonicalEnclosingContainer(); + _canonicalEnclosingContainerIsSet = true; + assert(_canonicalEnclosingContainer == null || + _canonicalEnclosingContainer.isDocumented); + } + return _canonicalEnclosingContainer; + } + + Container computeCanonicalEnclosingContainer() { + // TODO(jcollins-g): move Extension specific code to [Extendable] + if (enclosingElement is! Extension || + (enclosingElement is Extension && enclosingElement.isDocumented)) { + return packageGraph + .findCanonicalModelElementFor(enclosingElement.element); + } + return null; + } +} diff --git a/lib/src/model/documentable.dart b/lib/src/model/documentable.dart new file mode 100644 index 0000000000..52addfc02f --- /dev/null +++ b/lib/src/model/documentable.dart @@ -0,0 +1,81 @@ +// 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:dartdoc/src/dartdoc_options.dart'; +import 'package:dartdoc/src/markdown_processor.dart'; +import 'package:dartdoc/src/package_meta.dart'; +import 'package:path/path.dart' as path; + +import 'model.dart'; + +/// Bridges the gap between model elements and packages, +/// both of which have documentation. +abstract class Documentable extends Nameable { + String get documentation; + + String get documentationAsHtml; + + bool get hasDocumentation; + + bool get hasExtendedDocumentation; + + String get oneLineDoc; + + PackageGraph get packageGraph; + + bool get isDocumented; + + DartdocOptionContext get config; +} + +/// For a given package, indicate with this enum whether it should be documented +/// [local]ly, whether we should treat the package as [missing] and any references +/// to it made canonical to this package, or [remote], indicating that +/// we can build hrefs to an external source. +enum DocumentLocation { + local, + missing, + remote, +} + +abstract class MarkdownFileDocumentation + implements Documentable, Canonicalization { + DocumentLocation get documentedWhere; + + @override + String get documentation => documentationFile?.contents; + + Documentation __documentation; + + Documentation get _documentation { + if (__documentation != null) return __documentation; + __documentation = Documentation.forElement(this); + return __documentation; + } + + @override + String get documentationAsHtml => _documentation.asHtml; + + @override + bool get hasDocumentation => + documentationFile != null && documentationFile.contents.isNotEmpty; + + @override + bool get hasExtendedDocumentation => + documentation != null && documentation.isNotEmpty; + + @override + bool get isDocumented; + + @override + String get oneLineDoc => __documentation.asOneLiner; + + FileContents get documentationFile; + + @override + String get location => path.toUri(documentationFile.file.path).toString(); + + @override + Set get locationPieces => Set.from([location]); +} diff --git a/lib/src/model/dynamic.dart b/lib/src/model/dynamic.dart new file mode 100644 index 0000000000..f8a9636a67 --- /dev/null +++ b/lib/src/model/dynamic.dart @@ -0,0 +1,30 @@ +// 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/element/element.dart'; +import 'package:dartdoc/src/model/model.dart'; + +class Dynamic extends ModelElement { + Dynamic(Element element, PackageGraph packageGraph) + : super(element, null, packageGraph, null); + + /// [dynamic] is not a real object, and so we can't document it, so there + /// can be nothing canonical for it. + @override + ModelElement get canonicalModelElement => null; + + @override + ModelElement get enclosingElement => throw UnsupportedError(''); + + /// And similiarly, even if someone references it directly it can have + /// no hyperlink. + @override + String get href => null; + + @override + String get kind => 'dynamic'; + + @override + String get linkedName => 'dynamic'; +} diff --git a/lib/src/model/enclosed_element.dart b/lib/src/model/enclosed_element.dart new file mode 100644 index 0000000000..228be4b30a --- /dev/null +++ b/lib/src/model/enclosed_element.dart @@ -0,0 +1,12 @@ +// 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:dartdoc/src/model/model.dart'; + +/// An element that is enclosed by some other element. +/// +/// Libraries are not enclosed. +abstract class EnclosedElement { + ModelElement get enclosingElement; +} diff --git a/lib/src/model/enum.dart b/lib/src/model/enum.dart new file mode 100644 index 0000000000..0ff67e468a --- /dev/null +++ b/lib/src/model/enum.dart @@ -0,0 +1,104 @@ +// 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. + +// TODO(jcollins-g): Consider Enum as subclass of Container? +import 'package:analyzer/dart/element/element.dart'; +import 'package:dartdoc/src/model/model.dart'; + +class Enum extends Class { + Enum(ClassElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph); + + List _instanceProperties; + + @override + List get instanceProperties { + if (_instanceProperties == null) { + _instanceProperties = super + .instanceProperties + .map((Field p) => ModelElement.from( + p.element, p.library, p.packageGraph, + getter: p.getter, setter: p.setter) as EnumField) + .toList(growable: false); + } + return _instanceProperties; + } + + @override + String get kind => 'enum'; +} + +/// Enum's fields are virtual, so we do a little work to create +/// usable values for the docs. +class EnumField extends Field { + int _index; + + EnumField(FieldElement element, Library library, PackageGraph packageGraph, + Accessor getter, Accessor setter) + : super(element, library, packageGraph, getter, setter); + + EnumField.forConstant(this._index, FieldElement element, Library library, + PackageGraph packageGraph, Accessor getter) + : super(element, library, packageGraph, getter, null); + + @override + String get constantValueBase { + if (name == 'values') { + return 'const List<${field.enclosingElement.name}>'; + } else { + return 'const ${field.enclosingElement.name}($_index)'; + } + } + + @override + List get documentationFrom { + if (name == 'values' || name == 'index') return [this]; + return super.documentationFrom; + } + + @override + String get documentation { + if (name == 'values') { + return 'A constant List of the values in this enum, in order of their declaration.'; + } else if (name == 'index') { + return 'The integer index of this enum.'; + } else { + return super.documentation; + } + } + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(!(canonicalLibrary == null || canonicalEnclosingContainer == null)); + assert(canonicalLibrary == library); + assert(canonicalEnclosingContainer == enclosingElement); + return '${package.baseHref}${enclosingElement.library.dirName}/${(enclosingElement as Class).fileName}'; + } + + @override + String get linkedName => name; + + @override + bool get isCanonical { + if (name == 'index') return false; + // If this is something inherited from Object, e.g. hashCode, let the + // normal rules apply. + if (_index == null) { + return super.isCanonical; + } + // TODO(jcollins-g): We don't actually document this as a separate entity; + // do that or change this to false and deal with the + // consequences. + return true; + } + + @override + String get oneLineDoc => documentationAsHtml; + + @override + Inheritable get overriddenElement => null; +} diff --git a/lib/src/model/extendable.dart b/lib/src/model/extendable.dart new file mode 100644 index 0000000000..bb496c6b4f --- /dev/null +++ b/lib/src/model/extendable.dart @@ -0,0 +1,15 @@ +// 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:dartdoc/src/model/model.dart'; + +/// Mixin for subclasses of [ModelElement] representing Elements that can be +/// extension methods. +mixin Extendable on ContainerMember { + /// Returns this Extendable from the [Extension] in which it was declared. + Extendable get definingExtension => throw UnimplementedError; + + @override + Container get canonicalEnclosingContainer => throw UnimplementedError; +} diff --git a/lib/src/model/extension.dart b/lib/src/model/extension.dart new file mode 100644 index 0000000000..6755a7066c --- /dev/null +++ b/lib/src/model/extension.dart @@ -0,0 +1,155 @@ +// 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 'dart:collection'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/src/dart/element/element.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:quiver/iterables.dart' as quiver; + +/// Extension methods +class Extension extends Container + with TypeParameters, Categorization + implements EnclosedElement { + DefinedElementType extendedType; + + Extension( + ExtensionElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph) { + extendedType = + ElementType.from(_extension.extendedType, library, packageGraph); + } + + bool couldApplyTo(Class c) => _couldApplyTo(c.modelType); + + /// Return true if this extension could apply to [t]. + bool _couldApplyTo(DefinedElementType t) { + return t.instantiatedType == extendedType.instantiatedType || + (t.instantiatedType.element == extendedType.instantiatedType.element && + isSubtypeOf(t)) || + isBoundSupertypeTo(t); + } + + /// The instantiated to bounds [extendedType] of this extension is a subtype of + /// [t]. + bool isSubtypeOf(DefinedElementType t) => packageGraph.typeSystem + .isSubtypeOf(extendedType.instantiatedType, t.instantiatedType); + + bool isBoundSupertypeTo(DefinedElementType t) => + _isBoundSupertypeTo(t.type, HashSet()); + + /// Returns true if at least one supertype (including via mixins and + /// interfaces) is equivalent to or a subtype of [extendedType] when + /// instantiated to bounds. + bool _isBoundSupertypeTo( + InterfaceType superType, HashSet visited) { + ClassElement superClass = superType?.element; + if (visited.contains(superType)) return false; + visited.add(superType); + if (superClass == extendedType.type.element && + (superType == extendedType.instantiatedType || + packageGraph.typeSystem + .isSubtypeOf(superType, extendedType.instantiatedType))) { + return true; + } + List supertypes = []; + ClassElementImpl.collectAllSupertypes(supertypes, superType, null); + for (InterfaceType toVisit in supertypes) { + if (_isBoundSupertypeTo(toVisit, visited)) return true; + } + return false; + } + + @override + ModelElement get enclosingElement => library; + + ExtensionElement get _extension => (element as ExtensionElement); + + @override + String get kind => 'extension'; + + List _methods; + + @override + List get methods { + if (_methods == null) { + _methods = _extension.methods.map((e) { + return ModelElement.from(e, library, packageGraph) as Method; + }).toList(growable: false) + ..sort(byName); + } + return _methods; + } + + List _fields; + + @override + List get allFields { + if (_fields == null) { + _fields = _extension.fields.map((f) { + Accessor getter, setter; + if (f.getter != null) { + getter = ContainerAccessor(f.getter, library, packageGraph); + } + if (f.setter != null) { + setter = ContainerAccessor(f.setter, library, packageGraph); + } + return ModelElement.from(f, library, packageGraph, + getter: getter, setter: setter) as Field; + }).toList(growable: false) + ..sort(byName); + } + return _fields; + } + + List _typeParameters; + + // a stronger hash? + @override + List get typeParameters { + if (_typeParameters == null) { + _typeParameters = _extension.typeParameters.map((f) { + var lib = Library(f.enclosingElement.library, packageGraph); + return ModelElement.from(f, lib, packageGraph) as TypeParameter; + }).toList(); + } + return _typeParameters; + } + + @override + ParameterizedElementType get modelType => super.modelType; + + List _allModelElements; + + List get allModelElements { + if (_allModelElements == null) { + _allModelElements = List.from( + quiver.concat([ + instanceMethods, + allInstanceFields, + allAccessors, + allOperators, + constants, + staticMethods, + staticProperties, + typeParameters, + ]), + growable: false); + } + return _allModelElements; + } + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(canonicalLibrary != null); + assert(canonicalLibrary == library); + return '${package.baseHref}${library.dirName}/$fileName'; + } +} diff --git a/lib/src/model/field.dart b/lib/src/model/field.dart new file mode 100644 index 0000000000..fe8c27ace5 --- /dev/null +++ b/lib/src/model/field.dart @@ -0,0 +1,189 @@ +// 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/element/element.dart'; +import 'package:analyzer/src/dart/element/element.dart'; +import 'package:dartdoc/src/model/model.dart'; + +class Field extends ModelElement + with GetterSetterCombo, ContainerMember, Inheritable + implements EnclosedElement { + bool _isInherited = false; + Container _enclosingClass; + @override + final ContainerAccessor getter; + @override + final ContainerAccessor setter; + + Field(FieldElement element, Library library, PackageGraph packageGraph, + this.getter, this.setter) + : super(element, library, packageGraph, null) { + assert(getter != null || setter != null); + if (getter != null) getter.enclosingCombo = this; + if (setter != null) setter.enclosingCombo = this; + _setModelType(); + } + + factory Field.inherited( + FieldElement element, + Class enclosingClass, + Library library, + PackageGraph packageGraph, + Accessor getter, + Accessor setter) { + Field newField = Field(element, library, packageGraph, getter, setter); + newField._isInherited = true; + newField._enclosingClass = enclosingClass; + // Can't set _isInherited to true if this is the defining element, because + // that would mean it isn't inherited. + assert(newField.enclosingElement != newField.definingEnclosingContainer); + return newField; + } + + @override + String get documentation { + // Verify that hasSetter and hasGetterNoSetter are mutually exclusive, + // to prevent displaying more or less than one summary. + if (isPublic) { + Set assertCheck = Set() + ..addAll([hasPublicSetter, hasPublicGetterNoSetter]); + assert(assertCheck.containsAll([true, false])); + } + documentationFrom; + return super.documentation; + } + + @override + ModelElement get enclosingElement { + if (_enclosingClass == null) { + _enclosingClass = + ModelElement.from(field.enclosingElement, library, packageGraph); + } + return _enclosingClass; + } + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(canonicalLibrary != null); + assert(canonicalEnclosingContainer == enclosingElement); + assert(canonicalLibrary == library); + return '${package.baseHref}${enclosingElement.library.dirName}/${enclosingElement.name}/$fileName'; + } + + @override + bool get isConst => field.isConst; + + /// Returns true if the FieldElement is covariant, or if the first parameter + /// for the setter is covariant. + @override + bool get isCovariant => + setter?.isCovariant == true || (field as FieldElementImpl).isCovariant; + + @override + bool get isFinal { + /// isFinal returns true for the field even if it has an explicit getter + /// (which means we should not document it as "final"). + if (hasExplicitGetter) return false; + return field.isFinal; + } + + @override + bool get isInherited => _isInherited; + + @override + String get kind => isConst ? 'constant' : 'property'; + + @override + List get annotations { + List all_annotations = List(); + all_annotations.addAll(super.annotations); + + if (element is PropertyInducingElement) { + var pie = element as PropertyInducingElement; + all_annotations.addAll(annotationsFromMetadata(pie.getter?.metadata)); + all_annotations.addAll(annotationsFromMetadata(pie.setter?.metadata)); + } + return all_annotations.toList(growable: false); + } + + @override + Set get features { + Set allFeatures = super.features..addAll(comboFeatures); + // Combo features can indicate 'inherited' and 'override' if + // either the getter or setter has one of those properties, but that's not + // really specific enough for [Field]s that have public getter/setters. + if (hasPublicGetter && hasPublicSetter) { + if (getter.isInherited && setter.isInherited) { + allFeatures.add('inherited'); + } else { + allFeatures.remove('inherited'); + if (getter.isInherited) allFeatures.add('inherited-getter'); + if (setter.isInherited) allFeatures.add('inherited-setter'); + } + if (getter.isOverride && setter.isOverride) { + allFeatures.add('override'); + } else { + allFeatures.remove('override'); + if (getter.isOverride) allFeatures.add('override-getter'); + if (setter.isOverride) allFeatures.add('override-setter'); + } + } else { + if (isInherited) allFeatures.add('inherited'); + if (isOverride) allFeatures.add('override'); + } + return allFeatures; + } + + @override + String computeDocumentationComment() { + String docs = getterSetterDocumentationComment; + if (docs.isEmpty) return field.documentationComment; + return docs; + } + + FieldElement get field => (element as FieldElement); + + @override + String get fileName => isConst ? '$name-constant.html' : '$name.html'; + + String _sourceCode; + + @override + String get sourceCode { + if (_sourceCode == null) { + // We could use a set to figure the dupes out, but that would lose ordering. + String fieldSourceCode = modelNode.sourceCode ?? ''; + String getterSourceCode = getter?.sourceCode ?? ''; + String setterSourceCode = setter?.sourceCode ?? ''; + StringBuffer buffer = StringBuffer(); + if (fieldSourceCode.isNotEmpty) { + buffer.write(fieldSourceCode); + } + if (buffer.isNotEmpty) buffer.write('\n\n'); + if (fieldSourceCode != getterSourceCode) { + if (getterSourceCode != setterSourceCode) { + buffer.write(getterSourceCode); + if (buffer.isNotEmpty) buffer.write('\n\n'); + } + } + if (fieldSourceCode != setterSourceCode) { + buffer.write(setterSourceCode); + } + _sourceCode = buffer.toString(); + } + return _sourceCode; + } + + void _setModelType() { + if (hasGetter) { + setModelType(getter.modelType); + } + } + + @override + Inheritable get overriddenElement => null; +} diff --git a/lib/src/model/getter_setter_combo.dart b/lib/src/model/getter_setter_combo.dart new file mode 100644 index 0000000000..8684fa4952 --- /dev/null +++ b/lib/src/model/getter_setter_combo.dart @@ -0,0 +1,231 @@ +// 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 'dart:convert'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer/src/dart/element/element.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/utils.dart'; + +/// Mixin for top-level variables and fields (aka properties) +mixin GetterSetterCombo on ModelElement { + Accessor get getter; + + Accessor get setter; + + Iterable get allAccessors sync* { + for (Accessor a in [getter, setter]) { + if (a != null) yield a; + } + } + + Set get comboFeatures { + Set allFeatures = Set(); + if (hasExplicitGetter && hasPublicGetter) { + allFeatures.addAll(getter.features); + } + if (hasExplicitSetter && hasPublicSetter) { + allFeatures.addAll(setter.features); + } + if (readOnly && !isFinal && !isConst) allFeatures.add('read-only'); + if (writeOnly) allFeatures.add('write-only'); + if (readWrite) allFeatures.add('read / write'); + return allFeatures; + } + + @override + ModelElement enclosingElement; + + bool get isInherited; + + Expression get constantInitializer => + (element as ConstVariableElement).constantInitializer; + + String linkifyConstantValue(String original) { + if (constantInitializer is! InstanceCreationExpression) return original; + String constructorName = (constantInitializer as InstanceCreationExpression) + .constructorName + .toString(); + Element staticElement = + (constantInitializer as InstanceCreationExpression).staticElement; + Constructor target = ModelElement.fromElement(staticElement, packageGraph); + Class targetClass = target.enclosingElement; + // TODO(jcollins-g): this logic really should be integrated into Constructor, + // but that's not trivial because of linkedName's usage. + if (targetClass.name == target.name) { + return original.replaceAll(constructorName, "${target.linkedName}"); + } + return original.replaceAll("${targetClass.name}.${target.name}", + "${targetClass.linkedName}.${target.linkedName}"); + } + + String _buildConstantValueBase() { + String result = constantInitializer?.toString() ?? ''; + return const HtmlEscape(HtmlEscapeMode.unknown).convert(result); + } + + @override + CharacterLocation get characterLocation { + // Handle all synthetic possibilities. Ordinarily, warnings for + // explicit setters/getters will be handled by those objects, but + // if a warning comes up for an enclosing synthetic field we have to + // put it somewhere. So pick an accessor. + if (element.isSynthetic) { + if (hasExplicitGetter) return getter.characterLocation; + if (hasExplicitSetter) return setter.characterLocation; + assert(false, 'Field and accessors can not all be synthetic'); + } + return super.characterLocation; + } + + String get constantValue => linkifyConstantValue(constantValueBase); + + String get constantValueTruncated => + linkifyConstantValue(truncateString(constantValueBase, 200)); + String _constantValueBase; + + String get constantValueBase => + _constantValueBase ??= _buildConstantValueBase(); + + bool get hasPublicGetter => hasGetter && getter.isPublic; + + bool get hasPublicSetter => hasSetter && setter.isPublic; + + @override + bool get isPublic => hasPublicGetter || hasPublicSetter; + + List _documentationFrom; + + @override + List get documentationFrom { + if (_documentationFrom == null) { + _documentationFrom = []; + if (hasPublicGetter) { + _documentationFrom.addAll(getter.documentationFrom); + } else if (hasPublicSetter) { + _documentationFrom.addAll(setter.documentationFrom); + } + if (_documentationFrom.isEmpty || + _documentationFrom.every((e) => e.documentationComment == '')) { + _documentationFrom = computeDocumentationFrom; + } + } + return _documentationFrom; + } + + bool get hasAccessorsWithDocs => (hasPublicGetter && + !getter.isSynthetic && + getter.documentation.isNotEmpty || + hasPublicSetter && + !setter.isSynthetic && + setter.documentation.isNotEmpty); + + bool get getterSetterBothAvailable => (hasPublicGetter && + getter.documentation.isNotEmpty && + hasPublicSetter && + setter.documentation.isNotEmpty); + + String _oneLineDoc; + + @override + String get oneLineDoc { + if (_oneLineDoc == null) { + if (!hasAccessorsWithDocs) { + _oneLineDoc = computeOneLineDoc(); + } else { + StringBuffer buffer = StringBuffer(); + if (hasPublicGetter && getter.oneLineDoc.isNotEmpty) { + buffer.write('${getter.oneLineDoc}'); + } + if (hasPublicSetter && setter.oneLineDoc.isNotEmpty) { + buffer.write('${getterSetterBothAvailable ? "" : setter.oneLineDoc}'); + } + _oneLineDoc = buffer.toString(); + } + } + return _oneLineDoc; + } + + String get getterSetterDocumentationComment { + var buffer = StringBuffer(); + + if (hasPublicGetter && !getter.isSynthetic) { + assert(getter.documentationFrom.length == 1); + // We have to check against dropTextFrom here since documentationFrom + // doesn't yield the real elements for GetterSetterCombos. + if (!config.dropTextFrom + .contains(getter.documentationFrom.first.element.library.name)) { + String docs = getter.documentationFrom.first.documentationComment; + if (docs != null) buffer.write(docs); + } + } + + if (hasPublicSetter && !setter.isSynthetic) { + assert(setter.documentationFrom.length == 1); + if (!config.dropTextFrom + .contains(setter.documentationFrom.first.element.library.name)) { + String docs = setter.documentationFrom.first.documentationComment; + if (docs != null) { + if (buffer.isNotEmpty) buffer.write('\n\n'); + buffer.write(docs); + } + } + } + return buffer.toString(); + } + + String get linkedReturnType { + if (hasGetter) { + return getter.linkedReturnType; + } else { + return setter.linkedParamsNoMetadataOrNames; + } + } + + @override + bool get canHaveParameters => hasSetter; + + @override + List get parameters => setter.parameters; + + @override + String get linkedParamsNoMetadata { + if (hasSetter) return setter.linkedParamsNoMetadata; + return null; + } + + bool get hasExplicitGetter => hasPublicGetter && !getter.isSynthetic; + + bool get hasExplicitSetter => hasPublicSetter && !setter.isSynthetic; + + bool get hasGetter => getter != null; + + bool get hasNoGetterSetter => !hasGetterOrSetter; + + bool get hasGetterOrSetter => hasExplicitGetter || hasExplicitSetter; + + bool get hasSetter => setter != null; + + bool get hasPublicGetterNoSetter => (hasPublicGetter && !hasPublicSetter); + + String get arrow { + // → + if (readOnly) return r'→'; + // ← + if (writeOnly) return r'←'; + // ↔ + if (readWrite) return r'↔'; + throw UnsupportedError( + 'GetterSetterCombo must be one of readOnly, writeOnly, or readWrite'); + } + + bool get readOnly => hasPublicGetter && !hasPublicSetter; + + bool get readWrite => hasPublicGetter && hasPublicSetter; + + bool get writeOnly => hasPublicSetter && !hasPublicGetter; +} diff --git a/lib/src/model/indexable.dart b/lib/src/model/indexable.dart new file mode 100644 index 0000000000..0e46d0d1fd --- /dev/null +++ b/lib/src/model/indexable.dart @@ -0,0 +1,14 @@ +// 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:dartdoc/src/model/model.dart'; + +/// Something able to be indexed. +abstract class Indexable implements Nameable { + String get href; + + String get kind; + + int get overriddenDepth => 0; +} diff --git a/lib/src/model/inheritable.dart b/lib/src/model/inheritable.dart new file mode 100644 index 0000000000..59423f14c6 --- /dev/null +++ b/lib/src/model/inheritable.dart @@ -0,0 +1,161 @@ +// 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/element/element.dart'; +import 'package:analyzer/src/dart/element/member.dart' show Member; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/special_elements.dart'; + +/// Mixin for subclasses of ModelElement representing Elements that can be +/// inherited from one class to another. +/// +/// We can search the inheritance chain between this instance and +/// [definingEnclosingContainer] in [Inheritable.canonicalEnclosingContainer], +/// for the canonical [Class] closest to where this member was defined. We +/// can then know that when we find [Inheritable.element] inside that [Class]'s +/// namespace, that's the one we should treat as canonical and implementors +/// of this class can use that knowledge to determine canonicalization. +/// +/// We pick the class closest to the [definingEnclosingElement] so that all +/// children of that class inheriting the same member will point to the same +/// place in the documentation, and we pick a canonical class because that's +/// the one in the public namespace that will be documented. +mixin Inheritable on ContainerMember { + @override + ModelElement buildCanonicalModelElement() { + if (canonicalEnclosingContainer is Extension) { + return this; + } + if (canonicalEnclosingContainer is Class) { + return (canonicalEnclosingContainer as Class) + ?.allCanonicalModelElements + ?.firstWhere( + (m) => + m.name == name && m.isPropertyAccessor == isPropertyAccessor, + orElse: () => null); + } + return null; + } + + @override + Container computeCanonicalEnclosingContainer() { + if (isInherited) { + Element searchElement = element; + searchElement = searchElement is Member + ? PackageGraph.getBasestElement(searchElement) + : searchElement; + // TODO(jcollins-g): generate warning if an inherited element's definition + // is in an intermediate non-canonical class in the inheritance chain? + Class previous; + Class previousNonSkippable; + Class found; + for (Class c in inheritance.reversed) { + // Filter out mixins. + if (c.contains(searchElement)) { + if ((packageGraph.inheritThrough.contains(previous) && + c != definingEnclosingContainer) || + (packageGraph.inheritThrough.contains(c) && + c == definingEnclosingContainer)) { + return (previousNonSkippable.memberByExample(this) as Inheritable) + .canonicalEnclosingContainer; + } + Class canonicalC = + packageGraph.findCanonicalModelElementFor(c.element); + // TODO(jcollins-g): invert this lookup so traversal is recursive + // starting from the ModelElement. + if (canonicalC != null) { + assert(canonicalC.isCanonical); + assert(canonicalC.contains(searchElement)); + found = canonicalC; + break; + } + } + previous = c; + if (!packageGraph.inheritThrough.contains(c)) { + previousNonSkippable = c; + } + } + // This is still OK because we're never supposed to cloak public + // classes. + if (definingEnclosingContainer.isCanonical && + definingEnclosingContainer.isPublic) { + assert(definingEnclosingContainer == found); + return found; + } + } + return super.computeCanonicalEnclosingContainer(); + } + + List get inheritance { + List inheritance = []; + inheritance.addAll((enclosingElement as Class).inheritanceChain); + Class object = packageGraph.specialClasses[SpecialClass.object]; + if (!inheritance.contains(definingEnclosingContainer) && + definingEnclosingContainer != null) { + assert(definingEnclosingContainer == object); + } + // Unless the code explicitly extends dart-core's Object, we won't get + // an entry here. So add it. + if (inheritance.last != object && object != null) { + inheritance.add(object); + } + assert(inheritance.where((e) => e == object).length == 1); + return inheritance; + } + + Inheritable get overriddenElement; + + bool _isOverride; + + @override + bool get isOverride { + if (_isOverride == null) { + // The canonical version of the enclosing element -- not canonicalEnclosingElement, + // as that is the element enclosing the canonical version of this element, + // two different things. Defaults to the enclosing element. + // + // We use canonical elements here where possible to deal with reexports + // as seen in Flutter. + if (enclosingElement is Extension) { + _isOverride = false; + return _isOverride; + } + Class enclosingCanonical = enclosingElement.canonicalModelElement; + // The container in which this element was defined, canonical if available. + Container definingCanonical = + definingEnclosingContainer.canonicalModelElement ?? + definingEnclosingContainer; + // The canonical version of the element we're overriding, if available. + ModelElement overriddenCanonical = + overriddenElement?.canonicalModelElement ?? overriddenElement; + + // We have to have an overridden element for it to be possible for this + // element to be an override. + _isOverride = overriddenElement != null && + // The defining class and the enclosing class for this element + // must be the same (element is defined here). + enclosingCanonical == definingCanonical && + // If the overridden element isn't public, we shouldn't be an + // override in most cases. Approximation until #1623 is fixed. + overriddenCanonical.isPublic; + assert(!(_isOverride && isInherited)); + } + return _isOverride; + } + + int _overriddenDepth; + + @override + int get overriddenDepth { + if (_overriddenDepth == null) { + _overriddenDepth = 0; + Inheritable e = this; + while (e.overriddenElement != null) { + _overriddenDepth += 1; + e = e.overriddenElement; + } + } + return _overriddenDepth; + } +} diff --git a/lib/src/model/library.dart b/lib/src/model/library.dart new file mode 100644 index 0000000000..aa54ce07e6 --- /dev/null +++ b/lib/src/model/library.dart @@ -0,0 +1,661 @@ +// 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/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/visitor.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer/src/dart/analysis/driver.dart'; +import 'package:analyzer/src/dart/element/inheritance_manager3.dart'; +import 'package:analyzer/src/generated/sdk.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/package_meta.dart' show PackageMeta; +import 'package:dartdoc/src/warnings.dart'; +import 'package:path/path.dart' as path; +import 'package:quiver/iterables.dart' as quiver; + +/// Find all hashable children of a given element that are defined in the +/// [LibraryElement] given at initialization. +class _HashableChildLibraryElementVisitor + extends GeneralizingElementVisitor { + final void Function(Element) libraryProcessor; + + _HashableChildLibraryElementVisitor(this.libraryProcessor); + + @override + void visitElement(Element element) { + libraryProcessor(element); + super.visitElement(element); + return null; + } + + @override + void visitExportElement(ExportElement element) { + // [ExportElement]s are not always hashable; skip them. + return null; + } + + @override + void visitImportElement(ImportElement element) { + // [ImportElement]s are not always hashable; skip them. + return null; + } + + @override + void visitParameterElement(ParameterElement element) { + // [ParameterElement]s without names do not provide sufficiently distinct + // hashes / comparison, so just skip them all. (dart-lang/sdk#30146) + return null; + } +} + +class Library extends ModelElement with Categorization, TopLevelContainer { + List _variables; + List _exportedAndLocalElements; + String _name; + + factory Library(LibraryElement element, PackageGraph packageGraph) { + return packageGraph.findButDoNotCreateLibraryFor(element); + } + + Library.fromLibraryResult(ResolvedLibraryResult libraryResult, + PackageGraph packageGraph, this._package) + : super(libraryResult.element, null, packageGraph, null) { + if (element == null) throw ArgumentError.notNull('element'); + + // Initialize [packageGraph]'s cache of ModelNodes for relevant + // elements in this library. + Map _compilationUnitMap = Map(); + _compilationUnitMap.addEntries(libraryResult.units + .map((ResolvedUnitResult u) => MapEntry(u.path, u.unit))); + _HashableChildLibraryElementVisitor((Element e) => + packageGraph.populateModelNodeFor(e, _compilationUnitMap)) + .visitElement(element); + + // Initialize the list of elements defined in this library and + // exported via its export directives. + Set exportedAndLocalElements = + _libraryElement.exportNamespace.definedNames.values.toSet(); + // TODO(jcollins-g): Consider switch to [_libraryElement.topLevelElements]. + exportedAndLocalElements + .addAll(getDefinedElements(_libraryElement.definingCompilationUnit)); + for (CompilationUnitElement cu in _libraryElement.parts) { + exportedAndLocalElements.addAll(getDefinedElements(cu)); + } + _exportedAndLocalElements = exportedAndLocalElements.toList(); + + _package.allLibraries.add(this); + } + + static Iterable getDefinedElements( + CompilationUnitElement compilationUnit) { + return quiver.concat([ + compilationUnit.accessors, + compilationUnit.enums, + compilationUnit.extensions, + compilationUnit.functions, + compilationUnit.functionTypeAliases, + compilationUnit.mixins, + compilationUnit.topLevelVariables, + compilationUnit.types, + ]); + } + + List _allOriginalModelElementNames; + + bool get isInSdk => _libraryElement.isInSdk; + + final Package _package; + + @override + Package get package { + // Everything must be in a package. TODO(jcollins-g): Support other things + // that look like packages. + assert(_package != null); + return _package; + } + + /// [allModelElements] resolved to their original names. + /// + /// A collection of [ModelElement.fullyQualifiedName]s for [ModelElement]s + /// documented with this library, but these ModelElements and names correspond + /// to the defining library where each originally came from with respect + /// to inheritance and reexporting. Most useful for error reporting. + Iterable get allOriginalModelElementNames { + if (_allOriginalModelElementNames == null) { + _allOriginalModelElementNames = allModelElements.map((e) { + Accessor getter; + Accessor setter; + if (e is GetterSetterCombo) { + if (e.hasGetter) { + getter = ModelElement.fromElement(e.getter.element, packageGraph); + } + if (e.hasSetter) { + setter = ModelElement.fromElement(e.setter.element, packageGraph); + } + } + return ModelElement.from( + e.element, + packageGraph.findButDoNotCreateLibraryFor(e.element), + packageGraph, + getter: getter, + setter: setter) + .fullyQualifiedName; + }).toList(); + } + return _allOriginalModelElementNames; + } + + @override + CharacterLocation get characterLocation { + if (element.nameOffset == -1) { + assert(isAnonymous, + 'Only anonymous libraries are allowed to have no declared location'); + return CharacterLocation(1, 1); + } + return super.characterLocation; + } + + @override + CompilationUnitElement get compilationUnitElement => + (element as LibraryElement).definingCompilationUnit; + + @override + Iterable get classes => allClasses.where((c) => !c.isErrorOrException); + + List _extensions; + + @override + Iterable get extensions { + if (_extensions == null) { + _extensions = _exportedAndLocalElements + .whereType() + .map((e) => ModelElement.from(e, this, packageGraph) as Extension) + .toList(growable: false) + ..sort(byName); + } + return _extensions; + } + + SdkLibrary get sdkLib { + if (packageGraph.sdkLibrarySources.containsKey(element.librarySource)) { + return packageGraph.sdkLibrarySources[element.librarySource]; + } + return null; + } + + @override + bool get isPublic { + if (!super.isPublic) return false; + if (sdkLib != null && (sdkLib.isInternal || !sdkLib.isDocumented)) { + return false; + } + if (config.isLibraryExcluded(name) || + config.isLibraryExcluded(element.librarySource.uri.toString())) { + return false; + } + return true; + } + + List _constants; + + @override + Iterable get constants { + if (_constants == null) { + // _getVariables() is already sorted. + _constants = + _getVariables().where((v) => v.isConst).toList(growable: false); + } + return _constants; + } + + Set _packageImportedExportedLibraries; + + /// Returns all libraries either imported by or exported by any public library + /// this library's package. (Not [PackageGraph], but sharing a package name). + /// + /// Note: will still contain non-public libraries because those can be + /// imported or exported. + // TODO(jcollins-g): move this to [Package] once it really knows about + // more than one package. + Set get packageImportedExportedLibraries { + if (_packageImportedExportedLibraries == null) { + _packageImportedExportedLibraries = Set(); + packageGraph.publicLibraries + .where((l) => l.packageName == packageName) + .forEach((l) { + _packageImportedExportedLibraries.addAll(l.importedExportedLibraries); + }); + } + return _packageImportedExportedLibraries; + } + + Set _importedExportedLibraries; + + /// Returns all libraries either imported by or exported by this library, + /// recursively. + Set get importedExportedLibraries { + if (_importedExportedLibraries == null) { + _importedExportedLibraries = Set(); + Set importedExportedLibraryElements = Set(); + importedExportedLibraryElements + .addAll((element as LibraryElement).importedLibraries); + importedExportedLibraryElements + .addAll((element as LibraryElement).exportedLibraries); + for (LibraryElement l in importedExportedLibraryElements) { + Library lib = ModelElement.from(l, library, packageGraph); + _importedExportedLibraries.add(lib); + _importedExportedLibraries.addAll(lib.importedExportedLibraries); + } + } + return _importedExportedLibraries; + } + + Map> _prefixToLibrary; + + /// Map of import prefixes ('import "foo" as prefix;') to [Library]. + Map> get prefixToLibrary { + if (_prefixToLibrary == null) { + _prefixToLibrary = {}; + // It is possible to have overlapping prefixes. + for (ImportElement i in (element as LibraryElement).imports) { + // Ignore invalid imports. + if (i.prefix?.name != null && i.importedLibrary != null) { + _prefixToLibrary.putIfAbsent(i.prefix?.name, () => Set()); + _prefixToLibrary[i.prefix?.name] + .add(ModelElement.from(i.importedLibrary, library, packageGraph)); + } + } + } + return _prefixToLibrary; + } + + String _dirName; + + String get dirName { + if (_dirName == null) { + _dirName = name; + if (isAnonymous) { + _dirName = nameFromPath; + } + _dirName = _dirName.replaceAll(':', '-').replaceAll('/', '_'); + } + return _dirName; + } + + Set _canonicalFor; + + Set get canonicalFor { + if (_canonicalFor == null) { + // TODO(jcollins-g): restructure to avoid using side effects. + buildDocumentationAddition(documentationComment); + } + return _canonicalFor; + } + + /// Hide canonicalFor from doc while leaving a note to ourselves to + /// help with ambiguous canonicalization determination. + /// + /// Example: + /// {@canonicalFor libname.ClassName} + @override + String buildDocumentationAddition(String rawDocs) { + rawDocs = super.buildDocumentationAddition(rawDocs); + Set newCanonicalFor = Set(); + Set notFoundInAllModelElements = Set(); + final canonicalRegExp = RegExp(r'{@canonicalFor\s([^}]+)}'); + rawDocs = rawDocs.replaceAllMapped(canonicalRegExp, (Match match) { + newCanonicalFor.add(match.group(1)); + notFoundInAllModelElements.add(match.group(1)); + return ''; + }); + if (notFoundInAllModelElements.isNotEmpty) { + notFoundInAllModelElements.removeAll(allOriginalModelElementNames); + } + for (String notFound in notFoundInAllModelElements) { + warn(PackageWarning.ignoredCanonicalFor, message: notFound); + } + // TODO(jcollins-g): warn if a macro/tool _does_ generate an unexpected + // canonicalFor? + if (_canonicalFor == null) { + _canonicalFor = newCanonicalFor; + } + return rawDocs; + } + + /// Libraries are not enclosed by anything. + @override + ModelElement get enclosingElement => null; + + List _enums; + + @override + List get enums { + if (_enums == null) { + _enums = _exportedAndLocalElements + .whereType() + .where((element) => element.isEnum) + .map((e) => ModelElement.from(e, this, packageGraph) as Enum) + .toList(growable: false) + ..sort(byName); + } + return _enums; + } + + List _mixins; + + @override + List get mixins { + if (_mixins == null) { + /// Can not be [MixinElementImpl] because [ClassHandle]s are sometimes + /// returned from _exportedElements. + _mixins = _exportedAndLocalElements + .whereType() + .where((ClassElement c) => c.isMixin) + .map((e) => ModelElement.from(e, this, packageGraph) as Mixin) + .toList(growable: false) + ..sort(byName); + } + return _mixins; + } + + List _exceptions; + + @override + List get exceptions { + if (_exceptions == null) { + _exceptions = + allClasses.where((c) => c.isErrorOrException).toList(growable: false); + } + return _exceptions; + } + + @override + String get fileName => '$dirName-library.html'; + + List _functions; + + @override + List get functions { + if (_functions == null) { + _functions = + _exportedAndLocalElements.whereType().map((e) { + return ModelElement.from(e, this, packageGraph) as ModelFunction; + }).toList(growable: false) + ..sort(byName); + } + return _functions; + } + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + return '${package.baseHref}${library.dirName}/$fileName'; + } + + InheritanceManager3 _inheritanceManager; + + InheritanceManager3 get inheritanceManager { + if (_inheritanceManager == null) { + var typeSystem = element.context.typeSystem; + _inheritanceManager = InheritanceManager3(typeSystem); + } + return _inheritanceManager; + } + + bool get isAnonymous => element.name == null || element.name.isEmpty; + + @override + String get kind => 'library'; + + @override + Library get library => this; + + @override + String get name { + if (_name == null) { + _name = getLibraryName(element); + } + return _name; + } + + String _nameFromPath; + + /// Generate a name for this library based on its location. + /// + /// nameFromPath provides filename collision-proofing for anonymous libraries + /// by incorporating more from the location of the anonymous library into + /// the name calculation. Simple cases (such as an anonymous library in + /// 'lib') are the same, but this will include slashes and possibly colons + /// for anonymous libraries in subdirectories or other packages. + String get nameFromPath { + if (_nameFromPath == null) { + _nameFromPath = getNameFromPath(element, packageGraph.driver, package); + } + return _nameFromPath; + } + + /// The real package, as opposed to the package we are documenting it with, + /// [PackageGraph.name] + String get packageName => packageMeta?.name ?? ''; + + /// The real packageMeta, as opposed to the package we are documenting with. + PackageMeta _packageMeta; + + PackageMeta get packageMeta { + if (_packageMeta == null) { + _packageMeta = PackageMeta.fromElement(element, config); + } + return _packageMeta; + } + + List _properties; + + /// All variables ("properties") except constants. + @override + Iterable get properties { + if (_properties == null) { + _properties = + _getVariables().where((v) => !v.isConst).toList(growable: false); + } + return _properties; + } + + List _typedefs; + + @override + List get typedefs { + if (_typedefs == null) { + _typedefs = _exportedAndLocalElements + .whereType() + .map((e) => ModelElement.from(e, this, packageGraph) as Typedef) + .toList(growable: false) + ..sort(byName); + } + return _typedefs; + } + + List _classes; + + List get allClasses { + if (_classes == null) { + _classes = _exportedAndLocalElements + .whereType() + .where((e) => !e.isMixin && !e.isEnum) + .map((e) => ModelElement.from(e, this, packageGraph) as Class) + .toList(growable: false) + ..sort(byName); + } + return _classes; + } + + LibraryElement get _libraryElement => (element as LibraryElement); + + Class getClassByName(String name) { + return allClasses.firstWhere((it) => it.name == name, orElse: () => null); + } + + List _getVariables() { + if (_variables == null) { + Set elements = _exportedAndLocalElements + .whereType() + .toSet(); + elements.addAll(_exportedAndLocalElements + .whereType() + .map((a) => a.variable)); + _variables = []; + for (TopLevelVariableElement element in elements) { + Accessor getter; + if (element.getter != null) { + getter = ModelElement.from(element.getter, this, packageGraph); + } + Accessor setter; + if (element.setter != null) { + setter = ModelElement.from(element.setter, this, packageGraph); + } + ModelElement me = ModelElement.from(element, this, packageGraph, + getter: getter, setter: setter); + _variables.add(me); + } + _variables.sort(byName); + } + return _variables; + } + + /// Reverses URIs if needed to get a package URI. + /// Not the same as [PackageGraph.name] because there we always strip all + /// path components; this function only strips the package prefix if the + /// library is part of the default package or if it is being documented + /// remotely. + static String getNameFromPath( + LibraryElement element, AnalysisDriver driver, Package package) { + String name; + if (element.source.uri.toString().startsWith('dart:')) { + name = element.source.uri.toString(); + } else { + name = driver.sourceFactory.restoreUri(element.source).toString(); + } + PackageMeta hidePackage; + if (package.documentedWhere == DocumentLocation.remote) { + hidePackage = package.packageMeta; + } else { + hidePackage = package.packageGraph.packageMeta; + } + // restoreUri must not result in another file URI. + assert(!name.startsWith('file:')); + + String defaultPackagePrefix = 'package:$hidePackage/'; + if (name.startsWith(defaultPackagePrefix)) { + name = name.substring(defaultPackagePrefix.length, name.length); + } + if (name.endsWith('.dart')) { + name = name.substring(0, name.length - '.dart'.length); + } + assert(!name.startsWith('file:')); + return name; + } + + static String getLibraryName(LibraryElement element) { + var source = element.source; + + if (source.uri.isScheme('dart')) { + return '${source.uri}'; + } + + var name = element.name; + if (name != null && name.isNotEmpty) { + return name; + } + + name = path.basename(source.fullName); + if (name.endsWith('.dart')) { + name = name.substring(0, name.length - '.dart'.length); + } + return name; + } + + Map> _modelElementsNameMap; + + /// Map of [fullyQualifiedNameWithoutLibrary] to all matching [ModelElement]s + /// in this library. Used for code reference lookups. + Map> get modelElementsNameMap { + if (_modelElementsNameMap == null) { + _modelElementsNameMap = Map>(); + allModelElements.forEach((ModelElement modelElement) { + _modelElementsNameMap.putIfAbsent( + modelElement.fullyQualifiedNameWithoutLibrary, () => Set()); + _modelElementsNameMap[modelElement.fullyQualifiedNameWithoutLibrary] + .add(modelElement); + }); + } + return _modelElementsNameMap; + } + + Map> _modelElementsMap; + + Map> get modelElementsMap { + if (_modelElementsMap == null) { + Iterable results = quiver.concat([ + library.constants, + library.functions, + library.properties, + library.typedefs, + library.extensions.expand((e) { + return quiver.concat([ + [e], + e.allModelElements + ]); + }), + library.allClasses.expand((c) { + return quiver.concat([ + [c], + c.allModelElements + ]); + }), + library.enums.expand((e) { + return quiver.concat([ + [e], + e.allModelElements + ]); + }), + library.mixins.expand((m) { + return quiver.concat([ + [m], + m.allModelElements + ]); + }), + ]); + _modelElementsMap = Map>(); + results.forEach((modelElement) { + _modelElementsMap.putIfAbsent(modelElement.element, () => Set()); + _modelElementsMap[modelElement.element].add(modelElement); + }); + _modelElementsMap.putIfAbsent(element, () => Set()); + _modelElementsMap[element].add(this); + } + return _modelElementsMap; + } + + List _allModelElements; + + Iterable get allModelElements { + if (_allModelElements == null) { + _allModelElements = []; + for (Set modelElements in modelElementsMap.values) { + _allModelElements.addAll(modelElements); + } + } + return _allModelElements; + } + + List _allCanonicalModelElements; + + Iterable get allCanonicalModelElements { + return (_allCanonicalModelElements ??= + allModelElements.where((e) => e.isCanonical).toList()); + } +} diff --git a/lib/src/model/library_container.dart b/lib/src/model/library_container.dart new file mode 100644 index 0000000000..73930ec8ab --- /dev/null +++ b/lib/src/model/library_container.dart @@ -0,0 +1,63 @@ +// 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:collection/collection.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/model_utils.dart' as model_utils; + +/// A set of libraries, initialized after construction by accessing [libraries]. +/// Do not cache return values of any methods or members excepting [libraries] +/// and [name] before finishing initialization of a [LibraryContainer]. +abstract class LibraryContainer + implements Nameable, Comparable { + final List libraries = []; + + PackageGraph get packageGraph; + + Iterable get publicLibraries => + model_utils.filterNonPublic(libraries); + + bool get hasPublicLibraries => publicLibraries.isNotEmpty; + + /// The name of the container or object that this LibraryContainer is a part + /// of. Used for sorting in [containerOrder]. + String get enclosingName; + + /// Order by which this container should be sorted. + List get containerOrder; + + /// Sorting key. [containerOrder] should contain these. + String get sortKey => name; + + /// Does this container represent the SDK? This can be false for containers + /// that only represent a part of the SDK. + bool get isSdk => false; + + /// Returns: + /// -1 if this container is listed in [containerOrder]. + /// 0 if this container is named the same as the [enclosingName]. + /// 1 if this container represents the SDK. + /// 2 if this group has a name that contains the name [enclosingName]. + /// 3 otherwise. + int get _group { + if (containerOrder.contains(sortKey)) return -1; + if (equalsIgnoreAsciiCase(sortKey, enclosingName)) return 0; + if (isSdk) return 1; + if (sortKey.toLowerCase().contains(enclosingName.toLowerCase())) return 2; + return 3; + } + + @override + int compareTo(LibraryContainer other) { + if (_group == other._group) { + if (_group == -1) { + return Comparable.compare(containerOrder.indexOf(sortKey), + containerOrder.indexOf(other.sortKey)); + } else { + return sortKey.toLowerCase().compareTo(other.sortKey.toLowerCase()); + } + } + return Comparable.compare(_group, other._group); + } +} diff --git a/lib/src/model/locatable.dart b/lib/src/model/locatable.dart new file mode 100644 index 0000000000..517c3d12d2 --- /dev/null +++ b/lib/src/model/locatable.dart @@ -0,0 +1,22 @@ +// 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. + +/// Something that can be located for warning purposes. +abstract class Locatable { + List get documentationFrom; + + /// True if documentationFrom contains only one item, [this]. + bool get documentationIsLocal => + documentationFrom.length == 1 && identical(documentationFrom.first, this); + + String get fullyQualifiedName; + + String get href; + + /// A string indicating the URI of this Locatable, usually derived from + /// [Element.location]. + String get location; +} + +final RegExp locationSplitter = RegExp(r'(package:|[\\/;.])'); diff --git a/lib/src/model/method.dart b/lib/src/model/method.dart new file mode 100644 index 0000000000..1156ae2a47 --- /dev/null +++ b/lib/src/model/method.dart @@ -0,0 +1,121 @@ +// 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/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer/src/dart/element/member.dart' show Member; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; + +class Method extends ModelElement + with ContainerMember, Inheritable, TypeParameters + implements EnclosedElement { + bool _isInherited = false; + Container _enclosingContainer; + @override + List typeParameters = []; + + Method(MethodElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph, null) { + _calcTypeParameters(); + } + + Method.inherited(MethodElement element, this._enclosingContainer, + Library library, PackageGraph packageGraph, + {Member originalMember}) + : super(element, library, packageGraph, originalMember) { + _isInherited = true; + _calcTypeParameters(); + } + + void _calcTypeParameters() { + typeParameters = _method.typeParameters.map((f) { + return ModelElement.from(f, library, packageGraph) as TypeParameter; + }).toList(); + } + + @override + CharacterLocation get characterLocation { + if (enclosingElement is Enum && name == 'toString') { + // The toString() method on Enums is special, treated as not having + // a definition location by the analyzer yet not being inherited, either. + // Just directly override our location with the Enum definition -- + // this is OK because Enums can not inherit from each other nor + // have their definitions split between files. + return enclosingElement.characterLocation; + } + return super.characterLocation; + } + + @override + ModelElement get enclosingElement { + if (_enclosingContainer == null) { + _enclosingContainer = + ModelElement.from(_method.enclosingElement, library, packageGraph); + } + return _enclosingContainer; + } + + String get fullkind { + if (_method.isAbstract) return 'abstract $kind'; + return kind; + } + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(!(canonicalLibrary == null || canonicalEnclosingContainer == null)); + assert(canonicalLibrary == library); + assert(canonicalEnclosingContainer == enclosingElement); + return '${package.baseHref}${enclosingElement.library.dirName}/${enclosingElement.name}/${fileName}'; + } + + @override + bool get isInherited => _isInherited; + + bool get isOperator => false; + + @override + Set get features { + Set allFeatures = super.features; + if (isInherited) allFeatures.add('inherited'); + return allFeatures; + } + + @override + bool get isStatic => _method.isStatic; + + @override + String get kind => 'method'; + + String get linkedReturnType => modelType.createLinkedReturnTypeName(); + + @override + DefinedElementType get modelType => super.modelType; + + @override + Method get overriddenElement { + if (_enclosingContainer is Extension) { + return null; + } + ClassElement parent = element.enclosingElement; + for (InterfaceType t in parent.allSupertypes) { + Element e = t.getMethod(element.name); + if (e != null) { + assert(e.enclosingElement is ClassElement); + return ModelElement.fromElement(e, packageGraph); + } + } + return null; + } + + MethodElement get _method => (element as MethodElement); + + /// Methods can not be covariant; always returns false. + @override + bool get isCovariant => false; +} diff --git a/lib/src/model/mixin.dart b/lib/src/model/mixin.dart new file mode 100644 index 0000000000..c9fe4f7ea2 --- /dev/null +++ b/lib/src/model/mixin.dart @@ -0,0 +1,71 @@ +// 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/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/model_utils.dart' as model_utils; +import 'package:dartdoc/src/special_elements.dart'; + +/// Implements the Dart 2.1 "mixin" style of mixin declarations. +class Mixin extends Class { + Mixin(ClassElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph); + + @override + bool get isAbstract => false; + + List _inheritanceChain; + + @override + List get inheritanceChain { + if (_inheritanceChain == null) { + _inheritanceChain = []; + _inheritanceChain.add(this); + + // Mix-in interfaces come before other interfaces. + _inheritanceChain.addAll(superclassConstraints + .expand((ParameterizedElementType i) => + (i.element as Class).inheritanceChain) + .where((Class c) => + c != packageGraph.specialClasses[SpecialClass.object])); + + // Interfaces need to come last, because classes in the superChain might + // implement them even when they aren't mentioned. + _inheritanceChain.addAll( + interfaces.expand((e) => (e.element as Class).inheritanceChain)); + } + return _inheritanceChain.toList(growable: false); + } + + List _superclassConstraints; + + /// Returns a list of superclass constraints for this mixin. + Iterable get superclassConstraints { + if (_superclassConstraints == null) { + _superclassConstraints = (element as ClassElement) + .superclassConstraints + .map( + (InterfaceType i) => ElementType.from(i, library, packageGraph)) + .toList(); + } + return _superclassConstraints; + } + + bool get hasPublicSuperclassConstraints => + publicSuperclassConstraints.isNotEmpty; + + Iterable get publicSuperclassConstraints => + model_utils.filterNonPublic(superclassConstraints); + + @override + bool get hasModifiers => super.hasModifiers || hasPublicSuperclassConstraints; + + @override + String get fileName => "${name}-mixin.html"; + + @override + String get kind => 'mixin'; +} diff --git a/lib/src/model/model.dart b/lib/src/model/model.dart new file mode 100644 index 0000000000..d691c68575 --- /dev/null +++ b/lib/src/model/model.dart @@ -0,0 +1,42 @@ +// 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. + +export 'accessor.dart'; +export 'canonicalization.dart'; +export 'categorization.dart'; +export 'category.dart'; +export 'class.dart'; +export 'constructor.dart'; +export 'container.dart'; +export 'container_member.dart'; +export 'documentable.dart'; +export 'dynamic.dart'; +export 'enclosed_element.dart'; +export 'enum.dart'; +export 'extendable.dart'; +export 'extension.dart'; +export 'field.dart'; +export 'getter_setter_combo.dart'; +export 'indexable.dart'; +export 'inheritable.dart'; +export 'library.dart'; +export 'library_container.dart'; +export 'locatable.dart'; +export 'method.dart'; +export 'mixin.dart'; +export 'model_element.dart'; +export 'model_function.dart'; +export 'model_node.dart'; +export 'nameable.dart'; +export 'operator.dart'; +export 'package.dart'; +export 'package_builder.dart'; +export 'package_graph.dart'; +export 'parameter.dart'; +export 'privacy.dart'; +export 'source_code_mixin.dart'; +export 'top_level_container.dart'; +export 'top_level_variable.dart'; +export 'type_parameter.dart'; +export 'typedef.dart'; diff --git a/lib/src/model/model_element.dart b/lib/src/model/model_element.dart new file mode 100644 index 0000000000..3ef9b30a6b --- /dev/null +++ b/lib/src/model/model_element.dart @@ -0,0 +1,1777 @@ +// 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:async'; +import 'dart:collection' show UnmodifiableListView; +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:analyzer/src/dart/element/element.dart'; +import 'package:analyzer/src/dart/element/member.dart' + show ExecutableMember, Member, ParameterMember; +import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer/src/generated/source_io.dart'; +import 'package:args/args.dart'; +import 'package:collection/collection.dart'; +import 'package:crypto/crypto.dart'; +import 'package:dartdoc/src/dartdoc_options.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/logging.dart'; +import 'package:dartdoc/src/markdown_processor.dart' show Documentation; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/model_utils.dart' as utils; +import 'package:dartdoc/src/source_linker.dart'; +import 'package:dartdoc/src/tuple.dart'; +import 'package:dartdoc/src/utils.dart'; +import 'package:dartdoc/src/warnings.dart'; +import 'package:path/path.dart' as path; + +/// Items mapped less than zero will sort before custom annotations. +/// Items mapped above zero are sorted after custom annotations. +/// Items mapped to zero will sort alphabetically among custom annotations. +/// Custom annotations are assumed to be any annotation or feature not in this +/// map. +const Map featureOrder = { + 'read-only': 1, + 'write-only': 1, + 'read / write': 1, + 'covariant': 2, + 'final': 2, + 'inherited': 3, + 'inherited-getter': 3, + 'inherited-setter': 3, + 'override': 3, + 'override-getter': 3, + 'override-setter': 3, + 'extended': 3, +}; + +int byFeatureOrdering(String a, String b) { + int scoreA = 0; + int scoreB = 0; + + if (featureOrder.containsKey(a)) scoreA = featureOrder[a]; + if (featureOrder.containsKey(b)) scoreB = featureOrder[b]; + + if (scoreA < scoreB) return -1; + if (scoreA > scoreB) return 1; + return compareAsciiLowerCaseNatural(a, b); +} + +/// This doc may need to be processed in case it has a template or html +/// fragment. +final needsPrecacheRegExp = RegExp(r'{@(template|tool|inject-html)'); + +final templateRegExp = RegExp( + r'[ ]*{@template\s+(.+?)}([\s\S]+?){@endtemplate}[ ]*\n?', + multiLine: true); +final htmlRegExp = RegExp( + r'[ ]*{@inject-html\s*}([\s\S]+?){@end-inject-html}[ ]*\n?', + multiLine: true); +final htmlInjectRegExp = RegExp(r'([a-f0-9]+)'); + +// Matches all tool directives (even some invalid ones). This is so +// we can give good error messages if the directive is malformed, instead of +// just silently emitting it as-is. +final basicToolRegExp = RegExp( + r'[ ]*{@tool\s+([^}]+)}\n?([\s\S]+?)\n?{@end-tool}[ ]*\n?', + multiLine: true); + +/// Regexp to take care of splitting arguments, and handling the quotes +/// around arguments, if any. +/// +/// Match group 1 is the "foo=" (or "--foo=") part of the option, if any. +/// Match group 2 contains the quote character used (which is discarded). +/// Match group 3 is a quoted arg, if any, without the quotes. +/// Match group 4 is the unquoted arg, if any. +final RegExp argMatcher = RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name + r'(?:' // Start a new non-capture group for the two possibilities. + r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes. + r'([^ ]+))'); // without quotes. + +final macroRegExp = RegExp(r'{@macro\s+([^}]+)}'); + +// 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) { + Iterable inheritables = e.inheritedElements + .map((ee) => ModelElement.fromElement(ee, packageGraph) as Inheritable); + Inheritable foundInheritable; + int lowIndex = enclosingClass.inheritanceChain.length; + for (var inheritable in inheritables) { + int index = + enclosingClass.inheritanceChain.indexOf(inheritable.enclosingElement); + if (index < lowIndex) { + foundInheritable = inheritable; + lowIndex = index; + } + } + return ModelElement.from(foundInheritable.element, library, packageGraph, + enclosingContainer: enclosingClass); +} + +/// 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 + with Privacy, Warnable, Locatable, Nameable, SourceCodeMixin, Indexable + implements Comparable, Documentable { + final Element _element; + + // TODO(jcollins-g): This really wants a "member that has a type" class. + final Member _originalMember; + final Library _library; + + ElementType _modelType; + String _rawDocs; + Documentation __documentation; + UnmodifiableListView _parameters; + String _linkedName; + + String _fullyQualifiedName; + String _fullyQualifiedNameWithoutLibrary; + + // TODO(jcollins-g): make _originalMember optional after dart-lang/sdk#15101 + // is fixed. + ModelElement( + this._element, this._library, this._packageGraph, this._originalMember); + + factory ModelElement.fromElement(Element e, PackageGraph p) { + Library lib = p.findButDoNotCreateLibraryFor(e); + Accessor getter; + Accessor setter; + if (e is PropertyInducingElement) { + getter = e.getter != null ? ModelElement.from(e.getter, lib, p) : null; + setter = e.setter != null ? ModelElement.from(e.setter, lib, p) : null; + } + return ModelElement.from(e, lib, p, getter: getter, setter: setter); + } + + // TODO(jcollins-g): this way of using the optional parameter is messy, + // clean that up. + // TODO(jcollins-g): Refactor this into class-specific factories that + // call this one. + // 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. + /// Do not construct any ModelElements unless they are from this constructor. + /// Specify enclosingContainer if and only if this is to be an inherited or + /// extended object. + factory ModelElement.from( + Element e, Library library, PackageGraph packageGraph, + {Container enclosingContainer, Accessor getter, Accessor setter}) { + assert(packageGraph != null && e != null); + assert(library != null || + e is ParameterElement || + e is TypeParameterElement || + e is GenericFunctionTypeElementImpl || + e.kind == ElementKind.DYNAMIC); + + Member originalMember; + // TODO(jcollins-g): Refactor object model to instantiate 'ModelMembers' + // for members? + if (e is Member) { + var basest = PackageGraph.getBasestElement(e); + originalMember = e; + e = basest; + } + Tuple3 key = + Tuple3(e, library, enclosingContainer); + ModelElement newModelElement; + if (e.kind != ElementKind.DYNAMIC && + packageGraph.allConstructedModelElements.containsKey(key)) { + newModelElement = packageGraph.allConstructedModelElements[key]; + assert(newModelElement.element is! MultiplyInheritedExecutableElement); + } else { + if (e.kind == ElementKind.DYNAMIC) { + newModelElement = Dynamic(e, packageGraph); + } + if (e is MultiplyInheritedExecutableElement) { + newModelElement = resolveMultiplyInheritedElement( + e, library, packageGraph, enclosingContainer); + } else { + if (e is LibraryElement) { + newModelElement = Library(e, packageGraph); + } + // Also handles enums + if (e is ClassElement) { + if (e.isMixin) { + newModelElement = Mixin(e, library, packageGraph); + } else if (e.isEnum) { + newModelElement = Enum(e, library, packageGraph); + } else { + newModelElement = Class(e, library, packageGraph); + } + } + if (e is ExtensionElement) { + newModelElement = Extension(e, library, packageGraph); + } + if (e is FunctionElement) { + newModelElement = ModelFunction(e, library, packageGraph); + } else if (e is GenericFunctionTypeElement) { + // TODO(scheglov) "e" cannot be both GenericFunctionTypeElement, + // and FunctionTypeAliasElement or GenericTypeAliasElement. + if (e is FunctionTypeAliasElement) { + assert(e.name != ''); + newModelElement = ModelFunctionTypedef(e, library, packageGraph); + } else { + if (e.enclosingElement is GenericTypeAliasElement) { + assert(e.enclosingElement.name != ''); + newModelElement = ModelFunctionTypedef(e, library, packageGraph); + } else { + // Allowing null here is allowed as a workaround for + // dart-lang/sdk#32005. + assert(e.name == '' || e.name == null); + newModelElement = ModelFunctionAnonymous(e, packageGraph); + } + } + } + if (e is FunctionTypeAliasElement) { + newModelElement = Typedef(e, library, packageGraph); + } + if (e is FieldElement) { + if (enclosingContainer == null) { + if (e.isEnumConstant) { + int index = + e.computeConstantValue().getField(e.name).toIntValue(); + newModelElement = EnumField.forConstant( + index, e, library, packageGraph, getter); + // ignore: unnecessary_cast + } else if (e.enclosingElement is ExtensionElement) { + newModelElement = Field(e, library, packageGraph, getter, setter); + } else if (e.enclosingElement is ClassElement && + (e.enclosingElement as ClassElement).isEnum) { + newModelElement = + EnumField(e, library, packageGraph, getter, setter); + } else { + newModelElement = Field(e, library, packageGraph, getter, setter); + } + } else { + // EnumFields can't be inherited, so this case is simpler. + newModelElement = Field.inherited( + e, enclosingContainer, library, packageGraph, getter, setter); + } + } + if (e is ConstructorElement) { + newModelElement = Constructor(e, library, packageGraph); + } + if (e is MethodElement && e.isOperator) { + if (enclosingContainer == null) { + newModelElement = Operator(e, library, packageGraph); + } else { + newModelElement = Operator.inherited( + e, enclosingContainer, library, packageGraph, + originalMember: originalMember); + } + } + if (e is MethodElement && !e.isOperator) { + if (enclosingContainer == null) { + newModelElement = Method(e, library, packageGraph); + } else { + newModelElement = Method.inherited( + e, enclosingContainer, library, packageGraph, + originalMember: originalMember); + } + } + if (e is TopLevelVariableElement) { + assert(getter != null || setter != null); + newModelElement = + TopLevelVariable(e, library, packageGraph, getter, setter); + } + if (e is PropertyAccessorElement) { + // TODO(jcollins-g): why test for ClassElement in enclosingElement? + if (e.enclosingElement is ClassElement || + e is MultiplyInheritedExecutableElement) { + if (enclosingContainer == null) { + newModelElement = ContainerAccessor(e, library, packageGraph); + } else { + newModelElement = ContainerAccessor.inherited( + e, library, packageGraph, enclosingContainer, + originalMember: originalMember); + } + } else { + newModelElement = Accessor(e, library, packageGraph, null); + } + } + if (e is TypeParameterElement) { + newModelElement = TypeParameter(e, library, packageGraph); + } + if (e is ParameterElement) { + newModelElement = Parameter(e, library, packageGraph, + originalMember: originalMember); + } + } + } + + if (newModelElement == null) throw "Unknown type ${e.runtimeType}"; + if (enclosingContainer != null) assert(newModelElement is Inheritable); + // TODO(jcollins-g): Reenable Parameter caching when dart-lang/sdk#30146 + // is fixed? + if (library != null && newModelElement is! Parameter) { + library.packageGraph.allConstructedModelElements[key] = newModelElement; + if (newModelElement is Inheritable) { + Tuple2 iKey = Tuple2(e, library); + library.packageGraph.allInheritableElements + .putIfAbsent(iKey, () => Set()); + library.packageGraph.allInheritableElements[iKey].add(newModelElement); + } + } + if (newModelElement is GetterSetterCombo) { + assert(getter == null || newModelElement?.getter?.enclosingCombo != null); + assert(setter == null || newModelElement?.setter?.enclosingCombo != null); + } + + assert(newModelElement.element is! MultiplyInheritedExecutableElement); + return newModelElement; + } + + /// Stub for mustache4dart, or it will search enclosing elements to find + /// names for members. + bool get hasCategoryNames => false; + + Set get exportedInLibraries { + return library + .packageGraph.libraryElementReexportedBy[this.element.library]; + } + + ModelNode _modelNode; + + @override + ModelNode get modelNode => + _modelNode ??= packageGraph.getModelNodeFor(element); + + List get annotations => annotationsFromMetadata(element.metadata); + + /// Returns linked annotations from a given metadata set, with escaping. + List annotationsFromMetadata(List md) { + List annotationStrings = []; + if (md == null) return annotationStrings; + for (ElementAnnotation a in md) { + String annotation = (const HtmlEscape()).convert(a.toSource()); + Element annotationElement = a.element; + + ClassElement annotationClassElement; + if (annotationElement is ExecutableElement) { + annotationElement = + (annotationElement as ExecutableElement).returnType.element; + } + if (annotationElement is ClassElement) { + annotationClassElement = annotationElement; + } + ModelElement annotationModelElement = + packageGraph.findCanonicalModelElementFor(annotationElement); + // annotationElement can be null if the element can't be resolved. + Class annotationClass = packageGraph + .findCanonicalModelElementFor(annotationClassElement) as Class; + if (annotationClass == null && + annotationElement != null && + annotationClassElement != null) { + annotationClass = + ModelElement.fromElement(annotationClassElement, packageGraph) + as Class; + } + // Some annotations are intended to be invisible (@pragma) + if (annotationClass == null || + !packageGraph.invisibleAnnotations.contains(annotationClass)) { + if (annotationModelElement != null) { + annotation = annotation.replaceFirst( + annotationModelElement.name, annotationModelElement.linkedName); + } + annotationStrings.add(annotation); + } + } + return annotationStrings; + } + + bool _isPublic; + + @override + bool get isPublic { + if (_isPublic == null) { + if (name == '') { + _isPublic = false; + } else if (this is! Library && (library == null || !library.isPublic)) { + _isPublic = false; + } else if (enclosingElement is Class && + !(enclosingElement as Class).isPublic) { + _isPublic = false; + } else if (enclosingElement is Extension && + !(enclosingElement as Extension).isPublic) { + _isPublic = false; + } else { + String docComment = documentationComment; + if (docComment == null) { + _isPublic = utils.hasPublicName(element); + } else { + _isPublic = utils.hasPublicName(element) && + !(docComment.contains('@nodoc') || + docComment.contains('')); + } + } + } + return _isPublic; + } + + List _commentRefs; + + @override + List get commentRefs { + if (_commentRefs == null) { + _commentRefs = []; + for (ModelElement from in documentationFrom) { + List checkReferences = [from]; + if (from is Accessor) { + checkReferences.add(from.enclosingCombo); + } + for (ModelElement e in checkReferences) { + _commentRefs.addAll(e.modelNode.commentRefs ?? []); + } + } + } + return _commentRefs; + } + + DartdocOptionContext _config; + + @override + DartdocOptionContext get config { + if (_config == null) { + _config = + DartdocOptionContext.fromContextElement(packageGraph.config, element); + } + return _config; + } + + @override + Set get locationPieces { + return Set.from(element.location + .toString() + .split(locationSplitter) + .where((s) => s.isNotEmpty)); + } + + Set get features { + Set allFeatures = Set(); + allFeatures.addAll(annotations); + + // Replace the @override annotation with a feature that explicitly + // indicates whether an override has occurred. + allFeatures.remove('@override'); + + // Drop the plain "deprecated" annotation, that's indicated via + // strikethroughs. Custom @Deprecated() will still appear. + allFeatures.remove('@deprecated'); + // const and static are not needed here because const/static elements get + // their own sections in the doc. + if (isFinal) allFeatures.add('final'); + return allFeatures; + } + + String get featuresAsString { + List allFeatures = features.toList()..sort(byFeatureOrdering); + return allFeatures.join(', '); + } + + bool get canHaveParameters => + element is ExecutableElement || + element is FunctionTypedElement || + element is FunctionTypeAliasElement; + + ModelElement buildCanonicalModelElement() { + Container preferredClass; + if (enclosingElement is Class || enclosingElement is Extension) { + preferredClass = enclosingElement; + } + return packageGraph.findCanonicalModelElementFor(element, + preferredClass: preferredClass); + } + + // Returns the canonical ModelElement for this ModelElement, or null + // if there isn't one. + ModelElement _canonicalModelElement; + + ModelElement get canonicalModelElement => + _canonicalModelElement ??= buildCanonicalModelElement(); + + List _documentationFrom; + + // TODO(jcollins-g): untangle when mixins can call super + @override + List get documentationFrom { + if (_documentationFrom == null) { + _documentationFrom = computeDocumentationFrom; + } + return _documentationFrom; + } + + bool get hasSourceHref => sourceHref.isNotEmpty; + String _sourceHref; + + String get sourceHref { + _sourceHref ??= SourceLinker.fromElement(this).href(); + return _sourceHref; + } + + /// Returns the ModelElement(s) from which we will get documentation. + /// Can be more than one if this is a Field composing documentation from + /// multiple Accessors. + /// + /// This getter will walk up the inheritance hierarchy + /// to find docs, if the current class doesn't have docs + /// for this element. + List get computeDocumentationFrom { + List docFrom; + + if (documentationComment == null && + canOverride() && + this is Inheritable && + (this as Inheritable).overriddenElement != null) { + docFrom = (this as Inheritable).overriddenElement.documentationFrom; + } else if (this is Inheritable && (this as Inheritable).isInherited) { + Inheritable thisInheritable = (this as Inheritable); + ModelElement fromThis = ModelElement.fromElement( + element, thisInheritable.definingEnclosingContainer.packageGraph); + docFrom = fromThis.documentationFrom; + } else { + docFrom = [this]; + } + return docFrom; + } + + String _buildDocumentationLocal() => _buildDocumentationBaseSync(); + + /// Override this to add more features to the documentation builder in a + /// subclass. + String buildDocumentationAddition(String docs) => docs ??= ''; + + /// Separate from _buildDocumentationLocal for overriding. + String _buildDocumentationBaseSync() { + assert(_rawDocs == null, + 'reentrant calls to _buildDocumentation* not allowed'); + // Do not use the sync method if we need to evaluate tools or templates. + assert(!isCanonical || + !needsPrecacheRegExp.hasMatch(documentationComment ?? '')); + if (config.dropTextFrom.contains(element.library.name)) { + _rawDocs = ''; + } else { + _rawDocs = documentationComment ?? ''; + _rawDocs = stripComments(_rawDocs) ?? ''; + _rawDocs = _injectExamples(_rawDocs); + _rawDocs = _injectYouTube(_rawDocs); + _rawDocs = _injectAnimations(_rawDocs); + _rawDocs = _stripHtmlAndAddToIndex(_rawDocs); + } + _rawDocs = buildDocumentationAddition(_rawDocs); + return _rawDocs; + } + + /// Separate from _buildDocumentationLocal for overriding. Can only be + /// used as part of [PackageGraph.setUpPackageGraph]. + Future _buildDocumentationBase() async { + assert(_rawDocs == null, + 'reentrant calls to _buildDocumentation* not allowed'); + // Do not use the sync method if we need to evaluate tools or templates. + if (config.dropTextFrom.contains(element.library.name)) { + _rawDocs = ''; + } else { + _rawDocs = documentationComment ?? ''; + _rawDocs = stripComments(_rawDocs) ?? ''; + // Must evaluate tools first, in case they insert any other directives. + _rawDocs = await _evaluateTools(_rawDocs); + _rawDocs = _injectExamples(_rawDocs); + _rawDocs = _injectYouTube(_rawDocs); + _rawDocs = _injectAnimations(_rawDocs); + _rawDocs = _stripMacroTemplatesAndAddToIndex(_rawDocs); + _rawDocs = _stripHtmlAndAddToIndex(_rawDocs); + } + _rawDocs = buildDocumentationAddition(_rawDocs); + return _rawDocs; + } + + /// Returns the documentation for this literal element unless + /// [config.dropTextFrom] indicates it should not be returned. Macro + /// definitions are stripped, but macros themselves are not injected. This + /// is a two stage process to avoid ordering problems. + String _documentationLocal; + + String get documentationLocal => + _documentationLocal ??= _buildDocumentationLocal(); + + /// Returns the docs, stripped of their leading comments syntax. + @override + String get documentation { + return _injectMacros( + documentationFrom.map((e) => e.documentationLocal).join('

')); + } + + Library get definingLibrary => + packageGraph.findButDoNotCreateLibraryFor(element); + + Library _canonicalLibrary; + + // _canonicalLibrary can be null so we can't check against null to see whether + // we tried to compute it before. + bool _canonicalLibraryIsSet = false; + + @override + Library get canonicalLibrary { + if (!_canonicalLibraryIsSet) { + // This is not accurate if we are constructing the Package. + assert(packageGraph.allLibrariesAdded); + // Since we're may be looking for a library, find the [Element] immediately + // contained by a [CompilationUnitElement] in the tree. + Element topLevelElement = element; + while (topLevelElement != null && + topLevelElement.enclosingElement is! LibraryElement && + topLevelElement.enclosingElement is! CompilationUnitElement && + topLevelElement.enclosingElement != null) { + topLevelElement = topLevelElement.enclosingElement; + } + + // Privately named elements can never have a canonical library, so + // just shortcut them out. + if (!utils.hasPublicName(element)) { + _canonicalLibrary = null; + } else if (!packageGraph.localPublicLibraries.contains(definingLibrary)) { + List candidateLibraries = definingLibrary.exportedInLibraries + ?.where((l) => + l.isPublic && + l.package.documentedWhere != DocumentLocation.missing) + ?.toList(); + + if (candidateLibraries != null) { + candidateLibraries = candidateLibraries.where((l) { + Element lookup = (l.element as LibraryElement) + .exportNamespace + .definedNames[topLevelElement?.name]; + if (lookup is PropertyAccessorElement) { + lookup = (lookup as PropertyAccessorElement).variable; + } + if (topLevelElement == lookup) return true; + return false; + }).toList(); + + // 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)) { + warn(PackageWarning.reexportedPrivateApiAcrossPackages, + message: definingLibrary.package.fullyQualifiedName, + referredFrom: candidateLibraries); + } else { + candidateLibraries + .removeWhere((l) => l.package != definingLibrary.package); + } + + // Start with our top-level element. + ModelElement warnable = + ModelElement.fromElement(topLevelElement, packageGraph); + if (candidateLibraries.length > 1) { + // Heuristic scoring to determine which library a human likely + // considers this element to be primarily 'from', and therefore, + // canonical. Still warn if the heuristic isn't that confident. + List scoredCandidates = + warnable.scoreCanonicalCandidates(candidateLibraries); + candidateLibraries = + scoredCandidates.map((s) => s.library).toList(); + double secondHighestScore = + scoredCandidates[scoredCandidates.length - 2].score; + double highestScore = scoredCandidates.last.score; + double confidence = highestScore - secondHighestScore; + String message = + "${candidateLibraries.map((l) => l.name)} -> ${candidateLibraries.last.name} (confidence ${confidence.toStringAsPrecision(4)})"; + List debugLines = []; + debugLines.addAll(scoredCandidates.map((s) => '${s.toString()}')); + + if (confidence < config.ambiguousReexportScorerMinConfidence) { + warnable.warn(PackageWarning.ambiguousReexport, + message: message, extendedDebug: debugLines); + } + } + if (candidateLibraries.isNotEmpty) { + _canonicalLibrary = candidateLibraries.last; + } + } + } else { + _canonicalLibrary = definingLibrary; + } + // Only pretend when not linking to remote packages. + if (this is Inheritable && !config.linkToRemote) { + if ((this as Inheritable).isInherited && + _canonicalLibrary == null && + packageGraph.publicLibraries.contains(library)) { + // In the event we've inherited a field from an object that isn't directly reexported, + // we may need to pretend we are canonical for this. + _canonicalLibrary = library; + } + } + _canonicalLibraryIsSet = true; + } + assert(_canonicalLibrary == null || + packageGraph.publicLibraries.contains(_canonicalLibrary)); + return _canonicalLibrary; + } + + @override + bool get isCanonical { + if (library == canonicalLibrary) { + if (this is Inheritable) { + Inheritable i = (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). + if (i.enclosingElement == i.canonicalEnclosingContainer) { + return true; + } else { + return false; + } + } + // If there's no inheritance to deal with, we're done. + return true; + } + return false; + } + + String _htmlDocumentation; + + @override + String get documentationAsHtml { + if (_htmlDocumentation != null) return _htmlDocumentation; + _htmlDocumentation = _injectHtmlFragments(_documentation.asHtml); + return _htmlDocumentation; + } + + @override + Element get element => _element; + + @override + String get location { + // Call nothing from here that can emit warnings or you'll cause stack overflows. + if (characterLocation != null) { + return "(${path.toUri(sourceFileName)}:${characterLocation.toString()})"; + } + return "(${path.toUri(sourceFileName)})"; + } + + /// Returns a link to extended documentation, or the empty string if that + /// does not exist. + String get extendedDocLink { + if (hasExtendedDocumentation) { + return '[...]'; + } + return ''; + } + + String get fileName => "${name}.html"; + + /// Returns the fully qualified name. + /// + /// For example: libraryName.className.methodName + @override + String get fullyQualifiedName { + return (_fullyQualifiedName ??= _buildFullyQualifiedName()); + } + + String get fullyQualifiedNameWithoutLibrary { + // Remember, periods are legal in library names. + if (_fullyQualifiedNameWithoutLibrary == null) { + _fullyQualifiedNameWithoutLibrary = + fullyQualifiedName.replaceFirst("${library.fullyQualifiedName}.", ''); + } + return _fullyQualifiedNameWithoutLibrary; + } + + String get sourceFileName => element.source.fullName; + + CharacterLocation _characterLocation; + bool _characterLocationIsSet = false; + + @override + CharacterLocation get characterLocation { + if (!_characterLocationIsSet) { + LineInfo lineInfo = compilationUnitElement.lineInfo; + _characterLocationIsSet = true; + assert(element.nameOffset >= 0, + 'Invalid location data for element: $fullyQualifiedName'); + assert(lineInfo != null, + 'No lineInfo data available for element: $fullyQualifiedName'); + if (element.nameOffset >= 0) { + _characterLocation = lineInfo?.getLocation(element.nameOffset); + } + } + return _characterLocation; + } + + CompilationUnitElement get compilationUnitElement => + element.getAncestor((e) => e is CompilationUnitElement); + + bool get hasAnnotations => annotations.isNotEmpty; + + @override + bool get hasDocumentation => + documentation != null && documentation.isNotEmpty; + + @override + bool get hasExtendedDocumentation => + href != null && _documentation.hasExtendedDocs; + + bool get hasParameters => parameters.isNotEmpty; + + /// If canonicalLibrary (or canonicalEnclosingElement, for Inheritable + /// subclasses) is null, href should be null. + @override + String get href; + + String get htmlId => name; + + bool get isAsynchronous => + isExecutable && (element as ExecutableElement).isAsynchronous; + + 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].where((a) => a != null).toList(); + + // At least one of these should be non-null. Otherwise things are weird + assert(deprecatedValues.isNotEmpty); + + // 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); + } + + @override + bool get isDocumented => isCanonical && isPublic; + + bool get isExecutable => element is ExecutableElement; + + bool get isFinal => false; + + bool get isLocalElement => element is LocalElement; + + bool get isPropertyAccessor => element is PropertyAccessorElement; + + bool get isPropertyInducer => element is PropertyInducingElement; + + bool get isStatic { + if (isPropertyInducer) { + return (element as PropertyInducingElement).isStatic; + } + return false; + } + + /// A human-friendly name for the kind of element this is. + @override + String get kind; + + @override + Library get library => _library; + + String get linkedName { + if (_linkedName == null) { + _linkedName = _calculateLinkedName(); + } + return _linkedName; + } + + String get linkedParams => utils.linkedParams(parameters); + + String get linkedParamsLines => utils.linkedParams(parameters).trim(); + + String get linkedParamsNoMetadata => + utils.linkedParams(parameters, showMetadata: false); + + String get linkedParamsNoMetadataOrNames { + return utils.linkedParams(parameters, + showMetadata: false, showNames: false); + } + + ElementType get modelType { + if (_modelType == null) { + // TODO(jcollins-g): Need an interface for a "member with a type" (or changed object model). + if (_originalMember != null && + (_originalMember is ExecutableMember || + _originalMember is ParameterMember)) { + if (_originalMember is ExecutableMember) { + _modelType = ElementType.from( + (_originalMember as ExecutableMember).type, + library, + packageGraph); + } else { + // ParameterMember + _modelType = ElementType.from( + (_originalMember as ParameterMember).type, library, packageGraph); + } + } else if (element is ExecutableElement || + element is FunctionTypedElement || + element is ParameterElement || + element is TypeDefiningElement || + element is PropertyInducingElement) { + _modelType = + ElementType.from((element as dynamic).type, library, packageGraph); + } + } + return _modelType; + } + + void setModelType(ElementType type) { + _modelType = type; + } + + @override + String get name => element.name; + + // TODO(jcollins-g): refactor once dartdoc will only run in a VM where mixins + // calling super is allowed (SDK constraint >= 2.1.0). + String computeOneLineDoc() => + '${_documentation.asOneLiner}${extendedDocLink.isEmpty ? "" : " $extendedDocLink"}'; + String _oneLineDoc; + + @override + String get oneLineDoc { + if (_oneLineDoc == null) { + _oneLineDoc = computeOneLineDoc(); + } + return _oneLineDoc; + } + + Member get originalMember => _originalMember; + + final PackageGraph _packageGraph; + + @override + PackageGraph get packageGraph => _packageGraph; + + @override + Package get package => library.package; + + bool get isPublicAndPackageDocumented => + isPublic && library.packageGraph.packageDocumentedFor(this); + + List _allParameters; + + // TODO(jcollins-g): This is in the wrong place. Move parts to GetterSetterCombo, + // elsewhere as appropriate? + List get allParameters { + if (_allParameters == null) { + final Set recursedParameters = Set(); + final Set newParameters = Set(); + if (this is GetterSetterCombo && + (this as GetterSetterCombo).setter != null) { + newParameters.addAll((this as GetterSetterCombo).setter.parameters); + } else { + if (canHaveParameters) newParameters.addAll(parameters); + } + while (newParameters.isNotEmpty) { + recursedParameters.addAll(newParameters); + newParameters.clear(); + for (Parameter p in recursedParameters) { + var l = p.modelType.parameters + .where((pm) => !recursedParameters.contains(pm)); + newParameters.addAll(l); + } + } + _allParameters = recursedParameters.toList(); + } + return _allParameters; + } + + List get parameters { + if (!canHaveParameters) { + throw StateError("$element cannot have parameters"); + } + + if (_parameters == null) { + List params; + + if (element is ExecutableElement) { + if (_originalMember != null) { + assert(_originalMember is ExecutableMember); + params = (_originalMember as ExecutableMember).parameters; + } else { + params = (element as ExecutableElement).parameters; + } + } + if (params == null && element is FunctionTypedElement) { + if (_originalMember != null) { + params = (_originalMember as dynamic).parameters; + } else { + params = (element as FunctionTypedElement).parameters; + } + } + if (params == null && element is FunctionTypeAliasElement) { + params = (element as FunctionTypeAliasElement).function.parameters; + } + + _parameters = UnmodifiableListView(params + .map((p) => ModelElement.from(p, library, packageGraph) as Parameter) + .toList()); + } + return _parameters; + } + + @override + void warn(PackageWarning kind, + {String message, + Iterable referredFrom, + Iterable extendedDebug}) { + packageGraph.warnOnElement(this, kind, + message: message, + referredFrom: referredFrom, + extendedDebug: extendedDebug); + } + + String computeDocumentationComment() => element.documentationComment; + + bool _documentationCommentComputed = false; + String _documentationComment; + + String get documentationComment { + if (_documentationCommentComputed == false) { + _documentationComment = computeDocumentationComment(); + _documentationCommentComputed = true; + } + return _documentationComment; + } + + /// Unconditionally precache local documentation. + /// + /// Use only in factory for [PackageGraph]. + Future precacheLocalDocs() async { + _documentationLocal = await _buildDocumentationBase(); + } + + Documentation get _documentation { + if (__documentation != null) return __documentation; + __documentation = Documentation.forElement(this); + return __documentation; + } + + bool canOverride() => + element is ClassMemberElement || element is PropertyAccessorElement; + + @override + int compareTo(dynamic other) { + if (other is ModelElement) { + return name.toLowerCase().compareTo(other.name.toLowerCase()); + } else { + return 0; + } + } + + @override + String toString() => '$runtimeType $name'; + + String _buildFullyQualifiedName([ModelElement e, String fqName]) { + e ??= this; + fqName ??= e.name; + + if (e is! EnclosedElement || e.enclosingElement == null) { + return fqName; + } + + return _buildFullyQualifiedName( + e.enclosingElement, '${e.enclosingElement.name}.$fqName'); + } + + String _calculateLinkedName() { + // 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 || + this.element?.kind == ElementKind.DYNAMIC || + this is ModelFunction); + + if (href == null) { + if (isPublicAndPackageDocumented) { + warn(PackageWarning.noCanonicalFound); + } + return htmlEscape.convert(name); + } + + var classContent = isDeprecated ? ' class="deprecated"' : ''; + return '$name'; + } + + /// Replace {@example ...} in API comments with the content of named file. + /// + /// Syntax: + /// + /// {@example PATH [region=NAME] [lang=NAME]} + /// + /// If PATH is `dir/file.ext` and region is `r` then we'll look for the file + /// named `dir/file-r.ext.md`, relative to the project root directory of the + /// project for which the docs are being generated. + /// + /// Examples: (escaped in this comment to show literal values in dartdoc's + /// dartdoc) + /// + /// {@example examples/angular/quickstart/web/main.dart} + /// {@example abc/def/xyz_component.dart region=template lang=html} + /// + String _injectExamples(String rawdocs) { + final dirPath = package.packageMeta.dir.path; + RegExp exampleRE = RegExp(r'{@example\s+([^}]+)}'); + return rawdocs.replaceAllMapped(exampleRE, (match) { + var args = _getExampleArgs(match[1]); + if (args == null) { + // Already warned about an invalid parameter if this happens. + return ''; + } + var lang = + args['lang'] ?? path.extension(args['src']).replaceFirst('.', ''); + + var replacement = match[0]; // default to fully matched string. + + var fragmentFile = File(path.join(dirPath, args['file'])); + if (fragmentFile.existsSync()) { + replacement = fragmentFile.readAsStringSync(); + if (lang.isNotEmpty) { + replacement = replacement.replaceFirst('```', '```$lang'); + } + } else { + // TODO(jcollins-g): move this to Package.warn system + var filePath = + this.element.source.fullName.substring(dirPath.length + 1); + + logWarning( + 'warning: ${filePath}: @example file not found, ${fragmentFile.path}'); + } + return replacement; + }); + } + + static Future _replaceAllMappedAsync( + String string, Pattern exp, Future replace(Match match)) async { + StringBuffer replaced = StringBuffer(); + int currentIndex = 0; + for (Match match in exp.allMatches(string)) { + String prefix = match.input.substring(currentIndex, match.start); + currentIndex = match.end; + replaced..write(prefix)..write(await replace(match)); + } + replaced.write(string.substring(currentIndex)); + return replaced.toString(); + } + + /// Replace {@tool ...}{@end-tool} in API comments with the + /// output of an external tool. + /// + /// Looks for tools invocations, looks up their bound executables in the + /// options, and executes them with the source comment material as input, + /// returning the output of the tool. If a named tool isn't configured in the + /// options file, then it will not be executed, and dartdoc will quit with an + /// error. + /// + /// Tool command line arguments are passed to the tool, with the token + /// `$INPUT` replaced with the absolute path to a temporary file containing + /// the content for the tool to read and produce output from. If the tool + /// doesn't need any input, then no `$INPUT` is needed. + /// + /// Nested tool directives will not be evaluated, but tools may generate other + /// directives in their output and those will be evaluated. + /// + /// Syntax: + /// + /// {@tool TOOL [Tool arguments]} + /// Content to send to tool. + /// {@end-tool} + /// + /// Examples: + /// + /// In `dart_options.yaml`: + /// + /// ```yaml + /// dartdoc: + /// tools: + /// # Prefixes the given input with "## " + /// # Path is relative to project root. + /// prefix: "bin/prefix.dart" + /// # Prints the date + /// date: "/bin/date" + /// ``` + /// + /// In code: + /// + /// _This:_ + /// + /// {@tool prefix $INPUT} + /// Content to send to tool. + /// {@end-tool} + /// {@tool date --iso-8601=minutes --utc} + /// {@end-tool} + /// + /// _Produces:_ + /// + /// ## Content to send to tool. + /// 2018-09-18T21:15+00:00 + Future _evaluateTools(String rawDocs) async { + if (config.allowTools) { + int invocationIndex = 0; + return await _replaceAllMappedAsync(rawDocs, basicToolRegExp, + (basicMatch) async { + List args = _splitUpQuotedArgs(basicMatch[1]).toList(); + // Tool name must come first. + if (args.isEmpty) { + warn(PackageWarning.toolError, + message: + 'Must specify a tool to execute for the @tool directive.'); + return Future.value(''); + } + // Count the number of invocations of tools in this dartdoc block, + // so that tools can differentiate different blocks from each other. + invocationIndex++; + return await config.tools.runner.run( + args, + (String message) async => + warn(PackageWarning.toolError, message: message), + content: basicMatch[2], + environment: { + 'SOURCE_LINE': characterLocation?.lineNumber.toString(), + 'SOURCE_COLUMN': characterLocation?.columnNumber.toString(), + 'SOURCE_PATH': (sourceFileName == null || + package?.packagePath == null) + ? null + : path.relative(sourceFileName, from: package.packagePath), + 'PACKAGE_PATH': package?.packagePath, + 'PACKAGE_NAME': package?.name, + 'LIBRARY_NAME': library?.fullyQualifiedName, + 'ELEMENT_NAME': fullyQualifiedNameWithoutLibrary, + 'INVOCATION_INDEX': invocationIndex.toString(), + 'PACKAGE_INVOCATION_INDEX': + (package.toolInvocationIndex++).toString(), + }..removeWhere((key, value) => value == null)); + }); + } else { + return rawDocs; + } + } + + /// Replace {@youtube ...} in API comments with some HTML to embed + /// a YouTube video. + /// + /// Syntax: + /// + /// {@youtube WIDTH HEIGHT URL} + /// + /// Example: + /// + /// {@youtube 560 315 https://www.youtube.com/watch?v=oHg5SJYRHA0} + /// + /// Which will embed a YouTube player into the page that plays the specified + /// video. + /// + /// The width and height must be positive integers specifying the dimensions + /// of the video in pixels. The height and width are used to calculate the + /// aspect ratio of the video; the video is always rendered to take up all + /// available horizontal space to accommodate different screen sizes on + /// desktop and mobile. + /// + /// The video URL must have the following format: + /// https://www.youtube.com/watch?v=oHg5SJYRHA0. This format can usually be + /// found in the address bar of the browser when viewing a YouTube video. + String _injectYouTube(String rawDocs) { + // Matches all youtube directives (even some invalid ones). This is so + // we can give good error messages if the directive is malformed, instead of + // just silently emitting it as-is. + final RegExp basicAnimationRegExp = RegExp(r'''{@youtube\s+([^}]+)}'''); + + // Matches YouTube IDs from supported YouTube URLs. + final RegExp validYouTubeUrlRegExp = + RegExp('https://www\.youtube\.com/watch\\?v=([^&]+)\$'); + + return rawDocs.replaceAllMapped(basicAnimationRegExp, (basicMatch) { + final ArgParser parser = ArgParser(); + final ArgResults args = _parseArgs(basicMatch[1], parser, 'youtube'); + if (args == null) { + // Already warned about an invalid parameter if this happens. + return ''; + } + final List positionalArgs = args.rest.sublist(0); + if (positionalArgs.length != 3) { + warn(PackageWarning.invalidParameter, + message: 'Invalid @youtube directive, "${basicMatch[0]}"\n' + 'YouTube directives must be of the form "{@youtube WIDTH ' + 'HEIGHT URL}"'); + return ''; + } + + final int width = int.tryParse(positionalArgs[0]); + if (width == null || width <= 0) { + warn(PackageWarning.invalidParameter, + message: 'A @youtube directive has an invalid width, ' + '"${positionalArgs[0]}". The width must be a positive integer.'); + } + + final int height = int.tryParse(positionalArgs[1]); + if (height == null || height <= 0) { + warn(PackageWarning.invalidParameter, + message: 'A @youtube directive has an invalid height, ' + '"${positionalArgs[1]}". The height must be a positive integer.'); + } + + final Match url = validYouTubeUrlRegExp.firstMatch(positionalArgs[2]); + if (url == null) { + warn(PackageWarning.invalidParameter, + message: 'A @youtube directive has an invalid URL: ' + '"${positionalArgs[2]}". Supported YouTube URLs have the ' + 'follwing format: https://www.youtube.com/watch?v=oHg5SJYRHA0.'); + return ''; + } + final String youTubeId = url.group(url.groupCount); + final String aspectRatio = (height / width * 100).toStringAsFixed(2); + + // Blank lines before and after, and no indenting at the beginning and end + // is needed so that Markdown doesn't confuse this with code, so be + // careful of whitespace here. + return ''' + +

+ +

+ +'''; // String must end at beginning of line, or following inline text will be + // indented. + }); + } + + /// Replace {@animation ...} in API comments with some HTML to manage an + /// MPEG 4 video as an animation. + /// + /// Syntax: + /// + /// {@animation WIDTH HEIGHT URL [id=ID]} + /// + /// Example: + /// + /// {@animation 300 300 https://example.com/path/to/video.mp4 id="my_video"} + /// + /// Which will render the HTML necessary for embedding a simple click-to-play + /// HTML5 video player with no controls that has an HTML id of "my_video". + /// + /// The optional ID should be a unique id that is a valid JavaScript + /// identifier, and will be used as the id for the video tag. If no ID is + /// supplied, then a unique identifier (starting with "animation_") will be + /// generated. + /// + /// The width and height must be integers specifying the dimensions of the + /// video file in pixels. + String _injectAnimations(String rawDocs) { + // Matches all animation directives (even some invalid ones). This is so + // we can give good error messages if the directive is malformed, instead of + // just silently emitting it as-is. + final RegExp basicAnimationRegExp = RegExp(r'''{@animation\s+([^}]+)}'''); + + // Matches valid javascript identifiers. + final RegExp validIdRegExp = RegExp(r'^[a-zA-Z_]\w*$'); + + // Make sure we have a set to keep track of used IDs for this href. + package.usedAnimationIdsByHref[href] ??= {}; + + String getUniqueId(String base) { + int animationIdCount = 1; + String id = '$base$animationIdCount'; + // We check for duplicate IDs so that we make sure not to collide with + // user-supplied ids on the same page. + while (package.usedAnimationIdsByHref[href].contains(id)) { + animationIdCount++; + id = '$base$animationIdCount'; + } + return id; + } + + return rawDocs.replaceAllMapped(basicAnimationRegExp, (basicMatch) { + final ArgParser parser = ArgParser(); + parser.addOption('id'); + final ArgResults args = _parseArgs(basicMatch[1], parser, 'animation'); + if (args == null) { + // Already warned about an invalid parameter if this happens. + return ''; + } + final List positionalArgs = args.rest.sublist(0); + String uniqueId; + bool wasDeprecated = false; + if (positionalArgs.length == 4) { + // Supports the original form of the animation tag for backward + // compatibility. + uniqueId = positionalArgs.removeAt(0); + wasDeprecated = true; + } else if (positionalArgs.length == 3) { + uniqueId = args['id'] ?? getUniqueId('animation_'); + } else { + warn(PackageWarning.invalidParameter, + message: 'Invalid @animation directive, "${basicMatch[0]}"\n' + 'Animation directives must be of the form "{@animation WIDTH ' + 'HEIGHT URL [id=ID]}"'); + return ''; + } + + if (!validIdRegExp.hasMatch(uniqueId)) { + warn(PackageWarning.invalidParameter, + message: 'An animation has an invalid identifier, "$uniqueId". The ' + 'identifier can only contain letters, numbers and underscores, ' + 'and must not begin with a number.'); + return ''; + } + if (package.usedAnimationIdsByHref[href].contains(uniqueId)) { + warn(PackageWarning.invalidParameter, + message: 'An animation has a non-unique identifier, "$uniqueId". ' + 'Animation identifiers must be unique.'); + return ''; + } + package.usedAnimationIdsByHref[href].add(uniqueId); + + int width; + try { + width = int.parse(positionalArgs[0]); + } on FormatException { + warn(PackageWarning.invalidParameter, + message: 'An animation has an invalid width ($uniqueId), ' + '"${positionalArgs[0]}". The width must be an integer.'); + return ''; + } + + int height; + try { + height = int.parse(positionalArgs[1]); + } on FormatException { + warn(PackageWarning.invalidParameter, + message: 'An animation has an invalid height ($uniqueId), ' + '"${positionalArgs[1]}". The height must be an integer.'); + return ''; + } + + Uri movieUrl; + try { + movieUrl = Uri.parse(positionalArgs[2]); + } on FormatException catch (e) { + warn(PackageWarning.invalidParameter, + message: 'An animation URL could not be parsed ($uniqueId): ' + '${positionalArgs[2]}\n$e'); + return ''; + } + final String overlayId = '${uniqueId}_play_button_'; + + // Only warn about deprecation if some other warning didn't occur. + if (wasDeprecated) { + warn(PackageWarning.deprecated, + message: + 'Deprecated form of @animation directive, "${basicMatch[0]}"\n' + 'Animation directives are now of the form "{@animation ' + 'WIDTH HEIGHT URL [id=ID]}" (id is an optional ' + 'parameter)'); + } + + // Blank lines before and after, and no indenting at the beginning and end + // is needed so that Markdown doesn't confuse this with code, so be + // careful of whitespace here. + return ''' + +
+
+
+ +
+ +'''; // String must end at beginning of line, or following inline text will be + // indented. + }); + } + + /// Replace <[digest]> in API comments with + /// the contents of the HTML fragment earlier defined by the + /// {@inject-html} directive. The [digest] is a SHA1 of the contents + /// of the HTML fragment, automatically generated upon parsing the + /// {@inject-html} directive. + /// + /// This markup is generated and inserted by [_stripHtmlAndAddToIndex] when it + /// removes the HTML fragment in preparation for markdown processing. It isn't + /// meant to be used at a user level. + /// + /// Example: + /// + /// You place the fragment in a dartdoc comment: + /// + /// Some comments + /// {@inject-html} + /// <p>[HTML contents!]</p> + /// {@endtemplate} + /// More comments + /// + /// and [_stripHtmlAndAddToIndex] will replace your HTML fragment with this: + /// + /// Some comments + /// <dartdoc-html>4cc02f877240bf69855b4c7291aba8a16e5acce0</dartdoc-html> + /// More comments + /// + /// Which will render in the final HTML output as: + /// + /// Some comments + /// <p>[HTML contents!]</p> + /// More comments + /// + /// And the HTML fragment will not have been processed or changed by Markdown, + /// but just injected verbatim. + String _injectHtmlFragments(String rawDocs) { + if (!config.injectHtml) return rawDocs; + + return rawDocs.replaceAllMapped(htmlInjectRegExp, (match) { + String fragment = packageGraph.getHtmlFragment(match[1]); + if (fragment == null) { + warn(PackageWarning.unknownHtmlFragment, message: match[1]); + } + return fragment; + }); + } + + /// Replace {@macro ...} in API comments with the contents of the macro + /// + /// Syntax: + /// + /// {@macro NAME} + /// + /// Example: + /// + /// You define the template in any comment for a documentable entity like: + /// + /// {@template foo} + /// Foo contents! + /// {@endtemplate} + /// + /// and them somewhere use it like this: + /// + /// Some comments + /// {@macro foo} + /// More comments + /// + /// Which will render + /// + /// Some comments + /// Foo contents! + /// More comments + /// + String _injectMacros(String rawDocs) { + return rawDocs.replaceAllMapped(macroRegExp, (match) { + String macro = packageGraph.getMacro(match[1]); + if (macro == null) { + warn(PackageWarning.unknownMacro, message: match[1]); + } + return macro; + }); + } + + /// Parse and remove {@template ...} in API comments and store them + /// in the index on the package. + /// + /// Syntax: + /// + /// {@template NAME} + /// The contents of the macro + /// {@endtemplate} + /// + String _stripMacroTemplatesAndAddToIndex(String rawDocs) { + return rawDocs.replaceAllMapped(templateRegExp, (match) { + packageGraph.addMacro(match[1].trim(), match[2].trim()); + return "{@macro ${match[1].trim()}}"; + }); + } + + /// Parse and remove {@inject-html ...} in API comments and store + /// them in the index on the package, replacing them with a SHA1 hash of the + /// contents, where the HTML will be re-injected after Markdown processing of + /// the rest of the text is complete. + /// + /// Syntax: + /// + /// {@inject-html} + ///

The HTML to inject.

+ /// {@end-inject-html} + /// + String _stripHtmlAndAddToIndex(String rawDocs) { + if (!config.injectHtml) return rawDocs; + return rawDocs.replaceAllMapped(htmlRegExp, (match) { + String fragment = match[1]; + String digest = sha1.convert(fragment.codeUnits).toString(); + packageGraph.addHtmlFragment(digest, fragment); + // The newlines are so that Markdown will pass this through without + // touching it. + return '\n$digest\n'; + }); + } + + /// Helper to process arguments given as a (possibly quoted) string. + /// + /// First, this will split the given [argsAsString] into separate arguments, + /// taking any quoting (either ' or " are accepted) into account, including + /// handling backslash-escaped quotes. + /// + /// Then, it will prepend "--" to any args that start with an identifier + /// followed by an equals sign, allowing the argument parser to treat any + /// "foo=bar" argument as "--foo=bar". It does handle quoted args like + /// "foo='bar baz'" too, returning just bar (without quotes) for the foo + /// value. + Iterable _splitUpQuotedArgs(String argsAsString, + {bool convertToArgs = false}) { + final Iterable matches = argMatcher.allMatches(argsAsString); + // Remove quotes around args, and if convertToArgs is true, then for any + // args that look like assignments (start with valid option names followed + // by an equals sign), add a "--" in front so that they parse as options. + return matches.map((Match match) { + var option = ''; + if (convertToArgs && match[1] != null && !match[1].startsWith('-')) { + option = '--'; + } + if (match[2] != null) { + // This arg has quotes, so strip them. + return '$option${match[1] ?? ''}${match[3] ?? ''}${match[4] ?? ''}'; + } + return '$option${match[0]}'; + }); + } + + /// Helper to process arguments given as a (possibly quoted) string. + /// + /// First, this will split the given [argsAsString] into separate arguments + /// with [_splitUpQuotedArgs] it then parses the resulting argument list + /// normally with [argParser] and returns the result. + ArgResults _parseArgs( + String argsAsString, ArgParser argParser, String directiveName) { + var args = _splitUpQuotedArgs(argsAsString, convertToArgs: true); + try { + return argParser.parse(args); + } on ArgParserException catch (e) { + warn(PackageWarning.invalidParameter, + message: 'The {@$directiveName ...} directive was called with ' + 'invalid parameters. $e'); + return null; + } + } + + /// Helper for _injectExamples used to process @example arguments. + /// Returns a map of arguments. The first unnamed argument will have key 'src'. + /// The computed file path, constructed from 'src' and 'region' will have key + /// 'file'. + Map _getExampleArgs(String argsAsString) { + ArgParser parser = ArgParser(); + parser.addOption('lang'); + parser.addOption('region'); + ArgResults results = _parseArgs(argsAsString, parser, 'example'); + if (results == null) { + return null; + } + + // Extract PATH and fix the path separators. + final String src = results.rest.isEmpty + ? '' + : results.rest.first.replaceAll('/', Platform.pathSeparator); + final Map args = { + 'src': src, + 'lang': results['lang'], + 'region': results['region'] ?? '', + }; + + // Compute 'file' from region and src. + final fragExtension = '.md'; + var file = src + fragExtension; + var region = args['region'] ?? ''; + if (region.isNotEmpty) { + var dir = path.dirname(src); + var basename = path.basenameWithoutExtension(src); + var ext = path.extension(src); + file = path.join(dir, '$basename-$region$ext$fragExtension'); + } + args['file'] = config.examplePathPrefix == null + ? file + : path.join(config.examplePathPrefix, file); + return args; + } +} diff --git a/lib/src/model/model_function.dart b/lib/src/model/model_function.dart new file mode 100644 index 0000000000..3da5a20e78 --- /dev/null +++ b/lib/src/model/model_function.dart @@ -0,0 +1,119 @@ +// 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/element/element.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; + +/// A [ModelElement] for a [FunctionElement] that isn't part of a type definition. +class ModelFunction extends ModelFunctionTyped with Categorization { + ModelFunction( + FunctionElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph); + + @override + bool get isStatic { + return _func.isStatic; + } + + @override + String get name => element.name ?? ''; + + @override + FunctionElement get _func => (element as FunctionElement); +} + +/// A [ModelElement] for a [FunctionTypedElement] that is an +/// explicit typedef. +/// +/// Distinct from ModelFunctionTypedef in that it doesn't +/// have a name, but we document it as "Function" to match how these are +/// written in declarations. +class ModelFunctionAnonymous extends ModelFunctionTyped { + ModelFunctionAnonymous( + FunctionTypedElement element, PackageGraph packageGraph) + : super(element, null, packageGraph); + + @override + ModelElement get enclosingElement { + // These are not considered to be a part of libraries, so we can simply + // blindly instantiate a ModelElement for their enclosing element. + return ModelElement.fromElement(element.enclosingElement, packageGraph); + } + + @override + String get name => 'Function'; + + @override + String get linkedName => 'Function'; + + @override + bool get isPublic => false; +} + +/// A [ModelElement] for a [FunctionTypedElement] that is part of an +/// explicit typedef. +class ModelFunctionTypedef extends ModelFunctionTyped { + ModelFunctionTypedef( + FunctionTypedElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph); + + @override + String get name { + Element e = element; + while (e != null) { + if (e is FunctionTypeAliasElement || e is GenericTypeAliasElement) { + return e.name; + } + e = e.enclosingElement; + } + assert(false); + return super.name; + } +} + +class ModelFunctionTyped extends ModelElement + with TypeParameters + implements EnclosedElement { + @override + List typeParameters = []; + + ModelFunctionTyped( + FunctionTypedElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph, null) { + _calcTypeParameters(); + } + + void _calcTypeParameters() { + typeParameters = _func.typeParameters.map((f) { + return ModelElement.from(f, library, packageGraph) as TypeParameter; + }).toList(); + } + + @override + ModelElement get enclosingElement => library; + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(canonicalLibrary != null); + assert(canonicalLibrary == library); + return '${package.baseHref}${library.dirName}/$fileName'; + } + + @override + String get kind => 'function'; + + String get linkedReturnType => modelType.createLinkedReturnTypeName(); + + // Food for mustache. TODO(jcollins-g): what about enclosing elements? + bool get isInherited => false; + + @override + DefinedElementType get modelType => super.modelType; + + FunctionTypedElement get _func => (element as FunctionTypedElement); +} diff --git a/lib/src/model/model_node.dart b/lib/src/model/model_node.dart new file mode 100644 index 0000000000..ff488f9ddb --- /dev/null +++ b/lib/src/model/model_node.dart @@ -0,0 +1,77 @@ +// 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 'dart:convert'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:dartdoc/src/model_utils.dart' as model_utils; + +/// A stripped down [CommentReference] containing only that information needed +/// for Dartdoc. Drops link to the [CommentReference] after construction. +class ModelCommentReference { + final String name; + final Element staticElement; + + ModelCommentReference(CommentReference ref) + : name = ref.identifier.name, + staticElement = ref.identifier.staticElement; +} + +/// Stripped down information derived from [AstNode] containing only information +/// needed for Dartdoc. Drops link to the [AstNode] after construction. +class ModelNode { + final List commentRefs; + final Element element; + + final int _sourceOffset; + final int _sourceEnd; + + ModelNode(AstNode sourceNode, this.element) + : _sourceOffset = sourceNode?.offset, + _sourceEnd = sourceNode?.end, + commentRefs = _commentRefsFor(sourceNode); + + static List _commentRefsFor(AstNode node) { + if (node is AnnotatedNode && + node?.documentationComment?.references != null) { + return node.documentationComment.references + .map((c) => ModelCommentReference(c)) + .toList(growable: false); + } + return null; + } + + String _sourceCode; + + String get sourceCode { + if (_sourceCode == null) { + if (_sourceOffset != null) { + String contents = model_utils.getFileContentsFor(element); + // Find the start of the line, so that we can line up all the indents. + int i = _sourceOffset; + while (i > 0) { + i -= 1; + if (contents[i] == '\n' || contents[i] == '\r') { + i += 1; + break; + } + } + + // Trim the common indent from the source snippet. + var start = _sourceOffset - (_sourceOffset - i); + String source = contents.substring(start, _sourceEnd); + + source = const HtmlEscape().convert(source); + source = model_utils.stripIndentFromSource(source); + source = model_utils.stripDartdocCommentsFromSource(source); + + _sourceCode = source.trim(); + } else { + _sourceCode = ''; + } + } + return _sourceCode; + } +} diff --git a/lib/src/model/nameable.dart b/lib/src/model/nameable.dart new file mode 100644 index 0000000000..cd73e151f8 --- /dev/null +++ b/lib/src/model/nameable.dart @@ -0,0 +1,42 @@ +// 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:collection/collection.dart'; + +import 'locatable.dart'; + +/// Something that has a name. +abstract class Nameable { + String get name; + + String get fullyQualifiedName => name; + + Set _namePieces; + + Set get namePieces { + if (_namePieces == null) { + _namePieces = Set() + ..addAll(name.split(locationSplitter).where((s) => s.isNotEmpty)); + } + return _namePieces; + } + + String _namePart; + + /// Utility getter/cache for [_MarkdownCommentReference._getResultsForClass]. + String get namePart { + // TODO(jcollins-g): This should really be the same as 'name', but isn't + // because of accessors and operators. + if (_namePart == null) { + _namePart = fullyQualifiedName.split('.').last; + } + return _namePart; + } + + @override + String toString() => name; +} + +int byName(Nameable a, Nameable b) => + compareAsciiLowerCaseNatural(a.name, b.name); diff --git a/lib/src/model/operator.dart b/lib/src/model/operator.dart new file mode 100644 index 0000000000..ee10f01ee9 --- /dev/null +++ b/lib/src/model/operator.dart @@ -0,0 +1,62 @@ +// 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/element/element.dart'; +import 'package:analyzer/src/dart/element/member.dart' show Member; +import 'package:dartdoc/src/model/model.dart'; + +class Operator extends Method { + static const Map friendlyNames = { + "[]": "get", + "[]=": "put", + "~": "bitwise_negate", + "==": "equals", + "-": "minus", + "+": "plus", + "*": "multiply", + "/": "divide", + "<": "less", + ">": "greater", + ">=": "greater_equal", + "<=": "less_equal", + "<<": "shift_left", + ">>": "shift_right", + "^": "bitwise_exclusive_or", + "unary-": "unary_minus", + "|": "bitwise_or", + "&": "bitwise_and", + "~/": "truncate_divide", + "%": "modulo" + }; + + Operator(MethodElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph); + + Operator.inherited(MethodElement element, Class enclosingClass, + Library library, PackageGraph packageGraph, {Member originalMember}) + : super.inherited(element, enclosingClass, library, packageGraph, + originalMember: originalMember); + + @override + String get fileName { + var actualName = super.name; + if (friendlyNames.containsKey(actualName)) { + return "operator_${friendlyNames[actualName]}.html"; + } else { + return '$actualName.html'; + } + } + + @override + String get fullyQualifiedName => + '${library.name}.${enclosingElement.name}.${super.name}'; + + @override + bool get isOperator => true; + + @override + String get name { + return 'operator ${super.name}'; + } +} diff --git a/lib/src/model/package.dart b/lib/src/model/package.dart new file mode 100644 index 0000000000..4cc18d6519 --- /dev/null +++ b/lib/src/model/package.dart @@ -0,0 +1,326 @@ +// 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 'dart:io'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:dartdoc/src/dartdoc_options.dart'; +import 'package:dartdoc/src/markdown_processor.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/package_meta.dart'; +import 'package:dartdoc/src/warnings.dart'; +import 'package:path/path.dart' as path; +import 'package:pub_semver/pub_semver.dart'; + +final RegExp substituteNameVersion = RegExp(r'%([bnv])%'); + +/// A [LibraryContainer] that contains [Library] objects related to a particular +/// package. +class Package extends LibraryContainer + with Nameable, Locatable, Canonicalization, Warnable + implements Privacy, Documentable { + String _name; + PackageGraph _packageGraph; + + final Map _nameToCategory = {}; + + // Creates a package, if necessary, and adds it to the [packageGraph]. + factory Package.fromPackageMeta( + PackageMeta packageMeta, PackageGraph packageGraph) { + String packageName = packageMeta.name; + + bool expectNonLocal = false; + + if (!packageGraph.packageMap.containsKey(packageName) && + packageGraph.allLibrariesAdded) expectNonLocal = true; + packageGraph.packageMap.putIfAbsent( + packageName, () => Package._(packageName, packageGraph, packageMeta)); + // Verify that we don't somehow decide to document locally a package picked + // up after all documented libraries are added, because that breaks the + // assumption that we've picked up all documented libraries and packages + // before allLibrariesAdded is true. + assert( + !(expectNonLocal && + packageGraph.packageMap[packageName].documentedWhere == + DocumentLocation.local), + 'Found more libraries to document after allLibrariesAdded was set to true'); + return packageGraph.packageMap[packageName]; + } + + Package._(this._name, this._packageGraph, this._packageMeta); + + @override + bool get isCanonical => true; + + @override + Library get canonicalLibrary => null; + + /// Number of times we have invoked a tool for this package. + int toolInvocationIndex = 0; + + // The animation IDs that have already been used, indexed by the [href] of the + // object that contains them. + Map> usedAnimationIdsByHref = {}; + + /// Pieces of the location, split to remove 'package:' and slashes. + @override + Set get locationPieces => Set(); + + /// Holds all libraries added to this package. May include non-documented + /// libraries, but is not guaranteed to include a complete list of + /// non-documented libraries unless they are all referenced by documented ones. + /// Not sorted. + final Set allLibraries = Set(); + + bool get hasHomepage => + packageMeta.homepage != null && packageMeta.homepage.isNotEmpty; + + String get homepage => packageMeta.homepage; + + String get kind => (isSdk) ? 'SDK' : 'package'; + + @override + List get documentationFrom => [this]; + + /// Return true if the code has defined non-default categories for libraries + /// in this package. + bool get hasCategories => categories.isNotEmpty; + + LibraryContainer get defaultCategory => nameToCategory[null]; + + String _documentationAsHtml; + + @override + String get documentationAsHtml { + if (_documentationAsHtml != null) return _documentationAsHtml; + _documentationAsHtml = Documentation.forElement(this).asHtml; + + return _documentationAsHtml; + } + + @override + String get documentation { + return hasDocumentationFile ? documentationFile.contents : null; + } + + @override + bool get hasDocumentation => + documentationFile != null && documentationFile.contents.isNotEmpty; + + @override + bool get hasExtendedDocumentation => documentation.isNotEmpty; + + // TODO: Clients should use [documentationFile] so they can act differently on + // plain text or markdown. + bool get hasDocumentationFile => documentationFile != null; + + FileContents get documentationFile => packageMeta.getReadmeContents(); + + @override + String get oneLineDoc => ''; + + @override + bool get isDocumented => + isFirstPackage || documentedWhere != DocumentLocation.missing; + + @override + Warnable get enclosingElement => null; + + bool _isPublic; + + @override + bool get isPublic { + if (_isPublic == null) _isPublic = libraries.any((l) => l.isPublic); + return _isPublic; + } + + bool _isLocal; + + /// Return true if this is the default package, this is part of an embedder SDK, + /// or if [config.autoIncludeDependencies] is true -- but only if the package + /// was not excluded on the command line. + bool get isLocal { + if (_isLocal == null) { + _isLocal = (packageMeta == packageGraph.packageMeta || + packageGraph.hasEmbedderSdk && packageMeta.isSdk || + packageGraph.config.autoIncludeDependencies) && + !packageGraph.config.isPackageExcluded(name); + } + return _isLocal; + } + + DocumentLocation get documentedWhere { + if (isLocal) { + if (isPublic) { + return DocumentLocation.local; + } else { + // Possible if excludes result in a "documented" package not having + // any actual documentation. + return DocumentLocation.missing; + } + } else { + if (config.linkToRemote && config.linkToUrl.isNotEmpty && isPublic) { + return DocumentLocation.remote; + } else { + return DocumentLocation.missing; + } + } + } + + @override + String get enclosingName => packageGraph.defaultPackageName; + + @override + String get fullyQualifiedName => 'package:$name'; + + String _baseHref; + + String get baseHref { + if (_baseHref == null) { + if (documentedWhere == DocumentLocation.remote) { + _baseHref = + config.linkToUrl.replaceAllMapped(substituteNameVersion, (m) { + switch (m.group(1)) { + // Return the prerelease tag of the release if a prerelease, + // or 'stable' otherwise. Mostly coded around + // the Dart SDK's use of dev/stable, but theoretically applicable + // elsewhere. + case 'b': + { + Version version = Version.parse(packageMeta.version); + return version.isPreRelease + ? version.preRelease.first + : 'stable'; + } + case 'n': + return name; + // The full version string of the package. + case 'v': + return packageMeta.version; + default: + assert(false, 'Unsupported case: ${m.group(1)}'); + return null; + } + }); + if (!_baseHref.endsWith('/')) _baseHref = '${_baseHref}/'; + } else { + _baseHref = ''; + } + } + return _baseHref; + } + + @override + String get href => '${baseHref}index.html'; + + @override + String get location => path.toUri(packageMeta.resolvedDir).toString(); + + @override + String get name => _name; + + @override + Package get package => this; + + @override + PackageGraph get packageGraph => _packageGraph; + + // Workaround for mustache4dart issue where templates do not recognize + // inherited properties as being in-context. + @override + Iterable get publicLibraries { + assert(libraries.every((l) => l.packageMeta == _packageMeta)); + return super.publicLibraries; + } + + /// A map of category name to the category itself. + Map get nameToCategory { + if (_nameToCategory.isEmpty) { + Category categoryFor(String category) { + _nameToCategory.putIfAbsent( + category, () => Category(category, this, config)); + return _nameToCategory[category]; + } + + _nameToCategory[null] = Category(null, this, config); + for (Categorization c in libraries.expand( + (l) => l.allCanonicalModelElements.whereType())) { + for (String category in c.categoryNames) { + categoryFor(category).addItem(c); + } + } + } + return _nameToCategory; + } + + List _categories; + + List get categories { + if (_categories == null) { + _categories = nameToCategory.values.where((c) => c.name != null).toList() + ..sort(); + } + return _categories; + } + + Iterable get categoriesWithPublicLibraries => + categories.where((c) => c.publicLibraries.isNotEmpty); + + Iterable get documentedCategories => + categories.where((c) => c.isDocumented); + + bool get hasDocumentedCategories => documentedCategories.isNotEmpty; + + DartdocOptionContext _config; + + @override + DartdocOptionContext get config { + if (_config == null) { + _config = DartdocOptionContext.fromContext( + packageGraph.config, Directory(packagePath)); + } + return _config; + } + + /// Is this the package at the top of the list? We display the first + /// package specially (with "Libraries" rather than the package name). + bool get isFirstPackage => + packageGraph.localPackages.isNotEmpty && + identical(packageGraph.localPackages.first, this); + + @override + bool get isSdk => packageMeta.isSdk; + + String _packagePath; + + String get packagePath { + if (_packagePath == null) { + _packagePath = path.canonicalize(packageMeta.dir.path); + } + return _packagePath; + } + + String get version => packageMeta.version ?? '0.0.0-unknown'; + + @override + void warn(PackageWarning kind, + {String message, + Iterable referredFrom, + Iterable extendedDebug}) { + packageGraph.warnOnElement(this, kind, + message: message, + referredFrom: referredFrom, + extendedDebug: extendedDebug); + } + + final PackageMeta _packageMeta; + + PackageMeta get packageMeta => _packageMeta; + + @override + Element get element => null; + + @override + List get containerOrder => config.packageOrder; +} diff --git a/lib/src/model/package_builder.dart b/lib/src/model/package_builder.dart new file mode 100644 index 0000000000..02c6048a0d --- /dev/null +++ b/lib/src/model/package_builder.dart @@ -0,0 +1,462 @@ +// 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 'dart:async'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/file_system/file_system.dart' as file_system; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/src/context/builder.dart'; +import 'package:analyzer/src/dart/analysis/byte_store.dart'; +import 'package:analyzer/src/dart/analysis/driver.dart'; +import 'package:analyzer/src/dart/analysis/file_state.dart'; +import 'package:analyzer/src/dart/analysis/performance_logger.dart'; +import 'package:analyzer/src/dart/sdk/sdk.dart'; +import 'package:analyzer/src/generated/engine.dart'; +import 'package:analyzer/src/generated/java_io.dart'; +import 'package:analyzer/src/generated/sdk.dart'; +import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer/src/generated/source_io.dart'; +import 'package:analyzer/src/source/package_map_resolver.dart'; +import 'package:analyzer/src/source/sdk_ext.dart'; +import 'package:dartdoc/src/dartdoc_options.dart'; +import 'package:dartdoc/src/io_utils.dart'; +import 'package:dartdoc/src/logging.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/package_meta.dart' show PackageMeta; +import 'package:dartdoc/src/special_elements.dart'; +import 'package:package_config/discovery.dart' as package_config; +import 'package:path/path.dart' as path; +import 'package:quiver/iterables.dart' as quiver; + +/// Everything you need to instantiate a PackageGraph object for documenting. +class PackageBuilder { + final DartdocOptionContext config; + + PackageBuilder(this.config); + + Future buildPackageGraph() async { + if (config.topLevelPackageMeta.needsPubGet) { + config.topLevelPackageMeta.runPubGet(); + } + + PackageGraph newGraph = PackageGraph.UninitializedPackageGraph( + config, + driver, + await driver.currentSession.typeSystem, + sdk, + hasEmbedderSdkFiles); + await getLibraries(newGraph); + await newGraph.initializePackageGraph(); + return newGraph; + } + + DartSdk _sdk; + + DartSdk get sdk { + if (_sdk == null) { + _sdk = FolderBasedDartSdk(PhysicalResourceProvider.INSTANCE, + PhysicalResourceProvider.INSTANCE.getFolder(config.sdkDir)); + } + return _sdk; + } + + EmbedderSdk _embedderSdk; + + EmbedderSdk get embedderSdk { + if (_embedderSdk == null && !config.topLevelPackageMeta.isSdk) { + _embedderSdk = EmbedderSdk(PhysicalResourceProvider.INSTANCE, + EmbedderYamlLocator(packageMap).embedderYamls); + } + return _embedderSdk; + } + + static Map> _calculatePackageMap( + file_system.Folder dir) { + Map> map = Map(); + var info = package_config.findPackagesFromFile(dir.toUri()); + + for (String name in info.packages) { + Uri uri = info.asMap()[name]; + String packagePath = path.normalize(path.fromUri(uri)); + file_system.Resource resource = + PhysicalResourceProvider.INSTANCE.getResource(packagePath); + if (resource is file_system.Folder) { + map[name] = [resource]; + } + } + + return map; + } + + Map> _packageMap; + + Map> get packageMap { + if (_packageMap == null) { + file_system.Folder cwd = + PhysicalResourceProvider.INSTANCE.getResource(config.inputDir); + _packageMap = _calculatePackageMap(cwd); + } + return _packageMap; + } + + DartUriResolver _embedderResolver; + + DartUriResolver get embedderResolver { + if (_embedderResolver == null) { + _embedderResolver = DartUriResolver(embedderSdk); + } + return _embedderResolver; + } + + SourceFactory get sourceFactory { + List resolvers = []; + resolvers.add(SdkExtUriResolver(packageMap)); + final UriResolver packageResolver = + PackageMapUriResolver(PhysicalResourceProvider.INSTANCE, packageMap); + UriResolver sdkResolver; + if (embedderSdk == null || embedderSdk.urlMappings.isEmpty) { + // The embedder uri resolver has no mappings. Use the default Dart SDK + // uri resolver. + sdkResolver = DartUriResolver(sdk); + } else { + // The embedder uri resolver has mappings, use it instead of the default + // Dart SDK uri resolver. + sdkResolver = embedderResolver; + } + + /// [AnalysisDriver] seems to require package resolvers that + /// never resolve to embedded SDK files, and the resolvers list must still + /// contain a DartUriResolver. This hack won't be necessary once analyzer + /// has a clean public API. + resolvers.add(PackageWithoutSdkResolver(packageResolver, sdkResolver)); + resolvers.add(sdkResolver); + resolvers.add( + file_system.ResourceUriResolver(PhysicalResourceProvider.INSTANCE)); + + assert( + resolvers.any((UriResolver resolver) => resolver is DartUriResolver)); + SourceFactory sourceFactory = SourceFactory(resolvers); + return sourceFactory; + } + + AnalysisDriver _driver; + + AnalysisDriver get driver { + if (_driver == null) { + PerformanceLog log = PerformanceLog(null); + AnalysisDriverScheduler scheduler = AnalysisDriverScheduler(log); + AnalysisOptionsImpl options = AnalysisOptionsImpl(); + + // TODO(jcollins-g): pass in an ExperimentStatus instead? + options.enabledExperiments = config.enableExperiment + ..add('extension-methods'); + + // TODO(jcollins-g): Make use of currently not existing API for managing + // many AnalysisDrivers + // TODO(jcollins-g): make use of DartProject isApi() + _driver = AnalysisDriver( + scheduler, + log, + PhysicalResourceProvider.INSTANCE, + MemoryByteStore(), + FileContentOverlay(), + null, + sourceFactory, + options); + driver.results.listen((_) {}); + driver.exceptions.listen((_) {}); + scheduler.start(); + } + return _driver; + } + + /// Return an Iterable with the sdk files we should parse. + /// Filter can be String or RegExp (technically, anything valid for + /// [String.contains]) + Iterable getSdkFilesToDocument() sync* { + for (var sdkLib in sdk.sdkLibraries) { + Source source = sdk.mapDartUri(sdkLib.shortName); + yield source.fullName; + } + } + + /// Parse a single library at [filePath] using the current analysis driver. + /// If [filePath] is not a library, returns null. + Future processLibrary(String filePath) async { + String name = filePath; + + if (name.startsWith(directoryCurrentPath)) { + name = name.substring(directoryCurrentPath.length); + if (name.startsWith(Platform.pathSeparator)) name = name.substring(1); + } + JavaFile javaFile = JavaFile(filePath).getAbsoluteFile(); + Source source = FileBasedSource(javaFile); + + // TODO(jcollins-g): remove the manual reversal using embedderSdk when we + // upgrade to analyzer-0.30 (where DartUriResolver implements + // restoreAbsolute) + Uri uri = embedderSdk?.fromFileUri(source.uri)?.uri; + if (uri != null) { + source = FileBasedSource(javaFile, uri); + } else { + uri = driver.sourceFactory.restoreUri(source); + if (uri != null) { + source = FileBasedSource(javaFile, uri); + } + } + var sourceKind = await driver.getSourceKind(filePath); + // Allow dart source files with inappropriate suffixes (#1897). Those + // do not show up as SourceKind.LIBRARY. + if (sourceKind != SourceKind.PART) { + // Loading libraryElements from part files works, but is painfully slow + // and creates many duplicates. + return await driver.currentSession.getResolvedLibrary(source.fullName); + } + return null; + } + + Set _packageMetasForFiles(Iterable files) { + Set metas = Set(); + for (String filename in files) { + metas.add(PackageMeta.fromFilename(filename)); + } + return metas; + } + + /// Parse libraries with the analyzer and invoke a callback with the + /// result. + /// + /// Uses the [libraries] parameter to prevent calling + /// the callback more than once with the same [LibraryElement]. + /// Adds [LibraryElement]s found to that parameter. + Future _parseLibraries( + void Function(ResolvedLibraryResult) libraryAdder, + Set libraries, + Set files, + [bool Function(LibraryElement) isLibraryIncluded]) async { + isLibraryIncluded ??= (_) => true; + Set lastPass = Set(); + Set current; + do { + lastPass = _packageMetasForFiles(files); + + // Be careful here not to accidentally stack up multiple + // ResolvedLibraryResults, as those eat our heap. + for (String f in files) { + ResolvedLibraryResult r = await processLibrary(f); + if (r != null && + !libraries.contains(r.element) && + isLibraryIncluded(r.element)) { + logInfo('parsing ${f}...'); + libraryAdder(r); + libraries.add(r.element); + } + } + + // Be sure to give the analyzer enough time to find all the files. + await driver.discoverAvailableFiles(); + files.addAll(driver.knownFiles); + files.addAll(_includeExternalsFrom(driver.knownFiles)); + current = _packageMetasForFiles(files); + // To get canonicalization correct for non-locally documented packages + // (so we can generate the right hyperlinks), it's vital that we + // add all libraries in dependent packages. So if the analyzer + // discovers some files in a package we haven't seen yet, add files + // for that package. + for (PackageMeta meta in current.difference(lastPass)) { + if (meta.isSdk) { + files.addAll(getSdkFilesToDocument()); + } else { + files.addAll( + findFilesToDocumentInPackage(meta.dir.path, false, false)); + } + } + } while (!lastPass.containsAll(current)); + } + + /// Given a package name, explore the directory and pull out all top level + /// library files in the "lib" directory to document. + Iterable findFilesToDocumentInPackage( + String basePackageDir, bool autoIncludeDependencies, + [bool filterExcludes = true]) sync* { + final String sep = path.separator; + + Set packageDirs = Set()..add(basePackageDir); + + if (autoIncludeDependencies) { + Map info = package_config + .findPackagesFromFile( + Uri.file(path.join(basePackageDir, 'pubspec.yaml'))) + .asMap(); + for (String packageName in info.keys) { + if (!filterExcludes || !config.exclude.contains(packageName)) { + packageDirs.add(path.dirname(info[packageName].toFilePath())); + } + } + } + + for (String packageDir in packageDirs) { + var packageLibDir = path.join(packageDir, 'lib'); + var packageLibSrcDir = path.join(packageLibDir, 'src'); + // To avoid analyzing package files twice, only files with paths not + // containing '/packages' will be added. The only exception is if the file + // to analyze already has a '/package' in its path. + for (var lib + in listDir(packageDir, recursive: true, listDir: _packageDirList)) { + if (lib.endsWith('.dart') && + (!lib.contains('${sep}packages${sep}') || + packageDir.contains('${sep}packages${sep}'))) { + // Only include libraries within the lib dir that are not in lib/src + if (path.isWithin(packageLibDir, lib) && + !path.isWithin(packageLibSrcDir, lib)) { + // Only add the file if it does not contain 'part of' + var contents = File(lib).readAsStringSync(); + + if (contents.contains(newLinePartOfRegexp) || + contents.startsWith(partOfRegexp)) { + // NOOP: it's a part file + } else { + yield lib; + } + } + } + } + } + } + + /// Calculate includeExternals based on a list of files. Assumes each + /// file might be part of a [DartdocOptionContext], and loads those + /// objects to find any [DartdocOptionContext.includeExternal] configurations + /// therein. + Iterable _includeExternalsFrom(Iterable files) sync* { + for (String file in files) { + DartdocOptionContext fileContext = + DartdocOptionContext.fromContext(config, File(file)); + if (fileContext.includeExternal != null) { + yield* fileContext.includeExternal; + } + } + } + + Set getFiles() { + Iterable files; + if (config.topLevelPackageMeta.isSdk) { + files = getSdkFilesToDocument(); + } else { + files = findFilesToDocumentInPackage( + config.inputDir, config.autoIncludeDependencies); + } + files = quiver.concat([files, _includeExternalsFrom(files)]); + return Set.from(files.map((s) => File(s).absolute.path)); + } + + Iterable getEmbedderSdkFiles() sync* { + if (embedderSdk != null && + embedderSdk.urlMappings.isNotEmpty && + !config.topLevelPackageMeta.isSdk) { + for (String dartUri in embedderSdk.urlMappings.keys) { + Source source = embedderSdk.mapDartUri(dartUri); + yield (File(source.fullName)).absolute.path; + } + } + } + + bool get hasEmbedderSdkFiles => + embedderSdk != null && getEmbedderSdkFiles().isNotEmpty; + + Future getLibraries(PackageGraph uninitializedPackageGraph) async { + DartSdk findSpecialsSdk = sdk; + if (embedderSdk != null && embedderSdk.urlMappings.isNotEmpty) { + findSpecialsSdk = embedderSdk; + } + Set files = getFiles()..addAll(getEmbedderSdkFiles()); + Set specialFiles = specialLibraryFiles(findSpecialsSdk).toSet(); + + /// Returns true if this library element should be included according + /// to the configuration. + bool isLibraryIncluded(LibraryElement libraryElement) { + if (config.include.isNotEmpty && + !config.include.contains(libraryElement.name)) { + return false; + } + return true; + } + + Set foundLibraries = Set(); + await _parseLibraries(uninitializedPackageGraph.addLibraryToGraph, + foundLibraries, files, isLibraryIncluded); + if (config.include.isNotEmpty) { + Iterable knownLibraryNames = foundLibraries.map((l) => l.name); + Set notFound = Set.from(config.include) + .difference(Set.from(knownLibraryNames)) + .difference(Set.from(config.exclude)); + if (notFound.isNotEmpty) { + throw 'Did not find: [${notFound.join(', ')}] in ' + 'known libraries: [${knownLibraryNames.join(', ')}]'; + } + } + // Include directive does not apply to special libraries. + await _parseLibraries(uninitializedPackageGraph.addSpecialLibraryToGraph, + foundLibraries, specialFiles.difference(files)); + } + + /// If [dir] contains both a `lib` directory and a `pubspec.yaml` file treat + /// it like a package and only return the `lib` dir. + /// + /// This ensures that packages don't have non-`lib` content documented. + static Iterable _packageDirList(Directory dir) sync* { + var entities = dir.listSync(); + + var pubspec = entities.firstWhere( + (e) => e is File && path.basename(e.path) == 'pubspec.yaml', + orElse: () => null); + + var libDir = entities.firstWhere( + (e) => e is Directory && path.basename(e.path) == 'lib', + orElse: () => null); + + if (pubspec != null && libDir != null) { + yield libDir; + } else { + yield* entities; + } + } +} + +/// This class resolves package URIs, but only if a given SdkResolver doesn't +/// resolve them. +/// +/// TODO(jcollins-g): remove this hackery when a clean public API to analyzer +/// exists, and port dartdoc to it. +class PackageWithoutSdkResolver extends UriResolver { + final UriResolver _packageResolver; + final UriResolver _sdkResolver; + + PackageWithoutSdkResolver(this._packageResolver, this._sdkResolver); + + @override + Source resolveAbsolute(Uri uri, [Uri actualUri]) { + if (_sdkResolver.resolveAbsolute(uri, actualUri) == null) { + return _packageResolver.resolveAbsolute(uri, actualUri); + } + return null; + } + + @override + Uri restoreAbsolute(Source source) { + Uri resolved; + try { + resolved = _sdkResolver.restoreAbsolute(source); + } catch (ArgumentError) { + // SDK resolvers really don't like being thrown package paths. + } + if (resolved == null) { + return _packageResolver.restoreAbsolute(source); + } + return null; + } +} diff --git a/lib/src/model/package_graph.dart b/lib/src/model/package_graph.dart new file mode 100644 index 0000000000..864ea31981 --- /dev/null +++ b/lib/src/model/package_graph.dart @@ -0,0 +1,916 @@ +// 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 'dart:async'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/session.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/src/dart/analysis/driver.dart'; +import 'package:analyzer/src/dart/element/member.dart'; +import 'package:analyzer/src/generated/sdk.dart'; +import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer/src/generated/source_io.dart'; +import 'package:analyzer/src/generated/type_system.dart' show Dart2TypeSystem; +import 'package:collection/collection.dart'; +import 'package:dartdoc/src/dartdoc_options.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/model_utils.dart' as utils; +import 'package:dartdoc/src/package_meta.dart' show PackageMeta; +import 'package:dartdoc/src/special_elements.dart'; +import 'package:dartdoc/src/tuple.dart'; +import 'package:dartdoc/src/warnings.dart'; + +class PackageGraph { + PackageGraph.UninitializedPackageGraph( + this.config, this.driver, this.typeSystem, this.sdk, this.hasEmbedderSdk) + : packageMeta = config.topLevelPackageMeta, + session = driver.currentSession { + _packageWarningCounter = PackageWarningCounter(this); + // Make sure the default package exists, even if it has no libraries. + // This can happen for packages that only contain embedder SDKs. + Package.fromPackageMeta(packageMeta, this); + } + + /// Call during initialization to add a library to this [PackageGraph]. + /// + /// Libraries added in this manner are assumed to be part of documented + /// packages, even if includes or embedder.yaml files cause these to + /// span packages. + void addLibraryToGraph(ResolvedLibraryResult result) { + assert(!allLibrariesAdded); + LibraryElement element = result.element; + var packageMeta = PackageMeta.fromElement(element, config); + var lib = Library.fromLibraryResult( + result, this, Package.fromPackageMeta(packageMeta, this)); + packageMap[packageMeta.name].libraries.add(lib); + allLibraries[element] = lib; + } + + /// Call during initialization to add a library possibly containing + /// special/non-documented elements to this [PackageGraph]. Must be called + /// after any normal libraries. + void addSpecialLibraryToGraph(ResolvedLibraryResult result) { + allLibrariesAdded = true; + assert(!_localDocumentationBuilt); + findOrCreateLibraryFor(result); + } + + /// Call after all libraries are added. + Future initializePackageGraph() async { + allLibrariesAdded = true; + assert(!_localDocumentationBuilt); + // From here on in, we might find special objects. Initialize the + // specialClasses handler so when we find them, they get added. + specialClasses = SpecialClasses(); + // Go through docs of every ModelElement in package to pre-build the macros + // index. Uses toList() in order to get all the precaching on the stack. + List precacheFutures = precacheLocalDocs().toList(); + for (Future f in precacheFutures) { + await f; + } + _localDocumentationBuilt = true; + + // Scan all model elements to insure that interceptor and other special + // objects are found. + // After the allModelElements traversal to be sure that all packages + // are picked up. + documentedPackages.toList().forEach((package) { + package.libraries.sort((a, b) => compareNatural(a.name, b.name)); + package.libraries.forEach((library) { + library.allClasses.forEach(_addToImplementors); + _extensions.addAll(library.extensions); + }); + }); + _implementors.values.forEach((l) => l.sort()); + allImplementorsAdded = true; + allExtensionsAdded = true; + + // We should have found all special classes by now. + specialClasses.assertSpecials(); + } + + /// Generate a list of futures for any docs that actually require precaching. + Iterable precacheLocalDocs() sync* { + // Prevent reentrancy. + Set precachedElements = Set(); + + Iterable precacheOneElement(ModelElement m) sync* { + for (ModelElement d + in m.documentationFrom.where((d) => d.documentationComment != null)) { + if (needsPrecacheRegExp.hasMatch(d.documentationComment) && + !precachedElements.contains(d)) { + precachedElements.add(d); + yield d.precacheLocalDocs(); + // TopLevelVariables get their documentation from getters and setters, + // so should be precached if either has a template. + if (m is TopLevelVariable) { + precachedElements.add(m); + yield m.precacheLocalDocs(); + } + } + } + } + + for (ModelElement m in allModelElements) { + // Skip if there is a canonicalModelElement somewhere else we can run this + // for. Not the same as allCanonicalModelElements since we need to run + // for any ModelElement that might not have a canonical ModelElement, + // too. + if (m.canonicalModelElement != null && !m.isCanonical) continue; + yield* precacheOneElement(m); + } + } + + // Many ModelElements have the same ModelNode; don't build/cache this data more + // than once for them. + final Map _modelNodes = Map(); + + void populateModelNodeFor( + Element element, Map compilationUnitMap) { + _modelNodes.putIfAbsent( + element, + () => + ModelNode(utils.getAstNode(element, compilationUnitMap), element)); + } + + ModelNode getModelNodeFor(Element element) => _modelNodes[element]; + + SpecialClasses specialClasses; + + /// It is safe to cache values derived from the [_implementors] table if this + /// is true. + bool allImplementorsAdded = false; + + /// It is safe to cache values derived from the [_extensions] table if this + /// is true. + bool allExtensionsAdded = false; + + Map> get implementors { + assert(allImplementorsAdded); + return _implementors; + } + + Iterable get extensions { + assert(allExtensionsAdded); + return _extensions; + } + + Map> _findRefElementCache; + + Map> get findRefElementCache { + if (_findRefElementCache == null) { + assert(packageGraph.allLibrariesAdded); + _findRefElementCache = Map(); + for (final modelElement + in utils.filterNonDocumented(packageGraph.allLocalModelElements)) { + _findRefElementCache.putIfAbsent( + modelElement.fullyQualifiedNameWithoutLibrary, () => Set()); + _findRefElementCache.putIfAbsent( + modelElement.fullyQualifiedName, () => Set()); + _findRefElementCache[modelElement.fullyQualifiedName].add(modelElement); + _findRefElementCache[modelElement.fullyQualifiedNameWithoutLibrary] + .add(modelElement); + } + } + return _findRefElementCache; + } + + // All library objects related to this package; a superset of _libraries. + final Map allLibraries = Map(); + + /// Keep track of warnings + PackageWarningCounter _packageWarningCounter; + + /// All ModelElements constructed for this package; a superset of [allModelElements]. + final Map, ModelElement> + allConstructedModelElements = Map(); + + /// Anything that might be inheritable, place here for later lookup. + final Map, Set> + allInheritableElements = Map(); + + /// Map of Class.href to a list of classes implementing that class + final Map> _implementors = Map(); + + /// A list of extensions that exist in the package graph. + final List _extensions = []; + + /// PackageMeta for the default package. + final PackageMeta packageMeta; + + /// Name of the default package. + String get defaultPackageName => packageMeta.name; + + /// Dartdoc's configuration flags. + final DartdocOptionContext config; + + Package _defaultPackage; + + Package get defaultPackage { + if (_defaultPackage == null) { + _defaultPackage = Package.fromPackageMeta(packageMeta, this); + } + return _defaultPackage; + } + + final bool hasEmbedderSdk; + + bool get hasFooterVersion => !config.excludeFooterVersion; + + PackageGraph get packageGraph => this; + + /// Map of package name to Package. + final Map packageMap = {}; + + /// TODO(brianwilkerson) Replace the driver with the session. + final AnalysisDriver driver; + final AnalysisSession session; + final Dart2TypeSystem typeSystem; + final DartSdk sdk; + + Map _sdkLibrarySources; + + Map get sdkLibrarySources { + if (_sdkLibrarySources == null) { + _sdkLibrarySources = Map(); + for (SdkLibrary lib in sdk?.sdkLibraries) { + _sdkLibrarySources[sdk.mapDartUri(lib.shortName)] = lib; + } + } + return _sdkLibrarySources; + } + + final Map _macros = {}; + final Map _htmlFragments = {}; + bool allLibrariesAdded = false; + bool _localDocumentationBuilt = false; + + /// Returns true if there's at least one library documented in the package + /// that has the same package path as the library for the given element. + /// Usable as a cross-check for dartdoc's canonicalization to generate + /// warnings for ModelElement.isPublicAndPackageDocumented. + Set _allRootDirs; + + bool packageDocumentedFor(ModelElement element) { + if (_allRootDirs == null) { + _allRootDirs = Set() + ..addAll(publicLibraries.map((l) => l.packageMeta?.resolvedDir)); + } + return (_allRootDirs.contains(element.library.packageMeta?.resolvedDir)); + } + + PackageWarningCounter get packageWarningCounter => _packageWarningCounter; + + final Set> _warnAlreadySeen = Set(); + + void warnOnElement(Warnable warnable, PackageWarning kind, + {String message, + Iterable referredFrom, + Iterable extendedDebug}) { + var newEntry = Tuple3(warnable?.element, kind, message); + if (_warnAlreadySeen.contains(newEntry)) { + return; + } + // Warnings can cause other warnings. Queue them up via the stack but + // don't allow warnings we're already working on to get in there. + _warnAlreadySeen.add(newEntry); + _warnOnElement(warnable, kind, + message: message, + referredFrom: referredFrom, + extendedDebug: extendedDebug); + _warnAlreadySeen.remove(newEntry); + } + + void _warnOnElement(Warnable warnable, PackageWarning kind, + {String message, + Iterable referredFrom, + Iterable extendedDebug}) { + if (warnable != null) { + // This sort of warning is only applicable to top level elements. + if (kind == PackageWarning.ambiguousReexport) { + while (warnable.enclosingElement is! Library && + warnable.enclosingElement != null) { + warnable = warnable.enclosingElement; + } + } + } else { + // If we don't have an element, we need a message to disambiguate. + assert(message != null); + } + if (_packageWarningCounter.hasWarning(warnable, kind, message)) { + return; + } + // Some kinds of warnings it is OK to drop if we're not documenting them. + // TODO(jcollins-g): drop this and use new flag system instead. + if (warnable != null && + skipWarningIfNotDocumentedFor.contains(kind) && + !warnable.isDocumented) { + return; + } + // Elements that are part of the Dart SDK can have colons in their FQNs. + // This confuses IntelliJ and makes it so it can't link to the location + // of the error in the console window, so separate out the library from + // the path. + // TODO(jcollins-g): What about messages that may include colons? Substituting + // them out doesn't work as well there since it might confuse + // the user, yet we still want IntelliJ to link properly. + final warnableName = _safeWarnableName(warnable); + + String warnablePrefix = 'from'; + String referredFromPrefix = 'referred to by'; + String warningMessage; + switch (kind) { + case PackageWarning.noCanonicalFound: + // Fix these warnings by adding libraries with --include, or by using + // --auto-include-dependencies. + // TODO(jcollins-g): pipeline references through linkedName for error + // messages and warn for non-public canonicalization + // errors. + warningMessage = + "no canonical library found for ${warnableName}, not linking"; + break; + case PackageWarning.ambiguousReexport: + // Fix these warnings by adding the original library exporting the + // symbol with --include, by using --auto-include-dependencies, + // or by using --exclude to hide one of the libraries involved + warningMessage = + "ambiguous reexport of ${warnableName}, canonicalization candidates: ${message}"; + break; + case PackageWarning.noLibraryLevelDocs: + warningMessage = + "${warnable.fullyQualifiedName} has no library level documentation comments"; + break; + case PackageWarning.ambiguousDocReference: + warningMessage = "ambiguous doc reference ${message}"; + break; + case PackageWarning.ignoredCanonicalFor: + warningMessage = + "library says it is {@canonicalFor ${message}} but ${message} can't be canonical there"; + break; + case PackageWarning.packageOrderGivesMissingPackageName: + warningMessage = + "--package-order gives invalid package name: '${message}'"; + break; + case PackageWarning.reexportedPrivateApiAcrossPackages: + warningMessage = + "private API of ${message} is reexported by libraries in other packages: "; + break; + case PackageWarning.notImplemented: + warningMessage = message; + break; + case PackageWarning.unresolvedDocReference: + warningMessage = "unresolved doc reference [${message}]"; + referredFromPrefix = 'in documentation inherited from'; + break; + case PackageWarning.unknownMacro: + warningMessage = "undefined macro [${message}]"; + break; + case PackageWarning.unknownHtmlFragment: + warningMessage = "undefined HTML fragment identifier [${message}]"; + break; + case PackageWarning.brokenLink: + warningMessage = 'dartdoc generated a broken link to: ${message}'; + warnablePrefix = 'to element'; + referredFromPrefix = 'linked to from'; + break; + case PackageWarning.orphanedFile: + warningMessage = 'dartdoc generated a file orphan: ${message}'; + break; + case PackageWarning.unknownFile: + warningMessage = + 'dartdoc detected an unknown file in the doc tree: ${message}'; + break; + case PackageWarning.missingFromSearchIndex: + warningMessage = + 'dartdoc generated a file not in the search index: ${message}'; + break; + case PackageWarning.typeAsHtml: + // The message for this warning can contain many punctuation and other symbols, + // so bracket with a triple quote for defense. + warningMessage = 'generic type handled as HTML: """${message}"""'; + break; + case PackageWarning.invalidParameter: + warningMessage = 'invalid parameter to dartdoc directive: ${message}'; + break; + case PackageWarning.toolError: + warningMessage = 'tool execution failed: ${message}'; + break; + case PackageWarning.deprecated: + warningMessage = 'deprecated dartdoc usage: ${message}'; + break; + case PackageWarning.unresolvedExport: + warningMessage = 'unresolved export uri: ${message}'; + break; + } + + List messageParts = [warningMessage]; + if (warnable != null) { + messageParts.add("$warnablePrefix $warnableName: ${warnable.location}"); + } + if (referredFrom != null) { + for (Locatable referral in referredFrom) { + if (referral != warnable) { + var referredFromStrings = _safeWarnableName(referral); + messageParts.add( + "$referredFromPrefix $referredFromStrings: ${referral.location}"); + } + } + } + if (config.verboseWarnings && extendedDebug != null) { + messageParts.addAll(extendedDebug.map((s) => " $s")); + } + String fullMessage; + if (messageParts.length <= 2) { + fullMessage = messageParts.join(', '); + } else { + fullMessage = messageParts.join('\n '); + } + + packageWarningCounter.addWarning(warnable, kind, message, fullMessage); + } + + String _safeWarnableName(Locatable locatable) { + if (locatable == null) { + return ''; + } + + return locatable.fullyQualifiedName.replaceFirst(':', '-'); + } + + List get packages => packageMap.values.toList(); + + List _publicPackages; + + List get publicPackages { + if (_publicPackages == null) { + assert(allLibrariesAdded); + // Help the user if they pass us a package that doesn't exist. + for (String packageName in config.packageOrder) { + if (!packages.map((p) => p.name).contains(packageName)) { + warnOnElement( + null, PackageWarning.packageOrderGivesMissingPackageName, + message: + "${packageName}, packages: ${packages.map((p) => p.name).join(',')}"); + } + } + _publicPackages = packages.where((p) => p.isPublic).toList()..sort(); + } + return _publicPackages; + } + + /// Local packages are to be documented locally vs. remote or not at all. + List get localPackages => + publicPackages.where((p) => p.isLocal).toList(); + + /// Documented packages are documented somewhere (local or remote). + Iterable get documentedPackages => + packages.where((p) => p.documentedWhere != DocumentLocation.missing); + + Map> _libraryElementReexportedBy = Map(); + + /// Prevent cycles from breaking our stack. + Set> _reexportsTagged = Set(); + + void _tagReexportsFor( + final Library topLevelLibrary, final LibraryElement libraryElement, + [ExportElement lastExportedElement]) { + Tuple2 key = + Tuple2(topLevelLibrary, libraryElement); + if (_reexportsTagged.contains(key)) { + return; + } + _reexportsTagged.add(key); + if (libraryElement == null) { + // The first call to _tagReexportFor should not have a null libraryElement. + assert(lastExportedElement != null); + warnOnElement( + findButDoNotCreateLibraryFor(lastExportedElement.enclosingElement), + PackageWarning.unresolvedExport, + message: '"${lastExportedElement.uri}"', + referredFrom: [topLevelLibrary]); + return; + } + _libraryElementReexportedBy.putIfAbsent(libraryElement, () => Set()); + _libraryElementReexportedBy[libraryElement].add(topLevelLibrary); + for (ExportElement exportedElement in libraryElement.exports) { + _tagReexportsFor( + topLevelLibrary, exportedElement.exportedLibrary, exportedElement); + } + } + + int _lastSizeOfAllLibraries = 0; + + Map> get libraryElementReexportedBy { + // Table must be reset if we're still in the middle of adding libraries. + if (allLibraries.keys.length != _lastSizeOfAllLibraries) { + _lastSizeOfAllLibraries = allLibraries.keys.length; + _libraryElementReexportedBy = Map>(); + _reexportsTagged = Set(); + for (Library library in publicLibraries) { + _tagReexportsFor(library, library.element); + } + } + return _libraryElementReexportedBy; + } + + /// A lookup index for hrefs to allow warnings to indicate where a broken + /// link or orphaned file may have come from. Not cached because + /// [ModelElement]s can be created at any time and we're basing this + /// on more than just [allLocalModelElements] to make the error messages + /// comprehensive. + Map> get allHrefs { + Map> hrefMap = Map(); + // TODO(jcollins-g ): handle calculating hrefs causing new elements better + // than toList(). + for (ModelElement modelElement + in allConstructedModelElements.values.toList()) { + // Technically speaking we should be able to use canonical model elements + // only here, but since the warnings that depend on this debug + // canonicalization problems, don't limit ourselves in case an href is + // generated for something non-canonical. + if (modelElement is Dynamic) continue; + // TODO: see [Accessor.enclosingCombo] + if (modelElement is Accessor) continue; + if (modelElement.href == null) continue; + hrefMap.putIfAbsent(modelElement.href, () => Set()); + hrefMap[modelElement.href].add(modelElement); + } + for (Package package in packageMap.values) { + for (Library library in package.libraries) { + if (library.href == null) continue; + hrefMap.putIfAbsent(library.href, () => Set()); + hrefMap[library.href].add(library); + } + } + return hrefMap; + } + + void _addToImplementors(Class c) { + assert(!allImplementorsAdded); + _implementors.putIfAbsent(c.href, () => []); + void _checkAndAddClass(Class key, Class implClass) { + _implementors.putIfAbsent(key.href, () => []); + List list = _implementors[key.href]; + + if (!list.any((l) => l.element == c.element)) { + list.add(implClass); + } + } + + if (c.mixins.isNotEmpty) { + c.mixins.forEach((t) { + _checkAndAddClass(t.element, c); + }); + } + if (c.supertype != null) { + _checkAndAddClass(c.supertype.element, c); + } + if (c.interfaces.isNotEmpty) { + c.interfaces.forEach((t) { + _checkAndAddClass(t.element, c); + }); + } + } + + List get libraries => + packages.expand((p) => p.libraries).toList()..sort(); + + List _publicLibraries; + + Iterable get publicLibraries { + if (_publicLibraries == null) { + assert(allLibrariesAdded); + _publicLibraries = utils.filterNonPublic(libraries).toList(); + } + return _publicLibraries; + } + + List _localLibraries; + + Iterable get localLibraries { + if (_localLibraries == null) { + assert(allLibrariesAdded); + _localLibraries = localPackages.expand((p) => p.libraries).toList() + ..sort(); + } + return _localLibraries; + } + + List _localPublicLibraries; + + Iterable get localPublicLibraries { + if (_localPublicLibraries == null) { + assert(allLibrariesAdded); + _localPublicLibraries = utils.filterNonPublic(localLibraries).toList(); + } + return _localPublicLibraries; + } + + Set _inheritThrough; + + /// Return the set of [Class]es objects should inherit through if they + /// show up in the inheritance chain. Do not call before interceptorElement is + /// found. Add classes here if they are similar to Interceptor in that they + /// are to be ignored even when they are the implementors of [Inheritable]s, + /// and the class these inherit from should instead claim implementation. + Set get inheritThrough { + if (_inheritThrough == null) { + _inheritThrough = Set(); + _inheritThrough.add(specialClasses[SpecialClass.interceptor]); + } + return _inheritThrough; + } + + Set _invisibleAnnotations; + + /// Returns the set of [Class] objects that are similar to pragma + /// in that we should never count them as documentable annotations. + Set get invisibleAnnotations { + if (_invisibleAnnotations == null) { + _invisibleAnnotations = Set(); + _invisibleAnnotations.add(specialClasses[SpecialClass.pragma]); + } + return _invisibleAnnotations; + } + + @override + String toString() => 'PackageGraph built from ${defaultPackage.name}'; + + final Map _canonicalLibraryFor = Map(); + + /// Tries to find a top level library that references this element. + Library findCanonicalLibraryFor(Element e) { + assert(allLibrariesAdded); + Element searchElement = e; + if (e is PropertyAccessorElement) { + searchElement = e.variable; + } + if (e is GenericFunctionTypeElement) { + searchElement = e.enclosingElement; + } + + if (_canonicalLibraryFor.containsKey(e)) { + return _canonicalLibraryFor[e]; + } + _canonicalLibraryFor[e] = null; + for (Library library in publicLibraries) { + if (library.modelElementsMap.containsKey(searchElement)) { + for (ModelElement modelElement + in library.modelElementsMap[searchElement]) { + if (modelElement.isCanonical) { + _canonicalLibraryFor[e] = library; + break; + } + } + } + } + return _canonicalLibraryFor[e]; + } + + // TODO(jcollins-g): Revise when dart-lang/sdk#29600 is fixed. + static Element getBasestElement(Element possibleMember) { + Element element = possibleMember; + while (element is Member) { + element = (element as Member).baseElement; + } + return element; + } + + /// Tries to find a canonical ModelElement for this element. If we know + /// this element is related to a particular class, pass preferredClass to + /// disambiguate. + /// + /// This doesn't know anything about [PackageGraph.inheritThrough] and probably + /// shouldn't, so using it with [Inheritable]s without special casing is + /// not advised. + ModelElement findCanonicalModelElementFor(Element e, + {Container preferredClass}) { + assert(allLibrariesAdded); + Library lib = findCanonicalLibraryFor(e); + if (preferredClass != null && preferredClass is Container) { + Container canonicalClass = + findCanonicalModelElementFor(preferredClass.element); + if (canonicalClass != null) preferredClass = canonicalClass; + } + if (lib == null && preferredClass != null) { + lib = findCanonicalLibraryFor(preferredClass.element); + } + ModelElement modelElement; + // For elements defined in extensions, they are canonical. + if (e?.enclosingElement is ExtensionElement) { + lib ??= Library(e.enclosingElement.library, packageGraph); + // (TODO:keertip) Find a better way to exclude members of extensions + // when libraries are specified using the "--include" flag + if (lib?.isDocumented == true) { + return ModelElement.from(e, lib, packageGraph); + } + } + // TODO(jcollins-g): Special cases are pretty large here. Refactor to split + // out into helpers. + // TODO(jcollins-g): The data structures should be changed to eliminate guesswork + // with member elements. + if (e is ClassMemberElement || e is PropertyAccessorElement) { + if (e is Member) e = getBasestElement(e); + Set candidates = Set(); + Tuple2 iKey = Tuple2(e, lib); + Tuple4 key = + Tuple4(e, lib, null, null); + Tuple4 keyWithClass = + Tuple4(e, lib, preferredClass, null); + if (allConstructedModelElements.containsKey(key)) { + candidates.add(allConstructedModelElements[key]); + } + if (allConstructedModelElements.containsKey(keyWithClass)) { + candidates.add(allConstructedModelElements[keyWithClass]); + } + if (candidates.isEmpty && allInheritableElements.containsKey(iKey)) { + candidates + .addAll(allInheritableElements[iKey].where((me) => me.isCanonical)); + } + Class canonicalClass = findCanonicalModelElementFor(e.enclosingElement); + if (canonicalClass != null) { + candidates.addAll(canonicalClass.allCanonicalModelElements.where((m) { + if (m.element == e) return true; + return false; + })); + } + Set matches = Set() + ..addAll(candidates.where((me) => me.isCanonical)); + + // It's possible to find accessors but no combos. Be sure that if we + // have Accessors, we find their combos too. + if (matches.any((me) => me is Accessor)) { + List combos = + matches.whereType().map((a) => a.enclosingCombo).toList(); + matches.addAll(combos); + assert(combos.every((c) => c.isCanonical)); + } + + // This is for situations where multiple classes may actually be canonical + // for an inherited element whose defining Class is not canonical. + if (matches.length > 1 && + preferredClass != null && + preferredClass is Class) { + // Search for matches inside our superchain. + List superChain = preferredClass.superChain + .map((et) => et.element) + .cast() + .toList(); + superChain.add(preferredClass); + matches.removeWhere((me) => + !superChain.contains((me as EnclosedElement).enclosingElement)); + // Assumed all matches are EnclosedElement because we've been told about a + // preferredClass. + Set enclosingElements = Set() + ..addAll(matches + .map((me) => (me as EnclosedElement).enclosingElement as Class)); + for (Class c in superChain.reversed) { + if (enclosingElements.contains(c)) { + matches.removeWhere( + (me) => (me as EnclosedElement).enclosingElement != c); + } + if (matches.length <= 1) break; + } + } + + // Prefer a GetterSetterCombo to Accessors. + if (matches.any((me) => me is GetterSetterCombo)) { + matches.removeWhere((me) => me is Accessor); + } + + assert(matches.length <= 1); + if (matches.isNotEmpty) { + modelElement = matches.first; + } + } else { + if (lib != null) { + Accessor getter; + Accessor setter; + if (e is PropertyInducingElement) { + if (e.getter != null) { + getter = ModelElement.from(e.getter, lib, packageGraph); + } + if (e.setter != null) { + setter = ModelElement.from(e.setter, lib, packageGraph); + } + } + modelElement = ModelElement.from(e, lib, packageGraph, + getter: getter, setter: setter); + } + assert(modelElement is! Inheritable); + if (modelElement != null && !modelElement.isCanonical) { + modelElement = null; + } + } + // Prefer Fields. + if (e is PropertyAccessorElement && modelElement is Accessor) { + modelElement = (modelElement as Accessor).enclosingCombo; + } + return modelElement; + } + + /// This is used when we might need a Library object that isn't actually + /// a documentation entry point (for elements that have no Library within the + /// set of canonical Libraries). + Library findButDoNotCreateLibraryFor(Element e) { + // This is just a cache to avoid creating lots of libraries over and over. + if (allLibraries.containsKey(e.library)) { + return allLibraries[e.library]; + } + return null; + } + + /// This is used when we might need a Library object that isn't actually + /// a documentation entry point (for elements that have no Library within the + /// set of canonical Libraries). + Library findOrCreateLibraryFor(ResolvedLibraryResult result) { + // This is just a cache to avoid creating lots of libraries over and over. + if (allLibraries.containsKey(result.element.library)) { + return allLibraries[result.element.library]; + } + // can be null if e is for dynamic + if (result.element.library == null) { + return null; + } + Library foundLibrary = Library.fromLibraryResult( + result, + this, + Package.fromPackageMeta( + PackageMeta.fromElement(result.element.library, config), + packageGraph)); + allLibraries[result.element.library] = foundLibrary; + return foundLibrary; + } + + List _allModelElements; + + Iterable get allModelElements { + assert(allLibrariesAdded); + if (_allModelElements == null) { + _allModelElements = []; + Set packagesToDo = packages.toSet(); + Set completedPackages = Set(); + while (packagesToDo.length > completedPackages.length) { + packagesToDo.difference(completedPackages).forEach((Package p) { + Set librariesToDo = p.allLibraries.toSet(); + Set completedLibraries = Set(); + while (librariesToDo.length > completedLibraries.length) { + librariesToDo + .difference(completedLibraries) + .forEach((Library library) { + _allModelElements.addAll(library.allModelElements); + completedLibraries.add(library); + }); + librariesToDo.addAll(p.allLibraries); + } + completedPackages.add(p); + }); + packagesToDo.addAll(packages); + } + } + return _allModelElements; + } + + List _allLocalModelElements; + + Iterable get allLocalModelElements { + assert(allLibrariesAdded); + if (_allLocalModelElements == null) { + _allLocalModelElements = []; + this.localLibraries.forEach((library) { + _allLocalModelElements.addAll(library.allModelElements); + }); + } + return _allLocalModelElements; + } + + List _allCanonicalModelElements; + + Iterable get allCanonicalModelElements { + return (_allCanonicalModelElements ??= + allLocalModelElements.where((e) => e.isCanonical).toList()); + } + + String getMacro(String name) { + assert(_localDocumentationBuilt); + return _macros[name]; + } + + void addMacro(String name, String content) { + assert(!_localDocumentationBuilt); + _macros[name] = content; + } + + String getHtmlFragment(String name) { + assert(_localDocumentationBuilt); + return _htmlFragments[name]; + } + + void addHtmlFragment(String name, String content) { + assert(!_localDocumentationBuilt); + _htmlFragments[name] = content; + } +} diff --git a/lib/src/model/parameter.dart b/lib/src/model/parameter.dart new file mode 100644 index 0000000000..a1a028668f --- /dev/null +++ b/lib/src/model/parameter.dart @@ -0,0 +1,75 @@ +// 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/element/element.dart'; +import 'package:analyzer/src/dart/element/member.dart' show Member; +import 'package:dartdoc/src/model/model.dart'; + +class Parameter extends ModelElement implements EnclosedElement { + Parameter( + ParameterElement element, Library library, PackageGraph packageGraph, + {Member originalMember}) + : super(element, library, packageGraph, originalMember); + + String get defaultValue { + if (!hasDefaultValue) return null; + return _parameter.defaultValueCode; + } + + @override + ModelElement get enclosingElement => (_parameter.enclosingElement != null) + ? ModelElement.from(_parameter.enclosingElement, library, packageGraph) + : null; + + bool get hasDefaultValue { + return _parameter.defaultValueCode != null && + _parameter.defaultValueCode.isNotEmpty; + } + + @override + String get href { + throw StateError('href not implemented for parameters'); + } + + @override + String get htmlId { + if (_parameter.enclosingElement != null) { + String enclosingName = _parameter.enclosingElement.name; + if (_parameter.enclosingElement is GenericFunctionTypeElement) { + // TODO(jcollins-g): Drop when GenericFunctionTypeElement populates name. + // Also, allowing null here is allowed as a workaround for + // dart-lang/sdk#32005. + for (Element e = _parameter.enclosingElement; + e.enclosingElement != null; + e = e.enclosingElement) { + enclosingName = e.name; + if (enclosingName != null && enclosingName.isNotEmpty) break; + } + } + return '${enclosingName}-param-${name}'; + } else { + return 'param-${name}'; + } + } + + @override + int get hashCode => element == null ? 0 : element.hashCode; + + @override + bool operator ==(Object object) => + object is Parameter && (_parameter.type == object._parameter.type); + + bool get isCovariant => _parameter.isCovariant; + + bool get isOptional => _parameter.isOptional; + + bool get isOptionalNamed => _parameter.isNamed; + + bool get isOptionalPositional => _parameter.isOptionalPositional; + + @override + String get kind => 'parameter'; + + ParameterElement get _parameter => element as ParameterElement; +} diff --git a/lib/src/model/privacy.dart b/lib/src/model/privacy.dart new file mode 100644 index 0000000000..9961eb0dda --- /dev/null +++ b/lib/src/model/privacy.dart @@ -0,0 +1,8 @@ +// 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. + +/// Classes implementing this have a public/private distinction. +abstract class Privacy { + bool get isPublic; +} diff --git a/lib/src/model/source_code_mixin.dart b/lib/src/model/source_code_mixin.dart new file mode 100644 index 0000000000..1c09f98d13 --- /dev/null +++ b/lib/src/model/source_code_mixin.dart @@ -0,0 +1,21 @@ +// 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/element/element.dart'; +import 'package:analyzer/source/line_info.dart'; +import 'package:dartdoc/src/model/model.dart'; + +abstract class SourceCodeMixin implements Documentable { + ModelNode get modelNode; + + CharacterLocation get characterLocation; + + Element get element; + + bool get hasSourceCode => config.includeSource && sourceCode.isNotEmpty; + + Library get library; + + String get sourceCode => modelNode == null ? '' : modelNode.sourceCode; +} diff --git a/lib/src/model/top_level_container.dart b/lib/src/model/top_level_container.dart new file mode 100644 index 0000000000..b5fc82fbe0 --- /dev/null +++ b/lib/src/model/top_level_container.dart @@ -0,0 +1,72 @@ +// 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:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/model_utils.dart' as model_utils; + +/// A set of [Class]es, [Enum]s, [TopLevelVariable]s, [ModelFunction]s, +/// [Property]s, and [Typedef]s, possibly initialized after construction by +/// accessing private member variables. Do not call any methods or members +/// excepting [name] and the private Lists below before finishing initialization +/// of a [TopLevelContainer]. +abstract class TopLevelContainer implements Nameable { + Iterable get classes; + + Iterable get extensions; + + Iterable get enums; + + Iterable get mixins; + + Iterable get exceptions; + + Iterable get constants; + + Iterable get properties; + + Iterable get functions; + + Iterable get typedefs; + + bool get hasPublicClasses => publicClasses.isNotEmpty; + + bool get hasPublicExtensions => publicExtensions.isNotEmpty; + + bool get hasPublicConstants => publicConstants.isNotEmpty; + + bool get hasPublicEnums => publicEnums.isNotEmpty; + + bool get hasPublicExceptions => publicExceptions.isNotEmpty; + + bool get hasPublicFunctions => publicFunctions.isNotEmpty; + + bool get hasPublicMixins => publicMixins.isNotEmpty; + + bool get hasPublicProperties => publicProperties.isNotEmpty; + + bool get hasPublicTypedefs => publicTypedefs.isNotEmpty; + + Iterable get publicClasses => model_utils.filterNonPublic(classes); + + Iterable get publicExtensions => + model_utils.filterNonPublic(extensions); + + Iterable get publicConstants => + model_utils.filterNonPublic(constants); + + Iterable get publicEnums => model_utils.filterNonPublic(enums); + + Iterable get publicExceptions => + model_utils.filterNonPublic(exceptions); + + Iterable get publicFunctions => + model_utils.filterNonPublic(functions); + + Iterable get publicMixins => model_utils.filterNonPublic(mixins); + + Iterable get publicProperties => + model_utils.filterNonPublic(properties); + + Iterable get publicTypedefs => model_utils.filterNonPublic(typedefs); +} diff --git a/lib/src/model/top_level_variable.dart b/lib/src/model/top_level_variable.dart new file mode 100644 index 0000000000..2d93a8c174 --- /dev/null +++ b/lib/src/model/top_level_variable.dart @@ -0,0 +1,88 @@ +// 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/element/element.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; + +/// Top-level variables. But also picks up getters and setters? +class TopLevelVariable extends ModelElement + with Canonicalization, GetterSetterCombo, SourceCodeMixin, Categorization + implements EnclosedElement { + @override + final Accessor getter; + @override + final Accessor setter; + + TopLevelVariable(TopLevelVariableElement element, Library library, + PackageGraph packageGraph, this.getter, this.setter) + : super(element, library, packageGraph, null) { + if (getter != null) { + getter.enclosingCombo = this; + } + if (setter != null) { + setter.enclosingCombo = this; + } + } + + @override + bool get isInherited => false; + + @override + String get documentation { + // Verify that hasSetter and hasGetterNoSetter are mutually exclusive, + // to prevent displaying more or less than one summary. + if (isPublic) { + Set assertCheck = Set() + ..addAll([hasPublicSetter, hasPublicGetterNoSetter]); + assert(assertCheck.containsAll([true, false])); + } + return super.documentation; + } + + @override + ModelElement get enclosingElement => library; + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(canonicalLibrary != null); + assert(canonicalLibrary == library); + return '${package.baseHref}${library.dirName}/$fileName'; + } + + @override + bool get isConst => _variable.isConst; + + @override + bool get isFinal { + /// isFinal returns true for the variable even if it has an explicit getter + /// (which means we should not document it as "final"). + if (hasExplicitGetter) return false; + return _variable.isFinal; + } + + @override + String get kind => isConst ? 'top-level constant' : 'top-level property'; + + @override + Set get features => super.features..addAll(comboFeatures); + + @override + String computeDocumentationComment() { + String docs = getterSetterDocumentationComment; + if (docs.isEmpty) return _variable.documentationComment; + return docs; + } + + @override + String get fileName => isConst ? '$name-constant.html' : '$name.html'; + + @override + DefinedElementType get modelType => super.modelType; + + TopLevelVariableElement get _variable => (element as TopLevelVariableElement); +} diff --git a/lib/src/model/type_parameter.dart b/lib/src/model/type_parameter.dart new file mode 100644 index 0000000000..fc5d0aeb1f --- /dev/null +++ b/lib/src/model/type_parameter.dart @@ -0,0 +1,100 @@ +// 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/element/element.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; + +class TypeParameter extends ModelElement { + TypeParameter( + TypeParameterElement element, Library library, PackageGraph packageGraph) + : super(element, library, packageGraph, null); + + @override + ModelElement get enclosingElement => (element.enclosingElement != null) + ? ModelElement.from(element.enclosingElement, library, packageGraph) + : null; + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(canonicalLibrary != null); + assert(canonicalLibrary == library); + return '${package.baseHref}${enclosingElement.library.dirName}/${enclosingElement.name}/$name'; + } + + @override + String get kind => 'type parameter'; + + ElementType _boundType; + + ElementType get boundType { + if (_boundType == null) { + var bound = _typeParameter.bound; + if (bound != null) { + _boundType = ElementType.from(bound, library, packageGraph); + } + } + return _boundType; + } + + String _name; + + @override + String get name { + if (_name == null) { + _name = _typeParameter.bound != null + ? '${_typeParameter.name} extends ${boundType.nameWithGenerics}' + : _typeParameter.name; + } + return _name; + } + + String _linkedName; + + @override + String get linkedName { + if (_linkedName == null) { + _linkedName = _typeParameter.bound != null + ? '${_typeParameter.name} extends ${boundType.linkedName}' + : _typeParameter.name; + } + return _linkedName; + } + + TypeParameterElement get _typeParameter => element as TypeParameterElement; +} + +abstract class TypeParameters implements ModelElement { + String get nameWithGenerics => '$name$genericParameters'; + + String get nameWithLinkedGenerics => '$name$linkedGenericParameters'; + + bool get hasGenericParameters => typeParameters.isNotEmpty; + + String get genericParameters { + if (typeParameters.isEmpty) return ''; + + var joined = typeParameters + .map((t) => t.name) + .join(', '); + return '<${joined}>'; + } + + String get linkedGenericParameters { + if (typeParameters.isEmpty) return ''; + + var joined = typeParameters + .map((t) => t.linkedName) + .join(', '); + return '<${joined}>'; + } + + @override + DefinedElementType get modelType; + + List get typeParameters; +} diff --git a/lib/src/model/typedef.dart b/lib/src/model/typedef.dart new file mode 100644 index 0000000000..15a168e4a8 --- /dev/null +++ b/lib/src/model/typedef.dart @@ -0,0 +1,65 @@ +// 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/element/element.dart'; +import 'package:dartdoc/src/element_type.dart'; +import 'package:dartdoc/src/model/model.dart'; + +class Typedef extends ModelElement + with SourceCodeMixin, TypeParameters, Categorization + implements EnclosedElement { + Typedef(FunctionTypeAliasElement element, Library library, + PackageGraph packageGraph) + : super(element, library, packageGraph, null); + + @override + ModelElement get enclosingElement => library; + + @override + String get nameWithGenerics => '$name${super.genericParameters}'; + + @override + String get genericParameters { + if (element is GenericTypeAliasElement) { + List genericTypeParameters = + (element as GenericTypeAliasElement).function.typeParameters; + if (genericTypeParameters.isNotEmpty) { + var joined = genericTypeParameters + .map((t) => t.name) + .join(', '); + return '<${joined}>'; + } + } // else, all types are resolved. + return ''; + } + + @override + String get href { + if (!identical(canonicalModelElement, this)) { + return canonicalModelElement?.href; + } + assert(canonicalLibrary != null); + assert(canonicalLibrary == library); + return '${package.baseHref}${library.dirName}/$fileName'; + } + + // Food for mustache. + bool get isInherited => false; + + @override + String get kind => 'typedef'; + + String get linkedReturnType => modelType.createLinkedReturnTypeName(); + + @override + DefinedElementType get modelType => super.modelType; + + FunctionTypeAliasElement get _typedef => + (element as FunctionTypeAliasElement); + + @override + List get typeParameters => _typedef.typeParameters.map((f) { + return ModelElement.from(f, library, packageGraph) as TypeParameter; + }).toList(); +} diff --git a/lib/src/model_utils.dart b/lib/src/model_utils.dart index 18fbc358ed..35c1d1d48c 100644 --- a/lib/src/model_utils.dart +++ b/lib/src/model_utils.dart @@ -11,7 +11,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/src/dart/ast/utilities.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/element_type.dart'; final Map _fileContents = {}; diff --git a/lib/src/source_linker.dart b/lib/src/source_linker.dart index 992e5fe626..3e86e44ae4 100644 --- a/lib/src/source_linker.dart +++ b/lib/src/source_linker.dart @@ -6,7 +6,7 @@ library dartdoc.source_linker; import 'package:dartdoc/src/dartdoc_options.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:path/path.dart' as path; final uriTemplateRegexp = RegExp(r'(%[frl]%)'); diff --git a/lib/src/special_elements.dart b/lib/src/special_elements.dart index 0dceb95f68..028f665864 100644 --- a/lib/src/special_elements.dart +++ b/lib/src/special_elements.dart @@ -11,7 +11,7 @@ library dartdoc.special_elements; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/src/generated/sdk.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; /// Which of the [SpecialClasses] to get. enum SpecialClass { diff --git a/lib/src/warnings.dart b/lib/src/warnings.dart index a2a2280f12..2d59d7be22 100644 --- a/lib/src/warnings.dart +++ b/lib/src/warnings.dart @@ -7,7 +7,7 @@ import 'dart:io'; import 'package:analyzer/dart/element/element.dart'; import 'package:dartdoc/src/dartdoc_options.dart'; import 'package:dartdoc/src/logging.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/package_meta.dart'; import 'package:dartdoc/src/tuple.dart'; @@ -208,22 +208,6 @@ abstract class Warnable implements Canonicalization { Package get package; } -/// Something that can be located for warning purposes. -abstract class Locatable { - List get documentationFrom; - - /// True if documentationFrom contains only one item, [this]. - bool get documentationIsLocal => documentationFrom.length == 1 && identical(documentationFrom.first, this); - - String get fullyQualifiedName; - - String get href; - - /// A string indicating the URI of this Locatable, usually derived from - /// [Element.location]. - String get location; -} - // The kinds of warnings that can be displayed when documenting a package. enum PackageWarning { ambiguousDocReference, diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart index d049533348..02400fe720 100644 --- a/test/dartdoc_test.dart +++ b/test/dartdoc_test.dart @@ -9,7 +9,7 @@ import 'dart:io'; import 'package:dartdoc/dartdoc.dart'; import 'package:dartdoc/src/logging.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/tuple.dart'; import 'package:dartdoc/src/warnings.dart'; import 'package:path/path.dart' as path; diff --git a/test/model_test.dart b/test/model_test.dart index 29e7f246bc..1c103d6a49 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -7,7 +7,7 @@ library dartdoc.model_test; import 'dart:io'; import 'package:dartdoc/dartdoc.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/model_utils.dart'; import 'package:dartdoc/src/special_elements.dart'; import 'package:dartdoc/src/warnings.dart'; diff --git a/test/src/utils.dart b/test/src/utils.dart index 9167896d76..2436e23608 100644 --- a/test/src/utils.dart +++ b/test/src/utils.dart @@ -10,7 +10,7 @@ import 'dart:io'; import 'package:dartdoc/dartdoc.dart'; import 'package:dartdoc/src/html/html_generator.dart'; -import 'package:dartdoc/src/model.dart'; +import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/package_meta.dart'; import 'package:path/path.dart' as path;