diff options
author | Yigit Boyar <yboyar@google.com> | 2015-07-01 13:22:48 -0700 |
---|---|---|
committer | Yigit Boyar <yboyar@google.com> | 2015-07-06 16:49:02 -0700 |
commit | aafbe5a2394ff9826201cca97d3298a9f355e311 (patch) | |
tree | beb9afa55e41eed654b42356588bf0f1ae9878ec /compilerCommon | |
parent | b15fd21ad1821b5e6a1c0c4977bc24c01d6bc7ce (diff) | |
download | data-binding-aafbe5a2394ff9826201cca97d3298a9f355e311.tar.gz |
Add errors for layout files in different configs
This CL adds ScopedExceptions to multi-config layout parsing.
Bug: 21953001
Change-Id: I5d62b711120e890733e7f5c08108041109e4eefd
Diffstat (limited to 'compilerCommon')
5 files changed, 302 insertions, 86 deletions
diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java index d1d103de..a4ed1cbb 100644 --- a/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java +++ b/compilerCommon/src/main/java/android/databinding/tool/processing/ErrorMessages.java @@ -25,4 +25,15 @@ public class ErrorMessages { "Cannot find the setter for attribute '%s' with parameter type %s."; public static final String CANNOT_RESOLVE_TYPE = "Cannot resolve type for %s"; + public static final String MULTI_CONFIG_LAYOUT_CLASS_NAME_MISMATCH = + "Classname (%s) does not match the class name defined for layout(%s) in other" + + " configurations"; + public static final String MULTI_CONFIG_VARIABLE_TYPE_MISMATCH = + "Variable declaration (%s - %s) does not match the type defined for layout(%s) in other" + + " configurations"; + public static final String MULTI_CONFIG_IMPORT_TYPE_MISMATCH = + "Import declaration (%s - %s) does not match the import defined for layout(%s) in other" + + " configurations"; + public static final String MULTI_CONFIG_ID_USED_AS_IMPORT = + "Cannot use the same id (%s) for a View and an include tag."; } diff --git a/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java b/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java index 723fd0a7..35668cd5 100644 --- a/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java +++ b/compilerCommon/src/main/java/android/databinding/tool/processing/Scope.java @@ -35,6 +35,14 @@ public class Scope { private static ThreadLocal<ScopeEntry> sScopeItems = new ThreadLocal<ScopeEntry>(); static List<ScopedException> sDeferredExceptions = new ArrayList<>(); + public static void enter(final Location location) { + enter(new LocationScopeProvider() { + @Override + public List<Location> provideScopeLocation() { + return Arrays.asList(location); + } + }); + } public static void enter(ScopeProvider scopeProvider) { ScopeEntry peek = sScopeItems.get(); ScopeEntry entry = new ScopeEntry(scopeProvider, peek); @@ -51,6 +59,39 @@ public class Scope { sDeferredExceptions.add(exception); } + private static void registerErrorInternal(String msg, int scopeIndex, + ScopeProvider... scopeProviders) { + if (scopeProviders == null || scopeProviders.length <= scopeIndex) { + defer(new ScopedException(msg)); + } else if (scopeProviders[scopeIndex] == null) { + registerErrorInternal(msg, scopeIndex + 1, scopeProviders); + } else { + try { + Scope.enter(scopeProviders[scopeIndex]); + registerErrorInternal(msg, scopeIndex + 1, scopeProviders); + } finally { + Scope.exit(); + } + } + } + + /** + * Convenience method to add an error in a known list of scopes, w/o adding try catch flows. + * <p> + * This code actually starts entering the given scopes 1 by 1, starting from 0. When list is + * consumed, it creates the exception and defers if possible then exits from the provided + * scopes. + * <p> + * Note that these scopes are added on top of the already existing scopes. + * + * @param msg The exception message + * @param scopeProviders The list of additional scope providers to enter. Null scopes are + * automatically ignored. + */ + public static void registerError(String msg, ScopeProvider... scopeProviders) { + registerErrorInternal(msg, 0, scopeProviders); + } + public static void assertNoError() { if (sDeferredExceptions.isEmpty()) { return; 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 422cd7e2..91be28f6 100644 --- a/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java +++ b/compilerCommon/src/main/java/android/databinding/tool/store/LayoutFileParser.java @@ -298,9 +298,18 @@ public class LayoutFileParser { variable.toStringTree(), xml); bundle.addVariable(name, type, new Location(variable)); } - final String className = attributeMap(data).get("class"); - if (StringUtils.isNotBlank(className)) { - bundle.setBindingClass(className); + final XMLParser.AttributeContext className = findAttribute(data, "class"); + if (className != null) { + final String name = escapeQuotes(className.attrValue.getText(), true); + if (StringUtils.isNotBlank(name)) { + Location location = new Location( + className.attrValue.getLine() - 1, + className.attrValue.getCharPositionInLine() + 1, + className.attrValue.getLine() - 1, + className.attrValue.getCharPositionInLine() + name.length() + ); + bundle.setBindingClass(name, location); + } } } @@ -443,6 +452,16 @@ public class LayoutFileParser { return result; } + private static XMLParser.AttributeContext findAttribute(XMLParser.ElementContext element, + String name) { + for (XMLParser.AttributeContext attr : element.attribute()) { + if (escapeQuotes(attr.attrName.getText(), false).equals(name)) { + return attr; + } + } + return null; + } + private static String escapeQuotes(String textWithQuotes, boolean unescapeValue) { char first = textWithQuotes.charAt(0); int start = 0, end = textWithQuotes.length(); 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 3342016c..d88f0de7 100644 --- a/compilerCommon/src/main/java/android/databinding/tool/store/Location.java +++ b/compilerCommon/src/main/java/android/databinding/tool/store/Location.java @@ -19,6 +19,11 @@ import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.Token; import org.apache.commons.lang3.StringUtils; +import android.databinding.tool.processing.scopes.LocationScopeProvider; + +import java.util.Arrays; +import java.util.List; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; @@ -223,4 +228,13 @@ public class Location { into[1] = Integer.parseInt(content.substring(index + 1).trim()); return true; } + + public LocationScopeProvider createScope() { + return new LocationScopeProvider() { + @Override + public List<Location> provideScopeLocation() { + return Arrays.asList(Location.this); + } + }; + } } 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 be8a4fad..74a06762 100644 --- a/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java +++ b/compilerCommon/src/main/java/android/databinding/tool/store/ResourceBundle.java @@ -13,10 +13,12 @@ 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.ErrorMessages; +import android.databinding.tool.processing.Scope; +import android.databinding.tool.processing.ScopedException; +import android.databinding.tool.processing.scopes.FileScopeProvider; import android.databinding.tool.processing.scopes.LocationScopeProvider; import android.databinding.tool.util.L; import android.databinding.tool.util.ParserHelper; @@ -103,43 +105,30 @@ public class ResourceBundle implements Serializable { if (bundles.getValue().size() < 2) { continue; } + // validate all ids are in correct view types // and all variables have the same name - Map<String, NameTypeLocation> variableTypes = new HashMap<String, NameTypeLocation>(); - Map<String, NameTypeLocation> importTypes = new HashMap<String, NameTypeLocation>(); - String bindingClass = null; - for (LayoutFileBundle bundle : bundles.getValue()) { bundle.mHasVariations = true; - if (bindingClass == null) { - bindingClass = bundle.getFullBindingClass(); - } else { - if (!bindingClass.equals(bundle.getFullBindingClass())) { - L.e("Binding class names must match. Layout file for %s have " + - "different binding class names %s and %s", - bundle.getFileName(), - bindingClass, bundle.getFullBindingClass()); - } - } - for (NameTypeLocation variable : bundle.mVariables) { - NameTypeLocation existing = variableTypes.get(variable.name); - if (existing != null && !existing.equals(variable)) { - L.e("inconsistent variable types for %s for layout %s", - variable, bundle.mFileName); - continue; - } - variableTypes.put(variable.name, variable); - } - for (NameTypeLocation userImport : bundle.mImports) { - NameTypeLocation existing = importTypes.get(userImport.name); - if (existing != null && !existing.equals(userImport)) { - L.e("inconsistent variable types for %s for layout %s", - userImport, bundle.mFileName); - continue; - } - importTypes.put(userImport.name, userImport); - } } + String bindingClass = validateAndGetSharedClassName(bundles.getValue()); + Map<String, NameTypeLocation> variableTypes = validateAndMergeNameTypeLocations( + bundles.getValue(), ErrorMessages.MULTI_CONFIG_VARIABLE_TYPE_MISMATCH, + new ValidateAndFilterCallback() { + @Override + public List<NameTypeLocation> get(LayoutFileBundle bundle) { + return bundle.mVariables; + } + }); + + Map<String, NameTypeLocation> importTypes = validateAndMergeNameTypeLocations( + bundles.getValue(), ErrorMessages.MULTI_CONFIG_IMPORT_TYPE_MISMATCH, + new ValidateAndFilterCallback() { + @Override + public List<NameTypeLocation> get(LayoutFileBundle bundle) { + return bundle.mImports; + } + }); for (LayoutFileBundle bundle : bundles.getValue()) { // now add missing ones to each to ensure they can be referenced @@ -166,65 +155,102 @@ public class ResourceBundle implements Serializable { Map<String, String> viewTypes = new HashMap<String, String>(); Map<String, String> includes = new HashMap<String, String>(); L.d("validating ids for %s", bundles.getKey()); + Set<String> conflictingIds = new HashSet<>(); for (LayoutFileBundle bundle : bundles.getValue()) { - for (BindingTargetBundle target : bundle.mBindingTargetBundles) { - L.d("checking %s %s %s", target.getId(), target.getFullClassName(), - target.isBinder()); - if (target.mId != null) { - if (target.isBinder()) { - if (viewBindingIds.contains(target.getFullClassName())) { - L.e("Cannot use the same id for a View and an include tag. Error " + - "in file %s / %s", bundle.mFileName, bundle.mConfigName); - } - includeBindingIds.add(target.getFullClassName()); - } else { - if (includeBindingIds.contains(target.getFullClassName())) { - L.e("Cannot use the same id for a View and an include tag. Error in" - + " file %s / %s", bundle.mFileName, bundle.mConfigName); + try { + Scope.enter(bundle); + for (BindingTargetBundle target : bundle.mBindingTargetBundles) { + try { + Scope.enter(target); + L.d("checking %s %s %s", target.getId(), target.getFullClassName(), + target.isBinder()); + if (target.mId != null) { + if (target.isBinder()) { + if (viewBindingIds.contains(target.mId)) { + L.d("%s is conflicting", target.mId); + conflictingIds.add(target.mId); + continue; + } + includeBindingIds.add(target.mId); + } else { + if (includeBindingIds.contains(target.mId)) { + L.d("%s is conflicting", target.mId); + conflictingIds.add(target.mId); + continue; + } + viewBindingIds.add(target.mId); + } + String existingType = viewTypes.get(target.mId); + if (existingType == null) { + L.d("assigning %s as %s", target.getId(), + target.getFullClassName()); + viewTypes.put(target.mId, target.getFullClassName()); + if (target.isBinder()) { + includes.put(target.mId, target.getIncludedLayout()); + } + } else if (!existingType.equals(target.getFullClassName())) { + if (target.isBinder()) { + L.d("overriding %s as base binder", target.getId()); + viewTypes.put(target.mId, + "android.databinding.ViewDataBinding"); + includes.put(target.mId, target.getIncludedLayout()); + } else { + L.d("overriding %s as base view", target.getId()); + viewTypes.put(target.mId, "android.view.View"); + } + } } - viewBindingIds.add(target.getFullClassName()); + } catch (ScopedException ex) { + Scope.defer(ex); + } finally { + Scope.exit(); } - String existingType = viewTypes.get(target.mId); - if (existingType == null) { - L.d("assigning %s as %s", target.getId(), target.getFullClassName()); - viewTypes.put(target.mId, target.getFullClassName()); - if (target.isBinder()) { - includes.put(target.mId, target.getIncludedLayout()); - } - } else if (!existingType.equals(target.getFullClassName())) { - if (target.isBinder()) { - L.d("overriding %s as base binder", target.getId()); - viewTypes.put(target.mId, - "android.databinding.ViewDataBinding"); - includes.put(target.mId, target.getIncludedLayout()); - } else { - L.d("overriding %s as base view", target.getId()); - viewTypes.put(target.mId, "android.view.View"); - } + } + } finally { + Scope.exit(); + } + } + + if (!conflictingIds.isEmpty()) { + for (LayoutFileBundle bundle : bundles.getValue()) { + for (BindingTargetBundle target : bundle.mBindingTargetBundles) { + if (conflictingIds.contains(target.mId)) { + Scope.registerError(String.format( + ErrorMessages.MULTI_CONFIG_ID_USED_AS_IMPORT, + target.mId), bundle, target); } } } } for (LayoutFileBundle bundle : bundles.getValue()) { - for (Map.Entry<String, String> viewType : viewTypes.entrySet()) { - BindingTargetBundle target = bundle.getBindingTargetById(viewType.getKey()); - if (target == null) { - String include = includes.get(viewType.getKey()); - if (include == null) { - bundle.createBindingTarget(viewType.getKey(), viewType.getValue(), - false, null, null, null); + try { + Scope.enter(bundle); + for (Map.Entry<String, String> viewType : viewTypes.entrySet()) { + BindingTargetBundle target = bundle.getBindingTargetById(viewType.getKey()); + if (target == null) { + String include = includes.get(viewType.getKey()); + if (include == null) { + bundle.createBindingTarget(viewType.getKey(), viewType.getValue(), + false, null, null, null); + } else { + BindingTargetBundle bindingTargetBundle = bundle + .createBindingTarget( + viewType.getKey(), null, false, null, null, null); + bindingTargetBundle + .setIncludedLayout(includes.get(viewType.getKey())); + bindingTargetBundle.setInterfaceType(viewType.getValue()); + } } else { - BindingTargetBundle bindingTargetBundle = bundle.createBindingTarget( - viewType.getKey(), null, false, null, null, null); - bindingTargetBundle.setIncludedLayout(includes.get(viewType.getKey())); - bindingTargetBundle.setInterfaceType(viewType.getValue()); + L.d("setting interface type on %s (%s) as %s", target.mId, + target.getFullClassName(), viewType.getValue()); + target.setInterfaceType(viewType.getValue()); } - } else { - L.d("setting interface type on %s (%s) as %s", target.mId, - target.getFullClassName(), viewType.getValue()); - target.setInterfaceType(viewType.getValue()); } + } catch (ScopedException ex) { + Scope.defer(ex); + } finally { + Scope.exit(); } } } @@ -249,9 +275,88 @@ public class ResourceBundle implements Serializable { } } + /** + * Receives a list of bundles which are representations of the same layout file in different + * configurations. + * @param bundles + * @return The map for variables and their types + */ + private Map<String, NameTypeLocation> validateAndMergeNameTypeLocations( + List<LayoutFileBundle> bundles, String errorMessage, + ValidateAndFilterCallback callback) { + Map<String, NameTypeLocation> result = new HashMap<>(); + Set<String> mismatched = new HashSet<>(); + for (LayoutFileBundle bundle : bundles) { + for (NameTypeLocation item : callback.get(bundle)) { + NameTypeLocation existing = result.get(item.name); + if (existing != null && !existing.type.equals(item.type)) { + mismatched.add(item.name); + continue; + } + result.put(item.name, item); + } + } + if (mismatched.isEmpty()) { + return result; + } + // create exceptions. We could get more clever and find the outlier but for now, listing + // each file w/ locations seems enough + for (String mismatch : mismatched) { + for (LayoutFileBundle bundle : bundles) { + NameTypeLocation found = null; + for (NameTypeLocation item : callback.get(bundle)) { + if (mismatch.equals(item.name)) { + found = item; + break; + } + } + if (found == null) { + // variable is not defined in this layout, continue + continue; + } + Scope.registerError(String.format( + errorMessage, found.name, found.type, + bundle.mDirectory + "/" + bundle.getFileName()), bundle, + found.location.createScope()); + } + } + return result; + } + + /** + * Receives a list of bundles which are representations of the same layout file in different + * configurations. + * @param bundles + * @return The shared class name for these bundles + */ + private String validateAndGetSharedClassName(List<LayoutFileBundle> bundles) { + String sharedClassName = null; + boolean hasMismatch = false; + for (LayoutFileBundle bundle : bundles) { + bundle.mHasVariations = true; + String fullBindingClass = bundle.getFullBindingClass(); + if (sharedClassName == null) { + sharedClassName = fullBindingClass; + } else if (!sharedClassName.equals(fullBindingClass)) { + hasMismatch = true; + break; + } + } + if (!hasMismatch) { + return sharedClassName; + } + // generate proper exceptions for each + for (LayoutFileBundle bundle : bundles) { + Scope.registerError(String.format(ErrorMessages.MULTI_CONFIG_LAYOUT_CLASS_NAME_MISMATCH, + bundle.getFullBindingClass(), bundle.mDirectory + "/" + bundle.getFileName()), + bundle, bundle.getClassNameLocationProvider()); + } + return sharedClassName; + } + @XmlAccessorType(XmlAccessType.NONE) @XmlRootElement(name="Layout") - public static class LayoutFileBundle implements Serializable { + public static class LayoutFileBundle implements Serializable, FileScopeProvider { @XmlAttribute(name="layout", required = true) public String mFileName; @XmlAttribute(name="modulePackage", required = true) @@ -264,6 +369,9 @@ public class ResourceBundle implements Serializable { @XmlAttribute(name="bindingClass", required = false) public String mBindingClass; + // The location of the name of the generated class, optional + @XmlElement(name = "ClassNameLocation", required = false) + private Location mClassNameLocation; // The full package and class name as determined from mBindingClass and mModulePackage private String mFullBindingClass; @@ -290,6 +398,8 @@ public class ResourceBundle implements Serializable { @XmlAttribute(name="isMerge", required = true) private boolean mIsMerge; + private LocationScopeProvider mClassNameLocationProvider; + // for XML binding public LayoutFileBundle() { } @@ -303,6 +413,14 @@ public class ResourceBundle implements Serializable { mAbsoluteFilePath = file.getAbsolutePath(); } + public LocationScopeProvider getClassNameLocationProvider() { + if (mClassNameLocationProvider == null && mClassNameLocation != null + && mClassNameLocation.isValid()) { + mClassNameLocationProvider = mClassNameLocation.createScope(); + } + return mClassNameLocationProvider; + } + public void addVariable(String name, String type, Location location) { Preconditions.check(!NameTypeLocation.contains(mVariables, name), "Cannot use same variable name twice. %s in %s", name, location); @@ -373,8 +491,9 @@ public class ResourceBundle implements Serializable { return mBindingClassName; } - public void setBindingClass(String bindingClass) { + public void setBindingClass(String bindingClass, Location location) { mBindingClass = bindingClass; + mClassNameLocation = location; } public String getBindingClassPackage() { @@ -459,6 +578,11 @@ public class ResourceBundle implements Serializable { public String getAbsoluteFilePath() { return mAbsoluteFilePath; } + + @Override + public String provideScopeFilePath() { + return mAbsoluteFilePath; + } } @XmlAccessorType(XmlAccessType.NONE) @@ -707,4 +831,11 @@ public class ResourceBundle implements Serializable { } } } + + /** + * Just an inner callback class to process imports and variables w/ the same code. + */ + private interface ValidateAndFilterCallback { + List<NameTypeLocation> get(LayoutFileBundle bundle); + } } |