blob: f9c41b1963c2088be0b8421bebc489cba489e503 [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:analyzer/file_system/file_system.dart';
import 'package:dartdoc/src/comment_references/model_comment_reference.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 path 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 {
String _name;
PackageGraph _packageGraph;
final Map<String, Category> _nameToCategory = {};
// Creates a package, if necessary, and adds it to the [packageGraph].
factory Package.fromPackageMeta(
PackageMeta packageMeta, PackageGraph packageGraph) {
var packageName = packageMeta.name;
var 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<String, Set<String>> usedAnimationIdsByHref = {};
/// Pieces of the location, split to remove 'package:' and slashes.
@override
Set<String> get locationPieces => {};
/// 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 != null && packageMeta.homepage.isNotEmpty;
String get homepage => packageMeta.homepage;
@override
String get kind => (isSdk) ? 'SDK' : '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;
LibraryContainer get defaultCategory => nameToCategory[null];
String _documentationAsHtml;
@override
String get documentationAsHtml {
if (_documentationAsHtml != null) return _documentationAsHtml;
_documentationAsHtml = Documentation.forElement(this).asHtml;
return _documentationAsHtml;
}
String /*?*/ _documentation;
@override
String get documentation {
if (_documentation == null) {
final docFile = documentationFile;
if (docFile != null) {
_documentation = packageGraph.resourceProvider
.readAsMalformedAllowedStringSync(docFile);
}
}
return _documentation;
}
@override
bool get hasDocumentation => documentation?.isNotEmpty == true;
@override
bool get hasExtendedDocumentation => hasDocumentation;
File /*?*/ _documentationFile;
File /*?*/ get documentationFile =>
_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 {
_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 [DartdocOptionContext.autoIncludeDependencies] is true -- but
/// only if the package was not excluded on the command line.
bool get isLocal {
_isLocal ??= (
// Document as local if this is the default package.
packageMeta == packageGraph.packageMeta ||
// 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?
packageGraph.hasEmbedderSdk &&
packageMeta.isSdk &&
libraries.any((l) => _pathContext.isWithin(
packageGraph.packageMeta.dir.path,
(l.element.source.fullName))) ||
// autoIncludeDependencies means everything is local.
packageGraph.config.autoIncludeDependencies) &&
// Regardless of the above rules, do not document as local if
// we excluded this package by name.
!packageGraph.config.isPackageExcluded(name);
return _isLocal;
}
/* late */ DocumentLocation _documentedWhere;
DocumentLocation get documentedWhere {
if (_documentedWhere == null) {
if (isLocal) {
if (isPublic) {
_documentedWhere = DocumentLocation.local;
}
} else {
if (config.linkToRemote &&
config.linkToUrl.isNotEmpty &&
isPublic &&
!packageGraph.config.isPackageExcluded(name)) {
_documentedWhere = DocumentLocation.remote;
} else {
_documentedWhere = DocumentLocation.missing;
}
}
}
return _documentedWhere;
}
@override
String get enclosingName => packageGraph.defaultPackageName;
String get filePath => 'index.$fileType';
String _fileType;
String get 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.
return _fileType ??= (package.documentedWhere == DocumentLocation.remote)
? 'html'
: config.format;
}
@override
String get fullyQualifiedName => 'package:$name';
String _baseHref;
String get baseHref {
if (_baseHref != null) {
return _baseHref;
}
if (documentedWhere == DocumentLocation.remote) {
_baseHref = _remoteBaseHref;
if (!_baseHref.endsWith('/')) _baseHref = '$_baseHref/';
} else {
_baseHref = config.useBaseHref ? '' : htmlBasePlaceholder;
}
return _baseHref;
}
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 null;
}
});
}
static final _substituteNameVersion = RegExp(r'%([bnv])%');
@override
String get href => '$baseHref$filePath';
@override
String get location => _pathContext.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<Library> get publicLibraries {
assert(libraries.every((l) => l.packageMeta == _packageMeta));
return super.publicLibraries;
}
/// A map of category name to the category itself.
Map<String, Category> 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 (var c in libraries.expand(
(l) => l.allCanonicalModelElements.whereType<Categorization>())) {
if (c.hasCategoryNames) {
for (var category in c.categoryNames) {
categoryFor(category).addItem(c);
}
} else {
// Add to the default category.
categoryFor(null).addItem(c);
}
}
}
return _nameToCategory;
}
List<Category> _categories;
List<Category> get categories {
_categories ??= nameToCategory.values.where((c) => c.name != null).toList()
..sort();
return _categories;
}
Iterable<Category> get categoriesWithPublicLibraries =>
categories.where((c) => c.publicLibraries.isNotEmpty);
Iterable<Category> get documentedCategories =>
categories.where((c) => c.isDocumented);
Iterable<Category> get documentedCategoriesSorted {
// Category display order is configurable; leave the category order
// as defined if the order is specified.
if (config.categoryOrder.isEmpty) {
return documentedCategories;
}
return documentedCategories.toList()..sort(byName);
}
bool get hasDocumentedCategories => documentedCategories.isNotEmpty;
DartdocOptionContext _config;
@override
DartdocOptionContext get config {
_config ??= DartdocOptionContext.fromContext(
packageGraph.config,
packageGraph.resourceProvider.getFolder(packagePath),
packageGraph.resourceProvider);
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 {
_packagePath ??= _pathContext.canonicalize(packageMeta.dir.path);
return _packagePath;
}
String get version => packageMeta.version ?? '0.0.0-unknown';
final PackageMeta _packageMeta;
PackageMeta get packageMeta => _packageMeta;
@override
Element get element => null;
@override
List<String> get containerOrder => config.packageOrder;
Map<String, CommentReferable> _referenceChildren;
@override
Map<String, CommentReferable> get referenceChildren {
if (_referenceChildren == null) {
_referenceChildren = {};
_referenceChildren.addEntries(publicLibrariesSorted.generateEntries());
// 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.
_referenceChildren.addEntriesIfAbsent(
publicLibrariesSorted.expand((l) => l.referenceChildren.entries));
}
return _referenceChildren;
}
@override
Iterable<CommentReferable> get referenceParents => [packageGraph];
path.Context get _pathContext => _packageGraph.resourceProvider.pathContext;
@override
// Packages are not interpreted by the analyzer in such a way to generate
// [CommentReference] nodes, so this is always empty.
Map<String, ModelCommentReference> get commentRefs => {};
@override
String get referenceName => 'package:$name';
}