Fixes how we translate between Java and JavaScript source positions in BuildTypeMap.

This fixes issue #3506, which was caused by computing this incorrectly and overflowing the end of file in some cases.

Issue: 3506
Found by: rice
Review by: bobv

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5220 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java b/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java
index dda0617..4cede89 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/BuildTypeMap.java
@@ -314,6 +314,61 @@
       return process(typeDeclaration);
     }
 
+    /**
+     * JS reports the error as a line number, to find the absolute position in
+     * the real source stream, we have to walk from the absolute JS start
+     * position until we have counted down enough lines. Then we use the column
+     * position to find the exact spot.
+     */
+    private int computeAbsoluteProblemPosition(char[] source, int start,
+        int end, int jsStartLine, SourceDetail detail) {
+      int linesToCount = detail.getLine() - jsStartLine;
+      int i = start;
+      while (linesToCount > 0 && i < end) {
+        switch (source[i]) {
+          case '\r':
+            // if skip an extra character if this is a CR/LF
+            if (i + 1 < end && source[i + 1] == '\n') {
+              ++i;
+            }
+            // intentional fall through
+          case '\n':
+            --linesToCount;
+            // intentional fall through
+          default:
+            ++i;
+        }
+      }
+
+      // Jump to the correct (1-based) column.
+      i += detail.getLineOffset() - 1;
+      return i;
+    }
+
+    private int countLines(char[] source, int p1, int p2) {
+      assert p1 >= 0 && p1 < source.length;
+      assert p2 >= 0 && p2 <= source.length;
+      assert p1 <= p2;
+
+      int lines = 0;
+      while (p1 < p2) {
+        switch (source[p1]) {
+          case '\r':
+            // if skip an extra character if this is a CR/LF
+            if (p1 + 1 < p2 && source[p1 + 1] == '\n') {
+              ++p1;
+            }
+            // intentional fall through
+          case '\n':
+            ++lines;
+            // intentional fall through
+          default:
+            ++p1;
+        }
+      }
+      return lines;
+    }
+
     private JField createEnumField(SourceInfo info, FieldBinding binding,
         JReferenceType enclosingType) {
       JType type = (JType) typeMap.get(binding.type);
@@ -708,10 +763,28 @@
         }
         syntheticFnHeader += param.getName();
       }
-      syntheticFnHeader += ')';
-      StringReader sr = new StringReader(syntheticFnHeader + ' ' + jsniCode);
+      syntheticFnHeader += ") ";
+      StringReader sr = new StringReader(syntheticFnHeader + jsniCode);
+
+      // Absolute start and end position of braces in original source.
+      int absoluteJsStartPos = methodDeclaration.bodyStart + startPos;
+      int absoluteJsEndPos = absoluteJsStartPos + jsniCode.length();
+
+      // Adjust the points the JS parser sees to account for the synth header.
+      int jsStartPos = absoluteJsStartPos - syntheticFnHeader.length();
+      int jsEndPos = absoluteJsEndPos - syntheticFnHeader.length();
+
+      // To compute the start line, count lines from point to point.
+      int jsLine = info.getStartLine()
+          + countLines(source, info.getStartPos(), absoluteJsStartPos);
+
+      SourceInfo jsInfo = program.createSourceInfo(jsStartPos, jsEndPos,
+          jsLine, info.getFileName());
+      jsInfo.copyMissingCorrelationsFrom(info);
+
       try {
-        List<JsStatement> result = JsParser.parse(info, jsProgram.getScope(), sr);
+        List<JsStatement> result = JsParser.parse(jsInfo, jsProgram.getScope(),
+            sr);
         JsExprStmt jsExprStmt = (JsExprStmt) result.get(0);
         JsFunction jsFunction = (JsFunction) jsExprStmt.getExpression();
         jsFunction.setFromJava(true);
@@ -726,38 +799,13 @@
             "Internal error parsing JSNI in method '" + newMethod
                 + "' in type '" + enclosingType.getName() + "'", e);
       } catch (JsParserException e) {
-        /*
-         * count the number of characters to the problem (from the start of the
-         * JSNI code)
-         */
-        SourceDetail detail = e.getSourceDetail();
-        int line = detail.getLine();
-        char[] chars = jsniCode.toCharArray();
-        int i = 0, n = chars.length;
-        while (line > 0) {
-          // CHECKSTYLE_OFF
-          switch (chars[i]) {
-            case '\r':
-              // if skip an extra character if this is a CR/LF
-              if (i + 1 < n && chars[i + 1] == '\n') {
-                ++i;
-              }
-              // intentional fall-through
-            case '\n':
-              --line;
-              // intentional fall-through
-            default:
-              ++i;
-          }
-          // CHECKSTYLE_ON
-        }
-
-        // TODO: check this
-        // Map into the original source stream;
-        i += startPos + detail.getLineOffset();
-        info = program.createSourceInfo(i, i, info.getStartLine()
-            + detail.getLine(), info.getFileName());
-        GenerateJavaAST.reportJsniError(info, methodDeclaration, e.getMessage());
+        int problemCharPos = computeAbsoluteProblemPosition(source,
+            absoluteJsStartPos, absoluteJsEndPos, jsInfo.getStartLine(),
+            e.getSourceDetail());
+        SourceInfo errorInfo = program.createSourceInfo(problemCharPos,
+            problemCharPos, e.getSourceDetail().getLine(), info.getFileName());
+        GenerateJavaAST.reportJsniError(errorInfo, methodDeclaration,
+            e.getMessage());
       }
     }