Lint: Update diagnostics because of the new Uast. Also, these diagnostics are from AS 2.2.

This commit is contained in:
Yan Zhulanow
2016-10-13 20:24:37 +03:00
committed by Yan Zhulanow
parent 4c4d6a4ad4
commit c2ddd943f9
152 changed files with 27552 additions and 7266 deletions
+1
View File
@@ -2,6 +2,7 @@
<dictionary name="yan">
<words>
<w>kapt</w>
<w>uast</w>
</words>
</dictionary>
</component>
-1
View File
@@ -83,7 +83,6 @@
<module fileurl="file://$PROJECT_DIR$/compiler/serialization/serialization.iml" filepath="$PROJECT_DIR$/compiler/serialization/serialization.iml" group="compiler" />
<module fileurl="file://$PROJECT_DIR$/compiler/tests-common/tests-common.iml" filepath="$PROJECT_DIR$/compiler/tests-common/tests-common.iml" group="compiler" />
<module fileurl="file://$PROJECT_DIR$/compiler/tests-ir-jvm/tests-ir-jvm.iml" filepath="$PROJECT_DIR$/compiler/tests-ir-jvm/tests-ir-jvm.iml" group="compiler/ir" />
<module fileurl="file://$PROJECT_DIR$/plugins/lint/uast-android/uast-android.iml" filepath="$PROJECT_DIR$/plugins/lint/uast-android/uast-android.iml" group="plugins/lint" />
<module fileurl="file://$PROJECT_DIR$/plugins/uast-common/uast-common.iml" filepath="$PROJECT_DIR$/plugins/uast-common/uast-common.iml" group="plugins/lint" />
<module fileurl="file://$PROJECT_DIR$/plugins/uast-java/uast-java.iml" filepath="$PROJECT_DIR$/plugins/uast-java/uast-java.iml" group="plugins/lint" />
<module fileurl="file://$PROJECT_DIR$/plugins/uast-kotlin/uast-kotlin.iml" filepath="$PROJECT_DIR$/plugins/uast-kotlin/uast-kotlin.iml" group="plugins/lint" />
@@ -417,7 +417,7 @@ public class KotlinTypeMapper {
}
@NotNull
private Type mapType(
public Type mapType(
@NotNull KotlinType kotlinType,
@Nullable final JvmSignatureWriter signatureVisitor,
@NotNull TypeMappingMode mode
@@ -205,7 +205,7 @@ sealed class KtLightMethodImpl(
delegate: PsiMethod, origin: LightMemberOrigin?, containingClass: KtLightClass
) : KtLightMethodImpl(delegate, origin, containingClass)
private class KtLightAnnotationMethod(
class KtLightAnnotationMethod(
override val clsDelegate: PsiAnnotationMethod,
origin: LightMemberOrigin?,
containingClass: KtLightClass
@@ -185,4 +185,9 @@ public class KtLightParameter extends LightParameter implements KtLightDeclarati
}
return super.isEquivalentTo(another);
}
@Override
public boolean equals(Object obj) {
return obj instanceof PsiElement && isEquivalentTo((PsiElement) obj);
}
}
+48 -10
View File
@@ -1,22 +1,37 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<externalAnnotator language="kotlin" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintExternalAnnotator"/>
<projectService serviceInterface="org.jetbrains.uast.kotlin.KotlinUastBindingContextProviderService"
serviceImplementation="org.jetbrains.uast.kotlin.internal.IdeaKotlinUastBindingContextProviderService"/>
<projectService serviceInterface="org.jetbrains.uast.UastContext"
serviceImplementation="org.jetbrains.uast.UastContext"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintAddJavascriptInterface" displayName="addJavascriptInterface Called" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintAddJavascriptInterfaceInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintAllowAllHostnameVerifier" displayName="Insecure HostnameVerifier" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintAllowAllHostnameVerifierInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintAlwaysShowAction" displayName="Usage of showAsAction=always" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintAlwaysShowActionInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintAppCompatMethod" displayName="Using Wrong AppCompat Method" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintAppCompatMethodInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintAuthLeak" displayName="Code contains url auth" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintAuthLeakInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintBadHostnameVerifier" displayName="Insecure HostnameVerifier" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintBadHostnameVerifierInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintBatteryLife" displayName="Battery Life Issues" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintBatteryLifeInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintCommitPrefEdits" displayName="Missing commit() on SharedPreference editor" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintCommitPrefEditsInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintCommitTransaction" displayName="Missing commit() calls" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintCommitTransactionInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintCustomViewStyleable" displayName="Mismatched Styleable/Custom View Name" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintCustomViewStyleableInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintCutPasteId" displayName="Likely cut &amp; paste mistakes" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintCutPasteIdInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintDefaultLocale" displayName="Implied default locale in case conversion" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintDefaultLocaleInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintDrawAllocation" displayName="Memory allocations within drawing code" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintDrawAllocationInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintEasterEgg" displayName="Code contains easter egg" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="false" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintEasterEggInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintExportedContentProvider" displayName="Content provider does not require permission" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintExportedContentProviderInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintExportedPreferenceActivity" displayName="PreferenceActivity should not be exported" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintExportedPreferenceActivityInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintExportedReceiver" displayName="Receiver does not require permission" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintExportedReceiverInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintExportedService" displayName="Exported service does not require permission" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintExportedServiceInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintFloatMath" displayName="Using FloatMath instead of Math" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintFloatMathInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintGetInstance" displayName="Cipher.getInstance with ECB" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintGetInstanceInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintGifUsage" displayName="Using .gif format for bitmaps is discouraged" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintGifUsageInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintGoogleAppIndexingApiWarning" displayName="Missing support for Google App Indexing Api" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="false" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintGoogleAppIndexingApiWarningInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintGoogleAppIndexingUrlError" displayName="URL not supported by app for Google App Indexing" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintGoogleAppIndexingUrlErrorInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintGoogleAppIndexingWarning" displayName="Missing support for Google App Indexing" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintGoogleAppIndexingWarningInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintGrantAllUris" displayName="Content provider shares everything" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintGrantAllUrisInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintHandlerLeak" displayName="Handler reference leaks" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintHandlerLeakInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintIconColors" displayName="Icon colors do not follow the recommended visual style" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintIconColorsInspection"/>
@@ -35,49 +50,72 @@
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintInconsistentLayout" displayName="Inconsistent Layouts" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintInconsistentLayoutInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintInflateParams" displayName="Layout Inflation without a Parent" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintInflateParamsInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintInlinedApi" displayName="Using inlined constants on older versions" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintInlinedApiInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintInvalidUsesTagAttribute" displayName="Invalid name attribute for uses element." groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintInvalidUsesTagAttributeInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintJavascriptInterface" displayName="Missing @JavascriptInterface on methods" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintJavascriptInterfaceInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintLocalSuppress" displayName="@SuppressLint on invalid element" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintLocalSuppressInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintLogConditional" displayName="Unconditional Logging Calls" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="false" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintLogConditionalInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintLogTagMismatch" displayName="Mismatched Log Tags" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintLogTagMismatchInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintLongLogTag" displayName="Too Long Log Tags" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintLongLogTagInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintMergeRootFrame" displayName="FrameLayout can be replaced with &lt;merge> tag" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintMergeRootFrameInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintMissingIntentFilterForMediaSearch" displayName="Missing intent-filter with action android.media.action.MEDIA_PLAY_FROM_SEARCH" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintMissingIntentFilterForMediaSearchInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintMissingMediaBrowserServiceIntentFilter" displayName="Missing intent-filter with action android.media.browse.MediaBrowserService." groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintMissingMediaBrowserServiceIntentFilterInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintMissingOnPlayFromSearch" displayName="Missing onPlayFromSearch." groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintMissingOnPlayFromSearchInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintMissingSuperCall" displayName="Missing Super Call" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintMissingSuperCallInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintNewApi" displayName="Calling new methods on older versions" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintNewApiInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintOverdraw" displayName="Overdraw: Painting regions more than once" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintOverdrawInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintOverride" displayName="Method conflicts with new inherited method" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintOverrideInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintOverrideAbstract" displayName="Not overriding abstract methods on older platforms" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintOverrideAbstractInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintPackageManagerGetSignatures" displayName="Potential Multiple Certificate Exploit" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintPackageManagerGetSignaturesInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintParcelClassLoader" displayName="Default Parcel Class Loader" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintParcelClassLoaderInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintParcelCreator" displayName="Missing Parcelable CREATOR field" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintParcelCreatorInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintPendingBindings" displayName="Missing Pending Bindings" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintPendingBindingsInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintPluralsCandidate" displayName="Potential Plurals" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintPluralsCandidateInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintPrivateResource" displayName="Using private resources" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintPrivateResourceInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintRecycle" displayName="Missing recycle() calls" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintRecycleInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintRecyclerView" displayName="RecyclerView Problems" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintRecyclerViewInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintRegistered" displayName="Class is not registered in the manifest" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintRegisteredInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintRequiredSize" displayName="Missing layout_width or layout_height attributes" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintRequiredSizeInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintRtlCompat" displayName="Right-to-left text compatibility issues" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintRtlCompatInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintRtlEnabled" displayName="Using RTL attributes without enabling RTL support" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintRtlEnabledInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintRtlHardcoded" displayName="Using left/right instead of start/end attributes" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintRtlHardcodedInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintRtlSymmetry" displayName="Padding and margin symmetry" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintRtlSymmetryInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSQLiteString" displayName="Using STRING instead of TEXT" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSQLiteStringInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSdCardPath" displayName="Hardcoded reference to /sdcard" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSdCardPathInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSecureRandom" displayName="Using a fixed seed with SecureRandom" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSecureRandomInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintServiceCast" displayName="Wrong system service casts" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintServiceCastInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSetJavaScriptEnabled" displayName="Using setJavaScriptEnabled" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSetJavaScriptEnabledInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSetTextI18n" displayName="TextView Internationalization" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSetTextI18nInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSetWorldReadable" displayName="File.setReadable() used to make file world-readable" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSetWorldReadableInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSetWorldWritable" displayName="File.setWritable() used to make file world-writable" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSetWorldWritableInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintShiftFlags" displayName="Dangerous Flag Constant Declaration" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintShiftFlagsInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintShortAlarm" displayName="Short or Frequent Alarm" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintShortAlarmInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintShowToast" displayName="Toast created but not shown" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintShowToastInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSimpleDateFormat" displayName="Implied locale in date format" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSimpleDateFormatInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSQLiteString" displayName="Using STRING instead of TEXT" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSQLiteStringInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSSLCertificateSocketFactoryCreateSocket" displayName="Insecure call to SSLCertificateSocketFactory.createSocket()" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSSLCertificateSocketFactoryCreateSocketInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSSLCertificateSocketFactoryGetInsecure" displayName="Call to SSLCertificateSocketFactory.getInsecure()" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSSLCertificateSocketFactoryGetInsecureInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintStopShip" displayName="Code contains STOPSHIP marker" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="false" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintStopShipInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintStringFormatCount" displayName="Formatting argument types incomplete or inconsistent" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintStringFormatCountInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintStringFormatInvalid" displayName="Invalid format string" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintStringFormatInvalidInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintStringFormatMatches" displayName="String.format string doesn&apos;t match the XML format string" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintStringFormatMatchesInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSupportAnnotationUsage" displayName="Incorrect support annotation usage" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSupportAnnotationUsageInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSuspiciousImport" displayName="&apos;import android.R&apos; statement" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSuspiciousImportInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintSwitchIntDef" displayName="Missing @IntDef in Switch" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintSwitchIntDefInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintTrustAllX509TrustManager" displayName="Insecure TLS/SSL trust manager" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintTrustAllX509TrustManagerInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUniqueConstants" displayName="Overlapping Enumeration Constants" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUniqueConstantsInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUnlocalizedSms" displayName="SMS phone number missing country code" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnlocalizedSmsInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUnusedIds" displayName="Unused id" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="false" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnusedIdsInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUnusedResources" displayName="Unused resources" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnusedResourcesInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUnprotectedSMSBroadcastReceiver" displayName="Unprotected SMS BroadcastReceiver" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnprotectedSMSBroadcastReceiverInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUnsafeDynamicallyLoadedCode" displayName="load used to dynamically load code" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnsafeDynamicallyLoadedCodeInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUnsafeNativeCodeLocation" displayName="Native code outside library directory" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnsafeNativeCodeLocationInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUnsafeProtectedBroadcastReceiver" displayName="Unsafe Protected BroadcastReceiver" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnsafeProtectedBroadcastReceiverInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUnusedAttribute" displayName="Attribute unused on older versions" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUnusedAttributeInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUseSparseArrays" displayName="HashMap can be replaced with SparseArray" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUseSparseArraysInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintUseValueOf" displayName="Should use valueOf instead of new" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintUseValueOfInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintValidFragment" displayName="Fragment not instantiatable" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintValidFragmentInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintViewConstructor" displayName="Missing View constructors for XML inflation" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintViewConstructorInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintViewHolder" displayName="View Holder Candidates" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintViewHolderInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintWorldReadableFiles" displayName="openFileOutput() call passing MODE_WORLD_READABLE" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintWorldReadableFilesInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintWorldWriteableFiles" displayName="openFileOutput() call passing MODE_WORLD_WRITEABLE" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintWorldWriteableFilesInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintViewTag" displayName="Tagged object leaks" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintViewTagInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintWorldReadableFiles" displayName="openFileOutput() or similar call passing MODE_WORLD_READABLE" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintWorldReadableFilesInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintWorldWriteableFiles" displayName="openFileOutput() or similar call passing MODE_WORLD_WRITEABLE" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintWorldWriteableFilesInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintWrongCall" displayName="Using wrong draw/layout method" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintWrongCallInspection"/>
<globalInspection language="kotlin" hasStaticDescription="true" shortName="AndroidKLintWrongViewCast" displayName="Mismatched view type" groupKey="android.klint.inspections.group.name" bundle="org.jetbrains.kotlin.idea.KotlinBundle" enabledByDefault="true" level="ERROR" implementationClass="org.jetbrains.android.inspections.klint.AndroidLintInspectionToolProvider$AndroidKLintWrongViewCastInspection"/>
@@ -85,13 +123,13 @@
</extensions>
<extensionPoints>
<extensionPoint qualifiedName="com.android.tools.klint.client.api.lintLanguageExtension"
interface="com.android.tools.klint.client.api.LintLanguageExtension"
<extensionPoint qualifiedName="org.jetbrains.uast.uastLanguagePlugin"
interface="org.jetbrains.uast.UastLanguagePlugin"
area="IDEA_PROJECT"/>
</extensionPoints>
<extensions defaultExtensionNs="com.android.tools.klint.client.api">
<lintLanguageExtension implementation="org.jetbrains.android.inspections.klint.KotlinLintLanguageExtension"/>
<lintLanguageExtension implementation="com.android.tools.klint.client.api.JavaLintLanguageExtension"/>
<extensions defaultExtensionNs="org.jetbrains.uast">
<uastLanguagePlugin implementation="org.jetbrains.uast.kotlin.KotlinUastLanguagePlugin"/>
<uastLanguagePlugin implementation="org.jetbrains.uast.java.JavaUastLanguagePlugin"/>
</extensions>
</idea-plugin>
-1
View File
@@ -10,7 +10,6 @@
<orderEntry type="library" name="guava" level="project" />
<orderEntry type="module" module-name="uast-common" />
<orderEntry type="module" module-name="uast-java" />
<orderEntry type="module" module-name="uast-android" />
<orderEntry type="library" name="android-plugin" level="project" />
<orderEntry type="library" name="intellij-core" level="project" />
<orderEntry type="module" module-name="android-annotations" />
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2016 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 com.android.tools.klint.client.api;
import static com.android.SdkConstants.ANDROID_PKG;
import com.android.annotations.NonNull;
import com.android.resources.ResourceType;
import org.jetbrains.uast.UExpression;
public class AndroidReference {
public final UExpression node;
private final String rPackage;
private final ResourceType type;
private final String name;
// getPackage() can be empty if not a package-qualified import (e.g. android.R.id.name).
@NonNull
public String getPackage() {
return rPackage;
}
@NonNull
public ResourceType getType() {
return type;
}
@NonNull
public String getName() {
return name;
}
boolean isFramework() {
return rPackage.equals(ANDROID_PKG);
}
public AndroidReference(
UExpression node,
String rPackage,
ResourceType type,
String name) {
this.node = node;
this.rPackage = rPackage;
this.type = type;
this.name = name;
}
}
@@ -21,9 +21,18 @@ import com.android.tools.klint.detector.api.ClassContext;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.ClassScanner;
import com.google.common.annotations.Beta;
import org.jetbrains.org.objectweb.asm.tree.*;
import java.util.*;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Specialized visitor for running detectors on a class object model.
@@ -81,9 +90,9 @@ class AsmVisitor {
if (names != null) {
checkFullClass = false;
for (String element : names) {
List<ClassScanner> list = mMethodNameToChecks.get(element);
List<Detector.ClassScanner> list = mMethodNameToChecks.get(element);
if (list == null) {
list = new ArrayList<ClassScanner>();
list = new ArrayList<Detector.ClassScanner>();
mMethodNameToChecks.put(element, list);
}
list.add(scanner);
@@ -94,9 +103,9 @@ class AsmVisitor {
if (owners != null) {
checkFullClass = false;
for (String element : owners) {
List<ClassScanner> list = mMethodOwnerToChecks.get(element);
List<Detector.ClassScanner> list = mMethodOwnerToChecks.get(element);
if (list == null) {
list = new ArrayList<ClassScanner>();
list = new ArrayList<Detector.ClassScanner>();
mMethodOwnerToChecks.put(element, list);
}
list.add(scanner);
@@ -16,14 +16,19 @@
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAR;
import static org.objectweb.asm.Opcodes.ASM5;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import org.jetbrains.org.objectweb.asm.ClassReader;
import org.jetbrains.org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import java.io.File;
import java.io.FileInputStream;
@@ -35,10 +40,6 @@ import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAR;
import static org.jetbrains.org.objectweb.asm.Opcodes.ASM5;
/** A class, present either as a .class file on disk, or inside a .jar file. */
@VisibleForTesting
class ClassEntry implements Comparable<ClassEntry> {
@@ -27,7 +27,7 @@ import com.google.common.annotations.Beta;
/**
* Lint configuration for an Android project such as which specific rules to include,
* which specific rules to exclude, and which specific errors to ignore.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -16,6 +16,9 @@
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.CURRENT_PLATFORM;
import static com.android.SdkConstants.PLATFORM_WINDOWS;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Context;
@@ -28,22 +31,35 @@ import com.android.utils.XmlUtils;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import org.w3c.dom.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXParseException;
import java.io.*;
import java.util.*;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static com.android.SdkConstants.CURRENT_PLATFORM;
import static com.android.SdkConstants.PLATFORM_WINDOWS;
/**
* Default implementation of a {@link Configuration} which reads and writes
* configuration data into {@code lint.xml} in the project directory.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -251,7 +267,7 @@ public class DefaultConfiguration extends Configuration {
}, mClient);
mClient.report(new Context(driver, mProject, mProject, mConfigFile),
IssueRegistry.LINT_ERROR,
mProject.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR),
mProject.getConfiguration(driver).getSeverity(IssueRegistry.LINT_ERROR),
Location.create(mConfigFile), message, TextFormat.RAW);
}
@@ -348,7 +364,6 @@ public class DefaultConfiguration extends Configuration {
}
}
@VisibleForTesting
@NonNull
public static String globToRegexp(@NonNull String glob) {
StringBuilder sb = new StringBuilder(glob.length() * 2);
@@ -16,6 +16,52 @@
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ABS_LIST_VIEW;
import static com.android.SdkConstants.ABS_SEEK_BAR;
import static com.android.SdkConstants.ABS_SPINNER;
import static com.android.SdkConstants.ADAPTER_VIEW;
import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.CHECKABLE;
import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
import static com.android.SdkConstants.CHECK_BOX;
import static com.android.SdkConstants.COMPOUND_BUTTON;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
import static com.android.SdkConstants.FRAME_LAYOUT;
import static com.android.SdkConstants.GALLERY;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.IMAGE_BUTTON;
import static com.android.SdkConstants.IMAGE_VIEW;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.LIST_VIEW;
import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.PROGRESS_BAR;
import static com.android.SdkConstants.RADIO_BUTTON;
import static com.android.SdkConstants.RADIO_GROUP;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.SCROLL_VIEW;
import static com.android.SdkConstants.SEEK_BAR;
import static com.android.SdkConstants.SPINNER;
import static com.android.SdkConstants.SURFACE_VIEW;
import static com.android.SdkConstants.SWITCH;
import static com.android.SdkConstants.TABLE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.TAB_HOST;
import static com.android.SdkConstants.TAB_WIDGET;
import static com.android.SdkConstants.TEXT_VIEW;
import static com.android.SdkConstants.TOGGLE_BUTTON;
import static com.android.SdkConstants.VIEW;
import static com.android.SdkConstants.VIEW_ANIMATOR;
import static com.android.SdkConstants.VIEW_GROUP;
import static com.android.SdkConstants.VIEW_PKG_PREFIX;
import static com.android.SdkConstants.VIEW_STUB;
import static com.android.SdkConstants.VIEW_SWITCHER;
import static com.android.SdkConstants.WEB_VIEW;
import static com.android.SdkConstants.WIDGET_PKG_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
@@ -26,8 +72,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static com.android.SdkConstants.*;
/**
* Default simple implementation of an {@link SdkInfo}
* <p>
@@ -85,7 +129,7 @@ class DefaultSdkInfo extends SdkInfo {
if (parent.equals(child)) {
return true;
}
if (implementsInterface(child, parentType)) {
if (implementsInterface(child, parent)) {
return true;
}
child = PARENTS.get(child);
@@ -102,7 +146,7 @@ class DefaultSdkInfo extends SdkInfo {
return interfaceName.equals(INTERFACES.get(className));
}
// Strip off type parameters, e.g. AdapterView<?> => AdapterView
// Strip off type parameters, e.g. AdapterView<?> AdapterView
private static String getRawType(String type) {
if (type != null) {
int index = type.indexOf('<');
@@ -18,7 +18,6 @@ package com.android.tools.klint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
@@ -27,8 +26,16 @@ import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.google.common.annotations.Beta;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Registry which provides a list of checks to be performed on an Android project
@@ -38,8 +45,8 @@ import java.util.*;
*/
@Beta
public abstract class IssueRegistry {
private static List<Category> sCategories;
private static Map<String, Issue> sIdToIssue;
private static volatile List<Category> sCategories;
private static volatile Map<String, Issue> sIdToIssue;
private static Map<EnumSet<Scope>, List<Issue>> sScopeIssues = Maps.newHashMap();
/**
@@ -265,19 +272,31 @@ public abstract class IssueRegistry {
*
* @return an iterator for all the categories, never null
*/
@SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
@NonNull
public List<Category> getCategories() {
if (sCategories == null) {
final Set<Category> categories = new HashSet<Category>();
for (Issue issue : getIssues()) {
categories.add(issue.getCategory());
List<Category> categories = sCategories;
if (categories == null) {
synchronized (IssueRegistry.class) {
categories = sCategories;
if (categories == null) {
sCategories = categories = Collections.unmodifiableList(createCategoryList());
}
}
List<Category> sorted = new ArrayList<Category>(categories);
Collections.sort(sorted);
sCategories = Collections.unmodifiableList(sorted);
}
return sCategories;
return categories;
}
@NonNull
private List<Category> createCategoryList() {
Set<Category> categorySet = Sets.newHashSetWithExpectedSize(20);
for (Issue issue : getIssues()) {
categorySet.add(issue.getCategory());
}
List<Category> sorted = new ArrayList<Category>(categorySet);
Collections.sort(sorted);
return sorted;
}
/**
@@ -286,27 +305,39 @@ public abstract class IssueRegistry {
* @param id the id to be checked
* @return the corresponding issue, or null
*/
@SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
@Nullable
public final Issue getIssue(@NonNull String id) {
if (sIdToIssue == null) {
List<Issue> issues = getIssues();
sIdToIssue = new HashMap<String, Issue>(issues.size());
for (Issue issue : issues) {
sIdToIssue.put(issue.getId(), issue);
Map<String, Issue> map = sIdToIssue;
if (map == null) {
synchronized (IssueRegistry.class) {
map = sIdToIssue;
if (map == null) {
map = createIdToIssueMap();
sIdToIssue = map;
}
}
sIdToIssue.put(PARSER_ERROR.getId(), PARSER_ERROR);
sIdToIssue.put(LINT_ERROR.getId(), LINT_ERROR);
}
return sIdToIssue.get(id);
return map.get(id);
}
@NonNull
private Map<String, Issue> createIdToIssueMap() {
List<Issue> issues = getIssues();
Map<String, Issue> map = Maps.newHashMapWithExpectedSize(issues.size() + 2);
for (Issue issue : issues) {
map.put(issue.getId(), issue);
}
map.put(PARSER_ERROR.getId(), PARSER_ERROR);
map.put(LINT_ERROR.getId(), LINT_ERROR);
return map;
}
/**
* Reset the registry such that it recomputes its available issues.
* <p>
* NOTE: This is only intended for testing purposes.
*/
@VisibleForTesting
protected static void reset() {
sIdToIssue = null;
sCategories = null;
@@ -15,23 +15,32 @@
*/
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.DOT_CLASS;
import com.android.annotations.NonNull;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
/**
* <p> An {@link IssueRegistry} for a custom lint rule jar file. The rule jar should provide a
@@ -46,15 +55,24 @@ class JarFileIssueRegistry extends IssueRegistry {
* Manifest constant for declaring an issue provider. Example: Lint-Registry:
* foo.bar.CustomIssueRegistry
*/
private static final String MF_LINT_REGISTRY = "Lint-Registry"; //$NON-NLS-1$
private static final String MF_LINT_REGISTRY_OLD = "Lint-Registry"; //$NON-NLS-1$
private static final String MF_LINT_REGISTRY = "Lint-Registry-v2"; //$NON-NLS-1$
private static Map<File, SoftReference<JarFileIssueRegistry>> sCache;
private final List<Issue> myIssues;
private boolean mHasLegacyDetectors;
/** True if one or more java detectors were found that use the old Lombok-based API */
public boolean hasLegacyDetectors() {
return mHasLegacyDetectors;
}
@NonNull
static IssueRegistry get(@NonNull LintClient client, @NonNull File jarFile) throws IOException,
ClassNotFoundException, IllegalAccessException, InstantiationException {
static JarFileIssueRegistry get(@NonNull LintClient client, @NonNull File jarFile)
throws IOException, ClassNotFoundException, IllegalAccessException,
InstantiationException {
if (sCache == null) {
sCache = new HashMap<File, SoftReference<JarFileIssueRegistry>>();
} else {
@@ -67,6 +85,9 @@ class JarFileIssueRegistry extends IssueRegistry {
}
}
// Ensure that the scope-to-detector map doesn't return stale results
IssueRegistry.reset();
JarFileIssueRegistry registry = new JarFileIssueRegistry(client, jarFile);
sCache.put(jarFile, new SoftReference<JarFileIssueRegistry>(registry));
return registry;
@@ -78,19 +99,48 @@ class JarFileIssueRegistry extends IssueRegistry {
myIssues = Lists.newArrayList();
JarFile jarFile = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
jarFile = new JarFile(file);
Manifest manifest = jarFile.getManifest();
Attributes attrs = manifest.getMainAttributes();
Object object = attrs.get(new Attributes.Name(MF_LINT_REGISTRY));
boolean isLegacy = false;
if (object == null) {
object = attrs.get(new Attributes.Name(MF_LINT_REGISTRY_OLD));
//noinspection VariableNotUsedInsideIf
if (object != null) {
// It's an old rule. We don't yet conclude that
// mHasLegacyDetectors=true
// because the lint checks may not be Java related.
isLegacy = true;
}
}
if (object instanceof String) {
String className = (String) object;
// Make a class loader for this jar
URL url = SdkUtils.fileToUrl(file);
URLClassLoader loader = new URLClassLoader(new URL[]{url},
ClassLoader loader = client.createUrlClassLoader(new URL[]{url},
JarFileIssueRegistry.class.getClassLoader());
Class<?> registryClass = Class.forName(className, true, loader);
IssueRegistry registry = (IssueRegistry) registryClass.newInstance();
myIssues.addAll(registry.getIssues());
if (isLegacy) {
// If it's an old registry, look through the issues to see if it
// provides Java scanning and if so create the old style visitors
for (Issue issue : myIssues) {
EnumSet<Scope> scope = issue.getImplementation().getScope();
if (scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.JAVA_LIBRARIES)
|| scope.contains(Scope.ALL_JAVA_FILES)) {
mHasLegacyDetectors = true;
break;
}
}
}
if (loader instanceof URLClassLoader) {
loadAndCloseURLClassLoader(client, file, (URLClassLoader)loader);
}
} else {
client.log(Severity.ERROR, null,
"Custom lint rule jar %1$s does not contain a valid registry manifest key " +
@@ -105,6 +155,79 @@ class JarFileIssueRegistry extends IssueRegistry {
}
}
/**
* Work around http://bugs.java.com/bugdatabase/view_bug.do?bug_id=5041014 :
* URLClassLoader, on Windows, locks the .jar file forever.
* As of Java 7, there's a workaround: you can call close() when you're "done"
* with the file. We'll do that here. However, the whole point of the
* {@linkplain JarFileIssueRegistry} is that when lint is run over and over again
* as the user is editing in the IDE and we're background checking the code, we
* don't to keep loading the custom view classes over and over again: we want to
* cache them. Therefore, just closing the URLClassLoader right away isn't great
* either. However, it turns out it's safe to close the URLClassLoader once you've
* loaded the classes you need, since the URLClassLoader will continue to serve
* those classes even after its close() methods has been called.
* <p>
* Therefore, if we can call close() on this URLClassLoader, we'll proactively load
* all class files we find in the .jar file, then close it.
*
* @param client the client to report errors to
* @param file the .jar file
* @param loader the URLClassLoader we should close
*/
private static void loadAndCloseURLClassLoader(
@NonNull LintClient client,
@NonNull File file,
@NonNull URLClassLoader loader) {
try {
// Proactively close out the .jar file. This is only available on Java 7.
Method closeMethod = loader.getClass().getDeclaredMethod("close");
// But first, proactively load all classes:
try {
InputStream inputStream = new FileInputStream(file);
try {
JarInputStream jarInputStream = new JarInputStream(inputStream);
try {
ZipEntry entry = jarInputStream.getNextEntry();
while (entry != null) {
String name = entry.getName();
// Load non-inner-classes
if (name.endsWith(DOT_CLASS)) {
// Strip .class suffix and change .jar file path (/)
// to class name (.'s).
name = name.substring(0,
name.length() - DOT_CLASS.length());
name = name.replace('/', '.');
try {
Class.forName(name, true, loader);
} catch (Throwable e) {
client.log(Severity.ERROR, e,
"Failed to prefetch " + name + " from " + file);
}
}
entry = jarInputStream.getNextEntry();
}
} finally {
jarInputStream.close();
}
} finally {
inputStream.close();
}
} catch (Throwable ignore) {
} finally {
// Finally close the URL class loader
try {
closeMethod.invoke(loader);
} catch (Throwable ignore) {
// Couldn't close. This is unlikely.
}
}
} catch (NoSuchMethodException ignore) {
// No close method - we're on 1.6
}
}
@NonNull
@Override
public List<Issue> getIssues() {
@@ -0,0 +1,266 @@
/*
* Copyright (C) 2016 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 com.android.tools.klint.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.ClassContext;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMember;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiType;
import java.io.File;
@SuppressWarnings("MethodMayBeStatic") // Some of these methods may be overridden by LintClients
public abstract class JavaEvaluator {
public abstract boolean extendsClass(
@Nullable PsiClass cls,
@NonNull String className,
boolean strict);
public abstract boolean implementsInterface(
@NonNull PsiClass cls,
@NonNull String interfaceName,
boolean strict);
public boolean isMemberInSubClassOf(
@NonNull PsiMember method,
@NonNull String className,
boolean strict) {
PsiClass containingClass = method.getContainingClass();
return containingClass != null && extendsClass(containingClass, className, strict);
}
public static boolean isMemberInClass(
@Nullable PsiMember method,
@NonNull String className) {
if (method == null) {
return false;
}
PsiClass containingClass = method.getContainingClass();
return containingClass != null && className.equals(containingClass.getQualifiedName());
}
public int getParameterCount(@NonNull PsiMethod method) {
return method.getParameterList().getParametersCount();
}
/**
* Checks whether the class extends a super class or implements a given interface. Like calling
* both {@link #extendsClass(PsiClass, String, boolean)} and {@link
* #implementsInterface(PsiClass, String, boolean)}.
*/
public boolean inheritsFrom(
@NonNull PsiClass cls,
@NonNull String className,
boolean strict) {
return extendsClass(cls, className, strict) || implementsInterface(cls, className, strict);
}
/**
* Returns true if the given method (which is typically looked up by resolving a method call) is
* either a method in the exact given class, or if {@code allowInherit} is true, a method in a
* class possibly extending the given class, and if the parameter types are the exact types
* specified.
*
* @param method the method in question
* @param className the class name the method should be defined in or inherit from (or
* if null, allow any class)
* @param allowInherit whether we allow checking for inheritance
* @param argumentTypes the names of the types of the parameters
* @return true if this method is defined in the given class and with the given parameters
*/
public boolean methodMatches(
@NonNull PsiMethod method,
@Nullable String className,
boolean allowInherit,
@NonNull String... argumentTypes) {
if (className != null && allowInherit) {
if (!isMemberInSubClassOf(method, className, false)) {
return false;
}
}
return parametersMatch(method, argumentTypes);
}
/**
* Returns true if the given method's parameters are the exact types specified.
*
* @param method the method in question
* @param argumentTypes the names of the types of the parameters
* @return true if this method is defined in the given class and with the given parameters
*/
public boolean parametersMatch(
@NonNull PsiMethod method,
@NonNull String... argumentTypes) {
PsiParameterList parameterList = method.getParameterList();
if (parameterList.getParametersCount() != argumentTypes.length) {
return false;
}
PsiParameter[] parameters = parameterList.getParameters();
for (int i = 0; i < parameters.length; i++) {
PsiType type = parameters[i].getType();
if (!type.getCanonicalText().equals(argumentTypes[i])) {
return false;
}
}
return true;
}
/** Returns true if the given type matches the given fully qualified type name */
public boolean parameterHasType(
@Nullable PsiMethod method,
int parameterIndex,
@NonNull String typeName) {
if (method == null) {
return false;
}
PsiParameterList parameterList = method.getParameterList();
return parameterList.getParametersCount() > parameterIndex
&& typeMatches(parameterList.getParameters()[parameterIndex].getType(), typeName);
}
/** Returns true if the given type matches the given fully qualified type name */
public boolean typeMatches(
@Nullable PsiType type,
@NonNull String typeName) {
return type != null && type.getCanonicalText().equals(typeName);
}
@Nullable
public PsiElement resolve(@NonNull PsiElement element) {
if (element instanceof PsiReference) {
return ((PsiReference)element).resolve();
} else if (element instanceof PsiMethodCallExpression) {
PsiElement resolved = ((PsiMethodCallExpression) element).resolveMethod();
if (resolved != null) {
return resolved;
}
}
return null;
}
public boolean isPublic(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.PUBLIC);
}
return false;
}
public boolean isStatic(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.STATIC);
}
return false;
}
public boolean isPrivate(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.PRIVATE);
}
return false;
}
public boolean isAbstract(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.ABSTRACT);
}
return false;
}
public boolean isFinal(@Nullable PsiModifierListOwner owner) {
if (owner != null) {
PsiModifierList modifierList = owner.getModifierList();
return modifierList != null && modifierList.hasModifierProperty(PsiModifier.FINAL);
}
return false;
}
@Nullable
public PsiMethod getSuperMethod(@Nullable PsiMethod method) {
if (method == null) {
return null;
}
final PsiMethod[] superMethods = method.findSuperMethods();
if (superMethods.length > 0) {
return superMethods[0];
}
return null;
}
@NonNull
public String getInternalName(@NonNull PsiClass psiClass) {
String qualifiedName = psiClass.getQualifiedName();
if (qualifiedName == null) {
qualifiedName = psiClass.getName();
if (qualifiedName == null) {
assert psiClass instanceof PsiAnonymousClass;
//noinspection ConstantConditions
return getInternalName(psiClass.getContainingClass());
}
}
return ClassContext.getInternalName(qualifiedName);
}
@NonNull
public String getInternalName(@NonNull PsiClassType psiClassType) {
return ClassContext.getInternalName(psiClassType.getCanonicalText());
}
@Nullable
public abstract PsiClass findClass(@NonNull String qualifiedName);
@Nullable
public abstract PsiClassType getClassType(@Nullable PsiClass psiClass);
@NonNull
public abstract PsiAnnotation[] getAllAnnotations(@NonNull PsiModifierListOwner owner,
boolean inHierarchy);
@Nullable
public abstract PsiAnnotation findAnnotationInHierarchy(
@NonNull PsiModifierListOwner listOwner,
@NonNull String... annotationNames);
@Nullable
public abstract PsiAnnotation findAnnotation(
@Nullable PsiModifierListOwner listOwner,
@NonNull String... annotationNames);
@Nullable
public abstract File getFile(@NonNull PsiFile file);
}
@@ -1,38 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* 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 com.android.tools.klint.client.api;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UastVisitorExtension;
import org.jetbrains.uast.UastConverter;
import org.jetbrains.uast.java.JavaUastLanguagePlugin;
import java.util.List;
public class JavaLintLanguageExtension extends LintLanguageExtension {
@NotNull
@Override
public UastConverter getConverter() {
return JavaUastLanguagePlugin.INSTANCE.getConverter();
}
@NotNull
@Override
public List<UastVisitorExtension> getVisitorExtensions() {
return JavaUastLanguagePlugin.INSTANCE.getVisitorExtensions();
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -14,19 +14,21 @@
* limitations under the License.
*/
package com.android.tools.lint.client.api;
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.ANDROID_PKG;
import static com.android.SdkConstants.R_CLASS;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Detector.XmlScanner;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.klint.client.api.JavaParser.ResolvedClass;
import com.android.tools.klint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.klint.client.api.JavaParser.ResolvedNode;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaScanner;
import com.android.tools.klint.detector.api.Detector.XmlScanner;
import com.android.tools.klint.detector.api.JavaContext;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -285,6 +287,16 @@ public class JavaVisitor {
return;
}
if (e.getClass().getSimpleName().equals("IndexNotReadyException")) {
// Attempting to access PSI during startup before indices are ready; ignore these.
// See http://b.android.com/176644 for an example.
return;
} else if (e.getClass().getSimpleName().equals("ProcessCanceledException")) {
// Cancelling inspections in the IDE
context.getDriver().cancel();
return;
}
// Work around ECJ bugs; see https://code.google.com/p/android/issues/detail?id=172268
// Don't allow lint bugs to take down the whole build. TRY to log this as a
// lint error instead!
@@ -293,11 +305,13 @@ public class JavaVisitor {
sb.append(context.file.getName());
sb.append(" (this is a bug in lint or one of the libraries it depends on)\n");
sb.append(e.getClass().getSimpleName());
sb.append(':');
StackTraceElement[] stackTrace = e.getStackTrace();
int count = 0;
for (StackTraceElement frame : stackTrace) {
if (count > 0) {
sb.append("->");
sb.append("<-");
}
String className = frame.getClassName();
@@ -322,6 +336,24 @@ public class JavaVisitor {
}
}
/**
* For testing only: returns the number of exceptions thrown during Java AST analysis
*
* @return the number of internal errors found
*/
@VisibleForTesting
public static int getCrashCount() {
return sExceptionCount;
}
/**
* For testing only: clears the crash counter
*/
@VisibleForTesting
public static void clearCrashCount() {
sExceptionCount = 0;
}
public void prepare(@NonNull List<JavaContext> contexts) {
mParser.prepareJavaParse(contexts);
}
@@ -330,6 +362,29 @@ public class JavaVisitor {
mParser.dispose();
}
@Nullable
private static Set<String> getInterfaceNames(
@Nullable Set<String> addTo,
@NonNull ResolvedClass cls) {
Iterable<ResolvedClass> interfaces = cls.getInterfaces();
for (ResolvedClass resolvedInterface : interfaces) {
String name = resolvedInterface.getName();
if (addTo == null) {
addTo = Sets.newHashSet();
} else if (addTo.contains(name)) {
// Superclasses can explicitly implement the same interface,
// so keep track of visited interfaces as we traverse up the
// super class chain to avoid checking the same interface
// more than once.
continue;
}
addTo.add(name);
getInterfaceNames(addTo, resolvedInterface);
}
return addTo;
}
private static class VisitingDetector {
private AstVisitor mVisitor; // construct lazily, and clear out on context switch!
private JavaContext mContext;
@@ -388,18 +443,37 @@ public class JavaVisitor {
ResolvedClass resolvedClass = (ResolvedClass) resolved;
ResolvedClass cls = resolvedClass;
int depth = 0;
while (cls != null) {
String fqcn = cls.getSignature();
if (fqcn != null) {
List<VisitingDetector> list = mSuperClassDetectors.get(fqcn);
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, node, node, resolvedClass);
List<VisitingDetector> list = mSuperClassDetectors.get(cls.getName());
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, node, node, resolvedClass);
}
}
// Check interfaces too
Set<String> interfaceNames = getInterfaceNames(null, cls);
if (interfaceNames != null) {
for (String name : interfaceNames) {
list = mSuperClassDetectors.get(name);
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, node, node,
resolvedClass);
}
}
}
}
cls = cls.getSuperClass();
depth++;
if (depth == 500) {
// Shouldn't happen in practice; this prevents the IDE from
// hanging if the user has accidentally typed in an incorrect
// super class which creates a cycle.
break;
}
}
return false;
@@ -409,10 +483,7 @@ public class JavaVisitor {
public boolean visitConstructorInvocation(ConstructorInvocation node) {
NormalTypeBody anonymous = node.astAnonymousClassBody();
if (anonymous != null) {
ResolvedNode resolved = mContext.resolve(anonymous);
if (resolved instanceof ResolvedMethod) {
resolved = ((ResolvedMethod) resolved).getContainingClass();
}
ResolvedNode resolved = mContext.resolve(node.astTypeReference());
if (!(resolved instanceof ResolvedClass)) {
return true;
}
@@ -420,13 +491,24 @@ public class JavaVisitor {
ResolvedClass resolvedClass = (ResolvedClass) resolved;
ResolvedClass cls = resolvedClass;
while (cls != null) {
String fqcn = cls.getSignature();
if (fqcn != null) {
List<VisitingDetector> list = mSuperClassDetectors.get(fqcn);
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, null, anonymous,
resolvedClass);
List<VisitingDetector> list = mSuperClassDetectors.get(cls.getName());
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, null, anonymous,
resolvedClass);
}
}
// Check interfaces too
Set<String> interfaceNames = getInterfaceNames(null, cls);
if (interfaceNames != null) {
for (String name : interfaceNames) {
list = mSuperClassDetectors.get(name);
if (list != null) {
for (VisitingDetector v : list) {
v.getJavaScanner().checkClass(mContext, null, anonymous,
resolvedClass);
}
}
}
}
@@ -1377,7 +1459,7 @@ public class JavaVisitor {
ResolvedNode resolved = mContext.resolve(node);
if (resolved instanceof ResolvedMethod) {
ResolvedMethod method = (ResolvedMethod) resolved;
String type = method.getContainingClass().getSignature();
String type = method.getContainingClass().getName();
List<VisitingDetector> list = mConstructorDetectors.get(type);
if (list != null) {
for (VisitingDetector v : list) {
+219 -36
View File
@@ -16,16 +16,30 @@
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.CLASS_FOLDER;
import static com.android.SdkConstants.DOT_AAR;
import static com.android.SdkConstants.DOT_JAR;
import static com.android.SdkConstants.FD_ASSETS;
import static com.android.SdkConstants.GEN_FOLDER;
import static com.android.SdkConstants.LIBS_FOLDER;
import static com.android.SdkConstants.RES_FOLDER;
import static com.android.SdkConstants.SRC_FOLDER;
import static com.android.tools.klint.detector.api.LintUtils.endsWith;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidArtifact;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.Dependencies;
import com.android.builder.model.Variant;
import com.android.ide.common.repository.ResourceVisibilityLookup;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.prefs.AndroidLocation;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkVersionInfo;
import com.android.sdklib.repository.local.LocalSdk;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Issue;
@@ -40,7 +54,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import org.jetbrains.uast.UastLanguagePlugin;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -49,17 +63,21 @@ import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.util.*;
import static com.android.SdkConstants.*;
import static com.android.tools.klint.detector.api.LintUtils.endsWith;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Information about the tool embedding the lint analyzer. IDEs and other tools
* implementing lint support will extend this to integrate logging, displaying errors,
* etc.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -67,6 +85,16 @@ import static com.android.tools.klint.detector.api.LintUtils.endsWith;
public abstract class LintClient {
private static final String PROP_BIN_DIR = "com.android.tools.lint.bindir"; //$NON-NLS-1$
protected LintClient(@NonNull String clientName) {
//noinspection AssignmentToStaticFieldFromInstanceMethod
sClientName = clientName;
}
protected LintClient() {
//noinspection AssignmentToStaticFieldFromInstanceMethod
sClientName = "unknown";
}
/**
* Returns a configuration for use by the given project. The configuration
* provides information about which issues are enabled, any customizations
@@ -75,15 +103,17 @@ public abstract class LintClient {
* By default this method returns a {@link DefaultConfiguration}.
*
* @param project the project to obtain a configuration for
* @param driver the current driver, if any
* @return a configuration, never null.
*/
public Configuration getConfiguration(@NonNull Project project) {
@NonNull
public Configuration getConfiguration(@NonNull Project project, @Nullable LintDriver driver) {
return DefaultConfiguration.create(this, project, null);
}
/**
* Report the given issue. This method will only be called if the configuration
* provided by {@link #getConfiguration(Project)} has reported the corresponding
* provided by {@link #getConfiguration(Project,LintDriver)} has reported the corresponding
* issue as enabled and has not filtered out the issue with its
* {@link Configuration#ignore(Context,Issue,Location,String)} method.
* <p>
@@ -98,7 +128,7 @@ public abstract class LintClient {
@NonNull Context context,
@NonNull Issue issue,
@NonNull Severity severity,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@NonNull TextFormat format);
@@ -141,6 +171,19 @@ public abstract class LintClient {
@Nullable
public abstract XmlParser getXmlParser();
/**
* Returns a {@link JavaParser} to use to parse Java
*
* @param project the project to parse, if known (this can be used to look up
* the class path for type attribution etc, and it can also be used
* to more efficiently process a set of files, for example to
* perform type attribution for multiple units in a single pass)
* @return a new {@link JavaParser}, or null if this client does not
* support Java analysis
*/
@Nullable
public abstract JavaParser getJavaParser(@Nullable Project project);
/**
* Returns an optimal detector, if applicable. By default, just returns the
* original detector, but tools can replace detectors using this hook with a version
@@ -207,12 +250,15 @@ public abstract class LintClient {
/**
* Returns the list of Java libraries
*
* @param project the project to look up jar dependencies for
* @param project the project to look up jar dependencies for
* @param includeProvided If true, included provided libraries too (libraries that are not
* packaged with the app, but are provided for compilation purposes and
* are assumed to be present in the running environment)
* @return a list of jar dependencies containing .class files
*/
@NonNull
public List<File> getJavaLibraries(@NonNull Project project) {
return getClassPath(project).getLibraries();
public List<File> getJavaLibraries(@NonNull Project project, boolean includeProvided) {
return getClassPath(project).getLibraries(includeProvided);
}
/**
@@ -242,6 +288,22 @@ public abstract class LintClient {
return Collections.emptyList();
}
/**
* Returns the asset folders.
*
* @param project the project to look up the asset folder for
* @return a list of files pointing to the asset folders, possibly empty
*/
@NonNull
public List<File> getAssetFolders(@NonNull Project project) {
File assets = new File(project.getDir(), FD_ASSETS);
if (assets.exists()) {
return Collections.singletonList(assets);
}
return Collections.emptyList();
}
/**
* Returns the {@link SdkInfo} to use for the given project.
*
@@ -381,15 +443,6 @@ public abstract class LintClient {
return false;
}
@Nullable
public com.intellij.openapi.project.Project getProject() {
return null;
}
public List<UastLanguagePlugin> getLanguagePlugins() {
return Collections.emptyList();
}
/**
* Information about class paths (sources, class files and libraries)
* usually associated with a project.
@@ -398,16 +451,19 @@ public abstract class LintClient {
private final List<File> mClassFolders;
private final List<File> mSourceFolders;
private final List<File> mLibraries;
private final List<File> mNonProvidedLibraries;
private final List<File> mTestFolders;
public ClassPathInfo(
@NonNull List<File> sourceFolders,
@NonNull List<File> classFolders,
@NonNull List<File> libraries,
@NonNull List<File> nonProvidedLibraries,
@NonNull List<File> testFolders) {
mSourceFolders = sourceFolders;
mClassFolders = classFolders;
mLibraries = libraries;
mNonProvidedLibraries = nonProvidedLibraries;
mTestFolders = testFolders;
}
@@ -422,8 +478,8 @@ public abstract class LintClient {
}
@NonNull
public List<File> getLibraries() {
return mLibraries;
public List<File> getLibraries(boolean includeProvided) {
return includeProvided ? mLibraries : mNonProvidedLibraries;
}
public List<File> getTestSourceFolders() {
@@ -497,7 +553,7 @@ public abstract class LintClient {
File[] jars = libs.listFiles();
if (jars != null) {
for (File jar : jars) {
if (LintUtils.endsWith(jar.getPath(), DOT_JAR)
if (endsWith(jar.getPath(), DOT_JAR)
&& !libraries.contains(jar)) {
libraries.add(jar);
}
@@ -556,7 +612,7 @@ public abstract class LintClient {
}
}
info = new ClassPathInfo(sources, classes, libraries, tests);
info = new ClassPathInfo(sources, classes, libraries, libraries, tests);
mProjectInfo.put(project, info);
}
@@ -568,7 +624,7 @@ public abstract class LintClient {
* projects are unique for a directory (in case we process a library project
* before its including project for example)
*/
private Map<File, Project> mDirToProject;
protected Map<File, Project> mDirToProject;
/**
* Returns a project for the given directory. This should return the same
@@ -645,7 +701,7 @@ public abstract class LintClient {
mDirToProject.put(canonicalDir, project);
}
private Set<File> mProjectDirs = Sets.newHashSet();
protected Set<File> mProjectDirs = Sets.newHashSet();
/**
* Create a project for the given directory
@@ -756,6 +812,32 @@ public abstract class LintClient {
return max;
}
/**
* Returns the specific version of the build tools being used for the given project, if known
*
* @param project the project in question
*
* @return the build tools version in use by the project, or null if not known
*/
@Nullable
public BuildToolInfo getBuildTools(@NonNull Project project) {
//TODO
//AndroidSdkHandler sdk = getSdk();
//// Build systems like Eclipse and ant just use the latest available
//// build tools, regardless of project metadata. In Gradle, this
//// method is overridden to use the actual build tools specified in the
//// project.
//if (sdk != null) {
// IAndroidTarget compileTarget = getCompileTarget(project);
// if (compileTarget != null) {
// return compileTarget.getBuildToolInfo();
// }
// return sdk.getLatestBuildTool(getRepositoryLogger(), false);
//}
return null;
}
/**
* Returns the super class for the given class name, which should be in VM
* format (e.g. java/lang/Integer, not java.lang.Integer, and using $ rather
@@ -807,7 +889,7 @@ public abstract class LintClient {
*/
@NonNull
public Map<String, String> createSuperClassMap(@NonNull Project project) {
List<File> libraries = project.getJavaLibraries();
List<File> libraries = project.getJavaLibraries(true);
List<File> classFolders = project.getJavaClassFolders();
List<ClassEntry> classEntries = ClassEntry.fromClassPath(this, classFolders, true);
if (libraries.isEmpty()) {
@@ -904,10 +986,41 @@ public abstract class LintClient {
@SuppressWarnings("MethodMayBeStatic") // Intentionally instance method so it can be overridden
@NonNull
public List<File> findRuleJars(@NonNull Project project) {
if (project.getDir().getPath().endsWith(DOT_AAR)) {
File lintJar = new File(project.getDir(), "lint.jar"); //$NON-NLS-1$
if (lintJar.exists()) {
return Collections.singletonList(lintJar);
if (project.isGradleProject()) {
if (project.isLibrary()) {
AndroidLibrary model = project.getGradleLibraryModel();
if (model != null) {
File lintJar = model.getLintJar();
if (lintJar.exists()) {
return Collections.singletonList(lintJar);
}
}
} else if (project.getSubset() != null) {
// Probably just analyzing a single file: we still want to look for custom
// rules applicable to the file
List<File> rules = null;
final Variant variant = project.getCurrentVariant();
if (variant != null) {
Collection<AndroidLibrary> libraries = variant.getMainArtifact()
.getDependencies().getLibraries();
for (AndroidLibrary library : libraries) {
File lintJar = library.getLintJar();
if (lintJar.exists()) {
if (rules == null) {
rules = Lists.newArrayListWithExpectedSize(4);
}
rules.add(lintJar);
}
}
if (rules != null) {
return rules;
}
}
} else if (project.getDir().getPath().endsWith(DOT_AAR)) {
File lintJar = new File(project.getDir(), "lint.jar"); //$NON-NLS-1$
if (lintJar.exists()) {
return Collections.singletonList(lintJar);
}
}
}
@@ -921,7 +1034,7 @@ public abstract class LintClient {
* settings.
*
* @param url the URL to read
* @return a {@link java.net.URLConnection} or null
* @return a {@link URLConnection} or null
* @throws IOException if any kind of IO exception occurs
*/
@Nullable
@@ -929,7 +1042,7 @@ public abstract class LintClient {
return url.openConnection();
}
/** Closes a connection previously returned by {@link #openConnection(java.net.URL)} */
/** Closes a connection previously returned by {@link #openConnection(URL)} */
public void closeConnection(@NonNull URLConnection connection) throws IOException {
if (connection instanceof HttpURLConnection) {
((HttpURLConnection)connection).disconnect();
@@ -946,7 +1059,7 @@ public abstract class LintClient {
*/
@SuppressWarnings("MethodMayBeStatic") // Intentionally instance method so it can be overridden
public boolean isProjectDirectory(@NonNull File dir) {
return LintUtils.isManifestFolder(dir) || Project.isAospFrameworksProject(dir);
return LintUtils.isManifestFolder(dir) || Project.isAospFrameworksRelatedProject(dir);
}
/**
@@ -986,6 +1099,17 @@ public abstract class LintClient {
return registry;
}
/**
* Creates a {@link ClassLoader} which can load in a set of Jar files.
*
* @param urls the URLs
* @param parent the parent class loader
* @return a new class loader
*/
public ClassLoader createUrlClassLoader(@NonNull URL[] urls, @NonNull ClassLoader parent) {
return new URLClassLoader(urls, parent);
}
/**
* Returns true if this client supports project resource repository lookup via
* {@link #getProjectResources(Project,boolean)}
@@ -1034,4 +1158,63 @@ public abstract class LintClient {
}
return mResourceVisibility;
}
/**
* The client name returned by {@link #getClientName()} when running in
* Android Studio/IntelliJ IDEA
*/
public static final String CLIENT_STUDIO = "studio";
/**
* The client name returned by {@link #getClientName()} when running in
* Gradle
*/
public static final String CLIENT_GRADLE = "gradle";
/**
* The client name returned by {@link #getClientName()} when running in
* the CLI (command line interface) version of lint, {@code lint}
*/
public static final String CLIENT_CLI = "cli";
/**
* The client name returned by {@link #getClientName()} when running in
* some unknown client
*/
public static final String CLIENT_UNKNOWN = "unknown";
/** The client name. */
@NonNull
private static String sClientName = CLIENT_UNKNOWN;
/**
* Returns the name of the embedding client. It could be not just
* {@link #CLIENT_STUDIO}, {@link #CLIENT_GRADLE}, {@link #CLIENT_CLI}
* etc but other values too as lint is integrated in other embedding contexts.
*
* @return the name of the embedding client
*/
@NonNull
public static String getClientName() {
return sClientName;
}
/**
* Returns true if the embedding client currently running lint is Android Studio
* (or IntelliJ IDEA)
*
* @return true if running in Android Studio / IntelliJ IDEA
*/
public static boolean isStudio() {
return CLIENT_STUDIO.equals(sClientName);
}
/**
* Returns true if the embedding client currently running lint is Gradle
*
* @return true if running in Gradle
*/
public static boolean isGradle() {
return CLIENT_GRADLE.equals(sClientName);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,61 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* 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 com.android.tools.klint.client.api;
import com.android.annotations.Nullable;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import org.jetbrains.uast.UastLanguagePlugin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public abstract class LintLanguageExtension implements UastLanguagePlugin {
public static final ExtensionPointName<LintLanguageExtension> EP_NAME =
ExtensionPointName.create("com.android.tools.klint.client.api.lintLanguageExtension");
public static boolean isFileSupported(@Nullable Project project, String name) {
LintLanguageExtension[] extensions = getExtensions(project);
for (LintLanguageExtension ext : extensions) {
if (ext.getConverter().isFileSupported(name)) {
return true;
}
}
return false;
}
public static List<UastLanguagePlugin> getPlugins(@Nullable Project project) {
if (project == null) {
return Collections.emptyList();
}
LintLanguageExtension[] languageExtensions = project.getExtensions(EP_NAME);
List<UastLanguagePlugin> converters = new ArrayList<UastLanguagePlugin>(languageExtensions.length);
Collections.addAll(converters, languageExtensions);
return converters;
}
public static LintLanguageExtension[] getExtensions(@Nullable Project project) {
if (project == null) {
return new LintLanguageExtension[0];
}
return project.getExtensions(EP_NAME);
}
}
@@ -172,6 +172,7 @@ public class LintRequest {
* @param project the project to look up the main project for
* @return the main project
*/
@SuppressWarnings("MethodMayBeStatic")
@NonNull
public Project getMainProject(@NonNull Project project) {
return project;
@@ -15,6 +15,13 @@
*/
package com.android.tools.klint.client.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FD_ASSETS;
import static com.android.tools.klint.detector.api.Detector.OtherFileScanner;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Context;
@@ -25,10 +32,11 @@ import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.util.*;
import static com.android.SdkConstants.*;
import static com.android.tools.klint.detector.api.Detector.OtherFileScanner;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Visitor for "other" files: files that aren't java sources,
@@ -94,16 +102,16 @@ class OtherFileVisitor {
}
}
if (scopes.contains(Scope.SOURCE_FILE)) {
if (scopes.contains(Scope.JAVA_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getPath().endsWith(DOT_JAVA)) {
if (file.getPath().endsWith(".kt")) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.SOURCE_FILE, files);
mFiles.put(Scope.JAVA_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
@@ -111,7 +119,7 @@ class OtherFileVisitor {
collectFiles(files, srcFolder);
}
if (!files.isEmpty()) {
mFiles.put(Scope.SOURCE_FILE, files);
mFiles.put(Scope.JAVA_FILE, files);
}
}
}
@@ -24,10 +24,20 @@ import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.ResourceContext;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.annotations.Beta;
import org.w3c.dom.*;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
/**
* Specialized visitor for running detectors on resources: typically XML documents,
@@ -1,11 +1,11 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
* Copyright (C) 2016 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
* 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,
@@ -16,26 +16,303 @@
package com.android.tools.klint.client.api;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import org.jetbrains.uast.*;
import com.android.annotations.Nullable;
import com.android.resources.ResourceType;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.JavaContext;
import com.google.common.base.Joiner;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiVariable;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.ULiteralExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.UResolvable;
import org.jetbrains.uast.USimpleNameReferenceExpression;
import org.jetbrains.uast.UVariable;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.java.JavaAbstractUExpression;
import org.jetbrains.uast.java.JavaUVariableDeclarationsExpression;
import java.util.Collections;
import java.util.List;
public class UastLintUtils {
public static boolean isChildOfExpression(
@NonNull UExpression child,
@NonNull UExpression parent) {
UElement current = child;
while (current != null) {
if (current.equals(parent)) {
return true;
} else if (!(current instanceof UExpression)) {
return false;
}
current = current.getParent();
@NonNull
public static String getClassName(PsiClassType type) {
PsiClass psiClass = type.resolve();
if (psiClass == null) {
return type.getClassName();
} else {
return getClassName(psiClass);
}
return false;
}
@NonNull
public static String getClassName(PsiClass psiClass) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(psiClass.getName());
psiClass = psiClass.getContainingClass();
while (psiClass != null) {
stringBuilder.insert(0, psiClass.getName() + ".");
psiClass = psiClass.getContainingClass();
}
return stringBuilder.toString();
}
@Nullable
public static UExpression findLastAssignment(
@NonNull PsiVariable variable,
@NonNull UElement call,
@NonNull JavaContext context) {
UElement lastAssignment = null;
if (variable instanceof UVariable) {
variable = ((UVariable) variable).getPsi();
}
if (!variable.hasModifierProperty(PsiModifier.FINAL) &&
(variable instanceof PsiLocalVariable || variable instanceof PsiParameter)) {
UMethod containingFunction = UastUtils.getContainingUMethod(call);
if (containingFunction != null) {
ConstantEvaluator.LastAssignmentFinder
finder = new ConstantEvaluator.LastAssignmentFinder(
variable, call, context, null,
(variable instanceof PsiParameter) ? 1 : 0);
containingFunction.accept(finder);
lastAssignment = finder.getLastAssignment();
}
} else {
lastAssignment = context.getUastContext().getInitializerBody(variable);
}
if (lastAssignment instanceof UExpression) {
return (UExpression) lastAssignment;
}
return null;
}
@Nullable
public static String getReferenceName(UReferenceExpression expression) {
if (expression instanceof USimpleNameReferenceExpression) {
return ((USimpleNameReferenceExpression) expression).getIdentifier();
} else if (expression instanceof UQualifiedReferenceExpression) {
UExpression selector = ((UQualifiedReferenceExpression) expression).getSelector();
if (selector instanceof USimpleNameReferenceExpression) {
return ((USimpleNameReferenceExpression) selector).getIdentifier();
}
}
return null;
}
@Nullable
public static Object findLastValue(
@NonNull PsiVariable variable,
@NonNull UElement call,
@NonNull JavaContext context,
@NonNull ConstantEvaluator evaluator) {
Object value = null;
if (!variable.hasModifierProperty(PsiModifier.FINAL) &&
(variable instanceof PsiLocalVariable || variable instanceof PsiParameter)) {
UMethod containingFunction = UastUtils.getContainingUMethod(call);
if (containingFunction != null) {
ConstantEvaluator.LastAssignmentFinder
finder = new ConstantEvaluator.LastAssignmentFinder(
variable, call, context, evaluator, 1);
containingFunction.getUastBody().accept(finder);
value = finder.getCurrentValue();
}
} else {
UExpression initializer = context.getUastContext().getInitializerBody(variable);
if (initializer != null) {
value = initializer.evaluate();
}
}
return value;
}
@Nullable
private static AndroidReference toAndroidReference(UQualifiedReferenceExpression expression) {
List<String> path = UastUtils.asQualifiedPath(expression);
String packageNameFromResolved = null;
PsiClass containingClass = UastUtils.getContainingClass(expression.resolve());
if (containingClass != null) {
String containingClassFqName = containingClass.getQualifiedName();
if (containingClassFqName != null) {
int i = containingClassFqName.lastIndexOf(".R.");
if (i >= 0) {
packageNameFromResolved = containingClassFqName.substring(0, i);
}
}
}
if (path == null) {
return null;
}
int size = path.size();
if (size < 3) {
return null;
}
String r = path.get(size - 3);
if (!r.equals(SdkConstants.R_CLASS)) {
return null;
}
String packageName = packageNameFromResolved != null
? packageNameFromResolved
: Joiner.on('.').join(path.subList(0, size - 3));
String type = path.get(size - 2);
String name = path.get(size - 1);
ResourceType resourceType = null;
for (ResourceType value : ResourceType.values()) {
if (value.getName().equals(type)) {
resourceType = value;
break;
}
}
if (resourceType == null) {
return null;
}
return new AndroidReference(expression, packageName, resourceType, name);
}
@Nullable
public static AndroidReference toAndroidReferenceViaResolve(UElement element) {
if (element instanceof UQualifiedReferenceExpression
&& element instanceof JavaAbstractUExpression) {
AndroidReference ref = toAndroidReference((UQualifiedReferenceExpression) element);
if (ref != null) {
return ref;
}
}
PsiElement declaration;
if (element instanceof UVariable) {
declaration = ((UVariable) element).getPsi();
} else if (element instanceof UResolvable) {
declaration = ((UResolvable) element).resolve();
} else {
return null;
}
if (declaration == null && element instanceof USimpleNameReferenceExpression
&& element instanceof JavaAbstractUExpression) {
// R class can't be resolved in tests so we need to use heuristics to calc the reference
UExpression maybeQualified = UastUtils.getQualifiedParentOrThis((UExpression) element);
if (maybeQualified instanceof UQualifiedReferenceExpression) {
AndroidReference ref = toAndroidReference(
(UQualifiedReferenceExpression) maybeQualified);
if (ref != null) {
return ref;
}
}
}
if (!(declaration instanceof PsiVariable)) {
return null;
}
PsiVariable variable = (PsiVariable) declaration;
if (!(variable instanceof PsiField)
|| variable.getType() != PsiType.INT
|| !variable.hasModifierProperty(PsiModifier.STATIC)
|| !variable.hasModifierProperty(PsiModifier.FINAL)) {
return null;
}
PsiClass resTypeClass = ((PsiField) variable).getContainingClass();
if (resTypeClass == null || !resTypeClass.hasModifierProperty(PsiModifier.STATIC)) {
return null;
}
PsiClass rClass = resTypeClass.getContainingClass();
if (rClass == null || rClass.getContainingClass() != null || !"R".equals(rClass.getName())) {
return null;
}
String packageName = ((PsiJavaFile) rClass.getContainingFile()).getPackageName();
if (packageName.isEmpty()) {
return null;
}
String resourceTypeName = resTypeClass.getName();
ResourceType resourceType = null;
for (ResourceType value : ResourceType.values()) {
if (value.getName().equals(resourceTypeName)) {
resourceType = value;
break;
}
}
if (resourceType == null) {
return null;
}
String resourceName = variable.getName();
UExpression node;
if (element instanceof UExpression) {
node = (UExpression) element;
} else if (element instanceof UVariable) {
node = new JavaUVariableDeclarationsExpression(
null, Collections.singletonList(((UVariable) element)));
} else {
throw new IllegalArgumentException("element must be an expression or an UVariable");
}
return new AndroidReference(node, packageName, resourceType, resourceName);
}
public static boolean areIdentifiersEqual(UExpression first, UExpression second) {
String firstIdentifier = getIdentifier(first);
String secondIdentifier = getIdentifier(second);
return firstIdentifier != null && secondIdentifier != null
&& firstIdentifier.equals(secondIdentifier);
}
@Nullable
public static String getIdentifier(UExpression expression) {
if (expression instanceof ULiteralExpression) {
expression.asRenderString();
} else if (expression instanceof USimpleNameReferenceExpression) {
return ((USimpleNameReferenceExpression) expression).getIdentifier();
} else if (expression instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression qualified = (UQualifiedReferenceExpression) expression;
String receiverIdentifier = getIdentifier(qualified.getReceiver());
String selectorIdentifier = getIdentifier(qualified.getSelector());
if (receiverIdentifier == null || selectorIdentifier == null) {
return null;
}
return receiverIdentifier + "." + selectorIdentifier;
}
return null;
}
}
@@ -22,6 +22,7 @@ import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@@ -29,7 +30,7 @@ import org.w3c.dom.Node;
/**
* A wrapper for an XML parser. This allows tools integrating lint to map directly
* to builtin services, such as already-parsed data structures in XML editors.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -22,7 +22,7 @@ import com.google.common.annotations.Beta;
/**
* A category is a container for related issues.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -107,7 +107,36 @@ public final class Category implements Comparable<Category> {
}
@Override
public int compareTo(Category other) {
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Category category = (Category) o;
//noinspection SimplifiableIfStatement
if (!mName.equals(category.mName)) {
return false;
}
return mParent != null ? mParent.equals(category.mParent) : category.mParent == null;
}
@Override
public String toString() {
return getFullName();
}
@Override
public int hashCode() {
return mName.hashCode();
}
@Override
public int compareTo(@NonNull Category other) {
if (other.mPriority == mPriority) {
if (mParent == other) {
return 1;
@@ -115,7 +144,13 @@ public final class Category implements Comparable<Category> {
return -1;
}
}
return other.mPriority - mPriority;
int delta = other.mPriority - mPriority;
if (delta != 0) {
return delta;
}
return mName.compareTo(other.mName);
}
/** Issues related to running lint itself */
@@ -139,9 +174,6 @@ public final class Category implements Comparable<Category> {
/** Issues related to internationalization */
public static final Category I18N = create("Internationalization", 50);
/** Issues related to right to left and bi-directional text support */
public static final Category RTL = create("Bi-directional Text", 40);
// Sub categories
/** Issues related to icons */
@@ -152,4 +184,7 @@ public final class Category implements Comparable<Category> {
/** Issues related to messages/strings */
public static final Category MESSAGES = create(CORRECTNESS, "Messages", 95);
/** Issues related to right to left and bidirectional text support */
public static final Category RTL = create(I18N, "Bidirectional Text", 40);
}
@@ -16,22 +16,36 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.tools.klint.detector.api.Location.SearchDirection.BACKWARD;
import static com.android.tools.klint.detector.api.Location.SearchDirection.EOL_BACKWARD;
import static com.android.tools.klint.detector.api.Location.SearchDirection.FORWARD;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.detector.api.Location.SearchDirection;
import com.android.tools.klint.detector.api.Location.SearchHints;
import com.android.utils.AsmUtils;
import com.google.common.annotations.Beta;
import com.google.common.base.Splitter;
import org.jetbrains.org.objectweb.asm.Type;
import org.jetbrains.org.objectweb.asm.tree.*;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.io.File;
import java.util.List;
import static com.android.SdkConstants.*;
/**
* A {@link Context} used when checking .class files.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -148,11 +162,11 @@ public class ClassContext extends Context {
if (source == null) {
source = file.getName();
if (source.endsWith(DOT_CLASS)) {
source = source.substring(0, source.length() - DOT_CLASS.length()) + DOT_JAVA;
source = source.substring(0, source.length() - DOT_CLASS.length()) + ".kt";
}
int index = source.indexOf('$');
if (index != -1) {
source = source.substring(0, index) + DOT_JAVA;
source = source.substring(0, index) + ".kt";
}
}
if (source != null) {
@@ -248,7 +262,7 @@ public class ClassContext extends Context {
*/
@NonNull
public Location getLocationForLine(int line, @Nullable String patternStart,
@Nullable String patternEnd, @Nullable Location.SearchHints hints) {
@Nullable String patternEnd, @Nullable SearchHints hints) {
File sourceFile = getSourceFile();
if (sourceFile != null) {
// ASM line numbers are 1-based, and lint line numbers are 0-based
@@ -269,8 +283,8 @@ public class ClassContext extends Context {
* Detectors should only call this method if an error applies to the whole class
* scope and there is no specific method or field that applies to the error.
* If so, use
* {@link #report(Issue, org.jetbrains.org.objectweb.asm.tree.MethodNode, org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode, Location, String)} or
* {@link #report(Issue, org.jetbrains.org.objectweb.asm.tree.FieldNode, Location, String)}, such that
* {@link #report(Issue, org.objectweb.asm.tree.MethodNode, org.objectweb.asm.tree.AbstractInsnNode, Location, String)} or
* {@link #report(Issue, org.objectweb.asm.tree.FieldNode, Location, String)}, such that
* suppress annotations are checked.
*
* @param issue the issue to report
@@ -280,7 +294,7 @@ public class ClassContext extends Context {
@Override
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
if (mDriver.isSuppressed(issue, mClassNode)) {
return;
@@ -344,7 +358,7 @@ public class ClassContext extends Context {
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
if (method != null && mDriver.isSuppressed(issue, mClassNode, method, instruction)) {
return;
@@ -365,7 +379,7 @@ public class ClassContext extends Context {
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
if (field != null && mDriver.isSuppressed(issue, field)) {
return;
@@ -387,7 +401,7 @@ public class ClassContext extends Context {
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, method, instruction, location, message);
@@ -406,7 +420,7 @@ public class ClassContext extends Context {
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, field, location, message);
@@ -502,7 +516,7 @@ public class ClassContext extends Context {
}
return getLocationForLine(findLineNumber(classNode), pattern, null,
Location.SearchHints.create(Location.SearchDirection.BACKWARD).matchJavaSymbol());
SearchHints.create(BACKWARD).matchJavaSymbol());
}
@Nullable
@@ -543,21 +557,21 @@ public class ClassContext extends Context {
// to find a method, look up the corresponding line number then search
// around it for a suitable tag, such as the class name.
String pattern;
Location.SearchDirection searchMode;
SearchDirection searchMode;
if (methodNode.name.equals(CONSTRUCTOR_NAME)) {
searchMode = Location.SearchDirection.EOL_BACKWARD;
searchMode = EOL_BACKWARD;
if (isAnonymousClass(classNode.name)) {
pattern = classNode.superName.substring(classNode.superName.lastIndexOf('/') + 1);
} else {
pattern = classNode.name.substring(classNode.name.lastIndexOf('$') + 1);
}
} else {
searchMode = Location.SearchDirection.BACKWARD;
searchMode = BACKWARD;
pattern = methodNode.name;
}
return getLocationForLine(findLineNumber(methodNode), pattern, null,
Location.SearchHints.create(searchMode).matchJavaSymbol());
SearchHints.create(searchMode).matchJavaSymbol());
}
/**
@@ -569,7 +583,7 @@ public class ClassContext extends Context {
*/
@NonNull
public Location getLocation(@NonNull AbstractInsnNode instruction) {
Location.SearchHints hints = Location.SearchHints.create(Location.SearchDirection.FORWARD).matchJavaSymbol();
SearchHints hints = SearchHints.create(FORWARD).matchJavaSymbol();
String pattern = null;
if (instruction instanceof MethodInsnNode) {
MethodInsnNode call = (MethodInsnNode) instruction;
@@ -685,7 +699,7 @@ public class ClassContext extends Context {
// If class name contains $, it's not an ambiguous inner class name.
if (fqcn.indexOf('$') != -1) {
return fqcn.replace('.', '/');
return AsmUtils.toInternalName(fqcn);
}
// Let's assume that components that start with Caps are class names.
StringBuilder sb = new StringBuilder(fqcn.length());
@@ -16,6 +16,11 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.SUPPRESS_ALL;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.Configuration;
@@ -29,8 +34,6 @@ import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import static com.android.SdkConstants.*;
/**
* Context passed to the detectors during an analysis run. It provides
* information about the file being analyzed, it allows shared properties (so
@@ -100,7 +103,7 @@ public class Context {
mDriver = driver;
mProject = project;
mMainProject = main;
mConfiguration = project.getConfiguration();
mConfiguration = project.getConfiguration(driver);
}
/**
@@ -245,23 +248,33 @@ public class Context {
* Reports an issue. Convenience wrapper around {@link LintClient#report}
*
* @param issue the issue to report
* @param location the location of the issue, or null if not known
* @param location the location of the issue
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
//noinspection ConstantConditions
if (location == null) {
// Misbehaving third-party lint detectors
assert false : issue;
return;
}
if (location == Location.NONE) {
// Detector reported error for issue in a non-applicable location etc
return;
}
Configuration configuration = mConfiguration;
// If this error was computed for a context where the context corresponds to
// a project instead of a file, the actual error may be in a different project (e.g.
// a library project), so adjust the configuration as necessary.
if (location != null && location.getFile() != null) {
Project project = mDriver.findProjectFor(location.getFile());
if (project != null) {
configuration = project.getConfiguration();
}
Project project = mDriver.findProjectFor(location.getFile());
if (project != null) {
configuration = project.getConfiguration(mDriver);
}
// If an error occurs in a library project, but you've disabled that check in the
@@ -293,7 +306,7 @@ public class Context {
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, location, message);
@@ -348,7 +361,7 @@ public class Context {
// Java and XML files are handled in sub classes (XmlContext, JavaContext)
String path = file.getPath();
if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_GRADLE)) {
if (path.endsWith(DOT_JAVA) || path.endsWith(".kt") || path.endsWith(DOT_GRADLE)) {
return JavaContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(DOT_XML)) {
return XmlContext.SUPPRESS_COMMENT_PREFIX;
File diff suppressed because it is too large Load Diff
@@ -24,7 +24,7 @@ import java.util.EnumSet;
* An {@linkplain Implementation} of an {@link Issue} maps to the {@link Detector}
* class responsible for analyzing the issue, as well as the {@link Scope} required
* by the detector to perform its analysis.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -16,7 +16,10 @@
package com.android.tools.klint.detector.api;
import static com.android.tools.klint.detector.api.TextFormat.RAW;
import com.android.annotations.NonNull;
import com.android.tools.klint.client.api.Configuration;
import com.google.common.annotations.Beta;
import java.util.ArrayList;
@@ -32,7 +35,7 @@ import java.util.List;
* multiple different issues as it's analyzing code, and we want to be able to
* different severities for different issues, the ability to suppress one but
* not other issues from the same detector, and so on.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -139,7 +142,7 @@ public final class Issue implements Comparable<Issue> {
*/
@NonNull
public String getBriefDescription(@NonNull TextFormat format) {
return TextFormat.RAW.convertTo(mBriefDescription, format);
return RAW.convertTo(mBriefDescription, format);
}
/**
@@ -153,7 +156,7 @@ public final class Issue implements Comparable<Issue> {
*/
@NonNull
public String getExplanation(@NonNull TextFormat format) {
return TextFormat.RAW.convertTo(mExplanation, format);
return RAW.convertTo(mExplanation, format);
}
/**
@@ -16,29 +16,86 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.tools.klint.client.api.JavaParser.ResolvedNode;
import static com.android.tools.klint.client.api.JavaParser.TypeDescriptor;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.JavaParser;
import com.android.tools.klint.client.api.JavaParser.ResolvedClass;
import com.android.tools.klint.client.api.LintDriver;
import org.jetbrains.annotations.NotNull;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.google.common.collect.Iterators;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiEnumConstant;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiLabeledStatement;
import com.intellij.psi.PsiMember;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiNameIdentifierOwner;
import com.intellij.psi.PsiNewExpression;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiSwitchStatement;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.psi.PsiElementBacked;
import org.jetbrains.uast.psi.UElementWithLocation;
import java.io.File;
import java.util.List;
import java.util.Iterator;
import static com.android.SdkConstants.CLASS_CONTEXT;
import lombok.ast.AnnotationElement;
import lombok.ast.AnnotationMethodDeclaration;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.ConstructorInvocation;
import lombok.ast.EnumConstant;
import lombok.ast.Expression;
import lombok.ast.LabelledStatement;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Position;
import lombok.ast.TypeDeclaration;
import lombok.ast.VariableReference;
/**
* A {@link Context} used when checking Java files.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
public class JavaContext extends Context implements UastAndroidContext {
public class JavaContext extends Context {
static final String SUPPRESS_COMMENT_PREFIX = "//noinspection "; //$NON-NLS-1$
/**
* The parse tree
*
* @deprecated Use {@link #mJavaFile} instead (see {@link JavaPsiScanner})
*/
@Deprecated
private Node mCompilationUnit;
/** The parse tree */
private UFile mCompilationUnit;
private PsiJavaFile mJavaFile;
private UFile mUFile;
/** The parser which produced the parse tree */
private final JavaParser mParser;
/**
* Constructs a {@link JavaContext} for running lint on the given file, with
@@ -58,23 +115,184 @@ public class JavaContext extends Context implements UastAndroidContext {
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file) {
@NonNull File file,
@NonNull JavaParser parser) {
super(driver, project, main, file);
mParser = parser;
}
@NonNull
public UastContext getUastContext() {
return mParser.getUastContext();
}
/**
* Returns a location for the given node
*
* @param node the AST node to get a location for
* @return a location for the given node
*/
@Override
public Location getLocation(@Nullable UElement element) {
return UastAndroidContext.DefaultImpls.getLocation(this, element);
@NonNull
public Location getLocation(@NonNull Node node) {
return mParser.getLocation(this, node);
}
/**
* Returns a location for the given node range (from the starting offset of the first node to
* the ending offset of the second node).
*
* @param from the AST node to get a starting location from
* @param fromDelta Offset delta to apply to the starting offset
* @param to the AST node to get a ending location from
* @param toDelta Offset delta to apply to the ending offset
* @return a location for the given node
*/
@NonNull
public Location getRangeLocation(
@NonNull Node from,
int fromDelta,
@NonNull Node to,
int toDelta) {
return mParser.getRangeLocation(this, from, fromDelta, to, toDelta);
}
/**
* Returns a location for the given node range (from the starting offset of the first node to
* the ending offset of the second node).
*
* @param from the AST node to get a starting location from
* @param fromDelta Offset delta to apply to the starting offset
* @param to the AST node to get a ending location from
* @param toDelta Offset delta to apply to the ending offset
* @return a location for the given node
*/
@NonNull
public Location getRangeLocation(
@NonNull PsiElement from,
int fromDelta,
@NonNull PsiElement to,
int toDelta) {
return mParser.getRangeLocation(this, from, fromDelta, to, toDelta);
}
/**
* Returns a {@link Location} for the given node. This attempts to pick a shorter
* location range than the entire node; for a class or method for example, it picks
* the name node (if found). For statement constructs such as a {@code switch} statement
* it will highlight the keyword, etc.
*
* @param node the AST node to create a location for
* @return a location for the given node
*/
@NonNull
public Location getNameLocation(@NonNull Node node) {
return mParser.getNameLocation(this, node);
}
/**
* Returns a {@link Location} for the given node. This attempts to pick a shorter
* location range than the entire node; for a class or method for example, it picks
* the name node (if found). For statement constructs such as a {@code switch} statement
* it will highlight the keyword, etc.
*
* @param element the AST node to create a location for
* @return a location for the given node
*/
@NonNull
public Location getNameLocation(@NonNull PsiElement element) {
if (element instanceof PsiSwitchStatement) {
// Just use keyword
return mParser.getRangeLocation(this, element, 0, 6); // 6: "switch".length()
}
return mParser.getNameLocation(this, element);
}
@NonNull
public Location getUastNameLocation(@NonNull UElement element) {
if (element instanceof UDeclaration) {
UElement nameIdentifier = ((UDeclaration) element).getUastAnchor();
if (nameIdentifier != null) {
return getUastLocation(nameIdentifier);
}
} else if (element instanceof PsiNameIdentifierOwner) {
PsiElement nameIdentifier = ((PsiNameIdentifierOwner) element).getNameIdentifier();
if (nameIdentifier != null) {
return getLocation(nameIdentifier);
}
} else if (element instanceof UCallExpression) {
UElement methodReference = ((UCallExpression) element).getMethodIdentifier();
if (methodReference != null) {
return getUastLocation(methodReference);
}
}
return getUastLocation(element);
}
@NonNull
public Location getLocation(@NonNull PsiElement node) {
return mParser.getLocation(this, node);
}
@NonNull
public Location getUastLocation(@Nullable UElement node) {
if (node == null) {
return Location.NONE;
}
if (node instanceof UElementWithLocation) {
UFile file = UastUtils.getContainingFile(node);
if (file == null) {
return Location.NONE;
}
File ioFile = UastUtils.getIoFile(file);
if (ioFile == null) {
return Location.NONE;
}
UElementWithLocation segment = (UElementWithLocation) node;
return Location.create(ioFile, file.getPsi().getText(),
segment.getStartOffset(), segment.getEndOffset());
} else if (node instanceof PsiElementBacked) {
PsiElement psiElement = ((PsiElementBacked) node).getPsi();
if (psiElement != null) {
TextRange range = psiElement.getTextRange();
UFile containingFile = getUFile();
if (containingFile == null) {
return Location.NONE;
}
File file = this.file;
if (!containingFile.equals(getUFile())) {
// Reporting an error in a different file.
if (getDriver().getScope().size() == 1) {
// Don't bother with this error if it's in a different file during single-file analysis
return Location.NONE;
}
VirtualFile virtualFile = containingFile.getPsi().getVirtualFile();
if (virtualFile == null) {
return Location.NONE;
}
file = VfsUtilCore.virtualToIoFile(virtualFile);
}
return Location.create(file, getContents(), range.getStartOffset(),
range.getEndOffset());
}
}
return Location.NONE;
}
@NonNull
public JavaParser getParser() {
return mParser;
}
@NonNull
public JavaEvaluator getEvaluator() {
return mParser.getEvaluator();
}
@Nullable
public UFile getCompilationUnit() {
public Node getCompilationUnit() {
return mCompilationUnit;
}
@@ -84,12 +302,36 @@ public class JavaContext extends Context implements UastAndroidContext {
*
* @param compilationUnit the parse tree
*/
public void setCompilationUnit(@Nullable UFile compilationUnit) {
public void setCompilationUnit(@Nullable Node compilationUnit) {
mCompilationUnit = compilationUnit;
}
/**
* Returns the {@link UFile}.
*
* @return the parsed UFile
*/
@Nullable
public UFile getUFile() {
return mUFile;
}
/**
* Sets the compilation result. Not intended for client usage; the lint infrastructure
* will set this when a context has been processed
*
* @param javaFile the parse tree
*/
public void setJavaFile(@Nullable PsiJavaFile javaFile) {
mJavaFile = javaFile;
}
public void setUFile(@Nullable UFile file) {
mUFile = file;
}
@Override
public void report(@NonNull Issue issue, @Nullable Location location,
public void report(@NonNull Issue issue, @NonNull Location location,
@NonNull String message) {
if (mDriver.isSuppressed(this, issue, mCompilationUnit)) {
return;
@@ -108,60 +350,503 @@ public class JavaContext extends Context implements UastAndroidContext {
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
@Override
public void report(
@NonNull Issue issue,
@Nullable UElement scope,
@Nullable Location location,
@NonNull String message) {
@NonNull Issue issue,
@Nullable Node scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
public void report(
@NonNull Issue issue,
@Nullable PsiElement scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
public void report(
@NonNull Issue issue,
@Nullable UElement scope,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
/** UDeclaration is a PsiElement, so it's impossible to call report(Issue, UElement, ...)
* without an explicit cast. */
public void reportUast(
@NonNull Issue issue,
@Nullable UElement scope,
@NonNull Location location,
@NonNull String message) {
report(issue, scope, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, Node, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, Node, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
}
/**
* @deprecated Use {@link PsiTreeUtil#getParentOfType(PsiElement, Class[])}
* with PsiMethod.class instead
*/
@Deprecated
@Nullable
public static Node findSurroundingMethod(Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) {
return scope;
}
scope = scope.getParent();
}
return null;
}
/**
* @deprecated Use {@link PsiTreeUtil#getParentOfType(PsiElement, Class[])}
* with PsiMethod.class instead
*/
@Deprecated
@Nullable
public static ClassDeclaration findSurroundingClass(@Nullable Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == ClassDeclaration.class) {
return (ClassDeclaration) scope;
}
scope = scope.getParent();
}
return null;
}
@Override
@Nullable
protected String getSuppressCommentPrefix() {
return SUPPRESS_COMMENT_PREFIX;
}
/**
* @deprecated Use {@link #isSuppressedWithComment(PsiElement, Issue)} instead
*/
@Deprecated
public boolean isSuppressedWithComment(@NonNull Node scope, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
Position position = scope.getPosition();
if (position == null) {
return false;
}
int start = position.getStart();
return isSuppressedWithComment(start, issue);
}
public boolean isSuppressedWithComment(@NonNull UElement scope, @NonNull Issue issue) {
return false;
}
public boolean isSuppressedWithComment(@NonNull PsiElement scope, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
TextRange textRange = scope.getTextRange();
if (textRange == null) {
return false;
}
int start = textRange.getStartOffset();
return isSuppressedWithComment(start, issue);
}
/**
* @deprecated Location handles aren't needed for AST nodes anymore; just use the
* {@link PsiElement} from the AST
*/
@Deprecated
@NonNull
public Location.Handle createLocationHandle(@NonNull Node node) {
return mParser.createLocationHandle(this, node);
}
/**
* @deprecated Use PsiElement resolve methods (varies by AST node type, e.g.
* {@link PsiMethodCallExpression#resolveMethod()}
*/
@Deprecated
@Nullable
public ResolvedNode resolve(@NonNull Node node) {
return mParser.resolve(this, node);
}
/**
* @deprecated Use {@link JavaEvaluator#findClass(String)} instead
*/
@Deprecated
@Nullable
public ResolvedClass findClass(@NonNull String fullyQualifiedName) {
return mParser.findClass(this, fullyQualifiedName);
}
/**
* @deprecated Use {@link PsiExpression#getType()} )} instead
*/
@Deprecated
@Nullable
public TypeDescriptor getType(@NonNull Node node) {
return mParser.getType(this, node);
}
/**
* @deprecated Use {@link #getMethodName(PsiElement)} instead
*/
@Deprecated
@Nullable
public static String getMethodName(@NonNull Node call) {
if (call instanceof MethodInvocation) {
return ((MethodInvocation)call).astName().astValue();
} else if (call instanceof ConstructorInvocation) {
return ((ConstructorInvocation)call).astTypeReference().getTypeName();
} else if (call instanceof EnumConstant) {
return ((EnumConstant)call).astName().astValue();
} else {
return null;
}
}
@Nullable
public static String getMethodName(@NonNull PsiElement call) {
if (call instanceof PsiMethodCallExpression) {
return ((PsiMethodCallExpression)call).getMethodExpression().getReferenceName();
} else if (call instanceof PsiNewExpression) {
PsiJavaCodeReferenceElement classReference = ((PsiNewExpression) call).getClassReference();
if (classReference != null) {
return classReference.getReferenceName();
} else {
return null;
}
} else if (call instanceof PsiEnumConstant) {
return ((PsiEnumConstant)call).getName();
} else {
return null;
}
}
@Nullable
public static String getMethodName(@NonNull UElement call) {
if (call instanceof UEnumConstant) {
return ((UEnumConstant)call).getName();
} else if (call instanceof UCallExpression) {
String methodName = ((UCallExpression) call).getMethodName();
if (methodName != null) {
return methodName;
} else {
return UastUtils.getQualifiedName(((UCallExpression) call).getClassReference());
}
} else {
return null;
}
}
/**
* Searches for a name node corresponding to the given node
* @return the name node to use, if applicable
* @deprecated Use {@link #findNameElement(PsiElement)} instead
*/
@Deprecated
@Nullable
public static Node findNameNode(@NonNull Node node) {
if (node instanceof TypeDeclaration) {
// ClassDeclaration, AnnotationDeclaration, EnumDeclaration, InterfaceDeclaration
return ((TypeDeclaration) node).astName();
} else if (node instanceof MethodDeclaration) {
return ((MethodDeclaration)node).astMethodName();
} else if (node instanceof ConstructorDeclaration) {
return ((ConstructorDeclaration)node).astTypeName();
} else if (node instanceof MethodInvocation) {
return ((MethodInvocation)node).astName();
} else if (node instanceof ConstructorInvocation) {
return ((ConstructorInvocation)node).astTypeReference();
} else if (node instanceof EnumConstant) {
return ((EnumConstant)node).astName();
} else if (node instanceof AnnotationElement) {
return ((AnnotationElement)node).astName();
} else if (node instanceof AnnotationMethodDeclaration) {
return ((AnnotationMethodDeclaration)node).astMethodName();
} else if (node instanceof VariableReference) {
return ((VariableReference)node).astIdentifier();
} else if (node instanceof LabelledStatement) {
return ((LabelledStatement)node).astLabel();
}
return null;
}
/**
* Searches for a name node corresponding to the given node
* @return the name node to use, if applicable
*/
@Nullable
public static PsiElement findNameElement(@NonNull PsiElement element) {
if (element instanceof PsiClass) {
if (element instanceof PsiAnonymousClass) {
return ((PsiAnonymousClass)element).getBaseClassReference();
}
return ((PsiClass) element).getNameIdentifier();
} else if (element instanceof PsiMethod) {
return ((PsiMethod) element).getNameIdentifier();
} else if (element instanceof PsiMethodCallExpression) {
return ((PsiMethodCallExpression) element).getMethodExpression().
getReferenceNameElement();
} else if (element instanceof PsiNewExpression) {
return ((PsiNewExpression) element).getClassReference();
} else if (element instanceof PsiField) {
return ((PsiField)element).getNameIdentifier();
} else if (element instanceof PsiAnnotation) {
return ((PsiAnnotation)element).getNameReferenceElement();
} else if (element instanceof PsiReferenceExpression) {
return ((PsiReferenceExpression) element).getReferenceNameElement();
} else if (element instanceof PsiLabeledStatement) {
return ((PsiLabeledStatement)element).getLabelIdentifier();
}
return null;
}
@Deprecated
@NonNull
public static Iterator<Expression> getParameters(@NonNull Node call) {
if (call instanceof MethodInvocation) {
return ((MethodInvocation) call).astArguments().iterator();
} else if (call instanceof ConstructorInvocation) {
return ((ConstructorInvocation) call).astArguments().iterator();
} else if (call instanceof EnumConstant) {
return ((EnumConstant) call).astArguments().iterator();
} else {
return Iterators.emptyIterator();
}
}
@Deprecated
@Nullable
public static Node getParameter(@NonNull Node call, int parameter) {
Iterator<Expression> iterator = getParameters(call);
for (int i = 0; i < parameter - 1; i++) {
if (!iterator.hasNext()) {
return null;
}
iterator.next();
}
return iterator.hasNext() ? iterator.next() : null;
}
/**
* Returns true if the given method invocation node corresponds to a call on a
* {@code android.content.Context}
*
* @param node the method call node
* @return true iff the method call is on a class extending context
* @deprecated use {@link JavaEvaluator#isMemberInSubClassOf(PsiMember, String, boolean)} instead
*/
public boolean isContextMethod(@NonNull UCallExpression node) {
@Deprecated
public boolean isContextMethod(@NonNull MethodInvocation node) {
// Method name used in many other contexts where it doesn't have the
// same semantics; only use this one if we can resolve types
// and we're certain this is the Context method
UFunction resolved = node.resolve(this);
UClass containingClass = UastUtils.getContainingClass(resolved);
if (resolved != null && containingClass != null) {
if (containingClass.isSubclassOf(CLASS_CONTEXT)) {
ResolvedNode resolved = resolve(node);
if (resolved instanceof JavaParser.ResolvedMethod) {
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolved;
ResolvedClass containingClass = method.getContainingClass();
if (containingClass.isSubclassOf(CLASS_CONTEXT, false)) {
return true;
}
}
return false;
}
@NotNull
@Override
public JavaContext getLintContext() {
return this;
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz) {
return getParentOfType(element, clz, true);
}
@NotNull
@Override
public List<UastLanguagePlugin> getLanguagePlugins() {
return getClient().getLanguagePlugins();
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz,
boolean strict) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null) {
if (clz.isInstance(element)) {
//noinspection unchecked
return (T) element;
}
element = element.getParent();
}
return null;
}
@org.jetbrains.annotations.Nullable
@Override
public UElement convert(@Nullable Object element) {
return UastContext.DefaultImpls.convert(this, element);
/**
* Returns the first ancestor node of the given type, stopping at the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param terminators optional node types to terminate the search at
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
* @deprecated Use {@link PsiTreeUtil#getParentOfType} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getParentOfType(@Nullable Node element,
@NonNull Class<T> clz,
boolean strict,
@NonNull Class<? extends Node>... terminators) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null && !clz.isInstance(element)) {
for (Class<?> terminator : terminators) {
if (terminator.isInstance(element)) {
return null;
}
}
element = element.getParent();
}
//noinspection unchecked
return (T) element;
}
/**
* Returns the first sibling of the given node that is of the given class
*
* @param sibling the sibling to search from
* @param clz the type to look for
* @param <T> the type
* @return the first sibling of the given type, or null
* @deprecated Use {@link PsiTreeUtil#getNextSiblingOfType(PsiElement, Class)} instead
*/
@Deprecated
@Nullable
public static <T extends Node> T getNextSiblingOfType(@Nullable Node sibling,
@NonNull Class<T> clz) {
if (sibling == null) {
return null;
}
Node parent = sibling.getParent();
if (parent == null) {
return null;
}
Iterator<Node> iterator = parent.getChildren().iterator();
while (iterator.hasNext()) {
if (iterator.next() == sibling) {
break;
}
}
while (iterator.hasNext()) {
Node child = iterator.next();
if (clz.isInstance(child)) {
//noinspection unchecked
return (T) child;
}
}
return null;
}
/**
* Returns the given argument of the given call
*
* @param call the call containing arguments
* @param index the index of the target argument
* @return the argument at the given index
* @throws IllegalArgumentException if index is outside the valid range
*/
@Deprecated
@NonNull
public static Node getArgumentNode(@NonNull MethodInvocation call, int index) {
int i = 0;
for (Expression parameter : call.astArguments()) {
if (i == index) {
return parameter;
}
i++;
}
throw new IllegalArgumentException(Integer.toString(index));
}
}
@@ -16,17 +16,27 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_BOTTOM;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_TOP;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.google.common.annotations.Beta;
import org.w3c.dom.Element;
import static com.android.SdkConstants.*;
import org.w3c.dom.Element;
/**
* Abstract class specifically intended for layout detectors which provides some
* common utility methods shared by layout detectors.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
+237 -69
View File
@@ -16,6 +16,41 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LOCALE;
import static com.android.SdkConstants.BIN_FOLDER;
import static com.android.SdkConstants.DOT_GIF;
import static com.android.SdkConstants.DOT_JPEG;
import static com.android.SdkConstants.DOT_JPG;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.DOT_WEBP;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FN_BUILD_GRADLE;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.UTF_8;
import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER;
import static com.android.ide.common.resources.configuration.LocaleQualifier.BCP_47_PREFIX;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BYTE;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BYTE_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_CHAR;
import static com.android.tools.klint.client.api.JavaParser.TYPE_CHARACTER_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE;
import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INTEGER_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG;
import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_SHORT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_SHORT_WRAPPER;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidProject;
@@ -38,32 +73,45 @@ import com.android.tools.klint.client.api.LintClient;
import com.android.utils.PositionXmlParser;
import com.android.utils.SdkUtils;
import com.google.common.annotations.Beta;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.jetbrains.uast.UFile;
import org.jetbrains.uast.UImportStatement;
import org.jetbrains.org.objectweb.asm.Opcodes;
import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode;
import org.jetbrains.org.objectweb.asm.tree.ClassNode;
import org.jetbrains.org.objectweb.asm.tree.FieldNode;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiImportStatement;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiWhiteSpace;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UParenthesizedExpression;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static com.android.SdkConstants.*;
import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER;
import static com.android.ide.common.resources.configuration.LocaleQualifier.BCP_47_PREFIX;
import lombok.ast.ImportDeclaration;
/**
* Useful utility methods related to lint.
@@ -238,6 +286,22 @@ public class LintUtils {
return element == element.getOwnerDocument().getDocumentElement();
}
/**
* Returns the corresponding R field name for the given XML resource name
* @param styleName the XML name
* @return the corresponding R field name
*/
public static String getFieldName(@NonNull String styleName) {
for (int i = 0, n = styleName.length(); i < n; i++) {
char c = styleName.charAt(i);
if (c == '.' || c == '-' || c == ':') {
return styleName.replace('.', '_').replace('-', '_').replace(':', '_');
}
}
return styleName;
}
/**
* Returns the given id without an {@code @id/} or {@code @+id} prefix
*
@@ -559,7 +623,7 @@ public class LintUtils {
text = new String(data, offset, length, charset);
} catch (UnsupportedEncodingException e) {
try {
if (charset != defaultCharset) {
if (!charset.equals(defaultCharset)) {
text = new String(data, offset, length, defaultCharset);
}
} catch (UnsupportedEncodingException u) {
@@ -771,9 +835,12 @@ public class LintUtils {
* @param fullyQualifiedName the fully qualified class name
* @return true if the given imported name refers to the given fully
* qualified name
* @deprecated Use PSI element hierarchies instead where type resolution is more directly
* available (call {@link PsiImportStatement#resolve()})
*/
@Deprecated
public static boolean isImported(
@Nullable UFile compilationUnit,
@Nullable lombok.ast.Node compilationUnit,
@NonNull String fullyQualifiedName) {
if (compilationUnit == null) {
return false;
@@ -782,27 +849,26 @@ public class LintUtils {
int dotLength = fullyQualifiedName.length() - dotIndex;
boolean imported = false;
for (UImportStatement importStatement : compilationUnit.getImportStatements()) {
String fqn = importStatement.getFqNameToImport();
if (fqn == null) {
continue;
}
if (fqn.equals(fullyQualifiedName)) {
return true;
} else if (fullyQualifiedName.regionMatches(dotIndex, fqn,
fqn.length() - dotLength, dotLength)) {
// This import is importing the class name using some other prefix, so there
// fully qualified class name cannot be imported under that name
return false;
} else if (importStatement.isStarImport()
&& fqn.regionMatches(0, fqn, 0, dotIndex + 1)) {
imported = true;
// but don't break -- keep searching in case there's a non-wildcard
// import of the specific class name, e.g. if we're looking for
// android.content.SharedPreferences.Editor, don't match on the following:
// import android.content.SharedPreferences.*;
// import foo.bar.Editor;
for (lombok.ast.Node rootNode : compilationUnit.getChildren()) {
if (rootNode instanceof ImportDeclaration) {
ImportDeclaration importDeclaration = (ImportDeclaration) rootNode;
String fqn = importDeclaration.asFullyQualifiedName();
if (fqn.equals(fullyQualifiedName)) {
return true;
} else if (fullyQualifiedName.regionMatches(dotIndex, fqn,
fqn.length() - dotLength, dotLength)) {
// This import is importing the class name using some other prefix, so there
// fully qualified class name cannot be imported under that name
return false;
} else if (importDeclaration.astStarImport()
&& fqn.regionMatches(0, fqn, 0, dotIndex + 1)) {
imported = true;
// but don't break -- keep searching in case there's a non-wildcard
// import of the specific class name, e.g. if we're looking for
// android.content.SharedPreferences.Editor, don't match on the following:
// import android.content.SharedPreferences.*;
// import foo.bar.Editor;
}
}
}
@@ -1045,7 +1111,7 @@ public class LintUtils {
}
return new AndroidVersion(api.getApiLevel(), null);
}
/**
* Looks for a certain string within a larger string, which should immediately follow
* the given prefix and immediately precede the given suffix.
@@ -1130,40 +1196,6 @@ public class LintUtils {
return Collections.emptyList();
}
/**
* Escapes the given property file value (right hand side of property assignment)
* as required by the property file format (e.g. escapes colons and backslashes)
*
* @param value the value to be escaped
* @return the escaped value
*/
@NonNull
public static String escapePropertyValue(@NonNull String value) {
// Slow, stupid implementation, but is 100% compatible with Java's property file
// implementation
Properties properties = new Properties();
properties.setProperty("k", value); // key doesn't matter
StringWriter writer = new StringWriter();
try {
properties.store(writer, null);
String s = writer.toString();
int end = s.length();
// Writer inserts trailing newline
String lineSeparator = SdkUtils.getLineSeparator();
if (s.endsWith(lineSeparator)) {
end -= lineSeparator.length();
}
int start = s.indexOf('=');
assert start != -1 : s;
return s.substring(start + 1, end);
}
catch (IOException e) {
return value; // shouldn't happen; we're not going to disk
}
}
/**
* Returns the locale for the given parent folder.
*
@@ -1216,4 +1248,140 @@ public class LintUtils {
return "en".equals(locale.getLanguage()); //$NON-NLS-1$
}
}
/**
* Create a {@link Location} for an error in the top level build.gradle file.
* This is necessary when we're doing an analysis based on the Gradle interpreted model,
* not from parsing Gradle files - and the model doesn't provide source positions.
* @param project the project containing the gradle file being analyzed
* @return location for the top level gradle file if it exists, otherwise fall back to
* the project directory.
*/
public static Location guessGradleLocation(@NonNull Project project) {
File dir = project.getDir();
Location location;
File topLevel = new File(dir, FN_BUILD_GRADLE);
if (topLevel.exists()) {
location = Location.create(topLevel);
} else {
location = Location.create(dir);
}
return location;
}
/**
* Returns true if the given element is the null literal
*
* @param element the element to check
* @return true if the element is "null"
*/
public static boolean isNullLiteral(@Nullable PsiElement element) {
return element instanceof PsiLiteral && "null".equals(element.getText());
}
public static boolean isTrueLiteral(@Nullable PsiElement element) {
return element instanceof PsiLiteral && "true".equals(element.getText());
}
public static boolean isFalseLiteral(@Nullable PsiElement element) {
return element instanceof PsiLiteral && "false".equals(element.getText());
}
@Nullable
public static PsiElement skipParentheses(@Nullable PsiElement element) {
while (element instanceof PsiParenthesizedExpression) {
element = element.getParent();
}
return element;
}
@Nullable
public static UElement skipParentheses(@Nullable UElement element) {
while (element instanceof UParenthesizedExpression) {
element = element.getContainingElement();
}
return element;
}
@Nullable
public static PsiElement nextNonWhitespace(@Nullable PsiElement element) {
if (element != null) {
element = element.getNextSibling();
while (element instanceof PsiWhiteSpace) {
element = element.getNextSibling();
}
}
return element;
}
@Nullable
public static PsiElement prevNonWhitespace(@Nullable PsiElement element) {
if (element != null) {
element = element.getPrevSibling();
while (element instanceof PsiWhiteSpace) {
element = element.getPrevSibling();
}
}
return element;
}
public static boolean isString(@NonNull PsiType type) {
if (type instanceof PsiClassType) {
final String shortName = ((PsiClassType)type).getClassName();
if (!Objects.equal(shortName, CommonClassNames.JAVA_LANG_STRING_SHORT)) {
return false;
}
}
return CommonClassNames.JAVA_LANG_STRING.equals(type.getCanonicalText());
}
@Nullable
public static String getAutoBoxedType(@NonNull String primitive) {
if (TYPE_INT.equals(primitive)) {
return TYPE_INTEGER_WRAPPER;
} else if (TYPE_LONG.equals(primitive)) {
return TYPE_LONG_WRAPPER;
} else if (TYPE_CHAR.equals(primitive)) {
return TYPE_CHARACTER_WRAPPER;
} else if (TYPE_FLOAT.equals(primitive)) {
return TYPE_FLOAT_WRAPPER;
} else if (TYPE_DOUBLE.equals(primitive)) {
return TYPE_DOUBLE_WRAPPER;
} else if (TYPE_BOOLEAN.equals(primitive)) {
return TYPE_BOOLEAN_WRAPPER;
} else if (TYPE_SHORT.equals(primitive)) {
return TYPE_SHORT_WRAPPER;
} else if (TYPE_BYTE.equals(primitive)) {
return TYPE_BYTE_WRAPPER;
}
return null;
}
@Nullable
public static String getPrimitiveType(@NonNull String autoBoxedType) {
if (TYPE_INTEGER_WRAPPER.equals(autoBoxedType)) {
return TYPE_INT;
} else if (TYPE_LONG_WRAPPER.equals(autoBoxedType)) {
return TYPE_LONG;
} else if (TYPE_CHARACTER_WRAPPER.equals(autoBoxedType)) {
return TYPE_CHAR;
} else if (TYPE_FLOAT_WRAPPER.equals(autoBoxedType)) {
return TYPE_FLOAT;
} else if (TYPE_DOUBLE_WRAPPER.equals(autoBoxedType)) {
return TYPE_DOUBLE;
} else if (TYPE_BOOLEAN_WRAPPER.equals(autoBoxedType)) {
return TYPE_BOOLEAN;
} else if (TYPE_SHORT_WRAPPER.equals(autoBoxedType)) {
return TYPE_SHORT;
} else if (TYPE_BYTE_WRAPPER.equals(autoBoxedType)) {
return TYPE_BYTE;
}
return null;
}
}
@@ -21,13 +21,15 @@ import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.tools.klint.client.api.JavaParser;
import com.google.common.annotations.Beta;
import com.intellij.psi.PsiElement;
import java.io.File;
/**
* Location information for a warning
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -42,6 +44,13 @@ public class Location {
private Location mSecondary;
private Object mClientData;
/**
* Special marker location which means location not available, or not applicable, or filtered out, etc.
* For example, the infrastructure may return {@link #NONE} if you ask {@link JavaParser#getLocation(JavaContext, PsiElement)}
* for an element which is not in the current file during an incremental lint run in a single file.
*/
public static final Location NONE = new Location(new File("NONE"), null, null);
/**
* (Private constructor, use one of the factory methods
* {@link Location#create(File)},
@@ -355,7 +364,14 @@ public class Location {
index = findNextMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else {
assert direction == SearchDirection.NEAREST;
assert direction == SearchDirection.NEAREST ||
direction == SearchDirection.EOL_NEAREST;
int lineEnd = contents.indexOf('\n', offset);
if (lineEnd == -1) {
lineEnd = contents.length();
}
offset = lineEnd;
int before = findPreviousMatch(contents, offset, patternStart, hints);
int after = findNextMatch(contents, offset, patternStart, hints);
@@ -366,12 +382,27 @@ public class Location {
} else if (after == -1) {
index = before;
line = adjustLine(contents, line, offset, index);
} else if (offset - before < after - offset) {
index = before;
line = adjustLine(contents, line, offset, index);
} else {
index = after;
line = adjustLine(contents, line, offset, index);
int newLinesBefore = 0;
for (int i = before; i < offset; i++) {
if (contents.charAt(i) == '\n') {
newLinesBefore++;
}
}
int newLinesAfter = 0;
for (int i = offset; i < after; i++) {
if (contents.charAt(i) == '\n') {
newLinesAfter++;
}
}
if (newLinesBefore < newLinesAfter || newLinesBefore == newLinesAfter
&& offset - before < after - offset) {
index = before;
line = adjustLine(contents, line, offset, index);
} else {
index = after;
line = adjustLine(contents, line, offset, index);
}
}
}
@@ -674,6 +705,12 @@ public class Location {
* the match that is closest
*/
NEAREST,
/**
* Search both forwards and backwards from the end of the given line, and prefer
* the match that is closest
*/
EOL_NEAREST,
}
/**
@@ -20,7 +20,7 @@ import com.google.common.annotations.Beta;
/**
* Information about a position in a file/document.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
+185 -62
View File
@@ -16,21 +16,49 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_LIBRARY;
import static com.android.SdkConstants.ANDROID_LIBRARY_REFERENCE_FORMAT;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.APPCOMPAT_LIB_ARTIFACT;
import static com.android.SdkConstants.ATTR_MIN_SDK_VERSION;
import static com.android.SdkConstants.ATTR_PACKAGE;
import static com.android.SdkConstants.ATTR_TARGET_SDK_VERSION;
import static com.android.SdkConstants.FN_ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
import static com.android.SdkConstants.OLD_PROGUARD_FILE;
import static com.android.SdkConstants.PROGUARD_CONFIG;
import static com.android.SdkConstants.PROJECT_PROPERTIES;
import static com.android.SdkConstants.RES_FOLDER;
import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT;
import static com.android.SdkConstants.TAG_USES_SDK;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_API;
import static com.android.sdklib.SdkVersionInfo.LOWEST_ACTIVE_API;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.FilterData;
import com.android.build.OutputFile;
import com.android.builder.model.*;
import com.android.builder.model.AndroidArtifact;
import com.android.builder.model.AndroidArtifactOutput;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.ProductFlavor;
import com.android.builder.model.ProductFlavorContainer;
import com.android.builder.model.Variant;
import com.android.ide.common.repository.ResourceVisibilityLookup;
import com.android.resources.Density;
import com.android.resources.ResourceFolderType;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkVersionInfo;
import com.android.tools.klint.client.api.CircularDependencyException;
import com.android.tools.klint.client.api.Configuration;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.client.api.SdkInfo;
import com.google.common.annotations.Beta;
import com.google.common.base.CharMatcher;
@@ -40,6 +68,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -48,13 +77,16 @@ import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.SdkConstants.*;
import static com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_API;
/**
* A project contains information about an Android project being scanned for
* Lint errors.
@@ -93,9 +125,11 @@ public class Project {
protected List<File> mManifestFiles;
protected List<File> mJavaSourceFolders;
protected List<File> mJavaClassFolders;
protected List<File> mNonProvidedJavaLibraries;
protected List<File> mJavaLibraries;
protected List<File> mTestSourceFolders;
protected List<File> mResourceFolders;
protected List<File> mAssetFolders;
protected List<Project> mDirectLibraries;
protected List<Project> mAllLibraries;
protected boolean mReportIssues = true;
@@ -104,6 +138,7 @@ public class Project {
protected Boolean mAppCompat;
private Map<String, String> mSuperClassMap;
private ResourceVisibilityLookup mResourceVisibility;
private BuildToolInfo mBuildTools;
/**
* Creates a new {@link Project} for the given directory.
@@ -155,7 +190,7 @@ public class Project {
public AndroidProject getGradleProjectModel() {
return null;
}
/**
* Returns the project model for this project if it corresponds to
* a Gradle library. This is the case if both
@@ -292,6 +327,18 @@ public class Project {
} else {
mDirectLibraries = Collections.emptyList();
}
if (isAospBuildEnvironment()) {
if (isAospFrameworksRelatedProject(mDir)) {
// No manifest file for this project: just init the manifest values here
mManifestMinSdk = mManifestTargetSdk = new AndroidVersion(HIGHEST_KNOWN_API, null);
} else if (mBuildSdk == -1) {
// only set BuildSdk for projects other than frameworks and
// the ones that don't have one set in project.properties.
mBuildSdk = getClient().getHighestKnownApiLevel();
}
}
}
@Override
@@ -348,7 +395,7 @@ public class Project {
@NonNull
public List<File> getJavaSourceFolders() {
if (mJavaSourceFolders == null) {
if (isAospFrameworksProject(mDir)) {
if (isAospFrameworksRelatedProject(mDir)) {
return Collections.singletonList(new File(mDir, "java")); //$NON-NLS-1$
}
if (isAospBuildEnvironment()) {
@@ -373,9 +420,9 @@ public class Project {
public List<File> getJavaClassFolders() {
if (mJavaClassFolders == null) {
if (isAospFrameworksProject(mDir)) {
File top = mDir.getParentFile().getParentFile().getParentFile();
String top = getAospTop();
if (top != null) {
File out = new File(top, "out");
File out = new File(top, "out"); //$NON-NLS-1$
if (out.exists()) {
String relative =
"target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar";
@@ -406,20 +453,39 @@ public class Project {
* library projects which are processed in a separate pass with their own
* source and class folders.
*
* @param includeProvided If true, included provided libraries too (libraries
* that are not packaged with the app, but are provided
* for compilation purposes and are assumed to be present
* in the running environment)
* @return a list of .jar files (or class folders) that this project depends
* on.
*/
@NonNull
public List<File> getJavaLibraries() {
if (mJavaLibraries == null) {
// AOSP builds already merge libraries and class folders into
// the single classes.jar file, so these have already been processed
// in getJavaClassFolders.
mJavaLibraries = mClient.getJavaLibraries(this);
public List<File> getJavaLibraries(boolean includeProvided) {
if (includeProvided) {
if (mJavaLibraries == null) {
// AOSP builds already merge libraries and class folders into
// the single classes.jar file, so these have already been processed
// in getJavaClassFolders.
mJavaLibraries = mClient.getJavaLibraries(this, true);
if (isAospBuildEnvironment()) {
// We still need to add the support-annotations library in the case of AOSP
File out = new File(getAospTop(), "out");
String relative = "target/common/obj/JAVA_LIBRARIES/"
+ "android-support-annotations_intermediates/classes";
File annotationsDir = new File(out, relative.replace('/', File.separatorChar));
if (annotationsDir.exists()) {
mJavaLibraries.add(annotationsDir);
}
}
}
return mJavaLibraries;
} else {
if (mNonProvidedJavaLibraries == null) {
mNonProvidedJavaLibraries = mClient.getJavaLibraries(this, false);
}
return mNonProvidedJavaLibraries;
}
return mJavaLibraries;
}
/**
@@ -437,17 +503,17 @@ public class Project {
}
/**
* Returns the resource folder.
* Returns the resource folders.
*
* @return a file pointing to the resource folder, or null if the project
* does not contain any resources
* @return a list of files pointing to the resource folders, which might be empty if the project
* does not provide any resources.
*/
@NonNull
public List<File> getResourceFolders() {
if (mResourceFolders == null) {
List<File> folders = mClient.getResourceFolders(this);
if (folders.size() == 1 && isAospFrameworksProject(mDir)) {
if (folders.size() == 1 && isAospFrameworksRelatedProject(mDir)) {
// No manifest file for this project: just init the manifest values here
mManifestMinSdk = mManifestTargetSdk = new AndroidVersion(HIGHEST_KNOWN_API, null);
File folder = new File(folders.get(0), RES_FOLDER);
@@ -460,7 +526,21 @@ public class Project {
}
return mResourceFolders;
}
/**
* Returns the asset folders.
*
* @return a list of files pointing to the asset folders, which might be empty if the project
* does not provide any resources.
*/
@NonNull
public List<File> getAssetFolders() {
if (mAssetFolders == null) {
mAssetFolders = mClient.getAssetFolders(this);
}
return mAssetFolders;
}
/**
@@ -536,12 +616,13 @@ public class Project {
/**
* Gets the configuration associated with this project
*
* @param driver the current driver, if any
* @return the configuration associated with this project
*/
@NonNull
public Configuration getConfiguration() {
public Configuration getConfiguration(@Nullable LintDriver driver) {
if (mConfiguration == null) {
mConfiguration = mClient.getConfiguration(this);
mConfiguration = mClient.getConfiguration(this, driver);
}
return mConfiguration;
}
@@ -553,9 +634,6 @@ public class Project {
*/
@Nullable
public String getPackage() {
//assert !mLibrary; // Should call getPackage on the master project, not the library
// Assertion disabled because you might be running lint on a standalone library project.
return mPackage;
}
@@ -613,6 +691,20 @@ public class Project {
return mBuildSdk;
}
/**
* Returns the specific version of the build tools being used, if known
*
* @return the build tools version in use, or null if not known
*/
@Nullable
public BuildToolInfo getBuildTools() {
if (mBuildTools == null) {
mBuildTools = mClient.getBuildTools(this);
}
return mBuildTools;
}
/**
* Returns the target used to build the project, or null if not known
*
@@ -919,7 +1011,7 @@ public class Project {
private static Boolean sAospBuild;
/** Is lint running in an AOSP build environment */
private static boolean isAospBuildEnvironment() {
public static boolean isAospBuildEnvironment() {
if (sAospBuild == null) {
sAospBuild = getAospTop() != null;
}
@@ -928,29 +1020,51 @@ public class Project {
}
/**
* Is this the frameworks AOSP project? Needs some hardcoded support since
* Is this the frameworks or related AOSP project? Needs some hardcoded support since
* it doesn't have a manifest file, etc.
*
* A frameworks AOSP projects can be any directory under "frameworks" that
* 1. Is not the "support" directory (which uses the public support annotations)
* 2. Doesn't have an AndroidManifest.xml (it's an app instead)
*
* @param dir the project directory to check
* @return true if this looks like the frameworks/base/core project
* @return true if this looks like the frameworks/dir project and does not have
* an AndroidManifest.xml
*/
public static boolean isAospFrameworksRelatedProject(@NonNull File dir) {
if (isAospBuildEnvironment()) {
File frameworks = new File(getAospTop(), "frameworks"); //$NON-NLS-1$
String frameworksDir = frameworks.getAbsolutePath();
String supportDir = new File(frameworks, "support").getAbsolutePath(); //$NON-NLS-1$
if (dir.exists()
&& !dir.getAbsolutePath().startsWith(supportDir)
&& dir.getAbsolutePath().startsWith(frameworksDir)
&& !(new File(dir, FN_ANDROID_MANIFEST_XML).exists())) {
return true;
}
}
return false;
}
/**
* Is this the actual frameworks project.
* @param dir the project directory to check.
* @return true if this is the frameworks project.
*/
public static boolean isAospFrameworksProject(@NonNull File dir) {
if (!dir.getPath().endsWith("core")) { //$NON-NLS-1$
String top = getAospTop();
if (top != null) {
File toCompare = new File(top, "frameworks" //$NON-NLS-1$
+ File.separator + "base" //$NON-NLS-1$
+ File.separator + "core"); //$NON-NLS-1$
try {
return dir.getCanonicalFile().equals(toCompare) && dir.exists();
} catch (IOException e) {
return false;
}
} else {
return false;
}
File parent = dir.getParentFile();
if (parent == null || !parent.getName().equals("base")) { //$NON-NLS-1$
return false;
}
parent = parent.getParentFile();
//noinspection RedundantIfStatement
if (parent == null || !parent.getName().equals("frameworks")) { //$NON-NLS-1$
return false;
}
return true;
}
/** Get the root AOSP dir, if any */
@@ -1025,6 +1139,13 @@ public class Project {
// some Android.mk files do some complicated things with it - and most
// projects use the same module name as the directory name.
String moduleName = mDir.getName();
try {
// Get the actual directory name instead of '.' that's possible
// when using this via CLI.
moduleName = mDir.getCanonicalFile().getName();
} catch (IOException ioe) {
// pass
}
String top = getAospTop();
final String[] outFolders = new String[] {
@@ -1092,32 +1213,34 @@ public class Project {
/** In an AOSP build environment, identify the currently built image version, if available */
private static AndroidVersion findCurrentAospVersion() {
if (sCurrentVersion == null) {
File apiDir = new File(getAospTop(), "frameworks/base/api" //$NON-NLS-1$
File versionMk = new File(getAospTop(), "build/core/version_defaults.mk" //$NON-NLS-1$
.replace('/', File.separatorChar));
File[] apiFiles = apiDir.listFiles();
if (apiFiles == null) {
if (!versionMk.exists()) {
sCurrentVersion = AndroidVersion.DEFAULT;
return sCurrentVersion;
}
int max = 1;
for (File apiFile : apiFiles) {
String name = apiFile.getName();
int index = name.indexOf('.');
if (index > 0) {
String base = name.substring(0, index);
if (Character.isDigit(base.charAt(0))) {
int sdkVersion = LOWEST_ACTIVE_API;
try {
Pattern p = Pattern.compile("PLATFORM_SDK_VERSION\\s*:=\\s*(.*)");
List<String> lines = Files.readLines(versionMk, Charsets.UTF_8);
for (String line : lines) {
line = line.trim();
Matcher matcher = p.matcher(line);
if (matcher.matches()) {
String version = matcher.group(1);
try {
int version = Integer.parseInt(base);
if (version > max) {
max = version;
}
} catch (NumberFormatException nufe) {
sdkVersion = Integer.parseInt(version);
} catch (NumberFormatException nfe) {
// pass
}
break;
}
}
} catch (IOException io) {
// pass
}
sCurrentVersion = new AndroidVersion(max, null);
sCurrentVersion = new AndroidVersion(sdkVersion, null);
}
return sCurrentVersion;
@@ -1137,7 +1260,7 @@ public class Project {
public Boolean dependsOn(@NonNull String artifact) {
if (SUPPORT_LIB_ARTIFACT.equals(artifact)) {
if (mSupportLib == null) {
for (File file : getJavaLibraries()) {
for (File file : getJavaLibraries(true)) {
String name = file.getName();
if (name.equals("android-support-v4.jar") //$NON-NLS-1$
|| name.startsWith("support-v4-")) { //$NON-NLS-1$
@@ -1162,7 +1285,7 @@ public class Project {
return mSupportLib;
} else if (APPCOMPAT_LIB_ARTIFACT.equals(artifact)) {
if (mAppCompat == null) {
for (File file : getJavaLibraries()) {
for (File file : getJavaLibraries(true)) {
String name = file.getName();
if (name.startsWith("appcompat-v7-")) { //$NON-NLS-1$
mAppCompat = true;
@@ -28,7 +28,7 @@ import java.io.File;
* A {@link com.android.tools.lint.detector.api.Context} used when checking resource files
* (both bitmaps and XML files; for XML files a subclass of this context is used:
* {@link com.android.tools.lint.detector.api.XmlContext}.)
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -0,0 +1,690 @@
/*
* Copyright (C) 2016 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 com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_PKG;
import static com.android.SdkConstants.ANDROID_PKG_PREFIX;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.SdkConstants.CLASS_FRAGMENT;
import static com.android.SdkConstants.CLASS_RESOURCES;
import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
import static com.android.SdkConstants.R_CLASS;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.tools.klint.client.api.UastLintUtils.toAndroidReferenceViaResolve;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.AndroidReference;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.UastLintUtils;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiConditionalExpression;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionStatement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UIfExpression;
import org.jetbrains.uast.UParenthesizedExpression;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
/** Evaluates constant expressions */
public class ResourceEvaluator {
/**
* Marker ResourceType used to signify that an expression is of type {@code @ColorInt},
* which isn't actually a ResourceType but one we want to specifically compare with.
* We're using {@link ResourceType#PUBLIC} because that one won't appear in the R
* class (and ResourceType is an enum we can't just create new constants for.)
*/
public static final ResourceType COLOR_INT_MARKER_TYPE = ResourceType.PUBLIC;
/**
* Marker ResourceType used to signify that an expression is of type {@code @Px},
* which isn't actually a ResourceType but one we want to specifically compare with.
* We're using {@link ResourceType#DECLARE_STYLEABLE} because that one doesn't
* have a corresponding {@code *Res} constant (and ResourceType is an enum we can't
* just create new constants for.)
*/
public static final ResourceType PX_MARKER_TYPE = ResourceType.DECLARE_STYLEABLE;
public static final String COLOR_INT_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "ColorInt"; //$NON-NLS-1$
public static final String PX_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "Px"; //$NON-NLS-1$
public static final String RES_SUFFIX = "Res";
public static final String CLS_TYPED_ARRAY = "android.content.res.TypedArray";
private final JavaContext mContext;
private final JavaEvaluator mEvaluator;
private boolean mAllowDereference = true;
/**
* Creates a new resource evaluator
*
* @param context Java context
*/
public ResourceEvaluator(JavaContext context) {
mContext = context;
mEvaluator = context.getEvaluator();
}
/**
* Whether we allow dereferencing resources when computing constants;
* e.g. if we ask for the resource for {@code x} when the code is
* {@code x = getString(R.string.name)}, if {@code allowDereference} is
* true we'll return R.string.name, otherwise we'll return null.
*
* @return this for constructor chaining
*/
public ResourceEvaluator allowDereference(boolean allow) {
mAllowDereference = allow;
return this;
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource url (type and name)
*/
@Nullable
public static ResourceUrl getResource(
@NonNull JavaContext context,
@NonNull PsiElement element) {
return new ResourceEvaluator(context).getResource(element);
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource url (type and name)
*/
@Nullable
public static ResourceUrl getResource(
@NonNull JavaContext context,
@NonNull UElement element) {
return new ResourceEvaluator(context).getResource(element);
}
/**
* Evaluates the given node and returns the resource types implied by the given element,
* if any.
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource types
*/
@Nullable
public static EnumSet<ResourceType> getResourceTypes(
@NonNull JavaContext context,
@NonNull PsiElement element) {
return new ResourceEvaluator(context).getResourceTypes(element);
}
/**
* Evaluates the given node and returns the resource types implied by the given element,
* if any.
*
* @param context Java context
* @param element the node to compute the constant value for
* @return the corresponding resource types
*/
@Nullable
public static EnumSet<ResourceType> getResourceTypes(
@NonNull JavaContext context,
@NonNull UElement element) {
return new ResourceEvaluator(context).getResourceTypes(element);
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param element the node to compute the constant value for
* @return the corresponding constant value - a String, an Integer, a Float, and so on
*/
@Nullable
public ResourceUrl getResource(@Nullable UElement element) {
if (element == null) {
return null;
}
if (element instanceof UIfExpression) {
UIfExpression expression = (UIfExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResource(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResource(expression.getElseExpression());
}
} else if (element instanceof UParenthesizedExpression) {
UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) element;
return getResource(parenthesizedExpression.getExpression());
} else if (mAllowDereference && element instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression qualifiedExpression = (UQualifiedReferenceExpression) element;
UExpression selector = qualifiedExpression.getSelector();
if ((selector instanceof UCallExpression)) {
UCallExpression call = (UCallExpression) selector;
PsiMethod function = call.resolve();
PsiClass containingClass = UastUtils.getContainingClass(function);
if (function != null && containingClass != null) {
String qualifiedName = containingClass.getQualifiedName();
String name = call.getMethodName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
List<UExpression> args = call.getValueArguments();
if (!args.isEmpty()) {
return getResource(args.get(0));
}
}
}
}
}
if (element instanceof UReferenceExpression) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return url;
}
PsiElement resolved = ((UReferenceExpression) element).resolve();
if (resolved instanceof PsiVariable) {
PsiVariable variable = (PsiVariable) resolved;
UElement lastAssignment =
UastLintUtils.findLastAssignment(
variable, element, mContext);
if (lastAssignment != null) {
return getResource(lastAssignment);
}
return null;
}
}
return null;
}
/**
* Evaluates the given node and returns the resource reference (type and name) it
* points to, if any
*
* @param element the node to compute the constant value for
* @return the corresponding constant value - a String, an Integer, a Float, and so on
*/
@Nullable
public ResourceUrl getResource(@Nullable PsiElement element) {
if (element == null) {
return null;
}
if (element instanceof PsiConditionalExpression) {
PsiConditionalExpression expression = (PsiConditionalExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResource(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResource(expression.getElseExpression());
}
} else if (element instanceof PsiParenthesizedExpression) {
PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) element;
return getResource(parenthesizedExpression.getExpression());
} else if (element instanceof PsiMethodCallExpression && mAllowDereference) {
PsiMethodCallExpression call = (PsiMethodCallExpression) element;
PsiReferenceExpression expression = call.getMethodExpression();
PsiMethod method = call.resolveMethod();
if (method != null && method.getContainingClass() != null) {
String qualifiedName = method.getContainingClass().getQualifiedName();
String name = expression.getReferenceName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
PsiExpression[] args = call.getArgumentList().getExpressions();
if (args.length > 0) {
return getResource(args[0]);
}
}
}
} else if (element instanceof PsiReference) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return url;
}
PsiElement resolved = ((PsiReference) element).resolve();
if (resolved instanceof PsiField) {
url = getResourceConstant(resolved);
if (url != null) {
return url;
}
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
return getResource(field.getInitializer());
}
return null;
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
PsiDeclarationStatement prevStatement = (PsiDeclarationStatement) prev;
for (PsiElement e : prevStatement.getDeclaredElements()) {
if (variable.equals(e)) {
return getResource(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement) prev)
.getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign
= (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return getResource(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
}
}
return null;
}
/**
* Evaluates the given node and returns the resource types applicable to the
* node, if any.
*
* @param element the element to compute the types for
* @return the corresponding resource types
*/
@Nullable
public EnumSet<ResourceType> getResourceTypes(@Nullable UElement element) {
if (element == null) {
return null;
}
if (element instanceof UIfExpression) {
UIfExpression expression = (UIfExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResourceTypes(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResourceTypes(expression.getElseExpression());
} else {
EnumSet<ResourceType> left = getResourceTypes(
expression.getThenExpression());
EnumSet<ResourceType> right = getResourceTypes(
expression.getElseExpression());
if (left == null) {
return right;
} else if (right == null) {
return left;
} else {
EnumSet<ResourceType> copy = EnumSet.copyOf(left);
copy.addAll(right);
return copy;
}
}
} else if (element instanceof UParenthesizedExpression) {
UParenthesizedExpression parenthesizedExpression = (UParenthesizedExpression) element;
return getResourceTypes(parenthesizedExpression.getExpression());
} else if ((element instanceof UQualifiedReferenceExpression && mAllowDereference)
|| element instanceof UCallExpression) {
UElement probablyCallExpression = element;
if (element instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression qualifiedExpression =
(UQualifiedReferenceExpression) element;
probablyCallExpression = qualifiedExpression.getSelector();
}
if ((probablyCallExpression instanceof UCallExpression)) {
UCallExpression call = (UCallExpression) probablyCallExpression;
PsiMethod method = call.resolve();
PsiClass containingClass = UastUtils.getContainingClass(method);
if (method != null && containingClass != null) {
EnumSet<ResourceType> types = getTypesFromAnnotations(method);
if (types != null) {
return types;
}
String qualifiedName = containingClass.getQualifiedName();
String name = call.getMethodName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
List<UExpression> args = call.getValueArguments();
if (!args.isEmpty()) {
types = getResourceTypes(args.get(0));
if (types != null) {
return types;
}
}
}
}
}
}
if (element instanceof UReferenceExpression) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return EnumSet.of(url.type);
}
PsiElement resolved = ((UReferenceExpression) element).resolve();
if (resolved instanceof PsiVariable) {
PsiVariable variable = (PsiVariable) resolved;
UElement lastAssignment =
UastLintUtils.findLastAssignment(variable, element, mContext);
if (lastAssignment != null) {
return getResourceTypes(lastAssignment);
}
return null;
}
}
return null;
}
/**
* Evaluates the given node and returns the resource types applicable to the
* node, if any.
*
* @param element the element to compute the types for
* @return the corresponding resource types
*/
@Nullable
public EnumSet<ResourceType> getResourceTypes(@Nullable PsiElement element) {
if (element == null) {
return null;
}
if (element instanceof PsiConditionalExpression) {
PsiConditionalExpression expression = (PsiConditionalExpression) element;
Object known = ConstantEvaluator.evaluate(null, expression.getCondition());
if (known == Boolean.TRUE && expression.getThenExpression() != null) {
return getResourceTypes(expression.getThenExpression());
} else if (known == Boolean.FALSE && expression.getElseExpression() != null) {
return getResourceTypes(expression.getElseExpression());
} else {
EnumSet<ResourceType> left = getResourceTypes(
expression.getThenExpression());
EnumSet<ResourceType> right = getResourceTypes(
expression.getElseExpression());
if (left == null) {
return right;
} else if (right == null) {
return left;
} else {
EnumSet<ResourceType> copy = EnumSet.copyOf(left);
copy.addAll(right);
return copy;
}
}
} else if (element instanceof PsiParenthesizedExpression) {
PsiParenthesizedExpression parenthesizedExpression = (PsiParenthesizedExpression) element;
return getResourceTypes(parenthesizedExpression.getExpression());
} else if (element instanceof PsiMethodCallExpression && mAllowDereference) {
PsiMethodCallExpression call = (PsiMethodCallExpression) element;
PsiReferenceExpression expression = call.getMethodExpression();
PsiMethod method = call.resolveMethod();
if (method != null && method.getContainingClass() != null) {
EnumSet<ResourceType> types = getTypesFromAnnotations(method);
if (types != null) {
return types;
}
String qualifiedName = method.getContainingClass().getQualifiedName();
String name = expression.getReferenceName();
if ((CLASS_RESOURCES.equals(qualifiedName)
|| CLASS_CONTEXT.equals(qualifiedName)
|| CLASS_FRAGMENT.equals(qualifiedName)
|| CLASS_V4_FRAGMENT.equals(qualifiedName)
|| CLS_TYPED_ARRAY.equals(qualifiedName))
&& name != null
&& name.startsWith("get")) {
PsiExpression[] args = call.getArgumentList().getExpressions();
if (args.length > 0) {
types = getResourceTypes(args[0]);
if (types != null) {
return types;
}
}
}
}
} else if (element instanceof PsiReference) {
ResourceUrl url = getResourceConstant(element);
if (url != null) {
return EnumSet.of(url.type);
}
PsiElement resolved = ((PsiReference) element).resolve();
if (resolved instanceof PsiField) {
url = getResourceConstant(resolved);
if (url != null) {
return EnumSet.of(url.type);
}
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
return getResourceTypes(field.getInitializer());
}
return null;
} else if (resolved instanceof PsiParameter) {
return getTypesFromAnnotations((PsiParameter)resolved);
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
PsiDeclarationStatement prevStatement = (PsiDeclarationStatement) prev;
for (PsiElement e : prevStatement.getDeclaredElements()) {
if (variable.equals(e)) {
return getResourceTypes(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement) prev)
.getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign
= (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return getResourceTypes(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
}
}
return null;
}
@Nullable
private EnumSet<ResourceType> getTypesFromAnnotations(PsiModifierListOwner owner) {
if (mEvaluator == null) {
return null;
}
for (PsiAnnotation annotation : mEvaluator.getAllAnnotations(owner, true)) {
String signature = annotation.getQualifiedName();
if (signature == null) {
continue;
}
if (signature.equals(COLOR_INT_ANNOTATION)) {
return EnumSet.of(COLOR_INT_MARKER_TYPE);
}
if (signature.equals(PX_ANNOTATION)) {
return EnumSet.of(PX_MARKER_TYPE);
}
if (signature.endsWith(RES_SUFFIX)
&& signature.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
String typeString = signature
.substring(SUPPORT_ANNOTATIONS_PREFIX.length(),
signature.length() - RES_SUFFIX.length())
.toLowerCase(Locale.US);
ResourceType type = ResourceType.getEnum(typeString);
if (type != null) {
return EnumSet.of(type);
} else if (typeString.equals("any")) { // @AnyRes
return getAnyRes();
}
}
}
return null;
}
/** Returns a resource URL based on the field reference in the code */
@Nullable
public static ResourceUrl getResourceConstant(@NonNull PsiElement node) {
// R.type.name
if (node instanceof PsiReferenceExpression) {
PsiReferenceExpression expression = (PsiReferenceExpression) node;
if (expression.getQualifier() instanceof PsiReferenceExpression) {
PsiReferenceExpression select = (PsiReferenceExpression) expression.getQualifier();
if (select.getQualifier() instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) select
.getQualifier();
if (R_CLASS.equals(reference.getReferenceName())) {
String typeName = select.getReferenceName();
String name = expression.getReferenceName();
ResourceType type = ResourceType.getEnum(typeName);
if (type != null && name != null) {
boolean isFramework =
reference.getQualifier() instanceof PsiReferenceExpression
&& ANDROID_PKG
.equals(((PsiReferenceExpression) reference.
getQualifier()).getReferenceName());
return ResourceUrl.create(type, name, isFramework, false);
}
}
}
}
} else if (node instanceof PsiField) {
PsiField field = (PsiField) node;
PsiClass typeClass = field.getContainingClass();
if (typeClass != null) {
PsiClass rClass = typeClass.getContainingClass();
if (rClass != null && R_CLASS.equals(rClass.getName())) {
String name = field.getName();
ResourceType type = ResourceType.getEnum(typeClass.getName());
if (type != null && name != null) {
String qualifiedName = rClass.getQualifiedName();
boolean isFramework = qualifiedName != null
&& qualifiedName.startsWith(ANDROID_PKG_PREFIX);
return ResourceUrl.create(type, name, isFramework, false);
}
}
}
}
return null;
}
/** Returns a resource URL based on the field reference in the code */
@Nullable
public static ResourceUrl getResourceConstant(@NonNull UElement node) {
AndroidReference androidReference = toAndroidReferenceViaResolve(node);
if (androidReference == null) {
return null;
}
String name = androidReference.getName();
ResourceType type = androidReference.getType();
boolean isFramework = androidReference.getPackage().equals("android");
return ResourceUrl.create(type, name, isFramework, false);
}
private static EnumSet<ResourceType> getAnyRes() {
EnumSet<ResourceType> types = EnumSet.allOf(ResourceType.class);
types.remove(ResourceEvaluator.COLOR_INT_MARKER_TYPE);
types.remove(ResourceEvaluator.PX_MARKER_TYPE);
return types;
}
}
@@ -26,7 +26,7 @@ import java.io.File;
* Specialized detector intended for XML resources. Detectors that apply to XML
* resources should extend this detector instead since it provides special
* iteration hooks that are more efficient.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -16,17 +16,25 @@
package com.android.tools.klint.detector.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.DOT_PROPERTIES;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
import static com.android.SdkConstants.OLD_PROGUARD_FILE;
import static com.android.SdkConstants.RES_FOLDER;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
import org.jetbrains.uast.UastConverterUtils;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import static com.android.SdkConstants.*;
/**
* The scope of a detector is the set of files a detector must consider when
* performing its analysis. This can be used to determine when issues are
@@ -54,7 +62,7 @@ public enum Scope {
BINARY_RESOURCE_FILE,
/**
* The analysis considers the resource folders
* The analysis considers the resource folders (which also includes asset folders)
*/
RESOURCE_FOLDER,
@@ -72,14 +80,14 @@ public enum Scope {
* Issues which are only affected by a single Java source file can be
* checked for incrementally when a Java source file is edited.
*/
SOURCE_FILE,
JAVA_FILE,
/**
* The analysis considers <b>all</b> the Java source files together.
* <p>
* This flag is mutually exclusive with {@link #SOURCE_FILE}.
* This flag is mutually exclusive with {@link #JAVA_FILE}.
*/
ALL_SOURCE_FILES,
ALL_JAVA_FILES,
/**
* The analysis only considers a single Java class file at a time.
@@ -104,7 +112,8 @@ public enum Scope {
/**
* The analysis considers classes in the libraries for this project. These
* will be analyzed before the classes themselves.
* will be analyzed before the classes themselves. NOTE: This excludes
* provided libraries.
*/
JAVA_LIBRARIES,
@@ -135,10 +144,10 @@ public enum Scope {
if (size == 2) {
// When single checking a Java source file, we check both its Java source
// and the associated class files
return scopes.contains(SOURCE_FILE) && scopes.contains(CLASS_FILE);
return scopes.contains(JAVA_FILE) && scopes.contains(CLASS_FILE);
} else {
return size == 1 &&
(scopes.contains(SOURCE_FILE)
(scopes.contains(JAVA_FILE)
|| scopes.contains(CLASS_FILE)
|| scopes.contains(RESOURCE_FILE)
|| scopes.contains(PROGUARD_FILE)
@@ -183,8 +192,8 @@ public enum Scope {
scope.add(MANIFEST);
} else if (name.endsWith(DOT_XML)) {
scope.add(RESOURCE_FILE);
} else if (name.endsWith(DOT_JAVA)) {
scope.add(SOURCE_FILE);
} else if (name.endsWith(".kt")) {
scope.add(JAVA_FILE);
} else if (name.endsWith(DOT_CLASS)) {
scope.add(CLASS_FILE);
} else if (name.endsWith(DOT_GRADLE)) {
@@ -202,9 +211,6 @@ public enum Scope {
scope.add(RESOURCE_FILE);
scope.add(BINARY_RESOURCE_FILE);
scope.add(RESOURCE_FOLDER);
} else if (UastConverterUtils.isFileSupported(
project.getClient().getLanguagePlugins(), name)) {
scope.add(SOURCE_FILE);
}
}
} else {
@@ -226,7 +232,7 @@ public enum Scope {
/** Scope-set used for detectors which scan all resources */
public static final EnumSet<Scope> ALL_RESOURCES_SCOPE = EnumSet.of(ALL_RESOURCE_FILES);
/** Scope-set used for detectors which are affected by a single Java source file */
public static final EnumSet<Scope> SOURCE_FILE_SCOPE = EnumSet.of(SOURCE_FILE);
public static final EnumSet<Scope> JAVA_FILE_SCOPE = EnumSet.of(JAVA_FILE);
/** Scope-set used for detectors which are affected by a single Java class file */
public static final EnumSet<Scope> CLASS_FILE_SCOPE = EnumSet.of(CLASS_FILE);
/** Scope-set used for detectors which are affected by a single Gradle build file */
@@ -243,8 +249,8 @@ public enum Scope {
public static final EnumSet<Scope> MANIFEST_AND_RESOURCE_SCOPE =
EnumSet.of(Scope.MANIFEST, Scope.RESOURCE_FILE);
/** Scope-set used for detectors which are affected by single XML and Java source files */
public static final EnumSet<Scope> SOURCE_AND_RESOURCE_FILES =
EnumSet.of(RESOURCE_FILE, SOURCE_FILE);
public static final EnumSet<Scope> JAVA_AND_RESOURCE_FILES =
EnumSet.of(RESOURCE_FILE, JAVA_FILE);
/** Scope-set used for analyzing individual class files and all resource files */
public static final EnumSet<Scope> CLASS_AND_ALL_RESOURCE_FILES =
EnumSet.of(ALL_RESOURCE_FILES, CLASS_FILE);
@@ -22,7 +22,7 @@ import com.google.common.annotations.Beta;
/**
* Severity of an issue found by lint
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -27,7 +27,6 @@ import com.android.utils.XmlUtils;
* also be converted to plain text and to HTML markup, using the
* {@link #convertTo(String, TextFormat)} method.
*
* @see Issue#getDescription(TextFormat)
* @see Issue#getExplanation(TextFormat)
* @see Issue#getBriefDescription(TextFormat)
*/
@@ -56,7 +55,16 @@ public enum TextFormat {
/**
* HTML formatted output (note: does not include surrounding {@code <html></html>} tags)
*/
HTML;
HTML,
/**
* HTML formatted output (note: does not include surrounding {@code <html></html>} tags).
* This is like {@link #HTML}, but it does not escape unicode characters with entities.
* <p>
* (This is used for example in the IDE, where some partial HTML support in some
* label widgets support some HTML markup, but not numeric code character entities.)
*/
HTML_WITH_UNICODE;
/**
* Converts the given text to HTML
@@ -102,6 +110,7 @@ public enum TextFormat {
return message;
case TEXT:
case HTML:
case HTML_WITH_UNICODE:
return to.fromRaw(message);
}
}
@@ -111,6 +120,7 @@ public enum TextFormat {
case RAW:
return message;
case HTML:
case HTML_WITH_UNICODE:
return XmlUtils.toXmlTextValue(message);
}
}
@@ -118,6 +128,20 @@ public enum TextFormat {
switch (to) {
case HTML:
return message;
case HTML_WITH_UNICODE:
return removeNumericEntities(message);
case RAW:
case TEXT: {
return to.fromHtml(message);
}
}
}
case HTML_WITH_UNICODE: {
switch (to) {
case HTML:
case HTML_WITH_UNICODE:
return message;
case RAW:
case TEXT: {
return to.fromHtml(message);
@@ -137,51 +161,103 @@ public enum TextFormat {
// Drop all tags; replace all entities, insert newlines
// (this won't do wrapping)
StringBuilder sb = new StringBuilder(html.length());
boolean inPre = false;
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '<') {
// Scan forward to the end
if (html.startsWith("<br>", i) ||
html.startsWith("<br />", i) ||
html.startsWith("<BR>", i) ||
html.startsWith("<BR />", i)) {
sb.append('\n');
} else if (html.startsWith("<!--")) {
i = Math.max(i, html.indexOf("-->", i));
// Strip comments
if (html.startsWith("<!--", i)) {
int end = html.indexOf("-->", i);
if (end == -1) {
break; // Unclosed comment
} else {
i = end + 2;
}
continue;
}
// Tags: scan forward to the end
int begin;
boolean isEndTag = false;
if (html.startsWith("</", i)) {
begin = i + 2;
isEndTag = true;
} else {
begin = i + 1;
}
i = html.indexOf('>', i);
if (i == -1) {
// Unclosed tag
break;
}
int end = i;
if (html.charAt(i - 1) == '/') {
end--;
isEndTag = true;
}
// TODO: Handle <pre> such that we don't collapse spaces and reformat there!
// (We do need to strip out tags and expand entities)
String tag = html.substring(begin, end).trim();
if (tag.equalsIgnoreCase("br")) {
sb.append('\n');
} else if (tag.equalsIgnoreCase("p") // Most common block tags
|| tag.equalsIgnoreCase("div")
|| tag.equalsIgnoreCase("pre")
|| tag.equalsIgnoreCase("blockquote")
|| tag.equalsIgnoreCase("dl")
|| tag.equalsIgnoreCase("dd")
|| tag.equalsIgnoreCase("dt")
|| tag.equalsIgnoreCase("ol")
|| tag.equalsIgnoreCase("ul")
|| tag.equalsIgnoreCase("li")
|| tag.length() == 2 && tag.startsWith("h")
&& Character.isDigit(tag.charAt(1))) {
// Block tag: ensure new line
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '\n') {
sb.append('\n');
}
if (tag.equals("li") && !isEndTag) {
sb.append("* ");
}
if (tag.equalsIgnoreCase("pre")) {
inPre = !isEndTag;
}
}
} else if (c == '&') {
int end = html.indexOf(';', i);
if (end > i) {
String entity = html.substring(i, end + 1);
sb.append(XmlUtils.fromXmlAttributeValue(entity));
String s = XmlUtils.fromXmlAttributeValue(entity);
if (s.startsWith("&")) {
// Not an XML entity; for example, &nbsp;
// Sadly Guava's HtmlEscapes don't handle this either.
if (entity.equalsIgnoreCase("&nbsp;")) {
s = " ";
} else if (entity.startsWith("&#")) {
try {
int value = Integer.parseInt(entity.substring(2));
s = Character.toString((char)value);
} catch (NumberFormatException ignore) {
}
}
}
sb.append(s);
i = end;
} else {
sb.append(c);
}
} else if (c == '\n') {
sb.append(' ');
} else if (Character.isWhitespace(c)) {
if (inPre) {
sb.append(c);
} else if (sb.length() == 0
|| !Character.isWhitespace(sb.charAt(sb.length() - 1))) {
sb.append(' ');
}
} else {
sb.append(c);
}
}
// Collapse repeated spaces
String s = sb.toString();
sb.setLength(0);
boolean wasSpace = false;
for (int i = 0, n = s.length(); i < n; i++) {
char c = s.charAt(i);
if (c == '\t') { // we keep newlines; came from <br>'s
c = ' ';
}
boolean isSpace = c == ' ';
if (!isSpace || !wasSpace) {
wasSpace = isSpace;
sb.append(c);
}
}
s = sb.toString();
// Line-wrap
s = SdkUtils.wrap(s, 60, null);
@@ -194,16 +270,17 @@ public enum TextFormat {
/** Converts to this output format from the given raw-format text */
@NonNull
private String fromRaw(@NonNull String text) {
assert this == HTML || this == TEXT : this;
assert this == HTML || this == HTML_WITH_UNICODE || this == TEXT : this;
StringBuilder sb = new StringBuilder(3 * text.length() / 2);
boolean html = this == HTML;
boolean html = this == HTML || this == HTML_WITH_UNICODE;
boolean escapeUnicode = this == HTML;
char prev = 0;
int flushIndex = 0;
int n = text.length();
for (int i = 0; i < n; i++) {
char c = text.charAt(i);
if ((c == '*' || c == '`' && i < n - 1)) {
if ((c == '*' || c == '`') && i < n - 1) {
// Scout ahead for range end
if (!Character.isLetterOrDigit(prev)
&& !Character.isWhitespace(text.charAt(i + 1))) {
@@ -212,15 +289,15 @@ public enum TextFormat {
int end = text.indexOf(c, i + 1);
if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i);
appendEscapedText(sb, text, html, flushIndex, i, escapeUnicode);
}
if (html) {
String tag = c == '*' ? "b" : "code"; //$NON-NLS-1$ //$NON-NLS-2$
sb.append('<').append(tag).append('>');
appendEscapedText(sb, text, html, i + 1, end);
appendEscapedText(sb, text, html, i + 1, end, escapeUnicode);
sb.append('<').append('/').append(tag).append('>');
} else {
appendEscapedText(sb, text, html, i + 1, end);
appendEscapedText(sb, text, html, i + 1, end, escapeUnicode);
}
flushIndex = end + 1;
i = flushIndex - 1; // -1: account for the i++ in the loop
@@ -243,7 +320,7 @@ public enum TextFormat {
}
if (end > i + HTTP_PREFIX.length()) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i);
appendEscapedText(sb, text, html, flushIndex, i, escapeUnicode);
}
String url = text.substring(i, end);
@@ -261,14 +338,42 @@ public enum TextFormat {
}
if (flushIndex < n) {
appendEscapedText(sb, text, html, flushIndex, n);
appendEscapedText(sb, text, html, flushIndex, n, escapeUnicode);
}
return sb.toString();
}
private static String removeNumericEntities(@NonNull String html) {
if (!html.contains("&#")) {
return html;
}
StringBuilder sb = new StringBuilder(html.length());
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '&' && i < n - 1 && html.charAt(i + 1) == '#') {
int end = html.indexOf(';', i + 2);
if (end != -1) {
String decimal = html.substring(i + 2, end);
try {
c = (char)Integer.parseInt(decimal);
sb.append(c);
i = end;
continue;
} catch (NumberFormatException ignore) {
// fall through to not escape this
}
}
}
sb.append(c);
}
return sb.toString();
}
private static void appendEscapedText(@NonNull StringBuilder sb, @NonNull String text,
boolean html, int start, int end) {
boolean html, int start, int end, boolean escapeUnicode) {
if (html) {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
@@ -279,7 +384,7 @@ public enum TextFormat {
} else if (c == '\n') {
sb.append("<br/>\n");
} else {
if (c > 255) {
if (c > 255 && escapeUnicode) {
sb.append("&#"); //$NON-NLS-1$
sb.append(Integer.toString(c));
sb.append(';');
@@ -0,0 +1,417 @@
/*
* Copyright (C) 2015 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 com.android.tools.klint.detector.api;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.klint.client.api.JavaParser.TYPE_CHAR;
import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE;
import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import static com.android.tools.klint.detector.api.JavaContext.getParentOfType;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaParser.DefaultTypeDescriptor;
import com.android.tools.klint.client.api.JavaParser.ResolvedClass;
import com.android.tools.klint.client.api.JavaParser.ResolvedField;
import com.android.tools.klint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.klint.client.api.JavaParser.ResolvedNode;
import com.android.tools.klint.client.api.JavaParser.ResolvedVariable;
import com.android.tools.klint.client.api.JavaParser.TypeDescriptor;
import com.android.tools.klint.client.api.UastLintUtils;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionStatement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UVariable;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import java.util.ListIterator;
import lombok.ast.BinaryExpression;
import lombok.ast.BinaryOperator;
import lombok.ast.BooleanLiteral;
import lombok.ast.Cast;
import lombok.ast.CharLiteral;
import lombok.ast.Expression;
import lombok.ast.ExpressionStatement;
import lombok.ast.FloatingPointLiteral;
import lombok.ast.InlineIfExpression;
import lombok.ast.IntegralLiteral;
import lombok.ast.Literal;
import lombok.ast.Node;
import lombok.ast.NullLiteral;
import lombok.ast.Statement;
import lombok.ast.StringLiteral;
import lombok.ast.UnaryExpression;
import lombok.ast.VariableDeclaration;
import lombok.ast.VariableDefinition;
import lombok.ast.VariableDefinitionEntry;
import lombok.ast.VariableReference;
/**
* Evaluates the types of nodes. This goes deeper than
* {@link JavaContext#getType(Node)} in that it analyzes the
* flow and for example figures out that if you ask for the type of {@code var}
* in this code snippet:
* <pre>
* Object o = new StringBuilder();
* Object var = o;
* </pre>
* it will return "java.lang.StringBuilder".
* <p>
* <b>NOTE:</b> This type evaluator does not (yet) compute the correct
* types when involving implicit type conversions, so be careful
* if using this for primitives; e.g. for "int * long" it might return
* the type "int".
*/
public class TypeEvaluator {
private final JavaContext mContext;
/**
* Creates a new constant evaluator
*
* @param context the context to use to resolve field references, if any
*/
public TypeEvaluator(@Nullable JavaContext context) {
mContext = context;
}
/**
* Returns the inferred type of the given node
* @deprecated Use {@link #evaluate(PsiElement)} instead
*/
@Deprecated
@Nullable
public TypeDescriptor evaluate(@NonNull Node node) {
ResolvedNode resolved = null;
if (mContext != null) {
resolved = mContext.resolve(node);
}
if (resolved instanceof ResolvedMethod) {
TypeDescriptor type;
ResolvedMethod method = (ResolvedMethod) resolved;
if (method.isConstructor()) {
ResolvedClass containingClass = method.getContainingClass();
type = containingClass.getType();
} else {
type = method.getReturnType();
}
return type;
}
if (resolved instanceof ResolvedField) {
ResolvedField field = (ResolvedField) resolved;
Node astNode = field.findAstNode();
if (astNode instanceof VariableDeclaration) {
VariableDeclaration declaration = (VariableDeclaration)astNode;
VariableDefinition definition = declaration.astDefinition();
if (definition != null) {
VariableDefinitionEntry first = definition.astVariables().first();
if (first != null) {
Expression initializer = first.astInitializer();
if (initializer != null) {
TypeDescriptor type = evaluate(initializer);
if (type != null) {
return type;
}
}
}
}
}
return field.getType();
}
if (node instanceof VariableReference) {
Statement statement = getParentOfType(node, Statement.class, false);
if (statement != null) {
ListIterator<Node> iterator = statement.getParent().getChildren().listIterator();
while (iterator.hasNext()) {
if (iterator.next() == statement) {
if (iterator.hasPrevious()) { // should always be true
iterator.previous();
}
break;
}
}
String targetName = ((VariableReference) node).astIdentifier().astValue();
while (iterator.hasPrevious()) {
Node previous = iterator.previous();
if (previous instanceof VariableDeclaration) {
VariableDeclaration declaration = (VariableDeclaration) previous;
VariableDefinition definition = declaration.astDefinition();
for (VariableDefinitionEntry entry : definition.astVariables()) {
if (entry.astInitializer() != null && entry.astName().astValue()
.equals(targetName)) {
return evaluate(entry.astInitializer());
}
}
} else if (previous instanceof ExpressionStatement) {
ExpressionStatement expressionStatement = (ExpressionStatement) previous;
Expression expression = expressionStatement.astExpression();
if (expression instanceof BinaryExpression &&
((BinaryExpression) expression).astOperator()
== BinaryOperator.ASSIGN) {
BinaryExpression binaryExpression = (BinaryExpression) expression;
if (targetName.equals(binaryExpression.astLeft().toString())) {
return evaluate(binaryExpression.astRight());
}
}
}
}
}
} else if (node instanceof Cast) {
Cast cast = (Cast) node;
if (mContext != null) {
ResolvedNode typeReference = mContext.resolve(cast.astTypeReference());
if (typeReference instanceof ResolvedClass) {
return ((ResolvedClass) typeReference).getType();
}
}
TypeDescriptor viewType = evaluate(cast.astOperand());
if (viewType != null) {
return viewType;
}
} else if (node instanceof Literal) {
if (node instanceof NullLiteral) {
return null;
} else if (node instanceof BooleanLiteral) {
return new DefaultTypeDescriptor(TYPE_BOOLEAN);
} else if (node instanceof StringLiteral) {
return new DefaultTypeDescriptor(TYPE_STRING);
} else if (node instanceof CharLiteral) {
return new DefaultTypeDescriptor(TYPE_CHAR);
} else if (node instanceof IntegralLiteral) {
IntegralLiteral literal = (IntegralLiteral) node;
// Don't combine to ?: since that will promote astIntValue to a long
if (literal.astMarkedAsLong()) {
return new DefaultTypeDescriptor(TYPE_LONG);
} else {
return new DefaultTypeDescriptor(TYPE_INT);
}
} else if (node instanceof FloatingPointLiteral) {
FloatingPointLiteral literal = (FloatingPointLiteral) node;
// Don't combine to ?: since that will promote astFloatValue to a double
if (literal.astMarkedAsFloat()) {
return new DefaultTypeDescriptor(TYPE_FLOAT);
} else {
return new DefaultTypeDescriptor(TYPE_DOUBLE);
}
}
} else if (node instanceof UnaryExpression) {
return evaluate(((UnaryExpression) node).astOperand());
} else if (node instanceof InlineIfExpression) {
InlineIfExpression expression = (InlineIfExpression) node;
if (expression.astIfTrue() != null) {
return evaluate(expression.astIfTrue());
} else if (expression.astIfFalse() != null) {
return evaluate(expression.astIfFalse());
}
} else if (node instanceof BinaryExpression) {
BinaryExpression expression = (BinaryExpression) node;
BinaryOperator operator = expression.astOperator();
switch (operator) {
case LOGICAL_OR:
case LOGICAL_AND:
case EQUALS:
case NOT_EQUALS:
case GREATER:
case GREATER_OR_EQUAL:
case LESS:
case LESS_OR_EQUAL:
return new DefaultTypeDescriptor(TYPE_BOOLEAN);
}
TypeDescriptor type = evaluate(expression.astLeft());
if (type != null) {
return type;
}
return evaluate(expression.astRight());
}
if (resolved instanceof ResolvedVariable) {
ResolvedVariable variable = (ResolvedVariable) resolved;
return variable.getType();
}
return null;
}
/**
* Returns the inferred type of the given node
*/
@Nullable
public PsiType evaluate(@Nullable PsiElement node) {
if (node == null) {
return null;
}
PsiElement resolved = null;
if (node instanceof PsiReference) {
resolved = ((PsiReference) node).resolve();
}
if (resolved instanceof PsiMethod) {
PsiMethod method = (PsiMethod) resolved;
if (method.isConstructor()) {
PsiClass containingClass = method.getContainingClass();
if (containingClass != null && mContext != null) {
return mContext.getEvaluator().getClassType(containingClass);
}
} else {
return method.getReturnType();
}
}
if (resolved instanceof PsiField) {
PsiField field = (PsiField) resolved;
if (field.getInitializer() != null) {
PsiType type = evaluate(field.getInitializer());
if (type != null) {
return type;
}
}
return field.getType();
} else if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(node, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
for (PsiElement element : ((PsiDeclarationStatement)prev).getDeclaredElements()) {
if (variable.equals(element)) {
return evaluate(variable.getInitializer());
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement)prev).getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign = (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
return evaluate(assign.getRExpression());
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
return variable.getType();
} else if (node instanceof PsiExpression) {
PsiExpression expression = (PsiExpression) node;
return expression.getType();
}
return null;
}
@Nullable
public static PsiType evaluate(@NonNull JavaContext context, @Nullable UElement node) {
if (node == null) {
return null;
}
UElement resolved = node;
if (resolved instanceof UReferenceExpression) {
resolved = UastUtils.tryResolveUDeclaration(resolved, context.getUastContext());
}
if (resolved instanceof UMethod) {
return ((UMethod) resolved).getPsi().getReturnType();
} else if (resolved instanceof UVariable) {
UVariable variable = (UVariable) resolved;
UElement lastAssignment = UastLintUtils.findLastAssignment(variable, node, context);
if (lastAssignment != null) {
return evaluate(context, lastAssignment);
}
return variable.getType();
} else if (resolved instanceof UCallExpression) {
if (UastExpressionUtils.isMethodCall(resolved)) {
PsiMethod resolvedMethod = ((UCallExpression) resolved).resolve();
return resolvedMethod != null ? resolvedMethod.getReturnType() : null;
} else {
return ((UCallExpression) resolved).getExpressionType();
}
} else if (resolved instanceof UExpression) {
return ((UExpression) resolved).getExpressionType();
}
return null;
}
/**
* Evaluates the given node and returns the likely type of the instance. Convenience
* wrapper which creates a new {@linkplain TypeEvaluator}, evaluates the node and returns
* the result.
*
* @param context the context to use to resolve field references, if any
* @param node the node to compute the type for
* @return the corresponding type descriptor, if found
* @deprecated Use {@link #evaluate(JavaContext, PsiElement)} instead
*/
@Deprecated
@Nullable
public static TypeDescriptor evaluate(@NonNull JavaContext context, @NonNull Node node) {
return new TypeEvaluator(context).evaluate(node);
}
/**
* Evaluates the given node and returns the likely type of the instance. Convenience
* wrapper which creates a new {@linkplain TypeEvaluator}, evaluates the node and returns
* the result.
*
* @param context the context to use to resolve field references, if any
* @param node the node to compute the type for
* @return the corresponding type descriptor, if found
*/
@Nullable
public static PsiType evaluate(@NonNull JavaContext context, @NonNull PsiElement node) {
return new TypeEvaluator(context).evaluate(node);
}
}
@@ -22,6 +22,7 @@ import com.android.resources.ResourceFolderType;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.client.api.XmlParser;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@@ -30,7 +31,7 @@ import java.io.File;
/**
* A {@link Context} used when checking XML files.
* <p/>
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@@ -132,7 +133,7 @@ public class XmlContext extends ResourceContext {
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
@@ -153,7 +154,7 @@ public class XmlContext extends ResourceContext {
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
@@ -162,7 +163,7 @@ public class XmlContext extends ResourceContext {
@Override
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull Location location,
@NonNull String message) {
// Warn if clients use the non-scoped form? No, there are cases where an
// XML detector's error isn't applicable to one particular location (or it's
-1
View File
@@ -12,7 +12,6 @@
<orderEntry type="module" module-name="lint-idea" />
<orderEntry type="library" name="android-plugin" level="project" />
<orderEntry type="library" name="guava" level="project" />
<orderEntry type="module" module-name="uast-android" />
<orderEntry type="library" name="intellij-core" level="project" />
<orderEntry type="library" name="kotlin-runtime" level="project" />
</component>
@@ -22,25 +22,27 @@ import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
/**
* Ensures that addJavascriptInterface is not called for API levels below 17.
*/
public class AddJavascriptInterfaceDetector extends Detector implements UastScanner {
public class AddJavascriptInterfaceDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"AddJavascriptInterface", //$NON-NLS-1$
"addJavascriptInterface Called",
@@ -53,49 +55,40 @@ public class AddJavascriptInterfaceDetector extends Detector implements UastScan
Severity.WARNING,
new Implementation(
AddJavascriptInterfaceDetector.class,
Scope.SOURCE_FILE_SCOPE)).
Scope.JAVA_FILE_SCOPE)).
addMoreInfo(
"https://labs.mwrinfosecurity.com/blog/2013/09/24/webview-addjavascriptinterface-remote-code-execution/");
private static final String WEB_VIEW = "android.webkit.WebView"; //$NON-NLS-1$
private static final String ADD_JAVASCRIPT_INTERFACE = "addJavascriptInterface"; //$NON-NLS-1$
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE);
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
// Ignore the issue if we never build for any API less than 17.
if (context.getLintContext().getMainProject().getMinSdk() >= 17) {
if (context.getMainProject().getMinSdk() >= 17) {
return;
}
// Ignore if the method doesn't fit our description.
UFunction resolvedFunction = node.resolveOrEmpty(context);
UClass containingClass = UastUtils.getContainingClassOrEmpty(resolvedFunction);
if (!containingClass.isSubclassOf(WEB_VIEW)) {
return;
}
List<UVariable> valueParameters = resolvedFunction.getValueParameters();
if (node.getValueArgumentCount() != 2
|| !valueParameters.get(0).getType().matchesFqName(TYPE_OBJECT)
|| !valueParameters.get(1).getType().matchesFqName(TYPE_STRING)) {
JavaEvaluator evaluator = context.getEvaluator();
if (!evaluator.methodMatches(method, WEB_VIEW, true, TYPE_OBJECT, TYPE_STRING)) {
return;
}
String message = "`WebView.addJavascriptInterface` should not be called with minSdkVersion < 17 for security reasons: " +
"JavaScript can use reflection to manipulate application";
context.report(ISSUE, node, context.getLocation(node), message);
"JavaScript can use reflection to manipulate application";
UElement reportElement = call.getMethodIdentifier();
if (reportElement == null) {
reportElement = call;
}
context.reportUast(ISSUE, reportElement, context.getUastNameLocation(reportElement), message);
}
}
@@ -17,31 +17,33 @@
package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import java.io.File;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
/**
* Makes sure that alarms are handled correctly
*/
public class AlarmDetector extends Detector implements UastScanner {
public class AlarmDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
AlarmDetector.class,
Scope.SOURCE_FILE_SCOPE);
Scope.JAVA_FILE_SCOPE);
/** Alarm set too soon/frequently */
public static final Issue ISSUE = Issue.create(
@@ -65,49 +67,39 @@ public class AlarmDetector extends Detector implements UastScanner {
public AlarmDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Collections.singletonList("setRepeating");
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
UFunction setRepeatingFunction = node.resolve(context);
UClass containingClass = UastUtils.getContainingClassOrEmpty(setRepeatingFunction);
if (containingClass.matchesFqName("android.app.AlarmManager")
&& node.getValueArgumentCount() == 4) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.isMemberInClass(method, "android.app.AlarmManager") &&
evaluator.getParameterCount(method) == 4) {
ensureAtLeast(context, node, 1, 5000L);
ensureAtLeast(context, node, 2, 60000L);
}
}
private static void ensureAtLeast(@NonNull UastAndroidContext context,
private static void ensureAtLeast(@NonNull JavaContext context,
@NonNull UCallExpression node, int parameter, long min) {
UExpression arg = node.getValueArguments().get(parameter);
long value = getLongValue(arg);
UExpression argument = node.getValueArguments().get(parameter);
long value = getLongValue(context, argument);
if (value < min) {
String message = String.format("Value will be forced up to %d as of Android 5.1; "
String message = String.format("Value will be forced up to %1$d as of Android 5.1; "
+ "don't rely on this to be exact", min);
context.report(ISSUE, arg, context.getLocation(arg), message);
context.report(ISSUE, argument, context.getUastLocation(argument), message);
}
}
private static long getLongValue(@NonNull UExpression argument) {
Object value = argument.evaluate();
private static long getLongValue(
@NonNull JavaContext context,
@NonNull UExpression argument) {
Object value = ConstantEvaluator.evaluate(context, argument);
if (value instanceof Number) {
return ((Number)value).longValue();
}
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2015 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 com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class AllowAllHostnameVerifierDetector extends Detector implements Detector.UastScanner {
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION =
new Implementation(AllowAllHostnameVerifierDetector.class,
Scope.JAVA_FILE_SCOPE);
public static final Issue ISSUE = Issue.create("AllowAllHostnameVerifier",
"Insecure HostnameVerifier",
"This check looks for use of HostnameVerifier implementations " +
"whose `verify` method always returns true (thus trusting any hostname) " +
"which could result in insecure network traffic caused by trusting arbitrary " +
"hostnames in TLS/SSL certificates presented by peers.",
Category.SECURITY,
6,
Severity.WARNING,
IMPLEMENTATION);
// ---- Implements JavaScanner ----
@Override
@Nullable @SuppressWarnings("javadoc")
public List<String> getApplicableConstructorTypes() {
return Collections.singletonList("org.apache.http.conn.ssl.AllowAllHostnameVerifier");
}
@Override
public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod constructor) {
Location location = context.getUastLocation(node);
context.report(ISSUE, node, location,
"Using the AllowAllHostnameVerifier HostnameVerifier is unsafe " +
"because it always returns true, which could cause insecure network " +
"traffic due to trusting TLS/SSL server certificates for wrong " +
"hostnames");
}
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("setHostnameVerifier", "setDefaultHostnameVerifier");
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.methodMatches(method, null, false, "javax.net.ssl.HostnameVerifier")) {
UExpression argument = node.getValueArguments().get(0);
PsiElement resolvedArgument = UastUtils.tryResolve(argument);
if (resolvedArgument instanceof PsiField) {
PsiField field = (PsiField) resolvedArgument;
if ("ALLOW_ALL_HOSTNAME_VERIFIER".equals(field.getName())) {
Location location = context.getUastLocation(argument);
String message = "Using the ALLOW_ALL_HOSTNAME_VERIFIER HostnameVerifier "
+ "is unsafe because it always returns true, which could cause "
+ "insecure network traffic due to trusting TLS/SSL server "
+ "certificates for wrong hostnames";
context.report(ISSUE, argument, location, message);
}
}
}
}
}
@@ -21,9 +21,12 @@ import static com.android.SdkConstants.VALUE_ALWAYS;
import static com.android.SdkConstants.VALUE_IF_ROOM;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
@@ -31,21 +34,16 @@ import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UQualifiedExpression;
import org.jetbrains.uast.USimpleReferenceExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Attr;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -55,7 +53,8 @@ import java.util.List;
* MenuItem.SHOW_AS_ACTION_ALWAYS in code), which is usually a style guide violation.
* (Use ifRoom instead).
*/
public class AlwaysShowActionDetector extends ResourceXmlDetector implements UastScanner {
public class AlwaysShowActionDetector extends ResourceXmlDetector implements
Detector.UastScanner {
/** The main issue discovered by this detector */
@SuppressWarnings("unchecked")
@@ -81,7 +80,7 @@ public class AlwaysShowActionDetector extends ResourceXmlDetector implements Uas
Severity.WARNING,
new Implementation(
AlwaysShowActionDetector.class,
Scope.SOURCE_AND_RESOURCE_FILES,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/design/patterns/actionbar.html"); //$NON-NLS-1$
@@ -103,12 +102,6 @@ public class AlwaysShowActionDetector extends ResourceXmlDetector implements Uas
return folderType == ResourceFolderType.MENU;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_SHOW_AS_ACTION);
@@ -164,8 +157,10 @@ public class AlwaysShowActionDetector extends ResourceXmlDetector implements Uas
location.setSecondary(next);
}
}
context.report(ISSUE, location,
"Prefer \"`ifRoom`\" instead of \"`always`\"");
if (location != null) {
context.report(ISSUE, location,
"Prefer \"`ifRoom`\" instead of \"`always`\"");
}
}
}
}
@@ -198,45 +193,29 @@ public class AlwaysShowActionDetector extends ResourceXmlDetector implements Uas
// ---- Implements UastScanner ----
@Nullable
@Override
public UastVisitor createUastVisitor(UastAndroidContext context) {
return new FieldAccessChecker(context);
public List<String> getApplicableReferenceNames() {
return Arrays.asList("SHOW_AS_ACTION_IF_ROOM", "SHOW_AS_ACTION_ALWAYS");
}
private class FieldAccessChecker extends AbstractUastVisitor {
private final UastAndroidContext mContext;
public FieldAccessChecker(UastAndroidContext context) {
mContext = context;
}
@Override
public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) {
UElement selector = node.getSelector();
if (!(selector instanceof USimpleReferenceExpression)) {
return false;
}
String description = ((USimpleReferenceExpression)selector).getIdentifier();
boolean isIfRoom = description.equals("SHOW_AS_ACTION_IF_ROOM"); //$NON-NLS-1$
boolean isAlways = description.equals("SHOW_AS_ACTION_ALWAYS"); //$NON-NLS-1$
if ((isIfRoom || isAlways)
&& UastUtils.endsWithQualified(node.getReceiver(), "MenuItem")) { //$NON-NLS-1$
if (isAlways) {
JavaContext lintContext = mContext.getLintContext();
if (lintContext.getDriver().isSuppressed(lintContext, ISSUE, node)) {
return super.visitQualifiedExpression(node);
}
if (mAlwaysFields == null) {
mAlwaysFields = new ArrayList<Location>();
}
mAlwaysFields.add(mContext.getLocation(node));
} else {
mHasIfRoomRefs = true;
@Override
public void visitReference(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UReferenceExpression reference, @NonNull PsiElement resolved) {
if (resolved instanceof PsiField
&& JavaEvaluator.isMemberInClass((PsiField) resolved,
"android.view.MenuItem")) {
if ("SHOW_AS_ACTION_ALWAYS".equals(((PsiField) resolved).getName())) {
if (context.getDriver().isSuppressed(context, ISSUE, reference)) {
return;
}
if (mAlwaysFields == null) {
mAlwaysFields = new ArrayList<Location>();
}
mAlwaysFields.add(context.getUastLocation(reference));
} else {
mHasIfRoomRefs = true;
}
return super.visitQualifiedExpression(node);
}
}
}
@@ -0,0 +1,399 @@
/*
* Copyright (C) 2015 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 com.android.tools.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.TAG_INTENT_FILTER;
import static com.android.SdkConstants.TAG_SERVICE;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import static com.android.xml.AndroidManifest.NODE_ACTION;
import static com.android.xml.AndroidManifest.NODE_APPLICATION;
import static com.android.xml.AndroidManifest.NODE_METADATA;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.XmlScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiMethod;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
/**
* Detector for Android Auto issues.
* <p> Uses a {@code <meta-data>} tag with a {@code name="com.google.android.gms.car.application"}
* as a trigger for validating Automotive specific issues.
*/
public class AndroidAutoDetector extends ResourceXmlDetector
implements XmlScanner, Detector.UastScanner {
@SuppressWarnings("unchecked")
public static final Implementation IMPL = new Implementation(
AndroidAutoDetector.class,
EnumSet.of(Scope.RESOURCE_FILE, Scope.MANIFEST, Scope.JAVA_FILE),
Scope.RESOURCE_FILE_SCOPE);
/** Invalid attribute for uses tag.*/
public static final Issue INVALID_USES_TAG_ISSUE = Issue.create(
"InvalidUsesTagAttribute", //$NON-NLS-1$
"Invalid `name` attribute for `uses` element.",
"The <uses> element in `<automotiveApp>` should contain a " +
"valid value for the `name` attribute.\n" +
"Valid values are `media` or `notification`.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPL).addMoreInfo(
"https://developer.android.com/training/auto/start/index.html#auto-metadata");
/** Missing MediaBrowserService action */
public static final Issue MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE = Issue.create(
"MissingMediaBrowserServiceIntentFilter", //$NON-NLS-1$
"Missing intent-filter with action `android.media.browse.MediaBrowserService`.",
"An Automotive Media App requires an exported service that extends " +
"`android.service.media.MediaBrowserService` with an " +
"`intent-filter` for the action `android.media.browse.MediaBrowserService` " +
"to be able to browse and play media.\n" +
"To do this, add\n" +
"`<intent-filter>`\n" +
" `<action android:name=\"android.media.browse.MediaBrowserService\" />`\n" +
"`</intent-filter>`\n to the service that extends " +
"`android.service.media.MediaBrowserService`",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPL).addMoreInfo(
"https://developer.android.com/training/auto/audio/index.html#config_manifest");
/** Missing intent-filter for Media Search. */
public static final Issue MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH = Issue.create(
"MissingIntentFilterForMediaSearch", //$NON-NLS-1$
"Missing intent-filter with action `android.media.action.MEDIA_PLAY_FROM_SEARCH`",
"To support voice searches on Android Auto, you should also register an " +
"`intent-filter` for the action `android.media.action.MEDIA_PLAY_FROM_SEARCH`" +
".\nTo do this, add\n" +
"`<intent-filter>`\n" +
" `<action android:name=\"android.media.action.MEDIA_PLAY_FROM_SEARCH\" />`\n" +
"`</intent-filter>`\n" +
"to your `<activity>` or `<service>`.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPL).addMoreInfo(
"https://developer.android.com/training/auto/audio/index.html#support_voice");
/** Missing implementation of MediaSession.Callback#onPlayFromSearch*/
public static final Issue MISSING_ON_PLAY_FROM_SEARCH = Issue.create(
"MissingOnPlayFromSearch", //$NON-NLS-1$
"Missing `onPlayFromSearch`.",
"To support voice searches on Android Auto, in addition to adding an " +
"`intent-filter` for the action `onPlayFromSearch`," +
" you also need to override and implement " +
"`onPlayFromSearch(String query, Bundle bundle)`",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPL).addMoreInfo(
"https://developer.android.com/training/auto/audio/index.html#support_voice");
private static final String CAR_APPLICATION_METADATA_NAME =
"com.google.android.gms.car.application"; //$NON-NLS-1$
private static final String VAL_NAME_MEDIA = "media"; //$NON-NLS-1$
private static final String VAL_NAME_NOTIFICATION = "notification"; //$NON-NLS-1$
private static final String TAG_AUTOMOTIVE_APP = "automotiveApp"; //$NON-NLS-1$
private static final String ATTR_RESOURCE = "resource"; //$NON-NLS-1$
private static final String TAG_USES = "uses"; //$NON-NLS-1$
private static final String ACTION_MEDIA_BROWSER_SERVICE =
"android.media.browse.MediaBrowserService"; //$NON-NLS-1$
private static final String ACTION_MEDIA_PLAY_FROM_SEARCH =
"android.media.action.MEDIA_PLAY_FROM_SEARCH"; //$NON-NLS-1$
private static final String CLASS_MEDIA_SESSION_CALLBACK =
"android.media.session.MediaSession.Callback"; //$NON-NLS-1$
private static final String CLASS_V4MEDIA_SESSION_COMPAT_CALLBACK =
"android.support.v4.media.session.MediaSessionCompat.Callback"; //$NON-NLS-1$
private static final String METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH =
"onPlayFromSearch"; //$NON-NLS-1$
private static final String BUNDLE_ARG = "android.os.Bundle"; //$NON-NLS-1$
/**
* Indicates whether we identified that the current app is an automotive app and
* that we should validate all the automotive specific issues.
*/
private boolean mDoAutomotiveAppCheck;
/** Indicates that a {@link #ACTION_MEDIA_BROWSER_SERVICE} intent-filter action was found. */
private boolean mMediaIntentFilterFound;
/** Indicates that a {@link #ACTION_MEDIA_PLAY_FROM_SEARCH} intent-filter action was found. */
private boolean mMediaSearchIntentFilterFound;
/** The resource file name deduced by the meta-data resource value */
private String mAutomotiveResourceFileName;
/** Indicates whether this app is an automotive Media App. */
private boolean mIsAutomotiveMediaApp;
/** {@link Location.Handle} to the application element */
private Location.Handle mMainApplicationHandle;
/** Constructs a new {@link AndroidAutoDetector} check */
public AndroidAutoDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
// We only need to check the meta data resource file in res/xml if any.
return folderType == ResourceFolderType.XML;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_AUTOMOTIVE_APP, // Root element of a declared automotive descriptor.
NODE_METADATA, // meta-data from AndroidManifest.xml
TAG_SERVICE, // service from AndroidManifest.xml
TAG_INTENT_FILTER, // Any declared intent-filter from AndroidManifest.xml
NODE_APPLICATION // Used for storing the application element/location.
);
}
@Override
public void beforeCheckProject(@NonNull Context context) {
mIsAutomotiveMediaApp = false;
mAutomotiveResourceFileName = null;
mMediaIntentFilterFound = false;
mMediaSearchIntentFilterFound = false;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tagName = element.getTagName();
if (NODE_METADATA.equals(tagName) && !mDoAutomotiveAppCheck) {
checkAutoMetadataTag(element);
} else if (TAG_AUTOMOTIVE_APP.equals(tagName)) {
checkAutomotiveAppElement(context, element);
} else if (NODE_APPLICATION.equals(tagName)) {
// Disable reporting the error if the Issue was suppressed at
// the application level.
if (context.getMainProject() == context.getProject()
&& !context.getProject().isLibrary()) {
mMainApplicationHandle = context.createLocationHandle(element);
mMainApplicationHandle.setClientData(element);
}
} else if (TAG_SERVICE.equals(tagName)) {
checkServiceForBrowserServiceIntentFilter(element);
} else if (TAG_INTENT_FILTER.equals(tagName)) {
checkForMediaSearchIntentFilter(element);
}
}
private void checkAutoMetadataTag(Element element) {
String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (CAR_APPLICATION_METADATA_NAME.equals(name)) {
String autoFileName = element.getAttributeNS(ANDROID_URI, ATTR_RESOURCE);
if (autoFileName != null && autoFileName.startsWith("@xml/")) { //$NON-NLS-1$
// Store the fact that we need to check all the auto issues.
mDoAutomotiveAppCheck = true;
mAutomotiveResourceFileName =
autoFileName.substring("@xml/".length()) + DOT_XML; //$NON-NLS-1$
}
}
}
private void checkAutomotiveAppElement(XmlContext context, Element element) {
// Indicates whether the current file matches the resource that was registered
// in AndroidManifest.xml.
boolean isMetadataResource =
mAutomotiveResourceFileName != null
&& mAutomotiveResourceFileName.equals(context.file.getName());
for (Element child : LintUtils.getChildren(element)) {
if (TAG_USES.equals(child.getTagName())) {
String attrValue = child.getAttribute(ATTR_NAME);
if (VAL_NAME_MEDIA.equals(attrValue)) {
mIsAutomotiveMediaApp |= isMetadataResource;
} else if (!VAL_NAME_NOTIFICATION.equals(attrValue)
&& context.isEnabled(INVALID_USES_TAG_ISSUE)) {
// Error invalid value for attribute.
Attr node = child.getAttributeNode(ATTR_NAME);
if (node == null) {
// no name specified
continue;
}
context.report(INVALID_USES_TAG_ISSUE, node,
context.getLocation(node),
"Expecting one of `" + VAL_NAME_MEDIA + "` or `" +
VAL_NAME_NOTIFICATION + "` for the name " +
"attribute in " + TAG_USES + " tag.");
}
}
}
// Report any errors that we have collected that can be shown to the user
// once we determine that this is an Automotive Media App.
if (mIsAutomotiveMediaApp
&& !context.getProject().isLibrary()
&& mMainApplicationHandle != null
&& mDoAutomotiveAppCheck) {
Element node = (Element) mMainApplicationHandle.getClientData();
if (!mMediaIntentFilterFound
&& context.isEnabled(MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE)) {
context.report(MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE, node,
mMainApplicationHandle.resolve(),
"Missing `intent-filter` for action " +
"`android.media.browse.MediaBrowserService` that is required for " +
"android auto support");
}
if (!mMediaSearchIntentFilterFound
&& context.isEnabled(MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH)) {
context.report(MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH, node,
mMainApplicationHandle.resolve(),
"Missing `intent-filter` for action " +
"`android.media.action.MEDIA_PLAY_FROM_SEARCH`.");
}
}
}
private void checkServiceForBrowserServiceIntentFilter(Element element) {
if (TAG_SERVICE.equals(element.getTagName())
&& !mMediaIntentFilterFound) {
for (Element child : LintUtils.getChildren(element)) {
String tagName = child.getTagName();
if (TAG_INTENT_FILTER.equals(tagName)) {
for (Element filterChild : LintUtils.getChildren(child)) {
if (NODE_ACTION.equals(filterChild.getTagName())) {
String actionValue = filterChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (ACTION_MEDIA_BROWSER_SERVICE.equals(actionValue)) {
mMediaIntentFilterFound = true;
return;
}
}
}
}
}
}
}
private void checkForMediaSearchIntentFilter(Element element) {
if (!mMediaSearchIntentFilterFound) {
for (Element filterChild : LintUtils.getChildren(element)) {
if (NODE_ACTION.equals(filterChild.getTagName())) {
String actionValue = filterChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (ACTION_MEDIA_PLAY_FROM_SEARCH.equals(actionValue)) {
mMediaSearchIntentFilterFound = true;
break;
}
}
}
}
}
// Implementation of the UastScanner
@Override
@Nullable
public List<String> applicableSuperClasses() {
// We currently enable scanning only for media apps.
return mIsAutomotiveMediaApp ?
Arrays.asList(CLASS_MEDIA_SESSION_CALLBACK,
CLASS_V4MEDIA_SESSION_COMPAT_CALLBACK)
: null;
}
@Override
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
// Only check classes that are not declared abstract.
if (!context.getEvaluator().isAbstract(declaration)) {
MediaSessionCallbackVisitor visitor = new MediaSessionCallbackVisitor(context);
declaration.accept(visitor);
if (!visitor.isPlayFromSearchMethodFound()
&& context.isEnabled(MISSING_ON_PLAY_FROM_SEARCH)) {
context.reportUast(MISSING_ON_PLAY_FROM_SEARCH, declaration,
context.getUastNameLocation(declaration),
"This class does not override `" +
METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH + "` from `MediaSession.Callback`" +
" The method should be overridden and implemented to support " +
"Voice search on Android Auto.");
}
}
}
/**
* A Visitor class to search for {@code MediaSession.Callback#onPlayFromSearch(..)}
* method declaration.
*/
private static class MediaSessionCallbackVisitor extends JavaRecursiveElementVisitor {
private final JavaContext mContext;
private boolean mOnPlayFromSearchFound;
public MediaSessionCallbackVisitor(JavaContext context) {
this.mContext = context;
}
public boolean isPlayFromSearchMethodFound() {
return mOnPlayFromSearchFound;
}
@Override
public void visitMethod(PsiMethod method) {
super.visitMethod(method);
if (METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH.equals(method.getName())
&& mContext.getEvaluator().parametersMatch(method, TYPE_STRING,
BUNDLE_ARG)) {
mOnPlayFromSearchFound = true;
}
}
}
// Used by the IDE to show errors.
@SuppressWarnings("unused")
@NonNull
public static String[] getAllowedAutomotiveAppTypes() {
return new String[]{VAL_NAME_MEDIA, VAL_NAME_NOTIFICATION};
}
}
@@ -16,35 +16,115 @@
package com.android.tools.klint.checks;
import static com.android.SdkConstants.ATTR_VALUE;
import static com.android.SdkConstants.FQCN_SUPPRESS_LINT;
import static com.android.SdkConstants.SUPPRESS_LINT;
import static com.android.SdkConstants.INT_DEF_ANNOTATION;
import static com.android.SdkConstants.STRING_DEF_ANNOTATION;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.SdkConstants.TYPE_DEF_FLAG_ATTRIBUTE;
import static com.android.tools.klint.checks.PermissionRequirement.getAnnotationBooleanValue;
import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_ALL_OF;
import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_ANY_OF;
import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_FROM;
import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_MAX;
import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_MIN;
import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_MULTIPLE;
import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_TO;
import static com.android.tools.klint.checks.SupportAnnotationDetector.CHECK_RESULT_ANNOTATION;
import static com.android.tools.klint.checks.SupportAnnotationDetector.FLOAT_RANGE_ANNOTATION;
import static com.android.tools.klint.checks.SupportAnnotationDetector.INT_RANGE_ANNOTATION;
import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION;
import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_READ;
import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_WRITE;
import static com.android.tools.klint.checks.SupportAnnotationDetector.SIZE_ANNOTATION;
import static com.android.tools.klint.checks.SupportAnnotationDetector.filterRelevantAnnotations;
import static com.android.tools.klint.checks.SupportAnnotationDetector.getDoubleAttribute;
import static com.android.tools.klint.checks.SupportAnnotationDetector.getLongAttribute;
import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE;
import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import static com.android.tools.klint.detector.api.LintUtils.findSubstring;
import static com.android.tools.klint.detector.api.LintUtils.getAutoBoxedType;
import static com.android.tools.klint.detector.api.ResourceEvaluator.COLOR_INT_ANNOTATION;
import static com.android.tools.klint.detector.api.ResourceEvaluator.PX_ANNOTATION;
import static com.android.tools.klint.detector.api.ResourceEvaluator.RES_SUFFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.IssueRegistry;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.TextFormat;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiAnnotationOwner;
import com.intellij.psi.PsiArrayInitializerMemberValue;
import com.intellij.psi.PsiArrayType;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiCodeBlock;
import com.intellij.psi.PsiConditionalExpression;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionStatement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiNameValuePair;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiSwitchLabelStatement;
import com.intellij.psi.PsiSwitchStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeCastExpression;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.util.PsiTreeUtil;
import java.io.File;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
/**
* Checks annotations to make sure they are valid
*/
public class AnnotationDetector extends Detector implements UastScanner {
public class AnnotationDetector extends Detector implements JavaPsiScanner {
public static final Implementation IMPLEMENTATION = new Implementation(
AnnotationDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Placing SuppressLint on a local variable doesn't work for class-file based checks */
public static final Issue ISSUE = Issue.create(
public static final Issue INSIDE_METHOD = Issue.create(
"LocalSuppress", //$NON-NLS-1$
"@SuppressLint on invalid element",
@@ -59,105 +139,710 @@ public class AnnotationDetector extends Detector implements UastScanner {
Category.CORRECTNESS,
3,
Severity.ERROR,
new Implementation(
AnnotationDetector.class,
Scope.SOURCE_FILE_SCOPE));
IMPLEMENTATION);
/** Incorrectly using a support annotation */
@SuppressWarnings("WeakerAccess")
public static final Issue ANNOTATION_USAGE = Issue.create(
"SupportAnnotationUsage", //$NON-NLS-1$
"Incorrect support annotation usage",
"This lint check makes sure that the support annotations (such as " +
"`@IntDef` and `@ColorInt`) are used correctly. For example, it's an " +
"error to specify an `@IntRange` where the `from` value is higher than " +
"the `to` value.",
Category.CORRECTNESS,
2,
Severity.ERROR,
IMPLEMENTATION);
/** IntDef annotations should be unique */
public static final Issue UNIQUE = Issue.create(
"UniqueConstants", //$NON-NLS-1$
"Overlapping Enumeration Constants",
"The `@IntDef` annotation allows you to " +
"create a light-weight \"enum\" or type definition. However, it's possible to " +
"accidentally specify the same value for two or more of the values, which can " +
"lead to hard-to-detect bugs. This check looks for this scenario and flags any " +
"repeated constants.\n" +
"\n" +
"In some cases, the repeated constant is intentional (for example, renaming a " +
"constant to a more intuitive name, and leaving the old name in place for " +
"compatibility purposes.) In that case, simply suppress this check by adding a " +
"`@SuppressLint(\"UniqueConstants\")` annotation.",
Category.CORRECTNESS,
3,
Severity.ERROR,
IMPLEMENTATION);
/** Flags should typically be specified as bit shifts */
public static final Issue FLAG_STYLE = Issue.create(
"ShiftFlags", //$NON-NLS-1$
"Dangerous Flag Constant Declaration",
"When defining multiple constants for use in flags, the recommended style is " +
"to use the form `1 << 2`, `1 << 3`, `1 << 4` and so on to ensure that the " +
"constants are unique and non-overlapping.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION);
/** All IntDef constants should be included in switch */
public static final Issue SWITCH_TYPE_DEF = Issue.create(
"SwitchIntDef", //$NON-NLS-1$
"Missing @IntDef in Switch",
"This check warns if a `switch` statement does not explicitly include all " +
"the values declared by the typedef `@IntDef` declaration.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a new {@link AnnotationDetector} check */
public AnnotationDetector() {
}
// ---- Implements JavaScanner ----
/**
* Set of fields we've already warned about {@link #FLAG_STYLE} for; these can
* be referenced multiple times, so we should only flag them once
*/
private Set<PsiElement> mWarnedFlags;
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
List<Class<? extends PsiElement>> types = new ArrayList<Class<? extends PsiElement>>(2);
types.add(PsiAnnotation.class);
types.add(PsiSwitchStatement.class);
return types;
}
@NonNull
@Nullable
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Override
public UastVisitor createUastVisitor(UastAndroidContext context) {
public JavaElementVisitor createPsiVisitor(@NonNull JavaContext context) {
return new AnnotationChecker(context);
}
private static class AnnotationChecker extends AbstractUastVisitor {
private final UastAndroidContext mContext;
private class AnnotationChecker extends JavaElementVisitor {
private final JavaContext mContext;
public AnnotationChecker(UastAndroidContext context) {
public AnnotationChecker(JavaContext context) {
mContext = context;
}
@Override
public boolean visitAnnotation(@NotNull UAnnotation node) {
String type = node.getName();
if (SUPPRESS_LINT.equals(type) || FQCN_SUPPRESS_LINT.equals(type)) {
UElement parent = node.getParent();
if (parent instanceof UVariable) {
for (UNamedExpression element : node.getValueArguments()) {
UExpression valueNode = element.getExpression();
if (UastLiteralUtils.isStringLiteral(valueNode)) {
ULiteralExpression literal = (ULiteralExpression) valueNode;
String id = ((String)literal.getValue());
if (!checkId(node, id)) {
return super.visitAnnotation(node);
}
} else if (valueNode instanceof UCallExpression &&
((UCallExpression) valueNode).getKind() == UastCallKind.ARRAY_INITIALIZER) {
UCallExpression array = (UCallExpression) valueNode;
if (array.getValueArgumentCount() == 0) {
continue;
}
for (UExpression arrayElement : array.getValueArguments()) {
if (UastLiteralUtils.isStringLiteral(arrayElement)) {
String id = ((String)((ULiteralExpression)arrayElement).getValue());
if (!checkId(node, id)) {
return super.visitAnnotation(node);
public void visitAnnotation(PsiAnnotation annotation) {
String type = annotation.getQualifiedName();
if (type == null || type.startsWith("java.lang.")) {
return;
}
if (FQCN_SUPPRESS_LINT.equals(type)) {
PsiAnnotationOwner owner = annotation.getOwner();
if (owner == null) {
return;
}
if (owner instanceof PsiModifierList) {
PsiElement parent = ((PsiModifierList) owner).getParent();
// Only flag local variables and parameters (not classes, fields and methods)
if (!(parent instanceof PsiDeclarationStatement
|| parent instanceof PsiLocalVariable
|| parent instanceof PsiParameter)) {
return;
}
} else {
return;
}
PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes();
if (attributes.length == 1) {
PsiNameValuePair attribute = attributes[0];
PsiAnnotationMemberValue value = attribute.getValue();
if (value instanceof PsiLiteral) {
Object v = ((PsiLiteral) value).getValue();
if (v instanceof String) {
String id = (String) v;
checkSuppressLint(annotation, id);
}
} else if (value instanceof PsiArrayInitializerMemberValue) {
PsiArrayInitializerMemberValue initializer =
(PsiArrayInitializerMemberValue) value;
for (PsiAnnotationMemberValue expression : initializer.getInitializers()) {
if (expression instanceof PsiLiteral) {
Object v = ((PsiLiteral) expression).getValue();
if (v instanceof String) {
String id = (String) v;
if (!checkSuppressLint(annotation, id)) {
return;
}
}
}
}
}
}
}
} else if (type.startsWith(SUPPORT_ANNOTATIONS_PREFIX)) {
if (CHECK_RESULT_ANNOTATION.equals(type)) {
// Check that the return type of this method is not void!
if (annotation.getParent() instanceof PsiModifierList
&& annotation.getParent().getParent() instanceof PsiMethod) {
PsiMethod method = (PsiMethod) annotation.getParent().getParent();
if (!method.isConstructor()
&& PsiType.VOID.equals(method.getReturnType())) {
mContext.report(ANNOTATION_USAGE, annotation,
mContext.getLocation(annotation),
"@CheckResult should not be specified on `void` methods");
}
}
} else if (INT_RANGE_ANNOTATION.equals(type)
|| FLOAT_RANGE_ANNOTATION.equals(type)) {
// Check that the annotated element's type is int or long.
// Also make sure that from <= to.
boolean invalid;
if (INT_RANGE_ANNOTATION.equals(type)) {
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
return super.visitAnnotation(node);
long from = getLongAttribute(annotation, ATTR_FROM, Long.MIN_VALUE);
long to = getLongAttribute(annotation, ATTR_TO, Long.MAX_VALUE);
invalid = from > to;
} else {
checkTargetType(annotation, TYPE_FLOAT, TYPE_DOUBLE, true);
double from = getDoubleAttribute(annotation, ATTR_FROM,
Double.NEGATIVE_INFINITY);
double to = getDoubleAttribute(annotation, ATTR_TO,
Double.POSITIVE_INFINITY);
invalid = from > to;
}
if (invalid) {
mContext.report(ANNOTATION_USAGE, annotation, mContext.getLocation(annotation),
"Invalid range: the `from` attribute must be less than "
+ "the `to` attribute");
}
} else if (SIZE_ANNOTATION.equals(type)) {
// Check that the annotated element's type is an array, or a collection
// (or at least not an int or long; if so, suggest IntRange)
// Make sure the size and the modulo is not negative.
int unset = -42;
long exact = getLongAttribute(annotation, ATTR_VALUE, unset);
long min = getLongAttribute(annotation, ATTR_MIN, Long.MIN_VALUE);
long max = getLongAttribute(annotation, ATTR_MAX, Long.MAX_VALUE);
long multiple = getLongAttribute(annotation, ATTR_MULTIPLE, 1);
if (min > max) {
mContext.report(ANNOTATION_USAGE, annotation, mContext.getLocation(annotation),
"Invalid size range: the `min` attribute must be less than "
+ "the `max` attribute");
} else if (multiple < 1) {
mContext.report(ANNOTATION_USAGE, annotation, mContext.getLocation(annotation),
"The size multiple must be at least 1");
} else if (exact < 0 && exact != unset || min < 0 && min != Long.MIN_VALUE) {
mContext.report(ANNOTATION_USAGE, annotation, mContext.getLocation(annotation),
"The size can't be negative");
}
} else if (COLOR_INT_ANNOTATION.equals(type) || (PX_ANNOTATION.equals(type))) {
// Check that ColorInt applies to the right type
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
} else if (INT_DEF_ANNOTATION.equals(type)) {
// Make sure IntDef constants are unique
ensureUniqueValues(annotation);
} else if (PERMISSION_ANNOTATION.equals(type) ||
PERMISSION_ANNOTATION_READ.equals(type) ||
PERMISSION_ANNOTATION_WRITE.equals(type)) {
// Check that if there are no arguments, this is specified on a parameter,
// and conversely, on methods and fields there is a valid argument.
if (annotation.getParent() instanceof PsiModifierList
&& annotation.getParent().getParent() instanceof PsiMethod) {
String value = PermissionRequirement.getAnnotationStringValue(annotation, ATTR_VALUE);
String[] anyOf = PermissionRequirement.getAnnotationStringValues(annotation, ATTR_ANY_OF);
String[] allOf = PermissionRequirement.getAnnotationStringValues(annotation, ATTR_ALL_OF);
int set = 0;
//noinspection VariableNotUsedInsideIf
if (value != null) {
set++;
}
//noinspection VariableNotUsedInsideIf
if (allOf != null) {
set++;
}
//noinspection VariableNotUsedInsideIf
if (anyOf != null) {
set++;
}
if (set == 0) {
mContext.report(ANNOTATION_USAGE, annotation,
mContext.getLocation(annotation),
"For methods, permission annotation should specify one "
+ "of `value`, `anyOf` or `allOf`");
} else if (set > 1) {
mContext.report(ANNOTATION_USAGE, annotation,
mContext.getLocation(annotation),
"Only specify one of `value`, `anyOf` or `allOf`");
}
}
} else if (type.endsWith(RES_SUFFIX)) {
// Check that resource type annotations are on ints
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
}
} else {
// Look for typedefs (and make sure they're specified on the right type)
PsiJavaCodeReferenceElement referenceElement = annotation
.getNameReferenceElement();
if (referenceElement != null) {
PsiElement resolved = referenceElement.resolve();
if (resolved instanceof PsiClass) {
PsiClass cls = (PsiClass) resolved;
if (cls.isAnnotationType() && cls.getModifierList() != null) {
for (PsiAnnotation a : cls.getModifierList().getAnnotations()) {
String name = a.getQualifiedName();
if (INT_DEF_ANNOTATION.equals(name)) {
checkTargetType(annotation, TYPE_INT, TYPE_LONG, true);
} else if (STRING_DEF_ANNOTATION.equals(type)) {
checkTargetType(annotation, TYPE_STRING, null, true);
}
}
}
}
}
}
}
private boolean checkId(UAnnotation node, String id) {
IssueRegistry registry = mContext.getLintContext().getDriver().getRegistry();
private void checkTargetType(@NonNull PsiAnnotation node, @NonNull String type1,
@Nullable String type2, boolean allowCollection) {
PsiAnnotationOwner owner = node.getOwner();
if (owner instanceof PsiModifierList) {
PsiElement parent = ((PsiModifierList) owner).getParent();
PsiType type;
if (parent instanceof PsiDeclarationStatement) {
PsiElement[] elements = ((PsiDeclarationStatement) parent).getDeclaredElements();
if (elements.length > 0) {
PsiElement element = elements[0];
if (element instanceof PsiLocalVariable) {
type = ((PsiLocalVariable)element).getType();
} else {
return;
}
} else {
return;
}
} else if (parent instanceof PsiMethod) {
PsiMethod method = (PsiMethod) parent;
type = method.isConstructor()
? mContext.getEvaluator().getClassType(method.getContainingClass())
: method.getReturnType();
} else if (parent instanceof PsiVariable) {
// Field or local variable or parameter
type = ((PsiVariable)parent).getType();
} else {
return;
}
if (type == null) {
return;
}
if (allowCollection) {
if (type instanceof PsiArrayType) {
// For example, int[]
type = type.getDeepComponentType();
} else if (type instanceof PsiClassType) {
// For example, List<Integer>
PsiClassType classType = (PsiClassType)type;
if (classType.getParameters().length == 1) {
PsiClass resolved = classType.resolve();
if (resolved != null &&
mContext.getEvaluator().implementsInterface(resolved,
"java.util.Collection", false)) {
type = classType.getParameters()[0];
}
}
}
}
String typeName = type.getCanonicalText();
if (!typeName.equals(type1)
&& (type2 == null || !typeName.equals(type2))) {
// Autoboxing? You can put @DrawableRes on a java.lang.Integer for example
if (typeName.equals(getAutoBoxedType(type1))
|| type2 != null && typeName.equals(getAutoBoxedType(type2))) {
return;
}
String expectedTypes = type2 == null ? type1 : type1 + " or " + type2;
if (typeName.equals(TYPE_STRING)) {
typeName = "String";
}
String message = String.format(
"This annotation does not apply for type %1$s; expected %2$s",
typeName, expectedTypes);
Location location = mContext.getLocation(node);
mContext.report(ANNOTATION_USAGE, node, location, message);
}
}
}
@Override
public void visitSwitchStatement(PsiSwitchStatement statement) {
PsiExpression condition = statement.getExpression();
if (condition != null && PsiType.INT.equals(condition.getType())) {
PsiAnnotation annotation = findIntDef(condition);
if (annotation != null) {
checkSwitch(statement, annotation);
}
}
}
/**
* Searches for the corresponding @IntDef annotation definition associated
* with a given node
*/
@Nullable
private PsiAnnotation findIntDef(@NonNull PsiElement node) {
if (node instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReference) node).resolve();
if (resolved instanceof PsiModifierListOwner) {
PsiAnnotation[] annotations = mContext.getEvaluator().getAllAnnotations(
(PsiModifierListOwner)resolved, true);
PsiAnnotation annotation = SupportAnnotationDetector.findIntDef(
filterRelevantAnnotations(annotations));
if (annotation != null) {
return annotation;
}
}
if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable variable = (PsiLocalVariable) resolved;
PsiStatement statement = PsiTreeUtil.getParentOfType(node, PsiStatement.class,
false);
if (statement != null) {
PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
PsiStatement.class);
String targetName = variable.getName();
if (targetName == null) {
return null;
}
while (prev != null) {
if (prev instanceof PsiDeclarationStatement) {
for (PsiElement element : ((PsiDeclarationStatement) prev)
.getDeclaredElements()) {
if (variable.equals(element)) {
PsiExpression initializer = variable.getInitializer();
if (initializer != null) {
return findIntDef(initializer);
}
break;
}
}
} else if (prev instanceof PsiExpressionStatement) {
PsiExpression expression = ((PsiExpressionStatement) prev)
.getExpression();
if (expression instanceof PsiAssignmentExpression) {
PsiAssignmentExpression assign
= (PsiAssignmentExpression) expression;
PsiExpression lhs = assign.getLExpression();
if (lhs instanceof PsiReferenceExpression) {
PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
if (targetName.equals(reference.getReferenceName()) &&
reference.getQualifier() == null) {
PsiExpression rExpression = assign.getRExpression();
if (rExpression != null) {
return findIntDef(rExpression);
}
break;
}
}
}
}
prev = PsiTreeUtil.getPrevSiblingOfType(prev,
PsiStatement.class);
}
}
}
} else if (node instanceof PsiMethodCallExpression) {
PsiMethod method = ((PsiMethodCallExpression) node).resolveMethod();
if (method != null) {
PsiAnnotation[] annotations = mContext.getEvaluator().getAllAnnotations(method, true);
PsiAnnotation annotation = SupportAnnotationDetector.findIntDef(
filterRelevantAnnotations(annotations));
if (annotation != null) {
return annotation;
}
}
} else if (node instanceof PsiConditionalExpression) {
PsiConditionalExpression expression = (PsiConditionalExpression) node;
if (expression.getThenExpression() != null) {
PsiAnnotation result = findIntDef(expression.getThenExpression());
if (result != null) {
return result;
}
}
if (expression.getElseExpression() != null) {
PsiAnnotation result = findIntDef(expression.getElseExpression());
if (result != null) {
return result;
}
}
} else if (node instanceof PsiTypeCastExpression) {
PsiTypeCastExpression cast = (PsiTypeCastExpression) node;
if (cast.getOperand() != null) {
return findIntDef(cast.getOperand());
}
} else if (node instanceof PsiParenthesizedExpression) {
PsiParenthesizedExpression expression = (PsiParenthesizedExpression) node;
if (expression.getExpression() != null) {
return findIntDef(expression.getExpression());
}
}
return null;
}
private void checkSwitch(@NonNull PsiSwitchStatement node, @NonNull PsiAnnotation annotation) {
PsiCodeBlock block = node.getBody();
if (block == null) {
return;
}
PsiAnnotationMemberValue value = annotation.findDeclaredAttributeValue(ATTR_VALUE);
if (value == null) {
value = annotation.findDeclaredAttributeValue(null);
}
if (value == null) {
return;
}
if (!(value instanceof PsiArrayInitializerMemberValue)) {
return;
}
PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue)value;
PsiAnnotationMemberValue[] allowedValues = array.getInitializers();
List<PsiElement> fields = Lists.newArrayListWithCapacity(allowedValues.length);
for (PsiAnnotationMemberValue allowedValue : allowedValues) {
if (allowedValue instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReferenceExpression) allowedValue).resolve();
if (resolved != null) {
fields.add(resolved);
}
} else if (allowedValue instanceof PsiLiteral) {
fields.add(allowedValue);
}
}
// Empty switch: arguably we could skip these (since the IDE already warns about
// empty switches) but it's useful since the quickfix will kick in and offer all
// the missing ones when you're editing.
// if (block.getStatements().length == 0) { return; }
for (PsiStatement statement : block.getStatements()) {
if (statement instanceof PsiSwitchLabelStatement) {
PsiSwitchLabelStatement caseStatement = (PsiSwitchLabelStatement) statement;
PsiExpression expression = caseStatement.getCaseValue();
if (expression instanceof PsiLiteral) {
// Report warnings if you specify hardcoded constants.
// It's the wrong thing to do.
List<String> list = computeFieldNames(node, Arrays.asList(allowedValues));
// Keep error message in sync with {@link #getMissingCases}
String message = "Don't use a constant here; expected one of: " + Joiner
.on(", ").join(list);
mContext.report(SWITCH_TYPE_DEF, expression,
mContext.getLocation(expression), message);
return; // Don't look for other missing typedef constants since you might
// have aliased with value
} else if (expression instanceof PsiReferenceExpression) { // default case can have null expression
PsiElement resolved = ((PsiReferenceExpression) expression).resolve();
if (resolved == null) {
// If there are compilation issues (e.g. user is editing code) we
// can't be certain, so don't flag anything.
return;
}
if (resolved instanceof PsiField) {
// We can't just do
// fields.remove(resolved);
// since the fields list contains instances of potentially
// different types with different hash codes (due to the
// external annotations, which are not of the same type as
// for example the ECJ based ones.
//
// The equals method on external field class deliberately handles
// this (but it can't make its hash code match what
// the ECJ fields do, which is tied to the ECJ binding hash code.)
// So instead, manually check for equals. These lists tend to
// be very short anyway.
boolean found = false;
ListIterator<PsiElement> iterator = fields.listIterator();
while (iterator.hasNext()) {
PsiElement field = iterator.next();
if (field.isEquivalentTo(resolved)) {
iterator.remove();
found = true;
break;
}
}
if (!found) {
// Look for local alias
PsiExpression initializer = ((PsiField) resolved).getInitializer();
if (initializer instanceof PsiReferenceExpression) {
resolved = ((PsiReferenceExpression) expression).resolve();
if (resolved instanceof PsiField) {
iterator = fields.listIterator();
while (iterator.hasNext()) {
PsiElement field = iterator.next();
if (field.equals(initializer)) {
iterator.remove();
found = true;
break;
}
}
}
}
}
if (!found) {
List<String> list = computeFieldNames(node, Arrays.asList(allowedValues));
// Keep error message in sync with {@link #getMissingCases}
String message = "Unexpected constant; expected one of: " + Joiner
.on(", ").join(list);
Location location = mContext.getNameLocation(expression);
mContext.report(SWITCH_TYPE_DEF, expression, location, message);
}
}
}
}
}
if (!fields.isEmpty()) {
List<String> list = computeFieldNames(node, fields);
// Keep error message in sync with {@link #getMissingCases}
String message = "Switch statement on an `int` with known associated constant "
+ "missing case " + Joiner.on(", ").join(list);
Location location = mContext.getNameLocation(node);
mContext.report(SWITCH_TYPE_DEF, node, location, message);
}
}
private void ensureUniqueValues(@NonNull PsiAnnotation node) {
PsiAnnotationMemberValue value = node.findAttributeValue(ATTR_VALUE);
if (value == null) {
value = node.findAttributeValue(null);
}
if (value == null) {
return;
}
if (!(value instanceof PsiArrayInitializerMemberValue)) {
return;
}
PsiArrayInitializerMemberValue array = (PsiArrayInitializerMemberValue) value;
PsiAnnotationMemberValue[] initializers = array.getInitializers();
Map<Number,Integer> valueToIndex =
Maps.newHashMapWithExpectedSize(initializers.length);
boolean flag = getAnnotationBooleanValue(node, TYPE_DEF_FLAG_ATTRIBUTE) == Boolean.TRUE;
if (flag) {
ensureUsingFlagStyle(initializers);
}
ConstantEvaluator constantEvaluator = new ConstantEvaluator(mContext);
for (int index = 0; index < initializers.length; index++) {
PsiAnnotationMemberValue expression = initializers[index];
Object o = constantEvaluator.evaluate(expression);
if (o instanceof Number) {
Number number = (Number) o;
if (valueToIndex.containsKey(number)) {
@SuppressWarnings("UnnecessaryLocalVariable")
Number repeatedValue = number;
Location location;
String message;
int prevIndex = valueToIndex.get(number);
PsiElement prevConstant = initializers[prevIndex];
message = String.format(
"Constants `%1$s` and `%2$s` specify the same exact "
+ "value (%3$s); this is usually a cut & paste or "
+ "merge error",
expression.getText(), prevConstant.getText(),
repeatedValue.toString());
location = mContext.getLocation(expression);
Location secondary = mContext.getLocation(prevConstant);
secondary.setMessage("Previous same value");
location.setSecondary(secondary);
PsiElement scope = getAnnotationScope(node);
mContext.report(UNIQUE, scope, location, message);
break;
}
valueToIndex.put(number, index);
}
}
}
private void ensureUsingFlagStyle(@NonNull PsiAnnotationMemberValue[] constants) {
if (constants.length < 3) {
return;
}
for (PsiAnnotationMemberValue constant : constants) {
if (constant instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReferenceExpression) constant).resolve();
if (resolved instanceof PsiField) {
PsiExpression initializer = ((PsiField) resolved).getInitializer();
if (initializer instanceof PsiLiteral) {
PsiLiteral literal = (PsiLiteral) initializer;
Object o = literal.getValue();
if (!(o instanceof Number)) {
continue;
}
long value = ((Number)o).longValue();
// Allow -1, 0 and 1. You can write 1 as "1 << 0" but IntelliJ for
// example warns that that's a redundant shift.
if (Math.abs(value) <= 1) {
continue;
}
// Only warn if we're setting a specific bit
if (Long.bitCount(value) != 1) {
continue;
}
int shift = Long.numberOfTrailingZeros(value);
if (mWarnedFlags == null) {
mWarnedFlags = Sets.newHashSet();
}
if (!mWarnedFlags.add(resolved)) {
return;
}
String message = String.format(
"Consider declaring this constant using 1 << %1$d instead",
shift);
Location location = mContext.getLocation(initializer);
mContext.report(FLAG_STYLE, initializer, location, message);
}
}
}
}
}
private boolean checkSuppressLint(@NonNull PsiAnnotation node, @NonNull String id) {
IssueRegistry registry = mContext.getDriver().getRegistry();
Issue issue = registry.getIssue(id);
// Special-case the ApiDetector issue, since it does both source file analysis
// only on field references, and class file analysis on the rest, so we allow
// annotations outside of methods only on fields
if (issue != null && !issue.getImplementation().getScope().contains(Scope.SOURCE_FILE)
if (issue != null && !issue.getImplementation().getScope().contains(Scope.JAVA_FILE)
|| issue == ApiDetector.UNSUPPORTED) {
// Ensure that this isn't a field
UElement parent = node.getParent();
while (parent != null) {
if (parent instanceof UFunction || parent instanceof UBlockExpression) {
break;
} else if (issue == ApiDetector.UNSUPPORTED && parent instanceof UDeclarationsExpression) {
UDeclarationsExpression declarations = (UDeclarationsExpression) parent;
for (UVariable var : declarations.getVariables()) {
if (var.getKind() != UastVariableKind.MEMBER && var.getInitializer() instanceof UQualifiedExpression) {
return true;
}
}
}
parent = parent.getParent();
if (parent == null) {
return true;
}
}
// This issue doesn't have AST access: annotations are not
// available for local variables or parameters
mContext.report(ISSUE, node, mContext.getLocation(node), String.format(
PsiElement scope = getAnnotationScope(node);
mContext.report(INSIDE_METHOD, scope, mContext.getLocation(node), String.format(
"The `@SuppressLint` annotation cannot be used on a local " +
"variable with the lint check '%1$s': move out to the " +
"surrounding method", id));
@@ -167,4 +852,82 @@ public class AnnotationDetector extends Detector implements UastScanner {
return true;
}
}
@NonNull
private static List<String> computeFieldNames(@NonNull PsiSwitchStatement node,
Iterable<?> allowedValues) {
List<String> list = Lists.newArrayList();
for (Object o : allowedValues) {
if (o instanceof PsiReferenceExpression) {
PsiElement resolved = ((PsiReferenceExpression) o).resolve();
if (resolved != null) {
o = resolved;
}
} else if (o instanceof PsiLiteral) {
list.add("`" + ((PsiLiteral) o).getValue() + '`');
continue;
}
if (o instanceof PsiField) {
PsiField field = (PsiField) o;
// Only include class name if necessary
String name = field.getName();
PsiClass clz = PsiTreeUtil.getParentOfType(node, PsiClass.class, true);
if (clz != null) {
PsiClass containingClass = field.getContainingClass();
if (containingClass != null && !containingClass.equals(clz)) {
//if (Objects.equal(containingClass.getPackage(),
// ((ResolvedClass) resolved).getPackage())) {
// name = containingClass.getSimpleName() + '.' + field.getName();
//} else {
name = containingClass.getName() + '.' + field.getName();
//}
}
}
list.add('`' + name + '`');
}
}
Collections.sort(list);
return list;
}
/**
* Given an error message produced by this lint detector for the {@link #SWITCH_TYPE_DEF} issue
* type, returns the list of missing enum cases. <p> Intended for IDE quickfix implementations.
*
* @param errorMessage the error message associated with the error
* @param format the format of the error message
* @return the list of enum cases, or null if not recognized
*/
@Nullable
public static List<String> getMissingCases(@NonNull String errorMessage,
@NonNull TextFormat format) {
errorMessage = format.toText(errorMessage);
String substring = findSubstring(errorMessage, " missing case ", null);
if (substring == null) {
substring = findSubstring(errorMessage, "expected one of: ", null);
}
if (substring != null) {
return Splitter.on(",").trimResults().splitToList(substring);
}
return null;
}
/**
* Returns the node to use as the scope for the given annotation node.
* You can't annotate an annotation itself (with {@code @SuppressLint}), but
* you should be able to place an annotation next to it, as a sibling, to only
* suppress the error on this annotated element, not the whole surrounding class.
*/
@NonNull
private static PsiElement getAnnotationScope(@NonNull PsiAnnotation node) {
PsiElement scope = PsiTreeUtil.getParentOfType(node, PsiAnnotation.class, true);
if (scope == null) {
scope = node;
}
return scope;
}
}
@@ -17,11 +17,14 @@
package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -44,14 +47,25 @@ public class Api {
* @return a new ApiInfo
*/
public static Api parseApi(File apiFile) {
FileInputStream fileInputStream = null;
InputStream inputStream = null;
try {
fileInputStream = new FileInputStream(apiFile);
inputStream = new FileInputStream(apiFile);
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
SAXParser parser = parserFactory.newSAXParser();
ApiParser apiParser = new ApiParser();
parser.parse(fileInputStream, apiParser);
return new Api(apiParser.getClasses());
parser.parse(inputStream, apiParser);
inputStream.close();
// Also read in API (unless regenerating the map for newer libraries)
//noinspection PointlessBooleanExpression,TestOnlyProblems
if (!ApiLookup.DEBUG_FORCE_REGENERATE_BINARY) {
inputStream = Api.class.getResourceAsStream("api-versions-support-library.xml");
if (inputStream != null) {
parser.parse(inputStream, apiParser);
}
}
return new Api(apiParser.getClasses(), apiParser.getPackages());
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
@@ -59,9 +73,9 @@ public class Api {
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileInputStream != null) {
if (inputStream != null) {
try {
fileInputStream.close();
inputStream.close();
} catch (IOException e) {
// ignore
}
@@ -72,9 +86,13 @@ public class Api {
}
private final Map<String, ApiClass> mClasses;
private final Map<String, ApiPackage> mPackages;
private Api(Map<String, ApiClass> classes) {
private Api(
@NonNull Map<String, ApiClass> classes,
@NonNull Map<String, ApiPackage> packages) {
mClasses = new HashMap<String, ApiClass>(classes);
mPackages = new HashMap<String, ApiPackage>(packages);
}
ApiClass getClass(String fqcn) {
@@ -84,4 +102,8 @@ public class Api {
Map<String, ApiClass> getClasses() {
return Collections.unmodifiableMap(mClasses);
}
Map<String, ApiPackage> getPackages() {
return Collections.unmodifiableMap(mPackages);
}
}
@@ -18,6 +18,7 @@ package com.android.tools.klint.checks;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
@@ -36,20 +37,31 @@ import java.util.Set;
* {@link #getMethod} returns when the method was introduced.
* {@link #getField} returns when the field was introduced.
*/
public class ApiClass {
public class ApiClass implements Comparable<ApiClass> {
private final String mName;
private final int mSince;
private final int mDeprecatedIn;
private final List<Pair<String, Integer>> mSuperClasses = Lists.newArrayList();
private final List<Pair<String, Integer>> mInterfaces = Lists.newArrayList();
private final Map<String, Integer> mFields = new HashMap<String, Integer>();
private final Map<String, Integer> mMethods = new HashMap<String, Integer>();
private final Map<String, Integer> mDeprecatedMembersIn = new HashMap<String, Integer>();
ApiClass(String name, int since) {
// Persistence data: Used when writing out binary data in ApiLookup
List<String> members;
int index; // class number, e.g. entry in index where the pointer can be found
int indexOffset; // offset of the class entry
int memberOffsetBegin; // offset of the first member entry in the class
int memberOffsetEnd; // offset after the last member entry in the class
int memberIndexStart; // entry in index for first member
int memberIndexLength; // number of entries
ApiClass(String name, int since, int deprecatedIn) {
mName = name;
mSince = since;
mDeprecatedIn = deprecatedIn;
}
/**
@@ -69,11 +81,20 @@ public class ApiClass {
}
/**
* Returns when a field was added, or null if it doesn't exist.
* Returns the API level a method was deprecated in, or 0 if the method is not deprecated
*
* @return the API level a method was deprecated in, or 0 if the method is not deprecated
*/
int getDeprecatedIn() {
return mDeprecatedIn;
}
/**
* Returns when a field was added, or Integer.MAX_VALUE if it doesn't exist.
* @param name the name of the field.
* @param info the corresponding info
*/
Integer getField(String name, Api info) {
int getField(String name, Api info) {
// The field can come from this class or from a super class or an interface
// The value can never be lower than this introduction of this class.
// When looking at super classes and interfaces, it can never be lower than when the
@@ -98,11 +119,9 @@ public class ApiClass {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getField(name, info);
if (i != null) {
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
@@ -112,11 +131,9 @@ public class ApiClass {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getField(name, info);
if (i != null) {
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
@@ -125,8 +142,55 @@ public class ApiClass {
}
/**
* Returns when a method was added, or null if it doesn't exist. This goes through the super
* class to find method only present there.
* Returns when a field was deprecated, or 0 if it's not deprecated
*
* @param name the name of the field.
* @param info the corresponding info
*/
int getMemberDeprecatedIn(String name, Api info) {
int deprecatedIn = findMemberDeprecatedIn(name, info);
return deprecatedIn < Integer.MAX_VALUE ? deprecatedIn : 0;
}
private int findMemberDeprecatedIn(String name, Api info) {
// This follows the same logic as getField/getMethod.
// However, it also incorporates deprecation versions from the class.
int min = Integer.MAX_VALUE;
Integer i = mDeprecatedMembersIn.get(name);
if (i != null) {
min = i;
}
// now look at the super classes
for (Pair<String, Integer> superClassPair : mSuperClasses) {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.findMemberDeprecatedIn(name, info);
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
// now look at the interfaces
for (Pair<String, Integer> superClassPair : mInterfaces) {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.findMemberDeprecatedIn(name, info);
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
return min;
}
/**
* Returns when a method was added, or Integer.MAX_VALUE if it doesn't exist.
* This goes through the super class to find method only present there.
* @param methodSignature the method signature
*/
int getMethod(String methodSignature, Api info) {
@@ -159,11 +223,9 @@ public class ApiClass {
ApiClass superClass = info.getClass(superClassPair.getFirst());
if (superClass != null) {
i = superClass.getMethod(methodSignature, info);
if (i != null) {
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
@@ -173,11 +235,9 @@ public class ApiClass {
ApiClass superClass = info.getClass(interfacePair.getFirst());
if (superClass != null) {
i = superClass.getMethod(methodSignature, info);
if (i != null) {
int tmp = interfacePair.getSecond() > i ? interfacePair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
int tmp = interfacePair.getSecond() > i ? interfacePair.getSecond() : i;
if (tmp < min) {
min = tmp;
}
}
}
@@ -185,14 +245,16 @@ public class ApiClass {
return min;
}
void addField(String name, int since) {
void addField(String name, int since, int deprecatedIn) {
Integer i = mFields.get(name);
if (i == null || i.intValue() > since) {
mFields.put(name, Integer.valueOf(since));
assert i == null;
mFields.put(name, since);
if (deprecatedIn > 0) {
mDeprecatedMembersIn.put(name, deprecatedIn);
}
}
void addMethod(String name, int since) {
void addMethod(String name, int since, int deprecatedIn) {
// Strip off the method type at the end to ensure that the code which
// produces inherited methods doesn't get confused and end up multiple entries.
// For example, java/nio/Buffer has the method "array()Ljava/lang/Object;",
@@ -205,8 +267,10 @@ public class ApiClass {
}
Integer i = mMethods.get(name);
if (i == null || i.intValue() > since) {
mMethods.put(name, Integer.valueOf(since));
assert i == null || i == since : i;
mMethods.put(name, since);
if (deprecatedIn > 0) {
mDeprecatedMembersIn.put(name, deprecatedIn);
}
}
@@ -222,11 +286,12 @@ public class ApiClass {
// check if we already have that name (at a lower level)
for (Pair<String, Integer> pair : list) {
if (name.equals(pair.getFirst())) {
assert false;
return;
}
}
list.add(Pair.of(name, Integer.valueOf(value)));
list.add(Pair.of(name, value));
}
@@ -240,6 +305,16 @@ public class ApiClass {
return null;
}
@NonNull
public String getSimpleName() {
int index = mName.lastIndexOf('/');
if (index != -1) {
return mName.substring(index + 1);
}
return mName;
}
@Override
public String toString() {
return mName;
@@ -259,6 +334,14 @@ public class ApiClass {
return members;
}
List<Pair<String, Integer>> getInterfaces() {
return mInterfaces;
}
List<Pair<String, Integer>> getSuperClasses() {
return mSuperClasses;
}
private void addAllMethods(Api info, Set<String> set, boolean includeConstructors) {
if (!includeConstructors) {
for (String method : mMethods.keySet()) {
@@ -274,7 +357,6 @@ public class ApiClass {
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
clz.addAllMethods(info, set, false);
}
@@ -283,7 +365,6 @@ public class ApiClass {
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
clz.addAllMethods(info, set, false);
}
@@ -312,21 +393,22 @@ public class ApiClass {
for (Pair<String, Integer> superClass : mSuperClasses) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
clz.addAllFields(info, set);
}
clz.addAllFields(info, set);
}
// Get methods from implemented interfaces as well;
for (Pair<String, Integer> superClass : mInterfaces) {
ApiClass clz = info.getClass(superClass.getFirst());
assert clz != null : superClass.getSecond();
if (clz != null) {
clz.addAllFields(info, set);
}
clz.addAllFields(info, set);
}
}
@Override
public int compareTo(@NonNull ApiClass other) {
return mName.compareTo(other.mName);
}
/* This code can be used to scan through all the fields and look for fields
that have moved to a higher class:
Field android/view/MotionEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9
File diff suppressed because it is too large Load Diff
@@ -1,477 +0,0 @@
/*
* Copyright (C) 2012 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 com.android.tools.klint.checks
import com.android.SdkConstants.TARGET_API
import com.android.annotations.NonNull
import com.android.sdklib.SdkVersionInfo
import com.android.tools.klint.client.api.IssueRegistry
import com.android.tools.klint.detector.api.*
import org.jetbrains.uast.*
import org.jetbrains.uast.check.UastAndroidContext
import org.jetbrains.uast.check.UastScanner
import org.jetbrains.uast.java.JavaUVariable
import org.jetbrains.uast.visitor.AbstractUastVisitor
import org.jetbrains.uast.visitor.UastVisitor
import java.util.*
/**
* Looks for usages of APIs that are not supported in all the versions targeted
* by this application (according to its minimum API requirement in the manifest).
*/
open class ApiDetector : Detector(), UastScanner {
protected var mApiDatabase: ApiLookup? = null
private var mWarnedMissingDb: Boolean = false
@NonNull
override fun getSpeed(): Speed {
return Speed.SLOW
}
override fun beforeCheckProject(@NonNull context: Context) {
mApiDatabase = ApiLookup.get(context.client)
if (mApiDatabase == null && !mWarnedMissingDb) {
mWarnedMissingDb = true
context.report(IssueRegistry.LINT_ERROR, Location.create(context.file),
"Can't find API database; API check not performed")
}
}
override fun createUastVisitor(context: UastAndroidContext): UastVisitor {
return ApiVersionVisitor(context)
}
private inner class ApiVersionVisitor(val context: UastAndroidContext) : AbstractUastVisitor() {
private var mMinApi = -1
override fun visitCallExpression(node: UCallExpression): Boolean {
when (node.kind) {
UastCallKind.FUNCTION_CALL -> checkVersion(context, node, node.functionReference?.resolve(context))
UastCallKind.CONSTRUCTOR_CALL -> checkVersion(context, node, node.classReference?.resolve(context))
}
return super.visitCallExpression(node)
}
override fun visitBinaryExpressionWithType(node: UBinaryExpressionWithType): Boolean {
val typeRef = node.typeReference
if (typeRef != null) {
checkVersion(context, typeRef, node.type.resolve(context))
}
return super.visitBinaryExpressionWithType(node)
}
override fun visitSimpleReferenceExpression(node: USimpleReferenceExpression): Boolean {
checkVersion(context, node, node.resolve(context) as? UVariable)
return super.visitSimpleReferenceExpression(node)
}
override fun visitClass(node: UClass): Boolean {
val nameElement = node.nameElement
if (nameElement != null) {
for (type in node.superTypes) {
checkVersion(context, nameElement, type.resolveClass(context))
}
}
return super.visitClass(node)
}
override fun visitClassLiteralExpression(node: UClassLiteralExpression): Boolean {
val clazz = node.type?.resolve(context)
if (clazz != null) {
checkVersion(context, node, clazz)
}
return super.visitClassLiteralExpression(node)
}
override fun visitFunction(node: UFunction): Boolean {
if (!node.hasModifier(UastModifier.OVERRIDE)) {
val parentClass = node.parent as? UClass ?: return false
val db = mApiDatabase ?: return false
val desc = node.bytecodeDescriptor ?: return false
val buildSdk = context.lintContext.getMainProject().getBuildSdk()
if (buildSdk == -1) return false
for (type in parentClass.superTypes) {
val clazz = type.resolve(context) ?: continue
if (!isSdkClass(clazz)) continue
val internalName = clazz.internalName ?: continue
val methodSdkLevel = db.getCallVersion(internalName, node.name, desc)
if (methodSdkLevel != -1 && methodSdkLevel > buildSdk) {
val message = "This method is not overriding anything with the current build " +
"target, but will in API level $methodSdkLevel (current target is $buildSdk): `${node.name}`"
context.report(OVERRIDE, node, context.getLocation(node.nameElement), message)
}
}
}
return false
}
private fun checkIfSpecialCase(declaration: UDeclaration, owner: UClass): Boolean {
if (declaration is UVariable && owner.fqName == "android.os.Build.VERSION_CODES") return true
if (declaration.name == "MATCH_PARENT" && owner.fqName == "android.view.ViewGroup.LayoutParams") return true
if ((declaration.name == "CHOICE_MODE_NONE" || declaration.name == "CHOICE_MODE_MULTIPLE"
|| declaration.name == "CHOICE_MODE_SINGLE") && owner.fqName == "android.widget.AbsListView") return true
if ((declaration.name == "START" || declaration.name == "END") && owner.fqName == "android.view.Gravity") return true
return false
}
private fun checkVersion(context: UastAndroidContext, node: UElement, declaration: UDeclaration?) {
if (declaration == null) return
val db = mApiDatabase ?: return
val projectMinSdk = getMinSdk(context)
if (projectMinSdk == -1) return
fun check(declarationSdkLevel: Int, parentClass: UClass?) {
if (declarationSdkLevel == -1) return
if (parentClass != null && checkIfSpecialCase(declaration, parentClass)) return
if (isCheckedExplicitly(context, declarationSdkLevel, node)) return
if (projectMinSdk < declarationSdkLevel) {
val subject = when (declaration) {
is UFunction -> "Call"
is UVariable -> "Field"
is UClass -> "Class"
else -> "Call"
}
val message = "$subject requires API level $declarationSdkLevel" +
" (current min is $projectMinSdk): `${declaration.name}`"
context.report(UNSUPPORTED, node, context.getLocation(node), message)
return
}
}
fun checkAosp(clazz: UClass) = AOSP_BUILD && clazz.fqName?.startsWith("android.support.") ?: false
if (declaration is UClass) {
if (!isSdkClass(declaration) || checkAosp(declaration)) return
val internalName = declaration.internalName ?: return
check(db.getClassVersion(internalName), null)
}
val parentClass = declaration.parent as? UClass ?: return
if (!isSdkClass(parentClass) || checkAosp(parentClass)) return
var parentInternalName = parentClass.internalName
if (node is UCallExpression) {
val clazz = node.receiverType?.resolveClass(context)?.let { getSupertypeFromAndroidSdk(context, it) }
val internalName = clazz?.internalName
if (internalName != null) {
parentInternalName = internalName
}
}
if (parentInternalName == null) return
when (declaration) {
is UFunction -> {
val descriptor = declaration.bytecodeDescriptor ?: return
check(db.getCallVersion(parentInternalName, declaration.name, descriptor), parentClass)
}
is UVariable -> {
if (declaration.kind != UastVariableKind.MEMBER) return
if (declaration is JavaUVariable
&& declaration.hasModifier(UastModifier.IMMUTABLE)
&& declaration.hasModifier(UastModifier.STATIC)
&& declaration.visibility == UastVisibility.PUBLIC
&& declaration.initializer.evaluate() != null) {
val type = declaration.type
// Kotlin inlines Java field values with the primitive types, so we don't need to check its version.
if (type.isPrimitive || type.isString) {
return
}
}
check(db.getFieldVersion(parentInternalName, declaration.name), parentClass)
}
}
}
private fun isSdkClass(clazz: UClass): Boolean {
val fqName = clazz.fqName ?: return false
return fqName.startsWith("android.")
|| fqName.startsWith("java.")
|| fqName.startsWith("javax.")
|| fqName.startsWith("dalvik.")
}
private fun getMinSdk(context: UastAndroidContext): Int {
if (mMinApi == -1) {
val minSdkVersion = context.lintContext.mainProject.minSdkVersion
mMinApi = minSdkVersion.featureLevel
}
return mMinApi
}
private fun getSupertypeFromAndroidSdk(context: UastContext, clazz: UClass): UClass? {
tailrec fun getSuperclassFromAndroidSdk(clazz: UClass): UClass? {
if (clazz.fqName?.startsWith("android.") ?: false) return clazz
return getSuperclassFromAndroidSdk(clazz.getSuperClass(context) ?: return null)
}
val superClass = getSuperclassFromAndroidSdk(clazz)
if (superClass != null) return superClass
return clazz.superTypes.firstOrNull { it.fqName?.startsWith("android.") ?: false }?.resolve(context)
}
}
companion object {
private val AOSP_BUILD = System.getenv("ANDROID_BUILD_TOP") != null
private val SDK_INT_CONTAINING = "android.os.Build.VERSION"
private val SDK_INT = "SDK_INT"
tailrec fun getLocalMinSdk(scope: UElement?): Int {
if (scope == null) return -1
if (scope is UAnnotated) {
val targetApi = getTargetApi(scope.annotations)
if (targetApi != -1) return targetApi
}
return getLocalMinSdk(scope.parent)
}
fun getTargetApi(annotations: List<UAnnotation>): Int {
for (annotation in annotations) {
if (annotation.matchesName(TARGET_API)) {
for (element in annotation.valueArguments) {
val valueNode = element.expression
if (valueNode.isIntegralLiteral()) {
return (valueNode as ULiteralExpression).getLongValue().toInt()
} else if (valueNode.isStringLiteral()) {
val value = (valueNode as ULiteralExpression).value as String
return SdkVersionInfo.getApiByBuildCode(value, true)
} else if (valueNode is UQualifiedExpression) {
val codename = valueNode.getSelectorAsIdentifier() ?: return -1
return SdkVersionInfo.getApiByBuildCode(codename, true)
} else if (valueNode is USimpleReferenceExpression) {
val codename = valueNode.identifier
return SdkVersionInfo.getApiByBuildCode(codename, true)
}
}
}
}
return -1
}
fun isCheckedExplicitly(context: UastAndroidContext, requiredVersion: Int, node: UElement): Boolean {
tailrec fun UExpression.isSdkIntReference(): Boolean = when (this) {
is UParenthesizedExpression -> expression.isSdkIntReference()
is USimpleReferenceExpression -> resolve(context)?.matchesNameWithContaining(SDK_INT_CONTAINING, SDK_INT) ?: false
is UQualifiedExpression -> resolve(context)?.matchesNameWithContaining(SDK_INT_CONTAINING, SDK_INT) ?: false
else -> false
}
fun checkCondition(node: UExpression, invertCondition: Boolean = false): Boolean? {
if (node is UBinaryExpression && node.operator is UastBinaryOperator.ComparationOperator) {
var invert: Boolean = invertCondition
val value: UExpression
if (node.leftOperand.isSdkIntReference()) {
value = node.rightOperand
}
else if (node.rightOperand.isSdkIntReference()) {
invert = !invert
value = node.leftOperand
}
else return false
fun inv(cond: Boolean) = if (invert) !cond else cond
tailrec fun evaluateValue(value: UExpression?): Int? {
if (value == null) return null
return (value.evaluate() as? Number)?.toInt() ?: if (value is UResolvable) {
val declaration = value.resolve(context) ?: return null
if (declaration is UVariable)
evaluateValue(declaration.initializer)
else
null
} else null
}
val sdkLevel = evaluateValue(value) ?: return null
return when (node.operator) {
UastBinaryOperator.GREATER -> inv(sdkLevel > requiredVersion)
UastBinaryOperator.GREATER_OR_EQUAL -> sdkLevel == requiredVersion || inv(sdkLevel > requiredVersion)
UastBinaryOperator.LESS -> inv(sdkLevel < requiredVersion)
UastBinaryOperator.LESS_OR_EQUAL -> sdkLevel == requiredVersion || inv(sdkLevel < requiredVersion)
UastBinaryOperator.EQUALS -> requiredVersion == sdkLevel
else -> null
}
}
return when (node) {
is UBinaryExpression -> if (node.operator is UastBinaryOperator.LogicalOperator) {
checkCondition(node.leftOperand) ?: checkCondition(node.rightOperand)
} else null
is UUnaryExpression -> if (node.operator == UastPrefixOperator.LOGICAL_NOT) {
checkCondition(node.operand, true)
} else null
is UParenthesizedExpression -> checkCondition(node.expression)
else -> null
}
}
tailrec fun check(node: UElement?, prev: UElement?, context: UastAndroidContext): Boolean {
return when (node) {
null -> false
is UIfExpression -> {
val cond = checkCondition(node.condition) ?: return false
if ((cond && prev == node.thenBranch) || (!cond && prev == node.elseBranch))
true
else
check(node.parent, node, context)
}
is USwitchClauseExpression -> {
if (node.caseValues?.any { checkCondition(it) == true } ?: false)
true
else
check(node.parent, node, context)
}
is UBinaryExpression -> {
if (prev == node.rightOperand
&& node.operator is UastBinaryOperator.LogicalOperator
&& checkCondition(node.leftOperand) == true)
true
else
check(node.parent, node, context)
}
is UFunction -> false
is UVariable -> if (node.kind == UastVariableKind.MEMBER) false else check(node.parent, node, context)
is UClass -> false
else -> check(node.parent, node, context)
}
}
val minSdk = getLocalMinSdk(node)
if (minSdk != -1 && minSdk >= requiredVersion) return true
return check(node, null, context)
}
/** Accessing an unsupported API */
@SuppressWarnings("unchecked")
@JvmField
val UNSUPPORTED = Issue.create(
"NewApi", //$NON-NLS-1$
"Calling new methods on older versions",
"This check scans through all the Android API calls in the application and " +
"warns about any calls that are not available on *all* versions targeted " +
"by this application (according to its minimum SDK attribute in the manifest).\n" +
"\n" +
"If you really want to use this API and don't need to support older devices just " +
"set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files.\n" +
"\n" +
"If your code is *deliberately* accessing newer APIs, and you have ensured " +
"(e.g. with conditional execution) that this code will only ever be called on a " +
"supported platform, then you can annotate your class or method with the " +
"`@TargetApi` annotation specifying the local minimum SDK to apply, such as " +
"`@TargetApi(11)`, such that this check considers 11 rather than your manifest " +
"file's minimum SDK as the required API level.\n" +
"\n" +
"If you are deliberately setting `android:` attributes in style definitions, " +
"make sure you place this in a `values-vNN` folder in order to avoid running " +
"into runtime conflicts on certain devices where manufacturers have added " +
"custom attributes whose ids conflict with the new ones on later platforms.\n" +
"\n" +
"Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that " +
"the element will only be inflated in an adequate context.",
Category.CORRECTNESS,
6,
Severity.ERROR,
Implementation(
ApiDetector::class.java,
Scope.SOURCE_FILE_SCOPE))
/** Accessing an inlined API on older platforms */
@JvmField
val INLINED = Issue.create(
"InlinedApi", //$NON-NLS-1$
"Using inlined constants on older versions",
"This check scans through all the Android API field references in the application " +
"and flags certain constants, such as static final integers and Strings, " +
"which were introduced in later versions. These will actually be copied " +
"into the class files rather than being referenced, which means that " +
"the value is available even when running on older devices. In some " +
"cases that's fine, and in other cases it can result in a runtime " +
"crash or incorrect behavior. It depends on the context, so consider " +
"the code carefully and device whether it's safe and can be suppressed " +
"or whether the code needs tbe guarded.\n" +
"\n" +
"If you really want to use this API and don't need to support older devices just " +
"set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files." +
"\n" +
"If your code is *deliberately* accessing newer APIs, and you have ensured " +
"(e.g. with conditional execution) that this code will only ever be called on a " +
"supported platform, then you can annotate your class or method with the " +
"`@TargetApi` annotation specifying the local minimum SDK to apply, such as " +
"`@TargetApi(11)`, such that this check considers 11 rather than your manifest " +
"file's minimum SDK as the required API level.\n",
Category.CORRECTNESS,
6,
Severity.WARNING,
Implementation(
ApiDetector::class.java,
Scope.SOURCE_FILE_SCOPE))
/** Accessing an unsupported API */
@JvmField
val OVERRIDE = Issue.create(
"Override", //$NON-NLS-1$
"Method conflicts with new inherited method",
"Suppose you are building against Android API 8, and you've subclassed Activity. " +
"In your subclass you add a new method called `isDestroyed`(). At some later point, " +
"a method of the same name and signature is added to Android. Your method will " +
"now override the Android method, and possibly break its contract. Your method " +
"is not calling `super.isDestroyed()`, since your compilation target doesn't " +
"know about the method.\n" +
"\n" +
"The above scenario is what this lint detector looks for. The above example is " +
"real, since `isDestroyed()` was added in API 17, but it will be true for *any* " +
"method you have added to a subclass of an Android class where your build target " +
"is lower than the version the method was introduced in.\n" +
"\n" +
"To fix this, either rename your method, or if you are really trying to augment " +
"the builtin method if available, switch to a higher build target where you can " +
"deliberately add `@Override` on your overriding method, and call `super` if " +
"appropriate etc.\n",
Category.CORRECTNESS,
6,
Severity.ERROR,
Implementation(
ApiDetector::class.java,
Scope.SOURCE_FILE_SCOPE))
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2015 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 com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.google.common.collect.Lists;
import java.util.List;
/**
* Represents a package and its classes
*/
public class ApiPackage implements Comparable<ApiPackage> {
private final String mName;
private final List<ApiClass> mClasses = Lists.newArrayListWithExpectedSize(100);
// Persistence data: Used when writing out binary data in ApiLookup
int indexOffset; // offset of the package entry
ApiPackage(@NonNull String name) {
mName = name;
}
/**
* Returns the name of the class (fully qualified name)
* @return the name of the class
*/
@NonNull
public String getName() {
return mName;
}
/**
* Returns the classes in this package
* @return the classes in this package
*/
@NonNull
public List<ApiClass> getClasses() {
return mClasses;
}
void addClass(@NonNull ApiClass clz) {
mClasses.add(clz);
}
@Override
public int compareTo(@NonNull ApiPackage other) {
return mName.compareTo(other.mName);
}
@Override
public String toString() {
return mName;
}
}
@@ -38,8 +38,10 @@ public class ApiParser extends DefaultHandler {
private static final String ATTR_NAME = "name";
private static final String ATTR_SINCE = "since";
private static final String ATTR_DEPRECATED = "deprecated";
private final Map<String, ApiClass> mClasses = new HashMap<String, ApiClass>();
private final Map<String, ApiClass> mClasses = new HashMap<String, ApiClass>(1000);
private final Map<String, ApiPackage> mPackages = new HashMap<String, ApiPackage>();
private ApiClass mCurrentClass;
@@ -49,6 +51,7 @@ public class ApiParser extends DefaultHandler {
Map<String, ApiClass> getClasses() {
return mClasses;
}
Map<String, ApiPackage> getPackages() { return mPackages; }
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
@@ -59,14 +62,21 @@ public class ApiParser extends DefaultHandler {
}
try {
//noinspection StatementWithEmptyBody
if (NODE_API.equals(localName)) {
// do nothing.
} else if (NODE_CLASS.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = Integer.parseInt(attributes.getValue(ATTR_SINCE));
mCurrentClass = addClass(name, since);
String deprecatedAttr = attributes.getValue(ATTR_DEPRECATED);
int deprecatedIn;
if (deprecatedAttr != null) {
deprecatedIn = Integer.parseInt(deprecatedAttr);
} else {
deprecatedIn = 0;
}
mCurrentClass = addClass(name, since, deprecatedIn);
} else if (NODE_EXTENDS.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
@@ -83,14 +93,15 @@ public class ApiParser extends DefaultHandler {
} else if (NODE_METHOD.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = getSince(attributes);
mCurrentClass.addMethod(name, since);
int deprecatedIn = getDeprecatedIn(attributes);
mCurrentClass.addMethod(name, since, deprecatedIn);
} else if (NODE_FIELD.equals(localName)) {
String name = attributes.getValue(ATTR_NAME);
int since = getSince(attributes);
int deprecatedIn = getDeprecatedIn(attributes);
mCurrentClass.addField(name, since);
mCurrentClass.addField(name, since, deprecatedIn);
}
@@ -99,11 +110,21 @@ public class ApiParser extends DefaultHandler {
}
}
private ApiClass addClass(String name, int apiLevel) {
private ApiClass addClass(String name, int apiLevel, int deprecatedIn) {
// There should not be any duplicates
ApiClass theClass = mClasses.get(name);
if (theClass == null) {
theClass = new ApiClass(name, apiLevel);
mClasses.put(name, theClass);
assert theClass == null;
theClass = new ApiClass(name, apiLevel, deprecatedIn);
mClasses.put(name, theClass);
String pkg = theClass.getPackage();
if (pkg != null) {
ApiPackage apiPackage = mPackages.get(pkg);
if (apiPackage == null) {
apiPackage = new ApiPackage(pkg);
mPackages.put(pkg, apiPackage);
}
apiPackage.addClass(theClass);
}
return theClass;
@@ -119,4 +140,15 @@ public class ApiParser extends DefaultHandler {
return since;
}
private int getDeprecatedIn(Attributes attributes) {
int deprecatedIn = mCurrentClass.getDeprecatedIn();
String deprecatedAttr = attributes.getValue(ATTR_DEPRECATED);
if (deprecatedAttr != null) {
deprecatedIn = Integer.parseInt(deprecatedAttr);
}
return deprecatedIn;
}
}
@@ -21,28 +21,29 @@ import static com.android.tools.klint.detector.api.TextFormat.RAW;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.TextFormat;
import com.intellij.psi.PsiMethod;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Arrays;
import java.util.List;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UFunction;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
public class AppCompatCallDetector extends Detector implements UastScanner {
public class AppCompatCallDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"AppCompatMethod",
"Using Wrong AppCompat Method",
@@ -52,7 +53,7 @@ public class AppCompatCallDetector extends Detector implements UastScanner {
Category.CORRECTNESS, 6, Severity.WARNING,
new Implementation(
AppCompatCallDetector.class,
Scope.SOURCE_FILE_SCOPE)).
Scope.JAVA_FILE_SCOPE)).
addMoreInfo("http://developer.android.com/tools/support-library/index.html");
private static final String GET_ACTION_BAR = "getActionBar";
@@ -69,66 +70,62 @@ public class AppCompatCallDetector extends Detector implements UastScanner {
public AppCompatCallDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void beforeCheckProject(@NonNull Context context) {
Boolean dependsOnAppCompat = context.getProject().dependsOn(APPCOMPAT_LIB_ARTIFACT);
mDependsOnAppCompat = dependsOnAppCompat != null && dependsOnAppCompat;
}
@Nullable
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Arrays.asList(
GET_ACTION_BAR,
START_ACTION_MODE,
SET_PROGRESS_BAR_VIS,
SET_PROGRESS_BAR_IN_VIS,
SET_PROGRESS_BAR_INDETERMINATE,
REQUEST_WINDOW_FEATURE);
GET_ACTION_BAR,
START_ACTION_MODE,
SET_PROGRESS_BAR_VIS,
SET_PROGRESS_BAR_IN_VIS,
SET_PROGRESS_BAR_INDETERMINATE,
REQUEST_WINDOW_FEATURE);
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
if (mDependsOnAppCompat && isAppBarActivityCall(context, node)) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
if (mDependsOnAppCompat && isAppBarActivityCall(context, node, method)) {
String name = method.getName();
String replace = null;
if (node.matchesFunctionName(GET_ACTION_BAR)) {
if (GET_ACTION_BAR.equals(name)) {
replace = "getSupportActionBar";
} else if (node.matchesFunctionName(START_ACTION_MODE)) {
} else if (START_ACTION_MODE.equals(name)) {
replace = "startSupportActionMode";
} else if (node.matchesFunctionName(SET_PROGRESS_BAR_VIS)) {
} else if (SET_PROGRESS_BAR_VIS.equals(name)) {
replace = "setSupportProgressBarVisibility";
} else if (node.matchesFunctionName(SET_PROGRESS_BAR_IN_VIS)) {
} else if (SET_PROGRESS_BAR_IN_VIS.equals(name)) {
replace = "setSupportProgressBarIndeterminateVisibility";
} else if (node.matchesFunctionName(SET_PROGRESS_BAR_INDETERMINATE)) {
} else if (SET_PROGRESS_BAR_INDETERMINATE.equals(name)) {
replace = "setSupportProgressBarIndeterminate";
} else if (node.matchesFunctionName(REQUEST_WINDOW_FEATURE)) {
} else if (REQUEST_WINDOW_FEATURE.equals(name)) {
replace = "supportRequestWindowFeature";
}
if (replace != null) {
String message = String.format(ERROR_MESSAGE_FORMAT, replace, node.getFunctionName());
context.report(ISSUE, node, context.getLocation(node), message);
String message = String.format(ERROR_MESSAGE_FORMAT, replace, name);
context.report(ISSUE, node, context.getUastLocation(node), message);
}
}
}
private static boolean isAppBarActivityCall(@NonNull UastAndroidContext context,
@NonNull UCallExpression node) {
UFunction resolved = node.resolve(context);
if (resolved != null) {
UClass containingClass = UastUtils.getContainingClass(resolved);
if (containingClass != null && containingClass.isSubclassOf(CLASS_ACTIVITY)) {
// Make sure that the calling context is a subclass of ActionBarActivity;
// we don't want to flag these calls if they are in non-appcompat activities
// such as PreferenceActivity (see b.android.com/58512)
return UastUtils.getContainingClassOrEmpty(node)
.isSubclassOf("android.support.v7.app.ActionBarActivity");
}
private static boolean isAppBarActivityCall(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.isMemberInSubClassOf(method, CLASS_ACTIVITY, false)) {
// Make sure that the calling context is a subclass of ActionBarActivity;
// we don't want to flag these calls if they are in non-appcompat activities
// such as PreferenceActivity (see b.android.com/58512)
UClass cls = UastUtils.getParentOfType(node, UClass.class, true);
return cls != null && evaluator.extendsClass(cls,
"android.support.v7.app.ActionBarActivity", false);
}
return false;
}
@@ -0,0 +1,743 @@
/*
* Copyright (C) 2015 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 com.android.tools.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_EXPORTED;
import static com.android.SdkConstants.ATTR_HOST;
import static com.android.SdkConstants.ATTR_PATH;
import static com.android.SdkConstants.ATTR_PATH_PREFIX;
import static com.android.SdkConstants.ATTR_SCHEME;
import static com.android.SdkConstants.CLASS_ACTIVITY;
import static com.android.xml.AndroidManifest.ATTRIBUTE_MIME_TYPE;
import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME;
import static com.android.xml.AndroidManifest.ATTRIBUTE_PORT;
import static com.android.xml.AndroidManifest.NODE_ACTION;
import static com.android.xml.AndroidManifest.NODE_ACTIVITY;
import static com.android.xml.AndroidManifest.NODE_APPLICATION;
import static com.android.xml.AndroidManifest.NODE_CATEGORY;
import static com.android.xml.AndroidManifest.NODE_DATA;
import static com.android.xml.AndroidManifest.NODE_INTENT;
import static com.android.xml.AndroidManifest.NODE_MANIFEST;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.client.api.XmlParser;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.XmlScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
/**
* Check if the usage of App Indexing is correct.
*/
public class AppIndexingApiDetector extends Detector implements XmlScanner, Detector.UastScanner {
private static final Implementation URL_IMPLEMENTATION = new Implementation(
AppIndexingApiDetector.class, Scope.MANIFEST_SCOPE);
@SuppressWarnings("unchecked")
private static final Implementation APP_INDEXING_API_IMPLEMENTATION =
new Implementation(
AppIndexingApiDetector.class,
EnumSet.of(Scope.JAVA_FILE, Scope.MANIFEST),
Scope.JAVA_FILE_SCOPE, Scope.MANIFEST_SCOPE);
public static final Issue ISSUE_URL_ERROR = Issue.create(
"GoogleAppIndexingUrlError", //$NON-NLS-1$
"URL not supported by app for Google App Indexing",
"Ensure the URL is supported by your app, to get installs and traffic to your"
+ " app from Google Search.",
Category.USABILITY, 5, Severity.ERROR, URL_IMPLEMENTATION)
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio");
public static final Issue ISSUE_APP_INDEXING =
Issue.create(
"GoogleAppIndexingWarning", //$NON-NLS-1$
"Missing support for Google App Indexing",
"Adds URLs to get your app into the Google index, to get installs"
+ " and traffic to your app from Google Search.",
Category.USABILITY, 5, Severity.WARNING, URL_IMPLEMENTATION)
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio");
public static final Issue ISSUE_APP_INDEXING_API =
Issue.create(
"GoogleAppIndexingApiWarning", //$NON-NLS-1$
"Missing support for Google App Indexing Api",
"Adds URLs to get your app into the Google index, to get installs"
+ " and traffic to your app from Google Search.",
Category.USABILITY, 5, Severity.WARNING, APP_INDEXING_API_IMPLEMENTATION)
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio")
.setEnabledByDefault(false);
private static final String[] PATH_ATTR_LIST = new String[]{ATTR_PATH_PREFIX, ATTR_PATH};
private static final String SCHEME_MISSING = "android:scheme is missing";
private static final String HOST_MISSING = "android:host is missing";
private static final String DATA_MISSING = "Missing data element";
private static final String URL_MISSING = "Missing URL for the intent filter";
private static final String NOT_BROWSABLE
= "Activity supporting ACTION_VIEW is not set as BROWSABLE";
private static final String ILLEGAL_NUMBER = "android:port is not a legal number";
private static final String APP_INDEX_START = "start"; //$NON-NLS-1$
private static final String APP_INDEX_END = "end"; //$NON-NLS-1$
private static final String APP_INDEX_VIEW = "view"; //$NON-NLS-1$
private static final String APP_INDEX_VIEW_END = "viewEnd"; //$NON-NLS-1$
private static final String CLIENT_CONNECT = "connect"; //$NON-NLS-1$
private static final String CLIENT_DISCONNECT = "disconnect"; //$NON-NLS-1$
private static final String ADD_API = "addApi"; //$NON-NLS-1$
private static final String APP_INDEXING_API_CLASS
= "com.google.android.gms.appindexing.AppIndexApi";
private static final String GOOGLE_API_CLIENT_CLASS
= "com.google.android.gms.common.api.GoogleApiClient";
private static final String GOOGLE_API_CLIENT_BUILDER_CLASS
= "com.google.android.gms.common.api.GoogleApiClient.Builder";
private static final String API_CLASS = "com.google.android.gms.appindexing.AppIndex";
public enum IssueType {
SCHEME_MISSING(AppIndexingApiDetector.SCHEME_MISSING),
HOST_MISSING(AppIndexingApiDetector.HOST_MISSING),
DATA_MISSING(AppIndexingApiDetector.DATA_MISSING),
URL_MISSING(AppIndexingApiDetector.URL_MISSING),
NOT_BROWSABLE(AppIndexingApiDetector.NOT_BROWSABLE),
ILLEGAL_NUMBER(AppIndexingApiDetector.ILLEGAL_NUMBER),
EMPTY_FIELD("cannot be empty"),
MISSING_SLASH("attribute should start with '/'"),
UNKNOWN("unknown error type");
private final String message;
IssueType(String str) {
this.message = str;
}
public static IssueType parse(String str) {
for (IssueType type : IssueType.values()) {
if (str.contains(type.message)) {
return type;
}
}
return UNKNOWN;
}
}
// ---- Implements XmlScanner ----
@Override
@Nullable
public Collection<String> getApplicableElements() {
return Collections.singletonList(NODE_APPLICATION);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element application) {
List<Element> activities = extractChildrenByName(application, NODE_ACTIVITY);
boolean applicationHasActionView = false;
for (Element activity : activities) {
List<Element> intents = extractChildrenByName(activity, NODE_INTENT);
boolean activityHasActionView = false;
for (Element intent : intents) {
boolean actionView = hasActionView(intent);
if (actionView) {
activityHasActionView = true;
}
visitIntent(context, intent);
}
if (activityHasActionView) {
applicationHasActionView = true;
if (activity.hasAttributeNS(ANDROID_URI, ATTR_EXPORTED)) {
Attr exported = activity.getAttributeNodeNS(ANDROID_URI, ATTR_EXPORTED);
if (!exported.getValue().equals("true")) {
// Report error if the activity supporting action view is not exported.
context.report(ISSUE_URL_ERROR, activity,
context.getLocation(activity),
"Activity supporting ACTION_VIEW is not exported");
}
}
}
}
if (!applicationHasActionView && !context.getProject().isLibrary()) {
// Report warning if there is no activity that supports action view.
context.report(ISSUE_APP_INDEXING, application, context.getLocation(application),
// This error message is more verbose than the other app indexing lint warnings, because it
// shows up on a blank project, and we want to make it obvious by just looking at the error
// message what this is
"App is not indexable by Google Search; consider adding at least one Activity with an ACTION-VIEW " +
"intent filter. See issue explanation for more details.");
}
}
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList(CLASS_ACTIVITY);
}
@Override
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
if (declaration.getName() == null) {
return;
}
// In case linting the base class itself.
if (!context.getEvaluator().extendsClass(declaration, CLASS_ACTIVITY, true)) {
return;
}
declaration.accept(new MethodVisitor(context, declaration));
}
static class MethodVisitor extends AbstractUastVisitor {
private final JavaContext mContext;
private final UClass mCls;
private final List<UCallExpression> mStartMethods;
private final List<UCallExpression> mEndMethods;
private final List<UCallExpression> mConnectMethods;
private final List<UCallExpression> mDisconnectMethods;
private boolean mHasAddAppIndexApi;
private MethodVisitor(JavaContext context, UClass cls) {
mCls = cls;
mContext = context;
mStartMethods = Lists.newArrayListWithExpectedSize(2);
mEndMethods = Lists.newArrayListWithExpectedSize(2);
mConnectMethods = Lists.newArrayListWithExpectedSize(2);
mDisconnectMethods = Lists.newArrayListWithExpectedSize(2);
}
@Override
public boolean visitClass(UClass aClass) {
if (aClass.getPsi().equals(mCls.getPsi())) {
return super.visitClass(aClass);
} else {
// Don't go into inner classes
return true;
}
}
@Override
public void afterVisitClass(UClass node) {
report();
}
@Override
public boolean visitCallExpression(UCallExpression node) {
if (UastExpressionUtils.isMethodCall(node)) {
visitMethodCallExpression(node);
}
return super.visitCallExpression(node);
}
private void visitMethodCallExpression(UCallExpression node) {
String methodName = node.getMethodName();
if (methodName == null) {
return;
}
if (methodName.equals(APP_INDEX_START)) {
if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) {
mStartMethods.add(node);
}
}
else if (methodName.equals(APP_INDEX_END)) {
if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) {
mEndMethods.add(node);
}
}
else if (methodName.equals(APP_INDEX_VIEW)) {
if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) {
mStartMethods.add(node);
}
}
else if (methodName.equals(APP_INDEX_VIEW_END)) {
if (JavaEvaluator.isMemberInClass(node.resolve(), APP_INDEXING_API_CLASS)) {
mEndMethods.add(node);
}
}
else if (methodName.equals(CLIENT_CONNECT)) {
if (JavaEvaluator.isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_CLASS)) {
mConnectMethods.add(node);
}
}
else if (methodName.equals(CLIENT_DISCONNECT)) {
if (JavaEvaluator.isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_CLASS)) {
mDisconnectMethods.add(node);
}
}
else if (methodName.equals(ADD_API)) {
if (JavaEvaluator
.isMemberInClass(node.resolve(), GOOGLE_API_CLIENT_BUILDER_CLASS)) {
List<UExpression> args = node.getValueArguments();
if (!args.isEmpty()) {
PsiElement resolved = UastUtils.tryResolve(args.get(0));
if (resolved instanceof PsiField &&
JavaEvaluator.isMemberInClass((PsiField) resolved, API_CLASS)) {
mHasAddAppIndexApi = true;
}
}
}
}
}
private void report() {
// finds the activity classes that need app activity annotation
Set<String> activitiesToCheck = getActivitiesToCheck(mContext);
// app indexing API used but no support in manifest
boolean hasIntent = activitiesToCheck.contains(mCls.getQualifiedName());
if (!hasIntent) {
for (UCallExpression call : mStartMethods) {
mContext.report(ISSUE_APP_INDEXING_API, call,
mContext.getUastNameLocation(call),
"Missing support for Google App Indexing in the manifest");
}
for (UCallExpression call : mEndMethods) {
mContext.report(ISSUE_APP_INDEXING_API, call,
mContext.getUastNameLocation(call),
"Missing support for Google App Indexing in the manifest");
}
return;
}
// `AppIndex.AppIndexApi.start / end / view / viewEnd` should exist
if (mStartMethods.isEmpty() && mEndMethods.isEmpty()) {
mContext.reportUast(ISSUE_APP_INDEXING_API, mCls,
mContext.getUastNameLocation(mCls),
"Missing support for Google App Indexing API");
return;
}
for (UCallExpression startNode : mStartMethods) {
List<UExpression> expressions = startNode.getValueArguments();
if (expressions.isEmpty()) {
continue;
}
UExpression startClient = expressions.get(0);
// GoogleApiClient should `addApi(AppIndex.APP_INDEX_API)`
if (!mHasAddAppIndexApi) {
String message = String.format(
"GoogleApiClient `%1$s` has not added support for App Indexing API",
startClient.asSourceString());
mContext.report(ISSUE_APP_INDEXING_API, startClient,
mContext.getUastLocation(startClient), message);
}
// GoogleApiClient `connect` should exist
if (!hasOperand(startClient, mConnectMethods)) {
String message = String.format("GoogleApiClient `%1$s` is not connected",
startClient.asSourceString());
mContext.report(ISSUE_APP_INDEXING_API, startClient,
mContext.getUastLocation(startClient), message);
}
// `AppIndex.AppIndexApi.end` should pair with `AppIndex.AppIndexApi.start`
if (!hasFirstArgument(startClient, mEndMethods)) {
mContext.report(ISSUE_APP_INDEXING_API, startNode,
mContext.getUastNameLocation(startNode),
"Missing corresponding `AppIndex.AppIndexApi.end` method");
}
}
for (UCallExpression endNode : mEndMethods) {
List<UExpression> expressions = endNode.getValueArguments();
if (expressions.isEmpty()) {
continue;
}
UExpression endClient = expressions.get(0);
// GoogleApiClient should `addApi(AppIndex.APP_INDEX_API)`
if (!mHasAddAppIndexApi) {
String message = String.format(
"GoogleApiClient `%1$s` has not added support for App Indexing API",
endClient.asSourceString());
mContext.report(ISSUE_APP_INDEXING_API, endClient,
mContext.getUastLocation(endClient), message);
}
// GoogleApiClient `disconnect` should exist
if (!hasOperand(endClient, mDisconnectMethods)) {
String message = String.format("GoogleApiClient `%1$s`"
+ " is not disconnected", endClient.asSourceString());
mContext.report(ISSUE_APP_INDEXING_API, endClient,
mContext.getUastLocation(endClient), message);
}
// `AppIndex.AppIndexApi.start` should pair with `AppIndex.AppIndexApi.end`
if (!hasFirstArgument(endClient, mStartMethods)) {
mContext.report(ISSUE_APP_INDEXING_API, endNode,
mContext.getUastNameLocation(endNode),
"Missing corresponding `AppIndex.AppIndexApi.start` method");
}
}
}
}
/**
* Gets names of activities which needs app indexing. i.e. the activities have data tag in their
* intent filters.
* TODO: Cache the activities to speed up batch lint.
*
* @param context The context to check in.
*/
private static Set<String> getActivitiesToCheck(Context context) {
Set<String> activitiesToCheck = Sets.newHashSet();
List<File> manifestFiles = context.getProject().getManifestFiles();
XmlParser xmlParser = context.getDriver().getClient().getXmlParser();
if (xmlParser != null) {
// TODO: Avoid visit all manifest files before enable this check by default.
for (File manifest : manifestFiles) {
XmlContext xmlContext =
new XmlContext(context.getDriver(), context.getProject(),
null, manifest, null, xmlParser);
Document doc = xmlParser.parseXml(xmlContext);
if (doc != null) {
List<Element> children = LintUtils.getChildren(doc);
for (Element child : children) {
if (child.getNodeName().equals(NODE_MANIFEST)) {
List<Element> apps = extractChildrenByName(child, NODE_APPLICATION);
for (Element app : apps) {
List<Element> acts = extractChildrenByName(app, NODE_ACTIVITY);
for (Element act : acts) {
List<Element> intents = extractChildrenByName(act, NODE_INTENT);
for (Element intent : intents) {
List<Element> data = extractChildrenByName(intent,
NODE_DATA);
if (!data.isEmpty() && act.hasAttributeNS(
ANDROID_URI, ATTRIBUTE_NAME)) {
Attr attr = act.getAttributeNodeNS(
ANDROID_URI, ATTRIBUTE_NAME);
String activityName = attr.getValue();
int dotIndex = activityName.indexOf('.');
if (dotIndex <= 0) {
String pkg = context.getMainProject().getPackage();
if (pkg != null) {
if (dotIndex == 0) {
activityName = pkg + activityName;
}
else {
activityName = pkg + '.' + activityName;
}
}
}
activitiesToCheck.add(activityName);
}
}
}
}
}
}
}
}
}
return activitiesToCheck;
}
private static void visitIntent(@NonNull XmlContext context, @NonNull Element intent) {
boolean actionView = hasActionView(intent);
boolean browsable = isBrowsable(intent);
boolean isHttp = false;
boolean hasScheme = false;
boolean hasHost = false;
boolean hasPort = false;
boolean hasPath = false;
boolean hasMimeType = false;
Element firstData = null;
List<Element> children = extractChildrenByName(intent, NODE_DATA);
for (Element data : children) {
if (firstData == null) {
firstData = data;
}
if (isHttpSchema(data)) {
isHttp = true;
}
checkSingleData(context, data);
for (String name : PATH_ATTR_LIST) {
if (data.hasAttributeNS(ANDROID_URI, name)) {
hasPath = true;
}
}
if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
hasScheme = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTR_HOST)) {
hasHost = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
hasPort = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_MIME_TYPE)) {
hasMimeType = true;
}
}
// In data field, a URL is consisted by
// <scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]
// Each part of the URL should not have illegal character.
if ((hasPath || hasHost || hasPort) && !hasScheme) {
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
SCHEME_MISSING);
}
if ((hasPath || hasPort) && !hasHost) {
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
HOST_MISSING);
}
if (actionView && browsable) {
if (firstData == null) {
// If this activity is an ACTION_VIEW action with category BROWSABLE, but doesn't
// have data node, it may be a mistake and we will report error.
context.report(ISSUE_URL_ERROR, intent, context.getLocation(intent),
DATA_MISSING);
} else if (!hasScheme && !hasMimeType) {
// If this activity is an action view, is browsable, but has neither a
// URL nor mimeType, it may be a mistake and we will report error.
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
URL_MISSING);
}
}
// If this activity is an ACTION_VIEW action, has a http URL but doesn't have
// BROWSABLE, it may be a mistake and and we will report warning.
if (actionView && isHttp && !browsable) {
context.report(ISSUE_APP_INDEXING, intent, context.getLocation(intent),
NOT_BROWSABLE);
}
if (actionView && !hasScheme) {
context.report(ISSUE_APP_INDEXING, intent, context.getLocation(intent),
"Missing URL");
}
}
/**
* Check if the intent filter supports action view.
*
* @param intent the intent filter
* @return true if it does
*/
private static boolean hasActionView(@NonNull Element intent) {
List<Element> children = extractChildrenByName(intent, NODE_ACTION);
for (Element action : children) {
if (action.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) {
Attr attr = action.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME);
if (attr.getValue().equals("android.intent.action.VIEW")) {
return true;
}
}
}
return false;
}
/**
* Check if the intent filter is browsable.
*
* @param intent the intent filter
* @return true if it does
*/
private static boolean isBrowsable(@NonNull Element intent) {
List<Element> children = extractChildrenByName(intent, NODE_CATEGORY);
for (Element e : children) {
if (e.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) {
Attr attr = e.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME);
if (attr.getNodeValue().equals("android.intent.category.BROWSABLE")) {
return true;
}
}
}
return false;
}
/**
* Check if the data node contains http schema
*
* @param data the data node
* @return true if it does
*/
private static boolean isHttpSchema(@NonNull Element data) {
if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
String value = data.getAttributeNodeNS(ANDROID_URI, ATTR_SCHEME).getValue();
if (value.equalsIgnoreCase("http") || value.equalsIgnoreCase("https")) {
return true;
}
}
return false;
}
private static void checkSingleData(@NonNull XmlContext context, @NonNull Element data) {
// path, pathPrefix and pathPattern should starts with /.
for (String name : PATH_ATTR_LIST) {
if (data.hasAttributeNS(ANDROID_URI, name)) {
Attr attr = data.getAttributeNodeNS(ANDROID_URI, name);
String path = replaceUrlWithValue(context, attr.getValue());
if (!path.startsWith("/") && !path.startsWith(SdkConstants.PREFIX_RESOURCE_REF)) {
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
"android:" + name + " attribute should start with '/', but it is : "
+ path);
}
}
}
// port should be a legal number.
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
Attr attr = data.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_PORT);
try {
String port = replaceUrlWithValue(context, attr.getValue());
//noinspection ResultOfMethodCallIgnored
Integer.parseInt(port);
} catch (NumberFormatException e) {
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
ILLEGAL_NUMBER);
}
}
// Each field should be non empty.
NamedNodeMap attrs = data.getAttributes();
for (int i = 0; i < attrs.getLength(); i++) {
Node item = attrs.item(i);
if (item.getNodeType() == Node.ATTRIBUTE_NODE) {
Attr attr = (Attr) attrs.item(i);
if (attr.getValue().isEmpty()) {
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
attr.getName() + " cannot be empty");
}
}
}
}
private static String replaceUrlWithValue(@NonNull XmlContext context,
@NonNull String str) {
Project project = context.getProject();
LintClient client = context.getClient();
if (!client.supportsProjectResources()) {
return str;
}
ResourceUrl style = ResourceUrl.parse(str);
if (style == null || style.type != ResourceType.STRING || style.framework) {
return str;
}
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return str;
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.STRING, style.name);
if (items == null || items.isEmpty()) {
return str;
}
ResourceValue resourceValue = items.get(0).getResourceValue(false);
if (resourceValue == null) {
return str;
}
return resourceValue.getValue() == null ? str : resourceValue.getValue();
}
/**
* If a method with a certain argument exists in the list of methods.
*
* @param argument The first argument of the method.
* @param list The methods list.
* @return If such a method exists in the list.
*/
private static boolean hasFirstArgument(UExpression argument, List<UCallExpression> list) {
for (UCallExpression call : list) {
List<UExpression> expressions = call.getValueArguments();
if (!expressions.isEmpty()) {
UExpression argument2 = expressions.get(0);
if (argument.asSourceString().equals(argument2.asSourceString())) {
return true;
}
}
}
return false;
}
/**
* If a method with a certain operand exists in the list of methods.
*
* @param operand The operand of the method.
* @param list The methods list.
* @return If such a method exists in the list.
*/
private static boolean hasOperand(UExpression operand, List<UCallExpression> list) {
for (UCallExpression method : list) {
UElement operand2 = method.getReceiver();
if (operand2 != null && operand.asSourceString().equals(operand2.asSourceString())) {
return true;
}
}
return false;
}
private static List<Element> extractChildrenByName(@NonNull Element node,
@NonNull String name) {
List<Element> result = Lists.newArrayList();
List<Element> children = LintUtils.getChildren(node);
for (Element child : children) {
if (child.getNodeName().equals(name)) {
result.add(child);
}
}
return result;
}
}
@@ -0,0 +1,141 @@
/*
* Copyright (C) 2015 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 com.android.tools.klint.checks;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReturnStatement;
import com.intellij.psi.PsiThrowStatement;
import org.jetbrains.uast.*;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
public class BadHostnameVerifierDetector extends Detector implements Detector.UastScanner {
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION =
new Implementation(BadHostnameVerifierDetector.class,
Scope.JAVA_FILE_SCOPE);
public static final Issue ISSUE = Issue.create("BadHostnameVerifier",
"Insecure HostnameVerifier",
"This check looks for implementations of `HostnameVerifier` " +
"whose `verify` method always returns true (thus trusting any hostname) " +
"which could result in insecure network traffic caused by trusting arbitrary " +
"hostnames in TLS/SSL certificates presented by peers.",
Category.SECURITY,
6,
Severity.WARNING,
IMPLEMENTATION);
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList("javax.net.ssl.HostnameVerifier");
}
@Override
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
JavaEvaluator evaluator = context.getEvaluator();
for (PsiMethod method : declaration.findMethodsByName("verify", false)) {
if (evaluator.methodMatches(method, null, false,
TYPE_STRING, "javax.net.ssl.SSLSession")) {
ComplexVisitor visitor = new ComplexVisitor(context);
declaration.accept(visitor);
if (visitor.isComplex()) {
return;
}
Location location = context.getNameLocation(method);
String message = String.format("`%1$s` always returns `true`, which " +
"could cause insecure network traffic due to trusting "
+ "TLS/SSL server certificates for wrong hostnames",
method.getName());
context.report(ISSUE, location, message);
break;
}
}
}
private static class ComplexVisitor extends AbstractUastVisitor {
@SuppressWarnings("unused")
private final JavaContext mContext;
private boolean mComplex;
public ComplexVisitor(JavaContext context) {
mContext = context;
}
@Override
public boolean visitThrowExpression(UThrowExpression node) {
mComplex = true;
return super.visitThrowExpression(node);
}
@Override
public boolean visitCallExpression(UCallExpression node) {
// TODO: Ignore certain known safe methods, e.g. Logging etc
mComplex = true;
return super.visitCallExpression(node);
}
@Override
public boolean visitReturnExpression(UReturnExpression node) {
UExpression argument = node.getReturnExpression();
if (argument != null) {
// TODO: Only do this if certain that there isn't some intermediate
// assignment, as exposed by the unit test
//Object value = ConstantEvaluator.evaluate(mContext, argument);
//if (Boolean.TRUE.equals(value)) {
if (UastLiteralUtils.isTrueLiteral(argument)) {
mComplex = false;
} else {
mComplex = true; // "return false" or some complicated logic
}
}
return super.visitReturnExpression(node);
}
private boolean isComplex() {
return mComplex;
}
}
}
@@ -0,0 +1,151 @@
/*
* Copyright (C) 2016 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 com.android.tools.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.TAG_RECEIVER;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.XmlContext;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
/**
* Checks looking for issues that negatively affect battery life
*/
public class BatteryDetector extends ResourceXmlDetector implements
Detector.UastScanner {
@SuppressWarnings("unchecked")
public static final Implementation IMPLEMENTATION = new Implementation(
BatteryDetector.class,
EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE),
Scope.MANIFEST_SCOPE,
Scope.JAVA_FILE_SCOPE);
/** Issues that negatively affect battery life */
public static final Issue ISSUE = Issue.create(
"BatteryLife", //$NON-NLS-1$
"Battery Life Issues",
"This issue flags code that either\n" +
"* negatively affects battery life, or\n" +
"* uses APIs that have recently changed behavior to prevent background tasks " +
"from consuming memory and battery excessively.\n" +
"\n" +
"Generally, you should be using `JobScheduler` or `GcmNetworkManager` instead.\n" +
"\n" +
"For more details on how to update your code, please see" +
"http://developer.android.com/preview/features/background-optimization.html",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo("http://developer.android.com/preview/features/background-optimization.html");
/** Constructs a new {@link BatteryDetector} */
public BatteryDetector() {
}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList("action");
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
assert element.getTagName().equals("action");
Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (attr == null) {
return;
}
String name = attr.getValue();
if ("android.net.conn.CONNECTIVITY_CHANGE".equals(name)
&& element.getParentNode() != null
&& element.getParentNode().getParentNode() != null
&& TAG_RECEIVER.equals(element.getParentNode().getParentNode().getNodeName())
&& context.getMainProject().getTargetSdkVersion().getFeatureLevel() >= 24) {
String message = "Declaring a broadcastreceiver for "
+ "`android.net.conn.CONNECTIVITY_CHANGE` is deprecated for apps targeting "
+ "N and higher. In general, apps should not rely on this broadcast and "
+ "instead use `JobScheduler` or `GCMNetworkManager`.";
context.report(ISSUE, element, context.getValueLocation(attr), message);
}
if ("android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS".equals(name)
&& context.getMainProject().getTargetSdkVersion().getFeatureLevel() >= 23) {
String message = getBatteryOptimizationsErrorMessage();
context.report(ISSUE, element, context.getValueLocation(attr), message);
}
if ("android.hardware.action.NEW_PICTURE".equals(name)
|| "android.hardware.action.NEW_VIDEO".equals(name)
|| "com.android.camera.NEW_PICTURE".equals(name)) {
String message = String.format("Use of %1$s is deprecated for all apps starting "
+ "with the N release independent of the target SDK. Apps should not "
+ "rely on these broadcasts and instead use `JobScheduler`", name);
context.report(ISSUE, element, context.getValueLocation(attr), message);
}
}
@Nullable
@Override
public List<String> getApplicableReferenceNames() {
return Collections.singletonList("ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS");
}
@Override
public void visitReference(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UReferenceExpression reference, @NonNull PsiElement resolved) {
if (resolved instanceof PsiField &&
context.getEvaluator().isMemberInSubClassOf((PsiField) resolved,
"android.provider.Settings", false)
&& context.getMainProject().getTargetSdkVersion().getFeatureLevel() >= 23) {
String message = getBatteryOptimizationsErrorMessage();
context.report(ISSUE, reference, context.getUastNameLocation(reference), message);
}
}
@NonNull
private static String getBatteryOptimizationsErrorMessage() {
return "Use of `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` violates the "
+ "Play Store Content Policy regarding acceptable use cases, as described in "
+ "http://developer.android.com/training/monitoring-device-state/doze-standby.html";
}
}
@@ -30,28 +30,48 @@ import java.util.List;
/** Registry which provides a list of checks to be performed on an Android project */
public class BuiltinIssueRegistry extends IssueRegistry {
private static final List<Issue> sIssues;
static final int INITIAL_CAPACITY = 220;
static final int INITIAL_CAPACITY = 262;
static {
List<Issue> issues = new ArrayList<Issue>(INITIAL_CAPACITY);
issues.add(AddJavascriptInterfaceDetector.ISSUE);
issues.add(AlarmDetector.ISSUE);
issues.add(AllowAllHostnameVerifierDetector.ISSUE);
issues.add(AlwaysShowActionDetector.ISSUE);
issues.add(AnnotationDetector.ISSUE);
issues.add(AndroidAutoDetector.INVALID_USES_TAG_ISSUE);
issues.add(AndroidAutoDetector.MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH);
issues.add(AndroidAutoDetector.MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE);
issues.add(AndroidAutoDetector.MISSING_ON_PLAY_FROM_SEARCH);
issues.add(AnnotationDetector.ANNOTATION_USAGE);
issues.add(AnnotationDetector.FLAG_STYLE);
issues.add(AnnotationDetector.INSIDE_METHOD);
issues.add(AnnotationDetector.SWITCH_TYPE_DEF);
issues.add(AnnotationDetector.UNIQUE);
issues.add(ApiDetector.INLINED);
issues.add(ApiDetector.OVERRIDE);
issues.add(ApiDetector.UNSUPPORTED);
issues.add(ApiDetector.UNUSED);
issues.add(AppCompatCallDetector.ISSUE);
issues.add(AppIndexingApiDetector.ISSUE_APP_INDEXING_API);
issues.add(AppIndexingApiDetector.ISSUE_URL_ERROR);
issues.add(AppIndexingApiDetector.ISSUE_APP_INDEXING);
issues.add(BadHostnameVerifierDetector.ISSUE);
issues.add(BatteryDetector.ISSUE);
issues.add(CallSuperDetector.ISSUE);
issues.add(CipherGetInstanceDetector.ISSUE);
issues.add(CleanupDetector.COMMIT_FRAGMENT);
issues.add(CleanupDetector.RECYCLE_RESOURCE);
issues.add(CleanupDetector.SHARED_PREF);
issues.add(CommentDetector.EASTER_EGG);
issues.add(CommentDetector.STOP_SHIP);
issues.add(CustomViewDetector.ISSUE);
issues.add(CutPasteDetector.ISSUE);
issues.add(DateFormatDetector.DATE_FORMAT);
issues.add(SetTextDetector.SET_TEXT_I18N);
issues.add(UnsafeNativeCodeDetector.LOAD);
issues.add(UnsafeNativeCodeDetector.UNSAFE_NATIVE_CODE_LOCATION);
issues.add(FragmentDetector.ISSUE);
issues.add(GetSignaturesDetector.ISSUE);
issues.add(HandlerDetector.ISSUE);
@@ -69,15 +89,19 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(IconDetector.ICON_MIX_9PNG);
issues.add(IconDetector.ICON_NODPI);
issues.add(IconDetector.ICON_XML_AND_PNG);
issues.add(TrustAllX509TrustManagerDetector.ISSUE);
issues.add(JavaPerformanceDetector.PAINT_ALLOC);
issues.add(JavaPerformanceDetector.USE_SPARSE_ARRAY);
issues.add(JavaPerformanceDetector.USE_VALUE_OF);
issues.add(JavaScriptInterfaceDetector.ISSUE);
issues.add(LayoutConsistencyDetector.INCONSISTENT_IDS);
issues.add(LayoutInflationDetector.ISSUE);
issues.add(LeakDetector.ISSUE);
issues.add(LocaleDetector.STRING_LOCALE);
issues.add(LogDetector.CONDITIONAL);
issues.add(LogDetector.LONG_TAG);
issues.add(LogDetector.WRONG_TAG);
issues.add(MathDetector.ISSUE);
issues.add(MergeRootFrameLayoutDetector.ISSUE);
issues.add(NonInternationalizedSmsDetector.ISSUE);
issues.add(OverdrawDetector.ISSUE);
@@ -85,22 +109,31 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(ParcelDetector.ISSUE);
issues.add(PreferenceActivityDetector.ISSUE);
issues.add(PrivateResourceDetector.ISSUE);
issues.add(ReadParcelableDetector.ISSUE);
issues.add(RecyclerViewDetector.DATA_BINDER);
issues.add(RecyclerViewDetector.FIXED_POSITION);
issues.add(RegistrationDetector.ISSUE);
issues.add(RequiredAttributeDetector.ISSUE);
issues.add(RtlDetector.COMPAT);
issues.add(RtlDetector.ENABLED);
issues.add(RtlDetector.SYMMETRY);
issues.add(RtlDetector.USE_START);
issues.add(SdCardDetector.ISSUE);
issues.add(SecureRandomDetector.ISSUE);
issues.add(SecurityDetector.EXPORTED_PROVIDER);
issues.add(SecurityDetector.EXPORTED_RECEIVER);
issues.add(SecurityDetector.EXPORTED_SERVICE);
issues.add(SecurityDetector.SET_READABLE);
issues.add(SecurityDetector.SET_WRITABLE);
issues.add(SecurityDetector.OPEN_PROVIDER);
issues.add(SecurityDetector.WORLD_READABLE);
issues.add(SecurityDetector.WORLD_WRITEABLE);
issues.add(ServiceCastDetector.ISSUE);
issues.add(SetJavaScriptEnabledDetector.ISSUE);
issues.add(SharedPrefsDetector.ISSUE);
issues.add(SQLiteDetector.ISSUE);
issues.add(SslCertificateSocketFactoryDetector.CREATE_SOCKET);
issues.add(SslCertificateSocketFactoryDetector.GET_INSECURE);
issues.add(StringAuthLeakDetector.AUTH_LEAK);
issues.add(StringFormatDetector.ARG_COUNT);
issues.add(StringFormatDetector.ARG_TYPES);
issues.add(StringFormatDetector.INVALID);
@@ -114,10 +147,11 @@ public class BuiltinIssueRegistry extends IssueRegistry {
issues.add(SupportAnnotationDetector.THREAD);
issues.add(SupportAnnotationDetector.TYPE_DEF);
issues.add(ToastDetector.ISSUE);
issues.add(UnusedResourceDetector.ISSUE);
issues.add(UnusedResourceDetector.ISSUE_IDS);
issues.add(UnsafeBroadcastReceiverDetector.ACTION_STRING);
issues.add(UnsafeBroadcastReceiverDetector.BROADCAST_SMS);
issues.add(ViewConstructorDetector.ISSUE);
issues.add(ViewHolderDetector.ISSUE);
issues.add(ViewTagDetector.ISSUE);
issues.add(ViewTypeDetector.ISSUE);
issues.add(WrongCallDetector.ISSUE);
issues.add(WrongImportDetector.ISSUE);
@@ -144,17 +178,17 @@ public class BuiltinIssueRegistry extends IssueRegistry {
} else {
int initialSize = 12;
if (scope.contains(Scope.RESOURCE_FILE)) {
initialSize += 75;
initialSize += 80;
} else if (scope.contains(Scope.ALL_RESOURCE_FILES)) {
initialSize += 10;
initialSize += 12;
}
if (scope.contains(Scope.SOURCE_FILE)) {
initialSize += 55;
if (scope.contains(Scope.JAVA_FILE)) {
initialSize += 74;
} else if (scope.contains(Scope.CLASS_FILE)) {
initialSize += 15;
} else if (scope.contains(Scope.MANIFEST)) {
initialSize += 30;
initialSize += 38;
} else if (scope.contains(Scope.GRADLE_FILE)) {
initialSize += 5;
}
@@ -18,40 +18,45 @@ package com.android.tools.klint.checks;
import static com.android.SdkConstants.CLASS_VIEW;
import static com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX;
import static com.android.tools.klint.checks.SupportAnnotationDetector.filterRelevantAnnotations;
import static com.android.tools.klint.detector.api.LintUtils.skipParentheses;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import java.io.File;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.USuperExpression;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
/**
* Makes sure that methods call super when overriding methods.
*/
public class CallSuperDetector extends Detector implements UastScanner {
public class CallSuperDetector extends Detector implements Detector.UastScanner {
private static final String CALL_SUPER_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX + "CallSuper"; //$NON-NLS-1$
private static final String ON_DETACHED_FROM_WINDOW = "onDetachedFromWindow"; //$NON-NLS-1$
private static final String ON_VISIBILITY_CHANGED = "onVisibilityChanged"; //$NON-NLS-1$
private static final Implementation IMPLEMENTATION = new Implementation(
CallSuperDetector.class,
Scope.SOURCE_FILE_SCOPE);
Scope.JAVA_FILE_SCOPE);
/** Missing call to super */
public static final Issue ISSUE = Issue.create(
@@ -70,41 +75,37 @@ public class CallSuperDetector extends Detector implements UastScanner {
public CallSuperDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Nullable
@Override
public UastVisitor createUastVisitor(final UastAndroidContext context) {
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UMethod.class);
}
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull final JavaContext context) {
return new AbstractUastVisitor() {
@Override
public boolean visitFunction(@NotNull UFunction node) {
checkCallSuper(context, node);
return false;
public boolean visitMethod(UMethod method) {
checkCallSuper(context, method);
return super.visitMethod(method);
}
};
}
private static void checkCallSuper(@NonNull UastAndroidContext context,
@NonNull UFunction declaration) {
private static void checkCallSuper(@NonNull JavaContext context,
@NonNull UMethod method) {
UFunction superMethod = getRequiredSuperMethod(context, declaration);
PsiMethod superMethod = getRequiredSuperMethod(context, method);
if (superMethod != null) {
if (!SuperCallVisitor.callsSuper(context, declaration, superMethod)) {
String methodName = declaration.getName();
if (!SuperCallVisitor.callsSuper(method, superMethod)) {
String methodName = method.getName();
String message = "Overriding method should call `super."
+ methodName + "`";
Location location = context.getLocation(declaration.getNameElement());
context.report(ISSUE, declaration, location, message);
Location location = context.getUastNameLocation(method);
context.reportUast(ISSUE, method, location, message);
}
}
}
@@ -114,8 +115,14 @@ public class CallSuperDetector extends Detector implements UastScanner {
* to be invoked, and if so, returns it (otherwise returns null)
*/
@Nullable
private static UFunction getRequiredSuperMethod(UastAndroidContext context,
@NonNull UFunction method) {
private static PsiMethod getRequiredSuperMethod(@NonNull JavaContext context,
@NonNull PsiMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
PsiMethod directSuper = evaluator.getSuperMethod(method);
if (directSuper == null) {
return null;
}
String name = method.getName();
if (ON_DETACHED_FROM_WINDOW.equals(name)) {
@@ -124,43 +131,37 @@ public class CallSuperDetector extends Detector implements UastScanner {
// is still dangerous if supporting older versions so flag
// this for now (should make annotation carry metadata like
// compileSdkVersion >= N).
if (!UastUtils.getContainingClassOrEmpty(method).isSubclassOf(CLASS_VIEW)) {
if (!evaluator.isMemberInSubClassOf(method, CLASS_VIEW, false)) {
return null;
}
List<UFunction> superFunctions = method.getSuperFunctions(context);
return superFunctions.isEmpty() ? null : superFunctions.get(0);
return directSuper;
} else if (ON_VISIBILITY_CHANGED.equals(name)) {
// From Android Wear API; doesn't yet have an annotation
// but we want to enforce this right away until the AAR
// is updated to supply it once @CallSuper is available in
// the support library
if (!UastUtils.getContainingClassOrEmpty(method).isSubclassOf(
"android.support.wearable.watchface.WatchFaceService.Engine")) {
if (!evaluator.isMemberInSubClassOf(method,
"android.support.wearable.watchface.WatchFaceService.Engine", false)) {
return null;
}
List<UFunction> superFunctions = method.getSuperFunctions(context);
return superFunctions.isEmpty() ? null : superFunctions.get(0);
return directSuper;
}
// Look up annotations metadata
List<UFunction> superFunctions = method.getSuperFunctions(context);
UFunction directSuper = superFunctions.isEmpty() ? null : superFunctions.get(0);
UFunction superMethod = directSuper;
PsiMethod superMethod = directSuper;
while (superMethod != null) {
for (UAnnotation annotation : superMethod.getAnnotations()) {
annotation = SupportAnnotationDetector.getRelevantAnnotation(annotation, context);
if (annotation != null) {
String fqName = annotation.getFqName();
if (CALL_SUPER_ANNOTATION.equals(fqName)) {
return directSuper;
} else if (fqName != null && fqName.endsWith(".OverrideMustInvoke")) {
// Handle findbugs annotation on the fly too
return directSuper;
}
PsiAnnotation[] annotations = superMethod.getModifierList().getAnnotations();
annotations = filterRelevantAnnotations(annotations);
for (PsiAnnotation annotation : annotations) {
String signature = annotation.getQualifiedName();
if (CALL_SUPER_ANNOTATION.equals(signature)) {
return directSuper;
} else if (signature != null && signature.endsWith(".OverrideMustInvoke")) {
// Handle findbugs annotation on the fly too
return directSuper;
}
}
superFunctions = superMethod.getSuperFunctions(context);
superMethod = superFunctions.isEmpty() ? null : superFunctions.get(0);
superMethod = evaluator.getSuperMethod(superMethod);
}
return null;
@@ -168,45 +169,31 @@ public class CallSuperDetector extends Detector implements UastScanner {
/** Visits a method and determines whether the method calls its super method */
private static class SuperCallVisitor extends AbstractUastVisitor {
private final UastAndroidContext mContext;
private final String mMethodContainingClassFqName;
private final String mMethodName;
private final PsiMethod mMethod;
private boolean mCallsSuper;
public static boolean callsSuper(
@NonNull UastAndroidContext context,
@NonNull UFunction methodDeclaration,
@NonNull UFunction superMethod) {
SuperCallVisitor visitor = new SuperCallVisitor(context, superMethod);
methodDeclaration.accept(visitor);
public static boolean callsSuper(@NonNull UMethod method,
@NonNull PsiMethod superMethod) {
SuperCallVisitor visitor = new SuperCallVisitor(superMethod);
method.accept(visitor);
return visitor.mCallsSuper;
}
private SuperCallVisitor(@NonNull UastAndroidContext context, @NonNull UFunction function) {
mContext = context;
mMethodContainingClassFqName = UastUtils.getContainingClassOrEmpty(function).getFqName();
mMethodName = function.getName();
private SuperCallVisitor(@NonNull PsiMethod method) {
mMethod = method;
}
@Override
public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) {
UExpression receiver = node.getReceiver();
UExpression selector = node.getSelector();
if (receiver instanceof USuperExpression && selector instanceof UCallExpression) {
UFunction resolvedFunction = ((UCallExpression) selector).resolve(mContext);
if (resolvedFunction != null && resolvedFunction.matchesNameWithContaining(mMethodContainingClassFqName, mMethodName)) {
public boolean visitSuperExpression(USuperExpression node) {
UElement parent = skipParentheses(node.getContainingElement());
if (parent instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) parent).resolve();
if (mMethod.equals(resolved)) {
mCallsSuper = true;
return true;
}
}
return false;
}
@Override
public boolean visitElement(@NotNull UElement node) {
return mCallsSuper || super.visitElement(node);
return super.visitSuperExpression(node);
}
}
}
@@ -17,39 +17,44 @@
package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.google.common.collect.Sets;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.ULiteralExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
/**
* Ensures that Cipher.getInstance is not called with AES as the parameter.
*/
public class CipherGetInstanceDetector extends Detector implements UastScanner {
public class CipherGetInstanceDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"GetInstance", //$NON-NLS-1$
"Cipher.getInstance with ECB",
"`Cipher#getInstance` should not be called with ECB as the cipher mode or without "
+ "setting the cipher mode because the default mode on android is ECB, which "
+ "is insecure.",
"`Cipher#getInstance` should not be called with ECB as the cipher mode or without " +
"setting the cipher mode because the default mode on android is ECB, which " +
"is insecure.",
Category.SECURITY,
9,
Severity.WARNING,
new Implementation(
CipherGetInstanceDetector.class,
Scope.SOURCE_FILE_SCOPE));
Scope.JAVA_FILE_SCOPE));
private static final String CIPHER = "javax.crypto.Cipher"; //$NON-NLS-1$
private static final String GET_INSTANCE = "getInstance"; //$NON-NLS-1$
@@ -57,62 +62,44 @@ public class CipherGetInstanceDetector extends Detector implements UastScanner {
Sets.newHashSet("AES", "DES", "DESede"); //$NON-NLS-1$
private static final String ECB = "ECB"; //$NON-NLS-1$
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Collections.singletonList(GET_INSTANCE);
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
UClass containingClass = UastUtils.getContainingClass(node);
if (containingClass == null || !containingClass.isSubclassOf(CIPHER)) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
if (!context.getEvaluator().isMemberInSubClassOf(method, CIPHER, false)) {
return;
}
List<UExpression> argumentList = node.getValueArguments();
if (argumentList.size() == 1) {
UExpression expression = argumentList.get(0);
if (expression instanceof ULiteralExpression) {
ULiteralExpression argument = (ULiteralExpression)expression;
String parameter = argument.renderString();
checkParameter(context, node, argument, parameter, false);
} else if (expression instanceof UResolvable) {
UDeclaration declaration = ((UResolvable)expression).resolve(context);
if (declaration instanceof UVariable) {
UVariable field = (UVariable) declaration;
UExpression initializer = field.getInitializer();
if (initializer != null) {
Object value = initializer.evaluate();
if (value instanceof String) {
checkParameter(context, node, expression, (String)value, true);
}
}
}
List<UExpression> arguments = node.getValueArguments();
if (arguments.size() == 1) {
UExpression expression = arguments.get(0);
Object value = ConstantEvaluator.evaluate(context, expression);
if (value instanceof String) {
checkParameter(context, node, expression, (String)value,
!(expression instanceof ULiteralExpression));
}
}
}
private static void checkParameter(@NonNull UastAndroidContext context,
@NonNull UCallExpression call, @NonNull UExpression arg, @NonNull String value,
boolean includeValue) {
private static void checkParameter(@NonNull JavaContext context,
@NonNull UCallExpression call, @NonNull UElement node, @NonNull String value,
boolean includeValue) {
if (ALGORITHM_ONLY.contains(value)) {
String message = "`Cipher.getInstance` should not be called without setting the"
+ " encryption mode and padding";
context.report(ISSUE, call, context.getLocation(arg), message);
context.report(ISSUE, call, context.getUastLocation(node), message);
} else if (value.contains(ECB)) {
String message = "ECB encryption mode should not be used";
if (includeValue) {
message += " (was \"" + value + "\")";
}
context.report(ISSUE, call, context.getLocation(arg), message);
context.report(ISSUE, call, context.getUastLocation(node), message);
}
}
}
@@ -16,37 +16,69 @@
package com.android.tools.klint.checks;
import static com.android.SdkConstants.CLASS_CONTENTPROVIDER;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.tools.klint.detector.api.LintUtils.skipParentheses;
import static com.intellij.psi.util.PsiTreeUtil.getParentOfType;
import static org.jetbrains.uast.UastUtils.getOutermostQualified;
import static org.jetbrains.uast.UastUtils.getParentOfType;
import static org.jetbrains.uast.UastUtils.getQualifiedChain;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.google.common.collect.Lists;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiVariable;
import org.jetbrains.uast.UBinaryExpression;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UDoWhileExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UField;
import org.jetbrains.uast.UIfExpression;
import org.jetbrains.uast.ULocalVariable;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.UReturnExpression;
import org.jetbrains.uast.UUnaryExpression;
import org.jetbrains.uast.UVariable;
import org.jetbrains.uast.UWhileExpression;
import org.jetbrains.uast.UastCallKind;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Arrays;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
/**
* Checks for missing {@code recycle} calls on resources that encourage it, and
* for missing {@code commit} calls on FragmentTransactions, etc.
*/
public class CleanupDetector extends Detector implements UastScanner {
public class CleanupDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
CleanupDetector.class,
Scope.SOURCE_FILE_SCOPE);
Scope.JAVA_FILE_SCOPE);
/** Problems with missing recycle calls */
public static final Issue RECYCLE_RESOURCE = Issue.create(
@@ -74,6 +106,21 @@ public class CleanupDetector extends Detector implements UastScanner {
Severity.WARNING,
IMPLEMENTATION);
/** The main issue discovered by this detector */
public static final Issue SHARED_PREF = Issue.create(
"CommitPrefEdits", //$NON-NLS-1$
"Missing `commit()` on `SharedPreference` editor",
"After calling `edit()` on a `SharedPreference`, you must call `commit()` " +
"or `apply()` on the editor to save the results.",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
CleanupDetector.class,
Scope.JAVA_FILE_SCOPE));
// Target method names
private static final String RECYCLE = "recycle"; //$NON-NLS-1$
private static final String RELEASE = "release"; //$NON-NLS-1$
@@ -86,17 +133,17 @@ public class CleanupDetector extends Detector implements UastScanner {
private static final String OBTAIN_STYLED_ATTRIBUTES = "obtainStyledAttributes"; //$NON-NLS-1$
private static final String BEGIN_TRANSACTION = "beginTransaction"; //$NON-NLS-1$
private static final String COMMIT = "commit"; //$NON-NLS-1$
private static final String APPLY = "apply"; //$NON-NLS-1$
private static final String COMMIT_ALLOWING_LOSS = "commitAllowingStateLoss"; //$NON-NLS-1$
private static final String QUERY = "query"; //$NON-NLS-1$
private static final String RAW_QUERY = "rawQuery"; //$NON-NLS-1$
private static final String QUERY_WITH_FACTORY = "queryWithFactory"; //$NON-NLS-1$
private static final String RAW_QUERY_WITH_FACTORY = "rawQueryWithFactory"; //$NON-NLS-1$
private static final String CLOSE = "close"; //$NON-NLS-1$
private static final String EDIT = "edit"; //$NON-NLS-1$
private static final String MOTION_EVENT_CLS = "android.view.MotionEvent"; //$NON-NLS-1$
private static final String RESOURCES_CLS = "android.content.res.Resources"; //$NON-NLS-1$
private static final String PARCEL_CLS = "android.os.Parcel"; //$NON-NLS-1$
private static final String TYPED_ARRAY_CLS = "android.content.res.TypedArray"; //$NON-NLS-1$
private static final String VELOCITY_TRACKER_CLS = "android.view.VelocityTracker";//$NON-NLS-1$
private static final String DIALOG_FRAGMENT = "android.app.DialogFragment"; //$NON-NLS-1$
private static final String DIALOG_V4_FRAGMENT =
@@ -116,35 +163,43 @@ public class CleanupDetector extends Detector implements UastScanner {
= "android.content.ContentProviderClient";
public static final String CONTENT_RESOLVER_CLS = "android.content.ContentResolver";
public static final String CONTENT_PROVIDER_CLS = "android.content.ContentProvider";
@SuppressWarnings("SpellCheckingInspection")
public static final String SQLITE_DATABASE_CLS = "android.database.sqlite.SQLiteDatabase";
public static final String CURSOR_CLS = "android.database.Cursor";
public static final String ANDROID_CONTENT_SHARED_PREFERENCES =
"android.content.SharedPreferences"; //$NON-NLS-1$
private static final String ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR =
"android.content.SharedPreferences.Editor"; //$NON-NLS-1$
/** Constructs a new {@link CleanupDetector} */
public CleanupDetector() {
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Arrays.asList(
// FragmentManager commit check
BEGIN_TRANSACTION,
// FragmentManager commit check
BEGIN_TRANSACTION,
// Recycle check
OBTAIN, OBTAIN_NO_HISTORY,
OBTAIN_STYLED_ATTRIBUTES,
OBTAIN_ATTRIBUTES,
OBTAIN_TYPED_ARRAY,
// Recycle check
OBTAIN, OBTAIN_NO_HISTORY,
OBTAIN_STYLED_ATTRIBUTES,
OBTAIN_ATTRIBUTES,
OBTAIN_TYPED_ARRAY,
// Release check
ACQUIRE_CPC,
// Release check
ACQUIRE_CPC,
// Cursor close check
QUERY, RAW_QUERY, QUERY_WITH_FACTORY, RAW_QUERY_WITH_FACTORY
// Cursor close check
QUERY, RAW_QUERY, QUERY_WITH_FACTORY, RAW_QUERY_WITH_FACTORY,
// SharedPreferences check
EDIT
);
}
@@ -155,62 +210,70 @@ public class CleanupDetector extends Detector implements UastScanner {
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
if (node.matchesFunctionName(BEGIN_TRANSACTION)) {
checkTransactionCommits(context, node);
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
String name = method.getName();
if (BEGIN_TRANSACTION.equals(name)) {
checkTransactionCommits(context, call, method);
} else if (EDIT.equals(name)) {
checkEditorApplied(context, call, method);
} else {
checkResourceRecycled(context, node, node.getFunctionName());
checkResourceRecycled(context, call, method);
}
}
@Override
public void visitConstructor(UastAndroidContext context, UCallExpression functionCall, UFunction constructor) {
UClass containingClass = UastUtils.getContainingClass(constructor);
public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod constructor) {
PsiClass containingClass = constructor.getContainingClass();
if (containingClass != null) {
checkRecycled(context, functionCall, containingClass.getFqName(), RELEASE);
String type = containingClass.getQualifiedName();
if (type != null) {
checkRecycled(context, node, type, RELEASE);
}
}
}
private static void checkResourceRecycled(@NonNull UastAndroidContext context,
@NonNull UCallExpression node, @NonNull String name) {
private static void checkResourceRecycled(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod method) {
String name = method.getName();
// Recycle detector
UFunction method = node.resolve(context);
if (method == null) {
return;
}
UClass containingClass = UastUtils.getContainingClass(method);
PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
return;
}
JavaEvaluator evaluator = context.getEvaluator();
if ((OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) &&
containingClass.isSubclassOf(MOTION_EVENT_CLS)) {
evaluator.extendsClass(containingClass, MOTION_EVENT_CLS, false)) {
checkRecycled(context, node, MOTION_EVENT_CLS, RECYCLE);
} else if (OBTAIN.equals(name) && containingClass.isSubclassOf(PARCEL_CLS)) {
} else if (OBTAIN.equals(name) && evaluator.extendsClass(containingClass, PARCEL_CLS, false)) {
checkRecycled(context, node, PARCEL_CLS, RECYCLE);
} else if (OBTAIN.equals(name) &&
containingClass.isSubclassOf(VELOCITY_TRACKER_CLS)) {
evaluator.extendsClass(containingClass, VELOCITY_TRACKER_CLS, false)) {
checkRecycled(context, node, VELOCITY_TRACKER_CLS, RECYCLE);
} else if ((OBTAIN_STYLED_ATTRIBUTES.equals(name)
|| OBTAIN_ATTRIBUTES.equals(name)
|| OBTAIN_TYPED_ARRAY.equals(name)) &&
(containingClass.isSubclassOf(CLASS_CONTEXT) ||
containingClass.isSubclassOf(RESOURCES_CLS))) {
UType returnType = method.getReturnType();
if (returnType != null && returnType.matchesFqName(TYPED_ARRAY_CLS)) {
checkRecycled(context, node, TYPED_ARRAY_CLS, RECYCLE);
(evaluator.extendsClass(containingClass, CLASS_CONTEXT, false) ||
evaluator.extendsClass(containingClass, SdkConstants.CLASS_RESOURCES, false))) {
PsiType returnType = method.getReturnType();
if (returnType instanceof PsiClassType) {
PsiClass cls = ((PsiClassType)returnType).resolve();
if (cls != null && "android.content.res.TypedArray".equals(cls.getQualifiedName())) {
checkRecycled(context, node, "android.content.res.TypedArray", RECYCLE);
}
}
} else if (ACQUIRE_CPC.equals(name) && containingClass.isSubclassOf(
CONTENT_RESOLVER_CLS)) {
} else if (ACQUIRE_CPC.equals(name) && evaluator.extendsClass(containingClass,
CONTENT_RESOLVER_CLS, false)) {
checkRecycled(context, node, CONTENT_PROVIDER_CLIENT_CLS, RELEASE);
} else if ((QUERY.equals(name)
|| RAW_QUERY.equals(name)
|| QUERY_WITH_FACTORY.equals(name)
|| RAW_QUERY_WITH_FACTORY.equals(name))
&& (containingClass.isSubclassOf(SQLITE_DATABASE_CLS) ||
containingClass.isSubclassOf(CONTENT_RESOLVER_CLS) ||
containingClass.isSubclassOf(CONTENT_PROVIDER_CLS) ||
containingClass.isSubclassOf(CONTENT_PROVIDER_CLIENT_CLS))) {
&& (evaluator.extendsClass(containingClass, SQLITE_DATABASE_CLS, false) ||
evaluator.extendsClass(containingClass, CONTENT_RESOLVER_CLS, false) ||
evaluator.extendsClass(containingClass, CLASS_CONTENTPROVIDER, false) ||
evaluator.extendsClass(containingClass, CONTENT_PROVIDER_CLIENT_CLS, false))) {
// Other potential cursors-returning methods that should be tracked:
// android.app.DownloadManager#query
// android.content.ContentProviderClient#query
@@ -229,34 +292,33 @@ public class CleanupDetector extends Detector implements UastScanner {
}
}
private static void checkRecycled(@NonNull final UastAndroidContext context, @NonNull UElement node,
private static void checkRecycled(@NonNull final JavaContext context, @NonNull UCallExpression node,
@NonNull final String recycleType, @NonNull final String recycleName) {
UVariable boundVariable = getVariable(context, node);
PsiVariable boundVariable = getVariableElement(node);
if (boundVariable == null) {
return;
}
UFunction method = UastUtils.getContainingFunction(node);
UMethod method = getParentOfType(node, UMethod.class, true);
if (method == null) {
return;
}
FinishVisitor visitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull UCallExpression call) {
if (!call.matchesFunctionName(recycleName)) {
String methodName = call.getMethodName();
if (!recycleName.equals(methodName)) {
return false;
}
UDeclaration resolved = call.resolve(mContext);
if (resolved != null) {
UClass containingClass = UastUtils.getContainingClassOrEmpty(resolved);
if (containingClass.isSubclassOf(recycleType)) {
PsiMethod method = call.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
if (mContext.getEvaluator().extendsClass(containingClass, recycleType, false)) {
// Yes, called the right recycle() method; now make sure
// we're calling it on the right variable
UElement parent = call.getParent();
if (parent instanceof UQualifiedExpression) {
UExpression operand = ((UQualifiedExpression) parent).getReceiver();
resolved = UastUtils.resolveIfCan(operand, mContext);
UExpression operand = call.getReceiver();
if (operand instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) operand).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
@@ -283,46 +345,56 @@ public class CleanupDetector extends Detector implements UastScanner {
"This `%1$s` should be freed up after use with `#%2$s()`", className,
recycleName);
}
UElement locationNode = node instanceof UCallExpression ?
((UCallExpression) node).getFunctionNameElement() : node;
Location location = context.getLocation(locationNode);
((UCallExpression) node).getMethodIdentifier() : node;
if (locationNode == null) {
locationNode = node;
}
Location location = context.getUastLocation(locationNode);
context.report(RECYCLE_RESOURCE, node, location, message);
}
private static boolean checkTransactionCommits(@NonNull UastAndroidContext context,
@NonNull UCallExpression node) {
if (isBeginTransaction(context, node)) {
UVariable boundVariable = getVariable(context, node);
private static void checkTransactionCommits(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod calledMethod) {
if (isBeginTransaction(context, calledMethod)) {
PsiVariable boundVariable = getVariableElement(node, true);
if (boundVariable == null && isCommittedInChainedCalls(context, node)) {
return true;
return;
}
if (boundVariable != null) {
UFunction method = UastUtils.getContainingFunction(node);
UMethod method = getParentOfType(node, UMethod.class, true);
if (method == null) {
return true;
return;
}
FinishVisitor commitVisitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull UCallExpression call) {
if (isTransactionCommitMethodCall(mContext, call)) {
UExpression operand = UastUtils.getReceiver(call);
if (operand instanceof UResolvable) {
UDeclaration resolved = ((UResolvable) operand).resolve(mContext);
List<UExpression> chain = getQualifiedChain(getOutermostQualified(call));
if (chain.isEmpty()) {
return false;
}
UExpression operand = chain.get(0);
if (operand != null) {
PsiElement resolved = UastUtils.tryResolve(operand);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
} else if (resolved instanceof UFunction
&& operand instanceof UCallExpression
&& isCommittedInChainedCalls(mContext, (UCallExpression) operand)) {
} else if (resolved instanceof PsiMethod
&& operand instanceof UCallExpression
&& isCommittedInChainedCalls(mContext,
(UCallExpression) operand)) {
// Check that the target of the committed chains is the
// right variable!
while (operand instanceof UCallExpression) {
operand = UastUtils.getReceiver((UCallExpression)operand);
operand = ((UCallExpression) operand).getReceiver();
}
if (operand instanceof USimpleReferenceExpression) {
resolved = ((USimpleReferenceExpression)operand).resolve(mContext);
if (operand instanceof UReferenceExpression) {
resolved = ((UReferenceExpression) operand).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
@@ -334,7 +406,7 @@ public class CleanupDetector extends Detector implements UastScanner {
List<UExpression> arguments = call.getValueArguments();
if (arguments.size() == 2) {
UExpression first = arguments.get(0);
UDeclaration resolved = UastUtils.resolveIfCan(first, mContext);
PsiElement resolved = UastUtils.tryResolve(first);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
@@ -347,31 +419,29 @@ public class CleanupDetector extends Detector implements UastScanner {
method.accept(commitVisitor);
if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) {
return true;
return;
}
}
String message = "This transaction should be completed with a `commit()` call";
context.report(COMMIT_FRAGMENT, node, context.getLocation(node.getFunctionReference()),
message);
context.report(COMMIT_FRAGMENT, node, context.getUastNameLocation(node), message);
}
return false;
}
private static boolean isCommittedInChainedCalls(@NonNull UastAndroidContext context,
private static boolean isCommittedInChainedCalls(@NonNull JavaContext context,
@NonNull UCallExpression node) {
// Look for chained calls since the FragmentManager methods all return "this"
// to allow constructor chaining, e.g.
// getFragmentManager().beginTransaction().addToBackStack("test")
// .disallowAddToBackStack().hide(mFragment2).setBreadCrumbShortTitle("test")
// .show(mFragment2).setCustomAnimations(0, 0).commit();
List<UExpression> chain = UastUtils.getQualifiedChain(node);
if (chain.size() > 0) {
List<UExpression> chain = getQualifiedChain(getOutermostQualified(node));
if (!chain.isEmpty()) {
UExpression lastExpression = chain.get(chain.size() - 1);
if (lastExpression instanceof UCallExpression) {
UCallExpression methodInvocation = (UCallExpression) lastExpression;
if (isTransactionCommitMethodCall(context, methodInvocation)
|| isShowFragmentMethodCall(context, methodInvocation)) {
|| isShowFragmentMethodCall(context, methodInvocation)) {
return true;
}
}
@@ -380,80 +450,272 @@ public class CleanupDetector extends Detector implements UastScanner {
return false;
}
private static boolean isTransactionCommitMethodCall(@NonNull UastAndroidContext context,
private static boolean isTransactionCommitMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
return (call.matchesFunctionName(COMMIT) || call.matchesFunctionName(COMMIT_ALLOWING_LOSS)) &&
isMethodOnFragmentClass(context, call,
String methodName = call.getMethodName();
return (COMMIT.equals(methodName) || COMMIT_ALLOWING_LOSS.equals(methodName)) &&
isMethodOnFragmentClass(context, call,
FRAGMENT_TRANSACTION_CLS,
FRAGMENT_TRANSACTION_V4_CLS);
FRAGMENT_TRANSACTION_V4_CLS,
true);
}
private static boolean isShowFragmentMethodCall(@NonNull UastAndroidContext context,
private static boolean isShowFragmentMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
return call.matchesFunctionName(SHOW)
String methodName = call.getMethodName();
return SHOW.equals(methodName)
&& isMethodOnFragmentClass(context, call,
DIALOG_FRAGMENT, DIALOG_V4_FRAGMENT);
DIALOG_FRAGMENT, DIALOG_V4_FRAGMENT, true);
}
private static boolean isMethodOnFragmentClass(
@NonNull UastAndroidContext context,
@NonNull JavaContext context,
@NonNull UCallExpression call,
@NonNull String fragmentClass,
@NonNull String v4FragmentClass) {
UFunction resolved = call.resolve(context);
if (resolved != null) {
UClass containingClass = UastUtils.getContainingClassOrEmpty(resolved);
return containingClass.isSubclassOf(fragmentClass) ||
containingClass.isSubclassOf(v4FragmentClass);
@NonNull String v4FragmentClass,
boolean returnForUnresolved) {
PsiMethod method = call.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
return evaluator.extendsClass(containingClass, fragmentClass, false) ||
evaluator.extendsClass(containingClass, v4FragmentClass, false);
} else {
// If we *can't* resolve the method call, caller can decide
// whether to consider the method called or not
return returnForUnresolved;
}
}
private static void checkEditorApplied(@NonNull JavaContext context,
@NonNull UCallExpression node, @NonNull PsiMethod calledMethod) {
if (isSharedEditorCreation(context, calledMethod)) {
PsiVariable boundVariable = getVariableElement(node, true);
if (isEditorCommittedInChainedCalls(context, node)) {
return;
}
if (boundVariable != null) {
UMethod method = getParentOfType(node, UMethod.class, true);
if (method == null) {
return;
}
FinishVisitor commitVisitor = new FinishVisitor(context, boundVariable) {
@Override
protected boolean isCleanupCall(@NonNull UCallExpression call) {
if (isEditorApplyMethodCall(mContext, call)
|| isEditorCommitMethodCall(mContext, call)) {
UExpression operand = call.getReceiver();
if (operand != null) {
PsiElement resolved = UastUtils.tryResolve(operand);
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
} else if (resolved instanceof PsiMethod
&& operand instanceof UCallExpression
&& isCommittedInChainedCalls(mContext,
(UCallExpression) operand)) {
// Check that the target of the committed chains is the
// right variable!
while (operand instanceof UCallExpression) {
operand = ((UCallExpression)operand).getReceiver();
}
if (operand instanceof UReferenceExpression) {
resolved = ((UReferenceExpression) operand).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
return true;
}
}
}
}
}
return false;
}
};
method.accept(commitVisitor);
if (commitVisitor.isCleanedUp() || commitVisitor.variableEscapes()) {
return;
}
} else if (UastUtils.getParentOfType(node, UReturnExpression.class) != null) {
// Allocation is in a return statement
return;
}
String message = "`SharedPreferences.edit()` without a corresponding `commit()` or "
+ "`apply()` call";
context.report(SHARED_PREF, node, context.getUastLocation(node), message);
}
}
private static boolean isSharedEditorCreation(@NonNull JavaContext context,
@NonNull PsiMethod method) {
String methodName = method.getName();
if (EDIT.equals(methodName)) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
return evaluator.extendsClass(containingClass, ANDROID_CONTENT_SHARED_PREFERENCES,
false);
}
return false;
}
@Nullable
public static UVariable getVariable(@NonNull UastAndroidContext context,
@NonNull UElement expression) {
if (!(expression instanceof UExpression)) {
return null;
}
UQualifiedExpression topMostQualified = UastUtils.findOutermostQualifiedExpression(((UExpression) expression));
if (topMostQualified == null) {
return null;
}
UElement parent = topMostQualified.getParent();
if (parent instanceof UBinaryExpression) {
UBinaryExpression binaryExpression = (UBinaryExpression) parent;
if (binaryExpression.getOperator() == UastBinaryOperator.ASSIGN) {
UExpression lhs = binaryExpression.getLeftOperand();
if (lhs instanceof UResolvable) {
UDeclaration resolved = ((UResolvable) lhs).resolve(context);
if (resolved instanceof UVariable) {
return (UVariable) resolved;
}
private static boolean isEditorCommittedInChainedCalls(@NonNull JavaContext context,
@NonNull UCallExpression node) {
List<UExpression> chain = getQualifiedChain(getOutermostQualified(node));
if (!chain.isEmpty()) {
UExpression lastExpression = chain.get(chain.size() - 1);
if (lastExpression instanceof UCallExpression) {
UCallExpression methodInvocation = (UCallExpression) lastExpression;
if (isEditorCommitMethodCall(context, methodInvocation)
|| isEditorApplyMethodCall(context, methodInvocation)) {
return true;
}
}
} else if (parent instanceof UVariable) {
return (UVariable) parent;
}
return false;
}
private static boolean isEditorCommitMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
String methodName = call.getMethodName();
if (COMMIT.equals(methodName)) {
PsiMethod method = call.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.extendsClass(containingClass,
ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR, false)) {
suggestApplyIfApplicable(context, call);
return true;
}
}
}
return false;
}
private static boolean isEditorApplyMethodCall(@NonNull JavaContext context,
@NonNull UCallExpression call) {
String methodName = call.getMethodName();
if (APPLY.equals(methodName)) {
PsiMethod method = call.resolve();
if (method != null) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
return evaluator.extendsClass(containingClass,
ANDROID_CONTENT_SHARED_PREFERENCES_EDITOR, false);
}
}
return false;
}
private static void suggestApplyIfApplicable(@NonNull JavaContext context,
@NonNull UCallExpression node) {
if (context.getProject().getMinSdkVersion().getApiLevel() >= 9) {
// See if the return value is read: can only replace commit with
// apply if the return value is not considered
UElement qualifiedNode = node;
UElement parent = skipParentheses(node.getContainingElement());
while (parent instanceof UReferenceExpression) {
qualifiedNode = parent;
parent = skipParentheses(parent.getContainingElement());
}
boolean returnValueIgnored = true;
if (parent instanceof UCallExpression
|| parent instanceof UVariable
|| parent instanceof UBinaryExpression
|| parent instanceof UUnaryExpression
|| parent instanceof UReturnExpression) {
returnValueIgnored = false;
} else if (parent instanceof UIfExpression) {
UExpression condition = ((UIfExpression) parent).getCondition();
returnValueIgnored = !condition.equals(qualifiedNode);
} else if (parent instanceof UWhileExpression) {
UExpression condition = ((UWhileExpression) parent).getCondition();
returnValueIgnored = !condition.equals(qualifiedNode);
} else if (parent instanceof UDoWhileExpression) {
UExpression condition = ((UDoWhileExpression) parent).getCondition();
returnValueIgnored = !condition.equals(qualifiedNode);
}
if (returnValueIgnored) {
String message = "Consider using `apply()` instead; `commit` writes "
+ "its data to persistent storage immediately, whereas "
+ "`apply` will handle it in the background";
context.report(SHARED_PREF, node, context.getUastLocation(node), message);
}
}
}
/** Returns the variable the expression is assigned to, if any */
@Nullable
public static PsiVariable getVariableElement(@NonNull UCallExpression rhs) {
return getVariableElement(rhs, false);
}
@Nullable
public static PsiVariable getVariableElement(@NonNull UCallExpression rhs,
boolean allowChainedCalls) {
UElement parent = skipParentheses(
UastUtils.getQualifiedParentOrThis(rhs).getContainingElement());
// Handle some types of chained calls; e.g. you might have
// var = prefs.edit().put(key,value)
// and here we want to skip past the put call
if (allowChainedCalls) {
while (true) {
if ((parent instanceof UQualifiedReferenceExpression)) {
UElement parentParent = skipParentheses(parent.getContainingElement());
if ((parentParent instanceof UQualifiedReferenceExpression)) {
parent = skipParentheses(parentParent.getContainingElement());
} else if (parentParent instanceof UVariable
|| parentParent instanceof UBinaryExpression) {
parent = parentParent;
break;
} else {
break;
}
} else {
break;
}
}
}
if (UastExpressionUtils.isAssignment(parent)) {
UBinaryExpression assignment = (UBinaryExpression) parent;
assert assignment != null;
UExpression lhs = assignment.getLeftOperand();
if (lhs instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) lhs).resolve();
if (resolved instanceof PsiVariable && !(resolved instanceof PsiField)) {
// e.g. local variable, parameter - but not a field
return ((PsiVariable) resolved);
}
}
} else if (parent instanceof UVariable && !(parent instanceof UField)) {
return ((UVariable) parent).getPsi();
}
return null;
}
private static boolean isBeginTransaction(@NonNull UastAndroidContext context,
@NonNull UCallExpression node) {
assert node.matchesFunctionName(BEGIN_TRANSACTION) : node.renderString();
if (node.matchesFunctionName(BEGIN_TRANSACTION)) {
UFunction method = node.resolve(context);
if (method != null) {
UClass containingClass = UastUtils.getContainingClassOrEmpty(method);
if (containingClass.isSubclassOf(FRAGMENT_MANAGER_CLS)
|| containingClass.isSubclassOf(FRAGMENT_MANAGER_V4_CLS)) {
return true;
}
private static boolean isBeginTransaction(@NonNull JavaContext context, @NonNull PsiMethod method) {
String methodName = method.getName();
if (BEGIN_TRANSACTION.equals(methodName)) {
PsiClass containingClass = method.getContainingClass();
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.extendsClass(containingClass, FRAGMENT_MANAGER_CLS, false)
|| evaluator.extendsClass(containingClass, FRAGMENT_MANAGER_V4_CLS, false)) {
return true;
}
}
@@ -467,14 +729,17 @@ public class CleanupDetector extends Detector implements UastScanner {
* case of a database cursor we're looking for a "close" call, etc.
*/
private abstract static class FinishVisitor extends AbstractUastVisitor {
protected final UastAndroidContext mContext;
protected final List<UVariable> mVariables;
protected final JavaContext mContext;
protected final List<PsiVariable> mVariables;
private final PsiVariable mOriginalVariableNode;
private boolean mContainsCleanup;
private boolean mEscapes;
public FinishVisitor(UastAndroidContext context, @NonNull UVariable variable) {
public FinishVisitor(JavaContext context, @NonNull PsiVariable variableNode) {
mContext = context;
mVariables = Lists.newArrayList(variable);
mOriginalVariableNode = variableNode;
mVariables = Lists.newArrayList(variableNode);
}
public boolean isCleanedUp() {
@@ -486,37 +751,42 @@ public class CleanupDetector extends Detector implements UastScanner {
}
@Override
public boolean visitElement(@NotNull UElement node) {
public boolean visitElement(UElement node) {
return mContainsCleanup || super.visitElement(node);
}
protected abstract boolean isCleanupCall(@NonNull UCallExpression call);
@Override
public boolean visitCallExpression(@NotNull UCallExpression call) {
if (mContainsCleanup) {
return true;
public boolean visitCallExpression(UCallExpression node) {
if (node.getKind() == UastCallKind.METHOD_CALL) {
visitMethodCallExpression(node);
}
return super.visitCallExpression(node);
}
private void visitMethodCallExpression(UCallExpression call) {
if (mContainsCleanup) {
return;
}
// Look for escapes
if (!mEscapes) {
for (UExpression expression : call.getValueArguments()) {
if (expression instanceof USimpleReferenceExpression) {
UDeclaration resolved = ((USimpleReferenceExpression) expression).resolve(mContext);
if (expression instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) expression).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
boolean wasEscaped = mEscapes;
mEscapes = true;
// Special case: MotionEvent.obtain(MotionEvent): passing in an
// event here does not recycle the event, and we also know it
// doesn't escape
if (OBTAIN.equals(call.getFunctionName())) {
UFunction method = call.resolve(mContext);
if (method != null) {
UClass cls = UastUtils.getContainingClassOrEmpty(method);
if (cls.matchesFqName(MOTION_EVENT_CLS)) {
mEscapes = false;
}
if (OBTAIN.equals(call.getMethodName())) {
PsiMethod method = call.resolve();
if (JavaEvaluator.isMemberInClass(method, MOTION_EVENT_CLS)) {
mEscapes = wasEscaped;
}
}
}
@@ -526,66 +796,78 @@ public class CleanupDetector extends Detector implements UastScanner {
if (isCleanupCall(call)) {
mContainsCleanup = true;
return true;
} else {
return false;
}
}
@Override
public boolean visitVariable(@NotNull UVariable node) {
UExpression initializer = node.getInitializer();
if (initializer instanceof USimpleReferenceExpression) {
UDeclaration resolved = ((USimpleReferenceExpression) initializer).resolve(mContext);
public boolean visitVariable(UVariable variable) {
if (variable instanceof ULocalVariable) {
UExpression initializer = variable.getUastInitializer();
if (initializer instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) initializer).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
mVariables.add(variable.getPsi());
}
}
}
return super.visitVariable(variable);
}
@Override
public boolean visitBinaryExpression(UBinaryExpression expression) {
if (!UastExpressionUtils.isAssignment(expression)) {
return super.visitBinaryExpression(expression);
}
// TEMPORARILY DISABLED; see testDatabaseCursorReassignment
// This can result in some false positives right now. Play it
// safe instead.
boolean clearLhs = false;
UExpression rhs = expression.getRightOperand();
if (rhs instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) rhs).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
if (node.getKind() == UastVariableKind.LOCAL_VARIABLE) {
mVariables.add(node);
} else if (node.getKind() == UastVariableKind.MEMBER) {
clearLhs = false;
PsiElement lhs = UastUtils.tryResolve(expression.getLeftOperand());
if (lhs instanceof PsiLocalVariable) {
mVariables.add(((PsiLocalVariable) lhs));
} else if (lhs instanceof PsiField) {
mEscapes = true;
}
}
}
return false;
}
@Override
public boolean visitBinaryExpression(@NotNull UBinaryExpression node) {
if (node.getOperator() == UastBinaryOperator.ASSIGN) {
UExpression rhs = node.getRightOperand();
if (rhs instanceof USimpleReferenceExpression) {
UDeclaration resolved = ((USimpleReferenceExpression) rhs).resolve(mContext);
UExpression leftOperand = node.getLeftOperand();
//noinspection ConstantConditions
if (clearLhs) {
// If we reassign one of the variables, clear it out
PsiElement lhs = UastUtils.tryResolve(expression.getLeftOperand());
//noinspection SuspiciousMethodCalls
if (lhs != null && !lhs.equals(mOriginalVariableNode)
&& mVariables.contains(lhs)) {
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved) && leftOperand instanceof UResolvable) {
UDeclaration resolvedLhs = ((UResolvable) leftOperand).resolve(mContext);
if (resolvedLhs instanceof UVariable) {
UVariable variable = (UVariable) resolvedLhs;
if (variable.getKind() == UastVariableKind.LOCAL_VARIABLE) {
mVariables.add(variable);
} else if (variable.getKind() == UastVariableKind.MEMBER) {
mEscapes = true;
}
}
}
mVariables.remove(lhs);
}
}
return false;
return super.visitBinaryExpression(expression);
}
@Override
public boolean visitReturnExpression(@NotNull UReturnExpression node) {
UExpression value = node.getReturnExpression();
if (value instanceof USimpleReferenceExpression) {
UDeclaration resolved = ((USimpleReferenceExpression) value).resolve(mContext);
public boolean visitReturnExpression(UReturnExpression node) {
UExpression returnValue = node.getReturnExpression();
if (returnValue instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) returnValue).resolve();
//noinspection SuspiciousMethodCalls
if (resolved != null && mVariables.contains(resolved)) {
mEscapes = true;
}
}
return false;
return super.visitReturnExpression(node);
}
}
}
@@ -17,28 +17,33 @@
package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteralExpression;
import java.io.File;
import org.jetbrains.uast.check.UastScanner;
import java.util.Collections;
import java.util.List;
/**
* Looks for issues in Java comments
*/
public class CommentDetector extends Detector implements UastScanner {
public class CommentDetector extends Detector implements JavaPsiScanner {
private static final String STOPSHIP_COMMENT = "STOPSHIP"; //$NON-NLS-1$
private static final Implementation IMPLEMENTATION = new Implementation(
CommentDetector.class,
Scope.SOURCE_FILE_SCOPE);
Scope.JAVA_FILE_SCOPE);
/** Looks for hidden code */
public static final Issue EASTER_EGG = Issue.create(
@@ -69,7 +74,7 @@ public class CommentDetector extends Detector implements UastScanner {
private static final String ESCAPE_STRING = "\\u002a\\u002f"; //$NON-NLS-1$
/** Lombok's AST only passes comment nodes for Javadoc so I need to do manual token scanning
/** The current AST only passes comment nodes for Javadoc so I need to do manual token scanning
instead */
private static final boolean USE_AST = false;
@@ -79,27 +84,17 @@ public class CommentDetector extends Detector implements UastScanner {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
/*@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
if (USE_AST) {
return Collections.<Class<? extends Node>>singletonList(Comment.class);
return Collections.<Class<? extends PsiElement>>singletonList(
PsiLiteralExpression.class);
} else {
return null;
}
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
public JavaElementVisitor createPsiVisitor(@NonNull JavaContext context) {
// Lombok does not generate comment nodes for block and line comments, only for
// javadoc comments!
if (USE_AST) {
@@ -124,15 +119,15 @@ public class CommentDetector extends Detector implements UastScanner {
if (end == -1) {
end = n;
}
checkComment(context, source, 0, start, end);
checkComment(context, null, source, 0, start, end);
} else if (next == '*') {
// Block comment
int start = i + 2;
int end = source.indexOf("*//*", start);
int end = source.indexOf("*/", start);
if (end == -1) {
end = n;
}
checkComment(context, source, 0, start, end);
checkComment(context, null, source, 0, start, end);
}
}
}
@@ -140,7 +135,7 @@ public class CommentDetector extends Detector implements UastScanner {
}
}
private static class CommentChecker extends ForwardingAstVisitor {
private static class CommentChecker extends JavaElementVisitor {
private final JavaContext mContext;
public CommentChecker(JavaContext context) {
@@ -148,15 +143,16 @@ public class CommentDetector extends Detector implements UastScanner {
}
@Override
public boolean visitComment(Comment node) {
String contents = node.astContent();
checkComment(mContext, contents, node.getPosition().getStart(), 0, contents.length());
return super.visitComment(node);
public void visitComment(PsiComment comment) {
String contents = comment.getText();
checkComment(mContext, comment, contents, comment.getTextRange().getStartOffset(), 0,
contents.length());
}
}
private static void checkComment(
@NonNull Context context,
@NonNull JavaContext context,
@Nullable PsiComment node,
@NonNull String source,
int offset,
int start,
@@ -171,7 +167,7 @@ public class CommentDetector extends Detector implements UastScanner {
0, ESCAPE_STRING.length())) {
Location location = Location.create(context.file, source,
offset + i - 1, offset + i - 1 + ESCAPE_STRING.length());
context.report(EASTER_EGG, location,
context.report(EASTER_EGG, node, location,
"Code might be hidden here; found unicode escape sequence " +
"which is interpreted as comment end, compiled code follows");
}
@@ -183,10 +179,10 @@ public class CommentDetector extends Detector implements UastScanner {
// TODO: Only flag this issue in release mode??
Location location = Location.create(context.file, source,
offset + i - 1, offset + i - 1 + STOPSHIP_COMMENT.length());
context.report(STOP_SHIP, location,
context.report(STOP_SHIP, node, location,
"`STOPSHIP` comment found; points to code which must be fixed prior " +
"to release");
}
}
}*/
}
}
@@ -21,24 +21,24 @@ import com.android.annotations.Nullable;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.jetbrains.org.objectweb.asm.Opcodes;
import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode;
import org.jetbrains.org.objectweb.asm.tree.ClassNode;
import org.jetbrains.org.objectweb.asm.tree.FieldInsnNode;
import org.jetbrains.org.objectweb.asm.tree.FrameNode;
import org.jetbrains.org.objectweb.asm.tree.InsnList;
import org.jetbrains.org.objectweb.asm.tree.IntInsnNode;
import org.jetbrains.org.objectweb.asm.tree.JumpInsnNode;
import org.jetbrains.org.objectweb.asm.tree.LabelNode;
import org.jetbrains.org.objectweb.asm.tree.LdcInsnNode;
import org.jetbrains.org.objectweb.asm.tree.LineNumberNode;
import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode;
import org.jetbrains.org.objectweb.asm.tree.MethodNode;
import org.jetbrains.org.objectweb.asm.tree.TryCatchBlockNode;
import org.jetbrains.org.objectweb.asm.tree.TypeInsnNode;
import org.jetbrains.org.objectweb.asm.tree.analysis.Analyzer;
import org.jetbrains.org.objectweb.asm.tree.analysis.AnalyzerException;
import org.jetbrains.org.objectweb.asm.tree.analysis.BasicInterpreter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FrameNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -46,7 +46,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
//import org.jetbrains.org.objectweb.asm.util.Printer;
//import org.objectweb.asm.util.Printer;
/**
* A {@linkplain ControlFlowGraph} is a graph containing a node for each
@@ -284,7 +284,10 @@ public class ControlFlowGraph {
AbstractInsnNode curr = start;
Node handlerNode = getNode(tcb.handler);
while (curr != end && curr != null) {
if (curr.getType() == AbstractInsnNode.METHOD_INSN) {
// A method can throw can exception, or a throw instruction directly
if (curr.getType() == AbstractInsnNode.METHOD_INSN
|| (curr.getType() == AbstractInsnNode.INSN
&& curr.getOpcode() == Opcodes.ATHROW)) {
// Method call; add exception edge to handler
if (tcb.type == null) {
// finally block: not an exception path
@@ -388,6 +391,7 @@ public class ControlFlowGraph {
* @return a dot description of this control flow graph,
* useful for debugging
*/
@SuppressWarnings("unused")
public String toDot(@Nullable Set<Node> highlight) {
StringBuilder sb = new StringBuilder();
sb.append("digraph G {\n");
@@ -16,38 +16,44 @@
package com.android.tools.klint.checks;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.SdkConstants.CLASS_VIEW;
import static com.android.SdkConstants.CLASS_VIEWGROUP;
import static com.android.SdkConstants.DOT_LAYOUT_PARAMS;
import static com.android.SdkConstants.R_STYLEABLE_PREFIX;
import static com.android.tools.klint.detector.api.LintUtils.skipParentheses;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.intellij.psi.PsiClass;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.visitor.UastVisitor;
import java.io.File;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
/**
* Makes sure that custom views use a declare styleable that matches
* the name of the custom view
*/
public class CustomViewDetector extends Detector implements UastScanner {
public class CustomViewDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
CustomViewDetector.class,
Scope.SOURCE_FILE_SCOPE);
Scope.JAVA_FILE_SCOPE);
/** Mismatched style and class names */
public static final Issue ISSUE = Issue.create(
@@ -72,74 +78,62 @@ public class CustomViewDetector extends Detector implements UastScanner {
public CustomViewDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Collections.singletonList(OBTAIN_STYLED_ATTRIBUTES);
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
if (node.getParent() instanceof UExpression) {
if (!context.getLintContext().isContextMethod(node)) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
if (skipParentheses(node.getContainingElement()) instanceof UExpression) {
if (!context.getEvaluator().isMemberInSubClassOf(method, CLASS_CONTEXT, false)) {
return;
}
List<UExpression> expressions = node.getValueArguments();
int size = expressions.size();
List<UExpression> arguments = node.getValueArguments();
int size = arguments.size();
// Which parameter contains the styleable (attrs) ?
int parameterIndex;
if (size == 1) {
// obtainStyledAttributes(int[] attrs)
parameterIndex = 0;
} else if (size > 1) {
} else {
// obtainStyledAttributes(int resid, int[] attrs)
// obtainStyledAttributes(AttributeSet set, int[] attrs)
// obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
parameterIndex = 1;
} else {
}
UExpression expression = arguments.get(parameterIndex);
if (!UastUtils.startsWithQualified(expression, R_STYLEABLE_PREFIX)) {
return;
}
UExpression expression = expressions.get(parameterIndex);
if (!(expression instanceof UQualifiedExpression)) {
List<String> path = UastUtils.asQualifiedPath(expression);
if (path == null || path.size() < 3) {
return;
}
String s = expression.renderString();
if (!s.startsWith(R_STYLEABLE_PREFIX)) {
return;
}
String styleableName = s.substring(R_STYLEABLE_PREFIX.length());
UClass resolvedClass = UastUtils.getContainingClass(node);
if (resolvedClass == null) {
String styleableName = path.get(2);
UClass cls = UastUtils.getParentOfType(node, UClass.class, false);
if (cls == null) {
return;
}
String className = resolvedClass.getName();
if (resolvedClass.isSubclassOf(CLASS_VIEW)) {
String className = cls.getName();
if (context.getEvaluator().extendsClass(cls, CLASS_VIEW, false)) {
if (!styleableName.equals(className)) {
String message = String.format(
"By convention, the custom view (`%1$s`) and the declare-styleable (`%2$s`) "
+ "should have the same name (various editor features rely on "
+ "this convention)",
className, styleableName);
context.report(ISSUE, node, context.getLocation(expression), message);
"By convention, the custom view (`%1$s`) and the declare-styleable (`%2$s`) "
+ "should have the same name (various editor features rely on "
+ "this convention)",
className, styleableName);
context.report(ISSUE, node, context.getUastLocation(expression), message);
}
} else if (resolvedClass.isSubclassOf(CLASS_VIEWGROUP + DOT_LAYOUT_PARAMS)) {
UClass outer = UastUtils.getContainingClass(resolvedClass);
} else if (context.getEvaluator().extendsClass(cls,
CLASS_VIEWGROUP + DOT_LAYOUT_PARAMS, false)) {
PsiClass outer = PsiTreeUtil.getParentOfType(cls, PsiClass.class, true);
if (outer == null) {
return;
}
@@ -147,12 +141,12 @@ public class CustomViewDetector extends Detector implements UastScanner {
String expectedName = layoutClassName + "_Layout";
if (!styleableName.equals(expectedName)) {
String message = String.format(
"By convention, the declare-styleable (`%1$s`) for a layout parameter "
+ "class (`%2$s`) is expected to be the surrounding "
+ "class (`%3$s`) plus \"`_Layout`\", e.g. `%4$s`. "
+ "(Various editor features rely on this convention.)",
styleableName, className, layoutClassName, expectedName);
context.report(ISSUE, node, context.getLocation(expression), message);
"By convention, the declare-styleable (`%1$s`) for a layout parameter "
+ "class (`%2$s`) is expected to be the surrounding "
+ "class (`%3$s`) plus \"`_Layout`\", e.g. `%4$s`. "
+ "(Various editor features rely on this convention.)",
styleableName, className, layoutClassName, expectedName);
context.report(ISSUE, node, context.getUastLocation(expression), message);
}
}
}
@@ -17,34 +17,51 @@
package com.android.tools.klint.checks;
import static com.android.SdkConstants.RESOURCE_CLZ_ID;
import static com.android.tools.klint.detector.api.LintUtils.skipParentheses;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.google.common.collect.Maps;
import com.intellij.psi.PsiBreakStatement;
import com.intellij.psi.PsiContinueStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiIfStatement;
import com.intellij.psi.PsiJavaToken;
import com.intellij.psi.PsiLoopStatement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiReturnStatement;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiWhiteSpace;
import org.jetbrains.uast.UArrayAccessExpression;
import org.jetbrains.uast.UBinaryExpression;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.ULocalVariable;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.UastVisitor;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
/**
* Detector looking for cut & paste issues
* Detector looking for cut &amp; paste issues
*/
public class CutPasteDetector extends Detector implements UastScanner {
public class CutPasteDetector extends Detector implements Detector.UastScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"CutPasteId", //$NON-NLS-1$
@@ -62,9 +79,9 @@ public class CutPasteDetector extends Detector implements UastScanner {
Severity.WARNING,
new Implementation(
CutPasteDetector.class,
Scope.SOURCE_FILE_SCOPE));
Scope.JAVA_FILE_SCOPE));
private UFunction mLastMethod;
private PsiMethod mLastMethod;
private Map<String, UCallExpression> mIds;
private Map<String, String> mLhs;
private Map<String, String> mCallOperands;
@@ -73,50 +90,49 @@ public class CutPasteDetector extends Detector implements UastScanner {
public CutPasteDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
// ---- Implements UastScanner ----
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Collections.singletonList("findViewById"); //$NON-NLS-1$
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
String lhs = getLhs(node);
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod calledMethod) {
String lhs = getLhs(call);
if (lhs == null) {
return;
}
UFunction method = UastUtils.getContainingFunction(node);
UMethod method = UastUtils.getParentOfType(call, UMethod.class, false);
if (method == null) {
return;
return; // prevent doing the same work for multiple findViewById calls in same method
} else if (method != mLastMethod) {
mIds = Maps.newHashMap();
mLhs = Maps.newHashMap();
mCallOperands = Maps.newHashMap();
mLastMethod = method;
}
String callOperand = call.getReceiver() != null
? call.getReceiver().asSourceString() : "";
UElement parent = node.getParent();
String callOperand = "";
if (parent instanceof UQualifiedExpression) {
callOperand = ((UQualifiedExpression)parent).getReceiver().renderString();
List<UExpression> arguments = call.getValueArguments();
if (arguments.isEmpty()) {
return;
}
UExpression first = node.getValueArguments().get(0);
if (first instanceof UQualifiedExpression) {
UQualifiedExpression select = (UQualifiedExpression) first;
String id = select.getSelector().renderString();
UExpression operand = select.getReceiver();
if (operand instanceof UQualifiedExpression) {
UQualifiedExpression type = (UQualifiedExpression) operand;
if (type.getSelector().renderString().equals(RESOURCE_CLZ_ID)) {
UExpression first = arguments.get(0);
if (first instanceof UReferenceExpression) {
UReferenceExpression psiReferenceExpression = (UReferenceExpression) first;
String id = psiReferenceExpression.getResolvedName();
UElement operand = (first instanceof UQualifiedReferenceExpression)
? ((UQualifiedReferenceExpression) first).getReceiver()
: null;
if (operand instanceof UReferenceExpression) {
UReferenceExpression type = (UReferenceExpression) operand;
if (RESOURCE_CLZ_ID.equals(type.getResolvedName())) {
if (mIds.containsKey(id)) {
if (lhs.equals(mLhs.get(id))) {
return;
@@ -125,113 +141,161 @@ public class CutPasteDetector extends Detector implements UastScanner {
return;
}
UCallExpression earlierCall = mIds.get(id);
if (!isReachableFrom(method, earlierCall, node)) {
if (!isReachableFrom(method, earlierCall, call)) {
return;
}
Location location = context.getLocation(node);
Location secondary = context.getLocation(earlierCall);
if (location != null && secondary != null) {
secondary.setMessage("First usage here");
location.setSecondary(secondary);
context.report(ISSUE, node, location, String.format(
"The id `%1$s` has already been looked up in this method; possible " +
"cut & paste error?", first.toString()));
}
Location location = context.getUastLocation(call);
Location secondary = context.getUastLocation(earlierCall);
secondary.setMessage("First usage here");
location.setSecondary(secondary);
context.report(ISSUE, call, location, String.format(
"The id `%1$s` has already been looked up in this method; possible "
+
"cut & paste error?", first.asSourceString()));
} else {
mIds.put(id, node);
mIds.put(id, call);
mLhs.put(id, lhs);
mCallOperands.put(id, callOperand);
}
}
}
}
}
@Nullable
private static String getLhs(@NonNull UCallExpression call) {
UElement parent = call.getParent();
if (UastBinaryExpressionWithTypeUtils.isTypeCast(parent)) {
assert parent != null;
parent = parent.getParent();
UElement parent = skipParentheses(call.getContainingElement());
if (parent != null && UastExpressionUtils.isTypeCast(parent)) {
parent = parent.getContainingElement();
}
if (parent instanceof UVariable) {
UVariable vde = (UVariable) parent;
return vde.getName();
if (parent instanceof ULocalVariable) {
return ((ULocalVariable) parent).getName();
} else if (parent instanceof UBinaryExpression) {
UBinaryExpression be = (UBinaryExpression) parent;
UExpression left = be.getLeftOperand();
if (left instanceof USimpleReferenceExpression || left instanceof UQualifiedExpression) {
return be.getLeftOperand().toString();
if (left instanceof UReferenceExpression) {
return left.asRenderString();
} else if (left instanceof UArrayAccessExpression) {
UArrayAccessExpression aa = (UArrayAccessExpression) left;
return aa.getReceiver().toString();
return aa.getReceiver().asSourceString();
}
} else if (UastExpressionUtils.isAssignment(parent)) {
//noinspection ConstantConditions
UExpression left = ((UBinaryExpression) parent).getLeftOperand();
if (left instanceof UReferenceExpression) {
return left.asSourceString();
} else if (left instanceof UArrayAccessExpression) {
UArrayAccessExpression aa = (UArrayAccessExpression) left;
return aa.getReceiver().asSourceString();
}
}
return null;
}
private static boolean isReachableFrom(
@NonNull UElement method,
@NonNull UCallExpression from,
@NonNull UCallExpression to) {
ReachableVisitor visitor = new ReachableVisitor(from, to);
method.accept(visitor);
return visitor.isReachable();
static boolean isReachableFrom(
@NonNull PsiMethod method,
@NonNull PsiElement from,
@NonNull PsiElement to) {
PsiElement prev = from;
PsiElement curr = next(method, from, to, null);
//noinspection ConstantConditions
while (curr != null) {
if (containsElement(method, curr, to)) {
return true;
}
curr = next(method, curr, to, prev);
prev = curr;
}
return false;
}
private static class ReachableVisitor extends AbstractUastVisitor {
@NonNull private final UCallExpression mFrom;
@NonNull private final UCallExpression mTo;
private boolean mReachable;
private boolean mSeenEnd;
static boolean isReachableFrom(
@NonNull PsiMethod method,
@NonNull UElement from,
@NonNull UElement to) {
//TODO
return false;
}
public ReachableVisitor(@NonNull UCallExpression from, @NonNull UCallExpression to) {
mFrom = from;
mTo = to;
@Nullable
static PsiElement next(
@NonNull PsiMethod method,
@NonNull PsiElement curr,
@NonNull PsiElement target,
@Nullable PsiElement prev) {
if (curr instanceof PsiMethod) {
return null;
}
boolean isReachable() {
return mReachable;
}
@Override
public boolean visitCallExpression(@NotNull UCallExpression node) {
if (node == mFrom) {
mReachable = true;
} else if (node == mTo) {
mSeenEnd = true;
PsiElement parent = curr.getParent();
if (curr instanceof PsiContinueStatement) {
PsiStatement continuedStatement = ((PsiContinueStatement) curr)
.findContinuedStatement();
if (continuedStatement != null) {
if (containsElement(method, continuedStatement, target)) {
return target;
}
return next(method, continuedStatement, target, curr);
} else {
return next(method, parent, target, curr);
}
} else if (curr instanceof PsiBreakStatement) {
PsiStatement exitedStatement = ((PsiBreakStatement) curr).findExitedStatement();
if (exitedStatement != null) {
return next(method, exitedStatement, target, curr);
} else {
return next(method, parent, target, curr);
}
} else if (curr instanceof PsiReturnStatement) {
return null;
} else if (curr instanceof PsiLoopStatement && prev != null &&
containsElement(method, curr, prev)) {
// If we stepped *up* (from a last child nested in the loop) up to the loop
// itself, mark all children in the loop as reachable since we're iterating
if (containsElement(method, curr, target)) {
return target;
}
return false;
}
@Override
public boolean visitIfExpression(@NotNull UIfExpression node) {
UExpression condition = node.getCondition();
UExpression body = node.getThenBranch();
UElement elseBody = node.getElseBranch();
condition.accept(this);
PsiElement sibling = curr.getNextSibling();
while (sibling instanceof PsiWhiteSpace || sibling instanceof PsiJavaToken) {
// Skip whitespaces and tokens such as PsiJavaToken.SEMICOLON etc
sibling = sibling.getNextSibling();
}
if (sibling == null) {
return next(method, parent, target, curr);
}
if (body != null) {
boolean wasReachable = mReachable;
body.accept(this);
mReachable = wasReachable;
if (parent instanceof PsiIfStatement &&
curr == ((PsiIfStatement)parent).getThenBranch()) {
return next(method, parent, target, curr);
} else if (parent instanceof PsiLoopStatement) {
if (containsElement(method, parent, target)) {
return target;
}
if (elseBody != null) {
boolean wasReachable = mReachable;
elseBody.accept(this);
mReachable = wasReachable;
}
return sibling;
}
private static boolean containsElement(
@NonNull PsiMethod method,
@NonNull PsiElement root,
@NonNull PsiElement element) {
//noinspection ConstantConditions
while (element != null && element != method) {
if (root.equals(element)) {
return true;
}
return false;
element = element.getParent();
}
@Override
public boolean visitElement(@NotNull UElement node) {
return mSeenEnd;
}
return false;
}
}
@@ -22,28 +22,30 @@ import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiType;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.UFunction;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UType;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
/**
* Checks for errors related to Date Formats
*/
public class DateFormatDetector extends Detector implements UastScanner {
public class DateFormatDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
DateFormatDetector.class,
Scope.SOURCE_FILE_SCOPE);
Scope.JAVA_FILE_SCOPE);
/** Constructing SimpleDateFormat without an explicit locale */
public static final Issue DATE_FORMAT = Issue.create(
@@ -75,12 +77,6 @@ public class DateFormatDetector extends Detector implements UastScanner {
public DateFormatDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Nullable
@@ -90,24 +86,28 @@ public class DateFormatDetector extends Detector implements UastScanner {
}
@Override
public void visitConstructor(UastAndroidContext context, UCallExpression functionCall, UFunction constructor) {
public void visitConstructor(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod constructor) {
if (!specifiesLocale(constructor)) {
Location location = context.getLocation(functionCall);
Location location = context.getUastLocation(node);
String message =
"To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, " +
"or `getTimeInstance()`, or use `new SimpleDateFormat(String template, " +
"Locale locale)` with for example `Locale.US` for ASCII dates.";
context.report(DATE_FORMAT, functionCall, location, message);
"To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, " +
"or `getTimeInstance()`, or use `new SimpleDateFormat(String template, " +
"Locale locale)` with for example `Locale.US` for ASCII dates.";
context.report(DATE_FORMAT, node, location, message);
}
}
private static boolean specifiesLocale(@NonNull UFunction method) {
for (int i = 0, n = method.getValueParameterCount(); i < n; i++) {
UType parameterType = method.getValueParameters().get(i).getType();
if (parameterType.matchesFqName(LOCALE_CLS)) {
return true;
private static boolean specifiesLocale(@NonNull PsiMethod method) {
PsiParameterList parameterList = method.getParameterList();
PsiParameter[] parameters = parameterList.getParameters();
for (PsiParameter parameter : parameters) {
PsiType type = parameter.getType();
if (type.getCanonicalText().equals(LOCALE_CLS)) {
return true;
}
}
return false;
}
}
@@ -20,22 +20,27 @@ import static com.android.SdkConstants.CLASS_FRAGMENT;
import static com.android.SdkConstants.CLASS_V4_FRAGMENT;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import org.jetbrains.uast.UAnonymousClass;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import java.util.Arrays;
import java.util.List;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
/**
* Checks that Fragment subclasses can be instantiated via
* {link {@link Class#newInstance()}}: the class is public, static, and has
@@ -45,7 +50,7 @@ import org.jetbrains.uast.check.UastScanner;
* http://stackoverflow.com/questions/8058809/fragment-activity-crashes-on-screen-rotate
* (and countless duplicates)
*/
public class FragmentDetector extends Detector implements UastScanner {
public class FragmentDetector extends Detector implements Detector.UastScanner {
/** Are fragment subclasses instantiatable? */
public static final Issue ISSUE = Issue.create(
"ValidFragment", //$NON-NLS-1$
@@ -64,7 +69,7 @@ public class FragmentDetector extends Detector implements UastScanner {
Severity.FATAL,
new Implementation(
FragmentDetector.class,
Scope.SOURCE_FILE_SCOPE)
Scope.JAVA_FILE_SCOPE)
).addMoreInfo(
"http://developer.android.com/reference/android/app/Fragment.html#Fragment()"); //$NON-NLS-1$
@@ -73,83 +78,74 @@ public class FragmentDetector extends Detector implements UastScanner {
public FragmentDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableSuperClasses() {
public List<String> applicableSuperClasses() {
return Arrays.asList(CLASS_FRAGMENT, CLASS_V4_FRAGMENT);
}
private boolean isAllParametersWithDefaultValue(UFunction function) {
List<UVariable> parameters = function.getValueParameters();
for (UVariable parameter : parameters) {
if (parameter.getInitializer() == null) {
return false;
}
}
return true;
}
@Override
public void visitClass(UastAndroidContext context, UClass cls) {
if (cls.hasModifier(UastModifier.ABSTRACT) || cls.getKind() != UastClassKind.CLASS) {
public void checkClass(@NonNull JavaContext context, @NonNull UClass node) {
if (node instanceof UAnonymousClass) {
String message = "Fragments should be static such that they can be re-instantiated by " +
"the system, and anonymous classes are not static";
context.reportUast(ISSUE, node, context.getUastNameLocation(node), message);
return;
}
if (!cls.getVisibility().isPublic()) {
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.isAbstract(node)) {
return;
}
if (!evaluator.isPublic(node)) {
String message = String.format("This fragment class should be public (%1$s)",
cls.getFqName());
context.report(ISSUE, cls, context.getLocation(cls.getNameElement()), message);
node.getQualifiedName());
context.reportUast(ISSUE, node, context.getUastNameLocation(node), message);
return;
}
if (UastUtils.getContainingClass(cls) != null && !cls.hasModifier(UastModifier.STATIC)) {
if (node.getContainingClass() != null && !evaluator.isStatic(node)) {
String message = String.format(
"This fragment inner class should be static (%1$s)", cls.getName());
context.report(ISSUE, cls, context.getLocation(cls.getNameElement()), message);
"This fragment inner class should be static (%1$s)", node.getQualifiedName());
context.reportUast(ISSUE, node, context.getUastNameLocation(node), message);
return;
}
boolean hasDefaultConstructor = false;
boolean hasConstructor = false;
for (UFunction constructor : cls.getConstructors()) {
for (PsiMethod constructor : node.getConstructors()) {
hasConstructor = true;
if (constructor.getValueParameterCount() == 0 || isAllParametersWithDefaultValue(constructor)) {
if (constructor.getVisibility().isPublic()) {
if (constructor.getParameterList().getParametersCount() == 0) {
if (evaluator.isPublic(constructor)) {
hasDefaultConstructor = true;
} else {
Location location = context.getLocation(constructor.getNameElement());
Location location = context.getNameLocation(constructor);
context.report(ISSUE, constructor, location,
"The default constructor must be public");
"The default constructor must be public");
// Also mark that we have a constructor so we don't complain again
// below since we've already emitted a more specific error related
// to the default constructor
hasDefaultConstructor = true;
}
} else {
Location location = context.getLocation(constructor.getNameElement());
Location location = context.getNameLocation(constructor);
// TODO: Use separate issue for this which isn't an error
String message = "Avoid non-default constructors in fragments: "
+ "use a default constructor plus "
+ "`Fragment#setArguments(Bundle)` instead";
+ "use a default constructor plus "
+ "`Fragment#setArguments(Bundle)` instead";
context.report(ISSUE, constructor, location, message);
}
}
if (!hasDefaultConstructor && hasConstructor) {
String message = String.format(
"This fragment should provide a default constructor (a public " +
"constructor with no arguments) (`%1$s`)",
cls.getName());
context.report(ISSUE, cls, context.getLocation(cls.getNameElement()), message);
"This fragment should provide a default constructor (a public " +
"constructor with no arguments) (`%1$s`)",
node.getQualifiedName());
context.reportUast(ISSUE, node, context.getNameLocation(node), message);
}
}
}
@@ -16,120 +16,84 @@
package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.JavaParser;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
public class GetSignaturesDetector extends Detector implements UastScanner {
public class GetSignaturesDetector extends Detector implements Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"PackageManagerGetSignatures", //$NON-NLS-1$
"Potential Multiple Certificate Exploit",
"Improper validation of app signatures could lead to issues where a malicious app " +
"submits itself to the Play Store with both its real certificate and a fake " +
"certificate and gains access to functionality or information it shouldn't " +
"have due to another application only checking for the fake certificate and " +
"ignoring the rest. Please make sure to validate all signatures returned " +
"by this method.",
"submits itself to the Play Store with both its real certificate and a fake " +
"certificate and gains access to functionality or information it shouldn't " +
"have due to another application only checking for the fake certificate and " +
"ignoring the rest. Please make sure to validate all signatures returned " +
"by this method.",
Category.SECURITY,
8,
Severity.INFORMATIONAL,
new Implementation(
GetSignaturesDetector.class,
Scope.SOURCE_FILE_SCOPE))
Scope.JAVA_FILE_SCOPE))
.addMoreInfo("https://bluebox.com/technical/android-fake-id-vulnerability/");
private static final String PACKAGE_MANAGER_CLASS = "android.content.pm.PackageManager"; //$NON-NLS-1$
private static final String GET_PACKAGE_INFO = "getPackageInfo"; //$NON-NLS-1$
private static final int GET_SIGNATURES_FLAG = 0x00000040; //$NON-NLS-1$
// ---- Implements UastScanner ----
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableFunctionNames() {
@Nullable
public List<String> getApplicableMethodNames() {
return Collections.singletonList(GET_PACKAGE_INFO);
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
UFunction resolved = node.resolve(context);
if (resolved == null ||
!UastUtils.getContainingClassOrEmpty(resolved).isSubclassOf(PACKAGE_MANAGER_CLASS)) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
if (!evaluator.methodMatches(method, PACKAGE_MANAGER_CLASS, true,
JavaParser.TYPE_STRING,
JavaParser.TYPE_INT)) {
return;
}
List<UExpression> argumentList = node.getValueArguments();
// Ignore if the method doesn't fit our description.
if (argumentList.size() == 2) {
UType firstParameterType = argumentList.get(0).getExpressionType();
if (firstParameterType != null
&& firstParameterType.matchesFqName(JavaParser.TYPE_STRING)) {
maybeReportIssue(calculateValue(context, argumentList.get(1)), context, node);
}
List<UExpression> arguments = node.getValueArguments();
UExpression second = arguments.get(1);
Object number = ConstantEvaluator.evaluate(context, second);
if (number instanceof Number) {
int flagValue = ((Number)number).intValue();
maybeReportIssue(flagValue, context, node, second);
}
}
private static void maybeReportIssue(
int flagValue, UastAndroidContext context, UCallExpression node) {
int flagValue, JavaContext context, UCallExpression node,
UExpression last) {
if ((flagValue & GET_SIGNATURES_FLAG) != 0) {
context.report(ISSUE, node, context.getLocation(node.getValueArguments().get(1)),
context.report(ISSUE, node, context.getUastLocation(last),
"Reading app signatures from getPackageInfo: The app signatures "
+ "could be exploited if not validated properly; "
+ "see issue explanation for details.");
}
}
private static int calculateValue(UastAndroidContext context, UExpression expression) {
// This function assumes that the only inputs to the expression are static integer
// flags that combined via bitwise operands.
if (UastLiteralUtils.isIntegralLiteral(expression)) {
return (int) UastLiteralUtils.getLongValue((ULiteralExpression) expression);
}
if (expression instanceof UResolvable) {
UDeclaration resolvedNode = ((UResolvable) expression).resolve(context);
if (resolvedNode instanceof UVariable) {
UExpression initializer = ((UVariable)resolvedNode).getInitializer();
if (initializer != null) {
Object value = initializer.evaluate();
if (value instanceof Integer) {
return (Integer)value;
}
}
}
}
if (expression instanceof UBinaryExpression) {
UBinaryExpression binaryExpression = (UBinaryExpression) expression;
UastBinaryOperator operator = binaryExpression.getOperator();
int leftValue = calculateValue(context, binaryExpression.getLeftOperand());
int rightValue = calculateValue(context, binaryExpression.getRightOperand());
if (operator == UastBinaryOperator.BITWISE_OR) {
return leftValue | rightValue;
}
if (operator == UastBinaryOperator.BITWISE_AND) {
return leftValue & rightValue;
}
if (operator == UastBinaryOperator.BITWISE_XOR) {
return leftValue ^ rightValue;
}
}
return 0;
}
}
@@ -16,28 +16,42 @@
package com.android.tools.klint.checks;
import static com.intellij.psi.util.PsiTreeUtil.getParentOfType;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiType;
import org.jetbrains.uast.UAnonymousClass;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UObjectLiteralExpression;
import org.jetbrains.uast.UastUtils;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
/**
* Checks that Handler implementations are top level classes or static.
* See the corresponding check in the android.os.Handler source code.
*/
public class HandlerDetector extends Detector implements UastScanner {
public class HandlerDetector extends Detector implements Detector.UastScanner {
/** Potentially leaking handlers */
public static final Issue ISSUE = Issue.create(
@@ -58,7 +72,7 @@ public class HandlerDetector extends Detector implements UastScanner {
Severity.WARNING,
new Implementation(
HandlerDetector.class,
Scope.SOURCE_FILE_SCOPE));
Scope.JAVA_FILE_SCOPE));
private static final String LOOPER_CLS = "android.os.Looper";
private static final String HANDLER_CLS = "android.os.Handler";
@@ -67,48 +81,65 @@ public class HandlerDetector extends Detector implements UastScanner {
public HandlerDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableSuperClasses() {
public List<String> applicableSuperClasses() {
return Collections.singletonList(HANDLER_CLS);
}
@Override
public void visitClass(UastAndroidContext context, UClass node) {
if (UastUtils.isTopLevel(node)) {
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
// Only consider static inner classes
if (context.getEvaluator().isStatic(declaration)) {
return;
}
boolean isAnonymous = declaration instanceof UAnonymousClass;
if (declaration.getContainingClass() == null && !isAnonymous) {
return;
}
if (node.hasModifier(UastModifier.STATIC)) {
//// Only flag handlers using the default looper
//noinspection unchecked
UCallExpression invocation = UastUtils.getParentOfType(
declaration, UObjectLiteralExpression.class, true, UMethod.class);
if (invocation != null) {
if (isAnonymous && invocation.getValueArgumentCount() > 0) {
for (UExpression expression : invocation.getValueArguments()) {
PsiType type = expression.getExpressionType();
if (type instanceof PsiClassType
&& LOOPER_CLS.equals(type.getCanonicalText())) {
return;
}
}
}
} else if (hasLooperConstructorParameter(declaration)) {
// This is an inner class which takes a Looper parameter:
// possibly used correctly from elsewhere
return;
}
// Only flag handlers using the default looper
if (hasLooperConstructorParameter(node)) {
return;
Location location = context.getUastNameLocation(declaration);
String name;
if (isAnonymous) {
name = "anonymous " + ((UAnonymousClass)declaration).getBaseClassReference().getQualifiedName();
} else {
name = declaration.getQualifiedName();
}
UElement locationNode = node.getNameElement();
Location location = context.getLocation(locationNode);
context.report(ISSUE, node, location, String.format(
"This Handler class should be static or leaks might occur (%1$s)",
node.getName()));
//noinspection VariableNotUsedInsideIf
context.reportUast(ISSUE, declaration, location, String.format(
"This Handler class should be static or leaks might occur (%1$s)",
name));
}
private static boolean hasLooperConstructorParameter(@NonNull UClass cls) {
for (UFunction constructor : cls.getConstructors()) {
for (int i = 0, n = constructor.getValueParameterCount(); i < n; i++) {
UType type = constructor.getValueParameters().get(i).getType();
if (type.matchesFqName(LOOPER_CLS)) {
private static boolean hasLooperConstructorParameter(@NonNull PsiClass cls) {
for (PsiMethod constructor : cls.getConstructors()) {
for (PsiParameter parameter : constructor.getParameterList().getParameters()) {
PsiType type = parameter.getType();
if (LOOPER_CLS.equals(type.getCanonicalText())) {
return true;
}
}
@@ -34,9 +34,6 @@ import static com.android.SdkConstants.DRAWABLE_PREFIX;
import static com.android.SdkConstants.DRAWABLE_XHDPI;
import static com.android.SdkConstants.DRAWABLE_XXHDPI;
import static com.android.SdkConstants.DRAWABLE_XXXHDPI;
import static com.android.SdkConstants.MENU_TYPE;
import static com.android.SdkConstants.R_CLASS;
import static com.android.SdkConstants.R_DRAWABLE_PREFIX;
import static com.android.SdkConstants.TAG_ACTIVITY;
import static com.android.SdkConstants.TAG_APPLICATION;
import static com.android.SdkConstants.TAG_ITEM;
@@ -49,30 +46,49 @@ import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.ProductFlavor;
import com.android.builder.model.ProductFlavorContainer;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.Density;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.ResourceEvaluator;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.UAnonymousClass;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.USimpleNameReferenceExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Element;
@@ -107,7 +123,7 @@ import javax.imageio.stream.ImageInputStream;
* Checks for common icon problems, such as wrong icon sizes, placing icons in the
* density independent drawable folder, etc.
*/
public class IconDetector extends ResourceXmlDetector implements UastScanner {
public class IconDetector extends ResourceXmlDetector implements Detector.UastScanner {
private static final boolean INCLUDE_LDPI;
static {
@@ -149,7 +165,7 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner {
* the manifest, menu files etc to see how icons are used
*/
private static final EnumSet<Scope> ICON_TYPE_SCOPE = EnumSet.of(Scope.ALL_RESOURCE_FILES,
Scope.SOURCE_FILE, Scope.MANIFEST);
Scope.JAVA_FILE, Scope.MANIFEST);
private static final Implementation IMPLEMENTATION_JAVA = new Implementation(
IconDetector.class,
@@ -373,12 +389,6 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner {
public IconDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.SLOW;
}
@Override
public void beforeCheckProject(@NonNull Context context) {
mLauncherIcons = null;
@@ -697,7 +707,9 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner {
String message = String.format(
"The `%1$s` icon has identical contents in the following configuration folders: %2$s",
lastName, sb.toString());
if (location != null) {
context.report(DUPLICATES_CONFIGURATIONS, location, message);
}
} else {
StringBuilder sb = new StringBuilder(sameFiles.size() * 16);
for (File file : sameFiles) {
@@ -911,7 +923,9 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner {
"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);
if (location != null) {
context.report(ICON_DIP_SIZE, location, message);
}
}
}
}
@@ -1958,108 +1972,97 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner {
// ---- Implements UastScanner ----
private static final String NOTIFICATION_CLASS = "Notification"; //$NON-NLS-1$
private static final String NOTIFICATION_COMPAT_CLASS = "NotificationCompat"; //$NON-NLS-1$
private static final String NOTIFICATION_CLASS_FQNAME =
"android.app.Notification"; //$NON-NLS-1$
private static final String NOTIFICATION_COMPAT_CLASS_FQNAME =
"android.support.v4.app.NotificationCompat"; //$NON-NLS-1$
private static final String BUILDER_CLASS = "Builder"; //$NON-NLS-1$
private static final String SET_SMALL_ICON = "setSmallIcon"; //$NON-NLS-1$
private static final String ON_CREATE_OPTIONS_MENU = "onCreateOptionsMenu"; //$NON-NLS-1$
private static final String NOTIFICATION_CLASS = "Notification";
private static final String NOTIFICATION_BUILDER_CLASS = "Notification.Builder";
private static final String NOTIFICATION_COMPAT_BUILDER_CLASS = "NotificationCompat.Builder";
private static final String SET_SMALL_ICON = "setSmallIcon";
private static final String ON_CREATE_OPTIONS_MENU = "onCreateOptionsMenu";
@Nullable
@Override
public UastVisitor createUastVisitor(UastAndroidContext context) {
public List<Class<? extends UElement>> getApplicableUastTypes() {
List<Class<? extends UElement>> types = new ArrayList<Class<? extends UElement>>(2);
types.add(UCallExpression.class);
types.add(UMethod.class);
return types;
}
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull JavaContext context) {
return new NotificationFinder(context);
}
private final class NotificationFinder extends AbstractUastVisitor {
private UastAndroidContext mContext;
private final JavaContext mContext;
public NotificationFinder(UastAndroidContext mContext) {
this.mContext = mContext;
private NotificationFinder(JavaContext context) {
mContext = context;
}
@Override
public boolean visitFunction(@NotNull UFunction node) {
if (node.matchesName(ON_CREATE_OPTIONS_MENU)) {
public boolean visitMethod(UMethod method) {
if (ON_CREATE_OPTIONS_MENU.equals(method.getName())) {
// Gather any R.menu references found in this method
node.accept(new MenuFinder());
method.accept(new MenuFinder());
}
return super.visitFunction(node);
return super.visitMethod(method);
}
@Override
public boolean visitCallExpression(@NotNull UCallExpression node) {
if (node.getKind() == UastCallKind.CONSTRUCTOR_CALL) {
visitConstructorInvocation(node);
public boolean visitCallExpression(UCallExpression node) {
if (UastExpressionUtils.isConstructorCall(node)) {
visitConstructorCall(node);
}
return super.visitCallExpression(node);
}
private void visitConstructorInvocation(UCallExpression node) {
USimpleReferenceExpression reference = node.getClassReference();
if (reference == null) {
private void visitConstructorCall(UCallExpression node) {
UReferenceExpression classReference = node.getClassReference();
if (classReference == null) {
return;
}
List<String> parts = UastUtils.asQualifiedIdentifiers(UastUtils.getQualifiedCallElement(node));
String typeName = reference.getIdentifier();
PsiElement resolved = classReference.resolve();
if (!(resolved instanceof PsiClass)) {
return;
}
String typeName = ((PsiClass) resolved).getName();
if (NOTIFICATION_CLASS.equals(typeName)) {
List<UExpression> args = node.getValueArguments();
if (args.size() == 3) {
if (args.get(0) instanceof UQualifiedExpression
&& handleSelect((UQualifiedExpression) args.get(0))) {
if (args.get(0) instanceof UReferenceExpression && handleSelect(args.get(0))) {
return;
}
UFunction method = UastUtils.getContainingFunction(node);
if (method != null) {
// Must track local types
String name = StringFormatDetector.getResourceForFirstArg(method, node);
if (name != null) {
if (mNotificationIcons == null) {
mNotificationIcons = Sets.newHashSet();
}
mNotificationIcons.add(name);
ResourceUrl url = ResourceEvaluator.getResource(mContext, args.get(0));
if (url != null
&& (url.type == ResourceType.DRAWABLE
|| url.type == ResourceType.COLOR
|| url.type == ResourceType.MIPMAP)) {
if (mNotificationIcons == null) {
mNotificationIcons = Sets.newHashSet();
}
mNotificationIcons.add(url.name);
}
}
} else if (BUILDER_CLASS.equals(typeName)) {
boolean isBuilder = false;
if (parts != null && parts.size() == 1) {
isBuilder = true;
} else if (parts != null && parts.size() == 2) {
String clz = parts.get(0);
if (NOTIFICATION_CLASS.equals(clz) || NOTIFICATION_COMPAT_CLASS_FQNAME.equals(clz)) {
isBuilder = true;
}
}
if (isBuilder) {
UFunction method = UastUtils.getContainingFunction(node);
if (method != null) {
SetIconFinder finder = new SetIconFinder();
method.accept(finder);
}
} else if (NOTIFICATION_BUILDER_CLASS.equals(typeName)
|| NOTIFICATION_COMPAT_BUILDER_CLASS.equals(typeName)) {
UMethod method = UastUtils.getParentOfType(node, UMethod.class, true);
if (method != null) {
SetIconFinder finder = new SetIconFinder();
method.accept(finder);
}
}
}
}
private boolean handleSelect(UQualifiedExpression select) {
if (select.renderString().startsWith(R_DRAWABLE_PREFIX)) {
String name = select.getSelector().renderString();
private boolean handleSelect(UElement select) {
ResourceUrl url = ResourceEvaluator.getResourceConstant(select);
if (url != null && url.type == ResourceType.DRAWABLE && !url.framework) {
if (mNotificationIcons == null) {
mNotificationIcons = Sets.newHashSet();
}
mNotificationIcons.add(name);
mNotificationIcons.add(url.name);
return true;
}
@@ -2068,46 +2071,47 @@ public class IconDetector extends ResourceXmlDetector implements UastScanner {
}
private final class SetIconFinder extends AbstractUastVisitor {
@Override
public boolean visitCallExpression(@NotNull UCallExpression node) {
if (SET_SMALL_ICON.equals(node.getFunctionName())) {
List<UExpression> args = node.getValueArguments();
if (args.size() == 1 && args.get(0) instanceof UQualifiedExpression) {
handleSelect((UQualifiedExpression) args.get(0));
public boolean visitCallExpression(UCallExpression expression) {
if (UastExpressionUtils.isMethodCall(expression)) {
if (SET_SMALL_ICON.equals(expression.getMethodName())) {
List<UExpression> arguments = expression.getValueArguments();
if (arguments.size() == 1 && arguments.get(0) instanceof UReferenceExpression) {
handleSelect(arguments.get(0));
}
}
}
return super.visitCallExpression(expression);
}
return super.visitCallExpression(node);
@Override
public boolean visitClass(UClass node) {
if (node instanceof UAnonymousClass) {
return true;
}
return super.visitClass(node);
}
}
private final class MenuFinder extends AbstractUastVisitor {
@Override
public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) {
// R.type.name
if (node.getReceiver() instanceof UQualifiedExpression) {
UQualifiedExpression select = (UQualifiedExpression) node.getReceiver();
if (select.getReceiver() instanceof USimpleReferenceExpression) {
USimpleReferenceExpression reference = (USimpleReferenceExpression) select.getReceiver();
if (R_CLASS.equals(reference.getIdentifier())) {
if (select.selectorMatches(MENU_TYPE)) {
String name = node.getSelector().renderString();
// Reclassify icons in the given menu as action bar icons
if (mMenuToIcons != null) {
Collection<String> icons = mMenuToIcons.get(name);
if (icons != null) {
if (mActionBarIcons == null) {
mActionBarIcons = Sets.newHashSet();
}
mActionBarIcons.addAll(icons);
}
}
public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) {
ResourceUrl url = ResourceEvaluator.getResourceConstant(node);
if (url != null && url.type == ResourceType.MENU && !url.framework) {
// Reclassify icons in the given menu as action bar icons
if (mMenuToIcons != null) {
Collection<String> icons = mMenuToIcons.get(url.name);
if (icons != null) {
if (mActionBarIcons == null) {
mActionBarIcons = Sets.newHashSet();
}
mActionBarIcons.addAll(icons);
}
}
}
return super.visitQualifiedExpression(node);
return super.visitSimpleNameReferenceExpression(node);
}
}
}
@@ -17,47 +17,68 @@
package com.android.tools.klint.checks;
import static com.android.SdkConstants.SUPPORT_LIB_ARTIFACT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BOOLEAN_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_BYTE_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_CHARACTER_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_DOUBLE_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_FLOAT_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INT;
import static com.android.tools.klint.client.api.JavaParser.TYPE_INTEGER_WRAPPER;
import static com.android.tools.klint.client.api.JavaParser.TYPE_LONG_WRAPPER;
import static com.android.tools.klint.detector.api.LintUtils.skipParentheses;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.TextFormat;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiType;
import org.jetbrains.uast.UBinaryExpression;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UIfExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UParenthesizedExpression;
import org.jetbrains.uast.UPrefixExpression;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.USimpleNameReferenceExpression;
import org.jetbrains.uast.USuperExpression;
import org.jetbrains.uast.UThisExpression;
import org.jetbrains.uast.UThrowExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
/**
* Looks for performance issues in Java files, such as memory allocations during
* drawing operations and using HashMap instead of SparseArray.
*/
public class JavaPerformanceDetector extends Detector implements UastScanner {
public class JavaPerformanceDetector extends Detector implements Detector.UastScanner {
private static final Implementation
IMPLEMENTATION = new Implementation(
private static final Implementation IMPLEMENTATION = new Implementation(
JavaPerformanceDetector.class,
Scope.SOURCE_FILE_SCOPE);
Scope.JAVA_FILE_SCOPE);
/** Allocating objects during a paint method */
public static final Issue PAINT_ALLOC = Issue.create(
@@ -116,136 +137,113 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
Severity.WARNING,
IMPLEMENTATION);
static final String ON_MEASURE = "onMeasure"; //$NON-NLS-1$
static final String ON_DRAW = "onDraw"; //$NON-NLS-1$
static final String ON_LAYOUT = "onLayout"; //$NON-NLS-1$
private static final String INTEGER = "Integer"; //$NON-NLS-1$
private static final String BOOLEAN = "Boolean"; //$NON-NLS-1$
private static final String BYTE = "Byte"; //$NON-NLS-1$
private static final String LONG = "Long"; //$NON-NLS-1$
private static final String CHARACTER = "Character"; //$NON-NLS-1$
private static final String DOUBLE = "Double"; //$NON-NLS-1$
private static final String FLOAT = "Float"; //$NON-NLS-1$
private static final String HASH_MAP = "HashMap"; //$NON-NLS-1$
private static final String SPARSE_ARRAY = "SparseArray"; //$NON-NLS-1$
private static final String CANVAS = "Canvas"; //$NON-NLS-1$
private static final String LAYOUT = "layout"; //$NON-NLS-1$
static final String ON_MEASURE = "onMeasure";
static final String ON_DRAW = "onDraw";
static final String ON_LAYOUT = "onLayout";
private static final String LAYOUT = "layout";
private static final String HASH_MAP = "java.util.HashMap";
private static final String SPARSE_ARRAY = "android.util.SparseArray";
public static final String CLASS_CANVAS = "android.graphics.Canvas";
/** Constructs a new {@link JavaPerformanceDetector} check */
public JavaPerformanceDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Nullable
@Override
public UastVisitor createUastVisitor(UastAndroidContext context) {
public List<Class<? extends UElement>> getApplicableUastTypes() {
List<Class<? extends UElement>> types = new ArrayList<Class<? extends UElement>>(3);
types.add(UCallExpression.class);
types.add(UMethod.class);
return types;
}
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull JavaContext context) {
return new PerformanceVisitor(context);
}
private static class PerformanceVisitor extends AbstractUastVisitor {
private final UastAndroidContext mContext;
private final JavaContext mContext;
private final boolean mCheckMaps;
private final boolean mCheckAllocations;
private final boolean mCheckValueOf;
/** Whether allocations should be "flagged" in the current method */
private boolean mFlagAllocations;
public PerformanceVisitor(UastAndroidContext context) {
public PerformanceVisitor(JavaContext context) {
mContext = context;
JavaContext lintContext = context.getLintContext();
mCheckAllocations = lintContext.isEnabled(PAINT_ALLOC);
mCheckMaps = lintContext.isEnabled(USE_SPARSE_ARRAY);
mCheckValueOf = lintContext.isEnabled(USE_VALUE_OF);
mCheckAllocations = context.isEnabled(PAINT_ALLOC);
mCheckMaps = context.isEnabled(USE_SPARSE_ARRAY);
mCheckValueOf = context.isEnabled(USE_VALUE_OF);
}
@Override
public boolean visitFunction(@NotNull UFunction node) {
public boolean visitMethod(UMethod node) {
mFlagAllocations = isBlockedAllocationMethod(node);
return super.visitFunction(node);
return super.visitMethod(node);
}
@Override
public boolean visitCallExpression(@NotNull UCallExpression node) {
UastCallKind kind = node.getKind();
if (kind == UastCallKind.CONSTRUCTOR_CALL) {
visitConstructorInvocation(node);
} else if (kind == UastCallKind.FUNCTION_CALL) {
visitFunctionInvocation(node);
public boolean visitCallExpression(UCallExpression node) {
if (UastExpressionUtils.isConstructorCall(node)) {
visitConstructorCallExpression(node);
} else if (UastExpressionUtils.isMethodCall(node)) {
visitMethodCallExpression(node);
}
return super.visitCallExpression(node);
}
private void visitConstructorInvocation(UCallExpression node) {
USimpleReferenceExpression classReference = node.getClassReference();
if (classReference == null) {
return;
private void visitConstructorCallExpression(UCallExpression node) {
String typeName = null;
UReferenceExpression classReference = node.getClassReference();
if (mCheckMaps || mCheckValueOf) {
if (classReference != null) {
typeName = UastUtils.getQualifiedName(classReference);
}
}
String typeName = null;
if (mCheckMaps) {
UClass clazz = classReference.resolveClass(mContext);
if (clazz != null) {
typeName = clazz.getName();
// TODO: Should we handle factory method constructions of HashMaps as well,
// e.g. via Guava? This is a bit trickier since we need to infer the type
// arguments from the calling context.
if (clazz.matchesName(HASH_MAP)) {
checkHashMap(node);
} else if (clazz.matchesName(SPARSE_ARRAY)) {
checkSparseArray(node);
}
// TODO: Should we handle factory method constructions of HashMaps as well,
// e.g. via Guava? This is a bit trickier since we need to infer the type
// arguments from the calling context.
if (HASH_MAP.equals(typeName)) {
checkHashMap(node);
} else if (SPARSE_ARRAY.equals(typeName)) {
checkSparseArray(node);
}
}
if (mCheckValueOf) {
UType type = UastErrorType.INSTANCE;
if (typeName == null) {
UDeclaration resolvedDeclaration = classReference.resolve(mContext);
if (resolvedDeclaration instanceof UClass) {
type = ((UClass) resolvedDeclaration).getDefaultType();
typeName = type.getName();
}
}
if ((type.isInt()
|| type.isBoolean()
|| type.isFloat()
|| type.isChar()
|| type.isLong()
|| type.isDouble()
|| type.isShort()
|| type.isByte())
if (typeName != null
&& (typeName.equals(TYPE_INTEGER_WRAPPER)
|| typeName.equals(TYPE_BOOLEAN_WRAPPER)
|| typeName.equals(TYPE_FLOAT_WRAPPER)
|| typeName.equals(TYPE_CHARACTER_WRAPPER)
|| typeName.equals(TYPE_LONG_WRAPPER)
|| typeName.equals(TYPE_DOUBLE_WRAPPER)
|| typeName.equals(TYPE_BYTE_WRAPPER))
//&& node.astTypeReference().astParts().size() == 1
&& node.getValueArgumentCount() == 1) {
String argument = node.getValueArguments().get(0).renderString();
mContext.report(USE_VALUE_OF, node, mContext.getLocation(node), getUseValueOfErrorMessage(
String argument = node.getValueArguments().get(0).asSourceString();
mContext.report(USE_VALUE_OF, node, mContext.getUastLocation(node), getUseValueOfErrorMessage(
typeName, argument));
}
}
if (mFlagAllocations && !(node.getParent() instanceof UThrowExpression) && mCheckAllocations) {
if (mFlagAllocations
&& !(skipParentheses(node.getContainingElement()) instanceof UThrowExpression)
&& mCheckAllocations) {
// Make sure we're still inside the method declaration that marked
// mInDraw as true, in case we've left it and we're in a static
// block or something:
UElement method = node;
while (method != null) {
if (method instanceof UFunction) {
break;
}
method = method.getParent();
}
if (method != null && isBlockedAllocationMethod(((UFunction) method))
PsiMethod method = UastUtils.getParentOfType(node, UMethod.class);
if (method != null && isBlockedAllocationMethod(method)
&& !isLazilyInitialized(node)) {
reportAllocation(node);
}
@@ -253,42 +251,46 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
}
private void reportAllocation(UElement node) {
mContext.report(PAINT_ALLOC, node, mContext.getLocation(node),
mContext.report(PAINT_ALLOC, node, mContext.getUastLocation(node),
"Avoid object allocations during draw/layout operations (preallocate and " +
"reuse instead)");
}
private void visitFunctionInvocation(UCallExpression node) {
UExpression operand = UastUtils.getReceiver(node);
private void visitMethodCallExpression(UCallExpression node) {
if (!mFlagAllocations) {
return;
}
UExpression receiver = node.getReceiver();
if (receiver == null) {
return;
}
if (mFlagAllocations && operand != null) {
// Look for forbidden methods
String methodName = node.getFunctionName();
if ("createBitmap".equals(methodName) //$NON-NLS-1$
|| "createScaledBitmap".equals(methodName)) { //$NON-NLS-1$
String operandText = operand.renderString();
if (operandText.equals("Bitmap") //$NON-NLS-1$
|| operandText.equals("android.graphics.Bitmap")) { //$NON-NLS-1$
if (!isLazilyInitialized(node)) {
reportAllocation(node);
}
}
} else if (methodName != null && methodName.startsWith("decode")) { //$NON-NLS-1$
// decodeFile, decodeByteArray, ...
String operandText = operand.renderString();
if (operandText.equals("BitmapFactory") //$NON-NLS-1$
|| operandText.equals("android.graphics.BitmapFactory")) { //$NON-NLS-1$
if (!isLazilyInitialized(node)) {
reportAllocation(node);
}
}
} else if ("getClipBounds".equals(methodName)) { //$NON-NLS-1$
if (node.getValueArgumentCount() == 0) {
mContext.report(PAINT_ALLOC, node, mContext.getLocation(node),
"Avoid object allocations during draw operations: Use " +
"`Canvas.getClipBounds(Rect)` instead of `Canvas.getClipBounds()` " +
"which allocates a temporary `Rect`");
}
String functionName = node.getMethodName();
if (functionName == null) {
return;
}
// Look for forbidden methods
if (functionName.equals("createBitmap") //$NON-NLS-1$
|| functionName.equals("createScaledBitmap")) { //$NON-NLS-1$
PsiMethod method = node.resolve();
if (method != null && JavaEvaluator.isMemberInClass(method,
"android.graphics.Bitmap") && !isLazilyInitialized(node)) {
reportAllocation(node);
}
} else if (functionName.startsWith("decode")) { //$NON-NLS-1$
// decodeFile, decodeByteArray, ...
PsiMethod method = node.resolve();
if (method != null && JavaEvaluator.isMemberInClass(method,
"android.graphics.BitmapFactory") && !isLazilyInitialized(node)) {
reportAllocation(node);
}
} else if (functionName.equals("getClipBounds")) { //$NON-NLS-1$
if (node.getValueArguments().isEmpty()) {
mContext.report(PAINT_ALLOC, node, mContext.getUastLocation(node),
"Avoid object allocations during draw operations: Use " +
"`Canvas.getClipBounds(Rect)` instead of `Canvas.getClipBounds()` " +
"which allocates a temporary `Rect`");
}
}
}
@@ -314,9 +316,9 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
* </pre>
*/
private static boolean isLazilyInitialized(UElement node) {
UElement curr = node.getParent();
UElement curr = node.getContainingElement();
while (curr != null) {
if (curr instanceof UFunction) {
if (curr instanceof UMethod) {
return false;
} else if (curr instanceof UIfExpression) {
UIfExpression ifNode = (UIfExpression) curr;
@@ -328,10 +330,9 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
// guarded (so lazily initialized and an allocation we won't complain
// about.)
List<String> assignments = new ArrayList<String>();
UExpression thenBranch = ifNode.getThenBranch();
if (thenBranch != null) {
AssignmentTracker visitor = new AssignmentTracker(assignments);
thenBranch.accept(visitor);
AssignmentTracker visitor = new AssignmentTracker(assignments);
if (ifNode.getThenExpression() != null) {
ifNode.getThenExpression().accept(visitor);
}
if (!assignments.isEmpty()) {
List<String> references = new ArrayList<String>();
@@ -346,29 +347,40 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
return false;
}
curr = curr.getParent();
curr = curr.getContainingElement();
}
return false;
}
/** Adds any variables referenced in the given expression into the given list */
private static void addReferencedVariables(Collection<String> variables,
UExpression expression) {
private static void addReferencedVariables(
@NonNull Collection<String> variables,
@Nullable UExpression expression) {
if (expression instanceof UBinaryExpression) {
UBinaryExpression binary = (UBinaryExpression) expression;
addReferencedVariables(variables, binary.getLeftOperand());
addReferencedVariables(variables, binary.getRightOperand());
} else if (expression instanceof UUnaryExpression) {
UUnaryExpression unary = (UUnaryExpression) expression;
} else if (expression instanceof UPrefixExpression) {
UPrefixExpression unary = (UPrefixExpression) expression;
addReferencedVariables(variables, unary.getOperand());
} else if (expression instanceof USimpleReferenceExpression) {
USimpleReferenceExpression reference = (USimpleReferenceExpression) expression;
} else if (expression instanceof UParenthesizedExpression) {
UParenthesizedExpression exp = (UParenthesizedExpression) expression;
addReferencedVariables(variables, exp.getExpression());
} else if (expression instanceof USimpleNameReferenceExpression) {
USimpleNameReferenceExpression reference = (USimpleNameReferenceExpression) expression;
variables.add(reference.getIdentifier());
} else if (expression instanceof UQualifiedExpression) {
UQualifiedExpression select = (UQualifiedExpression) expression;
if (select.getReceiver() instanceof UThisExpression && select.getSelector() instanceof USimpleReferenceExpression) {
variables.add(((USimpleReferenceExpression) select.getSelector()).getIdentifier());
} else if (expression instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression ref = (UQualifiedReferenceExpression) expression;
UExpression receiver = ref.getReceiver();
UExpression selector = ref.getSelector();
if (receiver instanceof UThisExpression || receiver instanceof USuperExpression) {
String identifier = (selector instanceof USimpleNameReferenceExpression)
? ((USimpleNameReferenceExpression) selector).getIdentifier()
: null;
if (identifier != null) {
variables.add(identifier);
}
}
}
}
@@ -377,27 +389,23 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
* Returns whether the given method declaration represents a method
* where allocating objects is not allowed for performance reasons
*/
private static boolean isBlockedAllocationMethod(UFunction node) {
return isOnDrawMethod(node) || isOnMeasureMethod(node) || isOnLayoutMethod(node)
|| isLayoutMethod(node);
private boolean isBlockedAllocationMethod(
@NonNull PsiMethod node) {
JavaEvaluator evaluator = mContext.getEvaluator();
return isOnDrawMethod(evaluator, node)
|| isOnMeasureMethod(evaluator, node)
|| isOnLayoutMethod(evaluator, node)
|| isLayoutMethod(evaluator, node);
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code protected void onDraw(Canvas canvas)}
*/
private static boolean isOnDrawMethod(UFunction node) {
if (ON_DRAW.equals(node.getName())) {
List<UVariable> parameters = node.getValueParameters();
if (parameters.size() == 1) {
UVariable arg0 = parameters.get(0);
if (arg0.getType().matchesName(CANVAS)) {
return true;
}
}
}
return false;
private static boolean isOnDrawMethod(
@NonNull JavaEvaluator evaluator,
@NonNull PsiMethod node) {
return ON_DRAW.equals(node.getName()) && evaluator.parametersMatch(node, CLASS_CANVAS);
}
/**
@@ -406,148 +414,102 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
* {@code protected void onLayout(boolean changed, int left, int top,
* int right, int bottom)}
*/
private static boolean isOnLayoutMethod(UFunction node) {
if (ON_LAYOUT.equals(node.getName())) {
List<UVariable> parameters = node.getValueParameters();
if (parameters.size() == 5) {
Iterator<UVariable> iterator = parameters.iterator();
if (!iterator.hasNext()) {
return false;
}
// Ensure that the argument list matches boolean, int, int, int, int
UType type = iterator.next().getType();
if (!type.isBoolean() || !iterator.hasNext()) {
return false;
}
for (int i = 0; i < 4; i++) {
type = iterator.next().getType();
if (!type.isInt()) {
return false;
}
if (!iterator.hasNext()) {
return i == 3;
}
}
}
}
return false;
private static boolean isOnLayoutMethod(
@NonNull JavaEvaluator evaluator,
@NonNull PsiMethod node) {
return ON_LAYOUT.equals(node.getName()) && evaluator.parametersMatch(node,
TYPE_BOOLEAN, TYPE_INT, TYPE_INT, TYPE_INT, TYPE_INT);
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)}
*/
private static boolean isOnMeasureMethod(UFunction node) {
if (ON_MEASURE.equals(node.getName())) {
List<UVariable> parameters = node.getValueParameters();
if (parameters.size() == 2) {
UVariable arg0 = parameters.get(0);
UVariable arg1 = parameters.get(parameters.size() - 1);
return arg0.getType().isInt() && arg1.getType().isInt();
}
}
return false;
private static boolean isOnMeasureMethod(
@NonNull JavaEvaluator evaluator,
@NonNull PsiMethod node) {
return ON_MEASURE.equals(node.getName()) && evaluator.parametersMatch(node,
TYPE_INT, TYPE_INT);
}
/**
* Returns true if this method looks like it's overriding android.view.View's
* {@code public void layout(int l, int t, int r, int b)}
*/
private static boolean isLayoutMethod(UFunction node) {
if (LAYOUT.equals(node.getName())) {
List<UVariable> parameters = node.getValueParameters();
if (parameters.size() == 4) {
Iterator<UVariable> iterator = parameters.iterator();
for (int i = 0; i < 4; i++) {
if (!iterator.hasNext()) {
return false;
}
UVariable next = iterator.next();
if (!next.getType().isInt()) {
return false;
}
}
return true;
}
}
return false;
private static boolean isLayoutMethod(
@NonNull JavaEvaluator evaluator,
@NonNull PsiMethod node) {
return LAYOUT.equals(node.getName()) && evaluator.parametersMatch(node,
TYPE_INT, TYPE_INT, TYPE_INT, TYPE_INT);
}
/**
* Checks whether the given constructor call and type reference refers
* to a HashMap constructor call that is eligible for replacement by a
* SparseArray call instead
*/
private void checkHashMap(UCallExpression node) {
// reference.hasTypeArguments returns false where it should not
List<UType> types = node.getTypeArguments();
private void checkHashMap(@NonNull UCallExpression node) {
List<PsiType> types = node.getTypeArguments();
if (types.size() == 2) {
UType first = types.get(0);
Project mainProject = mContext.getLintContext().getMainProject();
int minSdk = mainProject.getMinSdk();
if (first.isInt() || first.isByte()) {
UType valueType = types.get(1);
String valueTypeText = valueType.getName();
if (valueType.isInt()) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
PsiType first = types.get(0);
String typeName = first.getCanonicalText();
int minSdk = mContext.getMainProject().getMinSdk();
if (TYPE_INTEGER_WRAPPER.equals(typeName) || TYPE_BYTE_WRAPPER.equals(typeName)) {
String valueType = types.get(1).getCanonicalText();
if (valueType.equals(TYPE_INTEGER_WRAPPER)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node),
"Use new `SparseIntArray(...)` instead for better performance");
} else if (valueType.isLong() && minSdk >= 18) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
} else if (valueType.equals(TYPE_LONG_WRAPPER) && minSdk >= 18) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node),
"Use `new SparseLongArray(...)` instead for better performance");
} else if (valueType.isBoolean()) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
} else if (valueType.equals(TYPE_BOOLEAN_WRAPPER)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
} else {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node),
String.format(
"Use `new SparseArray<%1$s>(...)` instead for better performance",
valueTypeText));
valueType.substring(valueType.lastIndexOf('.') + 1)));
}
} else if (first.isLong() && (minSdk >= 16 ||
Boolean.TRUE == mainProject.dependsOn(
} else if (TYPE_LONG_WRAPPER.equals(typeName) && (minSdk >= 16 ||
Boolean.TRUE == mContext.getMainProject().dependsOn(
SUPPORT_LIB_ARTIFACT))) {
boolean useBuiltin = minSdk >= 16;
String message = useBuiltin ?
"Use `new LongSparseArray(...)` instead for better performance" :
"Use `new android.support.v4.util.LongSparseArray(...)` instead for better performance";
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node),
message);
}
}
}
private void checkSparseArray(UCallExpression node) {
// reference.hasTypeArguments returns false where it should not
List<UType> types = node.getTypeArguments();
private void checkSparseArray(@NonNull UCallExpression node) {
List<PsiType> types = node.getTypeArguments();
if (types.size() == 1) {
UType first = types.get(0);
if (first.isInt()) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
String valueType = types.get(0).getCanonicalText();
if (valueType.equals(TYPE_INTEGER_WRAPPER)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node),
"Use `new SparseIntArray(...)` instead for better performance");
} else if (first.isBoolean()) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getLocation(node),
} else if (valueType.equals(TYPE_BOOLEAN_WRAPPER)) {
mContext.report(USE_SPARSE_ARRAY, node, mContext.getUastLocation(node),
"Use `new SparseBooleanArray(...)` instead for better performance");
}
}
}
}
private static String getUseValueOfErrorMessage(String typeName, String argument) {
// Keep in sync with {@link #getReplacedType} below
return String.format("Use `%1$s.valueOf(%2$s)` instead", typeName, argument);
return String.format("Use `%1$s.valueOf(%2$s)` instead",
typeName.substring(typeName.lastIndexOf('.') + 1), argument);
}
/**
* For an error message for an {@link #USE_VALUE_OF} issue reported by this detector,
* returns the type being replaced. Intended to use for IDE quickfix implementations.
*/
@SuppressWarnings("unused") // Used by the IDE
@Nullable
public static String getReplacedType(@NonNull String message, @NonNull TextFormat format) {
message = format.toText(message);
@@ -567,24 +529,28 @@ public class JavaPerformanceDetector extends Detector implements UastScanner {
}
@Override
public boolean visitBinaryExpression(@NotNull UBinaryExpression node) {
UastBinaryOperator operator = node.getOperator();
if (operator == UastBinaryOperator.ASSIGN || operator == UastBinaryOperator.OR_ASSIGN) {
public boolean visitBinaryExpression(UBinaryExpression node) {
if (UastExpressionUtils.isAssignment(node)) {
UExpression left = node.getLeftOperand();
String variable = null;
if (left instanceof UQualifiedExpression && ((UQualifiedExpression) left).getReceiver() instanceof UThisExpression) {
UExpression selector = ((UQualifiedExpression) left).getSelector();
if (selector instanceof USimpleReferenceExpression) {
variable = ((USimpleReferenceExpression) selector).getIdentifier();
if (left instanceof UQualifiedReferenceExpression) {
UQualifiedReferenceExpression ref = (UQualifiedReferenceExpression) left;
if (ref.getReceiver() instanceof UThisExpression ||
ref.getReceiver() instanceof USuperExpression) {
PsiElement resolved = ref.resolve();
if (resolved instanceof PsiField) {
mVariables.add(((PsiField) resolved).getName());
}
} else {
PsiElement resolved = ref.resolve();
if (resolved instanceof PsiField) {
mVariables.add(((PsiField) resolved).getName());
}
}
} else if (left instanceof USimpleReferenceExpression) {
variable = ((USimpleReferenceExpression) left).getIdentifier();
}
if (variable != null) {
mVariables.add(variable);
} else if (left instanceof USimpleNameReferenceExpression) {
mVariables.add(((USimpleNameReferenceExpression) left).getIdentifier());
}
}
return super.visitBinaryExpression(node);
}
}
@@ -17,31 +17,36 @@
package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.google.common.collect.Maps;
import com.android.tools.klint.detector.api.TypeEvaluator;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiType;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
/**
* Looks for addJavascriptInterface calls on interfaces have been properly annotated
* with {@code @JavaScriptInterface}
*/
public class JavaScriptInterfaceDetector extends Detector implements UastScanner {
public class JavaScriptInterfaceDetector extends Detector implements Detector.UastScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"JavascriptInterface", //$NON-NLS-1$
@@ -55,7 +60,7 @@ public class JavaScriptInterfaceDetector extends Detector implements UastScanner
Severity.ERROR,
new Implementation(
JavaScriptInterfaceDetector.class,
Scope.SOURCE_FILE_SCOPE))
Scope.JAVA_FILE_SCOPE))
.addMoreInfo(
"http://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)"); //$NON-NLS-1$
@@ -67,227 +72,69 @@ public class JavaScriptInterfaceDetector extends Detector implements UastScanner
public JavaScriptInterfaceDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.SLOW; // because it relies on class loading referenced javascript interface
}
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Collections.singletonList(ADD_JAVASCRIPT_INTERFACE);
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
if (context.getLintContext().getMainProject().getTargetSdk() < 17) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
if (context.getMainProject().getTargetSdk() < 17) {
return;
}
if (node.getValueArgumentCount() != 2) {
List<UExpression> arguments = call.getValueArguments();
if (arguments.size() != 2) {
return;
}
if (!isCallOnWebView(context, node)) {
JavaEvaluator evaluator = context.getEvaluator();
if (!JavaEvaluator.isMemberInClass(method, WEB_VIEW_CLS)) {
return;
}
UExpression first = node.getValueArguments().get(0);
UElement resolved = UastUtils.resolveIfCan(first, context);
if (resolved instanceof UVariable) {
// We're passing in a variable to the addJavaScriptInterface method;
// the variable may be of a more generic type than the actual
// value assigned to it. For example, we may have a scenario like this:
// Object object = new SpecificType();
// addJavaScriptInterface(object, ...)
// Here the type of the variable is Object, but we know that it can
// contain objects of type SpecificType, so we should check that type instead.
UFunction method = UastUtils.getContainingFunction(node);
if (method != null) {
ConcreteTypeVisitor v = new ConcreteTypeVisitor(context, node);
method.accept(v);
resolved = v.getType();
if (resolved == null) {
return;
}
} else {
UExpression first = arguments.get(0);
PsiType evaluated = TypeEvaluator.evaluate(context, first);
if (evaluated instanceof PsiClassType) {
PsiClassType classType = (PsiClassType) evaluated;
PsiClass cls = classType.resolve();
if (cls == null) {
return;
}
} else if (resolved instanceof UFunction) {
UFunction method = (UFunction) resolved;
if (method.getKind() == UastFunctionKind.CONSTRUCTOR) {
resolved = UastUtils.getContainingClass(method);
} else {
UType returnType = method.getReturnType();
if (returnType != null) {
UClass resolvedClass = returnType.resolve(context);
if (resolvedClass != null) {
resolved = resolvedClass;
}
}
}
} else {
UType type = first.getExpressionType();
if (type != null) {
UClass resolvedClass = type.resolve(context);
if (resolvedClass != null) {
resolved = resolvedClass;
}
}
}
if (resolved instanceof UClass) {
UClass cls = (UClass) resolved;
if (isJavaScriptAnnotated(context, cls)) {
if (isJavaScriptAnnotated(cls)) {
return;
}
Location location = context.getLocation(node.getFunctionNameElement());
Location location = context.getUastNameLocation(call);
String message = String.format(
"None of the methods in the added interface (%1$s) have been annotated " +
"with `@android.webkit.JavascriptInterface`; they will not " +
"be visible in API 17", cls.getName());
context.report(ISSUE, node, location, message);
"None of the methods in the added interface (%1$s) have been annotated " +
"with `@android.webkit.JavascriptInterface`; they will not " +
"be visible in API 17", cls.getName());
context.report(ISSUE, call, location, message);
}
}
private static boolean isCallOnWebView(UastAndroidContext context, UCallExpression call) {
UFunction resolved = call.resolve(context);
return UastUtils.getContainingClassOrEmpty(resolved).matchesFqName(WEB_VIEW_CLS);
}
private static boolean isJavaScriptAnnotated(UastAndroidContext context, UClass clz) {
private static boolean isJavaScriptAnnotated(PsiClass clz) {
while (clz != null) {
for (UAnnotation annotation : clz.getAnnotations()) {
if (JAVASCRIPT_INTERFACE_CLS.equals(annotation.getFqName())) {
PsiModifierList modifierList = clz.getModifierList();
if (modifierList != null
&& modifierList.findAnnotation(JAVASCRIPT_INTERFACE_CLS) != null) {
return true;
}
for (PsiMethod method : clz.getMethods()) {
if (method.getModifierList().findAnnotation(JAVASCRIPT_INTERFACE_CLS) != null) {
return true;
}
}
for (UFunction method : clz.getFunctions()) {
for (UAnnotation annotation : method.getAnnotations()) {
if (JAVASCRIPT_INTERFACE_CLS.equals(annotation.getFqName())) {
return true;
}
}
}
clz = clz.getSuperClass(context);
clz = clz.getSuperClass();
}
return false;
}
private static class ConcreteTypeVisitor extends AbstractUastVisitor {
private final UastAndroidContext mContext;
private final UCallExpression mTargetCall;
private boolean mFoundCall;
private Map<UElement, UClass> mTypes = Maps.newIdentityHashMap();
private Map<UVariable, UClass> mVariableTypes = Maps.newHashMap();
public ConcreteTypeVisitor(UastAndroidContext context, UCallExpression call) {
mContext = context;
mTargetCall = call;
}
public UClass getType() {
UExpression first = mTargetCall.getValueArguments().get(0);
UClass resolvedClass = mTypes.get(first);
if (resolvedClass == null && first instanceof UResolvable) {
UElement resolved = ((UResolvable) first).resolve(mContext);
if (resolved instanceof UVariable) {
resolvedClass = mVariableTypes.get(resolved);
if (resolvedClass == null) {
return ((UVariable) resolved).getType().resolve(mContext);
}
}
}
return resolvedClass;
}
@Override
public boolean visitElement(@NotNull UElement node) {
return mFoundCall || super.visitElement(node);
}
@Override
public void afterVisitCallExpression(@NotNull UCallExpression node) {
if (node.getKind() == UastCallKind.FUNCTION_CALL && node == mTargetCall) {
mFoundCall = true;
} else if (node.getKind() == UastCallKind.CONSTRUCTOR_CALL) {
UFunction resolved = node.resolve(mContext);
if (resolved != null) {
mTypes.put(node, UastUtils.getContainingClass(resolved));
}
}
}
@Override
public void afterVisitSimpleReferenceExpression(@NotNull USimpleReferenceExpression node) {
if (mTypes.get(node) == null) {
UElement resolved = node.resolve(mContext);
if (resolved instanceof UVariable) {
UClass resolvedClass = mVariableTypes.get(resolved);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
}
}
@Override
public void afterVisitBinaryExpression(@NotNull UBinaryExpression node) {
if (node.getOperator() == UastBinaryOperator.ASSIGN) {
UExpression rhs = node.getRightOperand();
UClass resolvedClass = mTypes.get(rhs);
if (resolvedClass != null) {
UExpression lhs = node.getLeftOperand();
mTypes.put(lhs, resolvedClass);
if (lhs instanceof UResolvable) {
UDeclaration variable = ((UResolvable) lhs).resolve(mContext);
if (variable instanceof UVariable) {
mVariableTypes.put((UVariable) variable, resolvedClass);
}
}
}
}
}
@Override
public void afterVisitIfExpression(@NotNull UIfExpression node) {
if (node.isTernary()) {
UClass resolvedClass = mTypes.get(node.getThenBranch());
if (resolvedClass == null) {
resolvedClass = mTypes.get(node.getElseBranch());
}
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
}
@Override
public void afterVisitVariable(@NotNull UVariable node) {
UExpression initializer = node.getInitializer();
if (initializer != null) {
UClass resolvedClass = mTypes.get(initializer);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
mVariableTypes.put(node, resolvedClass);
}
}
}
@Override
public void afterVisitBinaryExpressionWithType(@NotNull UBinaryExpressionWithType node) {
UClass resolvedClass = mTypes.get(node);
if (resolvedClass != null) {
mTypes.put(node, resolvedClass);
}
}
}
}
@@ -28,23 +28,26 @@ import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.LintDriver;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiElement;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -65,7 +68,7 @@ import java.util.Set;
/**
* Checks for consistency in layouts across different resource folders
*/
public class LayoutConsistencyDetector extends LayoutDetector implements UastScanner {
public class LayoutConsistencyDetector extends LayoutDetector implements Detector.UastScanner {
/** Map from layout resource names to a list of files defining that resource,
* and within each file the value is a map from string ids to the widget type
@@ -112,7 +115,7 @@ public class LayoutConsistencyDetector extends LayoutDetector implements UastSca
Severity.WARNING,
new Implementation(
LayoutConsistencyDetector.class,
Scope.SOURCE_AND_RESOURCE_FILES));
Scope.JAVA_AND_RESOURCE_FILES));
/** Constructs a consistency check */
public LayoutConsistencyDetector() {
@@ -123,12 +126,6 @@ public class LayoutConsistencyDetector extends LayoutDetector implements UastSca
return folderType == ResourceFolderType.LAYOUT;
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
Element root = document.getDocumentElement();
@@ -435,8 +432,10 @@ public class LayoutConsistencyDetector extends LayoutDetector implements UastSca
}
@Override
public void visitResourceReference(UastAndroidContext context, UElement element, String type, String name, boolean isFramework) {
if (!isFramework && type.equals(ResourceType.ID.getName())) {
public void visitResourceReference(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UElement node, @NonNull ResourceType type, @NonNull String name,
boolean isFramework) {
if (!isFramework && type == ResourceType.ID) {
mRelevantIds.add(name);
}
}
@@ -18,18 +18,21 @@ package com.android.tools.klint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
import static org.jetbrains.uast.UastLiteralUtils.*;
import static com.android.tools.klint.checks.ViewHolderDetector.INFLATE;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.AndroidReference;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
@@ -39,15 +42,16 @@ import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UastLiteralUtils;
import org.jetbrains.uast.visitor.UastVisitor;
import org.kxml2.io.KXmlParser;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
@@ -61,20 +65,19 @@ import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Looks for layout inflation calls passing null as the view root
*/
public class LayoutInflationDetector extends LayoutDetector implements UastScanner {
public class LayoutInflationDetector extends LayoutDetector implements Detector.UastScanner {
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION = new Implementation(
LayoutInflationDetector.class,
Scope.SOURCE_AND_RESOURCE_FILES,
Scope.SOURCE_FILE_SCOPE);
Scope.JAVA_AND_RESOURCE_FILES,
Scope.JAVA_FILE_SCOPE);
/** Passing in a null parent to a layout inflater */
public static final Issue ISSUE = Issue.create(
@@ -98,12 +101,6 @@ public class LayoutInflationDetector extends LayoutDetector implements UastScann
public LayoutInflationDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.NORMAL;
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPendingErrors != null) {
@@ -146,52 +143,48 @@ public class LayoutInflationDetector extends LayoutDetector implements UastScann
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableFunctionNames() {
return Collections.singletonList(ViewHolderDetector.INFLATE);
public List<String> getApplicableMethodNames() {
return Collections.singletonList(INFLATE);
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
assert ViewHolderDetector.INFLATE.equals(node.getFunctionName());
if (node instanceof USimpleReferenceExpression) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
assert method.getName().equals(INFLATE);
if (call.getReceiver() == null) {
return;
}
List<UExpression> arguments = node.getValueArguments();
List<UExpression> arguments = call.getValueArguments();
if (arguments.size() < 2) {
return;
}
Iterator<UExpression> iterator = arguments.iterator();
UExpression first = iterator.next();
UExpression second = iterator.next();
if (!isNullLiteral(second) || !(first instanceof UQualifiedExpression)) {
UExpression second = arguments.get(1);
if (!UastLiteralUtils.isNullLiteral(second)) {
return;
}
UQualifiedExpression select = (UQualifiedExpression) first;
UExpression receiver = select.getReceiver();
UExpression selector = select.getSelector();
if (receiver instanceof UQualifiedExpression && selector instanceof USimpleReferenceExpression) {
UQualifiedExpression rLayout = (UQualifiedExpression) receiver;
if (rLayout.selectorMatches(ResourceType.LAYOUT.getName()) &&
rLayout.getReceiver().renderString().endsWith(SdkConstants.R_CLASS)) {
String layoutName = ((USimpleReferenceExpression)selector).getIdentifier();
JavaContext lintContext = context.getLintContext();
UExpression first = arguments.get(0);
AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(first);
if (androidReference == null) {
return;
}
if (lintContext.getScope().contains(Scope.RESOURCE_FILE)) {
// We're doing a full analysis run: we can gather this information
// incrementally
if (!lintContext.getDriver().isSuppressed(lintContext, ISSUE, node)) {
if (mPendingErrors == null) {
mPendingErrors = Lists.newArrayList();
}
Location location = context.getLocation(second);
mPendingErrors.add(Pair.of(layoutName, location));
}
} else if (hasLayoutParams(lintContext, layoutName)) {
context.report(ISSUE, node, context.getLocation(second), ERROR_MESSAGE);
String layoutName = androidReference.getName();
if (context.getScope().contains(Scope.RESOURCE_FILE)) {
// We're doing a full analysis run: we can gather this information
// incrementally
if (!context.getDriver().isSuppressed(context, ISSUE, call)) {
if (mPendingErrors == null) {
mPendingErrors = Lists.newArrayList();
}
Location location = context.getUastLocation(second);
mPendingErrors.add(Pair.of(layoutName, location));
}
} else if (hasLayoutParams(context, layoutName)) {
context.report(ISSUE, call, context.getUastLocation(second), ERROR_MESSAGE);
}
}
@@ -0,0 +1,188 @@
/*
* Copyright (C) 2016 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 com.android.tools.klint.checks;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.SdkConstants.CLASS_FRAGMENT;
import static com.android.SdkConstants.CLASS_VIEW;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UField;
import org.jetbrains.uast.UVariable;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Collections;
import java.util.List;
/**
* Looks for leaks via static fields
*/
public class LeakDetector extends Detector implements Detector.UastScanner {
/** Leaking data via static fields */
public static final Issue ISSUE = Issue.create(
"StaticFieldLeak", //$NON-NLS-1$
"Static Field Leaks",
"A static field will leak contexts.",
Category.PERFORMANCE,
6,
Severity.WARNING,
new Implementation(
LeakDetector.class,
Scope.JAVA_FILE_SCOPE));
/** Constructs a new {@link LeakDetector} check */
public LeakDetector() {
}
// ---- Implements JavaScanner ----
@Override
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
return Collections.<Class<? extends PsiElement>>singletonList(PsiField.class);
}
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull JavaContext context) {
return new FieldChecker(context);
}
private static class FieldChecker extends AbstractUastVisitor {
private final JavaContext mContext;
private FieldChecker(JavaContext context) {
mContext = context;
}
@Override
public boolean visitClass(@NotNull UClass node) {
return super.visitClass(node);
}
@Override
public boolean visitVariable(UVariable node) {
if (node instanceof UField) {
checkField((UField) node);
}
return super.visitVariable(node);
}
private void checkField(UField field) {
PsiModifierList modifierList = field.getModifierList();
if (modifierList == null || !modifierList.hasModifierProperty(PsiModifier.STATIC)) {
return;
}
PsiType type = field.getType();
if (!(type instanceof PsiClassType)) {
return;
}
String fqn = type.getCanonicalText();
if (fqn.startsWith("java.")) {
return;
}
PsiClass cls = ((PsiClassType) type).resolve();
if (cls == null) {
return;
}
if (fqn.startsWith("android.")) {
if (isLeakCandidate(cls, mContext.getEvaluator())) {
String message = "Do not place Android context classes in static fields; "
+ "this is a memory leak (and also breaks Instant Run)";
report(field, modifierList, message);
}
} else {
// User application object -- look to see if that one itself has
// static fields?
// We only check *one* level of indirection here
int count = 0;
for (PsiField referenced : cls.getAllFields()) {
// Only check a few; avoid getting bogged down on large classes
if (count++ == 20) {
break;
}
PsiType innerType = referenced.getType();
if (!(innerType instanceof PsiClassType)) {
continue;
}
fqn = innerType.getCanonicalText();
if (fqn.startsWith("java.")) {
continue;
}
PsiClass innerCls = ((PsiClassType) innerType).resolve();
if (innerCls == null) {
continue;
}
if (fqn.startsWith("android.")) {
if (isLeakCandidate(innerCls, mContext.getEvaluator())) {
String message =
"Do not place Android context classes in static fields "
+ "(static reference to `"
+ cls.getName() + "` which has field "
+ "`" + referenced.getName() + "` pointing to `"
+ innerCls.getName() + "`); "
+ "this is a memory leak (and also breaks Instant Run)";
report(field, modifierList, message);
break;
}
}
}
}
}
private void report(@NonNull PsiField field, @NonNull PsiModifierList modifierList,
@NonNull String message) {
Location location = mContext.getLocation(
modifierList.getTextRange().getLength() > 0 ? modifierList : field);
mContext.report(ISSUE, field, location, message);
}
}
private static boolean isLeakCandidate(
@NonNull PsiClass cls,
@NonNull JavaEvaluator evaluator) {
return evaluator.extendsClass(cls, CLASS_CONTEXT, false)
|| evaluator.extendsClass(cls, CLASS_VIEW, false)
|| evaluator.extendsClass(cls, CLASS_FRAGMENT, false);
}
}
@@ -0,0 +1,166 @@
/*
* Copyright (C) 2012 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 com.android.tools.klint.checks;
import static com.android.SdkConstants.FORMAT_METHOD;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.LintClient;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.intellij.psi.PsiMethod;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Checks for errors related to locale handling
*/
public class LocaleDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
LocaleDetector.class,
Scope.JAVA_FILE_SCOPE);
/** Calling risky convenience methods */
public static final Issue STRING_LOCALE = Issue.create(
"DefaultLocale", //$NON-NLS-1$
"Implied default locale in case conversion",
"Calling `String#toLowerCase()` or `#toUpperCase()` *without specifying an " +
"explicit locale* is a common source of bugs. The reason for that is that those " +
"methods will use the current locale on the user's device, and even though the " +
"code appears to work correctly when you are developing the app, it will fail " +
"in some locales. For example, in the Turkish locale, the uppercase replacement " +
"for `i` is *not* `I`.\n" +
"\n" +
"If you want the methods to just perform ASCII replacement, for example to convert " +
"an enum name, call `String#toUpperCase(Locale.US)` instead. If you really want to " +
"use the current locale, call `String#toUpperCase(Locale.getDefault())` instead.",
Category.CORRECTNESS,
6,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo(
"http://developer.android.com/reference/java/util/Locale.html#default_locale"); //$NON-NLS-1$
/** Constructs a new {@link LocaleDetector} */
public LocaleDetector() {
}
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableMethodNames() {
if (LintClient.isStudio()) {
// In the IDE, don't flag toUpperCase/toLowerCase; these
// are already flagged by built-in IDE inspections, so we don't
// want duplicate warnings.
return Collections.singletonList(FORMAT_METHOD);
} else {
return Arrays.asList(
// Only when not running in the IDE
"toLowerCase", //$NON-NLS-1$
"toUpperCase", //$NON-NLS-1$
FORMAT_METHOD
);
}
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
if (JavaEvaluator.isMemberInClass(method, TYPE_STRING)) {
String name = method.getName();
if (name.equals(FORMAT_METHOD)) {
checkFormat(context, method, call);
} else if (method.getParameterList().getParametersCount() == 0) {
Location location = context.getUastNameLocation(call);
String message = String.format(
"Implicitly using the default locale is a common source of bugs: " +
"Use `%1$s(Locale)` instead", name);
context.report(STRING_LOCALE, call, location, message);
}
}
}
/** Returns true if the given node is a parameter to a Logging call */
private static boolean isLoggingParameter(@NonNull UCallExpression node) {
UCallExpression parentCall =
UastUtils.getParentOfType(node, UCallExpression.class, true);
if (parentCall != null) {
String name = parentCall.getMethodName();
if (name != null && name.length() == 1) { // "d", "i", "e" etc in Log
PsiMethod method = parentCall.resolve();
return JavaEvaluator.isMemberInClass(method, LogDetector.LOG_CLS);
}
}
return false;
}
private static void checkFormat(
@NonNull JavaContext context,
@NonNull PsiMethod method,
@NonNull UCallExpression call) {
// Only check the non-locale version of String.format
if (method.getParameterList().getParametersCount() == 0
|| !context.getEvaluator().parameterHasType(method, 0, TYPE_STRING)) {
return;
}
List<UExpression> expressions = call.getValueArguments();
if (expressions.isEmpty()) {
return;
}
// Find the formatting string
UExpression first = expressions.get(0);
Object value = ConstantEvaluator.evaluate(context, first);
if (!(value instanceof String)) {
return;
}
String format = (String) value;
if (StringFormatDetector.isLocaleSpecific(format)) {
if (isLoggingParameter(call)) {
return;
}
Location location = context.getUastLocation(call);
String message =
"Implicitly using the default locale is a common source of bugs: " +
"Use `String.format(Locale, ...)` instead";
context.report(STRING_LOCALE, call, location, message);
}
}
}
@@ -20,7 +20,10 @@ import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
@@ -28,21 +31,40 @@ import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiVariable;
import org.jetbrains.uast.UBinaryExpressionWithType;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UClassInitializer;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UField;
import org.jetbrains.uast.UIfExpression;
import org.jetbrains.uast.ULiteralExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.USimpleNameReferenceExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import java.util.Locale;
import java.util.Map;
/**
* Detector for finding inefficiencies and errors in logging calls.
*/
public class LogDetector extends Detector implements UastScanner {
public class LogDetector extends Detector implements Detector.UastScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
LogDetector.class, Scope.SOURCE_FILE_SCOPE);
LogDetector.class, Scope.JAVA_FILE_SCOPE);
/** Log call missing surrounding if */
@@ -93,69 +115,74 @@ public class LogDetector extends Detector implements UastScanner {
@SuppressWarnings("SpellCheckingInspection")
private static final String IS_LOGGABLE = "isLoggable"; //$NON-NLS-1$
private static final String LOG_CLS = "android.util.Log"; //$NON-NLS-1$
public static final String LOG_CLS = "android.util.Log"; //$NON-NLS-1$
private static final String PRINTLN = "println"; //$NON-NLS-1$
// ---- Implements UastScanner ----
private static final Map<String, String> TAG_PAIRS;
static {
Map<String, String> pairs = new HashMap<String, String>();
pairs.put("d", "DEBUG");
pairs.put("e", "ERROR");
pairs.put("i", "INFO");
pairs.put("v", "VERBOSE");
pairs.put("w", "WARN");
TAG_PAIRS = Collections.unmodifiableMap(pairs);
}
// ---- Implements Detector.UastScanner ----
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Arrays.asList(
"d", //$NON-NLS-1$
"e", //$NON-NLS-1$
"i", //$NON-NLS-1$
"v", //$NON-NLS-1$
"w", //$NON-NLS-1$
PRINTLN,
IS_LOGGABLE);
"d", //$NON-NLS-1$
"e", //$NON-NLS-1$
"i", //$NON-NLS-1$
"v", //$NON-NLS-1$
"w", //$NON-NLS-1$
PRINTLN,
IS_LOGGABLE);
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
UFunction method = node.resolve(context);
if (method == null) {
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression node, @NonNull UMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
if (!JavaEvaluator.isMemberInClass(method, LOG_CLS)) {
return;
}
if (!UastUtils.getContainingClassOrEmpty(method).matchesFqName(LOG_CLS)) {
return;
}
String name = node.getFunctionName();
String name = method.getName();
boolean withinConditional = IS_LOGGABLE.equals(name) ||
checkWithinConditional(context, node.getParent(), node);
checkWithinConditional(context, node.getContainingElement(), node);
// See if it's surrounded by an if statement (and it's one of the non-error, spammy
// log methods (info, verbose, etc))
if (("i".equals(name) || "d".equals(name) || "v".equals(name) || PRINTLN.equals(name))
&& !withinConditional
&& performsWork(node)
&& context.getLintContext().isEnabled(CONDITIONAL)) {
&& !withinConditional
&& performsWork(node)
&& context.isEnabled(CONDITIONAL)) {
String message = String.format("The log call Log.%1$s(...) should be " +
"conditional: surround with `if (Log.isLoggable(...))` or " +
"`if (BuildConfig.DEBUG) { ... }`",
node.getFunctionName());
context.report(CONDITIONAL, node, context.getLocation(node), message);
"conditional: surround with `if (Log.isLoggable(...))` or " +
"`if (BuildConfig.DEBUG) { ... }`",
node.getMethodIdentifier());
context.report(CONDITIONAL, node, context.getUastLocation(node), message);
}
// Check tag length
if (context.getLintContext().isEnabled(LONG_TAG)) {
if (context.isEnabled(LONG_TAG)) {
int tagArgumentIndex = PRINTLN.equals(name) ? 1 : 0;
if (method.getValueParameterCount() > tagArgumentIndex
&& method.getValueParameters().get(tagArgumentIndex).getType().matchesFqName(TYPE_STRING)
&& node.getValueArgumentCount() == method.getValueParameterCount()) {
Iterator<UExpression> iterator = node.getValueArguments().iterator();
if (tagArgumentIndex == 1) {
iterator.next();
}
UExpression argument = iterator.next();
String tag = argument.evaluateString();
PsiParameterList parameterList = method.getParameterList();
List<UExpression> argumentList = node.getValueArguments();
if (evaluator.parameterHasType(method, tagArgumentIndex, TYPE_STRING)
&& parameterList.getParametersCount() == argumentList.size()) {
UExpression argument = argumentList.get(tagArgumentIndex);
String tag = ConstantEvaluator.evaluateString(context, argument, true);
if (tag != null && tag.length() > 23) {
String message = String.format(
"The logging tag can be at most 23 characters, was %1$d (%2$s)",
tag.length(), tag);
context.report(LONG_TAG, node, context.getLocation(node), message);
"The logging tag can be at most 23 characters, was %1$d (%2$s)",
tag.length(), tag);
context.report(LONG_TAG, node, context.getUastLocation(node), message);
}
}
}
@@ -163,33 +190,41 @@ public class LogDetector extends Detector implements UastScanner {
}
/** Returns true if the given logging call performs "work" to compute the message */
private static boolean performsWork(
@NonNull UCallExpression node) {
int messageArgumentIndex = PRINTLN.equals(node.getFunctionName()) ? 2 : 1;
if (node.getValueArgumentCount() >= messageArgumentIndex) {
Iterator<UExpression> iterator = node.getValueArguments().iterator();
UExpression argument = null;
for (int i = 0; i <= messageArgumentIndex; i++) {
argument = iterator.next();
}
private static boolean performsWork(@NonNull UCallExpression node) {
String referenceName = node.getMethodName();
if (referenceName == null) {
return false;
}
int messageArgumentIndex = PRINTLN.equals(referenceName) ? 2 : 1;
List<UExpression> arguments = node.getValueArguments();
if (arguments.size() > messageArgumentIndex) {
UExpression argument = arguments.get(messageArgumentIndex);
if (argument == null) {
return false;
}
if (UastLiteralUtils.isStringLiteral(argument) || argument instanceof USimpleReferenceExpression) {
if (argument instanceof ULiteralExpression) {
return false;
}
if (argument instanceof UBinaryExpression) {
String string = argument.evaluateString();
if (argument instanceof UBinaryExpressionWithType) {
String string = UastUtils.evaluateString(argument);
//noinspection VariableNotUsedInsideIf
if (string != null) { // does it resolve to a constant?
return false;
}
} else if (argument instanceof UQualifiedExpression) {
String string = argument.evaluateString();
} else if (argument instanceof USimpleNameReferenceExpression) {
// Just a simple local variable/field reference
return false;
} else if (argument instanceof UQualifiedReferenceExpression) {
String string = UastUtils.evaluateString(argument);
//noinspection VariableNotUsedInsideIf
if (string != null) {
return false;
}
PsiElement resolved = ((UQualifiedReferenceExpression) argument).resolve();
if (resolved instanceof PsiVariable) {
// Just a reference to a property/field, parameter or variable
return false;
}
}
// Method invocations etc
@@ -198,96 +233,78 @@ public class LogDetector extends Detector implements UastScanner {
return false;
}
private static boolean checkWithinConditional(
@NonNull UastAndroidContext context,
@NonNull JavaContext context,
@Nullable UElement curr,
@NonNull UCallExpression logCall) {
while (curr != null) {
if (curr instanceof UIfExpression) {
UIfExpression ifNode = (UIfExpression) curr;
UExpression condition = ifNode.getCondition();
if (condition instanceof UCallExpression) {
UCallExpression call = (UCallExpression) condition;
if (IS_LOGGABLE.equals(call.getFunctionName())) {
checkTagConsistent(context, logCall, call);
}
} else if (condition instanceof UQualifiedExpression) {
UCallExpression call = UastUtils.getCallElementFromQualified((UQualifiedExpression) condition);
if (call != null && IS_LOGGABLE.equals(call.getFunctionName())) {
List<UExpression> chain = UastUtils.getQualifiedChain(ifNode.getCondition());
if (!chain.isEmpty() && chain.get(chain.size() - 1) instanceof UCallExpression) {
UCallExpression call = (UCallExpression) chain.get(chain.size() - 1);
if (IS_LOGGABLE.equals(call.getMethodName())) {
checkTagConsistent(context, logCall, call);
}
}
return true;
} else if (curr instanceof UCallExpression
|| curr instanceof UMethod
|| curr instanceof UClassInitializer
|| curr instanceof UField
|| curr instanceof UClass) { // static block
break;
}
curr = curr.getParent();
curr = curr.getContainingElement();
}
return false;
}
/** Checks that the tag passed to Log.s and Log.isLoggable match */
private static void checkTagConsistent(UastAndroidContext context, UCallExpression logCall,
UCallExpression call) {
Iterator<UExpression> isLogIterator = call.getValueArguments().iterator();
Iterator<UExpression> logIterator = logCall.getValueArguments().iterator();
if (!isLogIterator.hasNext() || !logIterator.hasNext()) {
private static void checkTagConsistent(JavaContext context, UCallExpression logCall,
UCallExpression isLoggableCall) {
List<UExpression> isLoggableArguments = isLoggableCall.getValueArguments();
List<UExpression> logArguments = logCall.getValueArguments();
if (isLoggableArguments.isEmpty() || logArguments.isEmpty()) {
return;
}
UExpression isLoggableTag = isLogIterator.next();
UExpression logTag = logIterator.next();
UExpression isLoggableTag = isLoggableArguments.get(0);
UExpression logTag = logArguments.get(0);
String logCallName = logCall.getFunctionName();
String logCallName = logCall.getMethodName();
if (logCallName == null) {
return;
}
boolean isPrintln = PRINTLN.equals(logCallName);
if (isPrintln) {
if (!logIterator.hasNext()) {
return;
}
logTag = logIterator.next();
if (isPrintln && logArguments.size() > 1) {
logTag = logArguments.get(1);
}
JavaContext lintContext = context.getLintContext();
if (logTag != null) {
String isLoggableTagString = null;
String logTagString = null;
boolean isOk = true;
Object isLoggableTagValue = isLoggableTag.evaluate();
Object logTagValue = logTag.evaluate();
if (isLoggableTagValue instanceof String && logTagValue instanceof String) {
isLoggableTagString = (String) isLoggableTagValue;
logTagString = (String) logTagValue;
isOk = isLoggableTagString.equals(logTagString);
} else if (isLoggableTag instanceof UResolvable && logTag instanceof UResolvable) {
UDeclaration isLoggableTagResolved = ((UResolvable) isLoggableTag).resolve(context);
UDeclaration logTagResolved = ((UResolvable) logTag).resolve(context);
if (isLoggableTagResolved != null && logTagResolved != null) {
isOk = isLoggableTagResolved.equals(logTagResolved);
}
}
if (!isOk) {
Location location = context.getLocation(logTag);
Location alternate = context.getLocation(isLoggableTag);
if (location != null && alternate != null) {
if (!UastLintUtils.areIdentifiersEqual(isLoggableTag, logTag)) {
PsiNamedElement resolved1 = UastUtils.tryResolveNamed(isLoggableTag);
PsiNamedElement resolved2 = UastUtils.tryResolveNamed(logTag);
if ((resolved1 == null || resolved2 == null || !resolved1.equals(resolved2))
&& context.isEnabled(WRONG_TAG)) {
Location location = context.getUastLocation(logTag);
Location alternate = context.getUastLocation(isLoggableTag);
alternate.setMessage("Conflicting tag");
location.setSecondary(alternate);
String isLoggableDescription = resolved1 != null
? resolved1.getName()
: isLoggableTag.asRenderString();
String logCallDescription = resolved2 != null
? resolved2.getName()
: logTag.asRenderString();
String message = String.format(
"Mismatched tags: the `%1$s()` and `isLoggable()` calls typically " +
"should pass the same tag: `%2$s` versus `%3$s`",
"should pass the same tag: `%2$s` versus `%3$s`",
logCallName,
isLoggableTagValue,
logTagValue);
context.report(WRONG_TAG, call, location, message);
isLoggableDescription,
logCallDescription);
context.report(WRONG_TAG, isLoggableCall, location, message);
}
}
}
@@ -295,50 +312,40 @@ public class LogDetector extends Detector implements UastScanner {
// Check log level versus the actual log call type (e.g. flag
// if (Log.isLoggable(TAG, Log.DEBUG) Log.info(TAG, "something")
if (logCallName.length() != 1 || !isLogIterator.hasNext()) { // e.g. println
if (logCallName.length() != 1 || isLoggableArguments.size() < 2) { // e.g. println
return;
}
UExpression isLoggableLevel = isLogIterator.next();
UExpression isLoggableLevel = isLoggableArguments.get(1);
if (isLoggableLevel == null) {
return;
}
String levelString = isLoggableLevel.toString();
if (isLoggableLevel instanceof UQualifiedExpression) {
String identifier = ((UQualifiedExpression)isLoggableLevel).getSelectorAsIdentifier();
if (identifier != null) {
levelString = identifier;
}
}
if (levelString.isEmpty()) {
PsiNamedElement resolved = UastUtils.tryResolveNamed(isLoggableLevel);
if (resolved == null) {
return;
}
char levelChar = Character.toLowerCase(levelString.charAt(0));
if (logCallName.charAt(0) == levelChar || !lintContext.isEnabled(WRONG_TAG)) {
return;
}
switch (levelChar) {
case 'd':
case 'e':
case 'i':
case 'v':
case 'w':
break;
default:
// Some other char; e.g. user passed in a literal value or some
// local constant or variable alias
if (resolved instanceof PsiVariable) {
PsiClass containingClass = UastUtils.getContainingClass(resolved);
if (containingClass == null
|| !"android.util.Log".equals(containingClass.getQualifiedName())
|| resolved.getName() == null
|| resolved.getName().equals(TAG_PAIRS.get(logCallName))) {
return;
}
String expectedCall = String.valueOf(levelChar);
String message = String.format(
"Mismatched logging levels: when checking `isLoggable` level `%1$s`, the " +
"corresponding log call should be `Log.%2$s`, not `Log.%3$s`",
levelString, expectedCall, logCallName);
Location location = context.getLocation(logCall.getFunctionNameElement());
Location alternate = context.getLocation(isLoggableLevel);
if (location != null && alternate != null) {
}
String expectedCall = resolved.getName().substring(0, 1)
.toLowerCase(Locale.getDefault());
String message = String.format(
"Mismatched logging levels: when checking `isLoggable` level `%1$s`, the " +
"corresponding log call should be `Log.%2$s`, not `Log.%3$s`",
resolved.getName(), expectedCall, logCallName);
Location location = context.getUastLocation(logCall.getMethodIdentifier());
Location alternate = context.getUastLocation(isLoggableLevel);
alternate.setMessage("Conflicting tag");
location.setSecondary(alternate);
context.report(WRONG_TAG, call, location, message);
context.report(WRONG_TAG, isLoggableCall, location, message);
}
}
}
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2011 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 com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.Arrays;
import java.util.List;
/**
* Looks for usages of {@link Math} methods which can be replaced with
* {@code android.util.FloatMath} methods to avoid casting.
*/
public class MathDetector extends Detector implements Detector.UastScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"FloatMath", //$NON-NLS-1$
"Using `FloatMath` instead of `Math`",
"In older versions of Android, using `android.util.FloatMath` was recommended " +
"for performance reasons when operating on floats. However, on modern hardware " +
"doubles are just as fast as float (though they take more memory), and in " +
"recent versions of Android, `FloatMath` is actually slower than using `java.lang.Math` " +
"due to the way the JIT optimizes `java.lang.Math`. Therefore, you should use " +
"`Math` instead of `FloatMath` if you are only targeting Froyo and above.",
Category.PERFORMANCE,
3,
Severity.WARNING,
new Implementation(
MathDetector.class,
Scope.JAVA_FILE_SCOPE))
.addMoreInfo(
"http://developer.android.com/guide/practices/design/performance.html#avoidfloat"); //$NON-NLS-1$
/** Constructs a new {@link MathDetector} check */
public MathDetector() {
}
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
"sin", //$NON-NLS-1$
"cos", //$NON-NLS-1$
"ceil", //$NON-NLS-1$
"sqrt", //$NON-NLS-1$
"floor" //$NON-NLS-1$
);
}
@Override
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
if (context.getEvaluator().isMemberInClass(method, "android.util.FloatMath")
&& context.getProject().getMinSdk() >= 8) {
String message = String.format(
"Use `java.lang.Math#%1$s` instead of `android.util.FloatMath#%1$s()` " +
"since it is faster as of API 8", method.getName());
Location location = context.getUastLocation(call.getMethodIdentifier());
context.report(ISSUE, call, location, message);
}
}
}
@@ -21,37 +21,45 @@ import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_FOREGROUND;
import static com.android.SdkConstants.ATTR_LAYOUT;
import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.FRAME_LAYOUT;
import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.R_LAYOUT_RESOURCE_PREFIX;
import static com.android.SdkConstants.VIEW_INCLUDE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.AndroidReference;
import com.android.tools.klint.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Location.Handle;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UQualifiedExpression;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -64,7 +72,7 @@ import java.util.Set;
/**
* Checks whether a root FrameLayout can be replaced with a {@code <merge>} tag.
*/
public class MergeRootFrameLayoutDetector extends LayoutDetector implements UastScanner {
public class MergeRootFrameLayoutDetector extends LayoutDetector implements Detector.UastScanner {
/**
* Set of layouts that we want to enable the warning for. We only warn for
* {@code <FrameLayout>}'s that are the root of a layout included from
@@ -95,7 +103,7 @@ public class MergeRootFrameLayoutDetector extends LayoutDetector implements Uast
Severity.WARNING,
new Implementation(
MergeRootFrameLayoutDetector.class,
EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.SOURCE_FILE)))
EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.JAVA_FILE)))
.addMoreInfo(
"http://android-developers.blogspot.com/2009/03/android-layout-tricks-3-optimize-by.html"); //$NON-NLS-1$
@@ -103,17 +111,6 @@ public class MergeRootFrameLayoutDetector extends LayoutDetector implements Uast
public MergeRootFrameLayoutDetector() {
}
@Override
@NonNull
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return LintUtils.isXmlFile(file) || LintUtils.endsWith(file.getName(), DOT_JAVA);
}
@Override
public void afterCheckProject(@NonNull Context context) {
if (mPending != null && mWhitelistedLayouts != null) {
@@ -188,27 +185,26 @@ public class MergeRootFrameLayoutDetector extends LayoutDetector implements Uast
mWhitelistedLayouts.add(layout);
}
// Implements UastScanner
// Implements JavaScanner
@Override
public List<String> getApplicableFunctionNames() {
public List<String> getApplicableMethodNames() {
return Collections.singletonList("setContentView"); //$NON-NLS-1$
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
if (node.getValueArgumentCount() != 1) {
return;
}
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
List<UExpression> expressions = call.getValueArguments();
if (expressions.size() == 1) {
AndroidReference androidReference =
UastLintUtils.toAndroidReferenceViaResolve(expressions.get(0));
List<UExpression> argumentList = node.getValueArguments();
UExpression argument = argumentList.get(0);
if (argument instanceof UQualifiedExpression) {
String expression = argument.renderString();
if (expression.startsWith(R_LAYOUT_RESOURCE_PREFIX)) {
whiteListLayout(expression.substring(R_LAYOUT_RESOURCE_PREFIX.length()));
if (androidReference != null && androidReference.getType() == ResourceType.LAYOUT) {
whiteListLayout(androidReference.getName());
}
}
}
}
@@ -17,27 +17,26 @@
package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import java.io.File;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.ULiteralExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.visitor.UastVisitor;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.ULiteralExpression;
import org.jetbrains.uast.USimpleReferenceExpression;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
/** Detector looking for text messages sent to an unlocalized phone number. */
public class NonInternationalizedSmsDetector extends Detector implements UastScanner {
public class NonInternationalizedSmsDetector extends Detector implements Detector.UastScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
"UnlocalizedSms", //$NON-NLS-1$
@@ -52,50 +51,50 @@ public class NonInternationalizedSmsDetector extends Detector implements UastSca
Severity.WARNING,
new Implementation(
NonInternationalizedSmsDetector.class,
Scope.SOURCE_FILE_SCOPE));
Scope.JAVA_FILE_SCOPE));
/** Constructs a new {@link NonInternationalizedSmsDetector} check */
public NonInternationalizedSmsDetector() {
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return true;
}
// ---- Implements UastScanner ----
// ---- Implements JavaScanner ----
@Override
public List<String> getApplicableFunctionNames() {
List<String> methodNames = new ArrayList<String>(2);
methodNames.add("sendTextMessage"); //$NON-NLS-1$
methodNames.add("sendMultipartTextMessage"); //$NON-NLS-1$
return methodNames;
public List<String> getApplicableMethodNames() {
List<String> methodNames = new ArrayList<String>(2);
methodNames.add("sendTextMessage"); //$NON-NLS-1$
methodNames.add("sendMultipartTextMessage"); //$NON-NLS-1$
return methodNames;
}
@Override
public void visitCall(UastAndroidContext context, UCallExpression node) {
String functionName = node.getFunctionName();
assert "sendTextMessage".equals(functionName) || //$NON-NLS-1$
"sendMultipartTextMessage".equals(functionName); //$NON-NLS-1$
List<UExpression> args = node.getValueArguments();
if (args.size() == 5) {
UExpression destinationAddress = args.get(0);
if (destinationAddress instanceof ULiteralExpression
&& ((ULiteralExpression)destinationAddress).isString()) {
String number = (String) ((ULiteralExpression) destinationAddress).getValue();
if (number != null && !number.startsWith("+")) { //$NON-NLS-1$
context.report(ISSUE, node, context.getLocation(destinationAddress),
"To make sure the SMS can be sent by all users, please start the SMS number " +
"with a + and a country code or restrict the code invocation to people in the country " +
"you are targeting.");
}
}
public void visitMethod(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UCallExpression call, @NonNull UMethod method) {
if (call.getReceiver() == null) {
// "sendTextMessage"/"sendMultipartTextMessage" in the code with no operand
return;
}
List<UExpression> args = call.getValueArguments();
if (args.size() != 5) {
return;
}
UExpression destinationAddress = args.get(0);
if (!(destinationAddress instanceof ULiteralExpression)) {
return;
}
Object literal = ((ULiteralExpression) destinationAddress).getValue();
if (!(literal instanceof String)) {
return;
}
String number = (String) literal;
if (number.startsWith("+")) { //$NON-NLS-1$
return;
}
context.report(ISSUE, call, context.getUastLocation(destinationAddress),
"To make sure the SMS can be sent by all users, please start the SMS number " +
"with a + and a country code or restrict the code invocation to people in the " +
"country you are targeting.");
}
}
@@ -16,32 +16,65 @@
package com.android.tools.klint.checks;
import static com.android.SdkConstants.*;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_CONTEXT;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_PARENT;
import static com.android.SdkConstants.ATTR_THEME;
import static com.android.SdkConstants.ATTR_TILE_MODE;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.DRAWABLE_PREFIX;
import static com.android.SdkConstants.NULL_RESOURCE;
import static com.android.SdkConstants.R_CLASS;
import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.TAG_ACTIVITY;
import static com.android.SdkConstants.TAG_APPLICATION;
import static com.android.SdkConstants.TAG_BITMAP;
import static com.android.SdkConstants.TAG_STYLE;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.TRANSPARENT_COLOR;
import static com.android.SdkConstants.VALUE_DISABLED;
import static com.android.tools.klint.detector.api.LintUtils.endsWith;
import static com.android.utils.SdkUtils.getResourceFieldName;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.klint.client.api.AndroidReference;
import com.android.tools.klint.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LayoutDetector;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.android.utils.Pair;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.UAnonymousClass;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UQualifiedReferenceExpression;
import org.jetbrains.uast.USimpleNameReferenceExpression;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -54,17 +87,14 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Check which looks for overdraw problems where view areas are painted and then
* painted over, meaning that the bottom paint operation is a waste of time.
*/
public class OverdrawDetector extends LayoutDetector implements UastScanner {
private static final String R_STYLE_PREFIX = "R.style."; //$NON-NLS-1$
public class OverdrawDetector extends LayoutDetector implements Detector.UastScanner {
private static final String SET_THEME = "setTheme"; //$NON-NLS-1$
/** The main issue discovered by this detector */
@@ -97,7 +127,7 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner {
Severity.WARNING,
new Implementation(
OverdrawDetector.class,
EnumSet.of(Scope.MANIFEST, Scope.SOURCE_FILE, Scope.ALL_RESOURCE_FILES)));
EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE, Scope.ALL_RESOURCE_FILES)));
/** Mapping from FQN activity names to theme names registered in the manifest */
private Map<String, String> mActivityToTheme;
@@ -111,10 +141,6 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner {
/** List of theme names registered in the project which have blank backgrounds */
private List<String> mBlankThemes;
/** Set of activities registered in the manifest. We will limit the Java analysis to
* these. */
private Set<String> mActivities;
/** List of drawable resources that are not flagged for overdraw (XML drawables
* except for {@code <bitmap>} drawables without tiling) */
private List<String> mValidDrawables;
@@ -140,17 +166,6 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner {
|| folderType == ResourceFolderType.DRAWABLE;
}
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return LintUtils.isXmlFile(file) || LintUtils.endsWith(file.getName(), DOT_JAVA);
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
/** Is the given theme a "blank" theme (one not painting its background) */
private boolean isBlankTheme(String name) {
if (name.startsWith("@android:style/Theme_")) { //$NON-NLS-1$
@@ -384,11 +399,6 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner {
}
}
if (mActivities == null) {
mActivities = new HashSet<String>();
}
mActivities.add(name);
String theme = element.getAttributeNS(ANDROID_URI, ATTR_THEME);
if (theme != null && !theme.isEmpty()) {
if (mActivityToTheme == null) {
@@ -470,72 +480,75 @@ public class OverdrawDetector extends LayoutDetector implements UastScanner {
// ---- Implements UastScanner ----
@Nullable
@Override
public UastVisitor createUastVisitor(@NonNull UastAndroidContext context) {
if (!context.getLintContext().getProject().getReportIssues()) {
return null;
public List<String> applicableSuperClasses() {
return Collections.singletonList("android.app.Activity");
}
@Nullable
@Override
public List<Class<? extends UElement>> getApplicableUastTypes() {
return Collections.<Class<? extends UElement>>singletonList(UClass.class);
}
@Override
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
if (!context.getProject().getReportIssues()) {
return;
}
String name = declaration.getQualifiedName();
if (name != null) {
declaration.accept(new OverdrawVisitor(name, declaration));
}
return new OverdrawVisitor();
}
private class OverdrawVisitor extends AbstractUastVisitor {
private static final String ACTIVITY = "Activity"; //$NON-NLS-1$
private String mClassFqn;
private final String mName;
private final PsiClass mCls;
@Override
public boolean visitClass(@NotNull UClass node) {
String name = node.getName();
if (mActivities != null && mActivities.contains(mClassFqn) || name.endsWith(ACTIVITY)
|| node.isSubclassOf(CLASS_ACTIVITY)) {
mClassFqn = node.getFqName();
return false;
}
return true; // Done: No need to look inside this class
public OverdrawVisitor(String name, PsiClass cls) {
mName = name;
mCls = cls;
}
// Store R.layout references in activity classes in a map mapping back layouts
// to activities
@Override
public boolean visitQualifiedExpression(@NotNull UQualifiedExpression node) {
if (node.getSelector().renderString().equals("layout") //$NON-NLS-1$
&& node.getReceiver() instanceof USimpleReferenceExpression
&& ((USimpleReferenceExpression) node.getReceiver()).getIdentifier()
.equals("R") //$NON-NLS-1$
&& node.getParent() instanceof UQualifiedExpression) {
String layout = ((UQualifiedExpression) node.getParent()).getSelector().renderString();
registerLayoutActivity(layout, mClassFqn);
public boolean visitClass(UClass node) {
// Don't go into inner classes
if (mCls.equals(node.getPsi())) {
return true;
}
return false;
return super.visitClass(node);
}
@Override
public boolean visitSimpleNameReferenceExpression(USimpleNameReferenceExpression node) {
AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(node);
if (androidReference != null && androidReference.getType() == ResourceType.LAYOUT) {
registerLayoutActivity(androidReference.getName(), mName);
}
// Look for setTheme(R.style.whatever) and register as a theme registration
// for the current activity
return super.visitSimpleNameReferenceExpression(node);
}
@Override
public boolean visitCallExpression(@NotNull UCallExpression node) {
if (SET_THEME.equals(node.getFunctionName())) {
public boolean visitCallExpression(UCallExpression node) {
if (SET_THEME.equals(node.getMethodName()) && node.getValueArgumentCount() == 1) {
// Look at argument
List<UExpression> args = node.getValueArguments();
if (args.size() == 1) {
UExpression arg = args.get(0);
if (arg instanceof UQualifiedExpression) {
String resource = arg.renderString();
if (resource.startsWith(R_STYLE_PREFIX)) {
if (mActivityToTheme == null) {
mActivityToTheme = new HashMap<String, String>();
}
String name = ((UQualifiedExpression) arg).getSelector().renderString();
mActivityToTheme.put(mClassFqn, STYLE_RESOURCE_PREFIX + name);
}
UExpression arg = node.getValueArguments().get(0);
AndroidReference androidReference = UastLintUtils.toAndroidReferenceViaResolve(arg);
if (androidReference != null && androidReference.getType() == ResourceType.STYLE) {
String style = androidReference.getName();
if (mActivityToTheme == null) {
mActivityToTheme = new HashMap<String, String>();
}
mActivityToTheme.put(mName, STYLE_RESOURCE_PREFIX + style);
}
}
return false;
return super.visitCallExpression(node);
}
}
}
@@ -16,26 +16,32 @@
package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.UClass;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
/**
* Checks that subclasses of certain APIs are overriding all methods that were abstract
* in one or more earlier API levels that are still targeted by the minSdkVersion
* of this project.
*/
public class OverrideConcreteDetector extends Detector implements UastScanner {
public class OverrideConcreteDetector extends Detector implements Detector.UastScanner {
/** Are previously-abstract methods all overridden? */
public static final Issue ISSUE = Issue.create(
"OverrideAbstract", //$NON-NLS-1$
@@ -55,7 +61,7 @@ public class OverrideConcreteDetector extends Detector implements UastScanner {
Severity.FATAL,
new Implementation(
OverrideConcreteDetector.class,
Scope.SOURCE_FILE_SCOPE)
Scope.JAVA_FILE_SCOPE)
);
// This check is currently hardcoded for the specific case of the
@@ -76,24 +82,22 @@ public class OverrideConcreteDetector extends Detector implements UastScanner {
public OverrideConcreteDetector() {
}
// ---- Implements UastScanner ----
// ---- Implements JavaScanner ----
@Nullable
@Override
public List<String> getApplicableSuperClasses() {
public List<String> applicableSuperClasses() {
return Collections.singletonList(NOTIFICATION_LISTENER_SERVICE_FQN);
}
@Override
public void visitClass(UastAndroidContext context, UClass node) {
if (node == null) {
return;
}
if (node.hasModifier(UastModifier.ABSTRACT)) {
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.isAbstract(declaration)) {
return;
}
int minSdk = Math.max(context.getLintContext().getProject().getMinSdk(), getTargetApi(node));
int minSdk = Math.max(context.getProject().getMinSdk(), getTargetApi(declaration));
if (minSdk >= CONCRETE_IN) {
return;
}
@@ -101,28 +105,26 @@ public class OverrideConcreteDetector extends Detector implements UastScanner {
String[] methodNames = {ON_NOTIFICATION_POSTED, ON_NOTIFICATION_REMOVED};
for (String methodName : methodNames) {
boolean found = false;
List<UFunction> allFunctions = UastUtils.getAllFunctions(node, context);
for (UFunction method : allFunctions) {
if (!method.matchesName(methodName)) {
continue;
}
for (PsiMethod method : declaration.findMethodsByName(methodName, true)) {
// Make sure it's not the base method, but that it's been defined
// in a subclass, concretely
UClass containingClass = UastUtils.getContainingClassOrEmpty(method);
if (containingClass.matchesFqName(NOTIFICATION_LISTENER_SERVICE_FQN)) {
PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
continue;
}
if (NOTIFICATION_LISTENER_SERVICE_FQN.equals(containingClass.getQualifiedName())) {
continue;
}
// Make sure subclass isn't just defining another abstract definition
// of the method
if (method.hasModifier(UastModifier.ABSTRACT)) {
if (evaluator.isAbstract(method)) {
continue;
}
// Make sure it has the exact right signature
if (method.getValueParameterCount() != 1) {
if (method.getParameterList().getParametersCount() != 1) {
continue; // Wrong signature
}
if (!method.getValueParameters().get(0).getType().matchesFqName(STATUS_BAR_NOTIFICATION_FQN)) {
if (!evaluator.parameterHasType(method, 0, STATUS_BAR_NOTIFICATION_FQN)) {
continue;
}
@@ -132,25 +134,24 @@ public class OverrideConcreteDetector extends Detector implements UastScanner {
if (!found) {
String message = String.format(
"Must override `%1$s.%2$s(%3$s)`: Method was abstract until %4$d, and your `minSdkVersion` is %5$d",
NOTIFICATION_LISTENER_SERVICE_FQN, methodName,
STATUS_BAR_NOTIFICATION_FQN, CONCRETE_IN, minSdk);
context.report(ISSUE, node, context.getLocation(node.getNameElement()),
message);
"Must override `%1$s.%2$s(%3$s)`: Method was abstract until %4$d, and your `minSdkVersion` is %5$d",
NOTIFICATION_LISTENER_SERVICE_FQN, methodName,
STATUS_BAR_NOTIFICATION_FQN, CONCRETE_IN, minSdk);
context.reportUast(ISSUE, declaration, context.getUastNameLocation(declaration), message);
break;
}
}
}
private static int getTargetApi(UClass node) {
private static int getTargetApi(@NonNull PsiClass node) {
while (node != null) {
int targetApi = ApiDetector.Companion.getTargetApi(node.getAnnotations());
int targetApi = ApiDetector.getTargetApi(node.getModifierList());
if (targetApi != -1) {
return targetApi;
}
node = UastUtils.getContainingClass(node);
node = PsiTreeUtil.getParentOfType(node, PsiClass.class, true);
}
return -1;
@@ -15,27 +15,33 @@
*/
package com.android.tools.klint.checks;
import static com.android.SdkConstants.CLASS_PARCELABLE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiModifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
import org.jetbrains.uast.visitor.UastVisitor;
import org.jetbrains.uast.UAnonymousClass;
import org.jetbrains.uast.UClass;
import java.util.Collections;
import java.util.List;
/**
* Looks for Parcelable classes that are missing a CREATOR field
*/
public class ParcelDetector extends Detector implements UastScanner {
public class ParcelDetector extends Detector implements Detector.UastScanner {
/** The main issue discovered by this detector */
public static final Issue ISSUE = Issue.create(
@@ -45,81 +51,55 @@ public class ParcelDetector extends Detector implements UastScanner {
"According to the `Parcelable` interface documentation, " +
"\"Classes implementing the Parcelable interface must also have a " +
"static field called `CREATOR`, which is an object implementing the " +
"`Parcelable.Creator` interface.",
"`Parcelable.Creator` interface.\"",
Category.USABILITY,
Category.CORRECTNESS,
3,
Severity.ERROR,
new Implementation(
ParcelDetector.class,
Scope.SOURCE_FILE_SCOPE))
Scope.JAVA_FILE_SCOPE))
.addMoreInfo("http://developer.android.com/reference/android/os/Parcelable.html");
/** Constructs a new {@link ParcelDetector} check */
public ParcelDetector() {
}
@NonNull
// ---- Implements JavaScanner ----
@Nullable
@Override
public Speed getSpeed() {
return Speed.FAST;
public List<String> applicableSuperClasses() {
return Collections.singletonList(CLASS_PARCELABLE);
}
// ---- Implements UastScanner ----
@Override
public UastVisitor createUastVisitor(UastAndroidContext context) {
return new ParcelVisitor(context);
}
private static class ParcelVisitor extends AbstractUastVisitor {
private final UastAndroidContext mContext;
public ParcelVisitor(UastAndroidContext context) {
mContext = context;
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
if (declaration instanceof UAnonymousClass) {
// Anonymous classes aren't parcelable
return;
}
@Override
public boolean visitClass(@NotNull UClass node) {
// Only applies to concrete classes
if (node.getKind() != UastClassKind.CLASS || node.hasModifier(UastModifier.ABSTRACT)) {
return true;
}
for (UType reference : node.getSuperTypes()) {
String name = reference.getName();
if (name.equals("Parcelable")) {
UVariable field = UastUtils.findStaticMemberOfType(node, "CREATOR", UVariable.class);
boolean hasField = field != null && field.hasModifier(UastModifier.JVM_FIELD) && field.hasModifier(UastModifier.STATIC);
boolean hasNamedCompanionObject = false;
if (!hasField) {
for (UClass companion : node.getCompanions()) {
if (companion.getName().equals("CREATOR")) {
hasNamedCompanionObject = true;
break;
}
}
}
if (!hasField && !hasNamedCompanionObject) {
// Make doubly sure that we're really implementing
// android.os.Parcelable
UClass parcelable = reference.resolve(mContext);
if (parcelable != null) {
if (!parcelable.isSubclassOf("android.os.Parcelable")) {
return true;
}
}
Location location = mContext.getLocation(node.getNameElement());
mContext.report(ISSUE, node, location,
"This class implements `Parcelable` but does not "
+ "provide a `CREATOR` field");
}
}
}
return true;
// Only applies to concrete classes
if (declaration.isInterface()) {
return;
}
if (declaration.hasModifierProperty(PsiModifier.ABSTRACT)) {
return;
}
// Parceling spans is handled in TextUtils#CHAR_SEQUENCE_CREATOR
if (context.getEvaluator().implementsInterface(declaration,
"android.text.ParcelableSpan", false)) {
return;
}
PsiField field = declaration.findFieldByName("CREATOR", false);
if (field == null) {
Location location = context.getUastNameLocation(declaration);
context.reportUast(ISSUE, declaration, location,
"This class implements `Parcelable` but does not "
+ "provide a `CREATOR` field");
}
}
}
@@ -0,0 +1,254 @@
/*
* Copyright (C) 2015 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 com.android.tools.klint.checks;
import static com.android.SdkConstants.CLASS_INTENT;
import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION;
import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_READ;
import static com.android.tools.klint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION_WRITE;
import static org.jetbrains.uast.UastBinaryExpressionWithTypeKind.TYPE_CAST;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.UastLintUtils;
import com.android.tools.klint.detector.api.JavaContext;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiConditionalExpression;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionList;
import com.intellij.psi.PsiExpressionStatement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiNameValuePair;
import com.intellij.psi.PsiNewExpression;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiTypeCastExpression;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.uast.UBinaryExpressionWithType;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UIfExpression;
import org.jetbrains.uast.ULiteralExpression;
import org.jetbrains.uast.UParenthesizedExpression;
import org.jetbrains.uast.UastCallKind;
import org.jetbrains.uast.UastLiteralUtils;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.expressions.UReferenceExpression;
import org.jetbrains.uast.util.UastExpressionUtils;
import java.util.List;
/**
* Utility for locating permissions required by an intent or content resolver
*/
public class PermissionFinder {
/**
* Operation that has a permission requirement -- such as a method call,
* a content resolver read or write operation, an intent, etc.
*/
public enum Operation {
CALL, ACTION, READ, WRITE;
/** Prefix to use when describing a name with a permission requirement */
public String prefix() {
switch (this) {
case ACTION:
return "by intent";
case READ:
return "to read";
case WRITE:
return "to write";
case CALL:
default:
return "by";
}
}
}
/** A permission requirement given a name and operation */
public static class Result {
@NonNull public final PermissionRequirement requirement;
@NonNull public final String name;
@NonNull public final Operation operation;
public Result(
@NonNull Operation operation,
@NonNull PermissionRequirement requirement,
@NonNull String name) {
this.operation = operation;
this.requirement = requirement;
this.name = name;
}
}
/**
* Searches for a permission requirement for the given parameter in the given call
*
* @param operation the operation to look up
* @param context the context to use for lookup
* @param parameter the parameter which contains the value which implies the permission
* @return the result with the permission requirement, or null if nothing is found
*/
@Nullable
public static Result findRequiredPermissions(
@NonNull Operation operation,
@NonNull JavaContext context,
@NonNull UElement parameter) {
// To find the permission required by an intent, we proceed in 3 steps:
// (1) Locate the parameter in the start call that corresponds to
// the Intent
//
// (2) Find the place where the intent is initialized, and figure
// out the action name being passed to it.
//
// (3) Find the place where the action is defined, and look for permission
// annotations on that action declaration!
return new PermissionFinder(context, operation).search(parameter);
}
private PermissionFinder(@NonNull JavaContext context, @NonNull Operation operation) {
mContext = context;
mOperation = operation;
}
@NonNull private final JavaContext mContext;
@NonNull private final Operation mOperation;
@Nullable
public Result search(@NonNull UElement node) {
if (UastLiteralUtils.isNullLiteral(node)) {
return null;
} else if (node instanceof UIfExpression) {
UIfExpression expression = (UIfExpression) node;
if (expression.getThenExpression() != null) {
Result result = search(expression.getThenExpression());
if (result != null) {
return result;
}
}
if (expression.getElseExpression() != null) {
Result result = search(expression.getElseExpression());
if (result != null) {
return result;
}
}
} else if (UastExpressionUtils.isTypeCast(node)) {
UBinaryExpressionWithType cast = (UBinaryExpressionWithType) node;
UExpression operand = cast.getOperand();
return search(operand);
} else if (node instanceof UParenthesizedExpression) {
UParenthesizedExpression parens = (UParenthesizedExpression) node;
UExpression expression = parens.getExpression();
if (expression != null) {
return search(expression);
}
} else if (UastExpressionUtils.isConstructorCall(node) && mOperation == Operation.ACTION) {
// Identifies "new Intent(argument)" calls and, if found, continues
// resolving the argument instead looking for the action definition
UCallExpression call = (UCallExpression) node;
UReferenceExpression classReference = call.getClassReference();
String type = classReference != null ? UastUtils.getQualifiedName(classReference) : null;
if (CLASS_INTENT.equals(type)) {
List<UExpression> expressions = call.getValueArguments();
if (!expressions.isEmpty()) {
UExpression action = expressions.get(0);
if (action != null) {
return search(action);
}
}
}
return null;
} else if (node instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) node).resolve();
if (resolved instanceof PsiField) {
PsiField field = (PsiField) resolved;
if (mOperation == Operation.ACTION) {
PsiModifierList modifierList = field.getModifierList();
PsiAnnotation annotation = modifierList != null
? modifierList.findAnnotation(PERMISSION_ANNOTATION) : null;
if (annotation != null) {
return getPermissionRequirement(field, annotation);
}
} else if (mOperation == Operation.READ || mOperation == Operation.WRITE) {
String fqn = mOperation == Operation.READ
? PERMISSION_ANNOTATION_READ : PERMISSION_ANNOTATION_WRITE;
PsiModifierList modifierList = field.getModifierList();
PsiAnnotation annotation = modifierList != null
? modifierList.findAnnotation(fqn) : null;
if (annotation != null) {
PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes();
PsiNameValuePair o = attributes.length == 1 ? attributes[0] : null;
if (o != null && o.getValue() instanceof PsiAnnotation) {
annotation = (PsiAnnotation) o.getValue();
if (PERMISSION_ANNOTATION.equals(annotation.getQualifiedName())) {
return getPermissionRequirement(field, annotation);
}
} else {
// The complex annotations used for read/write cannot be
// expressed in the external annotations format, so they're inlined.
// (See Extractor.AnnotationData#write).
//
// Instead we've inlined the fields of the annotation on the
// outer one:
return getPermissionRequirement(field, annotation);
}
}
} else {
assert false : mOperation;
}
}
if (resolved instanceof PsiVariable) {
PsiVariable variable = (PsiVariable) resolved;
UExpression lastAssignment =
UastLintUtils.findLastAssignment(variable, node, mContext);
if (lastAssignment != null) {
return search(lastAssignment);
}
}
}
return null;
}
@NonNull
private Result getPermissionRequirement(
@NonNull PsiField field,
@NonNull PsiAnnotation annotation) {
PermissionRequirement requirement = PermissionRequirement.create(mContext, annotation);
PsiClass containingClass = field.getContainingClass();
String name = containingClass != null
? containingClass.getName() + "." + field.getName()
: field.getName();
assert name != null;
return new Result(mOperation, requirement, name);
}
}
@@ -18,6 +18,8 @@ package com.android.tools.klint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.sdklib.AndroidVersion;
import java.util.Collections;
import java.util.Set;
@@ -31,34 +33,67 @@ public interface PermissionHolder {
/** Returns true if the permission holder has been granted the given permission */
boolean hasPermission(@NonNull String permission);
/** Returns true if the given permission is known to be revocable for targetSdkVersion >= M */
/** Returns true if the given permission is known to be revocable for targetSdkVersion &ge; M */
boolean isRevocable(@NonNull String permission);
@NonNull
AndroidVersion getMinSdkVersion();
@NonNull
AndroidVersion getTargetSdkVersion();
/**
* A convenience implementation of {@link PermissionHolder} backed by a set
*/
class SetPermissionLookup implements PermissionHolder {
private Set<String> myGrantedPermissions;
private Set<String> myRevocablePermissions;
private final Set<String> mGrantedPermissions;
private final Set<String> mRevocablePermissions;
private final AndroidVersion mMinSdkVersion;
private final AndroidVersion mTargetSdkVersion;
public SetPermissionLookup(@NonNull Set<String> grantedPermissions,
@NonNull Set<String> revocablePermissions) {
myGrantedPermissions = grantedPermissions;
myRevocablePermissions = revocablePermissions;
public SetPermissionLookup(
@NonNull Set<String> grantedPermissions,
@NonNull Set<String> revocablePermissions,
@NonNull AndroidVersion minSdkVersion,
@NonNull AndroidVersion targetSdkVersion) {
mGrantedPermissions = grantedPermissions;
mRevocablePermissions = revocablePermissions;
mMinSdkVersion = minSdkVersion;
mTargetSdkVersion = targetSdkVersion;
}
@VisibleForTesting
public SetPermissionLookup(@NonNull Set<String> grantedPermissions,
@NonNull Set<String> revocablePermissions) {
this(grantedPermissions, revocablePermissions, AndroidVersion.DEFAULT,
AndroidVersion.DEFAULT);
}
@VisibleForTesting
public SetPermissionLookup(@NonNull Set<String> grantedPermissions) {
this(grantedPermissions, Collections.<String>emptySet());
}
@Override
public boolean hasPermission(@NonNull String permission) {
return myGrantedPermissions.contains(permission);
return mGrantedPermissions.contains(permission);
}
@Override
public boolean isRevocable(@NonNull String permission) {
return myRevocablePermissions.contains(permission);
return mRevocablePermissions.contains(permission);
}
@NonNull
@Override
public AndroidVersion getMinSdkVersion() {
return mMinSdkVersion;
}
@NonNull
@Override
public AndroidVersion getTargetSdkVersion() {
return mTargetSdkVersion;
}
/**
@@ -69,7 +104,9 @@ public interface PermissionHolder {
@NonNull
public static PermissionHolder join(@NonNull PermissionHolder lookup,
@NonNull PermissionRequirement requirement) {
SetPermissionLookup empty = new SetPermissionLookup(Collections.<String>emptySet());
SetPermissionLookup empty = new SetPermissionLookup(Collections.<String>emptySet(),
Collections.<String>emptySet(), lookup.getMinSdkVersion(),
lookup.getTargetSdkVersion());
return join(lookup, requirement.getMissingPermissions(empty));
}
@@ -92,6 +129,18 @@ public interface PermissionHolder {
public boolean isRevocable(@NonNull String permission) {
return lookup.isRevocable(permission);
}
@NonNull
@Override
public AndroidVersion getMinSdkVersion() {
return lookup.getMinSdkVersion();
}
@NonNull
@Override
public AndroidVersion getTargetSdkVersion() {
return lookup.getTargetSdkVersion();
}
};
}
return lookup;
@@ -24,23 +24,21 @@ import static com.android.tools.klint.checks.SupportAnnotationDetector.ATTR_COND
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.klint.detector.api.Context;
import com.android.sdklib.AndroidVersion;
import com.android.tools.klint.detector.api.ConstantEvaluator;
import com.android.tools.klint.detector.api.JavaContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiArrayInitializerMemberValue;
import com.intellij.psi.tree.IElementType;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiElementFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.*;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.java.JavaUastLanguagePlugin;
import org.jetbrains.uast.visitor.AbstractUastVisitor;
/**
* A permission requirement is a boolean expression of permission names that a
@@ -50,7 +48,9 @@ public abstract class PermissionRequirement {
public static final String ATTR_PROTECTION_LEVEL = "protectionLevel"; //$NON-NLS-1$
public static final String VALUE_DANGEROUS = "dangerous"; //$NON-NLS-1$
private final UAnnotation annotation;
protected final PsiAnnotation annotation;
private int firstApi;
private int lastApi;
@SuppressWarnings("ConstantConditions")
public static final PermissionRequirement NONE = new PermissionRequirement(null) {
@@ -59,6 +59,11 @@ public abstract class PermissionRequirement {
return true;
}
@Override
public boolean appliesTo(@NonNull PermissionHolder available) {
return false;
}
@Override
public boolean isConditional() {
return false;
@@ -86,7 +91,7 @@ public abstract class PermissionRequirement {
@Nullable
@Override
public UastBinaryOperator getOperator() {
public IElementType getOperator() {
return null;
}
@@ -97,56 +102,227 @@ public abstract class PermissionRequirement {
}
};
private PermissionRequirement(@NonNull UAnnotation annotation) {
private PermissionRequirement(@NonNull PsiAnnotation annotation) {
this.annotation = annotation;
}
@NonNull
public static PermissionRequirement create(
@Nullable UastAndroidContext context,
@NonNull UAnnotation annotation) {
String value = (String)annotation.getValue(ATTR_VALUE);
if (value != null && !value.isEmpty()) {
for (int i = 0, n = value.length(); i < n; i++) {
char c = value.charAt(i);
// See if it's a complex expression and if so build it up
if (c == '&' || c == '|' || c == '^') {
return Complex.parse(annotation, context, value);
}
}
@NonNull JavaContext context,
@NonNull PsiAnnotation annotation) {
String value = getAnnotationStringValue(annotation, ATTR_VALUE);
if (value != null && !value.isEmpty()) {
return new Single(annotation, value);
}
Object v = annotation.getValue(ATTR_ANY_OF);
if (v != null) {
if (v instanceof String[]) {
String[] anyOf = (String[])v;
if (anyOf.length > 0) {
return new Many(annotation, UastBinaryOperator.LOGICAL_OR, anyOf);
}
} else if (v instanceof String) {
String[] anyOf = new String[] { (String)v };
return new Many(annotation, UastBinaryOperator.LOGICAL_OR, anyOf);
String[] anyOf = getAnnotationStringValues(annotation, ATTR_ANY_OF);
if (anyOf != null) {
if (anyOf.length > 1) {
return new Many(annotation, JavaTokenType.OROR, anyOf);
} else if (anyOf.length == 1) {
return new Single(annotation, anyOf[0]);
}
}
v = annotation.getValue(ATTR_ALL_OF);
if (v != null) {
if (v instanceof String[]) {
String[] allOf = (String[])v;
if (allOf.length > 0) {
return new Many(annotation, UastBinaryOperator.LOGICAL_AND, allOf);
}
} else if (v instanceof String) {
String[] allOf = new String[] { (String)v };
return new Many(annotation, UastBinaryOperator.LOGICAL_AND, allOf);
String[] allOf = getAnnotationStringValues(annotation, ATTR_ALL_OF);
if (allOf != null) {
if (allOf.length > 1) {
return new Many(annotation, JavaTokenType.ANDAND, allOf);
} else if (allOf.length == 1) {
return new Single(annotation, allOf[0]);
}
}
return NONE;
}
@Nullable
public static Boolean getAnnotationBooleanValue(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof Boolean) {
return (Boolean) o;
}
}
}
return null;
}
@Nullable
public static Long getAnnotationLongValue(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof Number) {
return ((Number)o).longValue();
}
}
}
return null;
}
@Nullable
public static Double getAnnotationDoubleValue(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof Number) {
return ((Number)o).doubleValue();
}
}
}
return null;
}
@Nullable
public static String getAnnotationStringValue(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof String) {
return (String) o;
}
}
}
return null;
}
@Nullable
public static String[] getAnnotationStringValues(@Nullable PsiAnnotation annotation,
@NonNull String name) {
if (annotation != null) {
PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
if (attributeValue == null && ATTR_VALUE.equals(name)) {
attributeValue = annotation.findDeclaredAttributeValue(null);
}
if (attributeValue instanceof PsiArrayInitializerMemberValue) {
PsiAnnotationMemberValue[] initializers =
((PsiArrayInitializerMemberValue) attributeValue).getInitializers();
List<String> result = Lists.newArrayListWithCapacity(initializers.length);
ConstantEvaluator constantEvaluator = new ConstantEvaluator(null);
for (PsiAnnotationMemberValue element : initializers) {
Object o = constantEvaluator.evaluate(element);
if (o instanceof String) {
result.add((String)o);
}
}
if (result.isEmpty()) {
return null;
} else {
return result.toArray(new String[0]);
}
} else {
// Use constant evaluator since we want to resolve field references as well
if (attributeValue != null) {
Object o = ConstantEvaluator.evaluate(null, attributeValue);
if (o instanceof String) {
return new String[]{(String) o};
} else if (o instanceof String[]) {
return (String[])o;
} else if (o instanceof Object[]) {
Object[] array = (Object[]) o;
List<String> strings = Lists.newArrayListWithCapacity(array.length);
for (Object element : array) {
if (element instanceof String) {
strings.add((String) element);
}
}
return strings.toArray(new String[0]);
}
}
}
}
return null;
}
/**
* Returns false if this permission does not apply given the specified minimum and
* target sdk versions
*
* @param minSdkVersion the minimum SDK version
* @param targetSdkVersion the target SDK version
* @return true if this permission requirement applies for the given versions
*/
/**
* Returns false if this permission does not apply given the specified minimum and target
* sdk versions
*
* @param available the permission holder which also knows the min and target versions
* @return true if this permission requirement applies for the given versions
*/
protected boolean appliesTo(@NonNull PermissionHolder available) {
if (firstApi == 0) { // initialized?
firstApi = -1; // initialized, not specified
// Not initialized
String range = getAnnotationStringValue(annotation, "apis");
if (range != null) {
// Currently only support the syntax "a..b" where a and b are inclusive end points
// and where "a" and "b" are optional
int index = range.indexOf("..");
if (index != -1) {
try {
if (index > 0) {
firstApi = Integer.parseInt(range.substring(0, index));
} else {
firstApi = 1;
}
if (index + 2 < range.length()) {
lastApi = Integer.parseInt(range.substring(index + 2));
} else {
lastApi = Integer.MAX_VALUE;
}
} catch (NumberFormatException ignore) {
}
}
}
}
if (firstApi != -1) {
AndroidVersion minSdkVersion = available.getMinSdkVersion();
if (minSdkVersion.getFeatureLevel() > lastApi) {
return false;
}
AndroidVersion targetSdkVersion = available.getTargetSdkVersion();
if (targetSdkVersion.getFeatureLevel() < firstApi) {
return false;
}
}
return true;
}
/**
* Returns whether this requirement is conditional, meaning that there are
* some circumstances in which the requirement is not necessary. For
@@ -165,9 +341,9 @@ public abstract class PermissionRequirement {
* @return true if this requirement is conditional
*/
public boolean isConditional() {
Object o = annotation.getValue(ATTR_CONDITIONAL);
if (o instanceof Boolean) {
return (Boolean)o;
Boolean o = getAnnotationBooleanValue(annotation, ATTR_CONDITIONAL);
if (o != null) {
return o;
}
return false;
}
@@ -228,7 +404,7 @@ public abstract class PermissionRequirement {
* for leaf nodes
*/
@Nullable
public abstract UastBinaryOperator getOperator();
public abstract IElementType getOperator();
/**
* Returns nested requirements, combined via {@link #getOperator()}
@@ -240,7 +416,7 @@ public abstract class PermissionRequirement {
private static class Single extends PermissionRequirement {
public final String name;
public Single(@NonNull UAnnotation annotation, @NonNull String name) {
public Single(@NonNull PsiAnnotation annotation, @NonNull String name) {
super(annotation);
this.name = name;
}
@@ -252,7 +428,7 @@ public abstract class PermissionRequirement {
@Nullable
@Override
public UastBinaryOperator getOperator() {
public IElementType getOperator() {
return null;
}
@@ -274,7 +450,7 @@ public abstract class PermissionRequirement {
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
return available.hasPermission(name);
return available.hasPermission(name) || !appliesTo(available);
}
@Override
@@ -299,14 +475,14 @@ public abstract class PermissionRequirement {
}
}
protected static void appendOperator(StringBuilder sb, UastBinaryOperator operator) {
protected static void appendOperator(StringBuilder sb, IElementType operator) {
sb.append(' ');
if (operator == UastBinaryOperator.LOGICAL_AND) {
if (operator == JavaTokenType.ANDAND) {
sb.append("and");
} else if (operator == UastBinaryOperator.LOGICAL_OR) {
} else if (operator == JavaTokenType.OROR) {
sb.append("or");
} else {
assert operator == UastBinaryOperator.BITWISE_XOR : operator;
assert operator == JavaTokenType.XOR : operator;
sb.append("xor");
}
sb.append(' ');
@@ -316,16 +492,16 @@ public abstract class PermissionRequirement {
* Require a series of permissions, all with the same operator.
*/
private static class Many extends PermissionRequirement {
public final UastBinaryOperator operator;
public final IElementType operator;
public final List<PermissionRequirement> permissions;
public Many(
@NonNull UAnnotation annotation,
UastBinaryOperator operator,
@NonNull PsiAnnotation annotation,
IElementType operator,
String[] names) {
super(annotation);
assert operator == UastBinaryOperator.LOGICAL_OR
|| operator == UastBinaryOperator.LOGICAL_AND : operator;
assert operator == JavaTokenType.OROR
|| operator == JavaTokenType.ANDAND : operator;
assert names.length >= 2;
this.operator = operator;
this.permissions = Lists.newArrayListWithExpectedSize(names.length);
@@ -355,17 +531,17 @@ public abstract class PermissionRequirement {
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
if (operator == UastBinaryOperator.LOGICAL_AND) {
if (operator == JavaTokenType.ANDAND) {
for (PermissionRequirement requirement : permissions) {
if (!requirement.isSatisfied(available)) {
if (!requirement.isSatisfied(available) && requirement.appliesTo(available)) {
return false;
}
}
return true;
} else {
assert operator == UastBinaryOperator.LOGICAL_OR : operator;
assert operator == JavaTokenType.OROR : operator;
for (PermissionRequirement requirement : permissions) {
if (requirement.isSatisfied(available)) {
if (requirement.isSatisfied(available) || !requirement.appliesTo(available)) {
return true;
}
}
@@ -411,7 +587,7 @@ public abstract class PermissionRequirement {
@Override
public boolean isRevocable(@NonNull PermissionHolder revocable) {
// TODO: Pass in the available set of permissions here, and if
// the operator is BinaryOperator.LOGICAL_OR, only return revocable=true
// the operator is JavaTokenType.OROR, only return revocable=true
// if an unsatisfied permission is also revocable. In other words,
// if multiple permissions are allowed, and some of them are satisfied and
// not revocable the overall permission requirement is not revocable.
@@ -425,7 +601,7 @@ public abstract class PermissionRequirement {
@Nullable
@Override
public UastBinaryOperator getOperator() {
public IElementType getOperator() {
return operator;
}
@@ -436,211 +612,9 @@ public abstract class PermissionRequirement {
}
}
/**
* Require multiple permissions. This is a group of permissions with some
* associated boolean logic, such as "B or (C and (D or E))".
*/
private static class Complex extends PermissionRequirement {
public final UastBinaryOperator operator;
public final PermissionRequirement left;
public final PermissionRequirement right;
public Complex(
@NonNull UAnnotation annotation,
UastBinaryOperator operator,
PermissionRequirement left,
PermissionRequirement right) {
super(annotation);
this.operator = operator;
this.left = left;
this.right = right;
}
@Override
public boolean isSingle() {
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
boolean needsParentheses = left instanceof Complex &&
((Complex) left).operator != UastBinaryOperator.LOGICAL_AND;
if (needsParentheses) {
sb.append('(');
}
sb.append(left.toString());
if (needsParentheses) {
sb.append(')');
}
appendOperator(sb, operator);
needsParentheses = right instanceof Complex &&
((Complex) right).operator != UastBinaryOperator.LOGICAL_AND;
if (needsParentheses) {
sb.append('(');
}
sb.append(right.toString());
if (needsParentheses) {
sb.append(')');
}
return sb.toString();
}
@Override
public boolean isSatisfied(@NonNull PermissionHolder available) {
boolean satisfiedLeft = left.isSatisfied(available);
boolean satisfiedRight = right.isSatisfied(available);
if (operator == UastBinaryOperator.LOGICAL_AND) {
return satisfiedLeft && satisfiedRight;
} else if (operator == UastBinaryOperator.LOGICAL_OR) {
return satisfiedLeft || satisfiedRight;
} else {
assert operator == UastBinaryOperator.BITWISE_XOR : operator;
return satisfiedLeft ^ satisfiedRight;
}
}
@Override
public String describeMissingPermissions(@NonNull PermissionHolder available) {
boolean satisfiedLeft = left.isSatisfied(available);
boolean satisfiedRight = right.isSatisfied(available);
if (operator == UastBinaryOperator.LOGICAL_AND || operator == UastBinaryOperator.LOGICAL_OR) {
if (satisfiedLeft) {
if (satisfiedRight) {
return "";
}
return right.describeMissingPermissions(available);
} else if (satisfiedRight) {
return left.describeMissingPermissions(available);
} else {
StringBuilder sb = new StringBuilder();
sb.append(left.describeMissingPermissions(available));
appendOperator(sb, operator);
sb.append(right.describeMissingPermissions(available));
return sb.toString();
}
} else {
assert operator == UastBinaryOperator.BITWISE_XOR : operator;
return toString();
}
}
@Override
protected void addMissingPermissions(@NonNull PermissionHolder available,
@NonNull Set<String> missing) {
boolean satisfiedLeft = left.isSatisfied(available);
boolean satisfiedRight = right.isSatisfied(available);
if (operator == UastBinaryOperator.LOGICAL_AND || operator == UastBinaryOperator.LOGICAL_OR) {
if (satisfiedLeft) {
if (satisfiedRight) {
return;
}
right.addMissingPermissions(available, missing);
} else if (satisfiedRight) {
left.addMissingPermissions(available, missing);
} else {
left.addMissingPermissions(available, missing);
right.addMissingPermissions(available, missing);
}
} else {
assert operator == UastBinaryOperator.BITWISE_XOR : operator;
left.addMissingPermissions(available, missing);
right.addMissingPermissions(available, missing);
}
}
@Override
protected void addRevocablePermissions(@NonNull Set<String> result,
@NonNull PermissionHolder revocable) {
left.addRevocablePermissions(result, revocable);
right.addRevocablePermissions(result, revocable);
}
@Override
public boolean isRevocable(@NonNull PermissionHolder revocable) {
// TODO: If operator == BinaryOperator.LOGICAL_OR only return
// revocable the there isn't a non-revocable term which is also satisfied.
return left.isRevocable(revocable) || right.isRevocable(revocable);
}
@NonNull
public static PermissionRequirement parse(@NonNull UAnnotation annotation,
@Nullable UastAndroidContext uastContext, @NonNull final String value) {
// Parse an expression of the form (A op1 B op2 C) op3 (D op4 E) etc.
// We'll just use the Java parser to handle this to ensure that operator
// precedence etc is correct.
if (uastContext == null) {
return NONE;
}
Context context = uastContext.getLintContext();
PsiElementFactory factory = JavaPsiFacade.getInstance(
context.getClient().getProject()).getElementFactory();
UElement node = JavaUastLanguagePlugin.INSTANCE.getConverter().
convertWithParent(factory.createClassFromText(
"class Test { void test() {\n"
+ "boolean result = " + value
+ ";\n}\n}"
, null).getContainingFile());
if (node != null) {
final AtomicReference<UExpression> reference = new AtomicReference<UExpression>();
node.accept(new AbstractUastVisitor() {
@Override
public boolean visitVariable(@NotNull UVariable node) {
reference.set(node.getInitializer());
return true;
}
});
UExpression expression = reference.get();
if (expression != null) {
return parse(annotation, expression);
}
}
return NONE;
}
private static PermissionRequirement parse(
@NonNull UAnnotation annotation,
@NonNull UExpression expression) {
if (expression instanceof UQualifiedExpression) {
return new Single(annotation, expression.renderString());
} else if (expression instanceof UBinaryExpression) {
UBinaryExpression binaryExpression = (UBinaryExpression) expression;
UastBinaryOperator operator = binaryExpression.getOperator();
if (operator == UastBinaryOperator.LOGICAL_AND
|| operator == UastBinaryOperator.LOGICAL_OR
|| operator == UastBinaryOperator.BITWISE_XOR) {
PermissionRequirement left = parse(annotation, binaryExpression.getLeftOperand());
PermissionRequirement right = parse(annotation, binaryExpression.getRightOperand());
return new Complex(annotation, operator, left, right);
}
}
return NONE;
}
@Nullable
@Override
public UastBinaryOperator getOperator() {
return operator;
}
@NonNull
@Override
public Iterable<PermissionRequirement> getChildren() {
return Arrays.asList(left, right);
}
}
/**
* Returns true if the given permission name is a revocable permission for
* targetSdkVersion >= 23
* targetSdkVersion &ge; 23
*
* @param name permission name
* @return true if this is a revocable permission
@@ -661,6 +635,7 @@ public abstract class PermissionRequirement {
"android.permission.READ_CALL_LOG",
"android.permission.READ_CELL_BROADCASTS",
"android.permission.READ_CONTACTS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.READ_PHONE_STATE",
"android.permission.READ_PROFILE",
"android.permission.READ_SMS",
@@ -675,6 +650,8 @@ public abstract class PermissionRequirement {
"android.permission.WRITE_CALENDAR",
"android.permission.WRITE_CALL_LOG",
"android.permission.WRITE_CONTACTS",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.WRITE_SETTINGS",
"android.permission.WRITE_PROFILE",
"android.permission.WRITE_SOCIAL_STREAM",
"com.android.voicemail.permission.ADD_VOICEMAIL",
@@ -0,0 +1,333 @@
/*
* Copyright (C) 2014 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 com.android.tools.klint.checks;
import static com.android.tools.klint.checks.PluralsDatabase.Quantity.few;
import static com.android.tools.klint.checks.PluralsDatabase.Quantity.many;
import static com.android.tools.klint.checks.PluralsDatabase.Quantity.one;
import static com.android.tools.klint.checks.PluralsDatabase.Quantity.two;
import static com.android.tools.klint.checks.PluralsDatabase.Quantity.zero;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.detector.api.LintUtils;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Database used by the {@link PluralsDetector} to get information
* about plural forms for a given language
*/
public class PluralsDatabase {
private static final EnumSet<Quantity> NONE = EnumSet.noneOf(Quantity.class);
private static final PluralsDatabase sInstance = new PluralsDatabase();
private final Map<String, EnumSet<Quantity>> mPlurals = Maps.newHashMap();
/** Bit set if this language uses quantity zero */
@SuppressWarnings("PointlessBitwiseExpression")
static final int FLAG_ZERO = 1 << 0;
/** Bit set if this language uses quantity one */
static final int FLAG_ONE = 1 << 1;
/** Bit set if this language uses quantity two */
static final int FLAG_TWO = 1 << 2;
/** Bit set if this language uses quantity few */
static final int FLAG_FEW = 1 << 3;
/** Bit set if this language uses quantity many */
static final int FLAG_MANY = 1 << 4;
/** Bit set if this language has multiple values that match quantity zero */
static final int FLAG_MULTIPLE_ZERO = 1 << 5;
/** Bit set if this language has multiple values that match quantity one */
static final int FLAG_MULTIPLE_ONE = 1 << 6;
/** Bit set if this language has multiple values that match quantity two */
static final int FLAG_MULTIPLE_TWO = 1 << 7;
@NonNull
public static PluralsDatabase get() {
return sInstance;
}
private static int getFlags(@NonNull String language) {
int index = getLanguageIndex(language);
if (index != -1) {
return FLAGS[index];
}
return 0;
}
private static int getLanguageIndex(@NonNull String language) {
int index = Arrays.binarySearch(LANGUAGE_CODES, language);
if (index >= 0) {
assert LANGUAGE_CODES[index].equals(language);
return index;
} else {
return -1;
}
}
@Nullable
public EnumSet<Quantity> getRelevant(@NonNull String language) {
EnumSet<Quantity> set = mPlurals.get(language);
if (set == null) {
int index = getLanguageIndex(language);
if (index == -1) {
mPlurals.put(language, NONE);
return null;
}
// Process each item and look for relevance
int flag = FLAGS[index];
set = EnumSet.noneOf(Quantity.class);
if ((flag & FLAG_ZERO) != 0) {
set.add(zero);
}
if ((flag & FLAG_ONE) != 0) {
set.add(one);
}
if ((flag & FLAG_TWO) != 0) {
set.add(two);
}
if ((flag & FLAG_FEW) != 0) {
set.add(few);
}
if ((flag & FLAG_MANY) != 0) {
set.add(many);
}
mPlurals.put(language, set);
}
return set == NONE ? null : set;
}
@SuppressWarnings("MethodMayBeStatic")
public boolean hasMultipleValuesForQuantity(
@NonNull String language,
@NonNull Quantity quantity) {
if (quantity == one) {
return (getFlags(language) & FLAG_MULTIPLE_ONE) != 0;
} else if (quantity == two) {
return (getFlags(language) & FLAG_MULTIPLE_TWO) != 0;
} else {
return quantity == zero && (getFlags(language) & FLAG_MULTIPLE_ZERO) != 0;
}
}
@SuppressWarnings("MethodMayBeStatic")
@Nullable
public String findIntegerExamples(@NonNull String language, @NonNull Quantity quantity) {
if (quantity == one) {
return getExampleForQuantityOne(language);
} else if (quantity == two) {
return getExampleForQuantityTwo(language);
} else if (quantity == zero) {
return getExampleForQuantityZero(language);
} else {
return null;
}
}
public enum Quantity {
// deliberately lower case to match attribute names
few, many, one, two, zero, other;
@Nullable
public static Quantity get(@NonNull String name) {
for (Quantity quantity : values()) {
if (name.equals(quantity.name())) {
return quantity;
}
}
return null;
}
public static String formatSet(@NonNull EnumSet<Quantity> set) {
List<String> list = new ArrayList<String>(set.size());
for (Quantity quantity : set) {
list.add('`' + quantity.name() + '`');
}
return LintUtils.formatList(list, Integer.MAX_VALUE);
}
}
// GENERATED DATA.
// This data is generated by the #testDatabaseAccurate method in PluralsDatabaseTest
// which will generate the following if it can find an ICU plurals database file
// in the unit test data folder.
/** Set of language codes relevant to plurals data */
private static final String[] LANGUAGE_CODES = new String[] {
"af", "ak", "am", "ar", "as", "az", "be", "bg", "bh", "bm",
"bn", "bo", "br", "bs", "ca", "ce", "cs", "cy", "da", "de",
"dv", "dz", "ee", "el", "en", "eo", "es", "et", "eu", "fa",
"ff", "fi", "fo", "fr", "fy", "ga", "gd", "gl", "gu", "gv",
"ha", "he", "hi", "hr", "hu", "hy", "id", "ig", "ii", "in",
"is", "it", "iu", "iw", "ja", "ji", "jv", "ka", "kk", "kl",
"km", "kn", "ko", "ks", "ku", "kw", "ky", "lb", "lg", "ln",
"lo", "lt", "lv", "mg", "mk", "ml", "mn", "mr", "ms", "mt",
"my", "nb", "nd", "ne", "nl", "nn", "no", "nr", "ny", "om",
"or", "os", "pa", "pl", "ps", "pt", "rm", "ro", "ru", "se",
"sg", "si", "sk", "sl", "sn", "so", "sq", "sr", "ss", "st",
"sv", "sw", "ta", "te", "th", "ti", "tk", "tl", "tn", "to",
"tr", "ts", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "wa",
"wo", "xh", "yi", "yo", "zh", "zu"
};
/**
* Relevant flags for each language (corresponding to each language listed
* in the same position in {@link #LANGUAGE_CODES})
*/
private static final int[] FLAGS = new int[] {
0x0002, 0x0042, 0x0042, 0x001f, 0x0042, 0x0002, 0x005a, 0x0002,
0x0042, 0x0000, 0x0042, 0x0000, 0x00de, 0x004a, 0x0002, 0x0002,
0x000a, 0x001f, 0x0002, 0x0002, 0x0002, 0x0000, 0x0002, 0x0002,
0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0042, 0x0042, 0x0002,
0x0002, 0x0042, 0x0002, 0x001e, 0x00ce, 0x0002, 0x0042, 0x00ce,
0x0002, 0x0016, 0x0042, 0x004a, 0x0002, 0x0042, 0x0000, 0x0000,
0x0000, 0x0000, 0x0042, 0x0002, 0x0006, 0x0016, 0x0000, 0x0002,
0x0000, 0x0002, 0x0002, 0x0002, 0x0000, 0x0042, 0x0000, 0x0002,
0x0002, 0x0006, 0x0002, 0x0002, 0x0002, 0x0042, 0x0000, 0x004a,
0x0063, 0x0042, 0x0042, 0x0002, 0x0002, 0x0042, 0x0000, 0x001a,
0x0000, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002, 0x0002,
0x0002, 0x0002, 0x0002, 0x0002, 0x0042, 0x001a, 0x0002, 0x0042,
0x0002, 0x000a, 0x005a, 0x0006, 0x0000, 0x0042, 0x000a, 0x00ce,
0x0002, 0x0002, 0x0002, 0x004a, 0x0002, 0x0002, 0x0002, 0x0002,
0x0002, 0x0002, 0x0000, 0x0042, 0x0002, 0x0042, 0x0002, 0x0000,
0x0002, 0x0002, 0x0002, 0x005a, 0x0002, 0x0002, 0x0002, 0x0000,
0x0002, 0x0042, 0x0000, 0x0002, 0x0002, 0x0000, 0x0000, 0x0042
};
@Nullable
private static String getExampleForQuantityZero(@NonNull String language) {
int index = getLanguageIndex(language);
switch (index) {
// set14
case 72: // lv
return "0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, \u2026";
case -1:
default:
return null;
}
}
@Nullable
private static String getExampleForQuantityOne(@NonNull String language) {
int index = getLanguageIndex(language);
switch (index) {
// set1
case 2: // am
case 4: // as
case 10: // bn
case 29: // fa
case 38: // gu
case 42: // hi
case 61: // kn
case 77: // mr
case 135: // zu
return "0, 1";
// set11
case 50: // is
return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
// set12
case 74: // mk
return "1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, \u2026";
// set13
case 117: // tl
return "0~3, 5, 7, 8, 10~13, 15, 17, 18, 20, 21, 100, 1000, 10000, 100000, 1000000, \u2026";
// set14
case 72: // lv
return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
// set2
case 30: // ff
case 33: // fr
case 45: // hy
return "0, 1";
// set20
case 13: // bs
case 43: // hr
case 107: // sr
return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
// set21
case 36: // gd
return "1, 11";
// set22
case 103: // sl
return "1, 101, 201, 301, 401, 501, 601, 701, 1001, \u2026";
// set27
case 6: // be
return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
// set28
case 71: // lt
return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
// set30
case 98: // ru
case 123: // uk
return "1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, \u2026";
// set31
case 12: // br
return "1, 21, 31, 41, 51, 61, 81, 101, 1001, \u2026";
// set33
case 39: // gv
return "1, 11, 21, 31, 41, 51, 61, 71, 101, 1001, \u2026";
// set4
case 101: // si
return "0, 1";
// set5
case 1: // ak
case 8: // bh
case 69: // ln
case 73: // mg
case 92: // pa
case 115: // ti
case 129: // wa
return "0, 1";
// set7
case 95: // pt
return "0, 1";
case -1:
default:
return null;
}
}
@Nullable
private static String getExampleForQuantityTwo(@NonNull String language) {
int index = getLanguageIndex(language);
switch (index) {
// set21
case 36: // gd
return "2, 12";
// set22
case 103: // sl
return "2, 102, 202, 302, 402, 502, 602, 702, 1002, \u2026";
// set31
case 12: // br
return "2, 22, 32, 42, 52, 62, 82, 102, 1002, \u2026";
// set33
case 39: // gv
return "2, 12, 22, 32, 42, 52, 62, 72, 102, 1002, \u2026";
case -1:
default:
return null;
}
}
}
@@ -23,21 +23,23 @@ import static com.android.SdkConstants.TAG_ACTIVITY;
import static com.android.tools.klint.client.api.JavaParser.TYPE_STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.klint.client.api.JavaEvaluator;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.klint.detector.api.Detector.XmlScanner;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
import org.jetbrains.uast.UClass;
import org.jetbrains.uast.UFunction;
import org.jetbrains.uast.UastUtils;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.w3c.dom.Element;
import java.util.Collection;
@@ -51,7 +53,7 @@ import java.util.Map;
* Ensures that PreferenceActivity and its subclasses are never exported.
*/
public class PreferenceActivityDetector extends Detector
implements Detector.XmlScanner, UastScanner {
implements XmlScanner, Detector.UastScanner {
public static final Issue ISSUE = Issue.create(
"ExportedPreferenceActivity", //$NON-NLS-1$
"PreferenceActivity should not be exported",
@@ -62,7 +64,7 @@ public class PreferenceActivityDetector extends Detector
Severity.WARNING,
new Implementation(
PreferenceActivityDetector.class,
EnumSet.of(Scope.MANIFEST, Scope.SOURCE_FILE)))
EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE)))
.addMoreInfo("http://securityintelligence.com/"
+ "new-vulnerability-android-framework-fragment-injection");
private static final String PREFERENCE_ACTIVITY = "android.preference.PreferenceActivity"; //$NON-NLS-1$
@@ -71,13 +73,8 @@ public class PreferenceActivityDetector extends Detector
private final Map<String, Location.Handle> mExportedActivities =
new HashMap<String, Location.Handle>();
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements XmlScanner ----
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(TAG_ACTIVITY);
@@ -91,7 +88,7 @@ public class PreferenceActivityDetector extends Detector
if (fqcn.equals(PREFERENCE_ACTIVITY) &&
!context.getDriver().isSuppressed(context, ISSUE, element)) {
String message = "`PreferenceActivity` should not be exported";
context.report(ISSUE, context.getLocation(element), message);
context.report(ISSUE, element, context.getLocation(element), message);
}
mExportedActivities.put(fqcn, context.createLocationHandle(element));
}
@@ -122,41 +119,42 @@ public class PreferenceActivityDetector extends Detector
// ---- Implements UastScanner ----
@Nullable
@Override
public List<String> getApplicableSuperClasses() {
public List<String> applicableSuperClasses() {
return Collections.singletonList(PREFERENCE_ACTIVITY);
}
@Override
public void visitClass(UastAndroidContext context, UClass node) {
if (!context.getLintContext().getProject().getReportIssues()) {
public void checkClass(@NonNull JavaContext context, @NonNull UClass declaration) {
if (!context.getProject().getReportIssues()) {
return;
}
String className = node.getName();
if (node.isSubclassOf(PREFERENCE_ACTIVITY)
&& mExportedActivities.containsKey(className)) {
JavaEvaluator evaluator = context.getEvaluator();
String className = declaration.getQualifiedName();
if (evaluator.extendsClass(declaration, PREFERENCE_ACTIVITY, false)
&& mExportedActivities.containsKey(className)) {
// Ignore the issue if we target an API greater than 19 and the class in
// question specifically overrides isValidFragment() and thus knowingly white-lists
// valid fragments.
if (context.getLintContext().getMainProject().getTargetSdk() >= 19
&& overridesIsValidFragment(node)) {
if (context.getMainProject().getTargetSdk() >= 19
&& overridesIsValidFragment(evaluator, declaration)) {
return;
}
String message = String.format(
"`PreferenceActivity` subclass `%1$s` should not be exported",
className);
context.report(ISSUE, node, mExportedActivities.get(className).resolve(), message);
"`PreferenceActivity` subclass `%1$s` should not be exported",
className);
Location location = mExportedActivities.get(className).resolve();
context.reportUast(ISSUE, declaration, location, message);
}
}
private static boolean overridesIsValidFragment(UClass resolvedClass) {
List<UFunction> functions = UastUtils.findFunctions(resolvedClass, IS_VALID_FRAGMENT);
for (UFunction func : functions) {
if (func.getValueParameterCount() == 1
&& func.getValueParameters().get(0).getType().matchesFqName(TYPE_STRING)) {
private static boolean overridesIsValidFragment(
@NonNull JavaEvaluator evaluator,
@NonNull PsiClass resolvedClass) {
for (PsiMethod method : resolvedClass.findMethodsByName(IS_VALID_FRAGMENT, false)) {
if (evaluator.parametersMatch(method, TYPE_STRING)) {
return true;
}
}
@@ -46,20 +46,20 @@ import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.tools.klint.detector.api.Category;
import com.android.tools.klint.detector.api.Context;
import com.android.tools.klint.detector.api.Detector;
import com.android.tools.klint.detector.api.Implementation;
import com.android.tools.klint.detector.api.Issue;
import com.android.tools.klint.detector.api.JavaContext;
import com.android.tools.klint.detector.api.LintUtils;
import com.android.tools.klint.detector.api.Location;
import com.android.tools.klint.detector.api.Project;
import com.android.tools.klint.detector.api.ResourceXmlDetector;
import com.android.tools.klint.detector.api.Scope;
import com.android.tools.klint.detector.api.Severity;
import com.android.tools.klint.detector.api.Speed;
import com.android.tools.klint.detector.api.XmlContext;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.check.UastAndroidContext;
import org.jetbrains.uast.check.UastScanner;
import org.jetbrains.uast.visitor.UastVisitor;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -73,15 +73,16 @@ import java.util.List;
/**
* Check which looks for access of private resources.
*/
public class PrivateResourceDetector extends ResourceXmlDetector implements UastScanner {
public class PrivateResourceDetector extends ResourceXmlDetector implements
Detector.UastScanner {
/** Attribute for overriding a resource */
private static final String ATTR_OVERRIDE = "override";
@SuppressWarnings("unchecked")
private static final Implementation IMPLEMENTATION = new Implementation(
PrivateResourceDetector.class,
Scope.SOURCE_AND_RESOURCE_FILES,
Scope.SOURCE_FILE_SCOPE,
Scope.JAVA_AND_RESOURCE_FILES,
Scope.JAVA_FILE_SCOPE,
Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
@@ -103,12 +104,6 @@ public class PrivateResourceDetector extends ResourceXmlDetector implements Uast
public PrivateResourceDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements UastScanner ----
@Override
@@ -117,21 +112,15 @@ public class PrivateResourceDetector extends ResourceXmlDetector implements Uast
}
@Override
public void visitResourceReference(
UastAndroidContext uastContext,
UElement element,
String type,
String name,
boolean isFramework
) {
Context context = uastContext.getLintContext();
Project project = context.getProject();
if (project.isGradleProject() && !isFramework) {
public void visitResourceReference(@NonNull JavaContext context, @Nullable UastVisitor visitor,
@NonNull UElement node, @NonNull ResourceType resourceType, @NonNull String name,
boolean isFramework) {
if (context.getProject().isGradleProject() && !isFramework) {
Project project = context.getProject();
if (project.getGradleProjectModel() != null && project.getCurrentVariant() != null) {
ResourceType resourceType = ResourceType.getEnum(type);
if (resourceType != null && isPrivate(context, resourceType, name)) {
if (isPrivate(context, resourceType, name)) {
String message = createUsageErrorMessage(context, resourceType, name);
uastContext.report(ISSUE, element, uastContext.getLocation(element), message);
context.report(ISSUE, node, context.getUastLocation(node), message);
}
}
}
@@ -211,6 +200,15 @@ public class PrivateResourceDetector extends ResourceXmlDetector implements Uast
}
private static boolean isPrivate(Context context, ResourceType type, String name) {
if (type == ResourceType.ID) {
// No need to complain about "overriding" id's. There's no harm
// in doing so. (This avoids warning about cases like for example
// appcompat's (private) @id/title resource, which would otherwise
// flag any attempt to create a resource named title in the user's
// project.
return false;
}
if (context.getProject().isGradleProject()) {
ResourceVisibilityLookup lookup = context.getProject().getResourceVisibility();
return lookup.isPrivate(type, name);
@@ -286,9 +284,9 @@ public class PrivateResourceDetector extends ResourceXmlDetector implements Uast
context.report(ISSUE, location, message);
}
}
private static String createOverrideErrorMessage(@NonNull Context context,
@NonNull ResourceType type, @NonNull String name) {
@NonNull ResourceType type, @NonNull String name) {
String libraryName = getLibraryName(context, type, name);
return String.format("Overriding `@%1$s/%2$s` which is marked as private in %3$s. If "
+ "deliberate, use tools:override=\"true\", otherwise pick a "

Some files were not shown because too many files have changed in this diff Show More