blob: 943b53481b14166950a9909de8c7aa985600ad16 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.android.inspections.lint;
import com.android.tools.lint.checks.*;
import com.android.tools.lint.detector.api.*;
import com.android.utils.XmlUtils;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.openapi.project.Project;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.PsiElement;
import org.jetbrains.android.AndroidTestCase;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
/** Ensures that all relevant lint checks are available and registered */
public class AndroidLintInspectionToolProviderTest extends AndroidTestCase {
private static final boolean LIST_ISSUES_WITH_QUICK_FIXES = false;
public void testAllLintChecksRegistered() throws Exception {
assertTrue(checkAllLintChecksRegistered(getProject()));
}
private static boolean sDone;
public static boolean checkAllLintChecksRegistered(Project project) throws Exception {
if (sDone) {
return true;
}
sDone = true;
// For some reason, I can't just use
// AndroidLintInspectionBase.getInspectionShortNameByIssue
// to iterate the available inspections from unit tests; at runtime this will enumerate all
// the available inspections, but from unit tests (even when extending IdeaTestCase) it's empty.
// So instead we take advantage of the knowledge that all our inspections are declared as inner classes
// of AndroidLintInspectionToolProvider and we iterate these instead. This won't catch cases if
// we declare a class there and forget to register in the plugin, but it's better than nothing.
Set<String> registered = Sets.newHashSetWithExpectedSize(200);
Set<Issue> quickfixes = Sets.newLinkedHashSetWithExpectedSize(200);
for (Class<?> c : AndroidLintInspectionToolProvider.class.getDeclaredClasses()) {
if (AndroidLintInspectionBase.class.isAssignableFrom(c) && ((c.getModifiers() & Modifier.ABSTRACT) == 0)) {
AndroidLintInspectionBase provider = (AndroidLintInspectionBase)c.newInstance();
registered.add(provider.getIssue().getId());
boolean hasQuickFix = true;
try {
provider.getClass().getDeclaredMethod("getQuickFixes", String.class);
} catch (NoSuchMethodException e1) {
try {
provider.getClass().getDeclaredMethod("getQuickFixes", PsiElement.class, PsiElement.class, String.class);
} catch (NoSuchMethodException e2) {
hasQuickFix = false;
}
}
if (hasQuickFix) {
quickfixes.add(provider.getIssue());
}
}
}
final List<Issue> missing = new ArrayList<Issue>();
final IntellijLintIssueRegistry fullRegistry = new IntellijLintIssueRegistry();
List<Issue> allIssues = fullRegistry.getIssues();
for (Issue issue : allIssues) {
if (!isRelevant(issue) || registered.contains(issue.getId())) {
continue;
}
assertFalse(IntellijLintProject.SUPPORT_CLASS_FILES); // When enabled, adjust this to register class based registrations
Implementation implementation = issue.getImplementation();
if (implementation.getScope().contains(Scope.CLASS_FILE) ||
implementation.getScope().contains(Scope.ALL_CLASS_FILES) ||
implementation.getScope().contains(Scope.JAVA_LIBRARIES)) {
boolean isOk = false;
for (EnumSet<Scope> analysisScope : implementation.getAnalysisScopes()) {
if (!analysisScope.contains(Scope.CLASS_FILE)
&& !analysisScope.contains(Scope.ALL_CLASS_FILES)
&& !analysisScope.contains(Scope.JAVA_LIBRARIES)) {
isOk = true;
break;
}
}
if (!isOk) {
System.out.println("Skipping issue " + issue + " because it requires classfile analysis. Consider rewriting in IDEA.");
continue;
}
}
final String inspectionShortName = AndroidLintInspectionBase.getInspectionShortNameByIssue(project, issue);
if (inspectionShortName == null) {
missing.add(issue);
continue;
}
// ELSE: compare severity, enabledByDefault, etc, message, etc
final HighlightDisplayKey key = HighlightDisplayKey.find(inspectionShortName);
if (key == null) {
//fail("No highlight display key for inspection " + inspectionShortName + " for issue " + issue);
System.out.println("No highlight display key for inspection " + inspectionShortName + " for issue " + issue);
continue;
}
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
PsiElement element = null;
HighlightDisplayLevel errorLevel = profile.getErrorLevel(key, element);
Severity s;
if (errorLevel == HighlightDisplayLevel.WARNING || errorLevel == HighlightDisplayLevel.WEAK_WARNING) {
s = Severity.WARNING;
} else if (errorLevel == HighlightDisplayLevel.ERROR || errorLevel == HighlightDisplayLevel.NON_SWITCHABLE_ERROR) {
s = Severity.ERROR;
} else if (errorLevel == HighlightDisplayLevel.DO_NOT_SHOW) {
s = Severity.IGNORE;
} else if (errorLevel == HighlightDisplayLevel.INFO) {
s = Severity.INFORMATIONAL;
} else {
//fail("Unexpected error level " + errorLevel);
System.out.println("Unexpected error level " + errorLevel);
continue;
}
Severity expectedSeverity = issue.getDefaultSeverity();
if (expectedSeverity == Severity.FATAL) {
expectedSeverity = Severity.ERROR;
}
//assertSame(expectedSeverity, s);
if (expectedSeverity != s) {
System.out.println("Wrong severity for " + issue + "; expected " + expectedSeverity + ", got " + s);
}
}
// Spit out registration information for the missing elements
if (!missing.isEmpty()) {
Collections.sort(missing);
StringBuilder sb = new StringBuilder(1000);
sb.append("Missing registration for " + missing.size() + " issues (out of a total issue count of " + allIssues.size() + ")");
sb.append("\nAdd to plugin.xml (and please try to preserve the case sensitive alphabetical order, where for example B < a):\n");
for (Issue issue : missing) {
sb.append(" <globalInspection hasStaticDescription=\"true\" shortName=\"AndroidLint");
String id = issue.getId();
sb.append(id);
sb.append("\" displayName=\"");
sb.append(XmlUtils.toXmlAttributeValue(issue.getBriefDescription(TextFormat.TEXT)));
sb.append("\" groupKey=\"android.lint.inspections.group.name\" bundle=\"messages.AndroidBundle\" enabledByDefault=\"");
sb.append(issue.isEnabledByDefault());
sb.append("\" level=\"");
sb.append(issue.getDefaultSeverity() == Severity.ERROR || issue.getDefaultSeverity() == Severity.FATAL ?
"ERROR" : "WARNING");
sb.append("\" implementationClass=\"org.jetbrains.android.inspections.lint.AndroidLintInspectionToolProvider$AndroidLint");
sb.append(id);
sb.append("Inspection\"/>\n");
}
sb.append("\nAdd to AndroidLintInspectionToolProvider.java:\n");
for (Issue issue : missing) {
String id = issue.getId();
String detectorName = getDetectorClass(issue).getName();
String issueName = getIssueFieldName(issue);
String messageKey = getMessageKey(issue);
sb.append("" +
" public static class AndroidLint" + id + "Inspection extends AndroidLintInspectionBase {\n" +
" public AndroidLint" + id + "Inspection() {\n" +
" super(AndroidBundle.message(\"android.lint.inspections." + messageKey + "\"), " + detectorName + "." + issueName + ");\n" +
" }\n" +
" }\n");
}
sb.append("\nAdd to AndroidBundle.properties:\n");
for (Issue issue : missing) {
String messageKey = getMessageKey(issue);
sb.append("android.lint.inspections." + messageKey + "=" + getBriefDescription(issue).replace(":", "\\:").replace("=", "\\=") + "\n");
}
sb.append("\nAdded registrations for " + missing.size() + " issues (out of a total issue count of " + allIssues.size() + ")");
System.out.println(sb.toString());
return false;
} else if (LIST_ISSUES_WITH_QUICK_FIXES) {
System.out.println("The following inspections have quickfixes (used for Reporter.java):\n");
List<String> fields = Lists.newArrayListWithExpectedSize(quickfixes.size());
Set<String> imports = Sets.newHashSetWithExpectedSize(quickfixes.size());
for (Issue issue : quickfixes) {
String detectorName = getDetectorClass(issue).getName();
imports.add(detectorName);
int index = detectorName.lastIndexOf('.');
if (index != -1) {
detectorName = detectorName.substring(index + 1);
}
String issueName = getIssueFieldName(issue);
fields.add(detectorName + "." + issueName);
}
Collections.sort(fields);
List<String> sortedImports = Lists.newArrayList(imports);
Collections.sort(sortedImports);
for (String cls : sortedImports) {
System.out.println("import " + cls + ";");
}
System.out.println();
System.out.println("sStudioFixes = Sets.newHashSet(\n" + Joiner.on(",\n").join(fields) + "\n);\n");
}
return true;
}
private static String getIssueFieldName(Issue issue) {
Class<? extends Detector> detectorClass = getDetectorClass(issue);
// Use reflection on the detector class, check all field instances and compare id's to locate the right one
for (Field field : detectorClass.getDeclaredFields()) {
if ((field.getModifiers() & Modifier.STATIC) != 0) {
try {
Object o = field.get(null);
if (o instanceof Issue) {
if (issue.getId().equals(((Issue)o).getId())) {
return field.getName();
}
}
}
catch (IllegalAccessException e) {
// pass; use default instead
}
}
}
return "/*findFieldFor: " + issue.getId() + "*/TODO";
}
private static Class<? extends Detector> getDetectorClass(Issue issue) {
Class<? extends Detector> detectorClass = issue.getImplementation().getDetectorClass();
// Undo the effects of IntellijLintIssueRegistry
if (detectorClass == IntellijApiDetector.class) {
detectorClass = ApiDetector.class;
} else if (detectorClass == IntellijGradleDetector.class) {
detectorClass = GradleDetector.class;
} else if (detectorClass == IntellijRegistrationDetector.class) {
detectorClass = RegistrationDetector.class;
} else if (detectorClass == IntellijViewTypeDetector.class) {
detectorClass = ViewTypeDetector.class;
}
return detectorClass;
}
private static String getBriefDescription(Issue issue) {
return issue.getBriefDescription(TextFormat.TEXT);
}
private static String getMessageKey(Issue issue) {
return camelCaseToUnderlines(issue.getId()).replace('_','.').toLowerCase(Locale.US);
}
private static String camelCaseToUnderlines(String string) {
if (string.isEmpty()) {
return string;
}
StringBuilder sb = new StringBuilder(2 * string.length());
int n = string.length();
boolean lastWasUpperCase = Character.isUpperCase(string.charAt(0));
for (int i = 0; i < n; i++) {
char c = string.charAt(i);
boolean isUpperCase = Character.isUpperCase(c);
if (isUpperCase && !lastWasUpperCase) {
sb.append('_');
}
lastWasUpperCase = isUpperCase;
c = Character.toLowerCase(c);
sb.append(c);
}
return sb.toString();
}
private static boolean isRelevant(Issue issue) {
// Supported more directly by other IntelliJ checks(?)
if (issue == NamespaceDetector.TYPO || // IDEA has its own spelling check
issue == SupportAnnotationDetector.RESOURCE_TYPE || // We have a separate inspection for this
issue == SupportAnnotationDetector.TYPE_DEF || // We have a separate inspection for this
issue == NamespaceDetector.UNUSED || // IDEA already does full validation
issue == ManifestTypoDetector.ISSUE || // IDEA already does full validation
issue == ManifestDetector.WRONG_PARENT || // IDEA already does full validation
issue == LocaleDetector.STRING_LOCALE) { // IDEA checks for this in Java code
return false;
}
return true;
}
}