/** * Computes the cumulative set of ids used in a given layout. We can't just depth-first-search * the graph and check the set of ids encountered along the way, because we need to detect when * multiple includes contribute the same ids. For example, if a file is included more than once, * that would result in duplicates. */ private Set<String> getIds(Layout layout, Deque<Layout> stack, Set<Layout> seen) { seen.add(layout); Set<String> layoutIds = layout.getIds(); List<Layout> includes = layout.getIncludes(); if (includes != null) { Set<String> ids = new HashSet<String>(); if (layoutIds != null) { ids.addAll(layoutIds); } stack.push(layout); Multimap<String, Set<String>> nameToIds = ArrayListMultimap.create(includes.size(), 4); for (Layout included : includes) { if (seen.contains(included)) { continue; } Set<String> includedIds = getIds(included, stack, seen); if (includedIds != null) { String layoutName = included.getLayoutName(); idCheck: for (String id : includedIds) { if (ids.contains(id)) { Collection<Set<String>> idSets = nameToIds.get(layoutName); if (idSets != null) { for (Set<String> siblingIds : idSets) { if (siblingIds.contains(id)) { // The id reference was added by a sibling, // so no need to complain (again) continue idCheck; } } } // Duplicate! Record location request for new phase. if (mLocations == null) { mErrors = new ArrayList<Occurrence>(); mLocations = ArrayListMultimap.create(); mContext .getDriver() .requestRepeat(DuplicateIdDetector.this, Scope.ALL_RESOURCES_SCOPE); } Map<Layout, Occurrence> occurrences = new HashMap<Layout, Occurrence>(); findId(layout, id, new ArrayDeque<Layout>(), occurrences, new HashSet<Layout>()); assert occurrences.size() >= 2; // Stash a request to find the given include Collection<Occurrence> values = occurrences.values(); List<Occurrence> sorted = new ArrayList<Occurrence>(values); Collections.sort(sorted); String msg = String.format( "Duplicate id %1$s, defined or included multiple " + "times in %2$s: %3$s", id, layout.getDisplayName(), sorted.toString()); // Store location request for the <include> tag Occurrence primary = new Occurrence(layout.getFile(), msg, null); Multimap<String, Occurrence> m = ArrayListMultimap.create(); m.put(layoutName, primary); mLocations.put(layout.getFile(), m); mErrors.add(primary); Occurrence prev = primary; // Now store all the included occurrences of the id for (Occurrence occurrence : values) { if (occurrence.file.equals(layout.getFile())) { occurrence.message = "Defined here"; } else { occurrence.message = String.format("Defined here, included via %1$s", occurrence.includePath); } m = ArrayListMultimap.create(); m.put(id, occurrence); mLocations.put(occurrence.file, m); // Link locations together prev.next = occurrence; prev = occurrence; } } ids.add(id); } // Store these ids such that on a conflict, we can tell when // an id was added by a single variation of this file nameToIds.put(layoutName, includedIds); } } Layout visited = stack.pop(); assert visited == layout; return ids; } else { return layoutIds; } }