Skip to content

Commit

Permalink
Merge pull request #2464 from sass/rest-param-comma
Browse files Browse the repository at this point in the history
Allow a trailing comma after rest parameters and arguments
  • Loading branch information
nex3 authored Dec 12, 2024
2 parents 219fe67 + 0230ccf commit f38dbb0
Show file tree
Hide file tree
Showing 39 changed files with 338 additions and 346 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## 1.82.1-dev
## 1.83.0

* No user-visible changes.
* Allow trailing commas in *all* argument and parameter lists.

## 1.82.0

Expand Down
6 changes: 3 additions & 3 deletions lib/src/ast/sass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

export 'sass/argument.dart';
export 'sass/argument_declaration.dart';
export 'sass/argument_invocation.dart';
export 'sass/argument_list.dart';
export 'sass/at_root_query.dart';
export 'sass/callable_invocation.dart';
export 'sass/configured_variable.dart';
Expand Down Expand Up @@ -33,6 +31,8 @@ export 'sass/import/dynamic.dart';
export 'sass/import/static.dart';
export 'sass/interpolation.dart';
export 'sass/node.dart';
export 'sass/parameter.dart';
export 'sass/parameter_list.dart';
export 'sass/reference.dart';
export 'sass/statement.dart';
export 'sass/statement/at_root_rule.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'node.dart';
/// A set of arguments passed in to a function or mixin.
///
/// {@category AST}
final class ArgumentInvocation implements SassNode {
final class ArgumentList implements SassNode {
/// The arguments passed by position.
final List<Expression> positional;

Expand All @@ -31,7 +31,7 @@ final class ArgumentInvocation implements SassNode {
/// Returns whether this invocation passes no arguments.
bool get isEmpty => positional.isEmpty && named.isEmpty && rest == null;

ArgumentInvocation(
ArgumentList(
Iterable<Expression> positional, Map<String, Expression> named, this.span,
{this.rest, this.keywordRest})
: positional = List.unmodifiable(positional),
Expand All @@ -40,7 +40,7 @@ final class ArgumentInvocation implements SassNode {
}

/// Creates an invocation that passes no arguments.
ArgumentInvocation.empty(this.span)
ArgumentList.empty(this.span)
: positional = const [],
named = const {},
rest = null,
Expand Down
4 changes: 2 additions & 2 deletions lib/src/ast/sass/callable_invocation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import 'package:meta/meta.dart';

import 'argument_invocation.dart';
import 'argument_list.dart';
import 'node.dart';

/// An abstract class for invoking a callable (a function or mixin).
Expand All @@ -13,5 +13,5 @@ import 'node.dart';
@sealed
abstract class CallableInvocation implements SassNode {
/// The arguments passed to the callable.
ArgumentInvocation get arguments;
ArgumentList get arguments;
}
4 changes: 2 additions & 2 deletions lib/src/ast/sass/expression/function.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:source_span/source_span.dart';
import '../../../util/span.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../argument_invocation.dart';
import '../argument_list.dart';
import '../callable_invocation.dart';
import '../reference.dart';

Expand All @@ -33,7 +33,7 @@ final class FunctionExpression extends Expression
final String originalName;

/// The arguments to pass to the function.
final ArgumentInvocation arguments;
final ArgumentList arguments;

final FileSpan span;

Expand Down
6 changes: 3 additions & 3 deletions lib/src/ast/sass/expression/if.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import '../../../visitor/interface/expression.dart';
/// {@category AST}
final class IfExpression extends Expression implements CallableInvocation {
/// The declaration of `if()`, as though it were a normal function.
static final declaration = ArgumentDeclaration.parse(
r"@function if($condition, $if-true, $if-false) {");
static final declaration =
ParameterList.parse(r"@function if($condition, $if-true, $if-false) {");

/// The arguments passed to `if()`.
final ArgumentInvocation arguments;
final ArgumentList arguments;

final FileSpan span;

Expand Down
4 changes: 2 additions & 2 deletions lib/src/ast/sass/expression/interpolated_function.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:source_span/source_span.dart';

import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../argument_invocation.dart';
import '../argument_list.dart';
import '../callable_invocation.dart';
import '../interpolation.dart';

Expand All @@ -21,7 +21,7 @@ final class InterpolatedFunctionExpression extends Expression
final Interpolation name;

/// The arguments to pass to the function.
final ArgumentInvocation arguments;
final ArgumentList arguments;

final FileSpan span;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import 'expression.dart';
import 'declaration.dart';
import 'node.dart';

/// An argument declared as part of an [ArgumentDeclaration].
/// An parameter declared as part of an [ParameterList].
///
/// {@category AST}
final class Argument implements SassNode, SassDeclaration {
/// The argument name.
final class Parameter implements SassNode, SassDeclaration {
/// The parameter name.
final String name;

/// The default value of this argument, or `null` if none was declared.
/// The default value of this parameter, or `null` if none was declared.
final Expression? defaultValue;

final FileSpan span;
Expand All @@ -33,7 +33,7 @@ final class Argument implements SassNode, SassDeclaration {
FileSpan get nameSpan =>
defaultValue == null ? span : span.initialIdentifier(includeLeading: 1);

Argument(this.name, this.span, {this.defaultValue});
Parameter(this.name, this.span, {this.defaultValue});

String toString() => defaultValue == null ? name : "$name: $defaultValue";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ import '../../parse/scss.dart';
import '../../util/character.dart';
import '../../util/span.dart';
import '../../utils.dart';
import 'argument.dart';
import 'parameter.dart';
import 'node.dart';

/// An argument declaration, as for a function or mixin definition.
/// An parameter declaration, as for a function or mixin definition.
///
/// {@category AST}
/// {@category Parsing}
final class ArgumentDeclaration implements SassNode {
/// The arguments that are taken.
final List<Argument> arguments;
final class ParameterList implements SassNode {
/// The parameters that are taken.
final List<Parameter> parameters;

/// The name of the rest argument (as in `$args...`), or `null` if none was
/// The name of the rest parameter (as in `$args...`), or `null` if none was
/// declared.
final String? restArgument;
final String? restParameter;

final FileSpan span;

Expand All @@ -31,7 +31,7 @@ final class ArgumentDeclaration implements SassNode {
FileSpan get spanWithName {
var text = span.file.getText(0);

// Move backwards through any whitespace between the name and the arguments.
// Move backwards through any whitespace between the name and the parameters.
var i = span.start.offset - 1;
while (i > 0 && text.codeUnitAt(i).isWhitespace) {
i--;
Expand All @@ -48,60 +48,59 @@ final class ArgumentDeclaration implements SassNode {
if (!text.codeUnitAt(i + 1).isNameStart) return span;

// Trim because it's possible that this span is empty (for example, a mixin
// may be declared without an argument list).
// may be declared without an parameter list).
return span.file.span(i + 1, span.end.offset).trim();
}

/// Returns whether this declaration takes no arguments.
bool get isEmpty => arguments.isEmpty && restArgument == null;
/// Returns whether this declaration takes no parameters.
bool get isEmpty => parameters.isEmpty && restParameter == null;

ArgumentDeclaration(Iterable<Argument> arguments, this.span,
{this.restArgument})
: arguments = List.unmodifiable(arguments);
ParameterList(Iterable<Parameter> parameters, this.span, {this.restParameter})
: parameters = List.unmodifiable(parameters);

/// Creates a declaration that declares no arguments.
ArgumentDeclaration.empty(this.span)
: arguments = const [],
restArgument = null;
/// Creates a declaration that declares no parameters.
ParameterList.empty(this.span)
: parameters = const [],
restParameter = null;

/// Parses an argument declaration from [contents], which should be of the
/// Parses an parameter declaration from [contents], which should be of the
/// form `@rule name(args) {`.
///
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
factory ArgumentDeclaration.parse(String contents, {Object? url}) =>
ScssParser(contents, url: url).parseArgumentDeclaration();
factory ParameterList.parse(String contents, {Object? url}) =>
ScssParser(contents, url: url).parseParameterList();

/// Throws a [SassScriptException] if [positional] and [names] aren't valid
/// for this argument declaration.
/// for this parameter declaration.
void verify(int positional, Set<String> names) {
var namedUsed = 0;
for (var i = 0; i < arguments.length; i++) {
var argument = arguments[i];
for (var i = 0; i < parameters.length; i++) {
var parameter = parameters[i];
if (i < positional) {
if (names.contains(argument.name)) {
if (names.contains(parameter.name)) {
throw SassScriptException(
"Argument ${_originalArgumentName(argument.name)} was passed "
"Argument ${_originalParameterName(parameter.name)} was passed "
"both by position and by name.");
}
} else if (names.contains(argument.name)) {
} else if (names.contains(parameter.name)) {
namedUsed++;
} else if (argument.defaultValue == null) {
} else if (parameter.defaultValue == null) {
throw MultiSpanSassScriptException(
"Missing argument ${_originalArgumentName(argument.name)}.",
"Missing argument ${_originalParameterName(parameter.name)}.",
"invocation",
{spanWithName: "declaration"});
}
}

if (restArgument != null) return;
if (restParameter != null) return;

if (positional > arguments.length) {
if (positional > parameters.length) {
throw MultiSpanSassScriptException(
"Only ${arguments.length} "
"Only ${parameters.length} "
"${names.isEmpty ? '' : 'positional '}"
"${pluralize('argument', arguments.length)} allowed, but "
"${pluralize('argument', parameters.length)} allowed, but "
"$positional ${pluralize('was', positional, plural: 'were')} "
"passed.",
"invocation",
Expand All @@ -110,54 +109,54 @@ final class ArgumentDeclaration implements SassNode {

if (namedUsed < names.length) {
var unknownNames = Set.of(names)
..removeAll(arguments.map((argument) => argument.name));
..removeAll(parameters.map((parameter) => parameter.name));
throw MultiSpanSassScriptException(
"No ${pluralize('argument', unknownNames.length)} named "
"No ${pluralize('parameter', unknownNames.length)} named "
"${toSentence(unknownNames.map((name) => "\$$name"), 'or')}.",
"invocation",
{spanWithName: "declaration"});
}
}

/// Returns the argument named [name] with a leading `$` and its original
/// Returns the parameter named [name] with a leading `$` and its original
/// underscores (which are otherwise converted to hyphens).
String _originalArgumentName(String name) {
if (name == restArgument) {
String _originalParameterName(String name) {
if (name == restParameter) {
var text = span.text;
var fromDollar = text.substring(text.lastIndexOf("\$"));
return fromDollar.substring(0, text.indexOf("."));
}

for (var argument in arguments) {
if (argument.name == name) return argument.originalName;
for (var parameter in parameters) {
if (parameter.name == name) return parameter.originalName;
}

throw ArgumentError('This declaration has no argument named "\$$name".');
throw ArgumentError('This declaration has no parameter named "\$$name".');
}

/// Returns whether [positional] and [names] are valid for this argument
/// Returns whether [positional] and [names] are valid for this parameter
/// declaration.
bool matches(int positional, Set<String> names) {
var namedUsed = 0;
for (var i = 0; i < arguments.length; i++) {
var argument = arguments[i];
for (var i = 0; i < parameters.length; i++) {
var parameter = parameters[i];
if (i < positional) {
if (names.contains(argument.name)) return false;
} else if (names.contains(argument.name)) {
if (names.contains(parameter.name)) return false;
} else if (names.contains(parameter.name)) {
namedUsed++;
} else if (argument.defaultValue == null) {
} else if (parameter.defaultValue == null) {
return false;
}
}

if (restArgument != null) return true;
if (positional > arguments.length) return false;
if (restParameter != null) return true;
if (positional > parameters.length) return false;
if (namedUsed < names.length) return false;
return true;
}

String toString() => [
for (var arg in arguments) '\$$arg',
if (restArgument != null) '\$$restArgument...'
for (var arg in parameters) '\$$arg',
if (restParameter != null) '\$$restParameter...'
].join(', ');
}
8 changes: 4 additions & 4 deletions lib/src/ast/sass/statement/callable_declaration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import 'package:source_span/source_span.dart';

import '../argument_declaration.dart';
import '../parameter_list.dart';
import '../statement.dart';
import 'parent.dart';
import 'silent_comment.dart';
Expand All @@ -24,12 +24,12 @@ abstract base class CallableDeclaration
/// The comment immediately preceding this declaration.
final SilentComment? comment;

/// The declared arguments this callable accepts.
final ArgumentDeclaration arguments;
/// The declared parameters this callable accepts.
final ParameterList parameters;

final FileSpan span;

CallableDeclaration(this.originalName, this.arguments,
CallableDeclaration(this.originalName, this.parameters,
Iterable<Statement> children, this.span,
{this.comment})
: name = originalName.replaceAll('_', '-'),
Expand Down
10 changes: 5 additions & 5 deletions lib/src/ast/sass/statement/content_block.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import 'package:source_span/source_span.dart';

import '../../../visitor/interface/statement.dart';
import '../statement.dart';
import '../argument_declaration.dart';
import '../parameter_list.dart';
import 'callable_declaration.dart';

/// An anonymous block of code that's invoked for a [ContentRule].
///
/// {@category AST}
final class ContentBlock extends CallableDeclaration {
ContentBlock(ArgumentDeclaration arguments, Iterable<Statement> children,
FileSpan span)
: super("@content", arguments, children, span);
ContentBlock(
ParameterList parameters, Iterable<Statement> children, FileSpan span)
: super("@content", parameters, children, span);

T accept<T>(StatementVisitor<T> visitor) => visitor.visitContentBlock(this);

String toString() =>
(arguments.isEmpty ? "" : " using ($arguments)") +
(parameters.isEmpty ? "" : " using ($parameters)") +
" {${children.join(' ')}}";
}
Loading

0 comments on commit f38dbb0

Please sign in to comment.