Skip to content

Commit 55cb4fd

Browse files
authored
Track stack traces through rethrows (#1545)
This requires a lot of manual machinery when displaying stack traces to the user, but it should make debugging errors (especially in JS) much easier. Works around dart-lang/sdk#10297 using expandos.
1 parent 6622eeb commit 55cb4fd

22 files changed

Lines changed: 336 additions & 168 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 1.43.5
2+
3+
### JS API
4+
5+
* Print more detailed JS stack traces. This is mostly useful for the Sass team's
6+
own debugging purposes.
7+
18
## 1.43.4
29

310
### JS API

bin/sass.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:sass/src/executable/watch.dart';
1616
import 'package:sass/src/import_cache.dart';
1717
import 'package:sass/src/io.dart';
1818
import 'package:sass/src/stylesheet_graph.dart';
19+
import 'package:sass/src/utils.dart';
1920

2021
Future<void> main(List<String> args) async {
2122
var printedError = false;
@@ -79,7 +80,7 @@ Future<void> main(List<String> args) async {
7980
}();
8081

8182
printError(error.toString(color: options.color),
82-
options.trace ? stackTrace : null);
83+
options.trace ? getTrace(error) ?? stackTrace : null);
8384

8485
// Exit code 65 indicates invalid data per
8586
// http://www.freebsd.org/cgi/man.cgi?query=sysexits.
@@ -93,7 +94,7 @@ Future<void> main(List<String> args) async {
9394
path == null
9495
? error.message
9596
: "Error reading ${p.relative(path)}: ${error.message}.",
96-
options.trace ? stackTrace : null);
97+
options.trace ? getTrace(error) ?? stackTrace : null);
9798

9899
// Error 66 indicates no input.
99100
exitCode = 66;
@@ -114,7 +115,7 @@ Future<void> main(List<String> args) async {
114115
buffer.writeln();
115116
buffer.writeln(error);
116117

117-
printError(buffer.toString(), stackTrace);
118+
printError(buffer.toString(), getTrace(error) ?? stackTrace);
118119
exitCode = 255;
119120
}
120121
}

lib/src/executable/repl.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../import_cache.dart';
1414
import '../importer/filesystem.dart';
1515
import '../logger/tracking.dart';
1616
import '../parse/parser.dart';
17+
import '../utils.dart';
1718
import '../visitor/evaluate.dart';
1819

1920
/// Runs an interactive SassScript shell according to [options].
@@ -42,7 +43,8 @@ Future<void> repl(ExecutableOptions options) async {
4243
print(evaluator.evaluate(Expression.parse(line, logger: logger)));
4344
}
4445
} on SassException catch (error, stackTrace) {
45-
_logError(error, stackTrace, line, repl, options, logger);
46+
_logError(
47+
error, getTrace(error) ?? stackTrace, line, repl, options, logger);
4648
}
4749
}
4850
}

lib/src/executable/watch.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../importer/filesystem.dart';
1414
import '../io.dart';
1515
import '../stylesheet_graph.dart';
1616
import '../util/multi_dir_watcher.dart';
17+
import '../utils.dart';
1718
import 'compile_stylesheet.dart';
1819
import 'options.dart';
1920

@@ -78,7 +79,8 @@ class _Watcher {
7879
return true;
7980
} on SassException catch (error, stackTrace) {
8081
if (!_options.emitErrorCss) _delete(destination);
81-
_printError(error.toString(color: _options.color), stackTrace);
82+
_printError(
83+
error.toString(color: _options.color), getTrace(error) ?? stackTrace);
8284
exitCode = 65;
8385
return false;
8486
} on FileSystemException catch (error, stackTrace) {
@@ -87,7 +89,7 @@ class _Watcher {
8789
path == null
8890
? error.message
8991
: "Error reading ${p.relative(path)}: ${error.message}.",
90-
stackTrace);
92+
getTrace(error) ?? stackTrace);
9193
exitCode = 66;
9294
return false;
9395
}

