Lint: Update diagnostics because of the new Uast. Also, these diagnostics are from AS 2.2.
This commit is contained in:
committed by
Yan Zhulanow
parent
4c4d6a4ad4
commit
c2ddd943f9
Generated
+1
@@ -2,6 +2,7 @@
|
||||
<dictionary name="yan">
|
||||
<words>
|
||||
<w>kapt</w>
|
||||
<w>uast</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
Generated
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 & 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 <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'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="'import android.R' 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>
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
+24
-9
@@ -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;
|
||||
|
||||
+127
-4
@@ -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);
|
||||
}
|
||||
-38
@@ -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) {
|
||||
|
||||
Executable → Regular
+219
-36
@@ -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
-61
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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());
|
||||
|
||||
+1941
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
*/
|
||||
|
||||
Executable → Regular
+237
-69
@@ -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>
|
||||
*/
|
||||
|
||||
Executable → Regular
+185
-62
@@ -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>
|
||||
*/
|
||||
|
||||
+690
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
// Sadly Guava's HtmlEscapes don't handle this either.
|
||||
if (entity.equalsIgnoreCase(" ")) {
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
+24
-31
@@ -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();
|
||||
}
|
||||
|
||||
+105
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-53
@@ -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};
|
||||
}
|
||||
}
|
||||
+842
-79
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+42
-45
@@ -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;
|
||||
}
|
||||
|
||||
+743
@@ -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;
|
||||
}
|
||||
}
|
||||
+141
@@ -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";
|
||||
}
|
||||
}
|
||||
+44
-10
@@ -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;
|
||||
}
|
||||
|
||||
+72
-85
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-45
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+479
-197
@@ -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");
|
||||
|
||||
+47
-53
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+169
-105
@@ -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 & 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;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-72
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+233
-267
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+47
-200
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-13
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-47
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-35
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+42
-43
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-34
@@ -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 ≥ 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;
|
||||
|
||||
+246
-269
@@ -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 ≥ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-32
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-27
@@ -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
Reference in New Issue
Block a user