Skip to content
This repository has been archived by the owner on May 15, 2023. It is now read-only.

Do not throw ProtocolErrorType.PARAMS for custom functions and importers #118

Merged
merged 6 commits into from Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 2 additions & 7 deletions bin/dart_sass_embedded.dart
Expand Up @@ -55,13 +55,8 @@ void main(List<String> args) {
_decodeImporter(dispatcher, request, importer) ??
(throw mandatoryError("Importer.importer")));

var globalFunctions = request.globalFunctions.map((signature) {
try {
return hostCallable(dispatcher, functions, request.id, signature);
} on sass.SassException catch (error) {
throw paramsError('CompileRequest.global_functions: $error');
}
});
var globalFunctions = request.globalFunctions.map((signature) =>
hostCallable(dispatcher, functions, request.id, signature));
ntkme marked this conversation as resolved.
Show resolved Hide resolved

late sass.CompileResult result;
switch (request.whichInput()) {
Expand Down
21 changes: 4 additions & 17 deletions lib/src/host_callable.dart
Expand Up @@ -7,7 +7,6 @@ import 'dart:cli';
import 'dart:io';

import 'package:sass_api/sass_api.dart' as sass;
import 'package:source_span/source_span.dart';

import 'dispatcher.dart';
import 'embedded_sass.pb.dart';
Expand All @@ -26,21 +25,8 @@ import 'utils.dart';
sass.Callable hostCallable(Dispatcher dispatcher, FunctionRegistry functions,
int compilationId, String signature,
{int? id}) {
var openParen = signature.indexOf('(');
if (openParen == -1) {
throw sass.SassException('"$signature" is missing "("',
SourceFile.fromString(signature).span(0));
}

if (!signature.endsWith(")")) {
throw sass.SassException('"$signature" doesn\'t end with ")"',
SourceFile.fromString(signature).span(signature.length));
}

var name = signature.substring(0, openParen);
return sass.Callable.function(
name, signature.substring(openParen + 1, signature.length - 1),
(arguments) {
late sass.Callable callable;
callable = sass.Callable.fromSignature(signature, (arguments) {
var protofier = Protofier(dispatcher, functions, compilationId);
var request = OutboundMessage_FunctionCallRequest()
..compilationId = compilationId
Expand All @@ -50,7 +36,7 @@ sass.Callable hostCallable(Dispatcher dispatcher, FunctionRegistry functions,
if (id != null) {
request.functionId = id;
} else {
request.name = name;
request.name = callable.name;
}

// ignore: deprecated_member_use
Expand All @@ -74,4 +60,5 @@ sass.Callable hostCallable(Dispatcher dispatcher, FunctionRegistry functions,
throw error.message;
}
});
return callable;
}
18 changes: 4 additions & 14 deletions lib/src/importer/base.dart
Expand Up @@ -6,8 +6,6 @@ import 'package:meta/meta.dart';
import 'package:sass_api/sass_api.dart' as sass;

import '../dispatcher.dart';
import '../embedded_sass.pb.dart' hide SourceSpan;
import '../utils.dart';

/// An abstract base class for importers that communicate with the host in some
/// way.
Expand All @@ -21,25 +19,17 @@ abstract class ImporterBase extends sass.Importer {
/// Parses [url] as a [Uri] and throws an error if it's invalid or relative
/// (including root-relative).
///
/// The [field] name is used in the error message if one is thrown.
/// The [source] name is used in the error message if one is thrown.
@protected
Uri parseAbsoluteUrl(String field, String url) {
Uri parseAbsoluteUrl(String source, String url) {
Uri parsedUrl;
try {
parsedUrl = Uri.parse(url);
} on FormatException catch (error) {
sendAndThrow(paramsError("$field is invalid: $error"));
throw '$source must return a URL, was "$url"';
}

if (parsedUrl.scheme.isNotEmpty) return parsedUrl;
sendAndThrow(paramsError('$field must be absolute, was "$parsedUrl"'));
}

/// Sends [error] to the remote endpoint, and also throws it so that the Sass
/// compilation fails.
@protected
Never sendAndThrow(ProtocolError error) {
dispatcher.sendError(error);
throw "Protocol error: ${error.message}";
throw '$source must return an absolute URL, was "$parsedUrl"';
}
}
7 changes: 2 additions & 5 deletions lib/src/importer/file.dart
Expand Up @@ -9,7 +9,6 @@ import 'package:sass_api/sass_api.dart' as sass;

import '../dispatcher.dart';
import '../embedded_sass.pb.dart' hide SourceSpan;
import '../utils.dart';
import 'base.dart';

/// A filesystem importer to use for most implementation details of
Expand Down Expand Up @@ -44,11 +43,9 @@ class FileImporter extends ImporterBase {

switch (response.whichResult()) {
case InboundMessage_FileImportResponse_Result.fileUrl:
var url =
parseAbsoluteUrl("FileImportResponse.file_url", response.fileUrl);
var url = parseAbsoluteUrl("The file importer", response.fileUrl);
if (url.scheme != 'file') {
sendAndThrow(paramsError(
'FileImportResponse.file_url must be a file: URL, was "$url"'));
throw 'The file importer must return a file: URL, was "$url"';
}

return _filesystemImporter.canonicalize(url);
Expand Down
6 changes: 3 additions & 3 deletions lib/src/importer/host.dart
Expand Up @@ -35,7 +35,7 @@ class HostImporter extends ImporterBase {

switch (response.whichResult()) {
case InboundMessage_CanonicalizeResponse_Result.url:
return parseAbsoluteUrl("CanonicalizeResponse.url", response.url);
return parseAbsoluteUrl("The importer", response.url);

case InboundMessage_CanonicalizeResponse_Result.error:
throw response.error;
Expand All @@ -60,8 +60,8 @@ class HostImporter extends ImporterBase {
return sass.ImporterResult(response.success.contents,
sourceMapUrl: response.success.sourceMapUrl.isEmpty
? null
: parseAbsoluteUrl("ImportResponse.success.source_map_url",
response.success.sourceMapUrl),
: parseAbsoluteUrl(
"The importer", response.success.sourceMapUrl),
syntax: syntaxToSyntax(response.success.syntax));

case InboundMessage_ImportResponse_Result.error:
Expand Down
34 changes: 21 additions & 13 deletions test/file_importer_test.dart
Expand Up @@ -54,6 +54,18 @@ void main() {
"InboundMessage_CanonicalizeResponse.");
await process.kill();
});
});

group("emits a compile failure", () {
late OutboundMessage_FileImportRequest request;

setUp(() async {
process.inbound.add(compileString("@import 'other'", importers: [
InboundMessage_CompileRequest_Importer()..fileImporterId = 1
]));

request = getFileImportRequest(await process.outbound.next);
});

group("for a FileImportResponse with a URL", () {
test("that's empty", () async {
Expand All @@ -62,8 +74,8 @@ void main() {
..id = request.id
..fileUrl = ""));

await _expectImportParamsError(
process, 'FileImportResponse.file_url must be absolute, was ""');
await _expectImportError(
process, 'The file importer must return an absolute URL, was ""');
await process.kill();
});

Expand All @@ -73,8 +85,8 @@ void main() {
..id = request.id
..fileUrl = "foo"));

await _expectImportParamsError(
process, 'FileImportResponse.file_url must be absolute, was "foo"');
await _expectImportError(process,
'The file importer must return an absolute URL, was "foo"');
await process.kill();
});

Expand All @@ -84,8 +96,8 @@ void main() {
..id = request.id
..fileUrl = "other:foo"));

await _expectImportParamsError(process,
'FileImportResponse.file_url must be a file: URL, was "other:foo"');
await _expectImportError(process,
'The file importer must return a file: URL, was "other:foo"');
await process.kill();
});
});
Expand Down Expand Up @@ -268,14 +280,10 @@ void main() {
});
}

