Add shared interfaces for various AST nodes (#1445)

Fixes #1401 and #1414.

Adds `Dependency`, `SassDeclaration`, and `SassReference` interfaces,
which expose some getters that multiple AST nodes have in common with a
single type.

These also add getters for common subspans (URL, name, and namespace) to
the interfaces.
This commit is contained in:
Jennifer Thakar 2021-08-23 16:33:36 -07:00 committed by GitHub
parent fd7eec9eac
commit d419df7d9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 424 additions and 118 deletions

View File

@ -1,3 +1,7 @@
## 1.38.1
* No user-visible changes
## 1.38.0
* In expanded mode, emit characters in Unicode private-use areas as escape

View File

@ -8,12 +8,15 @@ export 'sass/argument_invocation.dart';
export 'sass/at_root_query.dart';
export 'sass/callable_invocation.dart';
export 'sass/configured_variable.dart';
export 'sass/declaration.dart';
export 'sass/dependency.dart';
export 'sass/expression.dart';
export 'sass/expression/binary_operation.dart';
export 'sass/expression/boolean.dart';
export 'sass/expression/color.dart';
export 'sass/expression/function.dart';
export 'sass/expression/if.dart';
export 'sass/expression/interpolated_function.dart';
export 'sass/expression/list.dart';
export 'sass/expression/map.dart';
export 'sass/expression/null.dart';
@ -29,6 +32,7 @@ export 'sass/import/dynamic.dart';
export 'sass/import/static.dart';
export 'sass/interpolation.dart';
export 'sass/node.dart';
export 'sass/reference.dart';
export 'sass/statement.dart';
export 'sass/statement/at_root_rule.dart';
export 'sass/statement/at_rule.dart';

View File

@ -6,14 +6,16 @@ import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../utils.dart';
import '../../util/span.dart';
import 'expression.dart';
import 'declaration.dart';
import 'node.dart';
/// An argument declared as part of an [ArgumentDeclaration].
///
/// {@category AST}
@sealed
class Argument implements SassNode {
class Argument implements SassNode, SassDeclaration {
/// The argument name.
final String name;
@ -30,6 +32,9 @@ class Argument implements SassNode {
String get originalName =>
defaultValue == null ? span.text : declarationName(span);
FileSpan get nameSpan =>
defaultValue == null ? span : span.initialIdentifier(includeLeading: 1);
Argument(this.name, this.span, {this.defaultValue});
String toString() => defaultValue == null ? name : "$name: $defaultValue";

View File

@ -10,6 +10,7 @@ import '../../logger.dart';
import '../../parse/scss.dart';
import '../../utils.dart';
import '../../util/character.dart';
import '../../util/span.dart';
import 'argument.dart';
import 'node.dart';

View File

@ -5,14 +5,16 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../util/span.dart';
import 'expression.dart';
import 'declaration.dart';
import 'node.dart';
/// A variable configured by a `with` clause in a `@use` or `@forward` rule.
///
/// {@category AST}
@sealed
class ConfiguredVariable implements SassNode {
class ConfiguredVariable implements SassNode, SassDeclaration {
/// The name of the variable being configured.
final String name;
@ -26,6 +28,8 @@ class ConfiguredVariable implements SassNode {
final FileSpan span;
FileSpan get nameSpan => span.initialIdentifier(includeLeading: 1);
ConfiguredVariable(this.name, this.expression, this.span,
{bool guarded = false})
: isGuarded = guarded;

View File

@ -0,0 +1,24 @@
// 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 'package:source_span/source_span.dart';
import 'node.dart';
/// A common interface for any node that declares a Sass member.
///
/// {@category AST}
@sealed
abstract class SassDeclaration extends SassNode {
/// The name of the declaration, with underscores converted to hyphens.
///
/// This does not include the `$` for variables.
String get name;
/// The span containing this declaration's name.
///
/// This includes the `$` for variables.
FileSpan get nameSpan;
}

View File

@ -0,0 +1,20 @@
// 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 'package:source_span/source_span.dart';
import 'node.dart';
/// A common interface for [UseRule]s, [ForwardRule]s, and [DynamicImport]s.
///
/// {@category AST}
@sealed
abstract class SassDependency extends SassNode {
/// The URL of the dependency this rule loads.
Uri get url;
/// The span of the URL for this dependency, including the quotes.
FileSpan get urlSpan;
}

View File

@ -5,38 +5,50 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../argument_invocation.dart';
import '../callable_invocation.dart';
import '../interpolation.dart';
import '../reference.dart';
/// A function invocation.
///
/// This may be a plain CSS function or a Sass function.
/// This may be a plain CSS function or a Sass function, but may not include
/// interpolation.
///
/// {@category AST}
@sealed
class FunctionExpression implements Expression, CallableInvocation {
class FunctionExpression
implements Expression, CallableInvocation, SassReference {
/// The namespace of the function being invoked, or `null` if it's invoked
/// without a namespace.
final String? namespace;
/// The name of the function being invoked.
///
/// If [namespace] is non-`null`, underscores are converted to hyphens in this name.
/// If [namespace] is `null`, this could be a plain CSS function call, so underscores are kept unchanged.
///
/// If this is interpolated, the function will be interpreted as plain CSS,
/// even if it has the same name as a Sass function.
final Interpolation name;
/// The name of the function being invoked, with underscores left as-is.
final String originalName;
/// The arguments to pass to the function.
final ArgumentInvocation arguments;
final FileSpan span;
FunctionExpression(this.name, this.arguments, this.span, {this.namespace});
/// The name of the function being invoked, with underscores converted to
/// hyphens.
///
/// If this function is a plain CSS function, use [originalName] instead.
String get name => originalName.replaceAll('_', '-');
FileSpan get nameSpan {
if (namespace == null) return span.initialIdentifier();
return span.withoutNamespace().initialIdentifier();
}
FileSpan? get namespaceSpan =>
namespace == null ? null : span.initialIdentifier();
FunctionExpression(this.originalName, this.arguments, this.span,
{this.namespace});
T accept<T>(ExpressionVisitor<T> visitor) =>
visitor.visitFunctionExpression(this);
@ -44,7 +56,7 @@ class FunctionExpression implements Expression, CallableInvocation {
String toString() {
var buffer = StringBuffer();
if (namespace != null) buffer.write("$namespace.");
buffer.write("$name$arguments");
buffer.write("$originalName$arguments");
return buffer.toString();
}
}

View File

@ -0,0 +1,35 @@
// 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 'package:source_span/source_span.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../argument_invocation.dart';
import '../callable_invocation.dart';
import '../interpolation.dart';
/// An interpolated function invocation.
///
/// This is always a plain CSS function.
///
/// {@category AST}
@sealed
class InterpolatedFunctionExpression implements Expression, CallableInvocation {
/// The name of the function being invoked.
final Interpolation name;
/// The arguments to pass to the function.
final ArgumentInvocation arguments;
final FileSpan span;
InterpolatedFunctionExpression(this.name, this.arguments, this.span);
T accept<T>(ExpressionVisitor<T> visitor) =>
visitor.visitInterpolatedFunctionExpression(this);
String toString() => '$name$arguments';
}

View File

@ -5,14 +5,16 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../reference.dart';
/// A Sass variable.
///
/// {@category AST}
@sealed
class VariableExpression implements Expression {
class VariableExpression implements Expression, SassReference {
/// The namespace of the variable being referenced, or `null` if it's
/// referenced without a namespace.
final String? namespace;
@ -22,6 +24,14 @@ class VariableExpression implements Expression {
final FileSpan span;
FileSpan get nameSpan {
if (namespace == null) return span;
return span.withoutNamespace();
}
FileSpan? get namespaceSpan =>
namespace == null ? null : span.initialIdentifier();
VariableExpression(this.name, this.span, {this.namespace});
T accept<T>(ExpressionVisitor<T> visitor) =>

View File

@ -6,13 +6,14 @@ import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../expression/string.dart';
import '../dependency.dart';
import '../import.dart';
/// An import that will load a Sass file at runtime.
///
/// {@category AST}
@sealed
class DynamicImport implements Import {
class DynamicImport implements Import, SassDependency {
/// The URL of the file to import.
///
/// If this is relative, it's relative to the containing file.
@ -30,6 +31,7 @@ class DynamicImport implements Import {
final String urlString;
final FileSpan span;
FileSpan get urlSpan => span;
DynamicImport(this.urlString, this.span);

View File

@ -0,0 +1,33 @@
// 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 'package:source_span/source_span.dart';
import 'node.dart';
/// A common interface for any node that references a Sass member.
///
/// {@category AST}
@sealed
abstract class SassReference extends SassNode {
/// The namespace of the member being referenced, or `null` if it's referenced
/// without a namespace.
String? get namespace;
/// The name of the member being referenced, with underscores converted to
/// hyphens.
///
/// This does not include the `$` for variables.
String get name;
/// The span containing this reference's name.
///
/// For variables, this should include the `$`.
FileSpan get nameSpan;
/// The span containing this reference's namespace, null if [namespace] is
/// null.
FileSpan? get namespaceSpan;
}

View File

@ -6,8 +6,10 @@ import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/statement.dart';
import '../configured_variable.dart';
import '../dependency.dart';
import '../expression/string.dart';
import '../statement.dart';
@ -15,7 +17,7 @@ import '../statement.dart';
///
/// {@category AST}
@sealed
class ForwardRule implements Statement {
class ForwardRule implements Statement, SassDependency {
/// The URI of the module to forward.
///
/// If this is relative, it's relative to the containing file.
@ -74,6 +76,8 @@ class ForwardRule implements Statement {
final FileSpan span;
FileSpan get urlSpan => span.withoutInitialAtRule().initialQuoted();
/// Creates a `@forward` rule that allows all members to be accessed.
ForwardRule(this.url, this.span,
{this.prefix, Iterable<ConfiguredVariable>? configuration})

View File

@ -5,8 +5,10 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/statement.dart';
import '../argument_declaration.dart';
import '../declaration.dart';
import '../statement.dart';
import 'callable_declaration.dart';
import 'silent_comment.dart';
@ -17,7 +19,9 @@ import 'silent_comment.dart';
///
/// {@category AST}
@sealed
class FunctionRule extends CallableDeclaration {
class FunctionRule extends CallableDeclaration implements SassDeclaration {
FileSpan get nameSpan => span.withoutInitialAtRule().initialIdentifier();
FunctionRule(String name, ArgumentDeclaration arguments,
Iterable<Statement> children, FileSpan span,
{SilentComment? comment})

View File

@ -5,10 +5,11 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../utils.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/statement.dart';
import '../argument_invocation.dart';
import '../callable_invocation.dart';
import '../reference.dart';
import '../statement.dart';
import 'content_block.dart';
@ -16,7 +17,7 @@ import 'content_block.dart';
///
/// {@category AST}
@sealed
class IncludeRule implements Statement, CallableInvocation {
class IncludeRule implements Statement, CallableInvocation, SassReference {
/// The namespace of the mixin being invoked, or `null` if it's invoked
/// without a namespace.
final String? namespace;
@ -39,6 +40,22 @@ class IncludeRule implements Statement, CallableInvocation {
? span
: span.file.span(span.start.offset, arguments.span.end.offset).trim();
FileSpan get nameSpan {
var startSpan = span.text.startsWith('+')
? span.subspan(1).trimLeft()
: span.withoutInitialAtRule();
if (namespace != null) startSpan = startSpan.withoutNamespace();
return startSpan.initialIdentifier();
}
FileSpan? get namespaceSpan {
if (namespace == null) return null;
var startSpan = span.text.startsWith('+')
? span.subspan(1).trimLeft()
: span.withoutInitialAtRule();
return startSpan.initialIdentifier();
}
IncludeRule(this.name, this.arguments, this.span,
{this.namespace, this.content});

View File

@ -5,9 +5,11 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/statement.dart';
import '../../../visitor/statement_search.dart';
import '../argument_declaration.dart';
import '../declaration.dart';
import '../statement.dart';
import 'callable_declaration.dart';
import 'content_rule.dart';
@ -19,11 +21,18 @@ import 'silent_comment.dart';
///
/// {@category AST}
@sealed
class MixinRule extends CallableDeclaration {
class MixinRule extends CallableDeclaration implements SassDeclaration {
/// Whether the mixin contains a `@content` rule.
late final bool hasContent =
const _HasContentVisitor().visitMixinRule(this) == true;
FileSpan get nameSpan {
var startSpan = span.text.startsWith('=')
? span.subspan(1).trimLeft()
: span.withoutInitialAtRule();
return startSpan.initialIdentifier();
}
MixinRule(String name, ArgumentDeclaration arguments,
Iterable<Statement> children, FileSpan span,
{SilentComment? comment})

View File

@ -8,8 +8,10 @@ import 'package:source_span/source_span.dart';
import '../../../exception.dart';
import '../../../logger.dart';
import '../../../parse/scss.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/statement.dart';
import '../configured_variable.dart';
import '../dependency.dart';
import '../expression/string.dart';
import '../statement.dart';
@ -17,7 +19,7 @@ import '../statement.dart';
///
/// {@category AST}
@sealed
class UseRule implements Statement {
class UseRule implements Statement, SassDependency {
/// The URI of the module to use.
///
/// If this is relative, it's relative to the containing file.
@ -32,6 +34,8 @@ class UseRule implements Statement {
final FileSpan span;
FileSpan get urlSpan => span.withoutInitialAtRule().initialQuoted();
UseRule(this.url, this.namespace, this.span,
{Iterable<ConfiguredVariable>? configuration})
: configuration = configuration == null

View File

@ -9,8 +9,10 @@ import '../../../exception.dart';
import '../../../logger.dart';
import '../../../parse/scss.dart';
import '../../../utils.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/statement.dart';
import '../expression.dart';
import '../declaration.dart';
import '../statement.dart';
import 'silent_comment.dart';
@ -20,12 +22,12 @@ import 'silent_comment.dart';
///
/// {@category AST}
@sealed
class VariableDeclaration implements Statement {
class VariableDeclaration implements Statement, SassDeclaration {
/// The namespace of the variable being set, or `null` if it's defined or set
/// without a namespace.
final String? namespace;
/// The name of the variable.
/// The name of the variable, with underscores converted to hyphens.
final String name;
/// The comment immediately preceding this declaration.
@ -53,6 +55,15 @@ class VariableDeclaration implements Statement {
/// messages.
String get originalName => declarationName(span);
FileSpan get nameSpan {
var span = this.span;
if (namespace != null) span = span.withoutNamespace();
return span.initialIdentifier(includeLeading: 1);
}
FileSpan? get namespaceSpan =>
namespace == null ? null : span.initialIdentifier();
VariableDeclaration(this.name, this.expression, this.span,
{this.namespace,
bool guarded = false,

View File

@ -128,7 +128,7 @@ class CssParser extends ScssParser {
"This function isn't allowed in plain CSS.", scanner.spanFrom(start));
}
return FunctionExpression(
return InterpolatedFunctionExpression(
// Create a fake interpolation to force the function to be interpreted
// as plain CSS, rather than calling a user-defined function.
Interpolation([StringExpression(identifier)], identifier.span),

View File

@ -479,35 +479,7 @@ class Parser {
/// Consumes an escape sequence and returns the character it represents.
@protected
int escapeCharacter() {
// See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
scanner.expectChar($backslash);
var first = scanner.peekChar();
if (first == null) {
return 0xFFFD;
} else if (isNewline(first)) {
scanner.error("Expected escape sequence.");
} else if (isHex(first)) {
var value = 0;
for (var i = 0; i < 6; i++) {
var next = scanner.peekChar();
if (next == null || !isHex(next)) break;
value = (value << 4) + asHex(scanner.readChar());
}
if (isWhitespace(scanner.peekChar())) scanner.readChar();
if (value == 0 ||
(value >= 0xD800 && value <= 0xDFFF) ||
value >= 0x10FFFF) {
return 0xFFFD;
} else {
return value;
}
} else {
return scanner.readChar();
}
}
int escapeCharacter() => consumeEscapedCharacter(scanner);
// Consumes the next character if it matches [condition].
//

View File

@ -2661,17 +2661,18 @@ abstract class StylesheetParser extends Parser {
namespace: plain);
}
var beforeName = scanner.state;
var name =
Interpolation([_publicIdentifier()], scanner.spanFrom(beforeName));
return FunctionExpression(
name, _argumentInvocation(), scanner.spanFrom(start),
_publicIdentifier(), _argumentInvocation(), scanner.spanFrom(start),
namespace: plain);
case $lparen:
return FunctionExpression(
if (plain == null) {
return InterpolatedFunctionExpression(
identifier, _argumentInvocation(), scanner.spanFrom(start));
} else {
return FunctionExpression(
plain, _argumentInvocation(), scanner.spanFrom(start));
}
default:
return StringExpression(identifier);
@ -2949,8 +2950,10 @@ abstract class StylesheetParser extends Parser {
var contents = _tryUrlContents(start);
if (contents != null) return StringExpression(contents);
return FunctionExpression(Interpolation(["url"], scanner.spanFrom(start)),
_argumentInvocation(), scanner.spanFrom(start));
return InterpolatedFunctionExpression(
Interpolation(["url"], scanner.spanFrom(start)),
_argumentInvocation(),
scanner.spanFrom(start));
}
/// Consumes tokens up to "{", "}", ";", or "!".

94
lib/src/util/span.dart Normal file
View File

@ -0,0 +1,94 @@
// 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:charcode/charcode.dart';
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
import '../utils.dart';
import 'character.dart';
extension SpanExtensions on FileSpan {
/// Returns this span with all whitespace trimmed from both sides.
FileSpan trim() => trimLeft().trimRight();
/// Returns this span with all leading whitespace trimmed.
FileSpan trimLeft() {
var start = 0;
while (isWhitespace(text.codeUnitAt(start))) {
start++;
}
return subspan(start);
}
/// Returns this span with all trailing whitespace trimmed.
FileSpan trimRight() {
var end = text.length - 1;
while (isWhitespace(text.codeUnitAt(end))) {
end--;
}
return subspan(0, end + 1);
}
/// Returns the span of the identifier at the start of this span.
///
/// If [includeLeading] is greater than 0, that many additional characters
/// will be included from the start of this span before looking for an
/// identifier.
FileSpan initialIdentifier({int includeLeading = 0}) {
var scanner = StringScanner(text);
for (var i = 0; i < includeLeading; i++) {
scanner.readChar();
}
_scanIdentifier(scanner);
return subspan(0, scanner.position);
}
/// Returns a subspan excluding the identifier at the start of this span.
FileSpan withoutInitialIdentifier() {
var scanner = StringScanner(text);
_scanIdentifier(scanner);
return subspan(scanner.position);
}
/// Returns a subspan excluding a namespace and `.` at the start of this span.
FileSpan withoutNamespace() => withoutInitialIdentifier().subspan(1);
/// Returns the span of the quoted text at the start of this span.
///
/// This span must start with `"` or `'`.
FileSpan initialQuoted() {
var scanner = StringScanner(text);
var quote = scanner.readChar();
while (true) {
var next = scanner.readChar();
if (next == quote) break;
if (next == $backslash) scanner.readChar();
}
return subspan(0, scanner.position);
}
/// Returns a subspan excluding an initial at-rule and any whitespace after
/// it.
FileSpan withoutInitialAtRule() {
var scanner = StringScanner(text);
scanner.expectChar($at);
_scanIdentifier(scanner);
return subspan(scanner.position).trimLeft();
}
}
/// Consumes an identifier from [scanner].
void _scanIdentifier(StringScanner scanner) {
while (!scanner.isDone) {
var char = scanner.peekChar()!;
if (char == $backslash) {
consumeEscapedCharacter(scanner);
} else if (isName(char)) {
scanner.readChar();
} else {
break;
}
}
}

View File

@ -8,6 +8,7 @@ import 'package:charcode/charcode.dart';
import 'package:collection/collection.dart';
import 'package:source_span/source_span.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:string_scanner/string_scanner.dart';
import 'package:term_glyph/term_glyph.dart' as glyph;
import 'util/character.dart';
@ -383,24 +384,35 @@ Map<K1, Map<K2, V>> copyMapOfMap<K1, K2, V>(Map<K1, Map<K2, V>> map) =>
Map<K, List<E>> copyMapOfList<K, E>(Map<K, List<E>> map) =>
{for (var entry in map.entries) entry.key: entry.value.toList()};
extension SpanExtensions on FileSpan {
/// Returns this span with all whitespace trimmed from both sides.
FileSpan trim() {
var text = this.text;
/// Consumes an escape sequence from [scanner] and returns the character it
/// represents.
int consumeEscapedCharacter(StringScanner scanner) {
// See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
var start = 0;
while (isWhitespace(text.codeUnitAt(start))) {
start++;
scanner.expectChar($backslash);
var first = scanner.peekChar();
if (first == null) {
return 0xFFFD;
} else if (isNewline(first)) {
scanner.error("Expected escape sequence.");
} else if (isHex(first)) {
var value = 0;
for (var i = 0; i < 6; i++) {
var next = scanner.peekChar();
if (next == null || !isHex(next)) break;
value = (value << 4) + asHex(scanner.readChar());
}
if (isWhitespace(scanner.peekChar())) scanner.readChar();
var end = text.length - 1;
while (isWhitespace(text.codeUnitAt(end))) {
end--;
if (value == 0 ||
(value >= 0xD800 && value <= 0xDFFF) ||
value >= 0x10FFFF) {
return 0xFFFD;
} else {
return value;
}
return start == 0 && end == text.length - 1
? this
: file.span(this.start.offset + start, this.start.offset + end + 1);
} else {
return scanner.readChar();
}
}

View File

@ -433,10 +433,8 @@ class _EvaluateVisitor
deprecation: true);
var callableNode = _callableNode!;
var expression = FunctionExpression(
Interpolation([function.text], callableNode.span),
invocation,
callableNode.span);
var expression =
FunctionExpression(function.text, invocation, callableNode.span);
return await expression.accept(this);
}
@ -2227,26 +2225,15 @@ class _EvaluateVisitor
}
Future<Value> visitFunctionExpression(FunctionExpression node) async {
var plainName = node.name.asPlain;
AsyncCallable? function;
if (plainName != null) {
function = _addExceptionSpan(
node,
() => _getFunction(
// If the node has a namespace, the plain name was already
// normalized at parse-time so we don't need to renormalize here.
node.namespace == null
? plainName.replaceAll("_", "-")
: plainName,
namespace: node.namespace));
}
var function = _addExceptionSpan(
node, () => _getFunction(node.name, namespace: node.namespace));
if (function == null) {
if (node.namespace != null) {
throw _exception("Undefined function.", node.span);
}
function = PlainCssCallable(await _performInterpolation(node.name));
function = PlainCssCallable(node.originalName);
}
var oldInFunction = _inFunction;
@ -2257,6 +2244,17 @@ class _EvaluateVisitor
return result;
}
Future<Value> visitInterpolatedFunctionExpression(
InterpolatedFunctionExpression node) async {
var function = PlainCssCallable(await _performInterpolation(node.name));
var oldInFunction = _inFunction;
_inFunction = true;
var result = await _addErrorSpan(
node, () => _runFunctionCallable(node.arguments, function, node));
_inFunction = oldInFunction;
return result;
}
/// Like `_environment.getFunction`, but also returns built-in
/// globally-available functions.
AsyncCallable? _getFunction(String name, {String? namespace}) {

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: ffd96390e470c2feb704a96bc4092c62a8209989
// Checksum: 37fd148527c89ecfa79b51500b589b27a83f542e
//
// ignore_for_file: unused_import
@ -440,10 +440,8 @@ class _EvaluateVisitor
deprecation: true);
var callableNode = _callableNode!;
var expression = FunctionExpression(
Interpolation([function.text], callableNode.span),
invocation,
callableNode.span);
var expression =
FunctionExpression(function.text, invocation, callableNode.span);
return expression.accept(this);
}
@ -2215,26 +2213,15 @@ class _EvaluateVisitor
}
Value visitFunctionExpression(FunctionExpression node) {
var plainName = node.name.asPlain;
Callable? function;
if (plainName != null) {
function = _addExceptionSpan(
node,
() => _getFunction(
// If the node has a namespace, the plain name was already
// normalized at parse-time so we don't need to renormalize here.
node.namespace == null
? plainName.replaceAll("_", "-")
: plainName,
namespace: node.namespace));
}
var function = _addExceptionSpan(
node, () => _getFunction(node.name, namespace: node.namespace));
if (function == null) {
if (node.namespace != null) {
throw _exception("Undefined function.", node.span);
}
function = PlainCssCallable(_performInterpolation(node.name));
function = PlainCssCallable(node.originalName);
}
var oldInFunction = _inFunction;
@ -2245,6 +2232,17 @@ class _EvaluateVisitor
return result;
}
Value visitInterpolatedFunctionExpression(
InterpolatedFunctionExpression node) {
var function = PlainCssCallable(_performInterpolation(node.name));
var oldInFunction = _inFunction;
_inFunction = true;
var result = _addErrorSpan(
node, () => _runFunctionCallable(node.arguments, function, node));
_inFunction = oldInFunction;
return result;
}
/// Like `_environment.getFunction`, but also returns built-in
/// globally-available functions.
Callable? _getFunction(String name, {String? namespace}) {

View File

@ -13,6 +13,7 @@ abstract class ExpressionVisitor<T> {
T visitBinaryOperationExpression(BinaryOperationExpression node);
T visitBooleanExpression(BooleanExpression node);
T visitColorExpression(ColorExpression node);
T visitInterpolatedFunctionExpression(InterpolatedFunctionExpression node);
T visitFunctionExpression(FunctionExpression node);
T visitIfExpression(IfExpression node);
T visitListExpression(ListExpression node);

View File

@ -161,6 +161,11 @@ abstract class RecursiveAstVisitor extends RecursiveStatementVisitor
}
void visitFunctionExpression(FunctionExpression node) {
visitArgumentInvocation(node.arguments);
}
void visitInterpolatedFunctionExpression(
InterpolatedFunctionExpression node) {
visitInterpolation(node.name);
visitArgumentInvocation(node.arguments);
}

View File

@ -1,3 +1,23 @@
## 1.0.0-beta.4
* `UseRule`, `ForwardRule`, and `DynamicImport` now share a common `Dependency`
interface that exposes a `url` getter and a `urlSpan` getter.
* `VariableDeclaration`, `MixinRule`, `FunctionRule`, `Argument`, and
`ConfiguredVariable` now share a common `SassDeclaration` interface that
exposes a `name` getter (with underscores converted to hyphens) and a
`nameSpan` getter.
* Function calls with interpolation have now been split into their own AST node:
`InterpolatedFunctionExpression`. `FunctionExpression.name` is now always a
string (with underscores converted to hyphens). `FunctionExpression` also now
has an `originalName` getter, which leaves underscores as-is.
* `VariableExpression`, `IncludeRule`, and `FunctionExpression` now share a
common `SassReference` interface that exposes a `namespace` getter and a
`name` getter (with underscores converted to hyphens), as well as
corresponding `namespaceSpan` and `nameSpan` getters.
## 1.0.0-beta.3
* No user-visible changes.

View File

@ -2,7 +2,7 @@ name: sass_api
# Note: Every time we add a new Sass AST node, we need to bump the *major*
# version because it's a breaking change for anyone who's implementing the
# visitor interface(s).
version: 1.0.0-beta.3
version: 1.0.0-beta.4
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass
@ -10,7 +10,7 @@ environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
sass: 1.38.0
sass: 1.38.1
dependency_overrides:
sass: {path: ../..}

View File

@ -1,5 +1,5 @@
name: sass
version: 1.38.0
version: 1.38.1
description: A Sass implementation in Dart.
homepage: https://github.com/sass/dart-sass