blob: 79cbd78a4507a81e5593bd518c36c425a799ba87 [file] [log] [blame]
// 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/dartdoc_options.dart';
import 'package:dartdoc/src/io_utils.dart';
import 'package:dartdoc/src/model/comment_referable.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/model/model_object_builder.dart';
import 'package:dartdoc/src/package_meta.dart';
import 'package:dartdoc/src/warnings.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p show Context;
import 'package:pub_semver/pub_semver.dart';
// All hrefs are emitted as relative paths from the output root. We are unable
// to compute them from the page we are generating, and many properties computed
// using hrefs are memoized anyway. To build complete relative hrefs, we emit
// the href with this placeholder, and then replace it with the current page's
// base href afterwards.
// See https://github.com/dart-lang/dartdoc/issues/2090 for further context.
// TODO: Find an approach that doesn't require doing this.
// Unlikely to be mistaken for an identifier, html tag, or something else that
// might reasonably exist normally.
@internal
const String htmlBasePlaceholder = r'%%__HTMLBASE_dartdoc_internal__%%';
/// A [LibraryContainer] that contains [Library] objects related to a particular
/// package.
class Package extends LibraryContainer
with
Nameable,
Locatable,
Canonicalization,
Warnable,
CommentReferable,
ModelBuilder
implements Privacy, Documentable {
@override
final String name;
@override
final PackageGraph packageGraph;
// Creates a package, if necessary, and adds it to the [packageGraph].
factory Package.fromPackageMeta(
PackageMeta packageMeta, PackageGraph packageGraph) {
var packageName = packageMeta.name;
var expectNonLocal = !packageGraph.packageMap.containsKey(packageName) &&
packageGraph.allLibrariesAdded;
var packagePath = packageGraph.resourceProvider.pathContext
.canonicalize(packageMeta.dir.path);
var package = packageGraph.packageMap.putIfAbsent(
packageMeta.name,
() => Package._(
packageMeta.name,
packageGraph,
packageMeta,
packagePath,
),
);
// 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 && package.documentedWhere == DocumentLocation.local),
"Found more libraries to document after 'allLibrariesAdded' was set to "
'true');
return package;
}
Package._(this.name, this.packageGraph, this.packageMeta, this.packagePath)
: config = DartdocOptionContext.fromContext(
packageGraph.config,
packageGraph.resourceProvider.getFolder(packagePath),
packageGraph.resourceProvider,
);
@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<String?, Set<String>> usedAnimationIdsByHref = {};
/// Pieces of the location, split to remove 'package:' and slashes.
@override
Set<String> get locationPieces => const {};
/// 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.
final Set<Library> allLibraries = {};
bool get hasHomepage => packageMeta.homepage.isNotEmpty;
String get homepage => packageMeta.homepage;
@override
Kind get kind => isSdk ? Kind.sdk : Kind.package;
@override
List<Locatable> get documentationFrom => [this];
/// Return true if the code has defined non-default categories for libraries
/// in this package.
bool get hasCategories => categories.isNotEmpty;
@override
late final String documentationAsHtml = Documentation.forElement(this).asHtml;
/// The documentation from the README contents.
@override
late final String? documentation = () {
final docFile = packageMeta.getReadmeContents();
return docFile != null
? packageGraph.resourceProvider
.readAsMalformedAllowedStringSync(docFile)
: null;
}();
@override
bool get hasDocumentation => documentation?.isNotEmpty == true;
@override
String get oneLineDoc => '';
@override
bool get isDocumented =>
isFirstPackage || documentedWhere != DocumentLocation.missing;
/// If we have public libraries, this is the default package, or we are
/// auto-including dependencies, this package is public.
@override
bool get isPublic =>
_isLocalPublicByDefault || libraries.any((l) => l.isPublic);
/// Return true if this is the default package, this is part of an embedder
/// SDK, or if [DartdocOptionContext.autoIncludeDependencies] is true -- but
/// only if the package was not excluded on the command line.
late final bool isLocal = () {
// Do not document as local if we excluded this package by name.
if (_isExcluded) return false;
// Document as local if this is the default package.
if (_isLocalPublicByDefault) return true;
// Assume we want to document an embedded SDK as local if it has libraries
// defined in the default package.
// TODO(jcollins-g): Handle case where embedder SDKs can be assembled from
// multiple locations?
if (!packageGraph.hasEmbedderSdk) return false;
if (!packageMeta.isSdk) return false;
final packagePath = packageGraph.packageMeta.dir.path;
return libraries.any(
(l) => _pathContext.isWithin(packagePath, l.element.source.fullName));
}();
/// True if the global config excludes this package by name.
bool get _isExcluded => packageGraph.config.isPackageExcluded(name);
/// True if this is the package being documented by default, or the
/// global config indicates we are auto-including dependencies.
bool get _isLocalPublicByDefault =>
packageMeta == packageGraph.packageMeta ||
packageGraph.config.autoIncludeDependencies;
/// Returns the location of documentation for this package, for linkToRemote
/// and canonicalization decision making.
late final DocumentLocation documentedWhere = () {
if (isLocal && isPublic) {
return DocumentLocation.local;
}
if (config.linkToRemote &&
config.linkToUrl.isNotEmpty &&
isPublic &&
!packageGraph.config.isPackageExcluded(name)) {
return DocumentLocation.remote;
}
return DocumentLocation.missing;
}();
@override
String get enclosingName => packageGraph.defaultPackageName;
String get filePath => 'index.$fileType';
// TODO(jdkoren): Provide a way to determine file type of a remote package's
// docs. Perhaps make this configurable through dartdoc options.
// In theory, a remote package could be documented in any supported format.
// In practice, devs depend on Dart, Flutter, and/or packages fetched
// from pub.dev, and we know that all of those use html docs.
String get fileType => package.documentedWhere == DocumentLocation.remote
? 'html'
: config.format;
@override
String get fullyQualifiedName => 'package:$name';
late final String baseHref = documentedWhere == DocumentLocation.remote
? (_remoteBaseHref.endsWith('/') ? _remoteBaseHref : '$_remoteBaseHref/')
: (config.useBaseHref ? '' : htmlBasePlaceholder);
String get _remoteBaseHref {
return 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':
{
var version = Version.parse(packageMeta.version);
var tag = 'stable';
if (version.isPreRelease) {
// `version.preRelease` is a `List<dynamic>` with a mix of
// integers and strings. Given this, handle
// "2.8.0-dev.1.0, 2.9.0-1.0.dev", and similar variations.
tag = version.preRelease.whereType<String>().first;
// Who knows about non-SDK packages, but SDKs must conform to the
// known format.
assert(packageMeta.isSdk == false || int.tryParse(tag) == null,
'Got an integer as string instead of the expected "dev" tag');
}
return tag;
}
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 '';
}
});
}
static final _substituteNameVersion = RegExp(r'%([bnv])%');
@override
String get href => '$baseHref$filePath';
@override
String get location => _pathContext.toUri(packageMeta.resolvedDir).toString();
@override
Package get package => this;
// Workaround for mustache4dart issue where templates do not recognize
// inherited properties as being in-context.
@override
Iterable<Library> get publicLibraries {
assert(libraries.every((l) => l.packageMeta == packageMeta));
return super.publicLibraries;
}
/// The default, unnamed category.
///
/// This is initialized by [initializeCategories].
late final Category defaultCategory;
/// A map of category name to the category itself.
///
/// This is initialized by [initializeCategories].
late final Map<String?, Category> nameToCategory;
/// Adds [categorization] to one or more categories in the [nameToCategory]
/// map, if it is annotated with `{@category}`, or `[defaultCategory], if not,
/// via the [addTo] callback.
void addToCategories(
Categorization categorization, void Function(Category) addTo) {
if (!categorization.isCanonical) {
return;
}
var categoryNames = categorization.categoryNames;
if (categoryNames == null || categoryNames.isEmpty) {
addTo(defaultCategory);
return;
}
for (var category in categoryNames) {
addTo(nameToCategory.putIfAbsent(
category, () => Category(category, this, config)));
}
}
/// Initializes the [defaultCategory] and the [nameToCategory] map, with all
/// appropriate elements.
void initializeCategories() {
defaultCategory = Category(null, this, config);
nameToCategory = {};
for (var library in libraries) {
addToCategories(library, (c) => c.libraries.add(library));
for (var constant in library.constants) {
addToCategories(constant, (c) => c.constants.add(constant));
}
for (var function in library.functions) {
addToCategories(function, (c) => c.functions.add(function));
}
for (var property in library.properties) {
addToCategories(property, (c) => c.properties.add(property));
}
for (var typedef_ in library.typedefs) {
addToCategories(typedef_, (c) => c.typedefs.add(typedef_));
}
for (var extension in library.extensions) {
addToCategories(extension, (c) => c.extensions.add(extension));
}
for (var class_ in library.allClasses) {
addToCategories(class_, (c) => c.addClass(class_));
}
for (var enum_ in library.enums) {
addToCategories(enum_, (c) => c.enums.add(enum_));
}
for (var mixin in library.mixins) {
addToCategories(mixin, (c) => c.mixins.add(mixin));
}
}
}
/// The categories, sorted alphabetically by name.
late final List<Category> categories = [
defaultCategory,
...nameToCategory.values,
].where((c) => c.name.isNotEmpty).toList(growable: false)
..sort();
Iterable<Category> get categoriesWithPublicLibraries =>
categories.where((c) => c.publicLibraries.isNotEmpty);
Iterable<Category> get documentedCategories =>
categories.where((c) => c.isDocumented);
/// The documented categories, sorted either by the 'categoryOrder' option, or
/// by name.
///
/// In the case that 'categoryOrder' is given, any documented categories which
/// are not found in 'categoryOrder' are listed after the ones which are,
/// ordered by name.
Iterable<Category> get documentedCategoriesSorted {
if (config.categoryOrder.isNotEmpty) {
final documentedCategories =
this.documentedCategories.toList(growable: false);
return documentedCategories
..sort((a, b) {
var aIndex = config.categoryOrder.indexOf(a.name);
var bIndex = config.categoryOrder.indexOf(b.name);
if (aIndex >= 0 && bIndex >= 0) {
return aIndex.compareTo(bIndex);
} else if (aIndex < 0 && bIndex >= 0) {
// `a` is not found in the category order, but `b` is.
return 1;
} else if (bIndex < 0 && aIndex >= 0) {
// `b` is not found in the category order, but `a` is.
return -1;
} else {
// Neither is found in the category order.
return documentedCategories
.indexOf(a)
.compareTo(documentedCategories.indexOf(b));
}
});
} else {
// Category display order is configurable; leave the category order
// as defined if the order is specified.
return documentedCategories;
}
}
bool get hasDocumentedCategories => documentedCategories.isNotEmpty;
@override
final DartdocOptionContext 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;
final String packagePath;
String get version => packageMeta.version;
final PackageMeta packageMeta;
@override
Element? get element => null;
@override
List<String> get containerOrder => config.packageOrder;
@override
late final Map<String, CommentReferable> referenceChildren =
<String, CommentReferable>{
for (var library in publicLibrariesSorted) library.referenceName: library,
}
// Do not override any preexisting data, and insert based on the
// public library sort order.
// TODO(jcollins-g): warn when results require package-global
// lookups like this.
..addEntriesIfAbsent(
publicLibrariesSorted.expand((l) => l.referenceChildren.entries));
@override
Iterable<CommentReferable> get referenceParents => [packageGraph];
p.Context get _pathContext => packageGraph.resourceProvider.pathContext;
@override
String get referenceName => 'package:$name';
}