| /* |
| * 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; |
| } |
| } |