blob: 437f68302070a0cc068f26f05d647c70ae138f0b [file] [log] [blame]
// Copyright (c) 2020, 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/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.dart';
import 'package:dart_style/dart_style.dart';
import 'package:dartdoc/src/mustachio/annotations.dart';
import 'package:path/path.dart' as p;
import 'utilities.dart';
/// Builds [specs] into a Dart library containing runtime renderers.
String buildRuntimeRenderers(Set<RendererSpec> specs, Uri sourceUri,
TypeProvider typeProvider, TypeSystem typeSystem,
{bool rendererClassesArePublic = false}) {
var visibleElements = specs
.map((spec) => spec.visibleTypes)
.reduce((value, element) => value.union(element))
.map((type) => type.element!)
.toSet();
var raw = RuntimeRenderersBuilder(
sourceUri, typeProvider, typeSystem, visibleElements,
rendererClassesArePublic: rendererClassesArePublic)
._buildTemplateRenderers(specs);
return DartFormatter().format(raw);
}
/// This class builds runtime Mustache renderers from a set of [RendererSpec]s.
class RuntimeRenderersBuilder {
static const _contextTypeVariable = 'CT_';
final _buffer = StringBuffer();
/// A queue of types to process, in order to find all types for which we need
/// to build renderers.
final _typesToProcess = Queue<_RendererInfo>();
/// Maps a type to the name of the render function which can render that type
/// as a context type.
final _typeToRenderFunctionName = <ClassElement, String>{};
/// Maps a type to the name of the renderer class which can render that type
/// as a context type.
final _typeToRendererClassName = <ClassElement, String>{};
final Uri _sourceUri;
final TypeProvider _typeProvider;
final TypeSystem _typeSystem;
final Set<Element> _allVisibleElements;
/// Whether renderer classes are public. This should only be true for testing.
final bool _rendererClassesArePublic;
/// A mapping of getter names on classes which are not "visible."
final Map<String, Set<String>> _invisibleGetters = {};
RuntimeRenderersBuilder(this._sourceUri, this._typeProvider, this._typeSystem,
this._allVisibleElements,
{bool rendererClassesArePublic = false})
: _rendererClassesArePublic = rendererClassesArePublic;
String _buildTemplateRenderers(Set<RendererSpec> specs) {
// TODO(srawlins): There are some private renderer functions that are
// unused. Figure out if we can detect these statically, and then not
// generate them.
// TODO(srawlins): To really get the correct list of imports, we need to use
// the code_builder package.
_buffer.writeln('''
// GENERATED CODE. DO NOT EDIT.
//
// To change the contents of this library, make changes to the builder source
// files in the tool/mustachio/ directory.
// ignore_for_file: camel_case_types, deprecated_member_use_from_same_package
// ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes
// ignore_for_file: unused_import
import 'package:dartdoc/src/element_type.dart';
import 'package:dartdoc/src/generator/template_data.dart';
import 'package:dartdoc/src/model/annotation.dart';
import 'package:dartdoc/src/model/comment_referable.dart';
import 'package:dartdoc/src/model/extension_target.dart';
import 'package:dartdoc/src/model/feature.dart';
import 'package:dartdoc/src/model/feature_set.dart';
import 'package:dartdoc/src/model/language_feature.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/model/model_object_builder.dart';
import 'package:dartdoc/src/mustachio/parser.dart';
import 'package:dartdoc/src/mustachio/renderer_base.dart';
import 'package:dartdoc/src/warnings.dart';
import '${p.basename(_sourceUri.path)}';
''');
specs.forEach(_addTypesForRendererSpec);
var builtRenderers = <ClassElement>{};
var elementsToProcess = _typesToProcess.toList()
..sort((a, b) => a._typeName.compareTo(b._typeName));
for (var info in elementsToProcess) {
if (info.isFullRenderer) {
var buildOnlyPublicFunction =
builtRenderers.contains(info._contextClass);
_buildRenderer(info, buildOnlyPublicFunction: buildOnlyPublicFunction);
builtRenderers.add(info._contextClass);
}
}
_writeInvisibleGetters();
return _buffer.toString();
}
/// Adds type specified in [spec] to the [_typesToProcess] queue, as well as
/// all supertypes, and the types of all valid getters, recursively.
void _addTypesForRendererSpec(RendererSpec spec) {
var element = spec.contextElement;
var rendererInfo = _RendererInfo(element,
public: _rendererClassesArePublic, publicApiFunctionName: spec.name);
_typesToProcess.add(rendererInfo);
_typeToRenderFunctionName[element] = rendererInfo._renderFunctionName;
_typeToRendererClassName[element] = rendererInfo._rendererClassName;
spec.contextType.accessors.forEach(_addPropertyToProcess);
for (var mixin in spec.contextElement.mixins) {
_addTypeToProcess(mixin.element,
isFullRenderer: true, includeRenderFunction: false);
}
var superclass = spec.contextElement.supertype;
while (superclass != null) {
// Any type specified with a renderer spec (`@Renderer`) is full.
_addTypeToProcess(superclass.element,
isFullRenderer: true, includeRenderFunction: false);
for (var mixin in superclass.element.mixins) {
_addTypeToProcess(mixin.element,
isFullRenderer: true, includeRenderFunction: false);
}
superclass.accessors.forEach(_addPropertyToProcess);
superclass = superclass.element.supertype;
}
}
/// Adds the return type of [property] to the [_typesToProcess] queue, if it
/// is a "valid" property.
///
/// A "valid" property is a public, instance getter with an interface type
/// return type. Getters annotated with @internal, @protected, or
/// @visibleForTesting are not valid.
void _addPropertyToProcess(PropertyAccessorElement property) {
if (property.isPrivate || property.isStatic || property.isSetter) return;
if (property.hasProtected || property.hasVisibleForTesting) return;
var type = _relevantTypeFrom(property.type.returnType);
if (type == null) return;
var types = _typesToProcess.where((rs) => rs._contextClass == type.element);
if (types.isNotEmpty) {
assert(types.length == 1);
if (types.first.includeRenderFunction) {
// [type] has already been added to [_typesToProcess], and all of its
// supertypes and properties have been visited.
return;
}
}
_addTypeHierarchyToProcess(
type,
isFullRenderer: _isVisibleToMustache(type.element),
// If [type.element] is not visible to mustache, then [renderSimple] will
// be used, not [type.element]'s render function.
includeRenderFunction: _isVisibleToMustache(type.element),
);
}
/// Returns an [InterfaceType] which may be relevant for generating a
/// renderer, given a [type]:
///
/// * If [type] is assignable to [Iterable<T>], returns the relevant type from
/// `T`.
/// * If [type] is a [TypeParameterType] with a bound other than `dynamic`,
/// returns the relevant type from the bound.
/// * If [type] is an [InterfaceType] (not assignable to [Iterable]), returns
/// [type].
/// * Otherwise, returns `null`, indicating there is no relevant type.
InterfaceType? _relevantTypeFrom(DartType type) {
if (type is InterfaceType) {
if (_typeSystem.isAssignableTo(type, _typeProvider.iterableDynamicType)) {
var iterableElement = _typeProvider.iterableElement;
var iterableType = type.asInstanceOf(iterableElement)!;
var innerType = iterableType.typeArguments.first;
return _relevantTypeFrom(innerType);
} else {
return type;
}
} else if (type is TypeParameterType) {
var bound = type.bound;
if (bound.isDynamic) {
// Don't add functions for a generic type, for example
// `List<E>.first` has type `E`, which we don't have a specific
// renderer for.
// TODO(srawlins): Find a solution for this. We can track all of the
// concrete types substituted for `E` for example.
return null;
} else {
return _relevantTypeFrom(bound);
}
} else {
// We can do nothing with function types, etc.
return null;
}
}
/// Adds [type] to the queue of types to process, as well as related types:
///
/// * its supertypes (if [type] is not a mixin),
/// * mixed in types,
/// * superclass constraints (if [type] a mixin),
/// * types of relevant properties (recursively).
void _addTypeHierarchyToProcess(
InterfaceType? type, {
required bool isFullRenderer,
required bool includeRenderFunction,
}) {
while (type != null) {
_addTypeToProcess(
type.element,
isFullRenderer: isFullRenderer,
includeRenderFunction: includeRenderFunction,
);
if (isFullRenderer) {
for (var accessor in type.accessors) {
var accessorType = _relevantTypeFrom(accessor.type.returnType);
if (accessorType == null) continue;
_addPropertyToProcess(accessor);
}
}
for (var mixin in type.element.mixins) {
_addTypeHierarchyToProcess(
mixin,
isFullRenderer: isFullRenderer,
includeRenderFunction: false,
);
}
if (type.element.isMixin) {
for (var constraint in type.element.superclassConstraints) {
_addTypeToProcess(
constraint.element,
isFullRenderer: isFullRenderer,
includeRenderFunction: false,
);
}
break;
} else {
type = type.superclass;
// Render functions are not needed for superclasses.
includeRenderFunction = false;
}
}
}
/// Adds [type] to the [_typesToProcess] queue, if it is not already there.
void _addTypeToProcess(
ClassElement element, {
required bool isFullRenderer,
required bool includeRenderFunction,
}) {
var types = _typesToProcess.where((rs) => rs._contextClass == element);
if (types.isEmpty) {
var rendererInfo = _RendererInfo(
element,
isFullRenderer: isFullRenderer,
includeRenderFunction: includeRenderFunction,
public: _rendererClassesArePublic,
);
_typesToProcess.add(rendererInfo);
if (isFullRenderer) {
if (includeRenderFunction) {
_typeToRenderFunctionName[element] = rendererInfo._renderFunctionName;
}
_typeToRendererClassName[element] = rendererInfo._rendererClassName;
}
} else {
for (var typeToProcess in types) {
// "Upgrade" the renderer info to include a render function if the
// current one doesn't.
if (includeRenderFunction && !typeToProcess.includeRenderFunction) {
typeToProcess.includeRenderFunction = true;
}
// "Upgrade" the renderer info to "full" if the current one isn't.
if (isFullRenderer && !typeToProcess.isFullRenderer) {
typeToProcess.isFullRenderer = true;
}
// Log the names if we've perhaps just "upgraded" the renderer info.
if (isFullRenderer) {
if (typeToProcess.includeRenderFunction) {
_typeToRenderFunctionName[element] =
typeToProcess._renderFunctionName;
}
_typeToRendererClassName[element] = typeToProcess._rendererClassName;
}
}
}
}
/// Returns whether [element] or any of its supertypes are "visible" to Mustache.
bool _isVisibleToMustache(ClassElement element) {
if (_allVisibleElements.contains(element)) {
return true;
}
var supertype = element.supertype;
if (supertype == null) {
return false;
}
return _isVisibleToMustache(supertype.element);
}
/// Builds render functions and the renderer class for [renderer].
///
/// The function and the class are each written as Dart code to [_buffer].
///
/// If [renderer] also specifies a `publicApiFunctionName`, then a public API
/// function (which renders a context object using a template file at a path,
/// rather than an AST) is also written.
///
/// If [buildOnlyPublicFunction] is true, then the private render function and
/// renderer classes are not built, having been built for a different
/// [_RendererInfo].
void _buildRenderer(_RendererInfo renderer,
{required bool buildOnlyPublicFunction}) {
var typeName = renderer._typeName;
var typeWithVariables = '$typeName${renderer._typeVariablesString}';
if (renderer.publicApiFunctionName != null) {
_buffer.writeln('''
String ${renderer.publicApiFunctionName}${renderer._typeParametersString}(
$typeWithVariables context, Template template) {
var buffer = StringBuffer();
${renderer._renderFunctionName}(context, template.ast, template, buffer);
return buffer.toString();
}
''');
}
if (buildOnlyPublicFunction) return;
// Write out the render function.
if (renderer.includeRenderFunction) {
_buffer.writeln('''
void ${renderer._renderFunctionName}${renderer._typeParametersString}(
$typeWithVariables context, List<MustachioNode> ast,
Template template, StringSink sink,
{RendererBase<Object>? parent}) {
var renderer = ${renderer._rendererClassName}(context, parent, template, sink);
renderer.renderBlock(ast);
}
''');
}
// Write out the renderer class.
_buffer.write('''
class ${renderer._rendererClassName}${renderer._typeParametersString}
extends RendererBase<$typeWithVariables> {
''');
_writePropertyMap(renderer);
// Write out the constructor.
_buffer.writeln('''
${renderer._rendererClassName}(
$typeWithVariables context, RendererBase<Object>? parent,
Template template, StringSink sink)
: super(context, parent, template, sink);
''');
var propertyMapTypeArguments =
renderer._typeArgumentsStringWith(typeWithVariables);
var propertyMapName = 'propertyMap$propertyMapTypeArguments';
// Write out `getProperty`.
_buffer.writeln('''
@override
Property<$typeWithVariables>? getProperty(String key) {
if ($propertyMapName().containsKey(key)) {
return $propertyMapName()[key];
} else {
return null;
}
}
''');
// Close the class.
_buffer.writeln('}');
}
/// Write out the property map for [renderer].
///
/// For each valid property of the context type of [renderer], this maps the
/// property's name to the property's [Property] object.
void _writePropertyMap(_RendererInfo renderer) {
var contextClass = renderer._contextClass;
var generics = renderer._typeParametersStringWith(
'$_contextTypeVariable extends ${renderer._typeName}');
// It would be simplest if [propertyMap] were just a getter, but it must be
// parameterized on `CT_`, so it is a static method. Due to the possibly
// extensive amount of spreading (supertypes, mixins) and object
// construction (lots of [Property] objects with function literals), we
// cache the construction of each one, keyed to the `CT_` value. Each cache
// should not have many entries, as there are probably not many values for
// each type variable, `CT_`, typically one.
_buffer.writeln('''
static final Map<Type, Object> _propertyMapCache = {};
static Map<String, Property<$_contextTypeVariable>> propertyMap$generics() =>
_propertyMapCache.putIfAbsent($_contextTypeVariable, () => {''');
var supertype = contextClass.supertype;
if (supertype != null) {
var superclassRendererName = _typeToRendererClassName[supertype.element];
if (superclassRendererName != null) {
var superMapName = '$superclassRendererName.propertyMap';
var generics = asGenerics([
...supertype.typeArguments
.map((e) => e.getDisplayString(withNullability: false)),
_contextTypeVariable
]);
_buffer.writeln(' ...$superMapName$generics(),');
}
}
// Mixins are spread into the property map _after_ the super class, so
// that they override any values which need to be overridden. Superclass
// and mixins override from left to right, as do spreads:
// `class C extends E with M, N` first takes members from N, then M, then
// E. Similarly, `{...a, ...b, ...c}` will feature elements from `c` which
// override `b` and `a`.
for (var mixin in contextClass.mixins) {
var mixinRendererName = _typeToRendererClassName[mixin.element];
if (mixinRendererName != null) {
var mixinMapName = '$mixinRendererName.propertyMap';
var generics = asGenerics([
...mixin.typeArguments
.map((e) => e.getDisplayString(withNullability: false)),
_contextTypeVariable
]);
_buffer.writeln(' ...$mixinMapName$generics(),');
}
}
for (var property in [...contextClass.accessors]
..sort((a, b) => a.name.compareTo(b.name))) {
var returnType = property.type.returnType;
if (returnType is InterfaceType) {
_writeProperty(renderer, property, returnType);
} else if (returnType is TypeParameterType &&
!returnType.bound.isDynamic) {
_writeProperty(renderer, property, returnType.bound as InterfaceType);
}
}
_buffer.writeln('}) as Map<String, Property<$_contextTypeVariable>>;');
_buffer.writeln('');
}
void _writeProperty(_RendererInfo renderer, PropertyAccessorElement property,
InterfaceType getterType) {
if (getterType == _typeProvider.typeType) {
// The [Type] type is the first case of a type we don't want to traverse.
return;
}
if (property.isPrivate || property.isStatic || property.isSetter) return;
if (property.hasProtected || property.hasVisibleForTesting) return;
_buffer.writeln("'${property.name}': Property(");
_buffer
.writeln('getValue: ($_contextTypeVariable c) => c.${property.name},');
var getterName = property.name;
var getterTypeString = getterType.getDisplayString(withNullability: false);
// Only add a `getProperties` function, which returns the property map for
// [getterType], if [getterType] is a renderable type.
if (_typeToRendererClassName.containsKey(getterType.element)) {
var rendererClassName = _typeToRendererClassName[getterType.element];
_buffer.writeln('''
renderVariable:
($_contextTypeVariable c, Property<$_contextTypeVariable> self, List<String> remainingNames) {
if (remainingNames.isEmpty) {
return self.getValue(c).toString();
}
var name = remainingNames.first;
var nextProperty = $rendererClassName.propertyMap().getValue(name);
return nextProperty.renderVariable(
self.getValue(c) as $getterTypeString, nextProperty, [...remainingNames.skip(1)]);
},
''');
} else {
// [getterType] does not have a full renderer, so we just render a simple
// variable, with no opportunity to access fields on [getterType].
_buffer.writeln('''
renderVariable:
($_contextTypeVariable c, Property<CT_> self, List<String> remainingNames) =>
self.renderSimpleVariable(c, remainingNames, '$getterTypeString'),
''');
}
if (getterType.isDartCoreBool) {
_buffer.writeln(
'getBool: ($_contextTypeVariable c) => c.$getterName == true,');
} else if (_typeSystem.isAssignableTo(
getterType, _typeProvider.iterableDynamicType)) {
var iterableElement = _typeProvider.iterableElement;
var iterableType = getterType.asInstanceOf(iterableElement);
// Not sure why [iterableType] would be null... unresolved type?
if (iterableType != null) {
var innerType = iterableType.typeArguments.first;
// Don't add Iterable functions for a generic type, for example
// `List<E>.reversed` has inner type `E`, which we don't have a specific
// renderer for.
// TODO(srawlins): Find a solution for this. We can track all of the
// concrete types substituted for `E` for example.
if (innerType is! TypeParameterType) {
var renderFunctionName = _typeToRenderFunctionName[innerType.element];
String renderCall;
if (renderFunctionName == null) {
var typeName = innerType.element!.name!;
if (innerType is InterfaceType) {
_invisibleGetters.putIfAbsent(
typeName, () => innerType.element.allAccessorNames);
}
renderCall = 'renderSimple(e, ast, r.template, sink, parent: r, '
"getters: _invisibleGetters['$typeName']!)";
} else {
var bang = _typeSystem.isPotentiallyNullable(innerType) ? '!' : '';
renderCall =
'$renderFunctionName(e$bang, ast, r.template, sink, parent: r)';
}
_buffer.writeln('''
renderIterable:
($_contextTypeVariable c, RendererBase<$_contextTypeVariable> r,
List<MustachioNode> ast, StringSink sink) {
return c.$getterName.map((e) => $renderCall);
},
''');
}
}
} else {
// Don't add Iterable functions for a generic type, for example
// `List<E>.first` has type `E`, which we don't have a specific
// renderer for.
// TODO(srawlins): Find a solution for this. We can track all of the
// concrete types substituted for `E` for example.
if (getterName is! TypeParameterType) {
var renderFunctionName = _typeToRenderFunctionName[getterType.element];
String renderCall;
if (renderFunctionName == null) {
var typeName = getterType.element.name;
_invisibleGetters.putIfAbsent(
typeName, () => getterType.element.allAccessorNames);
renderCall =
'renderSimple(c.$getterName, ast, r.template, sink, parent: r, '
"getters: _invisibleGetters['$typeName']!)";
} else {
var bang = _typeSystem.isPotentiallyNullable(getterType) ? '!' : '';
renderCall =
'$renderFunctionName(c.$getterName$bang, ast, r.template, sink, parent: r)';
}
var nullValueGetter =
getterType.nullabilitySuffix == NullabilitySuffix.none
? 'false'
: 'c.$getterName == null';
_buffer.writeln('''
isNullValue: ($_contextTypeVariable c) => $nullValueGetter,
renderValue:
($_contextTypeVariable c, RendererBase<$_contextTypeVariable> r,
List<MustachioNode> ast, StringSink sink) {
$renderCall;
},
''');
}
}
_buffer.writeln('),');
}
/// Writes the mapping of invisible getters, used to report simple renderer
/// errors.
void _writeInvisibleGetters() {
_buffer.write('const _invisibleGetters = {');
for (var class_ in _invisibleGetters.keys.toList()..sort()) {
_buffer.write("'$class_':");
var getters = _invisibleGetters[class_]!.toList()..sort();
_buffer.write('{${getters.map((e) => "'$e'").join(', ')}},');
}
_buffer.write('};');
}
}
/// A container with the information needed to distinguish one
/// renderer-to-be-built from another.
///
/// This can be used when building a set of renderers to build (both the render
/// functions and the renderer class), and also to refer from one renderer to
/// another.
class _RendererInfo {
final ClassElement _contextClass;
/// The name of the top level render function.
///
/// This function is public when specified in a @Renderer annotation, and
/// private otherwise.
final String _renderFunctionName;
/// Whether the renderer should be a full renderer.
///
/// If a render spec is not specified with @Renderer, then a class needs to be
/// annotated with @visibleToRender in order to get a full renderer.
/// Otherwise, the [SimpleRenderer] will be used.
///
/// It may be initially determined that we only need an abbreviated, then
/// later determined that we need a full renderer, so this field is not final.
bool isFullRenderer;
bool includeRenderFunction;
/// The public API function name specified with @Renderer, or null.
String? publicApiFunctionName;
factory _RendererInfo(
ClassElement contextClass, {
bool public = false,
bool isFullRenderer = true,
bool includeRenderFunction = true,
String? publicApiFunctionName,
}) {
var typeBaseName = contextClass.name;
var renderFunctionName = '_render_$typeBaseName';
var rendererClassName =
public ? 'Renderer_$typeBaseName' : '_Renderer_$typeBaseName';
return _RendererInfo._(
contextClass,
renderFunctionName,
rendererClassName,
isFullRenderer: isFullRenderer,
includeRenderFunction: includeRenderFunction,
publicApiFunctionName: publicApiFunctionName,
);
}
_RendererInfo._(
this._contextClass,
this._renderFunctionName,
this._rendererClassName, {
required this.isFullRenderer,
required this.includeRenderFunction,
this.publicApiFunctionName,
});
String get _typeName => _contextClass.name;
final String _rendererClassName;
String get _typeParametersString => _contextClass.typeParametersString;
String get _typeVariablesString => _contextClass.typeVariablesString;
String _typeParametersStringWith(String extra) =>
_contextClass.typeParametersStringWith(extra);
String _renderSingleType(DartType tp) {
var displayString = tp.getDisplayString(withNullability: false);
var nullabilitySuffix =
tp.nullabilitySuffix == NullabilitySuffix.question ? '?' : '';
return '$displayString$nullabilitySuffix';
}
/// Returns the type arguments of the context type, and [extra], as they
/// appear in a list of generics.
String _typeArgumentsStringWith(String extra) {
return asGenerics([
..._contextClass.thisType.typeArguments
// withNullability will give star types, we actually want nullable or
// non-nullable only for generation.
.map(_renderSingleType),
extra,
]);
}
}
extension on ClassElement {
/// Returns a set of the names of all accessors on this [ClassElement], including supertypes.
Set<String> get allAccessorNames {
return {
...?supertype?.element.allAccessorNames,
...accessors
.where((e) => e.isPublic && !e.isStatic && !e.isSetter)
.map((e) => e.name),
};
}
}