| // Copyright (c) 2021, 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:io'; |
| |
| import 'package:analyzer/dart/element/element.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:collection/collection.dart'; |
| import 'package:dart_style/dart_style.dart'; |
| import 'package:dartdoc/src/mustachio/annotations.dart'; |
| import 'package:dartdoc/src/mustachio/parser.dart'; |
| import 'package:dartdoc/src/mustachio/renderer_base.dart'; |
| import 'package:dartdoc/src/type_utils.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:path/path.dart' as path; |
| |
| /// A list of [_VariableLookup]s used by a renderer constitutes a "context |
| /// stack". |
| typedef ContextStack = List<_VariableLookup>; |
| |
| /// Compiles all templates specified in [specs] into a Dart library containing |
| /// a renderer for each template. |
| Future<String> compileTemplatesToRenderers( |
| Set<RendererSpec> specs, |
| TypeProvider typeProvider, |
| TypeSystem typeSystem, { |
| required String root, |
| required String sourcePath, |
| }) async { |
| var buildData = _BuildData(typeProvider, typeSystem, sourcePath, root); |
| var rendererFunctions = <String>[]; |
| var partialRendererFunctions = <_AotCompiler, String>{}; |
| var referenceUris = <String>{}; |
| print('Compiling ${specs.length} renderer specs into renderer functions...'); |
| for (var spec in specs) { |
| var templatePath = spec.standardHtmlTemplate; |
| var compiler = await _AotCompiler._readAndParse( |
| spec.contextType, |
| spec.name, |
| templatePath, |
| buildData, |
| ); |
| rendererFunctions.add(await compiler._compileToRenderer(referenceUris)); |
| partialRendererFunctions.addAll(compiler._compiledPartials); |
| } |
| partialRendererFunctions = await _deduplicateRenderers( |
| partialRendererFunctions, typeSystem, referenceUris); |
| var buffer = StringBuffer(); |
| for (var uri in referenceUris.sorted()) { |
| buffer.writeln("import '$uri';"); |
| } |
| |
| for (var function in [ |
| ...rendererFunctions, |
| ...partialRendererFunctions.values, |
| ]) { |
| buffer.write(function); |
| buffer.writeln(); |
| buffer.writeln(); |
| } |
| return DartFormatter(languageVersion: DartFormatter.latestLanguageVersion) |
| .format(''' |
| // GENERATED CODE. DO NOT EDIT. |
| // |
| // To change the contents of this library, make changes to the builder source |
| // files in the tool/mustachio/ directory. |
| |
| // Some deduplicated render functions are generated but not used. |
| // TODO(srawlins): Detect these and do not write them. |
| // ignore_for_file: unused_element |
| // Sometimes we enter a new section which triggers creating a new variable, but |
| // the variable is not used; generally when the section is checking if a |
| // non-bool, non-Iterable field is non-null. |
| // ignore_for_file: unused_local_variable |
| // ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes |
| |
| import 'dart:convert'; |
| |
| $buffer |
| |
| extension on StringBuffer { |
| void writeEscaped(String? value) { |
| write(htmlEscape.convert(value ?? '')); |
| } |
| } |
| '''); |
| } |
| |
| /// Deduplicates multiple renderers which are each used to render a partial |
| /// into a single renderer. |
| /// |
| /// When a partial is referenced by more than one template, multiple compilers |
| /// are used to build multiple render functions, because the context stack of |
| /// types may be different in each case. But it is perfectly logical to expect |
| /// that these can be deduplicated, as they should be able to use one common |
| /// context stack of interfaces. |
| /// |
| /// Attempts to deduplicate the compilers (which build the renderers) by |
| /// replacing context types in each stack with their collective LUB. |
| Future<Map<_AotCompiler, String>> _deduplicateRenderers( |
| Map<_AotCompiler, String> partialRendererFunctions, |
| TypeSystem typeSystem, |
| Set<String> referenceUris, |
| ) async { |
| if (partialRendererFunctions.length < 2) return partialRendererFunctions; |
| print('Deduplicating the initial set of ${partialRendererFunctions.length} ' |
| 'partial renderer functions...'); |
| // Map each template (represented by its path) to the list of compilers which |
| // compile it to a renderer function. |
| var compilersPerPartial = <String, List<_AotCompiler>>{}; |
| for (var compiler in partialRendererFunctions.keys) { |
| compilersPerPartial |
| .putIfAbsent(compiler._templatePath, () => []) |
| .add(compiler); |
| } |
| var partialsToRemove = <_AotCompiler>{}; |
| for (var (MapEntry(key: filePath, value: compilers)) |
| in compilersPerPartial.entries) { |
| if (compilers.length < 2) { |
| // Nothing to deduplicate. |
| continue; |
| } |
| var firstCompiler = compilers.first; |
| var contextStacks = compilers.map((c) => c._usedContextStack).toList(); |
| var contextStackTypes = typeSystem.contextStackLub(contextStacks); |
| if (contextStackTypes == null) { |
| // The stack lengths are different; it is impossible to fully deduplicate |
| // such partial renderers. |
| // TODO(srawlins): It may be possible to deduplicate a _subset_ of the |
| // list of partial renderers, or even multiple subsets. The generated |
| // renderers in `templates.aot_renderers_for_html.dart` can be examined to |
| // determine if this would be a meaningful optimization. |
| continue; |
| } |
| |
| // The set of render functions for this partial can be replaced by a single, |
| // possibly more generic renderer which accepts the LUB types. The body of |
| // each replaced renderer can perform a simple redirect to the more generic |
| // renderer. |
| var rendererName = filePath.replaceAll('.', '_').replaceAll('/', '_'); |
| |
| // The names of the renderers which are being replaced all include some |
| // reference to the template/partial which referred to them; we must create |
| // a new renderer name from scratch. |
| var lubCompiler = _AotCompiler._( |
| contextStackTypes.first, |
| '_deduplicated_$rendererName', |
| filePath, |
| firstCompiler._syntaxTree, |
| firstCompiler._buildData, |
| contextStack: [ |
| ...contextStackTypes.map((t) => _VariableLookup(t, 'UNUSED')) |
| ], |
| ); |
| String compiledLubRenderer; |
| try { |
| compiledLubRenderer = await lubCompiler._compileToRenderer(referenceUris); |
| } on MustachioResolutionException { |
| // Oops, switching to the LUB type prevents the renderer from compiling; |
| // likely the properties accessed in the partial are not all declared on |
| // the LUB type. |
| var names = compilers.map((c) => "'${c._rendererName}'"); |
| print("Could not deduplicate '$filePath', keeping: ${names.join(', ')}"); |
| continue; |
| } |
| |
| void removeUnusedPartials(_AotCompiler c) { |
| for (var partial in c._partialCompilers) { |
| removeUnusedPartials(partial); |
| } |
| partialsToRemove.add(c); |
| } |
| |
| for (var compiler in compilers) { |
| partialRendererFunctions[compiler] = |
| await _redirectingMethod(compiler, lubCompiler); |
| |
| for (var c in compiler._partialCompilers) { |
| removeUnusedPartials(c); |
| } |
| } |
| partialRendererFunctions[lubCompiler] = compiledLubRenderer; |
| partialRendererFunctions.addAll(lubCompiler._compiledPartials); |
| } |
| for (var c in partialsToRemove) { |
| partialRendererFunctions.remove(c); |
| } |
| |
| print('Deduplicated down to ${partialRendererFunctions.length} ' |
| 'partial renderer functions.'); |
| return partialRendererFunctions; |
| } |
| |
| /// Returns a method body for the render function for [compiler], which simply |
| /// redirects to the render function for [lubCompiler]. |
| Future<String> _redirectingMethod( |
| _AotCompiler compiler, _AotCompiler lubCompiler) async { |
| var buffer = StringBuffer()..write('String ${compiler._rendererName}'); |
| |
| buffer.writeTypeParameters( |
| compiler._usedContextStack.expand((c) => c.type.element.typeParameters)); |
| buffer.write('('); |
| |
| for (var context in compiler._usedContextStack) { |
| var contextElement = context.type.element; |
| buffer.write(contextElement.displayName); |
| if (contextElement.typeParameters.isNotEmpty) { |
| buffer.write('<'); |
| } |
| for (var tp in contextElement.typeParameters) { |
| buffer.write(tp.name); |
| if (tp != contextElement.typeParameters.last) { |
| buffer.write(', '); |
| } |
| } |
| if (contextElement.typeParameters.isNotEmpty) { |
| buffer.write('>'); |
| } |
| buffer.write(' ${context.name}'); |
| } |
| buffer.writeln(') => ${lubCompiler._rendererName}('); |
| for (var context in compiler._usedContextStack) { |
| buffer.write(context.name); |
| if (context != compiler._usedContextStack.last) { |
| buffer.write(', '); |
| } |
| } |
| buffer.write(');'); |
| return buffer.toString(); |
| } |
| |
| /// A class which compiles a single template file into a renderer Dart function, |
| /// and possible support functions for partial templates. |
| class _AotCompiler { |
| /// The template to be compiled. |
| final String _templatePath; |
| |
| /// The context type which is to be rendered into the compiled template. |
| final InterfaceType _contextType; |
| |
| /// The name of the renderer, which is either a public name (for top-level |
| /// renderers specified in a `@Renderer` annotation), or a private name for |
| /// a partial. |
| final String _rendererName; |
| |
| final _BuildData _buildData; |
| |
| /// The parsed syntax tree of the template at [_templatePath]. |
| final List<MustachioNode> _syntaxTree; |
| |
| /// The set of compilers for all referenced partials. |
| /// |
| /// This field is only complete after [_compileToRenderer] has run. |
| final Set<_AotCompiler> _partialCompilers = {}; |
| |
| final Map<_AotCompiler, String> _compiledPartials = {}; |
| |
| /// The current stack of context objects (as variable lookups). |
| final ContextStack _contextStack; |
| |
| /// The set of context objects which are ultimately used by this compiler. |
| final Set<_VariableLookup> _usedContexts = {}; |
| |
| ContextStack get _usedContextStack => |
| [..._contextStack.where(_usedContexts.contains)]; |
| |
| /// A counter for naming partial render functions. |
| /// |
| /// Incrementing the counter keeps names unique. |
| int _partialCounter = 0; |
| |
| /// A counter for naming context variables. |
| /// |
| /// Incrementing the counter keeps names unique. |
| int _contextNameCounter; |
| |
| /// Reads the template at [templatePath] and parses it into a syntax tree, |
| /// returning an [_AotCompiler] with the necessary information to be able to |
| /// compile the template into a renderer. |
| static Future<_AotCompiler> _readAndParse( |
| InterfaceType contextType, |
| String rendererName, |
| String templatePath, |
| _BuildData buildData, { |
| ContextStack contextStack = const [], |
| }) async { |
| var template = |
| await File(path.join(buildData._root, templatePath)).readAsString(); |
| var syntaxTree = MustachioParser(template, templatePath).parse(); |
| return _AotCompiler._( |
| contextType, rendererName, templatePath, syntaxTree, buildData, |
| contextStack: contextStack); |
| } |
| |
| _AotCompiler._( |
| this._contextType, |
| this._rendererName, |
| this._templatePath, |
| this._syntaxTree, |
| this._buildData, { |
| required ContextStack contextStack, |
| }) : _contextStack = _rename(contextStack), |
| _contextNameCounter = contextStack.length; |
| |
| /// Returns a copy of [original], replacing each variable's name with |
| /// `context0` through `contextN` for `N - 1` variables. |
| /// |
| /// This ensures that each renderer accepts a simple list of context objects |
| /// with predictable names. |
| static ContextStack _rename(ContextStack original) { |
| var result = <_VariableLookup>[]; |
| var index = original.length - 1; |
| for (var variable in original) { |
| result.push(_VariableLookup(variable.type, 'context$index', |
| indexInParent: original.indexOf(variable))); |
| index--; |
| } |
| return [...result.reversed]; |
| } |
| |
| Future<String> _compileToRenderer(Set<String> referenceUris) async { |
| if (_contextStack.isEmpty) { |
| var contextVariable = _VariableLookup(_contextType, 'context0'); |
| _contextStack.push(contextVariable); |
| _contextNameCounter++; |
| } |
| |
| var blockCompiler = _BlockCompiler(this, _contextStack); |
| await blockCompiler._compile(_syntaxTree); |
| var rendererBody = blockCompiler._buffer.toString(); |
| _usedContexts.addAll(_contextStack |
| .where((c) => blockCompiler._usedContextTypes.contains(c))); |
| referenceUris.addAll(blockCompiler._referenceUris); |
| |
| var buffer = StringBuffer()..write('String $_rendererName'); |
| |
| // Get the type parameters of _each_ of the context types in the stack, |
| // including their bounds, concatenate them, and wrap them in angle |
| // brackets. |
| // TODO(srawlins): This will produce erroneous code if any two context types |
| // have type parameters with the same name. Something like: |
| // _renderFoo_partial_bar_1<T, T>(Baz<T> context1, Foo<T> context0) |
| // Rename type parameters to some predictable collision-free naming scheme; |
| // the body of the function should not reference the type parameters, so |
| // this should be perfectly possible. |
| |
| var referenceElements = buffer.writeTypeParameters( |
| _usedContexts.expand((c) => c.type.element.typeParameters), |
| ); |
| for (var element in referenceElements) { |
| referenceUris.add(_elementUri(element)); |
| } |
| buffer.write('('); |
| |
| for (var context in _usedContexts) { |
| var contextElement = context.type.element; |
| referenceUris.add(_elementUri(contextElement)); |
| buffer.write(contextElement.displayName); |
| if (contextElement.typeParameters.isNotEmpty) { |
| buffer.write('<'); |
| } |
| for (var tp in contextElement.typeParameters) { |
| buffer.write(tp.name); |
| if (tp != contextElement.typeParameters.last) { |
| buffer.write(', '); |
| } |
| } |
| if (contextElement.typeParameters.isNotEmpty) { |
| buffer.write('>'); |
| } |
| buffer.write(' ${context.name}'); |
| } |
| |
| buffer.write(''') { |
| final buffer = StringBuffer(); |
| $rendererBody |
| return buffer.toString(); |
| } |
| '''); |
| return buffer.toString(); |
| } |
| |
| /// Returns the URI of [element] for use in generated import directives. |
| String _elementUri(Element element) { |
| var libraryElement = element.library!; |
| var libraryUri = libraryElement.source.uri; |
| if (libraryUri.scheme == 'file') { |
| return path.relative(libraryUri.path, |
| from: path.absolute(path.dirname(_buildData._sourcePath))); |
| } |
| return libraryUri.toString(); |
| } |
| } |
| |
| /// A class which can compile a Mustache block of nodes into Dart source for a |
| /// renderer. |
| class _BlockCompiler { |
| final _AotCompiler _templateCompiler; |
| |
| final ContextStack _contextStack; |
| |
| final Set<_VariableLookup> _usedContextTypes = {}; |
| |
| /// The set of URIs of elements that need to be imported. |
| final Set<String> _referenceUris = {}; |
| |
| final _buffer = _CompressingBuffer(); |
| |
| _BlockCompiler(this._templateCompiler, this._contextStack); |
| |
| void write(String text) => _buffer.write(text); |
| |
| void writeln(String text) => _buffer.writeln(text); |
| |
| InterfaceType get contextType => _contextStack.first.type; |
| |
| String get contextName => _contextStack.first.name; |
| |
| TypeProvider get typeProvider => _templateCompiler._buildData._typeProvider; |
| |
| TypeSystem get typeSystem => _templateCompiler._buildData._typeSystem; |
| |
| /// Generates a new name for a context variable. Each context variable going |
| /// up the stack needs to be accessible, so they each need a unique variable |
| /// name. |
| String getNewContextName() { |
| var newContextName = 'context${_templateCompiler._contextNameCounter}'; |
| _templateCompiler._contextNameCounter++; |
| return newContextName; |
| } |
| |
| /// The base name of a partial rendering function. |
| String get partialBaseName => '_${_templateCompiler._rendererName}_partial'; |
| |
| Future<void> _compile(List<MustachioNode> syntaxTree) async { |
| for (var node in syntaxTree) { |
| switch (node) { |
| case Text(): |
| _writeText(node.content); |
| case Variable(): |
| var variableLookup = _lookUpGetter(node); |
| _writeGetter(variableLookup, escape: node.escape); |
| case Section(): |
| await _compileSection(node); |
| case Partial(): |
| await _compilePartial(node, _referenceUris); |
| } |
| } |
| } |
| |
| /// Compiles [node] into a renderer's Dart source. |
| Future<void> _compilePartial(Partial node, Set<String> referenceUris) async { |
| var extension = 'html'; |
| var filePath = node.key.split('/'); |
| var fileName = filePath.removeLast(); |
| filePath.add('_$fileName.$extension'); |
| var partialPath = path.join( |
| path.dirname(_templateCompiler._templatePath), filePath.join('/')); |
| var partialCompiler = _templateCompiler._partialCompilers.firstWhereOrNull( |
| (p) => p._templatePath == partialPath && p._contextType == contextType); |
| if (partialCompiler == null) { |
| var sanitizedKey = node.key.replaceAll('.', '_').replaceAll('/', '_'); |
| var name = '${partialBaseName}_' |
| '${sanitizedKey}_' |
| '${_templateCompiler._partialCounter}'; |
| partialCompiler = await _AotCompiler._readAndParse( |
| contextType, name, partialPath, _templateCompiler._buildData, |
| contextStack: _contextStack); |
| // Add this partial renderer; it is compiled here, but not written until |
| // later. |
| _templateCompiler._partialCompilers.add(partialCompiler); |
| _templateCompiler._partialCounter++; |
| _templateCompiler._compiledPartials[partialCompiler] = |
| await partialCompiler._compileToRenderer(referenceUris); |
| _templateCompiler._compiledPartials |
| .addAll(partialCompiler._compiledPartials); |
| } |
| // Call the partial's renderer function here; the definition of the renderer |
| // function is written later. |
| write('buffer.write('); |
| writeln('${partialCompiler._rendererName}('); |
| var usedContextStack = partialCompiler._usedContexts |
| .map((context) => context.indexInParent!) |
| .map((index) => _contextStack[index]); |
| writeln(usedContextStack.map((c) => c.name).join(',')); |
| _usedContextTypes.addAll(usedContextStack); |
| writeln('));'); |
| } |
| |
| /// Compiles [node] into a renderer's Dart source. |
| Future<void> _compileSection(Section node) async { |
| var variableLookup = _lookUpGetter(node); |
| if (variableLookup.type.isDartCoreBool) { |
| // Conditional block. |
| await _compileConditionalSection(variableLookup, node.children, |
| invert: node.invert); |
| } else if (typeSystem.isAssignableTo( |
| variableLookup.type, typeProvider.iterableDynamicType)) { |
| // Repeated block. |
| await _compileRepeatedSection(variableLookup, node.children, |
| invert: node.invert); |
| } else { |
| // Use accessor value as context. |
| await _compileValueSection(variableLookup, node.children, |
| invert: node.invert); |
| } |
| } |
| |
| /// Compiles a conditional section containing [block] into a renderer's Dart |
| /// source. |
| /// |
| /// Assumes that [variableLookup] references a non-nullable [bool]. |
| Future<void> _compileConditionalSection( |
| _VariableLookup variableLookup, List<MustachioNode> block, |
| {bool invert = false}) async { |
| var variableAccess = variableLookup.name; |
| if (invert) { |
| writeln('if (!$variableAccess) {'); |
| } else { |
| writeln('if ($variableAccess) {'); |
| } |
| await _compile(block); |
| writeln('}'); |
| } |
| |
| /// Compiles a repeated section containing [block] into a renderer's Dart |
| /// source. |
| Future<void> _compileRepeatedSection( |
| _VariableLookup variableLookup, List<MustachioNode> block, |
| {bool invert = false}) async { |
| var variableIsPotentiallyNullable = |
| typeSystem.isPotentiallyNullable(variableLookup.type); |
| var variableAccess = variableLookup.name; |
| if (invert) { |
| if (variableIsPotentiallyNullable) { |
| writeln('if ($variableAccess?.isEmpty ?? true) {'); |
| } else { |
| writeln('if ($variableAccess.isEmpty) {'); |
| } |
| await _compile(block); |
| writeln('}'); |
| } else { |
| var variableAccessResult = getNewContextName(); |
| writeln('var $variableAccessResult = $variableAccess;'); |
| var newContextName = getNewContextName(); |
| if (variableIsPotentiallyNullable) { |
| writeln('if ($variableAccessResult != null) {'); |
| } |
| writeln(' for (var $newContextName in $variableAccessResult) {'); |
| // If [loopType] is something like `C<int>` where |
| // `class C<T> implements Queue<Future<T>>`, we need the [ClassElement] |
| // for [Iterable], and then use [DartType.asInstanceOf] to ultimately |
| // determine that the inner type of the loop is, for example, |
| // `Future<int>`. |
| var iterableElement = typeProvider.iterableElement; |
| var iterableType = variableLookup.type.asInstanceOf(iterableElement)!; |
| var innerContextType = iterableType.typeArguments.first as InterfaceType; |
| var innerContext = _VariableLookup(innerContextType, newContextName); |
| _contextStack.push(innerContext); |
| await _compile(block); |
| _contextStack.pop(); |
| writeln(' }'); |
| if (variableIsPotentiallyNullable) { |
| writeln('}'); |
| } |
| } |
| } |
| |
| /// Compiles a value section containing [block] into a renderer's Dart source. |
| Future<void> _compileValueSection( |
| _VariableLookup variableLookup, List<MustachioNode> block, |
| {bool invert = false}) async { |
| var variableAccess = variableLookup.name; |
| var variableIsPotentiallyNullable = |
| typeSystem.isPotentiallyNullable(variableLookup.type); |
| if (invert) { |
| writeln('if ($variableAccess == null) {'); |
| await _compile(block); |
| writeln('}'); |
| } else { |
| var innerContextName = getNewContextName(); |
| writeln('var $innerContextName = $variableAccess;'); |
| if (variableIsPotentiallyNullable) { |
| writeln('if ($innerContextName != null) {'); |
| } |
| var innerContext = _VariableLookup( |
| typeSystem.promoteToNonNull(variableLookup.type) as InterfaceType, |
| innerContextName); |
| _contextStack.push(innerContext); |
| await _compile(block); |
| _contextStack.pop(); |
| if (variableIsPotentiallyNullable) { |
| writeln('}'); |
| } |
| } |
| } |
| |
| /// Returns a valid [_VariableLookup] on a Mustache node, [node] by resolving |
| /// its key. |
| _VariableLookup _lookUpGetter(HasMultiNamedKey node) { |
| var key = node.key; |
| |
| // '.' is an entirely special case. |
| if (key.length == 1 && key[0] == '.') { |
| _usedContextTypes.add(_contextStack.first); |
| return _VariableLookup(contextType, contextName); |
| } |
| |
| var primaryName = key[0]; |
| |
| late _VariableLookup context; |
| PropertyAccessorElement? getter; |
| for (var c in _contextStack) { |
| getter = c.type.lookUpGetter2(primaryName, contextType.element.library); |
| if (getter != null) { |
| context = c; |
| _usedContextTypes.add(c); |
| break; |
| } |
| } |
| if (getter == null) { |
| var contextTypes = [for (var c in _contextStack) c.type]; |
| throw MustachioResolutionException(node.keySpan |
| .message("Failed to resolve '$key' as a property on any types in the " |
| 'context chain: $contextTypes')); |
| } |
| |
| var type = getter.returnType as InterfaceType; |
| var contextChain = typeSystem.isPotentiallyNullable(context.type) |
| // This is imperfect; the idea is that in our templates, we may have |
| // `{{foo.bar.baz}}` and `foo.bar` may be nullably typed. Mustache |
| // (and Mustachio) does not have a null-aware property access |
| // operator, nor a null-check operator. This code translates |
| // `foo.bar.baz` to `foo.bar!.baz` for nullable `foo.bar`. |
| ? '${context.name}!.$primaryName' |
| : '${context.name}.$primaryName'; |
| var remainingNames = [...key.skip(1)]; |
| for (var secondaryKey in remainingNames) { |
| getter = type.lookUpGetter2(secondaryKey, type.element.library); |
| if (getter == null) { |
| throw MustachioResolutionException(node.keySpan.message( |
| "Failed to resolve '$secondaryKey' on ${context.type} while " |
| 'resolving $remainingNames as a property chain on any types in ' |
| 'the context chain: $contextChain, after first resolving ' |
| "'$primaryName' to a property on $type")); |
| } |
| contextChain = typeSystem.isPotentiallyNullable(type) |
| ? '$contextChain!.$secondaryKey' |
| : '$contextChain.$secondaryKey'; |
| type = getter.returnType as InterfaceType; |
| } |
| return _VariableLookup(type, contextChain); |
| } |
| |
| /// Writes [content] to the generated render functions as text, properly |
| /// handling newlines, quotes, and other special characters. |
| void _writeText(String content) { |
| if (content.isEmpty) return; |
| if (content == '\n') { |
| // Blank lines happen a lot; just indicate them as such. |
| writeln('buffer.writeln();'); |
| } else { |
| content = content |
| .replaceAll(r'\', r'\\') |
| .replaceAll("'", r"\'") |
| .replaceAll(r'$', r'\$'); |
| if (_multipleWhitespacePattern.hasMatch(content)) { |
| write("buffer.write('"); |
| write(content.replaceAll('\n', '\\n')); |
| writeln("');"); |
| } else { |
| if (content[0] == '\n') { |
| write('buffer.writeln();'); |
| } |
| write("buffer.write('''"); |
| write(content); |
| writeln("''');"); |
| } |
| } |
| } |
| |
| /// A pattern for a String containing only space and newlines, more than one. |
| static final RegExp _multipleWhitespacePattern = RegExp('^[ \\n]+\$'); |
| |
| /// Writes a call to [variableLookup] to the renderer. |
| /// |
| /// The result is HTML-escaped if [escape] is true. |
| void _writeGetter(_VariableLookup variableLookup, {bool escape = true}) { |
| var variableAccess = variableLookup.name; |
| var toString = variableLookup.type.isDartCoreString |
| ? variableAccess |
| : typeSystem.isPotentiallyNullable(variableLookup.type) |
| ? '$variableAccess?.toString()' |
| : '$variableAccess.toString()'; |
| writeln(escape |
| ? 'buffer.writeEscaped($toString);' |
| : 'buffer.write($toString);'); |
| } |
| } |
| |
| /// Various static build data to be used for each renderer, including specified |
| /// renderers and template renderers. |
| @immutable |
| class _BuildData { |
| final TypeProvider _typeProvider; |
| |
| final TypeSystem _typeSystem; |
| |
| final String _sourcePath; |
| |
| final String _root; |
| |
| _BuildData( |
| this._typeProvider, this._typeSystem, this._sourcePath, this._root); |
| } |
| |
| /// Represents a variable lookup via property access chain [name] which returns |
| /// an object of type [type]. |
| @immutable |
| class _VariableLookup { |
| final InterfaceType type; |
| |
| final String name; |
| |
| /// The index of this variable in the declaring compiler's parent compiler's |
| /// context stack, if it was declared in the construction of a compiler. |
| final int? indexInParent; |
| |
| _VariableLookup(this.type, this.name, {this.indexInParent}); |
| } |
| |
| extension<T> on List<T> { |
| void push(T value) => insert(0, value); |
| |
| T pop() => removeAt(0); |
| } |
| |
| extension on StringBuffer { |
| Set<Element> writeTypeParameters( |
| Iterable<TypeParameterElement> typeParameters) { |
| var referencedElements = <Element>{}; |
| var hasTypeParameters = false; |
| for (var typeParameter in typeParameters) { |
| if (!hasTypeParameters) { |
| write('<'); |
| } else { |
| write(', '); |
| } |
| hasTypeParameters = true; |
| |
| var bound = typeParameter.bound; |
| if (bound == null) { |
| write(typeParameter.name); |
| } else { |
| var boundElement = bound.documentableElement!; |
| referencedElements.add(boundElement); |
| write('${typeParameter.name} extends ${boundElement.name!}'); |
| } |
| } |
| if (hasTypeParameters) { |
| write('>'); |
| } |
| return referencedElements; |
| } |
| } |
| |
| extension on TypeSystem { |
| /// Returns a list of types which represents the LUB of each of the types in |
| /// the corresponding positions in [contextStacks]. |
| List<InterfaceType>? contextStackLub(List<ContextStack> contextStacks) { |
| // The length of each and every context stack. |
| var contextStacksLength = contextStacks.first.length; |
| if (contextStacks.any((s) => s.length != contextStacksLength)) { |
| return null; |
| } |
| |
| // The new list of context types, each of which is the LUB of the associated |
| // context type of each of the compilers. |
| var contextStackTypes = <InterfaceType>[]; |
| for (var i = 0; i < contextStacksLength; i++) { |
| var types = contextStacks.map((s) => s[i].type); |
| var lubType = |
| types.fold<DartType>(types.first, leastUpperBound) as InterfaceType; |
| contextStackTypes.add(lubType); |
| } |
| |
| return contextStackTypes; |
| } |
| } |
| |
| /// A wrapper around a [StringBuffer] that removes blank lines. |
| final class _CompressingBuffer { |
| final StringBuffer _buffer = StringBuffer(); |
| |
| bool _prevEndsWithNewline = false; |
| |
| void write(String text) { |
| if (text.isEmpty) return; |
| text = text |
| // Strip any trailing spaces on each line. |
| .replaceAll(RegExp(r'\s+\n'), '\n') |
| // Compress consecutive newlines. |
| .replaceAll(RegExp(r'\n+'), '\n'); |
| if (_prevEndsWithNewline) { |
| var first = text.codeUnits.first; |
| if (first == 0x0A /* '\n' */) { |
| text = text.substring(1); |
| } |
| } |
| if (text.isEmpty) return; |
| var last = text.codeUnits.last; |
| if (last == 0x0A /* '\n' */) { |
| _prevEndsWithNewline = true; |
| } |
| _buffer.write(text); |
| } |
| |
| void writeln(String text) => write('$text\n'); |
| |
| @override |
| String toString() => _buffer.toString(); |
| } |