lib/src/extend/extension_store.dart

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,13 @@ class ExtensionStore {
187187
try {
188188
selector = _extendList(
189189
originalSelector, selectorSpan, _extensions, mediaContext);
190-
} on SassException catch (error) {
191-
throw SassException(
192-
"From ${error.span.message('')}\n"
193-
"${error.message}",
194-
error.span);
190+
} on SassException catch (error, stackTrace) {
191+
throwWithTrace(
192+
SassException(
193+
"From ${error.span.message('')}\n"
194+
"${error.message}",
195+
error.span),
196+
stackTrace);
195197
}
196198
}
197199

@@ -330,11 +332,13 @@ class ExtensionStore {
330332
selectors = _extendComplex(extension.extender.selector,
331333
extension.extender.span, newExtensions, extension.mediaContext);
332334
if (selectors == null) continue;
333-
} on SassException catch (error) {
334-
throw SassException(
335-
"From ${extension.extender.span.message('')}\n"
336-
"${error.message}",
337-
error.span);
335+
} on SassException catch (error, stackTrace) {
336+
throwWithTrace(
337+
SassException(
338+
"From ${extension.extender.span.message('')}\n"
339+
"${error.message}",
340+
error.span),
341+
stackTrace);
338342
}
339343

340344
var containsExtension = selectors.first == extension.extender.selector;
@@ -391,12 +395,14 @@ class ExtensionStore {
391395
try {
392396
selector.value = _extendList(selector.value, selector.span,
393397
newExtensions, _mediaContexts[selector]);
394-
} on SassException catch (error) {
398+
} on SassException catch (error, stackTrace) {
395399
// TODO(nweiz): Make this a MultiSpanSassException.
396-
throw SassException(
397-
"From ${selector.span.message('')}\n"
398-
"${error.message}",
399-
error.span);
400+
throwWithTrace(
401+
SassException(
402+
"From ${selector.span.message('')}\n"
403+
"${error.message}",
404+
error.span),
405+
stackTrace);
400406
}
401407

402408
// If no extends actually happened (for example because unification

lib/src/io/vm.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:source_span/source_span.dart';
1212
import 'package:watcher/watcher.dart';
1313

1414
import '../exception.dart';
15+
import '../utils.dart';
1516

1617
export 'dart:io' show exitCode, FileSystemException;
1718

@@ -41,16 +42,18 @@ String readFile(String path) {
4142

4243
try {
4344
return utf8.decode(bytes);
44-
} on FormatException catch (error) {
45+
} on FormatException catch (error, stackTrace) {
4546
var decodedUntilError =
4647
utf8.decode(bytes.sublist(0, error.offset), allowMalformed: true);
4748
var stringOffset = decodedUntilError.length;
4849
if (decodedUntilError.endsWith("�")) stringOffset--;
4950

5051
var decoded = utf8.decode(bytes, allowMalformed: true);
5152
var sourceFile = SourceFile.fromString(decoded, url: p.toUri(path));
52-
throw SassException(
53-
"Invalid UTF-8.", sourceFile.location(stringOffset).pointSpan());
53+
throwWithTrace(
54+
SassException(
55+
"Invalid UTF-8.", sourceFile.location(stringOffset).pointSpan()),
56+
stackTrace);
5457
}
5558
}
5659

lib/src/node/compile.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import 'package:js/js.dart';
66
import 'package:node_interop/js.dart';
7-
import 'package:node_interop/util.dart';
7+
import 'package:node_interop/util.dart' hide futureToPromise;
88
import 'package:term_glyph/term_glyph.dart' as glyph;
99

1010
import '../../sass.dart';
@@ -42,8 +42,8 @@ NodeCompileResult compile(String path, [CompileOptions? options]) {
4242
ascii: ascii),
4343
importers: options?.importers?.map(_parseImporter));
4444
return _convertResult(result);
45-
} on SassException catch (error) {
46-
throwNodeException(error, color: color, ascii: ascii);
45+
} on SassException catch (error, stackTrace) {
46+
throwNodeException(error, color: color, ascii: ascii, trace: stackTrace);
4747
}
4848
}
4949

