diff options
author | Yigit Boyar <yboyar@google.com> | 2015-06-25 10:50:24 -0700 |
---|---|---|
committer | Yigit Boyar <yboyar@google.com> | 2015-06-30 10:19:08 -0700 |
commit | 731b74f7f44e67312a1fc4161c4e0aae221b2417 (patch) | |
tree | 62ff3308d8e30d00568db694469c54caacff90d1 /compilerCommon | |
parent | 4df4ba38a62b791bbbc25e923efe8d9c2f9a52e9 (diff) | |
download | data-binding-731b74f7f44e67312a1fc4161c4e0aae221b2417.tar.gz |
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
Diffstat (limited to 'compilerCommon')
13 files changed, 812 insertions, 47 deletions
diff --git a/compilerCommon/build.gradle b/compilerCommon/build.gradle index 4e3c5d38..bc59c721 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 @@ sourceSets { 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 00000000..d1d103de --- /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 00000000..723fd0a7 --- /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 00000000..dfd20397 --- /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 00000000..73f42a9d --- /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 00000000..43452d70 --- /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 00000000..ee7f8a92 --- /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 00000000..7c3cb829 --- /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 1805729e..422cd7e2 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 org.xml.sax.SAXException; 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 @@ public class LayoutFileParser { 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; + if (hasMergeInclude(rootView)) { + L.e(ErrorMessages.INCLUDE_INSIDE_MERGE); + return null; + } + boolean isMerge = "merge".equals(rootView.elmName.getText()); + + 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(); } - 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; } private void parseExpressions(String newTag, final XMLParser.ElementContext rootView, @@ -215,8 +237,16 @@ public class LayoutFileParser { 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 9fbd2c2c..3342016c 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 class Location { public int endLine; @XmlAttribute(name = "endOffset") public int endOffset; - @XmlElement + @XmlElement(name = "parentLocation") public Location parentLocation; // for XML unmarshalling @@ -48,6 +49,13 @@ public class Location { 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 @@ public class Location { 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 @@ public class Location { 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 @@ public class Location { 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 5e1232f0..be8a4fad 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.XmlAttribute; 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 class ResourceBundle implements Serializable { 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 class ResourceBundle implements Serializable { 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 class ResourceBundle implements Serializable { public String getModulePackage() { return mModulePackage; } + + public String getAbsoluteFilePath() { + return mAbsoluteFilePath; + } } @XmlAccessorType(XmlAccessType.NONE) @@ -527,7 +537,7 @@ public class ResourceBundle implements Serializable { } @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 @@ public class ResourceBundle implements Serializable { 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 @@ public class ResourceBundle implements Serializable { 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 class ResourceBundle implements Serializable { 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 478ff588..fe888ca2 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 @@ package android.databinding.tool.util; 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 @@ public class L { 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 class L { } } + 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 00000000..195febfd --- /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()); + } +} |