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:
Yan Zhulanow
2017-05-26 22:49:35 +03:00
committed by Yan Zhulanow
parent f4acf404ca
commit f7786a42ab
9 changed files with 126 additions and 50 deletions
@@ -188,7 +188,7 @@ class AndroidExpressionCodegenExtension : ExpressionCodegenExtension {
val declarationDescriptorType = typeMapper.mapType(receiverDescriptor) val declarationDescriptorType = typeMapper.mapType(receiverDescriptor)
receiver.put(declarationDescriptorType, v) receiver.put(declarationDescriptorType, v)
val resourceId = syntheticProperty.resourceId val resourceId = syntheticProperty.resource.id
val packageName = resourceId.packageName ?: androidPackage val packageName = resourceId.packageName ?: androidPackage
v.getstatic(packageName.replace(".", "/") + "/R\$id", resourceId.name, "I") v.getstatic(packageName.replace(".", "/") + "/R\$id", resourceId.name, "I")
@@ -22,10 +22,15 @@ import org.jetbrains.kotlin.android.synthetic.diagnostic.ErrorsAndroid.*
import org.jetbrains.kotlin.android.synthetic.res.AndroidSyntheticProperty import org.jetbrains.kotlin.android.synthetic.res.AndroidSyntheticProperty
import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.diagnostics.DiagnosticSink 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.KtExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.isFlexible
class AndroidExtensionPropertiesCallChecker : CallChecker { class AndroidExtensionPropertiesCallChecker : CallChecker {
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
@@ -38,6 +43,7 @@ class AndroidExtensionPropertiesCallChecker : CallChecker {
with(context.trace) { with(context.trace) {
checkUnresolvedWidgetType(reportOn, androidSyntheticProperty) checkUnresolvedWidgetType(reportOn, androidSyntheticProperty)
checkDeprecated(reportOn, containingPackage) 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 val warning = if (type.contains('.')) SYNTHETIC_UNRESOLVED_WIDGET_TYPE else SYNTHETIC_INVALID_WIDGET_TYPE
report(warning.on(expression, 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
}
} }
@@ -36,6 +36,9 @@ class DefaultErrorMessagesAndroid : DefaultErrorMessages.Extension {
MAP.put(ErrorsAndroid.SYNTHETIC_DEPRECATED_PACKAGE, MAP.put(ErrorsAndroid.SYNTHETIC_DEPRECATED_PACKAGE,
"Use properties from the build variant packages") "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")
} }
} }
@@ -27,6 +27,7 @@ public interface ErrorsAndroid {
DiagnosticFactory1<KtExpression, String> SYNTHETIC_INVALID_WIDGET_TYPE = DiagnosticFactory1.create(WARNING); DiagnosticFactory1<KtExpression, String> SYNTHETIC_INVALID_WIDGET_TYPE = DiagnosticFactory1.create(WARNING);
DiagnosticFactory1<KtExpression, String> SYNTHETIC_UNRESOLVED_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> SYNTHETIC_DEPRECATED_PACKAGE = DiagnosticFactory0.create(WARNING);
DiagnosticFactory0<KtExpression> UNSAFE_CALL_ON_PARTIALLY_DEFINED_RESOURCE = DiagnosticFactory0.create(WARNING);
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
Object _initializer = new Object() { Object _initializer = new Object() {
@@ -88,7 +88,7 @@ abstract class AndroidLayoutXmlFileManager(val project: Project) {
return filterDuplicates(doExtractResources(files, module)) 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 { protected fun parseAndroidResource(id: ResourceIdentifier, tag: String, sourceElement: PsiElement?): AndroidResource {
return when (tag) { 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 resourceMap = linkedMapOf<String, AndroidResource>()
val resourcesToExclude = hashSetOf<String>() val resourcesToExclude = hashSetOf<String>()
for (res in resources) { for (layoutGroup in layoutGroups) {
if (resourceMap.contains(res.id.name)) { val resources = layoutGroup.layouts.flatMap { it.resources }.groupBy {
val existing = resourceMap[res.id.name]!! 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) { for (resources in resources.values) {
resourcesToExclude.add(res.id.name) val isPartiallyDefined = resources.size < layoutGroup.layouts.size
}
else if (res is AndroidResource.Widget && existing is AndroidResource.Widget) { for (res in resources) {
// Widgets with the same id but different types exist. if (resourceMap.contains(res.id.name)) {
if (res.xmlType != existing.xmlType && existing.xmlType != AndroidConst.VIEW_FQNAME) { val existing = resourceMap[res.id.name]!!
resourceMap.put(res.id.name, AndroidResource.Widget(res.id, AndroidConst.VIEW_FQNAME, res.sourceElement))
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() return resourceMap.values.toList()
} }
@@ -16,7 +16,6 @@
package org.jetbrains.kotlin.android.synthetic.res package org.jetbrains.kotlin.android.synthetic.res
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.android.synthetic.AndroidXmlHandler import org.jetbrains.kotlin.android.synthetic.AndroidXmlHandler
@@ -30,31 +29,28 @@ class CliAndroidLayoutXmlFileManager(
applicationPackage: String, applicationPackage: String,
variants: List<AndroidVariant> variants: List<AndroidVariant>
) : AndroidLayoutXmlFileManager(project) { ) : AndroidLayoutXmlFileManager(project) {
private companion object {
val LOG = Logger.getInstance(CliAndroidLayoutXmlFileManager::class.java)
}
override val androidModule = AndroidModule(applicationPackage, variants) override val androidModule = AndroidModule(applicationPackage, variants)
private val saxParser: SAXParser = initSAX() private val saxParser: SAXParser = initSAX()
override fun doExtractResources(files: List<PsiFile>, module: ModuleDescriptor): List<AndroidResource> { override fun doExtractResources(files: List<PsiFile>, module: ModuleDescriptor): List<AndroidLayoutGroup> {
val resources = arrayListOf<AndroidResource>() val layoutGroupFiles = files.groupBy { it.name }
val layoutGroups = mutableListOf<AndroidLayoutGroup>()
val handler = AndroidXmlHandler { id, tag -> for ((name, layouts) in layoutGroupFiles) {
resources += parseAndroidResource(id, tag, null) 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) { return layoutGroups
try {
val inputStream = ByteArrayInputStream(file.virtualFile.contentsToByteArray())
saxParser.parse(inputStream, handler)
} catch (e: Throwable) {
LOG.error(e)
}
}
return resources
} }
private fun initSAX(): SAXParser { private fun initSAX(): SAXParser {
@@ -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 sameClass(other: AndroidResource): Boolean = false
open fun partiallyDefined(): AndroidResource = this
class Widget( class Widget(
id: ResourceIdentifier, id: ResourceIdentifier,
val xmlType: String, val xmlType: String,
sourceElement: PsiElement? sourceElement: PsiElement?,
) : AndroidResource(id, sourceElement) { partiallyDefined: Boolean = false
) : AndroidResource(id, sourceElement, partiallyDefined) {
override fun sameClass(other: AndroidResource) = other is Widget 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 sameClass(other: AndroidResource) = other is Fragment
override fun partiallyDefined() = Fragment(id, sourceElement, true)
} }
} }
@@ -68,7 +68,7 @@ internal fun genPropertyForWidget(
defaultType.constructor.parameters.map(::StarProjectionImpl)) defaultType.constructor.parameters.map(::StarProjectionImpl))
} ?: context.viewType } ?: 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( internal fun genPropertyForFragment(
@@ -78,11 +78,11 @@ internal fun genPropertyForFragment(
fragment: AndroidResource.Fragment fragment: AndroidResource.Fragment
): PropertyDescriptor { ): PropertyDescriptor {
val sourceElement = fragment.sourceElement?.let(::XmlSourceElement) ?: SourceElement.NO_SOURCE 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( private fun genProperty(
id: ResourceIdentifier, resource: AndroidResource,
receiverType: KotlinType, receiverType: KotlinType,
type: SimpleType, type: SimpleType,
containingDeclaration: AndroidSyntheticPackageFragmentDescriptor, containingDeclaration: AndroidSyntheticPackageFragmentDescriptor,
@@ -98,7 +98,7 @@ private fun genProperty(
Modality.FINAL, Modality.FINAL,
Visibilities.PUBLIC, Visibilities.PUBLIC,
false, false,
Name.identifier(id.name), Name.identifier(resource.id.name),
CallableMemberDescriptor.Kind.SYNTHESIZED, CallableMemberDescriptor.Kind.SYNTHESIZED,
sourceElement, sourceElement,
/* lateinit = */ false, /* lateinit = */ false,
@@ -110,7 +110,7 @@ private fun genProperty(
) { ) {
override val errorType = errorType override val errorType = errorType
override val cacheView = cacheView override val cacheView = cacheView
override val resourceId = id override val resource = resource
} }
// todo support (Mutable)List // todo support (Mutable)List
@@ -146,7 +146,7 @@ interface AndroidSyntheticFunction
interface AndroidSyntheticProperty { interface AndroidSyntheticProperty {
val errorType: String? val errorType: String?
val cacheView: Boolean val cacheView: Boolean
val resourceId: ResourceIdentifier val resource: AndroidResource
val isErrorType: Boolean val isErrorType: Boolean
get() = errorType != null get() = errorType != null
@@ -61,14 +61,21 @@ class IDEAndroidLayoutXmlFileManager(val module: Module) : AndroidLayoutXmlFileM
return project.getExtensions(PsiTreeChangePreprocessor.EP_NAME).first { it is AndroidPsiTreeChangePreprocessor } return project.getExtensions(PsiTreeChangePreprocessor.EP_NAME).first { it is AndroidPsiTreeChangePreprocessor }
} }
override fun doExtractResources(files: List<PsiFile>, module: ModuleDescriptor): List<AndroidResource> { override fun doExtractResources(files: List<PsiFile>, module: ModuleDescriptor): List<AndroidLayoutGroup> {
val widgets = arrayListOf<AndroidResource>() val layoutGroupFiles = files.groupBy { it.name }
val visitor = AndroidXmlVisitor { id, widgetType, attribute -> val layoutGroups = mutableListOf<AndroidLayoutGroup>()
widgets += parseAndroidResource(id, widgetType, attribute.valueElement)
for ((name, layouts) in layoutGroupFiles) {
layoutGroups += AndroidLayoutGroup(name, layouts.map { layout ->
val resources = arrayListOf<AndroidResource>()
layout.accept(AndroidXmlVisitor { id, widgetType, attribute ->
resources += parseAndroidResource(id, widgetType, attribute.valueElement)
})
AndroidLayout(resources)
})
} }
files.forEach { it.accept(visitor) } return layoutGroups
return widgets
} }