Rev package version as the extended source map format is a new feature.

Polish `MappingBundle.spanFor` handling of uris that have a suffix that exactly match a source map in the MappingBundle.

R=nweiz@google.com, sigmund@google.com

Review URL: https://codereview.chromium.org//2574593004 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 425e7ae..d2bed8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.10.2
+ * Support for extended source map format.
+ * Polish `MappingBundle.spanFor` handling of URIs that have a suffix that
+   exactly match a source map in the MappingBundle.
+
 ## 0.10.1+5
  * Fix strong mode warning in test.
 
diff --git a/lib/parser.dart b/lib/parser.dart
index 492e6cf..1c23187 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -8,7 +8,6 @@
 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;
@@ -185,6 +184,8 @@
     for (var map in json) {
       var mapping = parseJson(map, mapUrl: mapUrl) as SingleMapping;
       var targetUrl = mapping.targetUrl;
+      // TODO(jacobr): verify that targetUrl is valid uri instead of a windows
+      // path.
       _mappings[targetUrl] = mapping;
     }
   }
@@ -205,13 +206,29 @@
     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);
+
+    // Find the longest suffix of the uri that matches the sourcemap
+    // where the suffix starts after a path segment boundary.
+    // We consider ":" and "/" as path segment boundaries so that
+    // "package:" uris can be handled with minimal special casing. Having a
+    // few false positive path segment boundaries is not a significant issue
+    // as we prefer the longest matching prefix.
+    // Using package:path `path.split` to find path segment boundaries would
+    // not generate all of the path segment boundaries we want for "package:"
+    // urls as "package:package_name" would be one path segment when we want
+    // "package" and "package_name" to be sepearate path segments.
+
+    bool onBoundary = true;
+    var separatorCodeUnits = ['/'.codeUnitAt(0), ':'.codeUnitAt(0)];
+    for (var i = 0; i < uri.length; ++i) {
+      if (onBoundary) {
+        var candidate = uri.substring(i);
+        if (_mappings.containsKey(candidate)) {
+          return _mappings[candidate]
+              .spanFor(line, column, files: files, uri: candidate);
+        }
+      }
+      onBoundary = separatorCodeUnits.contains(uri.codeUnitAt(i));
     }
 
     // Note: when there is no source map for an uri, this behaves like an
diff --git a/pubspec.yaml b/pubspec.yaml
index 3beb3ea..64c0e9c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: source_maps
-version: 0.10.1+5
+version: 0.10.2
 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/test/parser_test.dart b/test/parser_test.dart
index 4b2d947..3cccf44 100644
--- a/test/parser_test.dart
+++ b/test/parser_test.dart
@@ -43,7 +43,7 @@
   'sources': const ['input1.dart'],
   'names': const ['var1'],
   'mappings': 'AAAAA',
-  'file': 'output1.dart'
+  'file': 'output.dart'
 };
 
 const Map<String, dynamic> MAP_WITH_SOURCE_LOCATION_AND_NAME_2 = const {
@@ -55,9 +55,19 @@
   'file': 'output2.dart'
 };
 
+const Map<String, dynamic> MAP_WITH_SOURCE_LOCATION_AND_NAME_3 = const {
+  'version': 3,
+  'sourceRoot': 'pkg/',
+  'sources': const ['input3.dart'],
+  'names': const ['var3'],
+  'mappings': 'AAAAA',
+  'file': '3/output.dart'
+};
+
 const List SOURCE_MAP_BUNDLE = const [
   MAP_WITH_SOURCE_LOCATION_AND_NAME_1,
-  MAP_WITH_SOURCE_LOCATION_AND_NAME_2
+  MAP_WITH_SOURCE_LOCATION_AND_NAME_2,
+  MAP_WITH_SOURCE_LOCATION_AND_NAME_3,
 ];
 
 main() {
@@ -155,11 +165,12 @@
   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: new Uri.file('/path/to/output.dart')))
               .sourceUrl,
           Uri.parse("file:///path/to/pkg/input1.dart"));
       expect(
@@ -168,13 +179,50 @@
                   sourceUrl: new Uri.file('/path/to/output2.dart')))
               .sourceUrl,
           Uri.parse("file:///path/to/pkg/input2.dart"));