@@ -70,8 +70,8 @@ NodeCompileResult compileString(String text, [CompileStringOptions? options]) {
7070
importer: options?.importer.andThen(_parseImporter) ??
7171
(options?.url == null ? NoOpImporter() : null));
7272
return _convertResult(result);
73-
} on SassException catch (error) {
74-
throwNodeException(error, color: color, ascii: ascii);
73+
} on SassException catch (error, stackTrace) {
74+
throwNodeException(error, color: color, ascii: ascii, trace: stackTrace);
7575
}
7676
}
7777

lib/src/node/exception.dart

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@
55
import 'dart:js_util';
66

77
import 'package:js/js.dart';
8+
import 'package:node_interop/node_interop.dart';
89
import 'package:term_glyph/term_glyph.dart' as glyph;
910

1011
import '../exception.dart';
12+
import '../utils.dart';
1113
import 'utils.dart';
1214

1315
@JS()
1416
@anonymous
15-
class _NodeException {
17+
class _NodeException extends JsError {
18+
// Fake constructor to silence the no_generative_constructor_in_superclass
19+
// error.
20+
external factory _NodeException();
21+
1622
external SassException get _dartException;
1723
}
1824

@@ -58,15 +64,20 @@ var exceptionConstructor = () {
5864
///
5965
/// If [ascii] is `false`, the thrown exception uses non-ASCII characters in its
6066
/// stringification.
67+
///
68+
/// If [trace] is passed, it's used as the stack trace for the JS exception.
6169
Never throwNodeException(SassException exception,
62-
{required bool color, required bool ascii}) {
70+
{required bool color, required bool ascii, StackTrace? trace}) {
6371
var wasAscii = glyph.ascii;
6472
glyph.ascii = ascii;
6573
try {
66-
jsThrow(callConstructor(exceptionConstructor, [
74+
var jsException = callConstructor(exceptionConstructor, [
6775
exception,
6876
exception.toString(color: color).replaceFirst('Error: ', '')
69-
]) as _NodeException);
77+
]) as _NodeException;
78+
trace = getTrace(exception) ?? trace;
79+
if (trace != null) attachJsStack(jsException, trace);
80+
jsThrow(jsException);
7081
} finally {
7182
glyph.ascii = wasAscii;
7283
}

lib/src/node/legacy.dart

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import '../logger/node_to_dart.dart';
2424
import '../parse/scss.dart';
2525
import '../syntax.dart';
2626
import '../util/nullable.dart';
27+
import '../utils.dart';
2728
import '../value.dart';
2829
import '../visitor/serialize.dart';
2930
import 'function.dart';
@@ -56,9 +57,12 @@ void render(
5657
callback(null, result);
5758
}, onError: (Object error, StackTrace stackTrace) {
5859
if (error is SassException) {
59-
callback(_wrapException(error), null);
60+
callback(_wrapException(error, stackTrace), null);
6061
} else {
61-
callback(_newRenderError(error.toString(), status: 3), null);
62+
callback(
63+
_newRenderError(error.toString(), getTrace(error) ?? stackTrace,
64+
status: 3),
65+
null);
6266
}
6367
});
6468
}
@@ -158,15 +162,16 @@ RenderResult renderSync(RenderOptions options) {
158162
}
159163

160164
return _newRenderResult(options, result, start);
161-
} on SassException catch (error) {
162-
jsThrow(_wrapException(error));
163-
} catch (error) {
164-
jsThrow(_newRenderError(error.toString(), status: 3));
165+
} on SassException catch (error, stackTrace) {
166+
jsThrow(_wrapException(error, stackTrace));
167+
} catch (error, stackTrace) {
168+
jsThrow(_newRenderError(error.toString(), getTrace(error) ?? stackTrace,
169+
status: 3));
165170
}
166171
}
167172

