Report if the widget has an invalid type

This commit is contained in:
Yan Zhulanow
2015-08-20 17:17:49 +03:00
parent a2096ab0be
commit d0d846ccfc
12 changed files with 203 additions and 11 deletions
@@ -21,6 +21,7 @@ import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.android.synthetic.codegen.AndroidExpressionCodegenExtension
import org.jetbrains.kotlin.android.synthetic.diagnostic.AndroidExtensionPropertiesCallChecker
import org.jetbrains.kotlin.android.synthetic.res.AndroidLayoutXmlFileManager
import org.jetbrains.kotlin.android.synthetic.res.CliAndroidLayoutXmlFileManager
import org.jetbrains.kotlin.android.synthetic.res.CliSyntheticFileGenerator
@@ -32,8 +33,13 @@ import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.CompilerConfigurationKey
import org.jetbrains.kotlin.container.StorageComponentContainer
import org.jetbrains.kotlin.container.useInstance
import org.jetbrains.kotlin.extensions.ExternalDeclarationsProvider
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.resolve.TargetPlatform
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform
public object AndroidConfigurationKeys {
public val ANDROID_RES_PATH: CompilerConfigurationKey<List<String>> = CompilerConfigurationKey.create<List<String>>("android resources search path")
@@ -86,6 +92,15 @@ public class AndroidComponentRegistrar : ComponentRegistrar {
ExternalDeclarationsProvider.registerExtension(project, CliAndroidDeclarationsProvider(project))
ExpressionCodegenExtension.registerExtension(project, AndroidExpressionCodegenExtension())
StorageComponentContainerContributor.registerExtension(project, AndroidExtensionPropertiesComponentContainerContributor())
}
}
}
public class AndroidExtensionPropertiesComponentContainerContributor : StorageComponentContainerContributor {
override fun addDeclarations(container: StorageComponentContainer, platform: TargetPlatform) {
if (platform is JvmPlatform) {
container.useInstance(AndroidExtensionPropertiesCallChecker())
}
}
}
@@ -81,8 +81,9 @@ public fun parseAndroidResource(id: String, tag: String, fqNameResolver: (String
"fragment" -> AndroidFragment(id)
"include" -> AndroidWidget(id, AndroidConst.VIEW_FQNAME)
else -> {
val fqName = fqNameResolver(tag) ?: AndroidConst.VIEW_FQNAME
AndroidWidget(id, fqName)
val fqName = fqNameResolver(tag)
val invalidType = if (fqName != null) null else tag
AndroidWidget(id, fqName ?: AndroidConst.VIEW_FQNAME, invalidType)
}
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2010-2015 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 org.jetbrains.kotlin.android.synthetic.diagnostic
import org.jetbrains.kotlin.android.synthetic.AndroidConst
import org.jetbrains.kotlin.android.synthetic.res.SyntheticFileGenerator
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
import org.jetbrains.kotlin.resolve.calls.context.BasicCallResolutionContext
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.constants.StringValue
public class AndroidExtensionPropertiesCallChecker : CallChecker {
override fun <F : CallableDescriptor> check(resolvedCall: ResolvedCall<F>, context: BasicCallResolutionContext) {
val expression = context.call.calleeExpression ?: return
val propertyDescriptor = resolvedCall.resultingDescriptor as? PropertyDescriptor ?: return
val syntheticPackage = propertyDescriptor.containingDeclaration as? PackageFragmentDescriptor ?: return
if (!syntheticPackage.fqName.asString().startsWith("${AndroidConst.SYNTHETIC_PACKAGE}.")) return
val invalidWidgetTypeAnnotation = propertyDescriptor.annotations.findAnnotation(
SyntheticFileGenerator.INVALID_WIDGET_TYPE_ANNOTATION_FQNAME) ?: return
val type = invalidWidgetTypeAnnotation.allValueArguments.filterKeys {
it.name.asString() == SyntheticFileGenerator.INVALID_WIDGET_TYPE_ANNOTATION_TYPE_PARAMETER
}.values().firstOrNull() as? StringValue ?: return
context.trace.report(ErrorsAndroid.SYNTHETIC_INVALID_WIDGET_TYPE.on(expression, type.value))
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2015 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 org.jetbrains.kotlin.android.synthetic.diagnostic
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages
import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap
import org.jetbrains.kotlin.diagnostics.rendering.Renderers
public class DefaultErrorMessagesAndroid : DefaultErrorMessages.Extension {
private companion object {
val MAP = DiagnosticFactoryToRendererMap()
init {
MAP.put(ErrorsAndroid.SYNTHETIC_INVALID_WIDGET_TYPE,
"Widget has an invalid type ''{0}''. Please specify the fully qualified widget class name in XML",
Renderers.TO_STRING)
}
}
override fun getMap() = MAP
}
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2015 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 org.jetbrains.kotlin.android.synthetic.diagnostic;
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1;
import org.jetbrains.kotlin.diagnostics.Errors;
import org.jetbrains.kotlin.psi.JetExpression;
import static org.jetbrains.kotlin.diagnostics.Severity.WARNING;
public interface ErrorsAndroid {
DiagnosticFactory1<JetExpression, String> SYNTHETIC_INVALID_WIDGET_TYPE = DiagnosticFactory1.create(WARNING);
@SuppressWarnings("UnusedDeclaration")
Object _initializer = new Object() {
{
Errors.Initializer.initializeFactoryNames(ErrorsAndroid.class);
}
};
}
@@ -28,10 +28,15 @@ import com.intellij.testFramework.LightVirtualFile
import org.jetbrains.kotlin.android.synthetic.AndroidConst
import org.jetbrains.kotlin.android.synthetic.KotlinStringWriter
import org.jetbrains.kotlin.android.synthetic.escapeAndroidIdentifier
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.types.Flexibility
public class AndroidSyntheticFile(val name: String, val contents: String)
public class AndroidSyntheticFile(val name: String, val contents: String) {
companion object {
fun create(name: String, vararg contents: String) = AndroidSyntheticFile(name, contents.joinToString("\n\n"))
}
}
public abstract class SyntheticFileGenerator(protected val project: Project) {
@@ -73,7 +78,7 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
}
val clearCacheFile = renderSyntheticFile
return listOf(clearCacheFile, FLEXIBLE_TYPE_FILE)
return listOf(clearCacheFile, TOOLS_FILE)
}
protected abstract fun extractLayoutResources(files: List<PsiFile>, scope: GlobalSearchScope): List<AndroidResource>
@@ -120,10 +125,15 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
}
private fun KotlinStringWriter.writeSyntheticProperty(receiver: String, resource: AndroidResource, stubCall: String) {
// extract startsWith() to fun
val className = if (isFromSupportV4Package(receiver)) resource.supportClassName else resource.className
val cast = if (resource.className != "View") " as? $className" else ""
val body = arrayListOf("return $stubCall$cast")
// Annotation on wrong widget type
if (resource is AndroidWidget && resource.invalidType != null) {
writeText("@${INVALID_WIDGET_TYPE_ANNOTATION_FQNAME.asString()}(\"${resource.invalidType}\")")
}
writeImmutableExtensionProperty(receiver,
name = resource.id,
retType = "$EXPLICIT_FLEXIBLE_CLASS_NAME<$className, $className?>",
@@ -172,9 +182,9 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
if (!res.sameClass(existing)) {
resourcesToExclude.add(res.id)
} else if (res is AndroidWidget && existing.className != res.className && existing.className != "View") {
} else if (res is AndroidWidget && existing.className != res.className && existing.className != AndroidConst.VIEW_FQNAME) {
// Widgets with the same id but different types exist.
resourceMap.put(res.id, AndroidWidget(res.id, "View"))
resourceMap.put(res.id, AndroidWidget(res.id, AndroidConst.VIEW_FQNAME))
}
}
else resourceMap.put(res.id, res)
@@ -198,14 +208,20 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
}
}
companion object {
public companion object {
private val EXPLICIT_FLEXIBLE_PACKAGE = Flexibility.FLEXIBLE_TYPE_CLASSIFIER.packageFqName.asString()
private val EXPLICIT_FLEXIBLE_CLASS_NAME = Flexibility.FLEXIBLE_TYPE_CLASSIFIER.relativeClassName.asString()
private val ANDROID_IMPORTS = listOf(Flexibility.FLEXIBLE_TYPE_CLASSIFIER.asSingleFqName().asString())
private val FLEXIBLE_TYPE_FILE =
AndroidSyntheticFile("ft", "package $EXPLICIT_FLEXIBLE_PACKAGE\n\nclass $EXPLICIT_FLEXIBLE_CLASS_NAME<L, U>")
private val INVALID_WIDGET_TYPE_ANNOTATION = "InvalidWidgetType"
public val INVALID_WIDGET_TYPE_ANNOTATION_TYPE_PARAMETER: String = "type"
public val INVALID_WIDGET_TYPE_ANNOTATION_FQNAME: FqName = FqName("$EXPLICIT_FLEXIBLE_PACKAGE.$INVALID_WIDGET_TYPE_ANNOTATION")
private val TOOLS_FILE = AndroidSyntheticFile.create("tools",
"package $EXPLICIT_FLEXIBLE_PACKAGE",
"class $EXPLICIT_FLEXIBLE_CLASS_NAME<L, U>",
"annotation class $INVALID_WIDGET_TYPE_ANNOTATION(public val $INVALID_WIDGET_TYPE_ANNOTATION_TYPE_PARAMETER: String)")
}
}
@@ -34,7 +34,11 @@ public abstract class AndroidResource(val id: String) {
public open fun sameClass(other: AndroidResource): Boolean = false
}
public class AndroidWidget(id: String, override val className: String) : AndroidResource(id) {
public class AndroidWidget(
id: String,
override val className: String,
val invalidType: String? = null // When widget type is invalid, this value is the widget tag name
) : AndroidResource(id) {
private companion object {
val MAIN_PROPERTIES = listOf(
AndroidConst.ACTIVITY_FQNAME to "findViewById(0)",