blob: 7b511ca685d745129602330f9a75af2703e51c64 [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/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/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<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 {
List<TopLevelVariable> _variables;
List<Element> _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<String, CompilationUnit> _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<Element> exportedAndLocalElements =
element.exportNamespace.definedNames.values.toSet();
// TODO(jcollins-g): Consider switch to [_libraryElement.topLevelElements].
exportedAndLocalElements
.addAll(getDefinedElements(element.definingCompilationUnit));
for (CompilationUnitElement cu in element.parts) {
exportedAndLocalElements.addAll(getDefinedElements(cu));
}
_exportedAndLocalElements = exportedAndLocalElements.toList();
_package.allLibraries.add(this);
}
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,
]);
}
List<String> _allOriginalModelElementNames;
bool get isInSdk => element.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<String> 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.definingCompilationUnit;
@override
Iterable<Class> get classes => allClasses.where((c) => !c.isErrorOrException);
@override
LibraryElement get element => super.element;
List<Extension> _extensions;
@override
Iterable<Extension> get extensions {
if (_extensions == null) {
_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 || !sdkLib.isDocumented)) {
return false;
}
if (config.isLibraryExcluded(name) ||
config.isLibraryExcluded(element.librarySource.uri.toString())) {
return false;
}
return true;
}
List<TopLevelVariable> _constants;
@override
Iterable<TopLevelVariable> get constants {
if (_constants == null) {
// _getVariables() is already sorted.
_constants =
_getVariables().where((v) => v.isConst).toList(growable: false);
}
return _constants;
}
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 = Set();
packageGraph.publicLibraries
.where((l) => l.packageName == packageName)
.forEach((l) {
_packageImportedExportedLibraries.addAll(l.importedExportedLibraries);
});
}
return _packageImportedExportedLibraries;
}
Set<Library> _importedExportedLibraries;
/// Returns all libraries either imported by or exported by this library,
/// recursively.
Set<Library> get importedExportedLibraries {
if (_importedExportedLibraries == null) {
_importedExportedLibraries = Set();
Set<LibraryElement> importedExportedLibraryElements = Set();
importedExportedLibraryElements.addAll(element.importedLibraries);
importedExportedLibraryElements.addAll(element.exportedLibraries);
for (LibraryElement l in importedExportedLibraryElements) {
Library lib = ModelElement.from(l, library, packageGraph);
_importedExportedLibraries.add(lib);
_importedExportedLibraries.addAll(lib.importedExportedLibraries);
}
}
return _importedExportedLibraries;
}
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 (ImportElement i in element.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<String> _canonicalFor;
Set<String> 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<String> newCanonicalFor = Set();
Set<String> 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<Enum> _enums;
@override
List<Enum> get enums {
if (_enums == null) {
_enums = _exportedAndLocalElements
.whereType<ClassElement>()
.where((element) => element.isEnum)
.map((e) => ModelElement.from(e, this, packageGraph) as Enum)
.toList(growable: false)
..sort(byName);
}
return _enums;
}
List<Mixin> _mixins;
@override
List<Mixin> get mixins {
if (_mixins == null) {
/// Can not be [MixinElementImpl] because [ClassHandle]s are sometimes
/// returned from _exportedElements.
_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;
}
List<Class> _exceptions;
@override
List<Class> get exceptions {
if (_exceptions == null) {
_exceptions =
allClasses.where((c) => c.isErrorOrException).toList(growable: false);
}
return _exceptions;
}
@override
String get fileName => '$dirName-library.html';
List<ModelFunction> _functions;
@override
List<ModelFunction> get functions {
if (_functions == null) {
_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}${library.dirName}/$fileName';
}
InheritanceManager3 _inheritanceManager;
InheritanceManager3 get inheritanceManager {
if (_inheritanceManager == null) {
_inheritanceManager = InheritanceManager3();
}
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<TopLevelVariable> _properties;
/// All variables ("properties") except constants.
@override
Iterable<TopLevelVariable> get properties {
if (_properties == null) {
_properties =
_getVariables().where((v) => !v.isConst).toList(growable: false);
}
return _properties;
}
List<Typedef> _typedefs;
@override
List<Typedef> get typedefs {
if (_typedefs == null) {
_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 {
if (_classes == null) {
_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);
}
List<TopLevelVariable> _getVariables() {
if (_variables == null) {
Set<TopLevelVariableElement> elements = _exportedAndLocalElements
.whereType<TopLevelVariableElement>()
.toSet();
elements.addAll(_exportedAndLocalElements
.whereType<PropertyAccessorElement>()
.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<String, Set<ModelElement>> _modelElementsNameMap;
/// Map of [fullyQualifiedNameWithoutLibrary] to all matching [ModelElement]s
/// in this library. Used for code reference lookups.
Map<String, Set<ModelElement>> get modelElementsNameMap {
if (_modelElementsNameMap == null) {
_modelElementsNameMap = Map<String, Set<ModelElement>>();
allModelElements.forEach((ModelElement modelElement) {
_modelElementsNameMap.putIfAbsent(
modelElement.fullyQualifiedNameWithoutLibrary, () => Set());
_modelElementsNameMap[modelElement.fullyQualifiedNameWithoutLibrary]
.add(modelElement);
});
}
return _modelElementsNameMap;
}
Map<Element, Set<ModelElement>> _modelElementsMap;
Map<Element, Set<ModelElement>> get modelElementsMap {
if (_modelElementsMap == null) {
Iterable<ModelElement> 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<Element, Set<ModelElement>>();
results.forEach((modelElement) {
_modelElementsMap.putIfAbsent(modelElement.element, () => Set());
_modelElementsMap[modelElement.element].add(modelElement);
});
_modelElementsMap.putIfAbsent(element, () => Set());
_modelElementsMap[element].add(this);
}
return _modelElementsMap;
}
List<ModelElement> _allModelElements;
Iterable<ModelElement> get allModelElements {
if (_allModelElements == null) {
_allModelElements = [];
for (Set<ModelElement> modelElements in modelElementsMap.values) {
_allModelElements.addAll(modelElements);
}
}
return _allModelElements;
}
List<ModelElement> _allCanonicalModelElements;
Iterable<ModelElement> get allCanonicalModelElements {
return (_allCanonicalModelElements ??=
allModelElements.where((e) => e.isCanonical).toList());
}
}