Skip to content

Commit 2758ca1

Browse files
committed
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.
1 parent 6f17b4a commit 2758ca1

File tree

7 files changed

+205
-21
lines changed

7 files changed

+205
-21
lines changed

lib/src/ast/sass/statement/mixin_rule.dart

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:source_span/source_span.dart';
66

77
import '../../../visitor/interface/statement.dart';
8+
import '../../../visitor/statement_search.dart';
89
import '../argument_declaration.dart';
910
import '../statement.dart';
1011
import 'callable_declaration.dart';
@@ -15,16 +16,12 @@ import 'silent_comment.dart';
1516
/// This declares a mixin that's invoked using `@include`.
1617
class MixinRule extends CallableDeclaration {
1718
/// Whether the mixin contains a `@content` rule.
18-
final bool hasContent;
19+
late final bool hasContent =
20+
const _HasContentVisitor().visitMixinRule(this) == true;
1921

20-
/// Creates a [MixinRule].
21-
///
22-
/// It's important that the caller passes [hasContent] if the mixin
23-
/// recursively contains a `@content` rule. Otherwise, invoking this mixin
24-
/// won't work correctly.
2522
MixinRule(String name, ArgumentDeclaration arguments,
2623
Iterable<Statement> children, FileSpan span,
27-
{this.hasContent = false, SilentComment? comment})
24+
{SilentComment? comment})
2825
: super(name, arguments, children, span, comment: comment);
2926

3027
T accept<T>(StatementVisitor<T> visitor) => visitor.visitMixinRule(this);
@@ -36,3 +33,14 @@ class MixinRule extends CallableDeclaration {
3633
return buffer.toString();
3734
}
3835
}
36+
37+
/// A visitor for determining whether a [MixinRule] recursively contains a
38+
/// [ContentRule].
39+
class _HasContentVisitor extends StatementSearchVisitor<bool> {
40+
const _HasContentVisitor();
41+
42+
bool visitContentRule(_) => true;
43+
bool? visitArgumentInvocation(_) => null;
44+
bool? visitSupportsCondition(_) => null;
45+
bool? visitInterpolation(_) => null;
46+
}

