Android Extensions: Show warning on a dot-call of a reference which is missing in some configurations (KT-18012)
When more than one layout configuration is available, a particular resource (view or fragment) may be absent in some of them. We should show a warning on such resource reference calls as the call may lead to NPE.
This commit is contained in:
committed by
Yan Zhulanow
parent
f4acf404ca
commit
f7786a42ab
+1
-1
@@ -188,7 +188,7 @@ class AndroidExpressionCodegenExtension : ExpressionCodegenExtension {
|
||||
val declarationDescriptorType = typeMapper.mapType(receiverDescriptor)
|
||||
receiver.put(declarationDescriptorType, v)
|
||||
|
||||
val resourceId = syntheticProperty.resourceId
|
||||
val resourceId = syntheticProperty.resource.id
|
||||
val packageName = resourceId.packageName ?: androidPackage
|
||||
v.getstatic(packageName.replace(".", "/") + "/R\$id", resourceId.name, "I")
|
||||
|
||||
|
||||
+38
@@ -22,10 +22,15 @@ import org.jetbrains.kotlin.android.synthetic.diagnostic.ErrorsAndroid.*
|
||||
import org.jetbrains.kotlin.android.synthetic.res.AndroidSyntheticProperty
|
||||
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
|
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticSink
|
||||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
|
||||
import org.jetbrains.kotlin.psi.KtElement
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtQualifiedExpression
|
||||
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
|
||||
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||
import org.jetbrains.kotlin.types.TypeUtils
|
||||
import org.jetbrains.kotlin.types.isFlexible
|
||||
|
||||
class AndroidExtensionPropertiesCallChecker : CallChecker {
|
||||
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
|
||||
@@ -38,6 +43,7 @@ class AndroidExtensionPropertiesCallChecker : CallChecker {
|
||||
with(context.trace) {
|
||||
checkUnresolvedWidgetType(reportOn, androidSyntheticProperty)
|
||||
checkDeprecated(reportOn, containingPackage)
|
||||
checkPartiallyDefinedResource(resolvedCall, androidSyntheticProperty, context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +60,36 @@ class AndroidExtensionPropertiesCallChecker : CallChecker {
|
||||
val warning = if (type.contains('.')) SYNTHETIC_UNRESOLVED_WIDGET_TYPE else SYNTHETIC_INVALID_WIDGET_TYPE
|
||||
report(warning.on(expression, type))
|
||||
}
|
||||
|
||||
private fun DiagnosticSink.checkPartiallyDefinedResource(
|
||||
resolvedCall: ResolvedCall<*>,
|
||||
property: AndroidSyntheticProperty,
|
||||
context: CallCheckerContext
|
||||
) {
|
||||
if (!property.resource.partiallyDefined) return
|
||||
val calleeExpression = resolvedCall.call.calleeExpression ?: return
|
||||
|
||||
val expectedType = context.resolutionContext.expectedType
|
||||
if (!TypeUtils.noExpectedType(expectedType) && !expectedType.isMarkedNullable && !expectedType.isFlexible()) {
|
||||
report(UNSAFE_CALL_ON_PARTIALLY_DEFINED_RESOURCE.on(calleeExpression))
|
||||
return
|
||||
}
|
||||
|
||||
val outermostQualifiedExpression = findLeftOutermostQualifiedExpression(calleeExpression) ?: return
|
||||
val usage = outermostQualifiedExpression.parent
|
||||
|
||||
if (usage is KtDotQualifiedExpression && usage.receiverExpression == outermostQualifiedExpression) {
|
||||
report(UNSAFE_CALL_ON_PARTIALLY_DEFINED_RESOURCE.on(calleeExpression))
|
||||
}
|
||||
}
|
||||
|
||||
private fun findLeftOutermostQualifiedExpression(calleeExpression: KtExpression?): KtElement? {
|
||||
val parent = calleeExpression?.parent ?: return null
|
||||
|
||||
if (parent is KtQualifiedExpression && parent.selectorExpression == calleeExpression) {
|
||||
return findLeftOutermostQualifiedExpression(parent)
|
||||
}
|
||||
|
||||
return calleeExpression
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -36,6 +36,9 @@ class DefaultErrorMessagesAndroid : DefaultErrorMessages.Extension {
|
||||
|
||||
MAP.put(ErrorsAndroid.SYNTHETIC_DEPRECATED_PACKAGE,
|
||||
"Use properties from the build variant packages")
|
||||
|
||||
MAP.put(ErrorsAndroid.UNSAFE_CALL_ON_PARTIALLY_DEFINED_RESOURCE,
|
||||
"Potential NullPointerException. The resource is missing in some of layout versions")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ public interface ErrorsAndroid {
|
||||
DiagnosticFactory1<KtExpression, String> SYNTHETIC_INVALID_WIDGET_TYPE = DiagnosticFactory1.create(WARNING);
|
||||
DiagnosticFactory1<KtExpression, String> SYNTHETIC_UNRESOLVED_WIDGET_TYPE = DiagnosticFactory1.create(WARNING);
|
||||
DiagnosticFactory0<KtExpression> SYNTHETIC_DEPRECATED_PACKAGE = DiagnosticFactory0.create(WARNING);
|
||||
DiagnosticFactory0<KtExpression> UNSAFE_CALL_ON_PARTIALLY_DEFINED_RESOURCE = DiagnosticFactory0.create(WARNING);
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
Object _initializer = new Object() {
|
||||
|
||||
+33
-14
@@ -88,7 +88,7 @@ abstract class AndroidLayoutXmlFileManager(val project: Project) {
|
||||
return filterDuplicates(doExtractResources(files, module))
|
||||
}
|
||||
|
||||
protected abstract fun doExtractResources(files: List<PsiFile>, module: ModuleDescriptor): List<AndroidResource>
|
||||
protected abstract fun doExtractResources(files: List<PsiFile>, module: ModuleDescriptor): List<AndroidLayoutGroup>
|
||||
|
||||
protected fun parseAndroidResource(id: ResourceIdentifier, tag: String, sourceElement: PsiElement?): AndroidResource {
|
||||
return when (tag) {
|
||||
@@ -98,27 +98,46 @@ abstract class AndroidLayoutXmlFileManager(val project: Project) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterDuplicates(resources: List<AndroidResource>): List<AndroidResource> {
|
||||
private fun filterDuplicates(layoutGroups: List<AndroidLayoutGroup>): List<AndroidResource> {
|
||||
val resourceMap = linkedMapOf<String, AndroidResource>()
|
||||
val resourcesToExclude = hashSetOf<String>()
|
||||
|
||||
for (res in resources) {
|
||||
if (resourceMap.contains(res.id.name)) {
|
||||
val existing = resourceMap[res.id.name]!!
|
||||
for (layoutGroup in layoutGroups) {
|
||||
val resources = layoutGroup.layouts.flatMap { it.resources }.groupBy {
|
||||
val id = it.id
|
||||
if (id.packageName == null) id.name else id.packageName + "/" + id.name
|
||||
}
|
||||
|
||||
if (!res.sameClass(existing) || res.id.packageName != existing.id.packageName) {
|
||||
resourcesToExclude.add(res.id.name)
|
||||
}
|
||||
else if (res is AndroidResource.Widget && existing is AndroidResource.Widget) {
|
||||
// Widgets with the same id but different types exist.
|
||||
if (res.xmlType != existing.xmlType && existing.xmlType != AndroidConst.VIEW_FQNAME) {
|
||||
resourceMap.put(res.id.name, AndroidResource.Widget(res.id, AndroidConst.VIEW_FQNAME, res.sourceElement))
|
||||
for (resources in resources.values) {
|
||||
val isPartiallyDefined = resources.size < layoutGroup.layouts.size
|
||||
|
||||
for (res in resources) {
|
||||
if (resourceMap.contains(res.id.name)) {
|
||||
val existing = resourceMap[res.id.name]!!
|
||||
|
||||
if (!res.sameClass(existing) || res.id.packageName != existing.id.packageName) {
|
||||
resourcesToExclude.add(res.id.name)
|
||||
}
|
||||
else if (res is AndroidResource.Widget && existing is AndroidResource.Widget) {
|
||||
// Widgets with the same id but different types exist.
|
||||
if (res.xmlType != existing.xmlType && existing.xmlType != AndroidConst.VIEW_FQNAME) {
|
||||
val mergedWidget = AndroidResource.Widget(
|
||||
res.id, AndroidConst.VIEW_FQNAME, res.sourceElement, isPartiallyDefined)
|
||||
resourceMap.put(res.id.name, mergedWidget)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (isPartiallyDefined) {
|
||||
resourceMap.put(res.id.name, res.partiallyDefined())
|
||||
}
|
||||
else {
|
||||
resourceMap.put(res.id.name, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
else resourceMap.put(res.id.name, res)
|
||||
}
|
||||
resourcesToExclude.forEach { resourceMap.remove(it) }
|
||||
|
||||
resourceMap.keys.removeAll(resourcesToExclude)
|
||||
return resourceMap.values.toList()
|
||||
}
|
||||
|
||||
|
||||
+15
-19
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.jetbrains.kotlin.android.synthetic.res
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiFile
|
||||
import org.jetbrains.kotlin.android.synthetic.AndroidXmlHandler
|
||||
@@ -30,31 +29,28 @@ class CliAndroidLayoutXmlFileManager(
|
||||
applicationPackage: String,
|
||||
variants: List<AndroidVariant>
|
||||
) : AndroidLayoutXmlFileManager(project) {
|
||||
private companion object {
|
||||
val LOG = Logger.getInstance(CliAndroidLayoutXmlFileManager::class.java)
|
||||
}
|
||||
|
||||
override val androidModule = AndroidModule(applicationPackage, variants)
|
||||
|
||||
private val saxParser: SAXParser = initSAX()
|
||||
|
||||
override fun doExtractResources(files: List<PsiFile>, module: ModuleDescriptor): List<AndroidResource> {
|
||||
val resources = arrayListOf<AndroidResource>()
|
||||
override fun doExtractResources(files: List<PsiFile>, module: ModuleDescriptor): List<AndroidLayoutGroup> {
|
||||
val layoutGroupFiles = files.groupBy { it.name }
|
||||
val layoutGroups = mutableListOf<AndroidLayoutGroup>()
|
||||
|
||||
val handler = AndroidXmlHandler { id, tag ->
|
||||
resources += parseAndroidResource(id, tag, null)
|
||||
for ((name, layouts) in layoutGroupFiles) {
|
||||
layoutGroups += AndroidLayoutGroup(name, layouts.map { layout ->
|
||||
val resources = arrayListOf<AndroidResource>()
|
||||
|
||||
val inputStream = ByteArrayInputStream(layout.virtualFile.contentsToByteArray())
|
||||
saxParser.parse(inputStream, AndroidXmlHandler { id, tag ->
|
||||
resources += parseAndroidResource(id, tag, null)
|
||||
})
|
||||
|
||||
AndroidLayout(resources)
|
||||
})
|
||||
}
|
||||
|
||||
for (file in files) {
|
||||
try {
|
||||
val inputStream = ByteArrayInputStream(file.virtualFile.contentsToByteArray())
|
||||
saxParser.parse(inputStream, handler)
|
||||
} catch (e: Throwable) {
|
||||
LOG.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
return layoutGroups
|
||||
}
|
||||
|
||||
private fun initSAX(): SAXParser {
|
||||
|
||||
+16
-4
@@ -61,19 +61,31 @@ class ResourceIdentifier(val name: String, val packageName: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AndroidResource(val id: ResourceIdentifier, val sourceElement: PsiElement?) {
|
||||
class AndroidLayoutGroup(val name: String, val layouts: List<AndroidLayout>)
|
||||
|
||||
class AndroidLayout(val resources: List<AndroidResource>)
|
||||
|
||||
sealed class AndroidResource(val id: ResourceIdentifier, val sourceElement: PsiElement?, val partiallyDefined: Boolean) {
|
||||
open fun sameClass(other: AndroidResource): Boolean = false
|
||||
open fun partiallyDefined(): AndroidResource = this
|
||||
|
||||
class Widget(
|
||||
id: ResourceIdentifier,
|
||||
val xmlType: String,
|
||||
sourceElement: PsiElement?
|
||||
) : AndroidResource(id, sourceElement) {
|
||||
sourceElement: PsiElement?,
|
||||
partiallyDefined: Boolean = false
|
||||
) : AndroidResource(id, sourceElement, partiallyDefined) {
|
||||
override fun sameClass(other: AndroidResource) = other is Widget
|
||||
override fun partiallyDefined() = Widget(id, xmlType, sourceElement, true)
|
||||
}
|
||||
|
||||
class Fragment(id: ResourceIdentifier, sourceElement: PsiElement?) : AndroidResource(id, sourceElement) {
|
||||
class Fragment(
|
||||
id: ResourceIdentifier,
|
||||
sourceElement: PsiElement?,
|
||||
partiallyDefined: Boolean = false
|
||||
) : AndroidResource(id, sourceElement, partiallyDefined) {
|
||||
override fun sameClass(other: AndroidResource) = other is Fragment
|
||||
override fun partiallyDefined() = Fragment(id, sourceElement, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -68,7 +68,7 @@ internal fun genPropertyForWidget(
|
||||
defaultType.constructor.parameters.map(::StarProjectionImpl))
|
||||
} ?: context.viewType
|
||||
|
||||
return genProperty(resolvedWidget.widget.id, receiverType, type, packageFragmentDescriptor, sourceEl, resolvedWidget.errorType)
|
||||
return genProperty(resolvedWidget.widget, receiverType, type, packageFragmentDescriptor, sourceEl, resolvedWidget.errorType)
|
||||
}
|
||||
|
||||
internal fun genPropertyForFragment(
|
||||
@@ -78,11 +78,11 @@ internal fun genPropertyForFragment(
|
||||
fragment: AndroidResource.Fragment
|
||||
): PropertyDescriptor {
|
||||
val sourceElement = fragment.sourceElement?.let(::XmlSourceElement) ?: SourceElement.NO_SOURCE
|
||||
return genProperty(fragment.id, receiverType, type, packageFragmentDescriptor, sourceElement, null)
|
||||
return genProperty(fragment, receiverType, type, packageFragmentDescriptor, sourceElement, null)
|
||||
}
|
||||
|
||||
private fun genProperty(
|
||||
id: ResourceIdentifier,
|
||||
resource: AndroidResource,
|
||||
receiverType: KotlinType,
|
||||
type: SimpleType,
|
||||
containingDeclaration: AndroidSyntheticPackageFragmentDescriptor,
|
||||
@@ -98,7 +98,7 @@ private fun genProperty(
|
||||
Modality.FINAL,
|
||||
Visibilities.PUBLIC,
|
||||
false,
|
||||
Name.identifier(id.name),
|
||||
Name.identifier(resource.id.name),
|
||||
CallableMemberDescriptor.Kind.SYNTHESIZED,
|
||||
sourceElement,
|
||||
/* lateinit = */ false,
|
||||
@@ -110,7 +110,7 @@ private fun genProperty(
|
||||
) {
|
||||
override val errorType = errorType
|
||||
override val cacheView = cacheView
|
||||
override val resourceId = id
|
||||
override val resource = resource
|
||||
}
|
||||
|
||||
// todo support (Mutable)List
|
||||
@@ -146,7 +146,7 @@ interface AndroidSyntheticFunction
|
||||
interface AndroidSyntheticProperty {
|
||||
val errorType: String?
|
||||
val cacheView: Boolean
|
||||
val resourceId: ResourceIdentifier
|
||||
val resource: AndroidResource
|
||||
|
||||
val isErrorType: Boolean
|
||||
get() = errorType != null
|
||||
|
||||
Reference in New Issue
Block a user