blob: 6185e9f3fb310ebf811c6f152201d27c2c616d01 [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/analysis/features.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/scope.dart';
import 'package:analyzer/source/line_info.dart';
// ignore: implementation_imports
import 'package:dartdoc/src/model/comment_referable.dart';
import 'package:dartdoc/src/model/kind.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/package_meta.dart' show PackageMeta;
import 'package:dartdoc/src/warnings.dart';
class _LibrarySentinel implements Library {
@override
dynamic noSuchMethod(Invocation invocation) =>
throw UnimplementedError('No members on Library.sentinel are accessible');
}
class Library extends ModelElement
with Categorization, TopLevelContainer, CanonicalFor {
@override
final LibraryElement2 element;
/// The set of [Element2]s declared directly in this library.
final Set<Element2> _localElements;
/// The set of [Element2]s exported by this library but not directly declared
/// in this library.
final Set<Element2> _exportedElements;
final String _restoredUri;
@override
final Package package;
/// A [Library] value used as a sentinel in three cases:
///
/// * the library for `dynamic` and `Never`
/// * the library for type parameters
/// * the library passed up to [ModelElement.library] when constructing a
/// `Library`, via the super constructor.
///
/// TODO(srawlins): I think this last case demonstrates that
/// [ModelElement.library] should not be a field, and instead should be an
/// abstract getter.
static final Library sentinel = _LibrarySentinel();
Library._(this.element, PackageGraph packageGraph, this.package,
this._restoredUri, this._localElements, this._exportedElements)
: super(sentinel, packageGraph);
factory Library.fromLibraryResult(DartDocResolvedLibrary resolvedLibrary,
PackageGraph packageGraph, Package package) {
packageGraph.gatherModelNodes(resolvedLibrary);
var libraryElement = resolvedLibrary.element;
var localElements = <Element2>{
...libraryElement.firstFragment.getters.map((g) => g.element),
...libraryElement.firstFragment.setters.map((s) => s.element),
...libraryElement.firstFragment.classes2.map((c) => c.element),
...libraryElement.firstFragment.enums2.map((e) => e.element),
...libraryElement.firstFragment.extensions2.map((e) => e.element),
...libraryElement.firstFragment.extensionTypes2.map((e) => e.element),
...libraryElement.firstFragment.functions2.map((f) => f.element),
...libraryElement.firstFragment.mixins2.map((m) => m.element),
...libraryElement.firstFragment.topLevelVariables2.map((v) => v.element),
...libraryElement.firstFragment.typeAliases2.map((a) => a.element),
};
var exportedElements = {
...libraryElement.exportNamespace.definedNames2.values
}.difference(localElements);
var library = Library._(
libraryElement,
packageGraph,
package,
resolvedLibrary.element.firstFragment.source.uri.toString(),
localElements,
exportedElements,
);
package.allLibraries.add(library);
return library;
}
/// Allow scope for Libraries.
@override
Scope get scope => element.firstFragment.scope;
bool get isInSdk => element.isInSdk;
@override
CharacterLocation? get characterLocation {
if (element.firstFragment.nameOffset2 == null) {
return CharacterLocation(1, 1);
}
return super.characterLocation;
}
@override
LibraryFragment get unitElement => element.library2.firstFragment;
@override
/// Whether this library is considered "public."
///
/// A library is considered public if it:
/// * is an SDK library and it is documented and it is not internal, or
/// * is found in a package's top-level 'lib' directory, and
/// not found in it's 'lib/src' directory, and it is not excluded.
bool get isPublic {
if (!super.isPublic) return false;
final sdkLib =
packageGraph.sdkLibrarySources[element.firstFragment.source];
if (sdkLib != null && (sdkLib.isInternal || !sdkLib.isDocumented)) {
return false;
}
if (
// TODO(srawlins): Stop supporting a 'name' here.
config.isLibraryExcluded(name) ||
config.isLibraryExcluded(
element.firstFragment.source.uri.toString())) {
return false;
}
return true;
}
/// Map of each import prefix ('import "foo" as prefix;') to the set of
/// libraries which are imported via that prefix.
Map<String, Set<Library>> get _prefixToLibrary {
var prefixToLibrary = <String, Set<Library>>{};
// It is possible to have overlapping prefixes.
for (var i in element.firstFragment.libraryImports2) {
var prefixName = i.prefix2?.element.name3;
if (prefixName == null) continue;
// Ignore invalid imports.
var importedLibrary = i.importedLibrary2;
if (importedLibrary != null) {
prefixToLibrary
.putIfAbsent(prefixName, () => {})
.add(getModelFor(importedLibrary, library) as Library);
}
}
return prefixToLibrary;
}
/// An identifier for this library based on its location.
///
/// This 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.
late final String dirName = () {
String nameFromPath;
if (isAnonymous) {
assert(!_restoredUri.startsWith('file:'),
'"$_restoredUri" must not start with "file:"');
// Strip the package prefix if the library is part of the default package
// or if it is being documented remotely.
var defaultPackage = package.documentedWhere == DocumentLocation.remote
? package.packageMeta
: package.packageGraph.packageMeta;
var packageNameToHide = defaultPackage.toString().toLowerCase();
var schemaToHide = 'package:$packageNameToHide/';
nameFromPath = _restoredUri;
if (nameFromPath.startsWith(schemaToHide)) {
nameFromPath = nameFromPath.substring(schemaToHide.length);
}
// Remove the trailing `.dart`.
if (nameFromPath.endsWith('.dart')) {
const dartExtensionLength = '.dart'.length;
nameFromPath = nameFromPath.substring(
0, nameFromPath.length - dartExtensionLength);
}
} else {
nameFromPath = name;
}
// Turn `package:foo/bar/baz` into `package-foo_bar_baz`.
return nameFromPath.replaceAll(':', '-').replaceAll('/', '_');
}();
/// Libraries are not enclosed by anything.
@override
ModelElement? get enclosingElement => null;
@override
String get filePath => '$dirName/$fileName';
@override
String get fileName => 'index.html';
String get sidebarPath => '$dirName/$dirName-library-sidebar.html';
/// The library template manually includes 'packages' in the left/above
/// sidebar.
@override
String? get aboveSidebarPath => null;
@override
String get belowSidebarPath => sidebarPath;
@override
String? get href {
if (!identical(canonicalModelElement, this)) {
return canonicalModelElement?.href;
}
// The file name for a library is 'index.html', so we just link to the
// directory name. This keeps the URL looking short, _without_ the
// 'index.html' in the URL.
return '${package.baseHref}$dirName/';
}
/// The previous value of [filePath].
///
/// This path is used to write a file that ontains an HTML redirect (not an
/// HTTP redirect) to a library's current [filePath].
String get redirectingPath => '$dirName/$dirName-library.html';
/// Whether a libary is anonymous, either because it has no library directive
/// or it has a library directive without a name.
bool get isAnonymous => element.name3 == null || element.name3!.isEmpty;
@override
Kind get kind => Kind.library;
@override
Library get library => this;
@override
String get name {
var source = element.firstFragment.source;
if (source.uri.isScheme('dart')) {
// There are inconsistencies in library naming + URIs for the Dart
// SDK libraries; we rationalize them here.
if (source.uri.toString().contains('/')) {
return element.name3!.replaceFirst('dart.', 'dart:');
}
return source.uri.toString();
} else if (element.name3!.isNotEmpty) {
// An empty name indicates that the library is "implicitly named" with the
// empty string. That is, it either has no `library` directive, or it has
// a `library` directive with no name.
return element.name3!;
}
var baseName = pathContext.basename(source.fullName);
if (baseName.endsWith('.dart')) {
const dartExtensionLength = '.dart'.length;
return baseName.substring(0, baseName.length - dartExtensionLength);
}
return baseName;
}
@override
String get displayName {
var fullName = breadcrumbName;
if (fullName.endsWith('.dart')) {
const dartExtensionLength = '.dart'.length;
return fullName.substring(0, fullName.length - dartExtensionLength);
}
return fullName;
}
@override
/// The path portion of this library's import URI as a 'package:' URI.
String get breadcrumbName {
var source = element.firstFragment.source;
if (source.uri.isScheme('dart')) {
return name;
}
var fullName = source.fullName;
if (!pathContext.isWithin(fullName, package.packagePath) &&
package.packagePath.contains('/google3/')) {
// In google3, `fullName` is specified as if the root of google3 was `/`.
// And `package.packagePath` contains the true google3 root.
var root = pathContext
.joinAll(pathContext.split(package.packagePath)..removeLast());
fullName = '$root$fullName';
}
var relativePath =
pathContext.relative(fullName, from: package.packagePath);
assert(relativePath.startsWith('lib${pathContext.separator}'));
const libDirectoryLength = 'lib/'.length;
return relativePath.substring(libDirectoryLength);
}
/// The name of the package we were defined in.
String get packageName => packageMeta?.name ?? '';
/// The real packageMeta, as opposed to the package we are documenting with.
late final PackageMeta? packageMeta =
packageGraph.packageMetaProvider.fromElement(element, config.sdkDir);
late final List<Class> classesAndExceptions = [
..._localElementsOfType<ClassElement2, Class>(),
..._exportedElementsOfType<ClassElement2, Class>(),
];
@override
Iterable<Class> get classes =>
classesAndExceptions.where((c) => !c.isErrorOrException);
@override
late final Iterable<TopLevelVariable> constants = [
..._localVariables.where((v) => v.isConst),
..._exportedVariables.where((v) => v.isConst),
];
@override
late final List<Enum> enums = [
..._localElementsOfType<EnumElement2, Enum>(),
..._exportedElementsOfType<EnumElement2, Enum>(),
];
@override
late final List<Class> exceptions = classesAndExceptions
.where((c) => c.isErrorOrException)
.toList(growable: false);
@override
late final List<Extension> extensions = [
..._localElementsOfType<ExtensionElement2, Extension>(),
..._exportedElementsOfType<ExtensionElement2, Extension>(),
];
@override
late final List<ExtensionType> extensionTypes = [
..._localElementsOfType<ExtensionTypeElement2, ExtensionType>(),
..._exportedElementsOfType<ExtensionTypeElement2, ExtensionType>(),
];
@override
late final List<ModelFunction> functions = [
..._localElementsOfType<TopLevelFunctionElement, ModelFunction>(),
..._exportedElementsOfType<TopLevelFunctionElement, ModelFunction>(),
];
@override
late final List<Mixin> mixins = [
..._localElementsOfType<MixinElement2, Mixin>(),
..._exportedElementsOfType<MixinElement2, Mixin>(),
];
@override
late final List<TopLevelVariable> properties = [
..._localVariables.where((v) => !v.isConst),
..._exportedVariables.where((v) => !v.isConst),
];
@override
late final List<Typedef> typedefs = [
..._localElementsOfType<TypeAliasElement2, Typedef>(),
..._exportedElementsOfType<TypeAliasElement2, Typedef>(),
];
Iterable<U>
_localElementsOfType<T extends Element2, U extends ModelElement>() =>
_localElements
.whereType<T>()
.map((e) => packageGraph.getModelFor(e, this) as U);
Iterable<U>
_exportedElementsOfType<T extends Element2, U extends ModelElement>() =>
_exportedElements.whereType<T>().map((e) {
var library = e.library2;
if (library == null) {
throw StateError("The library of '$e' is null!");
}
return packageGraph.getModelFor(
e,
packageGraph.getModelForElement(library) as Library,
) as U;
});
Iterable<TopLevelVariable> get _localVariables {
return {
..._localElements.whereType<TopLevelVariableElement2>(),
..._localElements
.whereType<PropertyAccessorElement2>()
.map((a) => a.variable3! as TopLevelVariableElement2),
}.map(_topLevelVariableFor);
}
Iterable<TopLevelVariable> get _exportedVariables {
return {
..._exportedElements.whereType<TopLevelVariableElement2>(),
..._exportedElements
.whereType<PropertyAccessorElement2>()
.map((a) => a.variable3! as TopLevelVariableElement2),
}.map(_topLevelVariableFor);
}
TopLevelVariable _topLevelVariableFor(
TopLevelVariableElement2 topLevelVariableElement) {
Accessor? getter;
var elementGetter = topLevelVariableElement.getter2;
if (elementGetter != null) {
getter = packageGraph.getModelFor(elementGetter, this) as Accessor;
}
Accessor? setter;
var elementSetter = topLevelVariableElement.setter2;
if (elementSetter != null) {
setter = packageGraph.getModelFor(elementSetter, this) as Accessor;
}
return getModelForPropertyInducingElement(topLevelVariableElement, this,
getter: getter, setter: setter) as TopLevelVariable;
}
/// All [ModelElement]s, direct and indirect, which are part of this library's
/// export namespace.
// Note: Keep this a late final field; converting to a getter (without further
// investigation) causes dartdoc to hang.
late final List<ModelElement> allModelElements = [
...constants,
...functions,
...properties,
...typedefs,
...extensions,
for (var e in extensions) ...e.allModelElements,
...extensionTypes,
for (var e in extensionTypes) ...e.allModelElements,
...classesAndExceptions,
for (var c in classesAndExceptions) ...c.allModelElements,
...enums,
for (var e in enums) ...e.allModelElements,
...mixins,
for (var m in mixins) ...m.allModelElements,
this,
];
@override
Map<String, CommentReferable> get referenceChildren {
var referenceChildrenBuilder = <String, CommentReferable>{};
var definedNamesModelElements =
element.exportNamespace.definedNames2.values.map(getModelForElement);
referenceChildrenBuilder
.addAll(definedNamesModelElements.whereNotType<Accessor>().asMapByName);
// TODO(jcollins-g): warn and get rid of this case where it shows up.
// If a user is hiding parts of a prefix import, the user should not
// refer to hidden members via the prefix, because that can be
// ambiguous. dart-lang/dartdoc#2683.
for (var MapEntry(key: prefix, value: libraries)
in _prefixToLibrary.entries) {
if (prefix == '_' &&
element.featureSet.isEnabled(Feature.wildcard_variables)) {
// A wildcard import prefix is non-binding.
continue;
}
referenceChildrenBuilder.putIfAbsent(prefix, () => libraries.first);
}
return referenceChildrenBuilder;
}
@override
Iterable<CommentReferable> get referenceParents => [package];
/// Check [canonicalFor] for correctness and warn if it refers to
/// non-existent elements (or those that this Library can not be canonical
/// for).
@override
String buildDocumentationAddition(String rawDocs) {
rawDocs = super.buildDocumentationAddition(rawDocs);
var elementNames = _allOriginalModelElementNames;
var notFoundInAllModelElements = {
for (var elementName in canonicalFor)
if (!elementNames.contains(elementName)) elementName,
};
for (var notFound in notFoundInAllModelElements) {
warn(PackageWarning.ignoredCanonicalFor, message: notFound);
}
// TODO(jcollins-g): warn if a macro/tool generates an unexpected
// canonicalFor?
return rawDocs;
}
/// The immediate elements of this library, 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 re-exporting. Only used for reporting
/// [PackageWarning.ignoredCanonicalFor].
late final Set<String> _allOriginalModelElementNames = () {
// Instead of using `allModelElements`, which includes deeper elements like
// methods on classes, gather up only the library's immediate members.
var libraryMembers = [
...library.extensions,
...library.extensionTypes,
...library.classesAndExceptions,
...library.enums,
...library.mixins,
...library.constants,
...library.functions,
...library.properties,
...library.typedefs,
];
return libraryMembers.map((member) {
if (member is! GetterSetterCombo) {
return getModelForElement(member.element).fullyQualifiedName;
}
var getter = switch (member.getter) {
Accessor accessor => getModelForElement(accessor.element) as Accessor,
_ => null,
};
var setter = switch (member.setter) {
Accessor accessor => getModelForElement(accessor.element) as Accessor,
_ => null,
};
return getModelForPropertyInducingElement(
member.element as TopLevelVariableElement2,
getModelForElement(member.element.library2!) as Library,
getter: getter,
setter: setter,
).fullyQualifiedName;
}).toSet();
}();
}