improve dart2js sourcemaps support

First off: this isn’t a regression, we’ve had issues with sourcemaps for years, only got round to file a bug now :-S

While stack trace deobfuscation seems to work reasonably well (modulo method inlining and some apparent off-by-one errors, see end of this bug), we’ve never managed to get satisfactory demangling of exception messages such as:

  • TypeError: x.y is not a function
  • NullError: method not found: 'x' on null
  • CastError: Casting value of type x to incompatible type y

It seems there’s just not enough data in dart2js’ sourcemaps to support such demangling, quite unlike with other transpilers such as uglify-js or Closure.

Take the following exception.dart:

// npm i -g source-map
// dart2js exception.dart -m -o exception.dart.js
// node analyze.js exception.dart exception.dart.js.map
void foo(x) {
  print(x);
  x.nonExistingMethod();
}
void main(List<String> args) {
  foo(1);
}

Its sourcemap contrasts starkly with that of the following JavaScript code minified with UglifyJS or by the Closure Compiler:

// npm i -g uglify-js source-map
// uglifyjs --compress --mangle --source-map -o exception.ugly.js exception.js
// java -jar closure-compiler-v20171203.jar --compilation_level ADVANCED_OPTIMIZATIONS --js exception.js --js_output_file exception.closure.js --create_source_map  "%%outname%%.map"

// node analyze.js exception.js exception.ugly.js.map
// node analyze.js exception.js exception.closure.js.map
function foo(x) {
  console.log(x);
	x.nonExistingMethod();
}

function main() {
  foo(1);
}

main();

To scrutinize the sourcemaps, I’ve used the following analyze.js:

// npm i source-map
// node analyze.js exception.dart exception.dart.js.map
// node analyze.js exception.js exception.ugly.js.map
const {SourceMapConsumer} = require('source-map');
const {readFileSync} = require('fs');
const [, , sourceFileName, sourceMapFileName] = process.argv;

var rawSourceMap = JSON.parse(readFileSync(sourceMapFileName));

var smc = new SourceMapConsumer(rawSourceMap);

var names = [...smc._names._array];
names.sort();
for (let name of names) {
	console.log(name);
}

smc.eachMapping(function (m) {
	if (m.source.indexOf(sourceFileName) >= 0) {
		console.log(JSON.stringify(m, '', 2));
	}
});

Results:

  • dart2js doesn’t seem to output the symbols nonExistingMethod (method called) or x (local var).
  • In mappings, name is always bound to the enclosing method name, never to the symbol name at the mapping’s position.
  • Even just for stack traces, the lines in mapping seem to get off-by one errors: for instance in the example code above, both statements in the foo method get the right line, but the statement in the main method (calling foo(1)) is listed as in line 10 instead of line 9.

This makes production exceptions extremely hard to debug compared to more mature stacks such as GWT / Closure.

Author: Fantashit

1 thought on “improve dart2js sourcemaps support

  1. @ochafik – we agree and have plans to make this better. We are close to completing our migration to work behind the shared front-end. Once we are done, this is one of our top priorities.

    We have many ideas in mind that we want to explore, including:

    • storing symbol information from minification
    • storing inlining metadata to be able to recover that information during deobfuscation
    • test coverage of step debugging in js engines (we believe we can automate this for v8 at least)
    • handling of deobfuscation of error messages and symbols that are mentioned in error messages

    I’m sure we have more in our list, but this is just the beginning.

Comments are closed.