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)
|
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")
|
||||||
|
|
||||||
|
|||||||
+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.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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
@@ -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() {
|
||||||
|
|||||||
+33
-14
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-19
@@ -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 {
|
||||||
|
|||||||
+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 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -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
|
||||||
|
|||||||
+13
-6
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user