lib/src/importer/package.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ class PackageImporter extends Importer {
2626
/// package.
2727
///
2828
/// [`PackageConfig`]: https://pub.dev/documentation/package_config/latest/package_config.package_config/PackageConfig-class.html
29-
PackageImporter(PackageConfig packageConfig)
30-
: _packageConfig = packageConfig;
29+
PackageImporter(PackageConfig packageConfig) : _packageConfig = packageConfig;
3130

3231
Uri? canonicalize(Uri url) {
3332
if (url.scheme == 'file') return _filesystemImporter.canonicalize(url);

lib/src/parse/stylesheet.dart

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ abstract class StylesheetParser extends Parser {
4242
/// declaration.
4343
var _inMixin = false;
4444

45-
/// Whether the current mixin contains at least one `@content` rule.
46-
///
47-
/// This is `null` unless [_inMixin] is `true`.
48-
bool? _mixinHasContent;
49-
5045
/// Whether the parser is currently parsing a content block passed to a mixin.
5146
var _inContentBlock = false;
5247

@@ -814,7 +809,6 @@ abstract class StylesheetParser extends Parser {
814809
? _argumentInvocation(mixin: true)
815810
: ArgumentInvocation.empty(scanner.emptySpan);
816811

817-
_mixinHasContent = true;
818812
expectStatementSeparator("@content rule");
819813
return ContentRule(arguments, scanner.spanFrom(start));
820814
}
@@ -1251,15 +1245,11 @@ abstract class StylesheetParser extends Parser {
12511245

12521246
whitespace();
12531247
_inMixin = true;
1254-
_mixinHasContent = false;
12551248

12561249
return _withChildren(_statement, start, (children, span) {
1257-
var hadContent = _mixinHasContent!;
12581250
_inMixin = false;
1259-
_mixinHasContent = null;
1260-
12611251
return MixinRule(name, arguments, children, span,
1262-
hasContent: hadContent, comment: precedingComment);
1252+
comment: precedingComment);
12631253
});
12641254
}
12651255

lib/src/util/nullable.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ extension NullableExtension<T> on T? {
77
/// result.
88
///
99
/// Based on Rust's `Option.and_then`.
10-
V? andThen<V>(V Function(T value)? fn) {
10+
V? andThen<V>(V? Function(T value)? fn) {
1111
var self = this; // dart-lang/language#1520
1212
return self == null ? null : fn!(self);
1313
}

lib/src/visitor/recursive_ast.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import 'recursive_statement.dart';
1313
/// addition to each statement.
1414
abstract class RecursiveAstVisitor extends RecursiveStatementVisitor
1515
implements ExpressionVisitor<void> {
16+
const RecursiveAstVisitor();
17+
1618
void visitExpression(Expression expression) {
1719
expression.accept(this);
1820
}

lib/src/visitor/recursive_statement.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import 'interface/statement.dart';
2121
/// * [visitInterpolation]
2222
/// * [visitExpression]
2323
abstract class RecursiveStatementVisitor implements StatementVisitor<void> {
24+
const RecursiveStatementVisitor();
25+
2426
void visitAtRootRule(AtRootRule node) {
2527
node.query.andThen(visitInterpolation);
2628
visitChildren(node.children);

lib/src/visitor/statement_search.dart

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright 2021 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'package:meta/meta.dart';
6+
7+
import '../ast/sass.dart';
8+
import '../util/nullable.dart';
9+
import 'interface/statement.dart';
10+
import 'recursive_statement.dart';
11+
12+
/// A [StatementVisitor] whose `visit*` methods default to returning `null`, but
13+
/// which returns the first non-`null` value returned by any method.
14+
///
15+
/// This can be extended to find the first instance of particular nodes in the
16+
/// AST.
17+
///
18+
/// This supports the same additional methods as [RecursiveStatementVisitor].
19+
abstract class StatementSearchVisitor<T> implements StatementVisitor<T?> {
20+
const StatementSearchVisitor();
21+
22+
T? visitAtRootRule(AtRootRule node) =>
23+
node.query.andThen(visitInterpolation) ?? visitChildren(node.children);
24+
25+
T? visitAtRule(AtRule node) =>
26+
visitInterpolation(node.name) ??
27+
node.value.andThen(visitInterpolation) ??
28+
node.children.andThen(visitChildren);
29+
30+
T? visitContentBlock(ContentBlock node) => visitCallableDeclaration(node);
31+
32+
T? visitContentRule(ContentRule node) =>
33+
visitArgumentInvocation(node.arguments);
34+
35+
T? visitDebugRule(DebugRule node) => visitExpression(node.expression);
36+
37+
T? visitDeclaration(Declaration node) =>
38+
visitInterpolation(node.name) ??
39+
node.value.andThen(visitExpression) ??
40+
node.children.andThen(visitChildren);
41+
42+
T? visitEachRule(EachRule node) =>
43+
visitExpression(node.list) ?? visitChildren(node.children);
44+
45+
T? visitErrorRule(ErrorRule node) => visitExpression(node.expression);
46+
47+
T? visitExtendRule(ExtendRule node) => visitInterpolation(node.selector);
48+
49+
T? visitForRule(ForRule node) =>
50+
visitExpression(node.from) ??
51+
visitExpression(node.to) ??
52+
visitChildren(node.children);
53+
54+
T? visitForwardRule(ForwardRule node) => null;
55+
56+
T? visitFunctionRule(FunctionRule node) => visitCallableDeclaration(node);
57+
58+
T? visitIfRule(IfRule node) =>
59+
node.clauses._search((clause) =>
60+
visitExpression(clause.expression) ??
61+
clause.children._search((child) => child.accept(this))) ??
62+
node.lastClause.andThen((lastClause) =>
63+
lastClause.children._search((child) => child.accept(this)));
64+
65+
T? visitImportRule(ImportRule node) => node.imports._search((import) {
66+
if (import is StaticImport) {
67+
return visitInterpolation(import.url) ??
68+
import.supports.andThen(visitSupportsCondition) ??
69+
import.media.andThen(visitInterpolation);
70+
}
71+
});
72+
73+
T? visitIncludeRule(IncludeRule node) =>
74+
visitArgumentInvocation(node.arguments) ??
75+
node.content.andThen(visitContentBlock);
76+
77+
T? visitLoudComment(LoudComment node) => visitInterpolation(node.text);
78+
79+
T? visitMediaRule(MediaRule node) =>
80+
visitInterpolation(node.query) ?? visitChildren(node.children);
81+
82+
T? visitMixinRule(MixinRule node) => visitCallableDeclaration(node);
83+
84+
T? visitReturnRule(ReturnRule node) => visitExpression(node.expression);
85+
86+
T? visitSilentComment(SilentComment node) => null;
87+
88+
T? visitStyleRule(StyleRule node) =>
89+
visitInterpolation(node.selector) ?? visitChildren(node.children);
90+
91+
T? visitStylesheet(Stylesheet node) => visitChildren(node.children);
92+
93+
T? visitSupportsRule(SupportsRule node) =>
94+
visitSupportsCondition(node.condition) ?? visitChildren(node.children);
95+
96+
T? visitUseRule(UseRule node) => null;
97+
98+
T? visitVariableDeclaration(VariableDeclaration node) =>
99+
visitExpression(node.expression);
100+
101+
T? visitWarnRule(WarnRule node) => visitExpression(node.expression);
102+
103+
T? visitWhileRule(WhileRule node) =>
104+
visitExpression(node.condition) ?? visitChildren(node.children);
105+
106+
/// Visits each of [node]'s expressions and children.
107+
///
108+
/// The default implementations of [visitFunctionRule] and [visitMixinRule]
109+
/// call this.
110+
@protected
111+
T? visitCallableDeclaration(CallableDeclaration node) =>
112+
node.arguments.arguments._search(
113+
(argument) => argument.defaultValue.andThen(visitExpression)) ??
114+
visitChildren(node.children);
115+
116+
/// Visits each expression in an [invocation].
117+
///
118+
/// The default implementation of the visit methods calls this to visit any
119+
/// argument invocation in a statement.
120+
@protected
121+
T? visitArgumentInvocation(ArgumentInvocation invocation) =>
122+
invocation.positional
123+
._search((expression) => visitExpression(expression)) ??
124+
invocation.named.values
125+
._search((expression) => visitExpression(expression)) ??
126+
invocation.rest.andThen(visitExpression) ??
127+
invocation.keywordRest.andThen(visitExpression);
128+
129+
/// Visits each expression in [condition].
130+
///
131+
/// The default implementation of the visit methods call this to visit any
132+
/// [SupportsCondition] they encounter.
133+
@protected
134+
T? visitSupportsCondition(SupportsCondition condition) {
135+
if (condition is SupportsOperation) {
136+
return visitSupportsCondition(condition.left) ??
137+
visitSupportsCondition(condition.right);
138+
} else if (condition is SupportsNegation) {
139+
return visitSupportsCondition(condition.condition);
140+
} else if (condition is SupportsInterpolation) {
141+
return visitExpression(condition.expression);
142+
} else if (condition is SupportsDeclaration) {
143+
return visitExpression(condition.name) ??
144+
visitExpression(condition.value);
145+
} else {
146+
return null;
147+
}
148+
}
149+
150+
/// Visits each child in [children].
151+
///
152+
/// The default implementation of the visit methods for all [ParentStatement]s
153+
/// call this.
154+
@protected
155+
T? visitChildren(List<Statement> children) =>
156+
children._search((child) => child.accept(this));
157+
158+
/// Visits each expression in an [interpolation].
159+
///
160+
/// The default implementation of the visit methods call this to visit any
161+
/// interpolation in a statement.
162+
@protected
163+
T? visitInterpolation(Interpolation interpolation) => interpolation.contents
164+
._search((node) => node is Expression ? visitExpression(node) : null);
165+
166+
/// Visits [expression].
167+
///
168+
/// The default implementation of the visit methods call this to visit any
169+
/// expression in a statement.
170+
@protected
171+
T? visitExpression(Expression expression) => null;
172+
}
173+
174+
extension _IterableExtension<E> on Iterable<E> {
175+
/// Returns the first `T` returned by [callback] for an element of [iterable],
176+
/// or `null` if it returns `null` for every element.
177+
T? _search<T>(T? Function(E element) callback) {
178+
for (var element in this) {
179+
var value = callback(element);
180+
if (value != null) return value;
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)