Support a new source map bundle format useful for the Dart Dev Compiler. A source map bundle is a JSON array where each entry is a source map. BUG= R=sigmund@google.com Review URL: https://codereview.chromium.org//2560623003 .
diff --git a/pkgs/source_maps/CHANGELOG.md b/pkgs/source_maps/CHANGELOG.md index fe88e6d..4e32866 100644 --- a/pkgs/source_maps/CHANGELOG.md +++ b/pkgs/source_maps/CHANGELOG.md
@@ -1,3 +1,10 @@ +## 0.10.1+3 + +* Add `MappingBundle` class that handles extended source map format that + supports source maps for multiple output files in a single mapper. + Extend `Mapping.spanFor` API to accept a uri parameter that is optional + for normal source maps but required for MappingBundle source maps. + ## 0.10.1+2 * Fix more strong mode warnings.
diff --git a/pkgs/source_maps/lib/parser.dart b/pkgs/source_maps/lib/parser.dart index b659654..c651f89 100644 --- a/pkgs/source_maps/lib/parser.dart +++ b/pkgs/source_maps/lib/parser.dart
@@ -8,6 +8,7 @@ import 'dart:collection'; import 'dart:convert'; +import 'package:path/path.dart' as path; import 'package:source_span/source_span.dart'; import 'builder.dart' as builder; @@ -25,22 +26,44 @@ // TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string // `)]}'` begins the string representation of the map. Mapping parse(String jsonMap, {Map<String, Map> otherMaps, mapUrl}) => - parseJson(JSON.decode(jsonMap), otherMaps: otherMaps, mapUrl: mapUrl); + parseJson(JSON.decode(jsonMap), otherMaps: otherMaps, mapUrl: mapUrl); -/// Parses a source map directly from a json map object. +/// Parses a source map or source map bundle directly from a json string. +/// +/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of +/// the source map file itself. If it's passed, any URLs in the source +/// map will be interpreted as relative to this URL when generating spans. +Mapping parseExtended(String jsonMap, {Map<String, Map> otherMaps, mapUrl}) => + parseJsonExtended(JSON.decode(jsonMap), + otherMaps: otherMaps, mapUrl: mapUrl); + +/// Parses a source map or source map bundle. +/// +/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of +/// the source map file itself. If it's passed, any URLs in the source +/// map will be interpreted as relative to this URL when generating spans. +Mapping parseJsonExtended(/*List|Map*/ json, + {Map<String, Map> otherMaps, mapUrl}) { + if (json is List) { + return new MappingBundle.fromJson(json, mapUrl: mapUrl); + } + return parseJson(json as Map); +} + +/// Parses a source map /// /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of /// the source map file itself. If it's passed, any URLs in the source /// map will be interpreted as relative to this URL when generating spans. Mapping parseJson(Map map, {Map<String, Map> otherMaps, mapUrl}) { if (map['version'] != 3) { - throw new ArgumentError( - 'unexpected source map version: ${map["version"]}. ' + throw new ArgumentError('unexpected source map version: ${map["version"]}. ' 'Only version 3 is supported.'); } if (map.containsKey('sections')) { - if (map.containsKey('mappings') || map.containsKey('sources') || + if (map.containsKey('mappings') || + map.containsKey('sources') || map.containsKey('names')) { throw new FormatException('map containing "sections" ' 'cannot contain "mappings", "sources", or "names".'); @@ -51,16 +74,21 @@ return new SingleMapping.fromJson(map, mapUrl: mapUrl); } - /// A mapping parsed out of a source map. abstract class Mapping { /// Returns the span associated with [line] and [column]. - SourceMapSpan spanFor(int line, int column, {Map<String, SourceFile> files}); + /// + /// [uri] is the optional location of the output file to find the span for + /// to disambiguate cases where a mapping may have different mappings for + /// different output files. + SourceMapSpan spanFor(int line, int column, + {Map<String, SourceFile> files, String uri}); /// Returns the span associated with [location]. SourceMapSpan spanForLocation(SourceLocation location, {Map<String, SourceFile> files}) { - return spanFor(location.line, location.column, files: files); + return spanFor(location.line, location.column, + uri: location.sourceUrl?.toString(), files: files); } } @@ -116,35 +144,79 @@ } int _indexFor(line, column) { - for(int i = 0; i < _lineStart.length; i++) { + for (int i = 0; i < _lineStart.length; i++) { if (line < _lineStart[i]) return i - 1; if (line == _lineStart[i] && column < _columnStart[i]) return i - 1; } return _lineStart.length - 1; } - SourceMapSpan spanFor(int line, int column, {Map<String, SourceFile> files}) { + SourceMapSpan spanFor(int line, int column, + {Map<String, SourceFile> files, String uri}) { + // TODO(jacobr): perhaps verify that targetUrl matches the actual uri + // or at least ends in the same file name. int index = _indexFor(line, column); return _maps[index].spanFor( - line - _lineStart[index], column - _columnStart[index], files: files); + line - _lineStart[index], column - _columnStart[index], + files: files); } String toString() { var buff = new StringBuffer("$runtimeType : ["); for (int i = 0; i < _lineStart.length; i++) { - buff..write('(') - ..write(_lineStart[i]) - ..write(',') - ..write(_columnStart[i]) - ..write(':') - ..write(_maps[i]) - ..write(')'); + buff + ..write('(') + ..write(_lineStart[i]) + ..write(',') + ..write(_columnStart[i]) + ..write(':') + ..write(_maps[i]) + ..write(')'); } buff.write(']'); return buff.toString(); } } +class MappingBundle extends Mapping { + Map<String, SingleMapping> _mappings = {}; + + MappingBundle.fromJson(List json, {String mapUrl}) { + for (var map in json) { + var mapping = parseJson(map, mapUrl: mapUrl) as SingleMapping; + var targetUrl = mapping.targetUrl; + _mappings[targetUrl] = mapping; + } + } + + /// Encodes the Mapping mappings as a json map. + List toJson() => _mappings.values.map((v) => v.toJson()).toList(); + + String toString() { + var buff = new StringBuffer(); + for (var map in _mappings.values) { + buff.write(map.toString()); + } + return buff.toString(); + } + + SourceMapSpan spanFor(int line, int column, + {Map<String, SourceFile> files, String uri}) { + if (uri == null) { + throw new ArgumentError.notNull('uri'); + } + if (_mappings.containsKey(uri)) { + return _mappings[uri].spanFor(line, column, files: files, uri: uri); + } + // Fall back to looking up the source map on just the basename. + var name = path.basename(uri.toString()); + if (_mappings.containsKey(name)) { + return _mappings[name].spanFor(line, column, files: files, uri: name); + } + return null; + } +} + /// A map containing direct source mappings. class SingleMapping extends Mapping { /// Source urls used in the mapping, indexed by id. @@ -167,8 +239,8 @@ SingleMapping._(this.targetUrl, this.urls, this.names, this.lines) : _mapUrl = null; - factory SingleMapping.fromEntries( - Iterable<builder.Entry> entries, [String fileUrl]) { + factory SingleMapping.fromEntries(Iterable<builder.Entry> entries, + [String fileUrl]) { // The entries needs to be sorted by the target offsets. var sourceEntries = new List.from(entries)..sort(); var lines = <TargetLineEntry>[]; @@ -196,14 +268,11 @@ var sourceUrl = sourceEntry.source.sourceUrl; var urlId = urls.putIfAbsent( sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length); - var srcNameId = sourceEntry.identifierName == null ? null : - names.putIfAbsent(sourceEntry.identifierName, () => names.length); - targetEntries.add(new TargetEntry( - sourceEntry.target.column, - urlId, - sourceEntry.source.line, - sourceEntry.source.column, - srcNameId)); + var srcNameId = sourceEntry.identifierName == null + ? null + : names.putIfAbsent(sourceEntry.identifierName, () => names.length); + targetEntries.add(new TargetEntry(sourceEntry.target.column, urlId, + sourceEntry.source.line, sourceEntry.source.column, srcNameId)); } } return new SingleMapping._( @@ -271,8 +340,8 @@ throw new StateError( 'Invalid name id: $targetUrl, $line, $srcNameId'); } - entries.add(new TargetEntry(column, srcUrlId, srcLine, srcColumn, - srcNameId)); + entries.add( + new TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId)); } } if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment(); @@ -326,8 +395,8 @@ 'version': 3, 'sourceRoot': sourceRoot == null ? '' : sourceRoot, 'sources': urls, - 'names' : names, - 'mappings' : buff.toString() + 'names': names, + 'mappings': buff.toString() }; if (targetUrl != null) { result['file'] = targetUrl; @@ -342,9 +411,9 @@ return newValue; } - _segmentError(int seen, int line) => new StateError( - 'Invalid entry in sourcemap, expected 1, 4, or 5' - ' values, but got $seen.\ntargeturl: $targetUrl, line: $line'); + _segmentError(int seen, int line) => + new StateError('Invalid entry in sourcemap, expected 1, 4, or 5' + ' values, but got $seen.\ntargeturl: $targetUrl, line: $line'); /// Returns [TargetLineEntry] which includes the location in the target [line] /// number. In particular, the resulting entry is the last entry whose line @@ -367,7 +436,8 @@ return (index <= 0) ? null : entries[index - 1]; } - SourceMapSpan spanFor(int line, int column, {Map<String, SourceFile> files}) { + SourceMapSpan spanFor(int line, int column, + {Map<String, SourceFile> files, String uri}) { var entry = _findColumn(line, column, _findLine(line)); if (entry == null || entry.sourceUrlId == null) return null; var url = urls[entry.sourceUrlId]; @@ -402,17 +472,18 @@ String toString() { return (new StringBuffer("$runtimeType : [") - ..write('targetUrl: ') - ..write(targetUrl) - ..write(', sourceRoot: ') - ..write(sourceRoot) - ..write(', urls: ') - ..write(urls) - ..write(', names: ') - ..write(names) - ..write(', lines: ') - ..write(lines) - ..write(']')).toString(); + ..write('targetUrl: ') + ..write(targetUrl) + ..write(', sourceRoot: ') + ..write(sourceRoot) + ..write(', urls: ') + ..write(urls) + ..write(', names: ') + ..write(names) + ..write(', lines: ') + ..write(lines) + ..write(']')) + .toString(); } String get debugString { @@ -420,24 +491,24 @@ for (var lineEntry in lines) { var line = lineEntry.line; for (var entry in lineEntry.entries) { - buff..write(targetUrl) - ..write(': ') - ..write(line) - ..write(':') - ..write(entry.column); + buff + ..write(targetUrl) + ..write(': ') + ..write(line) + ..write(':') + ..write(entry.column); if (entry.sourceUrlId != null) { - buff..write(' --> ') - ..write(sourceRoot) - ..write(urls[entry.sourceUrlId]) - ..write(': ') - ..write(entry.sourceLine) - ..write(':') - ..write(entry.sourceColumn); + buff + ..write(' --> ') + ..write(sourceRoot) + ..write(urls[entry.sourceUrlId]) + ..write(': ') + ..write(entry.sourceLine) + ..write(':') + ..write(entry.sourceColumn); } if (entry.sourceNameId != null) { - buff..write(' (') - ..write(names[entry.sourceNameId]) - ..write(')'); + buff..write(' (')..write(names[entry.sourceNameId])..write(')'); } buff.write('\n'); } @@ -463,8 +534,11 @@ final int sourceColumn; final int sourceNameId; - TargetEntry(this.column, [this.sourceUrlId, this.sourceLine, - this.sourceColumn, this.sourceNameId]); + TargetEntry(this.column, + [this.sourceUrlId, + this.sourceLine, + this.sourceColumn, + this.sourceNameId]); String toString() => '$runtimeType: ' '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)'; @@ -482,7 +556,7 @@ // Iterator API is used by decodeVlq to consume VLQ entries. bool moveNext() => ++index < _length; String get current => - (index >= 0 && index < _length) ? _internal[index] : null; + (index >= 0 && index < _length) ? _internal[index] : null; bool get hasTokens => index < _length - 1 && _length > 0; @@ -495,8 +569,13 @@ } int _consumeValue() => decodeVlq(this); - void _consumeNewLine() { ++index; } - void _consumeNewSegment() { ++index; } + void _consumeNewLine() { + ++index; + } + + void _consumeNewSegment() { + ++index; + } // Print the state of the iterator, with colors indicating the current // position.
diff --git a/pkgs/source_maps/pubspec.yaml b/pkgs/source_maps/pubspec.yaml index 0aea12b..33626c5 100644 --- a/pkgs/source_maps/pubspec.yaml +++ b/pkgs/source_maps/pubspec.yaml
@@ -1,5 +1,5 @@ name: source_maps -version: 0.10.1+2 +version: 0.10.1+3 author: Dart Team <misc@dartlang.org> description: Library to programmatically manipulate source map files. homepage: http://github.com/dart-lang/source_maps
diff --git a/pkgs/source_maps/test/parser_test.dart b/pkgs/source_maps/test/parser_test.dart index 88da8c5..9448ea0 100644 --- a/pkgs/source_maps/test/parser_test.dart +++ b/pkgs/source_maps/test/parser_test.dart
@@ -7,35 +7,59 @@ import 'dart:convert'; import 'package:test/test.dart'; import 'package:source_maps/source_maps.dart'; +import 'package:source_span/source_span.dart'; import 'common.dart'; const Map<String, dynamic> MAP_WITH_NO_SOURCE_LOCATION = const { - 'version': 3, - 'sourceRoot': '', - 'sources': const ['input.dart'], - 'names': const [], - 'mappings': 'A', - 'file': 'output.dart' + 'version': 3, + 'sourceRoot': '', + 'sources': const ['input.dart'], + 'names': const [], + 'mappings': 'A', + 'file': 'output.dart' }; const Map<String, dynamic> MAP_WITH_SOURCE_LOCATION = const { - 'version': 3, - 'sourceRoot': '', - 'sources': const ['input.dart'], - 'names': const [], - 'mappings': 'AAAA', - 'file': 'output.dart' + 'version': 3, + 'sourceRoot': '', + 'sources': const ['input.dart'], + 'names': const [], + 'mappings': 'AAAA', + 'file': 'output.dart' }; const Map<String, dynamic> MAP_WITH_SOURCE_LOCATION_AND_NAME = const { - 'version': 3, - 'sourceRoot': '', - 'sources': const ['input.dart'], - 'names': const ['var'], - 'mappings': 'AAAAA', - 'file': 'output.dart' + 'version': 3, + 'sourceRoot': '', + 'sources': const ['input.dart'], + 'names': const ['var'], + 'mappings': 'AAAAA', + 'file': 'output.dart' }; +const Map<String, dynamic> MAP_WITH_SOURCE_LOCATION_AND_NAME_1 = const { + 'version': 3, + 'sourceRoot': 'pkg/', + 'sources': const ['input1.dart'], + 'names': const ['var1'], + 'mappings': 'AAAAA', + 'file': 'output1.dart' +}; + +const Map<String, dynamic> MAP_WITH_SOURCE_LOCATION_AND_NAME_2 = const { + 'version': 3, + 'sourceRoot': 'pkg/', + 'sources': const ['input2.dart'], + 'names': const ['var2'], + 'mappings': 'AAAAA', + 'file': 'output2.dart' +}; + +const List SOURCE_MAP_BUNDLE = const [ + MAP_WITH_SOURCE_LOCATION_AND_NAME_1, + MAP_WITH_SOURCE_LOCATION_AND_NAME_2 +]; + main() { test('parse', () { var mapping = parseJson(EXPECTED_MAP); @@ -105,6 +129,12 @@ inputMap['sourceRoot'] = '/pkg/'; var mapping = parseJson(inputMap); expect(mapping.spanFor(0, 0).sourceUrl, Uri.parse("/pkg/input.dart")); + expect( + mapping + .spanForLocation( + new SourceLocation(0, sourceUrl: Uri.parse("ignored.dart"))) + .sourceUrl, + Uri.parse("/pkg/input.dart")); var newSourceRoot = '/new/'; @@ -122,14 +152,99 @@ Uri.parse("file:///path/to/pkg/input.dart")); }); + group('parse with bundle', () { + var mapping = + parseJsonExtended(SOURCE_MAP_BUNDLE, mapUrl: "file:///path/to/map"); + test('simple', () { + expect( + mapping + .spanForLocation(new SourceLocation(0, + sourceUrl: new Uri.file('/path/to/output1.dart'))) + .sourceUrl, + Uri.parse("file:///path/to/pkg/input1.dart")); + expect( + mapping + .spanForLocation(new SourceLocation(0, + sourceUrl: new Uri.file('/path/to/output2.dart'))) + .sourceUrl, + Uri.parse("file:///path/to/pkg/input2.dart")); + + expect( + mapping.spanFor(0, 0, uri: "file:///path/to/output1.dart").sourceUrl, + Uri.parse("file:///path/to/pkg/input1.dart")); + expect( + mapping.spanFor(0, 0, uri: "file:///path/to/output2.dart").sourceUrl, + Uri.parse("file:///path/to/pkg/input2.dart")); + }); + + test('unmapped path', () { + expect(mapping.spanFor(0, 0, uri: "unmapped_output.dart"), isNull); + }); + + test('missing path', () { + expect(() => mapping.spanFor(0, 0), throws); + }); + + test('incomplete paths', () { + expect(mapping.spanFor(0, 0, uri: "output1.dart").sourceUrl, + Uri.parse("file:///path/to/pkg/input1.dart")); + expect(mapping.spanFor(0, 0, uri: "output2.dart").sourceUrl, + Uri.parse("file:///path/to/pkg/input2.dart")); + }); + + test('parseExtended', () { + var mapping = parseExtended(JSON.encode(SOURCE_MAP_BUNDLE), + mapUrl: "file:///path/to/map"); + + expect(mapping.spanFor(0, 0, uri: "output1.dart").sourceUrl, + Uri.parse("file:///path/to/pkg/input1.dart")); + expect(mapping.spanFor(0, 0, uri: "output2.dart").sourceUrl, + Uri.parse("file:///path/to/pkg/input2.dart")); + }); + + // Test that the source map can handle cases where the uri passed in is + // not from the expected host but it is still unambiguous which source + // map should be used. + test('different paths', () { + expect( + mapping + .spanForLocation(new SourceLocation(0, + sourceUrl: Uri.parse('http://localhost/output1.dart'))) + .sourceUrl, + Uri.parse("file:///path/to/pkg/input1.dart")); + expect( + mapping + .spanForLocation(new SourceLocation(0, + sourceUrl: Uri.parse('http://localhost/output2.dart'))) + .sourceUrl, + Uri.parse("file:///path/to/pkg/input2.dart")); + + expect( + mapping.spanFor(0, 0, uri: "http://localhost/output1.dart").sourceUrl, + Uri.parse("file:///path/to/pkg/input1.dart")); + expect( + mapping.spanFor(0, 0, uri: "http://localhost/output2.dart").sourceUrl, + Uri.parse("file:///path/to/pkg/input2.dart")); + }); + }); + test('parse and re-emit', () { for (var expected in [ - EXPECTED_MAP, - MAP_WITH_NO_SOURCE_LOCATION, - MAP_WITH_SOURCE_LOCATION, - MAP_WITH_SOURCE_LOCATION_AND_NAME]) { + EXPECTED_MAP, + MAP_WITH_NO_SOURCE_LOCATION, + MAP_WITH_SOURCE_LOCATION, + MAP_WITH_SOURCE_LOCATION_AND_NAME + ]) { var mapping = parseJson(expected); expect(mapping.toJson(), equals(expected)); + + mapping = parseJsonExtended(expected); + expect(mapping.toJson(), equals(expected)); } + // Invalid for this case + expect(() => parseJson(SOURCE_MAP_BUNDLE), throws); + + var mapping = parseJsonExtended(SOURCE_MAP_BUNDLE); + expect(mapping.toJson(), equals(SOURCE_MAP_BUNDLE)); }); }