Beispiel #1
0
  @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);
        }
      }
    }
  }
Beispiel #10
0
  // 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);
            }
          }
        }
      }
    }
  }
Beispiel #11
0
  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);
      }
    }
  }
Beispiel #12
0
  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=\"&lt;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=&quot;?&quot;\"\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 `&lt;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=\"    &lt;uses-sdk android:minSdkVersion=&quot;8&quot; />\"\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 &quot;Fooo&quot;, 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=&quot;Fooo&quot; />\"\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();
    }
  }
Beispiel #13
0
  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', "&#188;", "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 ¼ (&amp;#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 ½ (&amp;#189;) and ¼ (&amp;#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();
    }
  }