diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/codegen/AndroidExpressionCodegenExtension.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/codegen/AndroidExpressionCodegenExtension.kt index 0c3946a6d62..199aa435693 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/codegen/AndroidExpressionCodegenExtension.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/codegen/AndroidExpressionCodegenExtension.kt @@ -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") diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/AndroidExtensionPropertiesCallChecker.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/AndroidExtensionPropertiesCallChecker.kt index f2f80fca5cc..4da0b4e405c 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/AndroidExtensionPropertiesCallChecker.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/AndroidExtensionPropertiesCallChecker.kt @@ -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 + } } diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/DefaultErrorMessagesAndroid.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/DefaultErrorMessagesAndroid.kt index fae85de22ec..735ccdee368 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/DefaultErrorMessagesAndroid.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/DefaultErrorMessagesAndroid.kt @@ -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") } } diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/ErrorsAndroid.java b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/ErrorsAndroid.java index 273997b0e3f..76bf3e10b5b 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/ErrorsAndroid.java +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/diagnostic/ErrorsAndroid.java @@ -27,6 +27,7 @@ public interface ErrorsAndroid { DiagnosticFactory1 SYNTHETIC_INVALID_WIDGET_TYPE = DiagnosticFactory1.create(WARNING); DiagnosticFactory1 SYNTHETIC_UNRESOLVED_WIDGET_TYPE = DiagnosticFactory1.create(WARNING); DiagnosticFactory0 SYNTHETIC_DEPRECATED_PACKAGE = DiagnosticFactory0.create(WARNING); + DiagnosticFactory0 UNSAFE_CALL_ON_PARTIALLY_DEFINED_RESOURCE = DiagnosticFactory0.create(WARNING); @SuppressWarnings("UnusedDeclaration") Object _initializer = new Object() { diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/AndroidLayoutXmlFileManager.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/AndroidLayoutXmlFileManager.kt index 0e731a9e927..d5c4fd63243 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/AndroidLayoutXmlFileManager.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/AndroidLayoutXmlFileManager.kt @@ -88,7 +88,7 @@ abstract class AndroidLayoutXmlFileManager(val project: Project) { return filterDuplicates(doExtractResources(files, module)) } - protected abstract fun doExtractResources(files: List, module: ModuleDescriptor): List + protected abstract fun doExtractResources(files: List, module: ModuleDescriptor): List 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): List { + private fun filterDuplicates(layoutGroups: List): List { val resourceMap = linkedMapOf() val resourcesToExclude = hashSetOf() - 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() } diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/CliAndroidLayoutXmlFileManager.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/CliAndroidLayoutXmlFileManager.kt index 42e648b7083..5dbdfa71d45 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/CliAndroidLayoutXmlFileManager.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/CliAndroidLayoutXmlFileManager.kt @@ -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 ) : 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, module: ModuleDescriptor): List { - val resources = arrayListOf() + override fun doExtractResources(files: List, module: ModuleDescriptor): List { + val layoutGroupFiles = files.groupBy { it.name } + val layoutGroups = mutableListOf() - val handler = AndroidXmlHandler { id, tag -> - resources += parseAndroidResource(id, tag, null) + for ((name, layouts) in layoutGroupFiles) { + layoutGroups += AndroidLayoutGroup(name, layouts.map { layout -> + val resources = arrayListOf() + + 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 { diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/androidResources.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/androidResources.kt index ca25b9c0a42..ae90deb22a2 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/androidResources.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/androidResources.kt @@ -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) + +class AndroidLayout(val resources: List) + +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) } } diff --git a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/syntheticDescriptorGeneration.kt b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/syntheticDescriptorGeneration.kt index 6518abc3dd5..a0482c1a0f0 100644 --- a/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/syntheticDescriptorGeneration.kt +++ b/plugins/android-extensions/android-extensions-compiler/src/org/jetbrains/kotlin/android/synthetic/res/syntheticDescriptorGeneration.kt @@ -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 diff --git a/plugins/android-extensions/android-extensions-idea/src/org/jetbrains/kotlin/android/synthetic/idea/res/IDEAndroidLayoutXmlFileManager.kt b/plugins/android-extensions/android-extensions-idea/src/org/jetbrains/kotlin/android/synthetic/idea/res/IDEAndroidLayoutXmlFileManager.kt index 369a5f47c21..dec4d9c7cbe 100644 --- a/plugins/android-extensions/android-extensions-idea/src/org/jetbrains/kotlin/android/synthetic/idea/res/IDEAndroidLayoutXmlFileManager.kt +++ b/plugins/android-extensions/android-extensions-idea/src/org/jetbrains/kotlin/android/synthetic/idea/res/IDEAndroidLayoutXmlFileManager.kt @@ -61,14 +61,21 @@ class IDEAndroidLayoutXmlFileManager(val module: Module) : AndroidLayoutXmlFileM return project.getExtensions(PsiTreeChangePreprocessor.EP_NAME).first { it is AndroidPsiTreeChangePreprocessor } } - override fun doExtractResources(files: List, module: ModuleDescriptor): List { - val widgets = arrayListOf() - val visitor = AndroidXmlVisitor { id, widgetType, attribute -> - widgets += parseAndroidResource(id, widgetType, attribute.valueElement) + override fun doExtractResources(files: List, module: ModuleDescriptor): List { + val layoutGroupFiles = files.groupBy { it.name } + val layoutGroups = mutableListOf() + + for ((name, layouts) in layoutGroupFiles) { + layoutGroups += AndroidLayoutGroup(name, layouts.map { layout -> + val resources = arrayListOf() + layout.accept(AndroidXmlVisitor { id, widgetType, attribute -> + resources += parseAndroidResource(id, widgetType, attribute.valueElement) + }) + AndroidLayout(resources) + }) } - files.forEach { it.accept(visitor) } - return widgets + return layoutGroups }