@Override public boolean isIgnored( @NonNull Context context, @NonNull Issue issue, @Nullable Location location, @NonNull String message, @Nullable Object data) { ensureInitialized(); String id = issue.getId(); List<String> paths = mSuppressed.get(id); if (paths == null) { paths = mSuppressed.get(VALUE_ALL); } if (paths != null && location != null) { File file = location.getFile(); String relativePath = context.getProject().getRelativePath(file); for (String suppressedPath : paths) { if (suppressedPath.equals(relativePath)) { return true; } // Also allow a prefix if (relativePath.startsWith(suppressedPath)) { return true; } } } if (mRegexps != null) { List<Pattern> regexps = mRegexps.get(id); if (regexps == null) { regexps = mRegexps.get(VALUE_ALL); } if (regexps != null && location != null) { File file = location.getFile(); String relativePath = context.getProject().getRelativePath(file); boolean checkUnixPath = false; for (Pattern regexp : regexps) { Matcher matcher = regexp.matcher(relativePath); if (matcher.find()) { return true; } else if (regexp.pattern().indexOf('/') != -1) { checkUnixPath = true; } } if (checkUnixPath && CURRENT_PLATFORM == PLATFORM_WINDOWS) { relativePath = relativePath.replace('\\', '/'); for (Pattern regexp : regexps) { Matcher matcher = regexp.matcher(relativePath); if (matcher.find()) { return true; } } } } } return mParent != null && mParent.isIgnored(context, issue, location, message, data); }
@Override public void afterCheckProject(@NonNull Context context) { if (context.getPhase() == 1) { // Look for duplicates if (!mIncludes.isEmpty()) { // Traverse all the include chains and ensure that there are no duplicates // across. if (context.isEnabled(CROSS_LAYOUT) && context.getScope().contains(Scope.ALL_RESOURCE_FILES)) { IncludeGraph graph = new IncludeGraph(context); graph.check(); } } } else { assert context.getPhase() == 2; if (mErrors != null) { for (Occurrence occurrence : mErrors) { // assert location != null : occurrence; Location location = occurrence.location; if (location == null) { location = Location.create(occurrence.file); } else { Object clientData = location.getClientData(); if (clientData instanceof Node) { Node node = (Node) clientData; if (context.getDriver().isSuppressed(CROSS_LAYOUT, node)) { continue; } } } List<Occurrence> sorted = new ArrayList<Occurrence>(); Occurrence curr = occurrence.next; while (curr != null) { sorted.add(curr); curr = curr.next; } Collections.sort(sorted); Location prev = location; for (Occurrence o : sorted) { if (o.location != null) { prev.setSecondary(o.location); prev = o.location; } } context.report(CROSS_LAYOUT, location, occurrence.message, null); } } } }
@Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { // Record include graph such that we can look for inter-layout duplicates after the // project has been fully checked String layout = element.getAttribute(ATTR_LAYOUT); // NOTE: Not in android: namespace if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length()); if (context.getPhase() == 1) { if (!context.getProject().getReportIssues()) { // If this is a library project not being analyzed, ignore it return; } List<String> to = mIncludes.get(context.file); if (to == null) { to = new ArrayList<String>(); mIncludes.put(context.file, to); } to.add(layout); } else { assert context.getPhase() == 2; Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file); if (maps != null && !maps.isEmpty()) { for (Multimap<String, Occurrence> map : maps) { if (!maps.isEmpty()) { Collection<Occurrence> occurrences = map.get(layout); if (occurrences != null && !occurrences.isEmpty()) { for (Occurrence occurrence : occurrences) { Location location = context.getLocation(element); location.setClientData(element); location.setMessage(occurrence.message); location.setSecondary(occurrence.location); occurrence.location = location; } } } } } } } }
@Override public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { assert attribute.getName().equals(ATTR_ID) || attribute.getLocalName().equals(ATTR_ID); String id = attribute.getValue(); if (context.getPhase() == 1) { if (mIds.contains(id)) { Location location = context.getLocation(attribute); Attr first = findIdAttribute(attribute.getOwnerDocument(), id); if (first != null && first != attribute) { Location secondLocation = context.getLocation(first); secondLocation.setMessage(String.format("%1$s originally defined here", id)); location.setSecondary(secondLocation); } context.report( WITHIN_LAYOUT, attribute, location, String.format("Duplicate id %1$s, already defined earlier in this layout", id), null); } else if (id.startsWith(NEW_ID_PREFIX)) { // Skip id's on include tags if (attribute.getOwnerElement().getTagName().equals(VIEW_INCLUDE)) { return; } mIds.add(id); } } else { Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file); if (maps != null && !maps.isEmpty()) { for (Multimap<String, Occurrence> map : maps) { if (!maps.isEmpty()) { Collection<Occurrence> occurrences = map.get(id); if (occurrences != null && !occurrences.isEmpty()) { for (Occurrence occurrence : occurrences) { if (context.getDriver().isSuppressed(CROSS_LAYOUT, attribute)) { return; } Location location = context.getLocation(attribute); location.setClientData(attribute); location.setMessage(occurrence.message); location.setSecondary(occurrence.location); occurrence.location = location; } } } } } } }
@Override public void ignore( @NonNull Context context, @NonNull Issue issue, @Nullable Location location, @NonNull String message) { // This configuration only supports suppressing warnings on a per-file basis if (location != null) { ignore(issue, location.getFile()); } }
private void formatError(String message, Object... args) { if (args != null && args.length > 0) { message = String.format(message, args); } message = "Failed to parse `lint.xml` configuration file: " + message; LintDriver driver = new LintDriver( new IssueRegistry() { @Override @NonNull public List<Issue> getIssues() { return Collections.emptyList(); } }, mClient); mClient.report( new Context(driver, mProject, mProject, mConfigFile), IssueRegistry.LINT_ERROR, mProject.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR), Location.create(mConfigFile), message, TextFormat.RAW); }
private void checkDensities(Context context, File res, Map<File, Set<String>> folderToNames) { // TODO: Is there a way to look at the manifest and figure out whether // all densities are expected to be needed? // Note: ldpi is probably not needed; it has very little usage // (about 2%; http://developer.android.com/resources/dashboard/screens.html) // TODO: Use the matrix to check out if we can eliminate densities based // on the target screens? Set<String> definedDensities = new HashSet<String>(); for (File f : folderToNames.keySet()) { definedDensities.add(f.getName()); } // Look for missing folders -- if you define say drawable-mdpi then you // should also define -hdpi and -xhdpi. if (context.isEnabled(ICON_MISSING_FOLDER)) { List<String> missing = new ArrayList<String>(); for (String density : REQUIRED_DENSITIES) { if (!definedDensities.contains(density)) { missing.add(density); } } if (missing.size() > 0) { context.report( ICON_MISSING_FOLDER, null /* location */, String.format( "Missing density variation folders in %1$s: %2$s", context.getProject().getDisplayPath(res), LintUtils.formatList(missing, missing.size())), null); } } if (context.isEnabled(ICON_NODPI)) { Set<String> noDpiNames = new HashSet<String>(); for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { if (isNoDpiFolder(entry.getKey())) { noDpiNames.addAll(entry.getValue()); } } if (noDpiNames.size() > 0) { // Make sure that none of the nodpi names appear in a non-nodpi folder Set<String> inBoth = new HashSet<String>(); List<File> files = new ArrayList<File>(); for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { File folder = entry.getKey(); String folderName = folder.getName(); if (!isNoDpiFolder(folder)) { assert DENSITY_PATTERN.matcher(folderName).matches(); Set<String> overlap = intersection(noDpiNames, entry.getValue()); inBoth.addAll(overlap); for (String name : overlap) { files.add(new File(folder, name)); } } } if (inBoth.size() > 0) { List<String> list = new ArrayList<String>(inBoth); Collections.sort(list); // Chain locations together Collections.sort(files); Location location = null; for (File file : files) { Location linkedLocation = location; location = Location.create(file); location.setSecondary(linkedLocation); } context.report( ICON_DENSITIES, location, String.format( "The following images appear in both -nodpi and in a density folder: %1$s", LintUtils.formatList(list, 10)), null); } } } if (context.isEnabled(ICON_DENSITIES)) { // Look for folders missing some of the specific assets Set<String> allNames = new HashSet<String>(); for (Entry<File, Set<String>> entry : folderToNames.entrySet()) { if (!isNoDpiFolder(entry.getKey())) { Set<String> names = entry.getValue(); allNames.addAll(names); } } for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { File file = entry.getKey(); if (isNoDpiFolder(file)) { continue; } Set<String> names = entry.getValue(); if (names.size() != allNames.size()) { List<String> delta = new ArrayList<String>(difference(allNames, names)); Collections.sort(delta); String foundIn = ""; if (delta.size() == 1) { // Produce list of where the icon is actually defined List<String> defined = new ArrayList<String>(); String name = delta.get(0); for (Map.Entry<File, Set<String>> e : folderToNames.entrySet()) { if (e.getValue().contains(name)) { defined.add(e.getKey().getName()); } } if (defined.size() > 0) { foundIn = String.format(" (found in %1$s)", LintUtils.formatList(defined, 5)); } } context.report( ICON_DENSITIES, Location.create(file), String.format( "Missing the following drawables in %1$s: %2$s%3$s", file.getName(), LintUtils.formatList(delta, 5), foundIn), null); } } } }
private void checkDrawableDir( Context context, File folder, File[] files, Map<File, Dimension> pixelSizes, Map<File, Long> fileSizes) { if (folder.getName().equals(DRAWABLE_FOLDER) && context.isEnabled(ICON_LOCATION)) { for (File file : files) { String name = file.getName(); if (name.endsWith(DOT_XML)) { // pass - most common case, avoids checking other extensions } else if (endsWith(name, DOT_PNG) || endsWith(name, DOT_JPG) || endsWith(name, DOT_GIF)) { context.report( ICON_LOCATION, Location.create(file), String.format( "Found bitmap drawable res/drawable/%1$s in " + "densityless folder", file.getName()), null); } } } if (context.isEnabled(GIF_USAGE)) { for (File file : files) { String name = file.getName(); if (endsWith(name, DOT_GIF)) { context.report( GIF_USAGE, Location.create(file), "Using the .gif format for bitmaps is discouraged", null); } } } // Check icon sizes if (context.isEnabled(ICON_EXPECTED_SIZE)) { checkExpectedSizes(context, folder, files); } if (pixelSizes != null || fileSizes != null) { for (File file : files) { // TODO: Combine this check with the check for expected sizes such that // I don't check file sizes twice! String fileName = file.getName(); if (endsWith(fileName, DOT_PNG) || endsWith(fileName, DOT_JPG)) { // Only scan .png files (except 9-patch png's) and jpg files for // dip sizes. Duplicate checks can also be performed on ninepatch files. if (pixelSizes != null && !endsWith(fileName, DOT_9PNG)) { Dimension size = getSize(file); pixelSizes.put(file, size); } if (fileSizes != null) { fileSizes.put(file, file.length()); } } } } }
// This method checks the given map from resource file to pixel dimensions for each // such image and makes sure that the normalized dip sizes across all the densities // are mostly the same. private void checkDipSizes(Context context, Map<File, Dimension> pixelSizes) { // Partition up the files such that I can look at a series by name. This // creates a map from filename (such as foo.png) to a list of files // providing that icon in various folders: drawable-mdpi/foo.png, drawable-hdpi/foo.png // etc. Map<String, List<File>> nameToFiles = new HashMap<String, List<File>>(); for (File file : pixelSizes.keySet()) { String name = file.getName(); List<File> list = nameToFiles.get(name); if (list == null) { list = new ArrayList<File>(); nameToFiles.put(name, list); } list.add(file); } ArrayList<String> names = new ArrayList<String>(nameToFiles.keySet()); Collections.sort(names); // We have to partition the files further because it's possible for the project // to have different configurations for an icon, such as this: // drawable-large-hdpi/foo.png, drawable-large-mdpi/foo.png, // drawable-hdpi/foo.png, drawable-mdpi/foo.png, // drawable-hdpi-v11/foo.png and drawable-mdpi-v11/foo.png. // In this case we don't want to compare across categories; we want to // ensure that the drawable-large-{density} icons are consistent, // that the drawable-{density}-v11 icons are consistent, and that // the drawable-{density} icons are consistent. // Map from name to list of map from parent folder to list of files Map<String, Map<String, List<File>>> configMap = new HashMap<String, Map<String, List<File>>>(); for (Map.Entry<String, List<File>> entry : nameToFiles.entrySet()) { String name = entry.getKey(); List<File> files = entry.getValue(); for (File file : files) { String parentName = file.getParentFile().getName(); // Strip out the density part int index = -1; for (String qualifier : DENSITY_QUALIFIERS) { index = parentName.indexOf(qualifier); if (index != -1) { parentName = parentName.substring(0, index) + parentName.substring(index + qualifier.length()); break; } } if (index == -1) { // No relevant qualifier found in the parent directory name, // e.g. it's just "drawable" or something like "drawable-nodpi". continue; } Map<String, List<File>> folderMap = configMap.get(name); if (folderMap == null) { folderMap = new HashMap<String, List<File>>(); configMap.put(name, folderMap); } // Map from name to a map from parent folder to files List<File> list = folderMap.get(parentName); if (list == null) { list = new ArrayList<File>(); folderMap.put(parentName, list); } list.add(file); } } for (String name : names) { // List<File> files = nameToFiles.get(name); Map<String, List<File>> configurations = configMap.get(name); if (configurations == null) { // Nothing in this configuration: probably only found in drawable/ or // drawable-nodpi etc directories. continue; } for (Map.Entry<String, List<File>> entry : configurations.entrySet()) { List<File> files = entry.getValue(); // Ensure that all the dip sizes are *roughly* the same Map<File, Dimension> dipSizes = new HashMap<File, Dimension>(); int dipWidthSum = 0; // Incremental computation of average int dipHeightSum = 0; // Incremental computation of average int count = 0; for (File file : files) { float factor = getMdpiScalingFactor(file.getParentFile().getName()); if (factor > 0) { Dimension size = pixelSizes.get(file); Dimension dip = new Dimension(Math.round(size.width / factor), Math.round(size.height / factor)); dipWidthSum += dip.width; dipHeightSum += dip.height; dipSizes.put(file, dip); count++; } } if (count == 0) { // Icons in drawable/ and drawable-nodpi/ continue; } int meanWidth = dipWidthSum / count; int meanHeight = dipHeightSum / count; // Compute standard deviation? int squareWidthSum = 0; int squareHeightSum = 0; for (Dimension size : dipSizes.values()) { squareWidthSum += (size.width - meanWidth) * (size.width - meanWidth); squareHeightSum += (size.height - meanHeight) * (size.height - meanHeight); } double widthStdDev = Math.sqrt(squareWidthSum / count); double heightStdDev = Math.sqrt(squareHeightSum / count); if (widthStdDev > meanWidth / 10 || heightStdDev > meanHeight) { Location location = null; StringBuilder sb = new StringBuilder(); // Sort entries by decreasing dip size List<Map.Entry<File, Dimension>> entries = new ArrayList<Map.Entry<File, Dimension>>(); for (Map.Entry<File, Dimension> entry2 : dipSizes.entrySet()) { entries.add(entry2); } Collections.sort( entries, new Comparator<Map.Entry<File, Dimension>>() { public int compare(Entry<File, Dimension> e1, Entry<File, Dimension> e2) { Dimension d1 = e1.getValue(); Dimension d2 = e2.getValue(); if (d1.width != d2.width) { return d2.width - d1.width; } return d2.height - d1.height; } }); for (Map.Entry<File, Dimension> entry2 : entries) { if (sb.length() > 0) { sb.append(", "); } File file = entry2.getKey(); // Chain locations together Location linkedLocation = location; location = Location.create(file); location.setSecondary(linkedLocation); Dimension dip = entry2.getValue(); Dimension px = pixelSizes.get(file); String fileName = file.getParentFile().getName() + File.separator + file.getName(); sb.append( String.format( "%1$s: %2$dx%3$d dp (%4$dx%5$d px)", fileName, dip.width, dip.height, px.width, px.height)); } String message = String.format( "The image %1$s varies significantly in its density-independent (dip) " + "size across the various density versions: %2$s", name, sb.toString()); context.report(ICON_DIP_SIZE, location, message, null); } } } }
// This method looks for duplicates in the assets. This uses two pieces of information // (file sizes and image dimensions) to quickly reject candidates, such that it only // needs to check actual file contents on a small subset of the available files. private void checkDuplicates( Context context, Map<File, Dimension> pixelSizes, Map<File, Long> fileSizes) { Map<Long, Set<File>> sameSizes = new HashMap<Long, Set<File>>(); Map<Long, File> seenSizes = new HashMap<Long, File>(fileSizes.size()); for (Map.Entry<File, Long> entry : fileSizes.entrySet()) { File file = entry.getKey(); Long size = entry.getValue(); if (seenSizes.containsKey(size)) { Set<File> set = sameSizes.get(size); if (set == null) { set = new HashSet<File>(); set.add(seenSizes.get(size)); sameSizes.put(size, set); } set.add(file); } else { seenSizes.put(size, file); } } if (sameSizes.size() == 0) { return; } // Now go through the files that have the same size and check to see if we can // split them apart based on image dimensions // Note: we may not have file sizes on all the icons; in particular, // we don't have file sizes for ninepatch files. Collection<Set<File>> candidateLists = sameSizes.values(); for (Set<File> candidates : candidateLists) { Map<Dimension, Set<File>> sameDimensions = new HashMap<Dimension, Set<File>>(candidates.size()); List<File> noSize = new ArrayList<File>(); for (File file : candidates) { Dimension dimension = pixelSizes.get(file); if (dimension != null) { Set<File> set = sameDimensions.get(dimension); if (set == null) { set = new HashSet<File>(); sameDimensions.put(dimension, set); } set.add(file); } else { noSize.add(file); } } // Files that we have no dimensions for must be compared against everything Collection<Set<File>> sets = sameDimensions.values(); if (noSize.size() > 0) { if (sets.size() > 0) { for (Set<File> set : sets) { set.addAll(noSize); } } else { // Must just test the noSize elements against themselves HashSet<File> noSizeSet = new HashSet<File>(noSize); sets = Collections.<Set<File>>singletonList(noSizeSet); } } // Map from file to actual byte contents of the file. // We store this in a map such that for repeated files, such as noSize files // which can appear in multiple buckets, we only need to read them once Map<File, byte[]> fileContents = new HashMap<File, byte[]>(); // Now we're ready for the final check where we actually check the // bits. We have to partition the files into buckets of files that // are identical. for (Set<File> set : sets) { if (set.size() < 2) { continue; } // Read all files in this set and store in map for (File file : set) { byte[] bits = fileContents.get(file); if (bits == null) { try { bits = LintUtils.readBytes(file); fileContents.put(file, bits); } catch (IOException e) { context.log(e, null); } } } // Map where the key file is known to be equal to the value file. // After we check individual files for equality this will be used // to look for transitive equality. Map<File, File> equal = new HashMap<File, File>(); // Now go and compare all the files. This isn't an efficient algorithm // but the number of candidates should be very small List<File> files = new ArrayList<File>(set); Collections.sort(files); for (int i = 0; i < files.size() - 1; i++) { for (int j = i + 1; j < files.size(); j++) { File file1 = files.get(i); File file2 = files.get(j); byte[] contents1 = fileContents.get(file1); byte[] contents2 = fileContents.get(file2); if (contents1 == null || contents2 == null) { // File couldn't be read: ignore continue; } if (contents1.length != contents2.length) { // Sizes differ: not identical. // This shouldn't happen since we've already partitioned based // on File.length(), but just make sure here since the file // system could have lied, or cached a value that has changed // if the file was just overwritten continue; } boolean same = true; for (int k = 0; k < contents1.length; k++) { if (contents1[k] != contents2[k]) { same = false; break; } } if (same) { equal.put(file1, file2); } } } if (equal.size() > 0) { Map<File, Set<File>> partitions = new HashMap<File, Set<File>>(); List<Set<File>> sameSets = new ArrayList<Set<File>>(); for (Map.Entry<File, File> entry : equal.entrySet()) { File file1 = entry.getKey(); File file2 = entry.getValue(); Set<File> set1 = partitions.get(file1); Set<File> set2 = partitions.get(file2); if (set1 != null) { set1.add(file2); } else if (set2 != null) { set2.add(file1); } else { set = new HashSet<File>(); sameSets.add(set); set.add(file1); set.add(file2); partitions.put(file1, set); partitions.put(file2, set); } } // We've computed the partitions of equal files. Now sort them // for stable output. List<List<File>> lists = new ArrayList<List<File>>(); for (Set<File> same : sameSets) { assert same.size() > 0; ArrayList<File> sorted = new ArrayList<File>(same); Collections.sort(sorted); lists.add(sorted); } // Sort overall partitions by the first item in each list Collections.sort( lists, new Comparator<List<File>>() { public int compare(List<File> list1, List<File> list2) { return list1.get(0).compareTo(list2.get(0)); } }); for (List<File> sameFiles : lists) { Location location = null; boolean sameNames = true; String lastName = null; for (File file : sameFiles) { if (lastName != null && !lastName.equals(file.getName())) { sameNames = false; } lastName = file.getName(); // Chain locations together Location linkedLocation = location; location = Location.create(file); location.setSecondary(linkedLocation); } if (sameNames) { StringBuilder sb = new StringBuilder(); for (File file : sameFiles) { if (sb.length() > 0) { sb.append(", "); // $NON-NLS-1$ } sb.append(file.getParentFile().getName()); } String message = String.format( "The %1$s icon has identical contents in the following configuration folders: %2$s", lastName, sb.toString()); context.report(DUPLICATES_CONFIGURATIONS, location, message, null); } else { StringBuilder sb = new StringBuilder(); for (File file : sameFiles) { if (sb.length() > 0) { sb.append(", "); // $NON-NLS-1$ } sb.append(file.getName()); } String message = String.format( "The following unrelated icon files have identical contents: %1$s", sb.toString()); context.report(DUPLICATES_NAMES, location, message, null); } } } } } }
private void checkSize( Context context, String folderName, File file, int mdpiWidth, int mdpiHeight, boolean exactMatch) { String fileName = file.getName(); // Only scan .png files (except 9-patch png's) and jpg files if (!((endsWith(fileName, DOT_PNG) && !endsWith(fileName, DOT_9PNG)) || endsWith(fileName, DOT_JPG))) { return; } int width = -1; int height = -1; // Use 3:4:6:8 scaling ratio to look up the other expected sizes if (folderName.startsWith(DRAWABLE_MDPI)) { width = mdpiWidth; height = mdpiHeight; } else if (folderName.startsWith(DRAWABLE_HDPI)) { // Perform math using floating point; if we just do // width = mdpiWidth * 3 / 2; // then for mdpiWidth = 25 (as in notification icons on pre-GB) we end up // with width = 37, instead of 38 (with floating point rounding we get 37.5 = 38) width = Math.round(mdpiWidth * 3.f / 2); height = Math.round(mdpiHeight * 3f / 2); } else if (folderName.startsWith(DRAWABLE_XHDPI)) { width = mdpiWidth * 2; height = mdpiHeight * 2; } else if (folderName.startsWith(DRAWABLE_LDPI)) { width = Math.round(mdpiWidth * 3f / 4); height = Math.round(mdpiHeight * 3f / 4); } else { return; } Dimension size = getSize(file); if (size != null) { if (exactMatch && size.width != width || size.height != height) { context.report( ICON_EXPECTED_SIZE, Location.create(file), String.format( "Incorrect icon size for %1$s: expected %2$dx%3$d, but was %4$dx%5$d", folderName + File.separator + file.getName(), width, height, size.width, size.height), null); } else if (!exactMatch && size.width > width || size.height > height) { context.report( ICON_EXPECTED_SIZE, Location.create(file), String.format( "Incorrect icon size for %1$s: icon size should be at most %2$dx%3$d, but was %4$dx%5$d", folderName + File.separator + file.getName(), width, height, size.width, size.height), null); } } }
public void test() throws Exception { File file = new File(getTargetDir(), "report"); try { LintCliClient client = new LintCliClient() { @Override String getRevision() { return "unittest"; // Hardcode version to keep unit test output stable } }; //noinspection ResultOfMethodCallIgnored file.getParentFile().mkdirs(); XmlReporter reporter = new XmlReporter(client, file); Project project = Project.create(client, new File("/foo/bar/Foo"), new File("/foo/bar/Foo")); Warning warning1 = new Warning( ManifestDetector.USES_SDK, "<uses-sdk> tag should specify a target API level (the highest verified " + "version; when running on later versions, compatibility behaviors may " + "be enabled) with android:targetSdkVersion=\"?\"", Severity.WARNING, project, null); warning1.line = 6; warning1.file = new File("/foo/bar/Foo/AndroidManifest.xml"); warning1.errorLine = " <uses-sdk android:minSdkVersion=\"8\" />\n ^\n"; warning1.path = "AndroidManifest.xml"; warning1.location = Location.create( warning1.file, new DefaultPosition(6, 4, 198), new DefaultPosition(6, 42, 236)); Warning warning2 = new Warning( HardcodedValuesDetector.ISSUE, "[I18N] Hardcoded string \"Fooo\", should use @string resource", Severity.WARNING, project, null); warning2.line = 11; warning2.file = new File("/foo/bar/Foo/res/layout/main.xml"); warning2.errorLine = " android:text=\"Fooo\" />\n" + " ~~~~~~~~~~~~~~~~~~~\n"; warning2.path = "res/layout/main.xml"; warning2.location = Location.create( warning2.file, new DefaultPosition(11, 8, 377), new DefaultPosition(11, 27, 396)); List<Warning> warnings = new ArrayList<Warning>(); warnings.add(warning1); warnings.add(warning2); reporter.write(0, 2, warnings); String report = Files.toString(file, Charsets.UTF_8); assertEquals( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<issues format=\"4\" by=\"lint unittest\">\n" + "\n" + " <issue\n" + " id=\"UsesMinSdkAttributes\"\n" + " severity=\"Warning\"\n" + " message=\"<uses-sdk> tag should specify a target API level (the highest verified version; when running on later versions, compatibility behaviors may be enabled) with android:targetSdkVersion="?"\"\n" + " category=\"Correctness\"\n" + " priority=\"9\"\n" + " summary=\"Checks that the minimum SDK and target SDK attributes are defined\"\n" + " explanation=\"The manifest should contain a `<uses-sdk>` element which defines the minimum API Level required for the application to run, as well as the target version (the highest API level you have tested the version for.)\"\n" + " url=\"http://developer.android.com/guide/topics/manifest/uses-sdk-element.html\"\n" + " urls=\"http://developer.android.com/guide/topics/manifest/uses-sdk-element.html\"\n" + " errorLine1=\" <uses-sdk android:minSdkVersion="8" />\"\n" + " errorLine2=\" ^\">\n" + " <location\n" + " file=\"AndroidManifest.xml\"\n" + " line=\"7\"\n" + " column=\"5\"/>\n" + " </issue>\n" + "\n" + " <issue\n" + " id=\"HardcodedText\"\n" + " severity=\"Warning\"\n" + " message=\"[I18N] Hardcoded string "Fooo", should use @string resource\"\n" + " category=\"Internationalization\"\n" + " priority=\"5\"\n" + " summary=\"Looks for hardcoded text attributes which should be converted to resource lookup\"\n" + " explanation=\"Hardcoding text attributes directly in layout files is bad for several reasons:\n" + "\n" + "* When creating configuration variations (for example for landscape or portrait)you have to repeat the actual text (and keep it up to date when making changes)\n" + "\n" + "* The application cannot be translated to other languages by just adding new translations for existing string resources.\n" + "\n" + "In Android Studio and Eclipse there are quickfixes to automatically extract this hardcoded string into a resource lookup.\"\n" + " errorLine1=\" android:text="Fooo" />\"\n" + " errorLine2=\" ~~~~~~~~~~~~~~~~~~~\">\n" + " <location\n" + " file=\"res/layout/main.xml\"\n" + " line=\"12\"\n" + " column=\"9\"/>\n" + " </issue>\n" + "\n" + "</issues>\n", report); // Make sure the XML is valid Document document = new PositionXmlParser().parse(report); assertNotNull(document); assertEquals(2, document.getElementsByTagName("issue").getLength()); } finally { //noinspection ResultOfMethodCallIgnored file.delete(); } }
public void testNonPrintableChars() throws Exception { // See https://code.google.com/p/android/issues/detail?id=56205 File file = new File(getTargetDir(), "report"); try { LintCliClient client = new LintCliClient() { @Override String getRevision() { return "unittest"; // Hardcode version to keep unit test output stable } }; //noinspection ResultOfMethodCallIgnored file.getParentFile().mkdirs(); XmlReporter reporter = new XmlReporter(client, file); Project project = Project.create(client, new File("/foo/bar/Foo"), new File("/foo/bar/Foo")); Warning warning1 = new Warning( TypographyDetector.FRACTIONS, String.format( "Use fraction character %1$c (%2$s) instead of %3$s ?", '\u00BC', "¼", "1/4"), Severity.WARNING, project, null); warning1.line = 592; warning1.file = new File("/foo/bar/Foo/AndroidManifest.xml"); warning1.errorLine = " <string name=\"user_registration_name3_3\">Register 3/3</string>\n" + " ^"; warning1.path = "res/values-en/common_strings.xml"; warning1.location = Location.create(warning1.file, new DefaultPosition(592, 46, -1), null); List<Warning> warnings = new ArrayList<Warning>(); warnings.add(warning1); reporter.write(0, 2, warnings); String report = Files.toString(file, Charsets.UTF_8); assertEquals( "" + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<issues format=\"4\" by=\"lint unittest\">\n" + "\n" + " <issue\n" + " id=\"TypographyFractions\"\n" + " severity=\"Warning\"\n" + " message=\"Use fraction character ¼ (&#188;) instead of 1/4 ?\"\n" + " category=\"Usability:Typography\"\n" + " priority=\"5\"\n" + " summary=\"Looks for fraction strings which can be replaced with a fraction character\"\n" + " explanation=\"You can replace certain strings, such as 1/2, and 1/4, with dedicated characters for these, such as ½ (&#189;) and ¼ (&#188;). This can help make the text more readable.\"\n" + " url=\"http://en.wikipedia.org/wiki/Number_Forms\"\n" + " urls=\"http://en.wikipedia.org/wiki/Number_Forms\">\n" + " <location\n" + " file=\"AndroidManifest.xml\"\n" + " line=\"593\"\n" + " column=\"47\"/>\n" + " </issue>\n" + "\n" + "</issues>\n", report); // Make sure the XML is valid Document document = new PositionXmlParser().parse(report); assertNotNull(document); assertEquals(1, document.getElementsByTagName("issue").getLength()); String explanation = ((Element) document.getElementsByTagName("issue").item(0)).getAttribute("explanation"); assertEquals( TypographyDetector.FRACTIONS.getExplanation(Issue.OutputFormat.RAW), explanation); } finally { //noinspection ResultOfMethodCallIgnored file.delete(); } }