+      expect(
+          mapping
+              .spanForLocation(new SourceLocation(0,
+                  sourceUrl: new Uri.file('/path/to/3/output.dart')))
+              .sourceUrl,
+          Uri.parse("file:///path/to/pkg/input3.dart"));
 
       expect(
-          mapping.spanFor(0, 0, uri: "file:///path/to/output1.dart").sourceUrl,
+          mapping.spanFor(0, 0, uri: "file:///path/to/output.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"));
+      expect(
+          mapping.spanFor(0, 0, uri: "file:///path/to/3/output.dart").sourceUrl,
+          Uri.parse("file:///path/to/pkg/input3.dart"));
+    });
+
+    test('package uris', () {
+      expect(
+          mapping
+              .spanForLocation(new SourceLocation(0,
+                  sourceUrl: Uri.parse('package:1/output.dart')))
+              .sourceUrl,
+          Uri.parse("file:///path/to/pkg/input1.dart"));
+      expect(
+          mapping
+              .spanForLocation(new SourceLocation(0,
+                  sourceUrl: Uri.parse('package:2/output2.dart')))
+              .sourceUrl,
+          Uri.parse("file:///path/to/pkg/input2.dart"));
+      expect(
+          mapping
+              .spanForLocation(new SourceLocation(0,
+                  sourceUrl: Uri.parse('package:3/output.dart')))
+              .sourceUrl,
+          Uri.parse("file:///path/to/pkg/input3.dart"));
+
+      expect(mapping.spanFor(0, 0, uri: "package:1/output.dart").sourceUrl,
+          Uri.parse("file:///path/to/pkg/input1.dart"));
+      expect(mapping.spanFor(0, 0, uri: "package:2/output2.dart").sourceUrl,
+          Uri.parse("file:///path/to/pkg/input2.dart"));
+      expect(mapping.spanFor(0, 0, uri: "package:3/output.dart").sourceUrl,
+          Uri.parse("file:///path/to/pkg/input3.dart"));
     });
 
     test('unmapped path', () {
@@ -194,20 +242,24 @@
     });
 
     test('incomplete paths', () {
-      expect(mapping.spanFor(0, 0, uri: "output1.dart").sourceUrl,
+      expect(mapping.spanFor(0, 0, uri: "output.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"));
+      expect(mapping.spanFor(0, 0, uri: "3/output.dart").sourceUrl,
+          Uri.parse("file:///path/to/pkg/input3.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,
+      expect(mapping.spanFor(0, 0, uri: "output.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"));
+      expect(mapping.spanFor(0, 0, uri: "3/output.dart").sourceUrl,
+          Uri.parse("file:///path/to/pkg/input3.dart"));
     });
 
     // Test that the source map can handle cases where the uri passed in is
@@ -217,7 +269,7 @@
       expect(
           mapping
               .spanForLocation(new SourceLocation(0,
-                  sourceUrl: Uri.parse('http://localhost/output1.dart')))
+                  sourceUrl: Uri.parse('http://localhost/output.dart')))
               .sourceUrl,
           Uri.parse("file:///path/to/pkg/input1.dart"));
       expect(
@@ -226,13 +278,24 @@
                   sourceUrl: Uri.parse('http://localhost/output2.dart')))
               .sourceUrl,
           Uri.parse("file:///path/to/pkg/input2.dart"));
+      expect(
+          mapping
+              .spanForLocation(new SourceLocation(0,
+                  sourceUrl: Uri.parse('http://localhost/3/output.dart')))
+              .sourceUrl,
+          Uri.parse("file:///path/to/pkg/input3.dart"));
 
       expect(
-          mapping.spanFor(0, 0, uri: "http://localhost/output1.dart").sourceUrl,
+          mapping.spanFor(0, 0, uri: "http://localhost/output.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"));
+      expect(
+          mapping
+              .spanFor(0, 0, uri: "http://localhost/3/output.dart")
+              .sourceUrl,
+          Uri.parse("file:///path/to/pkg/input3.dart"));
     });
   });