/// Asserts that [process] emits a [ProtocolError] params error with the given
/// Asserts that [process] emits a [CompileFailure] result with the given
/// [message] on its protobuf stream and causes the compilation to fail.
Future<void> _expectImportParamsError(
EmbeddedProcess process, Object message) async {
await expectLater(process.outbound,
emits(isProtocolError(errorId, ProtocolErrorType.PARAMS, message)));

Future<void> _expectImportError(EmbeddedProcess process, Object message) async {
var failure = getCompileFailure(await process.outbound.next);
expect(failure.message, equals('Protocol error: $message'));
expect(failure.message, equals(message));
expect(failure.span.text, equals("'other'"));
}
80 changes: 28 additions & 52 deletions test/function_test.dart
Expand Up @@ -21,74 +21,39 @@ void main() {
_process = await EmbeddedProcess.start();
});

group("emits a protocol error for a custom function with a signature", () {
group("emits a compile failure for a custom function with a signature", () {
test("that's empty", () async {
_process.inbound.add(compileString("a {b: c}", functions: [r""]));
await expectParamsError(
_process,
0,
'CompileRequest.global_functions: Error: "" is missing "("\n'
' ╷\n'
'1 │ \n'
' │ ^\n'
' ╵\n'
' - 1:1 root stylesheet');
await _expectFunctionError(
_process, r'Invalid signature "": Expected identifier.');
await _process.kill();
});

test("that's just a name", () async {
_process.inbound.add(compileString("a {b: c}", functions: [r"foo"]));
await expectParamsError(
_process,
0,
'CompileRequest.global_functions: Error: "foo" is missing "("\n'
' ╷\n'
'1 │ foo\n'
' │ ^^^\n'
' ╵\n'
' - 1:1 root stylesheet');
await _expectFunctionError(
_process, r'Invalid signature "foo": expected "(".');
await _process.kill();
});

test("without a closing paren", () async {
_process.inbound.add(compileString("a {b: c}", functions: [r"foo($bar"]));
await expectParamsError(
_process,
0,
'CompileRequest.global_functions: Error: "foo(\$bar" doesn\'t end with ")"\n'
' ╷\n'
'1 │ foo(\$bar\n'
' │ ^\n'
' ╵\n'
' - 1:9 root stylesheet');
await _expectFunctionError(
_process, r'Invalid signature "foo($bar": expected ")".');
await _process.kill();
});

test("with text after the closing paren", () async {
_process.inbound.add(compileString("a {b: c}", functions: [r"foo() "]));
await expectParamsError(
_process,
0,
'CompileRequest.global_functions: Error: "foo() " doesn\'t end with ")"\n'
' ╷\n'
'1 │ foo() \n'
' │ ^\n'
' ╵\n'
' - 1:7 root stylesheet');
await _expectFunctionError(
_process, r'Invalid signature "foo() ": expected no more input.');
await _process.kill();
});

test("with invalid arguments", () async {
_process.inbound.add(compileString("a {b: c}", functions: [r"foo($)"]));
await expectParamsError(
_process,
0,
'CompileRequest.global_functions: Error: Expected identifier.\n'
' ╷\n'
'1 │ @function foo(\$) {\n'
' │ ^\n'
' ╵\n'
' - 1:16 root stylesheet');
await _expectFunctionError(
_process, r'Invalid signature "foo($)": Expected identifier.');
await _process.kill();
});
});
Expand Down Expand Up @@ -1848,25 +1813,28 @@ void main() {
}

test("that's empty", () async {
await expectSignatureError("", '"" is missing "("');
await expectSignatureError(
"", r'Invalid signature "": Expected identifier.');
});

test("that's just a name", () async {
await expectSignatureError("foo", '"foo" is missing "("');
await expectSignatureError(
"foo", r'Invalid signature "foo": expected "(".');
});

test("without a closing paren", () async {
await expectSignatureError(
r"foo($bar", '"foo(\$bar" doesn\'t end with ")"');
r"foo($bar", r'Invalid signature "foo($bar": expected ")".');
});

test("with text after the closing paren", () async {
await expectSignatureError(
r"foo() ", '"foo() " doesn\'t end with ")"');
await expectSignatureError(r"foo() ",
r'Invalid signature "foo() ": expected no more input.');
});

test("with invalid arguments", () async {
await expectSignatureError(r"foo($)", 'Expected identifier.');
await expectSignatureError(
r"foo($)", r'Invalid signature "foo($)": Expected identifier.');
});
});
});
Expand Down Expand Up @@ -1989,3 +1957,11 @@ Value _hsl(num hue, num saturation, num lightness, double alpha) => Value()
..saturation = saturation * 1.0
..lightness = lightness * 1.0
..alpha = alpha);

/// Asserts that [process] emits a [CompileFailure] result with the given
/// [message] on its protobuf stream and causes the compilation to fail.
Future<void> _expectFunctionError(
EmbeddedProcess process, Object message) async {
var failure = getCompileFailure(await process.outbound.next);
expect(failure.message, equals(message));
}