Skip to content

Commit

Permalink
Lazily determine whether a mixin contains a @content block
Browse files Browse the repository at this point in the history
This avoids saddling the caller of `MixinRule()` with the
responsibility of correctly determining whether a content block
exists.
  • Loading branch information
nex3 committed Jul 31, 2021
1 parent 6f17b4a commit 2758ca1
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 21 deletions.
22 changes: 15 additions & 7 deletions lib/src/ast/sass/statement/mixin_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Statement> children, FileSpan span,
{this.hasContent = false, SilentComment? comment})
{SilentComment? comment})
: super(name, arguments, children, span, comment: comment);

T accept<T>(StatementVisitor<T> visitor) => visitor.visitMixinRule(this);
Expand All @@ -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<bool> {
const _HasContentVisitor();

bool visitContentRule(_) => true;
bool? visitArgumentInvocation(_) => null;
bool? visitSupportsCondition(_) => null;
bool? visitInterpolation(_) => null;
}
3 changes: 1 addition & 2 deletions lib/src/importer/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 1 addition & 11 deletions lib/src/parse/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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);
});
}

Expand Down
2 changes: 1 addition & 1 deletion lib/src/util/nullable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension NullableExtension<T> on T? {
/// result.
///
/// Based on Rust's `Option.and_then`.
V? andThen<V>(V Function(T value)? fn) {
V? andThen<V>(V? Function(T value)? fn) {
var self = this; // dart-lang/language#1520
return self == null ? null : fn!(self);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/src/visitor/recursive_ast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import 'recursive_statement.dart';
/// addition to each statement.
abstract class RecursiveAstVisitor extends RecursiveStatementVisitor
implements ExpressionVisitor<void> {
const RecursiveAstVisitor();

void visitExpression(Expression expression) {
expression.accept(this);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/src/visitor/recursive_statement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import 'interface/statement.dart';
/// * [visitInterpolation]
/// * [visitExpression]
abstract class RecursiveStatementVisitor implements StatementVisitor<void> {
const RecursiveStatementVisitor();

void visitAtRootRule(AtRootRule node) {
node.query.andThen(visitInterpolation);
visitChildren(node.children);
Expand Down
183 changes: 183 additions & 0 deletions lib/src/visitor/statement_search.dart
Original file line number Diff line number Diff line change
@@ -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<T> implements StatementVisitor<T?> {
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<Statement> 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<E> on Iterable<E> {
/// Returns the first `T` returned by [callback] for an element of [iterable],
/// or `null` if it returns `null` for every element.
T? _search<T>(T? Function(E element) callback) {
for (var element in this) {
var value = callback(element);
if (value != null) return value;
}
}
}

0 comments on commit 2758ca1

Please sign in to comment.