blob: 2a7b9622e744d3f3f97dc3a729950450170639c7 [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 'dart:collection';
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/type_system.dart';
import 'package:analyzer/dart/element/visitor.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:dartdoc/src/io_utils.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/package_meta.dart' show PackageMeta;
import 'package:dartdoc/src/quiver.dart' as quiver;
import 'package:dartdoc/src/warnings.dart';
import 'package:path/path.dart' as path;
/// Find all hashable children of a given element that are defined in the
/// [LibraryElement] given at initialization.
class _HashableChildLibraryElementVisitor
extends GeneralizingElementVisitor<void> {
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 {
final Set<Element> _exportedAndLocalElements;
final String _restoredUri;
@override
final Package package;
factory Library(LibraryElement element, PackageGraph packageGraph) {
return packageGraph.findButDoNotCreateLibraryFor(element);
}
Library._(LibraryElement element, PackageGraph packageGraph, this.package,
this._restoredUri, this._exportedAndLocalElements)
: super(element, null, packageGraph, null);
factory Library.fromLibraryResult(DartDocResolvedLibrary resolvedLibrary,
PackageGraph packageGraph, Package package) {
var element = resolvedLibrary.result.element;
if (element == null) throw ArgumentError.notNull('element');
// Initialize [packageGraph]'s cache of ModelNodes for relevant
// elements in this library.
var _compilationUnitMap = <String, CompilationUnit>{};
_compilationUnitMap.addEntries(resolvedLibrary.result.units
.map((ResolvedUnitResult u) => MapEntry(u.path, u.unit)));
_HashableChildLibraryElementVisitor((Element e) =>
packageGraph.populateModelNodeFor(e, _compilationUnitMap))
.visitElement(element);
var exportedAndLocalElements = {
// Initialize the list of elements defined in this library and
// exported via its export directives.
...element.exportNamespace.definedNames.values,
// TODO(jcollins-g): Consider switch to [_libraryElement.topLevelElements].
..._getDefinedElements(element.definingCompilationUnit),
for (var cu in element.parts) ..._getDefinedElements(cu),
};
var library = Library._(element, packageGraph, package,
resolvedLibrary.restoredUri, exportedAndLocalElements);
package.allLibraries.add(library);
return library;
}
static Iterable<Element> _getDefinedElements(
CompilationUnitElement compilationUnit) {
return quiver.concat([
compilationUnit.accessors,
compilationUnit.enums,
compilationUnit.extensions,
compilationUnit.functions,
compilationUnit.functionTypeAliases,
compilationUnit.mixins,
compilationUnit.topLevelVariables,
compilationUnit.types,
]);
}
@Deprecated(
'Public method intended to be private; will be removed as early as '
'Dartdoc 1.0.0')
static Iterable<Element> getDefinedElements(
CompilationUnitElement compilationUnit) =>
_getDefinedElements(compilationUnit);
List<String> __allOriginalModelElementNames;
/// Return true if this library is in a package configured to be treated as
/// as using Null safety and itself uses Null safety.
bool get _allowsNullSafety => element.isNonNullableByDefault;
/// Return true if this library should be documented as using Null safety.
/// A library may use Null safety but not documented that way.
@override
bool get isNullSafety =>
config.enableExperiment.contains('non-nullable') && _allowsNullSafety;
bool get isInSdk => element.isInSdk;
/// [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<String> get _allOriginalModelElementNames {
__allOriginalModelElementNames ??= allModelElements.map((e) {
if (e is GetterSetterCombo) {
Accessor getter;
Accessor setter;
if (e.hasGetter) {
getter = ModelElement.fromElement(e.getter.element, packageGraph);
}
if (e.hasSetter) {
setter = ModelElement.fromElement(e.setter.element, packageGraph);
}
return ModelElement.fromPropertyInducingElement(
e.element,
packageGraph.findButDoNotCreateLibraryFor(e.element),
packageGraph,
getter: getter,
setter: setter)
.fullyQualifiedName;
}
return ModelElement.from(
e.element,
packageGraph.findButDoNotCreateLibraryFor(e.element),
packageGraph)
.fullyQualifiedName;
}).toList();
return __allOriginalModelElementNames;
}
@Deprecated(
'Public getter intended to be private; will be removed as early as '
'Dartdoc 1.0.0')
Iterable<String> get allOriginalModelElementNames =>
_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.definingCompilationUnit;
@override
Iterable<Class> get classes => allClasses.where((c) => !c.isErrorOrException);
@override
LibraryElement get element => super.element;
/*late final*/ List<Extension> _extensions;
@override
Iterable<Extension> get extensions {
_extensions ??= _exportedAndLocalElements
.whereType<ExtensionElement>()
.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 || !isSdkLibraryDocumented(sdkLib))) {
return false;
}
if (config.isLibraryExcluded(name) ||
config.isLibraryExcluded(element.librarySource.uri.toString())) {
return false;
}
return true;
}
/*late final*/ List<TopLevelVariable> _constants;
@override
Iterable<TopLevelVariable> get constants {
_constants ??=
_getVariables().where((v) => v.isConst).toList(growable: false);
return _constants;
}
/*late final*/ Set<Library> _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<Library> get packageImportedExportedLibraries {
if (_packageImportedExportedLibraries == null) {
_packageImportedExportedLibraries = {};
packageGraph.publicLibraries
.where((l) => l.packageName == packageName)
.forEach((l) {
_packageImportedExportedLibraries.addAll(l.importedExportedLibraries);
});
}
return _packageImportedExportedLibraries;
}
/*late final*/ Set<Library> _importedExportedLibraries;
/// Returns all libraries either imported by or exported by this library,
/// recursively.
Set<Library> get importedExportedLibraries {
if (_importedExportedLibraries == null) {
_importedExportedLibraries = {};
var importedExportedLibraryElements = <LibraryElement>{};
importedExportedLibraryElements.addAll(element.importedLibraries);
importedExportedLibraryElements.addAll(element.exportedLibraries);
for (var l in importedExportedLibraryElements) {
Library lib = ModelElement.from(l, library, packageGraph);
_importedExportedLibraries.add(lib);
_importedExportedLibraries.addAll(lib.importedExportedLibraries);
}
}
return _importedExportedLibraries;
}
/*late final*/ Map<String, Set<Library>> _prefixToLibrary;
/// Map of import prefixes ('import "foo" as prefix;') to [Library].
Map<String, Set<Library>> get prefixToLibrary {
if (_prefixToLibrary == null) {
_prefixToLibrary = {};
// It is possible to have overlapping prefixes.
for (var i in element.imports) {
// Ignore invalid imports.
if (i.prefix?.name != null && i.importedLibrary != null) {
_prefixToLibrary
.putIfAbsent(i.prefix?.name, () => {})
.add(ModelElement.from(i.importedLibrary, library, packageGraph));
}
}
}
return _prefixToLibrary;
}
/*late final*/ String _dirName;
String get dirName {
if (_dirName == null) {
_dirName = name;
if (isAnonymous) {
_dirName = nameFromPath;
}
_dirName = _dirName.replaceAll(':', '-').replaceAll('/', '_');
}
return _dirName;
}
/*late final*/ Set<String> _canonicalFor;
Set<String> get canonicalFor {
if (_canonicalFor == null) {
// TODO(jcollins-g): restructure to avoid using side effects.
buildDocumentationAddition(documentationComment);
}
return _canonicalFor;
}
static final _canonicalRegExp = RegExp(r'{@canonicalFor\s([^}]+)}');
/// 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);
var newCanonicalFor = <String>{};
var notFoundInAllModelElements = <String>{};
rawDocs = rawDocs.replaceAllMapped(_canonicalRegExp, (Match match) {
newCanonicalFor.add(match.group(1));
notFoundInAllModelElements.add(match.group(1));
return '';
});
if (notFoundInAllModelElements.isNotEmpty) {
notFoundInAllModelElements.removeAll(_allOriginalModelElementNames);
}
for (var notFound in notFoundInAllModelElements) {
warn(PackageWarning.ignoredCanonicalFor, message: notFound);
}
// TODO(jcollins-g): warn if a macro/tool _does_ generate an unexpected
// canonicalFor?
_canonicalFor ??= newCanonicalFor;
return rawDocs;
}
/// Libraries are not enclosed by anything.
@override
ModelElement get enclosingElement => null;
/*late final*/ List<Enum> _enums;
@override
List<Enum> get enums {
_enums ??= _exportedAndLocalElements
.whereType<ClassElement>()
.where((element) => element.isEnum)
.map((e) => ModelElement.from(e, this, packageGraph) as Enum)
.toList(growable: false)
..sort(byName);
return _enums;
}
/*late final*/ List<Mixin> _mixins;
@override
List<Mixin> get mixins {
_mixins ??= _exportedAndLocalElements
.whereType<ClassElement>()
.where((ClassElement c) => c.isMixin)
.map((e) => ModelElement.from(e, this, packageGraph) as Mixin)
.toList(growable: false)
..sort(byName);
return _mixins;
}
/*late final*/ List<Class> _exceptions;
@override
List<Class> get exceptions {
_exceptions ??=
allClasses.where((c) => c.isErrorOrException).toList(growable: false);
return _exceptions;
}
@override
String get fileName => '$dirName-library.$fileType';
@override
String get filePath => '${library.dirName}/$fileName';
List<ModelFunction> _functions;
@override
List<ModelFunction> get functions {
_functions ??=
_exportedAndLocalElements.whereType<FunctionElement>().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}$filePath';
}
InheritanceManager3 _inheritanceManager;
// TODO(srawlins): Make a static field, likely on [Class].
InheritanceManager3 get inheritanceManager {
_inheritanceManager ??= InheritanceManager3();
return _inheritanceManager;
}
bool get isAnonymous => element.name == null || element.name.isEmpty;
@override
String get kind => 'library';
@override
Library get library => this;
/*late final*/ String _name;
@override
String get name {
_name ??= _getLibraryName(element);
return _name;
}
/*late final*/ 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 {
_nameFromPath ??= _getNameFromPath(element, package, _restoredUri);
return _nameFromPath;
}
/// 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.
PackageMeta _packageMeta;
PackageMeta get packageMeta {
_packageMeta ??= packageGraph.packageMetaProvider.fromElement(
element,
config.sdkDir,
);
return _packageMeta;
}
/*late final*/ List<TopLevelVariable> _properties;
/// All variables ("properties") except constants.
@override
Iterable<TopLevelVariable> get properties {
_properties ??=
_getVariables().where((v) => !v.isConst).toList(growable: false);
return _properties;
}
/*late final*/ List<Typedef> _typedefs;
@override
List<Typedef> get typedefs {
_typedefs ??= _exportedAndLocalElements
.whereType<FunctionTypeAliasElement>()
.map((e) => ModelElement.from(e, this, packageGraph) as Typedef)
.toList(growable: false)
..sort(byName);
return _typedefs;
}
TypeSystem get typeSystem => element.typeSystem;
List<Class> _classes;
List<Class> get allClasses {
_classes ??= _exportedAndLocalElements
.whereType<ClassElement>()
.where((e) => !e.isMixin && !e.isEnum)
.map((e) => ModelElement.from(e, this, packageGraph) as Class)
.toList(growable: false)
..sort(byName);
return _classes;
}
Class getClassByName(String name) {
return allClasses.firstWhere((it) => it.name == name, orElse: () => null);
}
/*late final*/ List<TopLevelVariable> _variables;
List<TopLevelVariable> _getVariables() {
if (_variables == null) {
var elements = _exportedAndLocalElements
.whereType<TopLevelVariableElement>()
.toSet();
elements.addAll(_exportedAndLocalElements
.whereType<PropertyAccessorElement>()
.map((a) => a.variable));
_variables = [];
for (var 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);
}
var me = ModelElement.fromPropertyInducingElement(
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, Package package, String restoredUri) {
var name = restoredUri;
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:'), '"$name" must not start with "file:"');
var 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;
}
@Deprecated(
'Public method intended to be private; will be removed as early as '
'Dartdoc 1.0.0')
static String getLibraryName(LibraryElement element) =>
_getLibraryName(element);
/*late final*/ HashMap<String, Set<ModelElement>> _modelElementsNameMap;
/// Map of [fullyQualifiedNameWithoutLibrary] to all matching [ModelElement]s
/// in this library. Used for code reference lookups.
HashMap<String, Set<ModelElement>> get modelElementsNameMap {
if (_modelElementsNameMap == null) {
_modelElementsNameMap = HashMap<String, Set<ModelElement>>();
allModelElements.forEach((ModelElement modelElement) {
// [definingLibrary] may be null if [element] has been imported or
// exported with a non-normalized URI, like "src//a.dart".
if (modelElement.definingLibrary == null) return;
_modelElementsNameMap
.putIfAbsent(
modelElement.fullyQualifiedNameWithoutLibrary, () => {})
.add(modelElement);
});
}
return _modelElementsNameMap;
}
/*late final*/ HashMap<Element, Set<ModelElement>> _modelElementsMap;
HashMap<Element, Set<ModelElement>> get modelElementsMap {
if (_modelElementsMap == null) {
var results = quiver.concat(<Iterable<ModelElement>>[
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 = HashMap<Element, Set<ModelElement>>();
results.forEach((modelElement) {
_modelElementsMap
.putIfAbsent(modelElement.element, () => {})
.add(modelElement);
});
_modelElementsMap.putIfAbsent(element, () => {}).add(this);
}
return _modelElementsMap;
}
/*late final*/ List<ModelElement> _allModelElements;
Iterable<ModelElement> get allModelElements {
return _allModelElements ??= [
for (var modelElements in modelElementsMap.values) ...modelElements,
];
}
/*late final*/ List<ModelElement> _allCanonicalModelElements;
Iterable<ModelElement> get allCanonicalModelElements {
return (_allCanonicalModelElements ??=
allModelElements.where((e) => e.isCanonical).toList());
}
}