Fixes in sourcemaps discovered in csslib

R=dgrove@google.com

Review URL: https://codereview.chromium.org//18749005

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/source_maps@24881 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/span.dart b/lib/span.dart
index e7bf5bf..7ca0ca9 100644
--- a/lib/span.dart
+++ b/lib/span.dart
@@ -219,11 +219,20 @@
   int getLine(int offset) => binarySearch(_lineStarts, (o) => o > offset) - 1;
 
   /// Gets the 0-based column corresponding to an offset.
-  int getColumn(int line, int offset) => offset - _lineStarts[line];
+  int getColumn(int line, int offset) {
+    if (line < 0 || line >= _lineStarts.length) return 0;
+    return offset - _lineStarts[line];
+  }
 
   /// Get the offset for a given line and column
-  int getOffset(int line, int column) =>
-      _lineStarts[max(min(line, _lineStarts.length - 1), 0)] + column;
+  int getOffset(int line, int column) {
+    if (line < 0) return getOffset(0, 0);
+    if (line < _lineStarts.length) {
+      return _lineStarts[line] + column;
+    } else {
+      return _decodedChars.length;
+    }
+  }
 
   /// Gets the text at the given offsets.
   String getText(int start, [int end]) =>
@@ -251,7 +260,6 @@
     // +1 for 0-indexing, +1 again to avoid the last line
     var textLine = getText(getOffset(line, 0), getOffset(line + 1, 0));
 
-
     column = min(column, textLine.length - 1);
     int toColumn = min(column + end - start, textLine.length);
     if (useColors) {
@@ -265,6 +273,7 @@
       buf.write(textLine.substring(toColumn));
     } else {
       buf.write(textLine);
+      if (textLine != '' && !textLine.endsWith('\n')) buf.write('\n');
     }
 
     int i = 0;
@@ -290,13 +299,13 @@
   final int _baseOffset;
   final int _baseLine;
   final int _baseColumn;
+  final int _maxOffset;
 
-  // TODO(sigmund): consider providing an end-offset to correctly truncate
-  // values passed the end of the segment.
   SourceFileSegment(String url, String textSegment, Location startOffset)
       : _baseOffset = startOffset.offset,
         _baseLine = startOffset.line,
         _baseColumn = startOffset.column,
+        _maxOffset = startOffset.offset + textSegment.length,
         super.text(url, textSegment);
 
   /// Craete a span, where [start] is relative to this segment's base offset.
@@ -313,9 +322,13 @@
 
   /// Return the line on the underlying file associated with the [offset] of the
   /// underlying file. This method operates on the real offsets from the
-  /// original file, so that error messages can be reported accurately.
-  int getLine(int offset) =>
-      super.getLine(max(offset - _baseOffset, 0)) + _baseLine;
+  /// original file, so that error messages can be reported accurately. When the
+  /// requested offset is past the length of the segment, this returns the line
+  /// number after the end of the segment (total lines + 1).
+  int getLine(int offset) {
+    var res = super.getLine(max(offset - _baseOffset, 0)) + _baseLine;
+    return (offset > _maxOffset) ? res + 1 : res;
+  }
 
   /// Return the column on the underlying file associated with [line] and
   /// [offset], where [line] is absolute from the beginning of the underlying
@@ -330,13 +343,12 @@
   /// on the real offsets from the original file, so that error messages can be
   /// reported accurately.
   int getOffset(int line, int column) =>
-      super.getOffset(line - _baseLine,
-         line == _baseLine ? column - _baseColumn : column) + _baseOffset;
+    super.getOffset(line - _baseLine,
+        line == _baseLine ? column - _baseColumn : column) + _baseOffset;
 
   /// Retrieve the text associated with the specified range. This method
   /// operates on the real offsets from the original file, so that error
   /// messages can be reported accurately.
   String getText(int start, [int end]) =>
-      super.getText(start - _baseOffset,
-          end == null ? null : end - _baseOffset);
+    super.getText(start - _baseOffset, end == null ? null : end - _baseOffset);
 }
diff --git a/test/span_test.dart b/test/span_test.dart
index 66bd8a8..c7b9cde 100644
--- a/test/span_test.dart
+++ b/test/span_test.dart
@@ -69,22 +69,80 @@
     expect(file.getText(line + 2, line + 11), '34+6789_1');
   });
 
