Merge pull request #52 from sass/argument-list

Add support for argument lists
This commit is contained in:
Natalie Weizenbaum 2021-08-23 16:34:56 -07:00 committed by GitHub
commit 0ed8d89a88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 583 additions and 23 deletions

View File

@ -1,3 +1,10 @@
## 1.0.0-beta.10
* Support version 1.0.0-beta.12 of the Sass embedded protocol:
* Support `Value.ArgumentList`.
* Support slash-separated lists.
## 1.0.0-beta.9
* No user-visible changes.

View File

@ -10,8 +10,8 @@ import 'package:sass/sass.dart' as sass;
import 'dispatcher.dart';
import 'embedded_sass.pb.dart';
import 'function_registry.dart';
import 'protofier.dart';
import 'utils.dart';
import 'value.dart';
/// Returns a Sass callable that invokes a function defined on the host with the
/// given [signature].
@ -41,11 +41,11 @@ sass.Callable hostCallable(Dispatcher dispatcher, FunctionRegistry functions,
return sass.Callable.function(
name, signature.substring(openParen + 1, signature.length - 1),
(arguments) {
var protofier = Protofier(dispatcher, functions, compilationId);
var request = OutboundMessage_FunctionCallRequest()
..compilationId = compilationId
..arguments.addAll([
for (var argument in arguments) protofyValue(functions, argument)
]);
..arguments.addAll(
[for (var argument in arguments) protofier.protofy(argument)]);
if (id != null) {
request.functionId = id;
@ -57,8 +57,7 @@ sass.Callable hostCallable(Dispatcher dispatcher, FunctionRegistry functions,
try {
switch (response.whichResult()) {
case InboundMessage_FunctionCallResponse_Result.success:
return deprotofyValue(
dispatcher, functions, compilationId, response.success);
return protofier.deprotofyResponse(response);
case InboundMessage_FunctionCallResponse_Result.error:
throw response.error;

274
lib/src/protofier.dart Normal file
View File

@ -0,0 +1,274 @@
// Copyright 2019 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:sass/sass.dart' as sass;
import 'dispatcher.dart';
import 'embedded_sass.pb.dart';
import 'function_registry.dart';
import 'host_callable.dart';
import 'utils.dart';
/// A class that converts Sass [sass.Value] objects into [Value] protobufs.
///
/// A given [Protofier] instance is valid only within the scope of a single
/// custom function call.
class Protofier {
/// The dispatcher, for invoking deprotofied [Value_HostFunction]s.
final Dispatcher _dispatcher;
/// The IDs of first-class functions.
final FunctionRegistry _functions;
/// The ID of the current compilation.
final int _compilationId;
/// Any argument lists transitively contained in [value].
///
/// The IDs of the [Value_ArgumentList] protobufs are always one greater than
/// the index of the corresponding list in this array (since 0 is reserved for
/// argument lists created by the host).
final _argumentLists = <sass.SassArgumentList>[];
/// Creates a [Protofier] that's valid within the scope of a single custom
/// function call.
///
/// The [functions] tracks the IDs of first-class functions so that the host
/// can pass them back to the compiler.
Protofier(this._dispatcher, this._functions, this._compilationId);
/// Converts [value] to its protocol buffer representation.
Value protofy(sass.Value value) {
var result = Value();
if (value is sass.SassString) {
result.string = Value_String()
..text = value.text
..quoted = value.hasQuotes;
} else if (value is sass.SassNumber) {
var number = Value_Number()..value = value.value * 1.0;
number.numerators.addAll(value.numeratorUnits);
number.denominators.addAll(value.denominatorUnits);
result.number = number;
} else if (value is sass.SassColor) {
// TODO(nweiz): If the color is represented as HSL internally, this coerces
// it to RGB. Is it worth providing some visibility into its internal
// representation so we can serialize without converting?
result.rgbColor = Value_RgbColor()
..red = value.red
..green = value.green
..blue = value.blue
..alpha = value.alpha * 1.0;
} else if (value is sass.SassArgumentList) {
_argumentLists.add(value);
var argList = Value_ArgumentList()
..id = _argumentLists.length
..separator = _protofySeparator(value.separator)
..contents.addAll([for (var element in value.asList) protofy(element)]);
value.keywordsWithoutMarking.forEach((key, value) {
argList.keywords[key] = protofy(value);
});
result.argumentList = argList;
} else if (value is sass.SassList) {
result.list = Value_List()
..separator = _protofySeparator(value.separator)
..hasBrackets = value.hasBrackets
..contents.addAll([for (var element in value.asList) protofy(element)]);
} else if (value is sass.SassMap) {
var map = Value_Map();
value.contents.forEach((key, value) {
map.entries.add(Value_Map_Entry()
..key = protofy(key)
..value = protofy(value));
});
result.map = map;
} else if (value is sass.SassFunction) {
result.compilerFunction = _functions.protofy(value);
} else if (value == sass.sassTrue) {
result.singleton = SingletonValue.TRUE;
} else if (value == sass.sassFalse) {
result.singleton = SingletonValue.FALSE;
} else if (value == sass.sassNull) {
result.singleton = SingletonValue.NULL;
} else {
throw "Unknown Value $value";
}
return result;
}
/// Converts [separator] to its protocol buffer representation.
ListSeparator _protofySeparator(sass.ListSeparator separator) {
switch (separator) {
case sass.ListSeparator.comma:
return ListSeparator.COMMA;
case sass.ListSeparator.space:
return ListSeparator.SPACE;
case sass.ListSeparator.slash:
return ListSeparator.SLASH;
case sass.ListSeparator.undecided:
return ListSeparator.UNDECIDED;
default:
throw "Unknown ListSeparator $separator";
}
}
/// Converts [response]'s return value to its Sass representation.
sass.Value deprotofyResponse(InboundMessage_FunctionCallResponse response) {
for (var id in response.accessedArgumentLists) {
// Mark the `keywords` field as accessed.
_argumentListForId(id).keywords;
}
return _deprotofy(response.success);
}
/// Converts [value] to its Sass representation.
sass.Value _deprotofy(Value value) {
try {
switch (value.whichValue()) {
case Value_Value.string:
return value.string.text.isEmpty
? sass.SassString.empty(quotes: value.string.quoted)
: sass.SassString(value.string.text, quotes: value.string.quoted);
case Value_Value.number:
return sass.SassNumber.withUnits(value.number.value,
numeratorUnits: value.number.numerators,
denominatorUnits: value.number.denominators);
case Value_Value.rgbColor:
return sass.SassColor.rgb(value.rgbColor.red, value.rgbColor.green,
value.rgbColor.blue, value.rgbColor.alpha);
case Value_Value.hslColor:
return sass.SassColor.hsl(
value.hslColor.hue,
value.hslColor.saturation,
value.hslColor.lightness,
value.hslColor.alpha);
case Value_Value.argumentList:
if (value.argumentList.id != 0) {
return _argumentListForId(value.argumentList.id);
}
var separator = _deprotofySeparator(value.argumentList.separator);
var length = value.argumentList.contents.length;
if (separator == sass.ListSeparator.undecided && length > 1) {
throw paramsError(
"List $value can't have an undecided separator because it has "
"$length elements");
}
return sass.SassArgumentList([
for (var element in value.argumentList.contents) _deprotofy(element)
], {
for (var entry in value.argumentList.keywords.entries)
entry.key: _deprotofy(entry.value)
}, separator);
case Value_Value.list:
var separator = _deprotofySeparator(value.list.separator);
if (value.list.contents.isEmpty) {
return sass.SassList.empty(
separator: separator, brackets: value.list.hasBrackets);
}
var length = value.list.contents.length;
if (separator == sass.ListSeparator.undecided && length > 1) {
throw paramsError(
"List $value can't have an undecided separator because it has "
"$length elements");
}
return sass.SassList([
for (var element in value.list.contents) _deprotofy(element)
], separator, brackets: value.list.hasBrackets);
case Value_Value.map:
return value.map.entries.isEmpty
? const sass.SassMap.empty()
: sass.SassMap({
for (var entry in value.map.entries)
_deprotofy(entry.key): _deprotofy(entry.value)
});
case Value_Value.compilerFunction:
var id = value.compilerFunction.id;
var function = _functions[id];
if (function == null) {
throw paramsError(
"CompilerFunction.id $id doesn't match any known functions");
}
return function;
case Value_Value.hostFunction:
return sass.SassFunction(hostCallable(_dispatcher, _functions,
_compilationId, value.hostFunction.signature,
id: value.hostFunction.id));
case Value_Value.singleton:
switch (value.singleton) {
case SingletonValue.TRUE:
return sass.sassTrue;
case SingletonValue.FALSE:
return sass.sassFalse;
case SingletonValue.NULL:
return sass.sassNull;
default:
throw "Unknown Value.singleton ${value.singleton}";
}
case Value_Value.notSet:
throw mandatoryError("Value.value");
}
} on RangeError catch (error) {
var name = error.name;
if (name == null || error.start == null || error.end == null) {
throw paramsError(error.toString());
}
if (value.whichValue() == Value_Value.rgbColor) {
name = 'RgbColor.$name';
} else if (value.whichValue() == Value_Value.hslColor) {
name = 'HslColor.$name';
}
throw paramsError(
'$name must be between ${error.start} and ${error.end}, was '
'${error.invalidValue}');
}
}
/// Returns the argument list in [_argumentLists] that corresponds to [id].
sass.SassArgumentList _argumentListForId(int id) {
if (id < 1) {
throw paramsError(
"Value.ArgumentList.id $id can't be marked as accessed");
} else if (id > _argumentLists.length) {
throw paramsError(
"Value.ArgumentList.id $id doesn't match any known argument "
"lists");
} else {
return _argumentLists[id - 1];
}
}
/// Converts [separator] to its Sass representation.
sass.ListSeparator _deprotofySeparator(ListSeparator separator) {
switch (separator) {
case ListSeparator.COMMA:
return sass.ListSeparator.comma;
case ListSeparator.SPACE:
return sass.ListSeparator.space;
case ListSeparator.SLASH:
return sass.ListSeparator.slash;
case ListSeparator.UNDECIDED:
return sass.ListSeparator.undecided;
default:
throw "Unknown separator $separator";
}
}
}

View File

@ -70,6 +70,8 @@ ListSeparator _protofySeparator(sass.ListSeparator separator) {
return ListSeparator.COMMA;
case sass.ListSeparator.space:
return ListSeparator.SPACE;
case sass.ListSeparator.slash:
return ListSeparator.SLASH;
case sass.ListSeparator.undecided:
return ListSeparator.UNDECIDED;
default:
@ -190,6 +192,8 @@ sass.ListSeparator _deprotofySeparator(ListSeparator separator) {
return sass.ListSeparator.comma;
case ListSeparator.SPACE:
return sass.ListSeparator.space;
case ListSeparator.SLASH:
return sass.ListSeparator.slash;
case ListSeparator.UNDECIDED:
return sass.ListSeparator.undecided;
default:

View File

@ -1,5 +1,5 @@
name: sass_embedded
version: 1.0.0-beta.9
version: 1.0.0-beta.10
description: An implementation of the Sass embedded protocol using Dart Sass.
author: Sass Team
homepage: https://github.com/sass/dart-sass-embedded
@ -14,7 +14,7 @@ dependencies:
async: ">=1.13.0 <3.0.0"
meta: ^1.1.0
protobuf: ^2.0.0
sass: ^1.36.0
sass: ^1.38.0
source_span: ^1.1.0
stack_trace: ^1.6.0
stream_channel: ">=1.6.0 <3.0.0"

View File

@ -133,22 +133,74 @@ void main() {
await _process.kill();
});
test("from argument lists", () async {
_process.inbound.add(compileString("a {b: foo(true, false, null)}",
functions: [r"foo($arg, $args...)"]));
var request = getFunctionCallRequest(await _process.outbound.next);
group("from argument lists", () {
test("with no named arguments", () async {
_process.inbound.add(compileString("a {b: foo(true, false, null)}",
functions: [r"foo($arg, $args...)"]));
var request = getFunctionCallRequest(await _process.outbound.next);
expect(
request.arguments,
equals([
_true,
Value()
..list = (Value_List()
..separator = ListSeparator.COMMA
..hasBrackets = false
..contents.addAll([_false, _null]))
]));
await _process.kill();
expect(
request.arguments,
equals([
_true,
Value()
..argumentList = (Value_ArgumentList()
..id = 1
..separator = ListSeparator.COMMA
..contents.addAll([_false, _null]))
]));
await _process.kill();
});
test("with named arguments", () async {
_process.inbound.add(compileString(r"a {b: foo(true, $arg: false)}",
functions: [r"foo($args...)"]));
var request = getFunctionCallRequest(await _process.outbound.next);
expect(
request.arguments,
equals([
Value()
..argumentList = (Value_ArgumentList()
..id = 1
..separator = ListSeparator.COMMA
..contents.addAll([_true])
..keywords.addAll({"arg": _false}))
]));
await _process.kill();
});
test("throws if named arguments are unused", () async {
_process.inbound.add(compileString(r"a {b: foo($arg: false)}",
functions: [r"foo($args...)"]));
var request = getFunctionCallRequest(await _process.outbound.next);
_process.inbound.add(InboundMessage()
..functionCallResponse = (InboundMessage_FunctionCallResponse()
..id = request.id
..success = _true));
var failure = getCompileFailure(await _process.outbound.next);
expect(failure.message, equals(r"No argument named $arg."));
await _process.kill();
});
test("doesn't throw if named arguments are used", () async {
_process.inbound.add(compileString(r"a {b: foo($arg: false)}",
functions: [r"foo($args...)"]));
var request = getFunctionCallRequest(await _process.outbound.next);
_process.inbound.add(InboundMessage()
..functionCallResponse = (InboundMessage_FunctionCallResponse()
..id = request.id
..accessedArgumentLists
.add(request.arguments.first.argumentList.id)
..success = _true));
await expectLater(_process.outbound,
emits(isSuccess(equals("a {\n b: true;\n}"))));
await _process.kill();
});
});
});
});
@ -497,6 +549,14 @@ void main() {
expect(list.hasBrackets, isTrue);
expect(list.separator, equals(ListSeparator.SPACE));
});
test("with a slash separator", () async {
var list =
(await _protofy(r"list.join([], [], $separator: slash)")).list;
expect(list.contents, isEmpty);
expect(list.hasBrackets, isTrue);
expect(list.separator, equals(ListSeparator.SLASH));
});
});
group("without brackets", () {
@ -522,6 +582,14 @@ void main() {
expect(list.hasBrackets, isFalse);
expect(list.separator, equals(ListSeparator.SPACE));
});
test("with a slash separator", () async {
var list =
(await _protofy(r"list.join((), (), $separator: slash)")).list;
expect(list.contents, isEmpty);
expect(list.hasBrackets, isFalse);
expect(list.separator, equals(ListSeparator.SLASH));
});
});
});
@ -549,6 +617,15 @@ void main() {
expect(list.hasBrackets, isTrue);
expect(list.separator, equals(ListSeparator.SPACE));
});
test("with a slash separator", () async {
var list =
(await _protofy(r"list.join([true], [], $separator: slash)"))
.list;
expect(list.contents, equals([_true]));
expect(list.hasBrackets, isTrue);
expect(list.separator, equals(ListSeparator.SLASH));
});
});
group("without brackets", () {
@ -567,6 +644,15 @@ void main() {
expect(list.hasBrackets, isFalse);
expect(list.separator, equals(ListSeparator.SPACE));
});
test("with a slash separator", () async {
var list =
(await _protofy(r"list.join(true, (), $separator: slash)"))
.list;
expect(list.contents, equals([_true]));
expect(list.hasBrackets, isFalse);
expect(list.separator, equals(ListSeparator.SLASH));
});
});
});
@ -601,10 +687,59 @@ void main() {
expect(list.hasBrackets, isFalse);
expect(list.separator, equals(ListSeparator.SPACE));
});
test("with a slash separator", () async {
var list = (await _protofy(r"list.slash(true, null, false)")).list;
expect(list.contents, equals([_true, _null, _false]));
expect(list.hasBrackets, isFalse);
expect(list.separator, equals(ListSeparator.SLASH));
});
});
});
});
group("an argument list", () {
test("that's empty", () async {
var list = (await _protofy(r"capture-args()")).argumentList;
expect(list.contents, isEmpty);
expect(list.keywords, isEmpty);
expect(list.separator, equals(ListSeparator.COMMA));
});
test("with arguments", () async {
var list =
(await _protofy(r"capture-args(true, null, false)")).argumentList;
expect(list.contents, [_true, _null, _false]);
expect(list.keywords, isEmpty);
expect(list.separator, equals(ListSeparator.COMMA));
});
test("with a space separator", () async {
var list =
(await _protofy(r"capture-args(true null false...)")).argumentList;
expect(list.contents, [_true, _null, _false]);
expect(list.keywords, isEmpty);
expect(list.separator, equals(ListSeparator.SPACE));
});
test("with a slash separator", () async {
var list =
(await _protofy(r"capture-args(list.slash(true, null, false)...)"))
.argumentList;
expect(list.contents, [_true, _null, _false]);
expect(list.keywords, isEmpty);
expect(list.separator, equals(ListSeparator.SLASH));
});
test("with keywords", () async {
var list = (await _protofy(r"capture-args($arg1: true, $arg2: false)"))
.argumentList;
expect(list.contents, isEmpty);
expect(list.keywords, equals({"arg1": _true, "arg2": _false}));
expect(list.separator, equals(ListSeparator.COMMA));
});
});
group("a map", () {
test("with no elements", () async {
expect((await _protofy("map.remove((1: 2), 1)")).map.entries, isEmpty);
@ -895,6 +1030,15 @@ void main() {
..separator = ListSeparator.SPACE),
"[]");
});
group("with a slash separator", () {
_testSerializationAndRoundTrip(
Value()
..list = (Value_List()
..hasBrackets = true
..separator = ListSeparator.SLASH),
"[]");
});
});
group("without brackets", () {
@ -927,6 +1071,16 @@ void main() {
"()",
inspect: true);
});
group("with a slash separator", () {
_testSerializationAndRoundTrip(
Value()
..list = (Value_List()
..hasBrackets = false
..separator = ListSeparator.SLASH),
"()",
inspect: true);
});
});
});
@ -963,6 +1117,16 @@ void main() {
..separator = ListSeparator.SPACE),
"[true]");
});
group("with a slash separator", () {
_testSerializationAndRoundTrip(
Value()
..list = (Value_List()
..contents.add(_true)
..hasBrackets = true
..separator = ListSeparator.SLASH),
"[true]");
});
});
group("without brackets", () {
@ -997,6 +1161,16 @@ void main() {
..separator = ListSeparator.SPACE),
"true");
});
group("with a slash separator", () {
_testSerializationAndRoundTrip(
Value()
..list = (Value_List()
..contents.add(_true)
..hasBrackets = false
..separator = ListSeparator.SLASH),
"true");
});
});
});
@ -1025,6 +1199,18 @@ void main() {
inspect: true),
"[true null false]");
});
test("with a slash separator", () async {
expect(
await _deprotofy(
Value()
..list = (Value_List()
..contents.addAll([_true, _null, _false])
..hasBrackets = true
..separator = ListSeparator.SLASH),
inspect: true),
"[true / null / false]");
});
});
group("without brackets", () {
@ -1051,10 +1237,87 @@ void main() {
inspect: true),
"true null false");
});
test("with a slash separator", () async {
expect(
await _deprotofy(
Value()
..list = (Value_List()
..contents.addAll([_true, _null, _false])
..hasBrackets = false
..separator = ListSeparator.SLASH),
inspect: true),
"true / null / false");
});
});
});
});
group("an argument list", () {
test("with no elements", () async {
expect(
await _roundTrip(Value()
..argumentList =
(Value_ArgumentList()..separator = ListSeparator.UNDECIDED)),
equals(Value()
..argumentList = (Value_ArgumentList()
..id = 1
..separator = ListSeparator.UNDECIDED)));
});
test("with comma separator", () async {
expect(
await _roundTrip(Value()
..argumentList = (Value_ArgumentList()
..contents.addAll([_true, _false, _null])
..separator = ListSeparator.COMMA)),
equals(Value()
..argumentList = (Value_ArgumentList()
..id = 1
..contents.addAll([_true, _false, _null])
..separator = ListSeparator.COMMA)));
});
test("with space separator", () async {
expect(
await _roundTrip(Value()
..argumentList = (Value_ArgumentList()
..contents.addAll([_true, _false, _null])
..separator = ListSeparator.SPACE)),
equals(Value()
..argumentList = (Value_ArgumentList()
..id = 1
..contents.addAll([_true, _false, _null])
..separator = ListSeparator.SPACE)));
});
test("with slash separator", () async {
expect(
await _roundTrip(Value()
..argumentList = (Value_ArgumentList()
..contents.addAll([_true, _false, _null])
..separator = ListSeparator.SLASH)),
equals(Value()
..argumentList = (Value_ArgumentList()
..id = 1
..contents.addAll([_true, _false, _null])
..separator = ListSeparator.SLASH)));
});
test("with keywords", () async {
expect(
await _roundTrip(Value()
..argumentList = (Value_ArgumentList()
..keywords.addAll({"arg1": _true, "arg2": _false})
..separator = ListSeparator.COMMA)),
equals(Value()
..argumentList = (Value_ArgumentList()
..id = 1
..keywords.addAll({"arg1": _true, "arg2": _false})
..separator = ListSeparator.COMMA)));
});
});
group("a map", () {
group("with no elements", () {
_testSerializationAndRoundTrip(Value()..map = Value_Map(), "()",
@ -1185,6 +1448,13 @@ void main() {
endsWith("can't have an undecided separator because it has 2 "
"elements"));
});
test("an arglist with an unknown id", () async {
await _expectDeprotofyError(
Value()..argumentList = (Value_ArgumentList()..id = 1),
equals(
"Value.ArgumentList.id 1 doesn't match any known argument lists"));
});
});
});
}
@ -1196,6 +1466,12 @@ Future<Value> _protofy(String sassScript) async {
@use 'sass:list';
@use 'sass:map';
@use 'sass:math';
@use 'sass:meta';
@function capture-args(\$args...) {
\$_: meta.keywords(\$args);
@return \$args;
}
\$_: foo(($sassScript));
""", functions: [r"foo($arg)"]));