From 2758ca114a914142bb4a0193761989cbd33da62a Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 30 Jul 2021 18:00:09 -0700 Subject: [PATCH] Lazily determine whether a mixin contains a @content block This avoids saddling the caller of `MixinRule()` with the responsibility of correctly determining whether a content block exists. --- lib/src/ast/sass/statement/mixin_rule.dart | 22 ++- lib/src/importer/package.dart | 3 +- lib/src/parse/stylesheet.dart | 12 +- lib/src/util/nullable.dart | 2 +- lib/src/visitor/recursive_ast.dart | 2 + lib/src/visitor/recursive_statement.dart | 2 + lib/src/visitor/statement_search.dart | 183 +++++++++++++++++++++ 7 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 lib/src/visitor/statement_search.dart diff --git a/lib/src/ast/sass/statement/mixin_rule.dart b/lib/src/ast/sass/statement/mixin_rule.dart index 1b3ceb2c..7d284558 100644 --- a/lib/src/ast/sass/statement/mixin_rule.dart +++ b/lib/src/ast/sass/statement/mixin_rule.dart @@ -5,6 +5,7 @@ import 'package:source_span/source_span.dart'; import '../../../visitor/interface/statement.dart'; +import '../../../visitor/statement_search.dart'; import '../argument_declaration.dart'; import '../statement.dart'; import 'callable_declaration.dart'; @@ -15,16 +16,12 @@ import 'silent_comment.dart'; /// This declares a mixin that's invoked using `@include`. class MixinRule extends CallableDeclaration { /// Whether the mixin contains a `@content` rule. - final bool hasContent; + late final bool hasContent = + const _HasContentVisitor().visitMixinRule(this) == true; - /// Creates a [MixinRule]. - /// - /// It's important that the caller passes [hasContent] if the mixin - /// recursively contains a `@content` rule. Otherwise, invoking this mixin - /// won't work correctly. MixinRule(String name, ArgumentDeclaration arguments, Iterable children, FileSpan span, - {this.hasContent = false, SilentComment? comment}) + {SilentComment? comment}) : super(name, arguments, children, span, comment: comment); T accept(StatementVisitor visitor) => visitor.visitMixinRule(this); @@ -36,3 +33,14 @@ class MixinRule extends CallableDeclaration { return buffer.toString(); } } + +/// A visitor for determining whether a [MixinRule] recursively contains a +/// [ContentRule]. +class _HasContentVisitor extends StatementSearchVisitor { + const _HasContentVisitor(); + + bool visitContentRule(_) => true; + bool? visitArgumentInvocation(_) => null; + bool? visitSupportsCondition(_) => null; + bool? visitInterpolation(_) => null; +} diff --git a/lib/src/importer/package.dart b/lib/src/importer/package.dart index c8822710..21f41509 100644 --- a/lib/src/importer/package.dart +++ b/lib/src/importer/package.dart @@ -26,8 +26,7 @@ class PackageImporter extends Importer { /// package. /// /// [`PackageConfig`]: https://pub.dev/documentation/package_config/latest/package_config.package_config/PackageConfig-class.html - PackageImporter(PackageConfig packageConfig) - : _packageConfig = packageConfig; + PackageImporter(PackageConfig packageConfig) : _packageConfig = packageConfig; Uri? canonicalize(Uri url) { if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 6301691f..943f8dce 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -42,11 +42,6 @@ abstract class StylesheetParser extends Parser { /// declaration. var _inMixin = false; - /// Whether the current mixin contains at least one `@content` rule. - /// - /// This is `null` unless [_inMixin] is `true`. - bool? _mixinHasContent; - /// Whether the parser is currently parsing a content block passed to a mixin. var _inContentBlock = false; @@ -814,7 +809,6 @@ abstract class StylesheetParser extends Parser { ? _argumentInvocation(mixin: true) : ArgumentInvocation.empty(scanner.emptySpan); - _mixinHasContent = true; expectStatementSeparator("@content rule"); return ContentRule(arguments, scanner.spanFrom(start)); } @@ -1251,15 +1245,11 @@ abstract class StylesheetParser extends Parser { whitespace(); _inMixin = true; - _mixinHasContent = false; return _withChildren(_statement, start, (children, span) { - var hadContent = _mixinHasContent!; _inMixin = false; - _mixinHasContent = null; - return MixinRule(name, arguments, children, span, - hasContent: hadContent, comment: precedingComment); + comment: precedingComment); }); } diff --git a/lib/src/util/nullable.dart b/lib/src/util/nullable.dart index f5bf6258..125f58d4 100644 --- a/lib/src/util/nullable.dart +++ b/lib/src/util/nullable.dart @@ -7,7 +7,7 @@ extension NullableExtension on T? { /// result. /// /// Based on Rust's `Option.and_then`. - V? andThen(V Function(T value)? fn) { + V? andThen(V? Function(T value)? fn) { var self = this; // dart-lang/language#1520 return self == null ? null : fn!(self); } diff --git a/lib/src/visitor/recursive_ast.dart b/lib/src/visitor/recursive_ast.dart index 88778961..a4c17698 100644 --- a/lib/src/visitor/recursive_ast.dart +++ b/lib/src/visitor/recursive_ast.dart @@ -13,6 +13,8 @@ import 'recursive_statement.dart'; /// addition to each statement. abstract class RecursiveAstVisitor extends RecursiveStatementVisitor implements ExpressionVisitor { + const RecursiveAstVisitor(); + void visitExpression(Expression expression) { expression.accept(this); } diff --git a/lib/src/visitor/recursive_statement.dart b/lib/src/visitor/recursive_statement.dart index 2c64f82b..14c5f076 100644 --- a/lib/src/visitor/recursive_statement.dart +++ b/lib/src/visitor/recursive_statement.dart @@ -21,6 +21,8 @@ import 'interface/statement.dart'; /// * [visitInterpolation] /// * [visitExpression] abstract class RecursiveStatementVisitor implements StatementVisitor { + const RecursiveStatementVisitor(); + void visitAtRootRule(AtRootRule node) { node.query.andThen(visitInterpolation); visitChildren(node.children); diff --git a/lib/src/visitor/statement_search.dart b/lib/src/visitor/statement_search.dart new file mode 100644 index 00000000..3b5aeb59 --- /dev/null +++ b/lib/src/visitor/statement_search.dart @@ -0,0 +1,183 @@ +// Copyright 2021 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:meta/meta.dart'; + +import '../ast/sass.dart'; +import '../util/nullable.dart'; +import 'interface/statement.dart'; +import 'recursive_statement.dart'; + +/// A [StatementVisitor] whose `visit*` methods default to returning `null`, but +/// which returns the first non-`null` value returned by any method. +/// +/// This can be extended to find the first instance of particular nodes in the +/// AST. +/// +/// This supports the same additional methods as [RecursiveStatementVisitor]. +abstract class StatementSearchVisitor implements StatementVisitor { + const StatementSearchVisitor(); + + T? visitAtRootRule(AtRootRule node) => + node.query.andThen(visitInterpolation) ?? visitChildren(node.children); + + T? visitAtRule(AtRule node) => + visitInterpolation(node.name) ?? + node.value.andThen(visitInterpolation) ?? + node.children.andThen(visitChildren); + + T? visitContentBlock(ContentBlock node) => visitCallableDeclaration(node); + + T? visitContentRule(ContentRule node) => + visitArgumentInvocation(node.arguments); + + T? visitDebugRule(DebugRule node) => visitExpression(node.expression); + + T? visitDeclaration(Declaration node) => + visitInterpolation(node.name) ?? + node.value.andThen(visitExpression) ?? + node.children.andThen(visitChildren); + + T? visitEachRule(EachRule node) => + visitExpression(node.list) ?? visitChildren(node.children); + + T? visitErrorRule(ErrorRule node) => visitExpression(node.expression); + + T? visitExtendRule(ExtendRule node) => visitInterpolation(node.selector); + + T? visitForRule(ForRule node) => + visitExpression(node.from) ?? + visitExpression(node.to) ?? + visitChildren(node.children); + + T? visitForwardRule(ForwardRule node) => null; + + T? visitFunctionRule(FunctionRule node) => visitCallableDeclaration(node); + + T? visitIfRule(IfRule node) => + node.clauses._search((clause) => + visitExpression(clause.expression) ?? + clause.children._search((child) => child.accept(this))) ?? + node.lastClause.andThen((lastClause) => + lastClause.children._search((child) => child.accept(this))); + + T? visitImportRule(ImportRule node) => node.imports._search((import) { + if (import is StaticImport) { + return visitInterpolation(import.url) ?? + import.supports.andThen(visitSupportsCondition) ?? + import.media.andThen(visitInterpolation); + } + }); + + T? visitIncludeRule(IncludeRule node) => + visitArgumentInvocation(node.arguments) ?? + node.content.andThen(visitContentBlock); + + T? visitLoudComment(LoudComment node) => visitInterpolation(node.text); + + T? visitMediaRule(MediaRule node) => + visitInterpolation(node.query) ?? visitChildren(node.children); + + T? visitMixinRule(MixinRule node) => visitCallableDeclaration(node); + + T? visitReturnRule(ReturnRule node) => visitExpression(node.expression); + + T? visitSilentComment(SilentComment node) => null; + + T? visitStyleRule(StyleRule node) => + visitInterpolation(node.selector) ?? visitChildren(node.children); + + T? visitStylesheet(Stylesheet node) => visitChildren(node.children); + + T? visitSupportsRule(SupportsRule node) => + visitSupportsCondition(node.condition) ?? visitChildren(node.children); + + T? visitUseRule(UseRule node) => null; + + T? visitVariableDeclaration(VariableDeclaration node) => + visitExpression(node.expression); + + T? visitWarnRule(WarnRule node) => visitExpression(node.expression); + + T? visitWhileRule(WhileRule node) => + visitExpression(node.condition) ?? visitChildren(node.children); + + /// Visits each of [node]'s expressions and children. + /// + /// The default implementations of [visitFunctionRule] and [visitMixinRule] + /// call this. + @protected + T? visitCallableDeclaration(CallableDeclaration node) => + node.arguments.arguments._search( + (argument) => argument.defaultValue.andThen(visitExpression)) ?? + visitChildren(node.children); + + /// Visits each expression in an [invocation]. + /// + /// The default implementation of the visit methods calls this to visit any + /// argument invocation in a statement. + @protected + T? visitArgumentInvocation(ArgumentInvocation invocation) => + invocation.positional + ._search((expression) => visitExpression(expression)) ?? + invocation.named.values + ._search((expression) => visitExpression(expression)) ?? + invocation.rest.andThen(visitExpression) ?? + invocation.keywordRest.andThen(visitExpression); + + /// Visits each expression in [condition]. + /// + /// The default implementation of the visit methods call this to visit any + /// [SupportsCondition] they encounter. + @protected + T? visitSupportsCondition(SupportsCondition condition) { + if (condition is SupportsOperation) { + return visitSupportsCondition(condition.left) ?? + visitSupportsCondition(condition.right); + } else if (condition is SupportsNegation) { + return visitSupportsCondition(condition.condition); + } else if (condition is SupportsInterpolation) { + return visitExpression(condition.expression); + } else if (condition is SupportsDeclaration) { + return visitExpression(condition.name) ?? + visitExpression(condition.value); + } else { + return null; + } + } + + /// Visits each child in [children]. + /// + /// The default implementation of the visit methods for all [ParentStatement]s + /// call this. + @protected + T? visitChildren(List children) => + children._search((child) => child.accept(this)); + + /// Visits each expression in an [interpolation]. + /// + /// The default implementation of the visit methods call this to visit any + /// interpolation in a statement. + @protected + T? visitInterpolation(Interpolation interpolation) => interpolation.contents + ._search((node) => node is Expression ? visitExpression(node) : null); + + /// Visits [expression]. + /// + /// The default implementation of the visit methods call this to visit any + /// expression in a statement. + @protected + T? visitExpression(Expression expression) => null; +} + +extension _IterableExtension on Iterable { + /// Returns the first `T` returned by [callback] for an element of [iterable], + /// or `null` if it returns `null` for every element. + T? _search(T? Function(E element) callback) { + for (var element in this) { + var value = callback(element); + if (value != null) return value; + } + } +}