-  test('get location message', () {
-    // fifth line (including 4 new lines), columns 2 .. 11
-    var line = 10 + 80 + 31 + 27 + 4;
-    expect(file.getLocationMessage('the message', line + 2, line + 11),
-        'file:5:3: the message\n'
-        '1234+6789_1234\n'
-        '  ^^^^^^^^^');
-  });
+  group('location message', () {
+    test('first line', () {
+      expect(file.getLocationMessage('the message', 1, 3),
+          'file:1:2: the message\n'
+          '+23456789_\n'
+          ' ^^');
+    });
 
-  test('get location message - no file url', () {
-    var line = 10 + 80 + 31 + 27 + 4;
-    expect(new SourceFile.text(null, TEST_FILE).getLocationMessage(
-        'the message', line + 2, line + 11),
-        ':5:3: the message\n'
-        '1234+6789_1234\n'
-        '  ^^^^^^^^^');
+    test('in the middle of the file', () {
+      // fifth line (including 4 new lines), columns 2 .. 11
+      var line = 10 + 80 + 31 + 27 + 4;
+      expect(file.getLocationMessage('the message', line + 2, line + 11),
+          'file:5:3: the message\n'
+          '1234+6789_1234\n'
+          '  ^^^^^^^^^');
+    });
+
+    test('no file url', () {
+      var line = 10 + 80 + 31 + 27 + 4;
+      expect(new SourceFile.text(null, TEST_FILE).getLocationMessage(
+          'the message', line + 2, line + 11),
+          ':5:3: the message\n'
+          '1234+6789_1234\n'
+          '  ^^^^^^^^^');
+    });
+
+    test('penultimate line', () {
+      // We search '\n' backwards twice because last line is \n terminated:
+      int index = TEST_FILE.lastIndexOf('\n');
+      var start = TEST_FILE.lastIndexOf('\n', index - 1) - 3;
+      expect(file.getLocationMessage('the message', start, start + 2),
+          'file:11:41: the message\n'
+          '123456789_+23456789_123456789_123456789_123\n'
+          '                                        ^^');
+    });
+
+    test('last line', () {
+      var start = TEST_FILE.lastIndexOf('\n') - 2;
+      expect(file.getLocationMessage('the message', start, start + 1),
+          'file:12:28: the message\n'
+          '123456789_1+3456789_123456789\n'
+          '                           ^');
+    });
+
+    group('no trailing empty-line at the end -', () {
+      var text = TEST_FILE.substring(0, TEST_FILE.length - 1);
+      var file2 = new SourceFile.text('file', text);
+
+      test('penultimate line', () {
+        var start = text.lastIndexOf('\n') - 3;
+        expect(file2.getLocationMessage('the message', start, start + 2),
+            'file:11:41: the message\n'
+            '123456789_+23456789_123456789_123456789_123\n'
+            '                                        ^^');
+      });
+
+      test('last line', () {
+        var start = text.length - 2;
+        expect(file2.getLocationMessage('the message', start, start + 1),
+            'file:12:28: the message\n'
+            '123456789_1+3456789_123456789\n'
+            '                           ^');
+      });
+    });
+
+    test('single line', () {
+      var text = "this is a single line";
+      int start = text.indexOf(' ') + 1;
+      var file2 = new SourceFile.text('file', text);
+      expect(file2.getLocationMessage('the message', start, start + 2),
+            'file:1:${start + 1}: the message\n'
+            'this is a single line\n'
+            '     ^^');
+    });
   });
 
   test('location getters', () {
@@ -242,17 +300,11 @@
           '123456789_1+3456789_123456789\n'
           '                            ^');
 
-        // TODO(sigmund): consider also fixing this and report file:10:4
         // The answer below is different because the segment parsing only knows
         // about the 10 lines it has (and nothing about the possible extra lines
         // afterwards)
         expect(segment.getLocationMessage('the message', start, start + 1),
-          'file:10:112: the message\n');
-
-        // The number 112 includes the count of extra characters past the
-        // segment, which we verify here:
-        var lastSegmentLineStart = segmentText.lastIndexOf('\n');
-        expect(start - baseOffset - lastSegmentLineStart, 112);
+          'file:11:1: the message\n');
       });
     });
   });