Introduce Scopes to track logical stack traces

This CL introduces a static class called Scope, which is
used the logical processing stack for data binding.
These scopes are used to generate meaningful error messages
when an error is detected.

Bug: 21953001
Change-Id: I5470a8c4ad94401d34a140762baae9d53c5a0402
diff --git a/compilerCommon/build.gradle b/compilerCommon/build.gradle
index 4e3c5d3..bc59c72 100644
--- a/compilerCommon/build.gradle
+++ b/compilerCommon/build.gradle
@@ -17,7 +17,6 @@
 apply plugin: 'java'
 
 
-version = '1.0'
 sourceCompatibility = config.javaTargetCompatibility
 targetCompatibility = config.javaSourceCompatibility
 repositories {
@@ -32,6 +31,12 @@
             srcDir 'src/main/grammar-gen'
         }
     }
+    test {
+        java {
+            srcDir 'src/test/java'
+        }
+    }
+
 }
 
 dependencies {
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
new file mode 100644
index 0000000..d1d103d
--- /dev/null
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.processing;
+
+public class ErrorMessages {
+    public static final String INCLUDE_INSIDE_MERGE =
+            "Data binding does not support include elements as direct children of a merge element.";
+    public static final String UNDEFINED_VARIABLE =
+            "Identifiers must have user defined types from the XML file. %s is missing it";
+    public static final String CANNOT_FIND_SETTER_CALL =
+            "Cannot find the setter for attribute '%s' with parameter type %s.";
+    public static final String CANNOT_RESOLVE_TYPE =
+            "Cannot resolve type for %s";
+}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java b/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java
new file mode 100644
index 0000000..723fd0a
--- /dev/null
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.processing;
+
+import android.databinding.tool.processing.scopes.FileScopeProvider;
+import android.databinding.tool.processing.scopes.LocationScopeProvider;
+import android.databinding.tool.processing.scopes.ScopeProvider;
+import android.databinding.tool.store.Location;
+import android.databinding.tool.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility class to keep track of "logical" stack traces, which we can use to print better error
+ * reports.
+ */
+public class Scope {
+
+    private static ThreadLocal<ScopeEntry> sScopeItems = new ThreadLocal<ScopeEntry>();
+    static List<ScopedException> sDeferredExceptions = new ArrayList<>();
+
+    public static void enter(ScopeProvider scopeProvider) {
+        ScopeEntry peek = sScopeItems.get();
+        ScopeEntry entry = new ScopeEntry(scopeProvider, peek);
+        sScopeItems.set(entry);
+    }
+
+    public static void exit() {
+        ScopeEntry entry = sScopeItems.get();
+        Preconditions.checkNotNull(entry, "Inconsistent scope exit");
+        sScopeItems.set(entry.mParent);
+    }
+
+    public static void defer(ScopedException exception) {
+        sDeferredExceptions.add(exception);
+    }
+
+    public static void assertNoError() {
+        if (sDeferredExceptions.isEmpty()) {
+            return;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (ScopedException ex : sDeferredExceptions) {
+            sb.append(ex.getMessage()).append("\n");
+        }
+        throw new RuntimeException("Found data binding errors.\n" + sb.toString());
+    }
+
+    static String produceScopeLog() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("full scope log\n");
+        ScopeEntry top = sScopeItems.get();
+        while (top != null) {
+            ScopeProvider provider = top.mProvider;
+            sb.append("---").append(provider).append("\n");
+            if (provider instanceof FileScopeProvider) {
+                sb.append("file:").append(((FileScopeProvider) provider).provideScopeFilePath())
+                        .append("\n");
+            }
+            if (provider instanceof LocationScopeProvider) {
+                LocationScopeProvider loc = (LocationScopeProvider) provider;
+                sb.append("loc:");
+                List<Location> locations = loc.provideScopeLocation();
+                if (locations == null) {
+                    sb.append("null\n");
+                } else {
+                    for (Location location : locations) {
+                        sb.append(location).append("\n");
+                    }
+                }
+            }
+            top = top.mParent;
+        }
+        sb.append("---\n");
+        return sb.toString();
+    }
+
+    static ScopedErrorReport createReport() {
+        ScopeEntry top = sScopeItems.get();
+        String filePath = null;
+        List<Location> locations = null;
+        while (top != null && (filePath == null || locations == null)) {
+            ScopeProvider provider = top.mProvider;
+            if (locations == null && provider instanceof LocationScopeProvider) {
+                locations = findAbsoluteLocationFrom(top, (LocationScopeProvider) provider);
+            }
+            if (filePath == null && provider instanceof FileScopeProvider) {
+                filePath = ((FileScopeProvider) provider).provideScopeFilePath();
+            }
+            top = top.mParent;
+        }
+        return new ScopedErrorReport(filePath, locations);
+    }
+
+    private static List<Location> findAbsoluteLocationFrom(ScopeEntry entry,
+            LocationScopeProvider top) {
+        List<Location> locations = top.provideScopeLocation();
+        if (locations == null || locations.isEmpty()) {
+            return null;
+        }
+        if (locations.size() == 1) {
+            return Arrays.asList(locations.get(0).toAbsoluteLocation());
+        }
+        // We have more than 1 location. Depending on the scope, we may or may not want all of them
+        List<Location> chosen = new ArrayList<>();
+        for (Location location : locations) {
+            Location absLocation = location.toAbsoluteLocation();
+            if (validatedContained(entry.mParent, absLocation)) {
+                chosen.add(absLocation);
+            }
+        }
+        return chosen.isEmpty() ? locations : chosen;
+    }
+
+    private static boolean validatedContained(ScopeEntry parent, Location absLocation) {
+        if (parent == null) {
+            return true;
+        }
+        ScopeProvider provider = parent.mProvider;
+        if (!(provider instanceof LocationScopeProvider)) {
+            return validatedContained(parent.mParent, absLocation);
+        }
+        List<Location> absoluteParents = findAbsoluteLocationFrom(parent,
+                (LocationScopeProvider) provider);
+        for (Location location : absoluteParents) {
+            if (location.contains(absLocation)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static class ScopeEntry {
+
+        ScopeProvider mProvider;
+
+        ScopeEntry mParent;
+
+        public ScopeEntry(ScopeProvider scopeProvider, ScopeEntry parent) {
+            mProvider = scopeProvider;
+            mParent = parent;
+        }
+    }
+}
\ No newline at end of file
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/ScopedErrorReport.java b/compilerCommon/src/main/java/android/databinding/tool/processing/ScopedErrorReport.java
new file mode 100644
index 0000000..dfd2039
--- /dev/null
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/ScopedErrorReport.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.processing;
+
+import org.apache.commons.lang3.StringUtils;
+
+import android.databinding.tool.store.Location;
+
+import java.util.List;
+
+public class ScopedErrorReport {
+
+    private final String mFilePath;
+
+    private final List<Location> mLocations;
+
+    /**
+     * Only created by Scope
+     */
+    ScopedErrorReport(String filePath, List<Location> locations) {
+        mFilePath = filePath;
+        mLocations = locations;
+    }
+
+    public String getFilePath() {
+        return mFilePath;
+    }
+
+    public List<Location> getLocations() {
+        return mLocations;
+    }
+
+    public boolean isValid() {
+        return StringUtils.isNotBlank(mFilePath);
+    }
+
+    public String toUserReadableString() {
+        StringBuilder sb = new StringBuilder();
+        if (mFilePath != null) {
+            sb.append("File:");
+            sb.append(mFilePath);
+        }
+        if (mLocations != null && mLocations.size() > 0) {
+            if (mLocations.size() > 1) {
+                sb.append("Locations:");
+                for (Location location : mLocations) {
+                    sb.append("\n    ").append(location.toUserReadableString());
+                }
+            } else {
+                sb.append("\n    Location: ").append(mLocations.get(0).toUserReadableString());
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/ScopedException.java b/compilerCommon/src/main/java/android/databinding/tool/processing/ScopedException.java
new file mode 100644
index 0000000..73f42a9
--- /dev/null
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/ScopedException.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.processing;
+
+import org.apache.commons.lang3.StringUtils;
+
+import android.databinding.tool.store.Location;
+import android.databinding.tool.util.L;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An exception that contains scope information.
+ */
+public class ScopedException extends RuntimeException {
+    public static final String ERROR_LOG_PREFIX = "****/ data binding error ****";
+    public static final String ERROR_LOG_SUFFIX = "****\\ data binding error ****";
+    public static final String MSG_KEY = "msg:";
+    public static final String LOCATION_KEY = "loc:";
+    public static final String FILE_KEY = "file:";
+    private ScopedErrorReport mScopedErrorReport;
+    private String mScopeLog;
+
+    public ScopedException(String message, Object... args) {
+        super(message == null ? "unknown data binding exception" : String.format(message, args));
+        mScopedErrorReport = Scope.createReport();
+        mScopeLog = L.isDebugEnabled() ? Scope.produceScopeLog() : null;
+    }
+
+    ScopedException(String message, ScopedErrorReport scopedErrorReport) {
+        super(message);
+        mScopedErrorReport = scopedErrorReport;
+    }
+
+    public String getBareMessage() {
+        return super.getMessage();
+    }
+
+    @Override
+    public String getMessage() {
+        ScopedErrorReport scopedError = getScopedErrorReport();
+        StringBuilder sb = new StringBuilder();
+        sb.append(ERROR_LOG_PREFIX)
+                .append(MSG_KEY).append(super.getMessage()).append("\n")
+                .append(FILE_KEY).append(scopedError.getFilePath()).append("\n");
+        if (scopedError.getLocations() != null) {
+            for (Location location : scopedError.getLocations()) {
+                sb.append(LOCATION_KEY).append(location.toUserReadableString()).append("\n");
+            }
+        }
+        sb.append(ERROR_LOG_SUFFIX);
+        return StringUtils.join(StringUtils.split(sb.toString(), System.lineSeparator()), " ");
+    }
+
+    public ScopedErrorReport getScopedErrorReport() {
+        return mScopedErrorReport;
+    }
+
+    public boolean isValid() {
+        return mScopedErrorReport.isValid();
+    }
+
+    public static ScopedException createFromOutput(String output) {
+        String message = "";
+        String file = "";
+        List<Location> locations = new ArrayList<>();
+        int msgStart = output.indexOf(MSG_KEY);
+        if (msgStart < 0) {
+            message = output;
+        } else {
+            int fileStart = output.indexOf(FILE_KEY, msgStart + MSG_KEY.length());
+            if (fileStart < 0) {
+                message = output;
+            } else {
+                message = output.substring(msgStart + MSG_KEY.length(), fileStart);
+                int locStart = output.indexOf(LOCATION_KEY, fileStart + FILE_KEY.length());
+                if (locStart < 0) {
+                    file = output.substring(fileStart + FILE_KEY.length());
+                } else {
+                    file = output.substring(fileStart + FILE_KEY.length(), locStart);
+                    int nextLoc = 0;
+                    while(nextLoc >= 0) {
+                        nextLoc = output.indexOf(LOCATION_KEY, locStart + LOCATION_KEY.length());
+                        Location loc;
+                        if (nextLoc < 0) {
+                            loc = Location.fromUserReadableString(
+                                    output.substring(locStart + LOCATION_KEY.length()));
+                        } else {
+                            loc = Location.fromUserReadableString(
+                                    output.substring(locStart + LOCATION_KEY.length(), nextLoc));
+                        }
+                        if (loc != null && loc.isValid()) {
+                            locations.add(loc);
+                        }
+                        locStart = nextLoc;
+                    }
+                }
+            }
+        }
+        return new ScopedException(message.trim(),
+                new ScopedErrorReport(StringUtils.isEmpty(file) ? null : file.trim(), locations));
+    }
+
+    public static List<ScopedException> extractErrors(String output) {
+        List<ScopedException> errors = new ArrayList<>();
+        int index = output.indexOf(ERROR_LOG_PREFIX);
+        final int limit = output.length();
+        while (index >= 0 && index < limit) {
+            int end = output.indexOf(ERROR_LOG_SUFFIX, index + ERROR_LOG_PREFIX.length());
+            if (end == -1) {
+                errors.add(createFromOutput(output.substring(index + ERROR_LOG_PREFIX.length())));
+                break;
+            } else {
+                errors.add(createFromOutput(output.substring(index + ERROR_LOG_PREFIX.length(),
+                        end)));
+                index = output.indexOf(ERROR_LOG_PREFIX, end + ERROR_LOG_SUFFIX.length());
+            }
+        }
+        return errors;
+    }
+}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/FileScopeProvider.java b/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/FileScopeProvider.java
new file mode 100644
index 0000000..43452d7
--- /dev/null
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/FileScopeProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.processing.scopes;
+
+/**
+ * An item that is tight to a source file.
+ */
+public interface FileScopeProvider extends ScopeProvider {
+    String provideScopeFilePath();
+}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/LocationScopeProvider.java b/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/LocationScopeProvider.java
new file mode 100644
index 0000000..ee7f8a9
--- /dev/null
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/LocationScopeProvider.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.processing.scopes;
+
+
+import android.databinding.tool.store.Location;
+
+import java.util.List;
+
+/**
+ * An item that is tight to locations in a source file.
+ */
+public interface LocationScopeProvider extends ScopeProvider {
+    public List<Location> provideScopeLocation();
+}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/ScopeProvider.java b/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/ScopeProvider.java
new file mode 100644
index 0000000..7c3cb82
--- /dev/null
+++ b/compilerCommon/src/main/java/android/databinding/tool/processing/scopes/ScopeProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.processing.scopes;
+
+/**
+ * Base class for all scopes
+ */
+public interface ScopeProvider {
+
+}
diff --git a/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java b/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
index 1805729..422cd7e 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java
@@ -28,6 +28,9 @@
 import android.databinding.parser.XMLLexer;
 import android.databinding.parser.XMLParser;
 import android.databinding.parser.XMLParserBaseVisitor;
+import android.databinding.tool.processing.ErrorMessages;
+import android.databinding.tool.processing.Scope;
+import android.databinding.tool.processing.scopes.FileScopeProvider;
 import android.databinding.tool.util.L;
 import android.databinding.tool.util.ParserHelper;
 import android.databinding.tool.util.Preconditions;
@@ -64,47 +67,66 @@
 
     private static final String LAYOUT_PREFIX = "@layout/";
 
-    public ResourceBundle.LayoutFileBundle parseXml(File xml, String pkg, int minSdk)
+    public ResourceBundle.LayoutFileBundle parseXml(final File xml, String pkg, int minSdk)
             throws ParserConfigurationException, IOException, SAXException,
             XPathExpressionException {
-        final String xmlNoExtension = ParserHelper.stripExtension(xml.getName());
-        final String newTag = xml.getParentFile().getName() + '/' + xmlNoExtension;
-        File original = stripFileAndGetOriginal(xml, newTag);
-        if (original == null) {
-            L.d("assuming the file is the original for %s", xml.getAbsoluteFile());
-            original = xml;
+        try {
+            Scope.enter(new FileScopeProvider() {
+                @Override
+                public String provideScopeFilePath() {
+                    return xml.getAbsolutePath();
+                }
+            });
+            final String xmlNoExtension = ParserHelper.stripExtension(xml.getName());
+            final String newTag = xml.getParentFile().getName() + '/' + xmlNoExtension;
+            File original = stripFileAndGetOriginal(xml, newTag);
+            if (original == null) {
+                L.d("assuming the file is the original for %s", xml.getAbsoluteFile());
+                original = xml;
+            }
+            return parseXml(original, pkg);
+        } finally {
+            Scope.exit();
         }
-        return parseXml(original, pkg);
     }
 
-    private ResourceBundle.LayoutFileBundle parseXml(File original, String pkg)
+    private ResourceBundle.LayoutFileBundle parseXml(final File original, String pkg)
             throws IOException {
-        final String xmlNoExtension = ParserHelper.stripExtension(original.getName());
-        ANTLRInputStream inputStream = new ANTLRInputStream(new FileReader(original));
-        XMLLexer lexer = new XMLLexer(inputStream);
-        CommonTokenStream tokenStream = new CommonTokenStream(lexer);
-        XMLParser parser = new XMLParser(tokenStream);
-        XMLParser.DocumentContext expr = parser.document();
-        XMLParser.ElementContext root = expr.element();
-        if (!"layout".equals(root.elmName.getText())) {
-            return null;
-        }
-        XMLParser.ElementContext data = getDataNode(root);
-        XMLParser.ElementContext rootView = getViewNode(original, root);
+        try {
+            Scope.enter(new FileScopeProvider() {
+                @Override
+                public String provideScopeFilePath() {
+                    return original.getAbsolutePath();
+                }
+            });
+            final String xmlNoExtension = ParserHelper.stripExtension(original.getName());
+            ANTLRInputStream inputStream = new ANTLRInputStream(new FileReader(original));
+            XMLLexer lexer = new XMLLexer(inputStream);
+            CommonTokenStream tokenStream = new CommonTokenStream(lexer);
+            XMLParser parser = new XMLParser(tokenStream);
+            XMLParser.DocumentContext expr = parser.document();
+            XMLParser.ElementContext root = expr.element();
+            if (!"layout".equals(root.elmName.getText())) {
+                return null;
+            }
+            XMLParser.ElementContext data = getDataNode(root);
+            XMLParser.ElementContext rootView = getViewNode(original, root);
 
-        if (hasMergeInclude(rootView)) {
-            L.e("Data binding does not support include elements as direct children of a " +
-                    "merge element: %s", original.getPath());
-            return null;
-        }
-        boolean isMerge = "merge".equals(rootView.elmName.getText());
+            if (hasMergeInclude(rootView)) {
+                L.e(ErrorMessages.INCLUDE_INSIDE_MERGE);
+                return null;
+            }
+            boolean isMerge = "merge".equals(rootView.elmName.getText());
 
-        ResourceBundle.LayoutFileBundle bundle = new ResourceBundle.LayoutFileBundle(
-                xmlNoExtension, original.getParentFile().getName(), pkg, isMerge);
-        final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
-        parseData(original, data, bundle);
-        parseExpressions(newTag, rootView, isMerge, bundle);
-        return bundle;
+            ResourceBundle.LayoutFileBundle bundle = new ResourceBundle.LayoutFileBundle(original,
+                    xmlNoExtension, original.getParentFile().getName(), pkg, isMerge);
+            final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
+            parseData(original, data, bundle);
+            parseExpressions(newTag, rootView, isMerge, bundle);
+            return bundle;
+        } finally {
+            Scope.exit();
+        }
     }
 
     private void parseExpressions(String newTag, final XMLParser.ElementContext rootView,
@@ -215,8 +237,16 @@
                 if (value.charAt(0) == '@' && value.charAt(1) == '{' &&
                         value.charAt(value.length() - 1) == '}') {
                     final String strippedValue = value.substring(2, value.length() - 1);
+                    Location attrLocation = new Location(attr);
+                    Location valueLocation = new Location();
+                    // offset to 0 based
+                    valueLocation.startLine = attr.attrValue.getLine() - 1;
+                    valueLocation.startOffset = attr.attrValue.getCharPositionInLine() +
+                            attr.attrValue.getText().indexOf(strippedValue);
+                    valueLocation.endLine = attrLocation.endLine;
+                    valueLocation.endOffset = attrLocation.endOffset - 2; // account for: "}
                     bindingTargetBundle.addBinding(escapeQuotes(attr.attrName.getText(), false)
-                            , strippedValue);
+                            , strippedValue, attrLocation, valueLocation);
                 }
             }
         }
diff --git a/compilerCommon/src/main/java/android/databinding/tool/store/Location.java b/compilerCommon/src/main/java/android/databinding/tool/store/Location.java
index 9fbd2c2..3342016 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/store/Location.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/store/Location.java
@@ -17,6 +17,7 @@
 package android.databinding.tool.store;
 import org.antlr.v4.runtime.ParserRuleContext;
 import org.antlr.v4.runtime.Token;
+import org.apache.commons.lang3.StringUtils;
 
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
@@ -40,7 +41,7 @@
     public int endLine;
     @XmlAttribute(name = "endOffset")
     public int endOffset;
-    @XmlElement
+    @XmlElement(name = "parentLocation")
     public Location parentLocation;
 
     // for XML unmarshalling
@@ -48,6 +49,13 @@
         startOffset = endOffset = startLine = endLine = NaN;
     }
 
+    public Location(Location other) {
+        startOffset = other.startOffset;
+        endOffset = other.endOffset;
+        startLine = other.startLine;
+        endLine = other.endLine;
+    }
+
     public Location(Token start, Token end) {
         if (start == null) {
             startLine = startOffset = NaN;
@@ -60,7 +68,10 @@
             endLine = endOffset = NaN;
         } else {
             endLine = end.getLine() - 1; // token lines start from 1
-            endOffset = end.getCharPositionInLine();
+            String endText = end.getText();
+            int lastLineStart = endText.lastIndexOf(System.lineSeparator());
+            String lastLine = lastLineStart < 0 ? endText : endText.substring(lastLineStart + 1);
+            endOffset = end.getCharPositionInLine() + lastLine.length() - 1;//end is inclusive
         }
     }
 
@@ -69,6 +80,24 @@
                 context == null ? null : context.getStop());
     }
 
+    public Location(int startLine, int startOffset, int endLine, int endOffset) {
+        this.startOffset = startOffset;
+        this.startLine = startLine;
+        this.endLine = endLine;
+        this.endOffset = endOffset;
+    }
+
+    @Override
+    public String toString() {
+        return "Location{" +
+                "startLine=" + startLine +
+                ", startOffset=" + startOffset +
+                ", endLine=" + endLine +
+                ", endOffset=" + endOffset +
+                ", parentLocation=" + parentLocation +
+                '}';
+    }
+
     public void setParentLocation(Location parentLocation) {
         this.parentLocation = parentLocation;
     }
@@ -112,4 +141,86 @@
         result = 31 * result + endOffset;
         return result;
     }
+
+    public boolean isValid() {
+        return startLine != NaN && endLine != NaN && startOffset != NaN && endOffset != NaN;
+    }
+
+    public boolean contains(Location other) {
+        if (startLine > other.startLine) {
+            return false;
+        }
+        if (startLine == other.startLine && startOffset > other.startOffset) {
+            return false;
+        }
+        if (endLine < other.endLine) {
+            return false;
+        }
+        if (endLine == other.endLine && endOffset < other.endOffset) {
+            return false;
+        }
+        return true;
+    }
+
+    private Location getValidParentAbsoluteLocation() {
+        if (parentLocation == null) {
+            return null;
+        }
+        if (parentLocation.isValid()) {
+            return parentLocation.toAbsoluteLocation();
+        }
+        return parentLocation.getValidParentAbsoluteLocation();
+    }
+
+    public Location toAbsoluteLocation() {
+        Location absoluteParent = getValidParentAbsoluteLocation();
+        if (absoluteParent == null) {
+            return this;
+        }
+        Location copy = new Location(this);
+        boolean sameLine = copy.startLine == copy.endLine;
+        if (copy.startLine == 0) {
+            copy.startOffset += absoluteParent.startOffset;
+        }
+        if (sameLine) {
+            copy.endOffset += absoluteParent.startOffset;
+        }
+
+        copy.startLine += absoluteParent.startLine;
+        copy.endLine += absoluteParent.startLine;
+        return copy;
+    }
+
+    public String toUserReadableString() {
+        return startLine + ":" + startOffset + " - " + endLine + ":" + endOffset;
+    }
+
+    public static Location fromUserReadableString(String str) {
+        int glue = str.indexOf('-');
+        if (glue == -1) {
+            return new Location();
+        }
+        String start = str.substring(0, glue);
+        String end = str.substring(glue + 1);
+        int[] point = new int[]{-1, -1};
+        Location location = new Location();
+        parsePoint(start, point);
+        location.startLine = point[0];
+        location.startOffset = point[1];
+        point[0] = point[1] = -1;
+        parsePoint(end, point);
+        location.endLine = point[0];
+        location.endOffset = point[1];
+        return location;
+    }
+
+    private static boolean parsePoint(String content, int[] into) {
+        int index = content.indexOf(':');
+        if (index == -1) {
+            return false;
+        }
+        into[0] = Integer.parseInt(content.substring(0, index).trim());
+        into[1] = Integer.parseInt(content.substring(index + 1).trim());
+        return true;
+    }
 }
diff --git a/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java b/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
index 5e1232f..be8a4fa 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java
@@ -13,14 +13,19 @@
 
 package android.databinding.tool.store;
 
+import org.antlr.v4.runtime.Token;
 import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
 
+import android.databinding.tool.processing.scopes.LocationScopeProvider;
 import android.databinding.tool.util.L;
 import android.databinding.tool.util.ParserHelper;
 import android.databinding.tool.util.Preconditions;
 
+import java.io.File;
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -33,8 +38,6 @@
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlElementWrapper;
 import javax.xml.bind.annotation.XmlRootElement;
-import javax.xml.bind.annotation.adapters.XmlAdapter;
-import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 
 /**
  * This is a serializable class that can keep the result of parsing layout files.
@@ -253,6 +256,8 @@
         public String mFileName;
         @XmlAttribute(name="modulePackage", required = true)
         public String mModulePackage;
+        @XmlAttribute(name="absoluteFilePath", required = true)
+        public String mAbsoluteFilePath;
         private String mConfigName;
 
         // The binding class as given by the user
@@ -289,12 +294,13 @@
         public LayoutFileBundle() {
         }
 
-        public LayoutFileBundle(String fileName, String directory, String modulePackage,
-                boolean isMerge) {
+        public LayoutFileBundle(File file, String fileName, String directory,
+                String modulePackage, boolean isMerge) {
             mFileName = fileName;
             mDirectory = directory;
             mModulePackage = modulePackage;
             mIsMerge = isMerge;
+            mAbsoluteFilePath = file.getAbsolutePath();
         }
 
         public void addVariable(String name, String type, Location location) {
@@ -449,6 +455,10 @@
         public String getModulePackage() {
             return mModulePackage;
         }
+
+        public String getAbsoluteFilePath() {
+            return mAbsoluteFilePath;
+        }
     }
 
     @XmlAccessorType(XmlAccessType.NONE)
@@ -527,7 +537,7 @@
     }
 
     @XmlAccessorType(XmlAccessType.NONE)
-    public static class BindingTargetBundle implements Serializable {
+    public static class BindingTargetBundle implements Serializable, LocationScopeProvider {
         // public for XML serialization
 
         @XmlAttribute(name="id")
@@ -562,8 +572,8 @@
             mLocation = location;
         }
 
-        public void addBinding(String name, String expr) {
-            mBindingBundleList.add(new BindingBundle(name, expr));
+        public void addBinding(String name, String expr, Location location, Location valueLocation) {
+            mBindingBundleList.add(new BindingBundle(name, expr, location, valueLocation));
         }
 
         public void setIncludedLayout(String includedLayout) {
@@ -637,17 +647,27 @@
             return mInterfaceType;
         }
 
+        @Override
+        public List<Location> provideScopeLocation() {
+            return mLocation == null ? null : Arrays.asList(mLocation);
+        }
+
         @XmlAccessorType(XmlAccessType.NONE)
         public static class BindingBundle implements Serializable {
 
             private String mName;
             private String mExpr;
+            private Location mLocation;
+            private Location mValueLocation;
 
             public BindingBundle() {}
 
-            public BindingBundle(String name, String expr) {
+            public BindingBundle(String name, String expr, Location location,
+                    Location valueLocation) {
                 mName = name;
                 mExpr = expr;
+                mLocation = location;
+                mValueLocation = valueLocation;
             }
 
             @XmlAttribute(name="attribute", required=true)
@@ -667,6 +687,24 @@
             public void setExpr(String expr) {
                 mExpr = expr;
             }
+
+            @XmlElement(name="Location")
+            public Location getLocation() {
+                return mLocation;
+            }
+
+            public void setLocation(Location location) {
+                mLocation = location;
+            }
+
+            @XmlElement(name="ValueLocation")
+            public Location getValueLocation() {
+                return mValueLocation;
+            }
+
+            public void setValueLocation(Location valueLocation) {
+                mValueLocation = valueLocation;
+            }
         }
     }
 }
diff --git a/compilerCommon/src/main/java/android/databinding/tool/util/L.java b/compilerCommon/src/main/java/android/databinding/tool/util/L.java
index 478ff58..fe888ca 100644
--- a/compilerCommon/src/main/java/android/databinding/tool/util/L.java
+++ b/compilerCommon/src/main/java/android/databinding/tool/util/L.java
@@ -18,6 +18,8 @@
 
 import org.apache.commons.lang3.exception.ExceptionUtils;
 
+import android.databinding.tool.processing.ScopedException;
+
 import javax.tools.Diagnostic;
 import javax.tools.Diagnostic.Kind;
 
@@ -66,13 +68,30 @@
                 String.format(msg, args) + " " + ExceptionUtils.getStackTrace(t));
     }
 
+    private static void tryToThrowScoped(Throwable t, String fullMessage) {
+        if (t instanceof ScopedException) {
+            ScopedException ex = (ScopedException) t;
+            if (ex.isValid()) {
+                throw ex;
+            }
+        }
+        ScopedException ex = new ScopedException(fullMessage);
+        if (ex.isValid()) {
+            throw ex;
+        }
+    }
+
     public static void e(String msg, Object... args) {
-        printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
+        String fullMsg = String.format(msg, args);
+        tryToThrowScoped(null, fullMsg);
+        printMessage(Diagnostic.Kind.ERROR, fullMsg);
     }
 
     public static void e(Throwable t, String msg, Object... args) {
+        String fullMsg = String.format(msg, args);
+        tryToThrowScoped(t, fullMsg);
         printMessage(Diagnostic.Kind.ERROR,
-                String.format(msg, args) + " " + ExceptionUtils.getStackTrace(t));
+                fullMsg + " " + ExceptionUtils.getStackTrace(t));
     }
 
     private static void printMessage(Diagnostic.Kind kind, String message) {
@@ -82,6 +101,10 @@
         }
     }
 
+    public static boolean isDebugEnabled() {
+        return sEnableDebug;
+    }
+
     public static interface Client {
         public void printMessage(Diagnostic.Kind kind, String message);
     }
diff --git a/compilerCommon/src/test/java/android/databinding/tool/store/LocationTest.java b/compilerCommon/src/test/java/android/databinding/tool/store/LocationTest.java
new file mode 100644
index 0000000..195febf
--- /dev/null
+++ b/compilerCommon/src/test/java/android/databinding/tool/store/LocationTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.databinding.tool.store;
+
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+
+public class LocationTest {
+    @Test
+    public void testInvalid() {
+        assertFalse(new Location().isValid());
+    }
+
+    @Test
+    public void testValid() {
+        Location location = new Location(0, 0, 1, 1);
+        assertTrue(location.isValid());
+    }
+
+    @Test
+    public void testContains() {
+        Location location1 = new Location(0, 0, 10, 1);
+        Location location2 = new Location(0, 0, 9, 1);
+        assertTrue(location1.contains(location2));
+        location2.endLine = 10;
+        assertTrue(location1.contains(location2));
+        location2.endOffset = 2;
+        assertFalse(location1.contains(location2));
+    }
+
+    @Test
+    public void testAbsolute() {
+        Location loc = new Location(1, 2, 3, 4);
+        assertEquals(loc, loc.toAbsoluteLocation());
+    }
+
+    @Test
+    public void testAbsoluteWithInvalidParent() {
+        Location loc = new Location(1, 2, 3, 4);
+        loc.setParentLocation(new Location());
+        assertEquals(loc, loc.toAbsoluteLocation());
+    }
+
+    @Test
+    public void testAbsoluteWithParent() {
+        Location loc = new Location(1, 2, 3, 4);
+        loc.setParentLocation(new Location(10, 0, 20, 0));
+        assertEquals(new Location(11, 2, 13, 4), loc.toAbsoluteLocation());
+    }
+
+    @Test
+    public void testAbsoluteWith2Parents() {
+        Location loc = new Location(1, 2, 3, 4);
+        Location parent1 = new Location(5, 6, 10, 11);
+        parent1.setParentLocation(new Location(5, 6, 17, 8));
+        loc.setParentLocation(parent1);
+        assertEquals(new Location(10, 6, 15, 11), parent1.toAbsoluteLocation());
+        assertEquals(new Location(11, 2, 13, 4), loc.toAbsoluteLocation());
+    }
+
+    @Test
+    public void testAbsoluteWithSameLine() {
+        Location loc = new Location(0, 2, 0, 4);
+        loc.setParentLocation(new Location(7, 2, 12, 46));
+        assertEquals(new Location(7, 4, 7, 6), loc.toAbsoluteLocation());
+    }
+}