Resolve Android widget class fq names properly (KT-8790)

This commit is contained in:
Yan Zhulanow
2015-08-20 15:11:01 +03:00
parent 8577f3e91b
commit e0bd532b7a
11 changed files with 171 additions and 123 deletions
@@ -21,6 +21,10 @@ 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.res.AndroidLayoutXmlFileManager
import org.jetbrains.kotlin.android.synthetic.res.CliAndroidLayoutXmlFileManager
import org.jetbrains.kotlin.android.synthetic.res.CliSyntheticFileGenerator
import org.jetbrains.kotlin.android.synthetic.res.SyntheticFileGenerator
import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension
import org.jetbrains.kotlin.compiler.plugin.CliOption
import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException
@@ -29,10 +33,6 @@ import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.CompilerConfigurationKey
import org.jetbrains.kotlin.extensions.ExternalDeclarationsProvider
import org.jetbrains.kotlin.android.synthetic.res.AndroidLayoutXmlFileManager
import org.jetbrains.kotlin.android.synthetic.res.SyntheticFileGenerator
import org.jetbrains.kotlin.android.synthetic.res.CliAndroidLayoutXmlFileManager
import org.jetbrains.kotlin.android.synthetic.res.CliSyntheticFileGenerator
import org.jetbrains.kotlin.psi.JetFile
public object AndroidConfigurationKeys {
@@ -70,8 +70,8 @@ public class AndroidCommandLineProcessor : CommandLineProcessor {
public class CliAndroidDeclarationsProvider(private val project: Project) : ExternalDeclarationsProvider {
override fun getExternalDeclarations(moduleInfo: ModuleInfo?): Collection<JetFile> {
val parser = ServiceManager.getService(project, javaClass<SyntheticFileGenerator>())
return parser.getSyntheticFiles() ?: listOf()
val parser = ServiceManager.getService(project, javaClass<SyntheticFileGenerator>()) as? CliSyntheticFileGenerator
return parser?.getSyntheticFiles() ?: listOf()
}
}
@@ -55,6 +55,8 @@ public object AndroidConst {
val ESCAPED_IDENTIFIERS = (JetTokens.KEYWORDS.getTypes() + JetTokens.SOFT_KEYWORDS.getTypes())
.map { it as? JetKeywordToken }.filterNotNull().map { it.getValue() }.toSet()
val FQNAME_RESOLVE_PACKAGES = listOf("android.widget", "android.webkit", "android.view")
}
public fun nameToIdDeclaration(name: String): String = AndroidConst.ID_DECLARATION_PREFIX + name
@@ -74,10 +76,13 @@ fun escapeAndroidIdentifier(id: String): String {
return if (id in AndroidConst.ESCAPED_IDENTIFIERS) "`$id`" else id
}
public fun parseAndroidResource(id: String, type: String): AndroidResource {
return when (type) {
public fun parseAndroidResource(id: String, tag: String, fqNameResolver: (String) -> String?): AndroidResource {
return when (tag) {
"fragment" -> AndroidFragment(id)
"include" -> AndroidWidget(id, "View")
else -> AndroidWidget(id, type)
"include" -> AndroidWidget(id, AndroidConst.VIEW_FQNAME)
else -> {
val fqName = fqNameResolver(tag) ?: AndroidConst.VIEW_FQNAME
AndroidWidget(id, fqName)
}
}
}
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.android.synthetic.codegen
import org.jetbrains.kotlin.android.synthetic.AndroidConst
import org.jetbrains.kotlin.codegen.ClassBuilder
import org.jetbrains.kotlin.codegen.FunctionCodegen
import org.jetbrains.kotlin.codegen.StackValue
@@ -23,7 +24,6 @@ import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.codegen.state.JetTypeMapper
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.android.synthetic.AndroidConst
import org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaClassDescriptor
import org.jetbrains.kotlin.psi.JetClassOrObject
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
@@ -16,23 +16,31 @@
package org.jetbrains.kotlin.android.synthetic.res
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.ModificationTracker
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiFile
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider.Result
import java.io.ByteArrayInputStream
import org.jetbrains.kotlin.android.synthetic.AndroidXmlHandler
import org.jetbrains.kotlin.android.synthetic.parseAndroidResource
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.psi.JetFile
import kotlin.properties.Delegates
public class CliSyntheticFileGenerator(
public open class CliSyntheticFileGenerator(
project: Project,
private val manifestPath: String,
private val resDirectories: List<String>,
private val supportV4: Boolean
) : SyntheticFileGenerator(project) {
private val javaPsiFacade: JavaPsiFacade by lazy { JavaPsiFacade.getInstance(project) }
private val projectScope: GlobalSearchScope by lazy { GlobalSearchScope.allScope(project) }
private val cachedJetFiles by lazy {
generateSyntheticJetFiles(generateSyntheticFiles(true, projectScope))
}
override fun supportV4(): Boolean {
return supportV4
}
@@ -41,27 +49,31 @@ public class CliSyntheticFileGenerator(
CliAndroidLayoutXmlFileManager(project, manifestPath, resDirectories)
}
override val cachedSources: CachedValue<List<AndroidSyntheticFile>> by lazy {
cachedValue {
Result.create(generateSyntheticFiles(), ModificationTracker.NEVER_CHANGED)
}
}
public override fun getSyntheticFiles(): List<JetFile> = cachedJetFiles
override fun extractLayoutResources(files: List<PsiFile>): List<AndroidResource> {
override fun extractLayoutResources(files: List<PsiFile>, scope: GlobalSearchScope): List<AndroidResource> {
val resources = arrayListOf<AndroidResource>()
val handler = AndroidXmlHandler { id, widgetType -> resources.add(parseAndroidResource(id, widgetType)) }
try {
for (file in files) {
val handler = AndroidXmlHandler { id, tag ->
resources += parseAndroidResource(id, tag) { tag ->
resolveFqClassNameForView(javaPsiFacade, scope, tag)
}
}
for (file in files) {
try {
val inputStream = ByteArrayInputStream(file.getVirtualFile().contentsToByteArray())
layoutXmlFileManager.saxParser.parse(inputStream, handler)
} catch (e: Throwable) {
LOG.error(e)
}
return filterDuplicates(resources)
}
catch (e: Throwable) {
LOG.error(e)
return listOf()
}
return filterDuplicates(resources)
}
private companion object {
private val LOG: Logger = Logger.getInstance(javaClass)
}
}
@@ -16,10 +16,11 @@
package org.jetbrains.kotlin.android.synthetic.res
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider.Result
import com.intellij.psi.util.CachedValuesManager
@@ -36,56 +37,20 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
public class NoAndroidManifestFound : Exception("No android manifest file found in project root")
protected val LOG: Logger = Logger.getInstance(javaClass)
public abstract val layoutXmlFileManager: AndroidLayoutXmlFileManager
protected abstract val cachedSources: CachedValue<List<AndroidSyntheticFile>>
private val cachedJetFiles: CachedValue<List<JetFile>> by lazy {
cachedValue {
val psiManager = PsiManager.getInstance(project)
val applicationPackage = layoutXmlFileManager.androidModuleInfo?.applicationPackage
val jetFiles = cachedSources.value.mapIndexed { index, syntheticFile ->
val fileName = AndroidConst.SYNTHETIC_FILENAME_PREFIX + syntheticFile.name + ".kt"
val virtualFile = LightVirtualFile(fileName, syntheticFile.contents)
val jetFile = psiManager.findFile(virtualFile) as JetFile
if (applicationPackage != null) {
jetFile.putUserData(AndroidConst.ANDROID_USER_PACKAGE, applicationPackage)
}
jetFile
}
Result.create(jetFiles, cachedSources)
}
}
protected abstract fun supportV4(): Boolean
public fun generateSyntheticFiles(generateCommonFiles: Boolean = true): List<AndroidSyntheticFile> {
val commonFiles = if (generateCommonFiles) {
val renderSyntheticFile = renderSyntheticFile("clearCache") {
writePackage(AndroidConst.SYNTHETIC_PACKAGE)
writeAndroidImports()
writeClearCacheFunction(AndroidConst.ACTIVITY_FQNAME)
writeClearCacheFunction(AndroidConst.FRAGMENT_FQNAME)
if (supportV4()) writeClearCacheFunction(AndroidConst.SUPPORT_FRAGMENT_FQNAME)
}
val clearCacheFile = renderSyntheticFile
public abstract fun getSyntheticFiles(): List<JetFile>
listOf(clearCacheFile,
FLEXIBLE_TYPE_FILE,
FAKE_SUPPORT_V4_APP_FILE,
FAKE_SUPPORT_V4_VIEW_FILE,
FAKE_SUPPORT_V4_WIDGET_FILE)
} else listOf()
protected fun generateSyntheticFiles(generateCommonFiles: Boolean = true, scope: GlobalSearchScope): List<AndroidSyntheticFile> {
val commonFiles = if (generateCommonFiles) generateCommonFiles() else listOf()
return layoutXmlFileManager.getLayoutXmlFiles().flatMap { entry ->
val files = entry.getValue()
val resources = extractLayoutResources(files)
val resources = extractLayoutResources(files, scope)
val layoutName = files[0].name.substringBefore('.')
val layoutName = entry.getKey()
val mainLayoutFile = renderMainLayoutFile(layoutName, resources)
val viewLayoutFile = renderViewLayoutFile(layoutName, resources)
@@ -94,9 +59,25 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
}.filterNotNull() + commonFiles
}
public fun getSyntheticFiles(): List<JetFile>? = cachedJetFiles.value
private fun generateCommonFiles(): List<AndroidSyntheticFile> {
val renderSyntheticFile = renderSyntheticFile("clearCache") {
writePackage(AndroidConst.SYNTHETIC_PACKAGE)
writeAndroidImports()
writeClearCacheFunction(AndroidConst.ACTIVITY_FQNAME)
writeClearCacheFunction(AndroidConst.FRAGMENT_FQNAME)
if (supportV4()) writeClearCacheFunction(AndroidConst.SUPPORT_FRAGMENT_FQNAME)
}
val clearCacheFile = renderSyntheticFile
protected abstract fun extractLayoutResources(files: List<PsiFile>): List<AndroidResource>
return listOf(
clearCacheFile,
FLEXIBLE_TYPE_FILE,
FAKE_SUPPORT_V4_APP_FILE,
FAKE_SUPPORT_V4_VIEW_FILE,
FAKE_SUPPORT_V4_WIDGET_FILE)
}
protected abstract fun extractLayoutResources(files: List<PsiFile>, scope: GlobalSearchScope): List<AndroidResource>
private fun renderMainLayoutFile(layoutName: String, resources: List<AndroidResource>): AndroidSyntheticFile {
return renderLayoutFile(layoutName + AndroidConst.LAYOUT_POSTFIX,
@@ -140,13 +121,13 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
writeEmptyLine()
}
private fun KotlinStringWriter.writeSyntheticProperty(receiver: String, widget: AndroidResource, stubCall: String) {
private fun KotlinStringWriter.writeSyntheticProperty(receiver: String, resource: AndroidResource, stubCall: String) {
// extract startsWith() to fun
val className = if (isFromSupportV4Package(receiver)) widget.supportClassName else widget.className
val cast = if (widget.className != "View") " as? $className" else ""
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")
writeImmutableExtensionProperty(receiver,
name = widget.id,
name = resource.id,
retType = "$EXPLICIT_FLEXIBLE_CLASS_NAME<$className, $className?>",
getterBody = body)
}
@@ -163,6 +144,22 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
return fqName.startsWith(AndroidConst.SUPPORT_V4_PACKAGE)
}
protected fun resolveFqClassNameForView(javaPsiFacade: JavaPsiFacade, scope: GlobalSearchScope, tag: String): String? {
if (tag.contains('.')) {
if (javaPsiFacade.findClass(tag, scope) == null) {
return null
}
return tag
}
for (pkg in AndroidConst.FQNAME_RESOLVE_PACKAGES) {
val fqName = "$pkg.$tag"
if (javaPsiFacade.findClass(fqName, scope) != null) {
return fqName
}
}
return null
}
protected fun filterDuplicates(resources: List<AndroidResource>): List<AndroidResource> {
val resourceMap = linkedMapOf<String, AndroidResource>()
val resourcesToExclude = hashSetOf<String>()
@@ -184,6 +181,21 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
return resourceMap.values().toList()
}
protected fun generateSyntheticJetFiles(files: List<AndroidSyntheticFile>): List<JetFile> {
val psiManager = PsiManager.getInstance(project)
val applicationPackage = layoutXmlFileManager.androidModuleInfo?.applicationPackage
return files.mapIndexed { index, syntheticFile ->
val fileName = AndroidConst.SYNTHETIC_FILENAME_PREFIX + syntheticFile.name + ".kt"
val virtualFile = LightVirtualFile(fileName, syntheticFile.contents)
val jetFile = psiManager.findFile(virtualFile) as JetFile
if (applicationPackage != null) {
jetFile.putUserData(AndroidConst.ANDROID_USER_PACKAGE, applicationPackage)
}
jetFile
}
}
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()