Support variants in Android Extensions (compiler plugin)

This commit is contained in:
Yan Zhulanow
2015-10-21 21:56:52 +03:00
parent a6e9ee9323
commit 161d8b105f
100 changed files with 806 additions and 143 deletions
@@ -25,10 +25,7 @@ import org.jetbrains.kotlin.android.synthetic.codegen.AndroidExpressionCodegenEx
import org.jetbrains.kotlin.android.synthetic.codegen.AndroidOnDestroyClassBuilderInterceptorExtension
import org.jetbrains.kotlin.android.synthetic.diagnostic.AndroidExtensionPropertiesCallChecker
import org.jetbrains.kotlin.android.synthetic.diagnostic.DefaultErrorMessagesAndroid
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.android.synthetic.res.*
import org.jetbrains.kotlin.codegen.extensions.ClassBuilderInterceptorExtension
import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension
import org.jetbrains.kotlin.compiler.plugin.CliOption
@@ -47,30 +44,30 @@ 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")
public val ANDROID_MANIFEST: CompilerConfigurationKey<String> = CompilerConfigurationKey.create<String>("android manifest file")
public val VARIANT: CompilerConfigurationKey<List<String>> = CompilerConfigurationKey.create<List<String>>("Android build variant")
public val PACKAGE: CompilerConfigurationKey<String> = CompilerConfigurationKey.create<String>("application package fq name")
}
public class AndroidCommandLineProcessor : CommandLineProcessor {
companion object {
public val ANDROID_COMPILER_PLUGIN_ID: String = "org.jetbrains.kotlin.android"
public val RESOURCE_PATH_OPTION: CliOption = CliOption("androidRes", "<path>", "Android resources path", allowMultipleOccurrences = true)
public val MANIFEST_FILE_OPTION: CliOption = CliOption("androidManifest", "<path>", "Android manifest file")
public val VARIANT_OPTION: CliOption = CliOption("variant", "<name;path>", "Android build variant", allowMultipleOccurrences = true)
public val PACKAGE_OPTION: CliOption = CliOption("package", "<fq name>", "Application package")
}
override val pluginId: String = ANDROID_COMPILER_PLUGIN_ID
override val pluginOptions: Collection<CliOption> = listOf(RESOURCE_PATH_OPTION, MANIFEST_FILE_OPTION)
override val pluginOptions: Collection<CliOption> = listOf(VARIANT_OPTION, PACKAGE_OPTION)
override fun processOption(option: CliOption, value: String, configuration: CompilerConfiguration) {
when (option) {
RESOURCE_PATH_OPTION -> {
val paths = configuration.getList(AndroidConfigurationKeys.ANDROID_RES_PATH).toArrayList()
VARIANT_OPTION -> {
val paths = configuration.getList(AndroidConfigurationKeys.VARIANT).toArrayList()
paths.add(value)
configuration.put(AndroidConfigurationKeys.ANDROID_RES_PATH, paths)
configuration.put(AndroidConfigurationKeys.VARIANT, paths)
}
MANIFEST_FILE_OPTION -> configuration.put(AndroidConfigurationKeys.ANDROID_MANIFEST, value)
PACKAGE_OPTION -> configuration.put(AndroidConfigurationKeys.PACKAGE, value)
else -> throw CliOptionProcessingException("Unknown option: ${option.name}")
}
}
@@ -86,14 +83,16 @@ public class CliAndroidDeclarationsProvider(private val project: Project) : Exte
public class AndroidComponentRegistrar : ComponentRegistrar {
public override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
val androidResPath = configuration.get(AndroidConfigurationKeys.ANDROID_RES_PATH)
val androidManifest = configuration.get(AndroidConfigurationKeys.ANDROID_MANIFEST)
val applicationPackage = configuration.get(AndroidConfigurationKeys.PACKAGE)
val variants = configuration.get(AndroidConfigurationKeys.VARIANT)?.map { parseVariant(it) }?.filterNotNull() ?: emptyList()
if (androidResPath != null && androidManifest != null) {
val xmlProcessor = CliSyntheticFileGenerator(project, androidManifest, androidResPath)
if (variants.isNotEmpty() && !applicationPackage.isNullOrBlank()) {
val xmlProcessor = CliSyntheticFileGenerator(project, applicationPackage!!, variants)
project.registerService(SyntheticFileGenerator::class.java, xmlProcessor)
project.registerService(AndroidLayoutXmlFileManager::class.java, CliAndroidLayoutXmlFileManager(project, androidManifest, androidResPath))
val layoutXmlFileManager = CliAndroidLayoutXmlFileManager(project, applicationPackage, variants)
project.registerService(AndroidLayoutXmlFileManager::class.java, layoutXmlFileManager)
ExternalDeclarationsProvider.registerExtension(project, CliAndroidDeclarationsProvider(project))
ExpressionCodegenExtension.registerExtension(project, AndroidExpressionCodegenExtension())
@@ -102,6 +101,12 @@ public class AndroidComponentRegistrar : ComponentRegistrar {
ClassBuilderInterceptorExtension.registerExtension(project, AndroidOnDestroyClassBuilderInterceptorExtension())
}
}
private fun parseVariant(s: String): AndroidVariant? {
val parts = s.split(';')
if (parts.size < 2) return null
return AndroidVariant(parts[0], parts.drop(0))
}
}
public class AndroidExtensionPropertiesComponentContainerContributor : StorageComponentContainerContributor {
@@ -34,7 +34,7 @@ open class Context(val buffer: StringBuffer = StringBuffer(), private var indent
indentDepth--
if (indentDepth < 0)
throw InvalidIndent(indentDepth)
currentIndent = currentIndent.substring(0, currentIndent.length() - indentUnit.length())
currentIndent = currentIndent.substring(0, currentIndent.length - indentUnit.length)
}
public open fun write(what: String) {
@@ -57,7 +57,7 @@ open class Context(val buffer: StringBuffer = StringBuffer(), private var indent
public fun trim(num: Int) {
buffer.delete(buffer.length() - num, buffer.length())
buffer.delete(buffer.length - num, buffer.length())
}
public fun fork(newBuffer: StringBuffer = StringBuffer(),
@@ -32,7 +32,7 @@ class KotlinStringWriter : KotlinWriter {
body.writeln("val $name: $retType")
body.incIndent()
body.write("get() ")
if (getterBody.size() > 1) {
if (getterBody.size > 1) {
body.writeNoIndent("{\n")
body.incIndent()
for (stmt in getterBody) {
@@ -27,15 +27,25 @@ import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.psi.KtProperty
import java.util.*
public abstract class AndroidLayoutXmlFileManager(val project: Project) {
class AndroidVariantData(val variant: AndroidVariant, private val layouts: Map<String, List<PsiFile>>): Map<String, List<PsiFile>> by layouts
class AndroidModuleData(val module: AndroidModule, private val variants: List<AndroidVariantData>): Iterable<AndroidVariantData> by variants {
companion object {
val EMPTY = AndroidModuleData(AndroidModule("android", listOf()), listOf())
}
}
public abstract val androidModuleInfo: AndroidModuleInfo?
abstract class AndroidLayoutXmlFileManager(val project: Project) {
public abstract val androidModule: AndroidModule?
public open fun propertyToXmlAttributes(property: KtProperty): List<PsiElement> = listOf()
public fun getLayoutXmlFiles(): Map<String, List<PsiFile>> {
val info = androidModuleInfo ?: return mapOf()
public fun getLayoutXmlFiles(): AndroidModuleData {
val androidModule = androidModule ?: return AndroidModuleData.EMPTY
return AndroidModuleData(androidModule, androidModule.variants.map { getVariantData(it) })
}
public fun getVariantData(variant: AndroidVariant): AndroidVariantData {
val psiManager = PsiManager.getInstance(project)
val fileManager = VirtualFileManager.getInstance()
@@ -53,11 +63,11 @@ public abstract class AndroidLayoutXmlFileManager(val project: Project) {
return allChildren
}
val resDirectories = info.resDirectories.map { fileManager.findFileByUrl("file://$it") }
val resDirectories = variant.resDirectories.map { fileManager.findFileByUrl("file://$it") }
val allChildren = resDirectories.flatMap { it?.getAllChildren() ?: listOf() }
val allLayoutFiles = allChildren.filter { it.parent.name.startsWith("layout") && it.name.toLowerCase().endsWith(".xml") }
val allLayoutPsiFiles = allLayoutFiles.fold(ArrayList<PsiFile>(allLayoutFiles.size())) { list, file ->
val allLayoutPsiFiles = allLayoutFiles.fold(ArrayList<PsiFile>(allLayoutFiles.size)) { list, file ->
val psiFile = psiManager.findFile(file)
if (psiFile != null && psiFile.parent != null) {
list += psiFile
@@ -67,9 +77,9 @@ public abstract class AndroidLayoutXmlFileManager(val project: Project) {
val layoutNameToXmlFiles = allLayoutPsiFiles
.groupBy { it.name.substringBeforeLast('.') }
.mapValues { it.getValue().sortedBy { it.parent!!.name.length() } }
.mapValues { it.value.sortedBy { it.parent!!.name.length } }
return layoutNameToXmlFiles
return AndroidVariantData(variant, layoutNameToXmlFiles)
}
companion object {
@@ -17,23 +17,16 @@
package org.jetbrains.kotlin.android.synthetic.res
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.android.synthetic.toMap
import org.xml.sax.Attributes
import org.xml.sax.helpers.DefaultHandler
import java.io.File
import java.io.FileInputStream
import javax.xml.parsers.SAXParser
import javax.xml.parsers.SAXParserFactory
public class CliAndroidLayoutXmlFileManager(
project: Project,
private val manifestPath: String,
private val resDirectories: List<String>
private val applicationPackage: String,
private val variants: List<AndroidVariant>
) : AndroidLayoutXmlFileManager(project) {
override val androidModuleInfo by lazy {
AndroidModuleInfo(getApplicationPackage(manifestPath), resDirectories)
}
override val androidModule by lazy { AndroidModule(applicationPackage, variants) }
val saxParser: SAXParser = initSAX()
@@ -43,26 +36,4 @@ public class CliAndroidLayoutXmlFileManager(
return saxFactory.newSAXParser()
}
private fun getApplicationPackage(manifestPath: String): String {
try {
val manifestXml = File(manifestPath)
var applicationPackage: String = ""
try {
saxParser.parse(FileInputStream(manifestXml), object : DefaultHandler() {
override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
if (localName == "manifest")
applicationPackage = attributes.toMap()["package"] ?: ""
}
})
}
catch (e: Exception) {
throw e
}
return applicationPackage
}
catch (e: Exception) {
throw SyntheticFileGenerator.NoAndroidManifestFound()
}
}
}
@@ -20,17 +20,17 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementFinder
import com.intellij.psi.PsiFile
import java.io.ByteArrayInputStream
import com.intellij.psi.impl.PsiElementFinderImpl
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.android.synthetic.AndroidConst
import org.jetbrains.kotlin.android.synthetic.AndroidXmlHandler
import org.jetbrains.kotlin.psi.KtFile
import java.io.ByteArrayInputStream
public open class CliSyntheticFileGenerator(
project: Project,
private val manifestPath: String,
private val resDirectories: List<String>
private val variants: List<AndroidVariant>
) : SyntheticFileGenerator(project) {
private val cachedJetFiles by lazy {
@@ -40,7 +40,7 @@ public open class CliSyntheticFileGenerator(
}
override val layoutXmlFileManager: CliAndroidLayoutXmlFileManager by lazy {
CliAndroidLayoutXmlFileManager(project, manifestPath, resDirectories)
CliAndroidLayoutXmlFileManager(project, manifestPath, variants)
}
public override fun getSyntheticFiles(): List<KtFile> = cachedJetFiles
@@ -47,16 +47,18 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
protected open fun generateSyntheticFiles(generateCommonFiles: Boolean, supportV4: Boolean): List<AndroidSyntheticFile> {
val commonFiles = if (generateCommonFiles) generateCommonFiles(supportV4) else listOf()
return layoutXmlFileManager.getLayoutXmlFiles().flatMap { entry ->
val files = entry.getValue()
val resources = extractLayoutResources(files)
return layoutXmlFileManager.getLayoutXmlFiles().flatMap { variantData ->
variantData.flatMap { entry ->
val files = entry.value
val resources = extractLayoutResources(files)
val layoutName = entry.getKey()
val layoutName = entry.key
val mainLayoutFile = renderMainLayoutFile(layoutName, resources, supportV4)
val viewLayoutFile = renderViewLayoutFile(layoutName, resources)
listOf(mainLayoutFile, viewLayoutFile)
arrayListOf<AndroidSyntheticFile>().apply {
this += renderMainLayoutFiles(variantData.variant, layoutName, resources, supportV4)
this += renderViewLayoutFiles(variantData.variant, layoutName, resources)
}
}
}.filterNotNull() + commonFiles
}
@@ -79,34 +81,59 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
protected abstract fun checkIfClassExist(fqName: String): Boolean
private fun renderMainLayoutFile(layoutName: String, resources: List<AndroidResource>, supportV4: Boolean): AndroidSyntheticFile {
return renderLayoutFile(layoutName + AndroidConst.LAYOUT_POSTFIX, escapeAndroidIdentifier(layoutName), resources) {
private fun renderMainLayoutFiles(
variant: AndroidVariant,
layoutName: String,
resources: List<AndroidResource>,
supportV4: Boolean
): List<AndroidSyntheticFile> {
return renderLayoutFile(variant, layoutName + AndroidConst.LAYOUT_POSTFIX, escapeAndroidIdentifier(layoutName), resources) {
val properties = it.mainProperties.toArrayList()
if (supportV4) properties.addAll(it.mainPropertiesForSupportV4)
properties
}
}
private fun renderViewLayoutFile(layoutName: String, resources: List<AndroidResource>): AndroidSyntheticFile {
return renderLayoutFile(layoutName + AndroidConst.VIEW_LAYOUT_POSTFIX,
private fun renderViewLayoutFiles(
variant: AndroidVariant,
layoutName: String,
resources: List<AndroidResource>
): List<AndroidSyntheticFile> {
return renderLayoutFile(variant, layoutName + AndroidConst.VIEW_LAYOUT_POSTFIX,
escapeAndroidIdentifier(layoutName) + ".view", resources) { it.viewProperties }
}
private fun renderLayoutFile(
variant: AndroidVariant,
filename: String,
packageSegment: String,
layoutName: String,
resources: List<AndroidResource>,
properties: (AndroidResource) -> List<Pair<String, String>>): AndroidSyntheticFile {
return renderSyntheticFile(filename) {
writePackage(AndroidConst.SYNTHETIC_PACKAGE + "." + packageSegment)
writeAndroidImports()
properties: (AndroidResource) -> List<Pair<String, String>>): List<AndroidSyntheticFile> {
fun render(defaultVariant: Boolean = false): AndroidSyntheticFile {
val fullFilename = (if (defaultVariant) "" else "${variant.name}_") + filename
return renderSyntheticFile(fullFilename) {
val packageName = if (defaultVariant)
AndroidConst.SYNTHETIC_PACKAGE + "." + layoutName
else
AndroidConst.SYNTHETIC_PACKAGE + '.' + variant.name + '.' + layoutName
for (res in resources) {
properties(res).forEach { property ->
writeSyntheticProperty(property.first, res, property.second)
writePackage(packageName)
writeAndroidImports()
for (res in resources) {
properties(res).forEach { property ->
if (defaultVariant) {
val deprecatedText = "Use the property from the 'main' variant instead"
val import = AndroidConst.SYNTHETIC_PACKAGE + '.' + variant.name + '.' + layoutName + '.' + res.id
writeText("@Deprecated(\"$deprecatedText\", ReplaceWith(\"${res.id}\", \"$import\"))")
}
writeSyntheticProperty(property.first, res, property.second)
}
}
}
}
return if (variant.isMainVariant) listOf(render(), render(true)) else listOf(render())
}
private fun renderSyntheticFile(filename: String, init: KotlinStringWriter.() -> Unit): AndroidSyntheticFile {
@@ -199,7 +226,7 @@ public abstract class SyntheticFileGenerator(protected val project: Project) {
protected fun generateSyntheticJetFiles(files: List<AndroidSyntheticFile>): List<KtFile> {
val psiManager = PsiManager.getInstance(project)
val applicationPackage = layoutXmlFileManager.androidModuleInfo?.applicationPackage
val applicationPackage = layoutXmlFileManager.androidModule?.applicationPackage
return files.mapIndexed { index, syntheticFile ->
val fileName = AndroidConst.SYNTHETIC_FILENAME_PREFIX + syntheticFile.name + ".kt"
@@ -18,13 +18,19 @@ package org.jetbrains.kotlin.android.synthetic.res
import org.jetbrains.kotlin.android.synthetic.AndroidConst
public class AndroidModuleInfo(val applicationPackage: String, resDirectories: List<String>) {
class AndroidVariant(val name: String, val resDirectories: List<String>) {
val packageName: String = name
val isMainVariant: Boolean
get() = name == "main"
override fun equals(other: Any?) = other is AndroidModuleInfo && applicationPackage == other.applicationPackage
companion object {
fun createMainVariant(resDirectories: List<String>) = AndroidVariant("main", resDirectories)
}
}
public class AndroidModule(val applicationPackage: String, val variants: List<AndroidVariant>) {
override fun equals(other: Any?) = other is AndroidModule && applicationPackage == other.applicationPackage
override fun hashCode() = applicationPackage.hashCode()
val resDirectories = resDirectories.sorted()
}
public abstract class AndroidResource(val id: String) {