168173
/// Converts an exception to a [JsError].
169-
JsError _wrapException(Object exception) {
174+
JsError _wrapException(Object exception, StackTrace stackTrace) {
170175
if (exception is SassException) {
171176
String file;
172177
var url = exception.span.sourceUrl;
@@ -179,12 +184,15 @@ JsError _wrapException(Object exception) {
179184
}
180185

181186
return _newRenderError(exception.toString().replaceFirst("Error: ", ""),
187+
getTrace(exception) ?? stackTrace,
182188
line: exception.span.start.line + 1,
183189
column: exception.span.start.column + 1,
184190
file: file,
185191
status: 1);
186192
} else {
187-
return JsError(exception.toString());
193+
var error = JsError(exception.toString());
194+
attachJsStack(error, getTrace(exception) ?? stackTrace);
195+
return error;
188196
}
189197
}
190198

@@ -203,9 +211,11 @@ List<AsyncCallable> _parseFunctions(RenderOptions options, DateTime start,
203211
Tuple2<String, ArgumentDeclaration> tuple;
204212
try {
205213
tuple = ScssParser(signature as String).parseSignature();
206-
} on SassFormatException catch (error) {
207-
throw SassFormatException(
208-
'Invalid signature "$signature": ${error.message}', error.span);
214+
} on SassFormatException catch (error, stackTrace) {
215+
throwWithTrace(
216+
SassFormatException(
217+
'Invalid signature "$signature": ${error.message}', error.span),
218+
stackTrace);
209219
}
210220

211221
var context = RenderContext(options: _contextOptions(options, start));
@@ -417,13 +427,14 @@ bool _enableSourceMaps(RenderOptions options) =>
417427

418428
/// Creates a [JsError] with the given fields added to it so it acts like a Node
419429
/// Sass error.
420-
JsError _newRenderError(String message,
430+
JsError _newRenderError(String message, StackTrace stackTrace,
421431
{int? line, int? column, String? file, int? status}) {
422432
var error = JsError(message);
423433
setProperty(error, 'formatted', 'Error: $message');
424434
if (line != null) setProperty(error, 'line', line);
425435
if (column != null) setProperty(error, 'column', column);
426436
if (file != null) setProperty(error, 'file', file);
427437
if (status != null) setProperty(error, 'status', status);
438+
attachJsStack(error, stackTrace);
428439
return error;
429440
}

lib/src/node/utils.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:js/js.dart';
1111
import 'package:js/js_util.dart';
1212

1313
import '../syntax.dart';
14+
import '../utils.dart';
1415
import 'array.dart';
1516
import 'function.dart';
1617
import 'url.dart';
@@ -44,6 +45,20 @@ external Function get jsErrorConstructor;
4445
/// Returns whether [value] is a JS Error object.
4546
bool isJSError(Object value) => instanceof(value, jsErrorConstructor);
4647

48+
/// Attaches [trace] to [error] as its stack trace.
49+
void attachJsStack(JsError error, StackTrace trace) {
50+
// Stack traces in v8 contain the error message itself as well as the stack
51+
// information, so we trim that out if it exists so we don't double-print it.
52+
var traceString = trace.toString();
53+
var firstRealLine = traceString.indexOf('\n at');
54+
if (firstRealLine != -1) {
55+
// +1 to account for the newline before the first line.
56+
traceString = traceString.substring(firstRealLine + 1);
57+
}
58+
59+
setProperty(error, 'stack', "Error: ${error.message}\n$traceString");
60+
}
61+
4762
/// Invokes [function] with [thisArg] as `this`.
4863
Object? call2(JSFunction function, Object thisArg, Object arg1, Object arg2) =>
4964
function.apply(thisArg, [arg1, arg2]);
@@ -169,6 +184,17 @@ external Function get _promiseClass;
169184
bool isPromise(Object? object) =>
170185
object != null && instanceof(object, _promiseClass);
171186

187+
/// Like [futureToPromise] from `node_interop`, but stores the stack trace for
188+
/// errors using [throwWithTrace].
189+
Promise futureToPromise(Future<Object?> future) => Promise(allowInterop(
190+
(void Function(Object?) resolve, void Function(Object?) reject) {
191+
future.then((result) => resolve(result),
192+
onError: (Object error, StackTrace stackTrace) {
193+
attachTrace(error, stackTrace);
194+
reject(error);
195+
});
196+
}));
197+
172198
@JS('URL')
173199
external Function get _urlClass;
174200

0 commit comments

Comments
 (0)