181: using InjectedFileViewProvider for light-files in language-injections

#KT-22929
This commit is contained in:
Nicolay Mitropolsky
2018-02-20 15:01:44 +03:00
committed by Nikolay Krasko
parent 5951fbb390
commit 55753d79e8
4 changed files with 340 additions and 0 deletions
@@ -0,0 +1,22 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
jvmTarget = "1.6"
dependencies {
compile(project(":compiler:util"))
compile(project(":compiler:backend"))
compile(project(":compiler:frontend"))
compile(project(":compiler:frontend.java"))
compileOnly(intellijCoreDep()) { includeJars("intellij-core") }
compileOnly(intellijDep()) { includeJars("annotations", "platform-impl", "asm-all", "trove4j", "guava", rootProject = rootProject) }
}
sourceSets {
"main" { projectDefault() }
"test" {}
}
@@ -0,0 +1,129 @@
/*
* Copyright 2010-2017 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.asJava.builder
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.impl.compiled.ClsFileImpl
import com.intellij.psi.impl.java.stubs.PsiJavaFileStub
import com.intellij.psi.impl.java.stubs.impl.PsiJavaFileStubImpl
import org.jetbrains.kotlin.asJava.elements.getFileViewProviderForLightClass
import com.intellij.psi.impl.source.tree.TreeElement
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
import java.lang.StringBuilder
data class LightClassBuilderResult(val stub: PsiJavaFileStub, val bindingContext: BindingContext, val diagnostics: Diagnostics)
fun buildLightClass(
packageFqName: FqName,
files: Collection<KtFile>,
generateClassFilter: GenerationState.GenerateClassFilter,
context: LightClassConstructionContext,
generate: (state: GenerationState, files: Collection<KtFile>) -> Unit
): LightClassBuilderResult {
val project = files.first().project
try {
val classBuilderFactory = KotlinLightClassBuilderFactory(createJavaFileStub(project, packageFqName, files))
val state = GenerationState.Builder(
project,
classBuilderFactory,
context.module,
context.bindingContext,
files.toList(),
CompilerConfiguration.EMPTY
).generateDeclaredClassFilter(generateClassFilter).wantsDiagnostics(false).build()
state.beforeCompile()
generate(state, files)
val javaFileStub = classBuilderFactory.result()
ServiceManager.getService(project, StubComputationTracker::class.java)?.onStubComputed(javaFileStub, context)
return LightClassBuilderResult(javaFileStub, context.bindingContext, state.collectedExtraJvmDiagnostics)
}
catch (e: ProcessCanceledException) {
throw e
}
catch (e: RuntimeException) {
logErrorWithOSInfo(e, packageFqName, null)
throw e
}
}
private fun createJavaFileStub(project: Project, packageFqName: FqName, files: Collection<KtFile>): PsiJavaFileStub {
val javaFileStub = PsiJavaFileStubImpl(packageFqName.asString(), /*compiled = */true)
javaFileStub.psiFactory = ClsWrapperStubPsiFactory.INSTANCE
val fakeFile = object : ClsFileImpl(getFileViewProviderForLightClass(files.first())) {
override fun getStub() = javaFileStub
override fun getPackageName() = packageFqName.asString()
override fun isPhysical() = false
override fun appendMirrorText(indentLevel: Int, buffer: StringBuilder) {
if (files.size == 1) {
LOG.error("Mirror text should never be calculated for light classes generated from a single file")
}
super.appendMirrorText(indentLevel, buffer)
}
override fun setMirror(element: TreeElement) {
if (files.size == 1) {
LOG.error("Mirror element should never be set for light classes generated from a single file")
}
super.setMirror(element)
}
override fun getMirror(): PsiElement {
if (files.size == 1) {
LOG.error("Mirror element should never be calculated for light classes generated from a single file")
}
return super.getMirror()
}
override fun getText(): String {
return files.singleOrNull()?.text ?: super.getText()
}
}
javaFileStub.psi = fakeFile
return javaFileStub
}
private fun logErrorWithOSInfo(cause: Throwable?, fqName: FqName, virtualFile: VirtualFile?) {
val path = if (virtualFile == null) "<null>" else virtualFile.path
LOG.error(
"Could not generate LightClass for $fqName declared in $path\n" +
"System: ${SystemInfo.OS_NAME} ${SystemInfo.OS_VERSION} Java Runtime: ${SystemInfo.JAVA_RUNTIME_VERSION}",
cause
)
}
private val LOG = Logger.getInstance(LightClassBuilderResult::class.java)
@@ -0,0 +1,94 @@
/*
* Copyright 2010-2016 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.asJava.elements
import com.intellij.pom.java.LanguageLevel
import com.intellij.psi.ClassFileViewProvider
import com.intellij.psi.FileViewProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.impl.compiled.ClsFileImpl
import com.intellij.psi.impl.source.tree.injected.InjectedFileViewProvider
import com.intellij.psi.stubs.PsiClassHolderFileStub
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade
import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
open class FakeFileForLightClass(
val ktFile: KtFile,
private val lightClass: () -> KtLightClass,
private val stub: () -> PsiClassHolderFileStub<*>,
private val packageFqName: FqName = ktFile.packageFqName
) : ClsFileImpl(getFileViewProviderForLightClass(ktFile)) {
override fun getPackageName() = packageFqName.asString()
override fun getStub() = stub()
override fun getClasses() = arrayOf(lightClass())
override fun getNavigationElement() = ktFile
override fun accept(visitor: PsiElementVisitor) {
// Prevent access to compiled PSI
// TODO: More complex traversal logic may be implemented when necessary
}
// this should be equal to current compiler target language level
override fun getLanguageLevel() = LanguageLevel.JDK_1_6
override fun hashCode(): Int {
val thisClass = lightClass()
if (thisClass is KtLightClassForSourceDeclaration) return ktFile.hashCode()
return thisClass.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is FakeFileForLightClass) return false
val thisClass = lightClass()
val anotherClass = other.lightClass()
if (thisClass is KtLightClassForSourceDeclaration) {
return anotherClass is KtLightClassForSourceDeclaration && ktFile == other.ktFile
}
return thisClass == anotherClass
}
override fun isEquivalentTo(another: PsiElement?) = this == another
override fun setPackageName(packageName: String) {
if (lightClass() is KtLightClassForFacade) {
ktFile.packageDirective?.fqName = FqName(packageName)
}
else {
super.setPackageName(packageName)
}
}
override fun isPhysical() = false
}
fun getFileViewProviderForLightClass(ktFile: KtFile): FileViewProvider {
ktFile.viewProvider.safeAs<InjectedFileViewProvider>()?.let { return it }
return ClassFileViewProvider(
ktFile.manager, ktFile.virtualFile ?: ktFile.originalFile.virtualFile ?: ktFile.viewProvider.virtualFile
)
}
@@ -0,0 +1,95 @@
/*
* 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.asJava
import com.intellij.injected.editor.EditorWindow
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.testFramework.LightProjectDescriptor
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.test.KotlinLightProjectDescriptor
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
class KtFileLightClassTest : KotlinLightCodeInsightFixtureTestCase() {
override fun getProjectDescriptor(): LightProjectDescriptor = KotlinLightProjectDescriptor.INSTANCE
fun testSimple() {
val file = myFixture.configureByText("A.kt", "class C {}\nobject O {}") as KtFile
val classes = file.classes
assertEquals(2, classes.size)
assertEquals("C", classes[0].qualifiedName)
assertEquals("O", classes[1].qualifiedName)
}
fun testFileClass() {
val file = myFixture.configureByText("A.kt", "fun f() {}") as KtFile
val classes = file.classes
assertEquals(1, classes.size)
assertEquals("AKt", classes[0].qualifiedName)
}
fun testMultifileClass() {
val file = myFixture.configureByFiles("multifile1.kt", "multifile2.kt")[0] as KtFile
val aClass = file.classes.single()
assertEquals(1, aClass.findMethodsByName("foo", false).size)
assertEquals(1, aClass.findMethodsByName("bar", false).size)
}
fun testAliasesOnly() {
val file = myFixture.configureByFile("aliasesOnly.kt") as KtFile
val aClass = file.classes.single()
assertEquals(0, aClass.getMethods().size)
}
fun testNoFacadeForScript() {
val file = myFixture.configureByText("foo.kts", "package foo") as KtFile
assertEquals(0, file.classes.size)
val facadeFiles = KotlinAsJavaSupport.getInstance(project).findFilesForFacade(FqName("foo.FooKt"), GlobalSearchScope.allScope(project))
assertEquals(0, facadeFiles.size)
}
fun testNoFacadeForHeaderClass() {
val file = myFixture.configureByText("foo.kt", "header fun foo(): Int") as KtFile
assertEquals(0, file.classes.size)
val facadeFiles = KotlinAsJavaSupport.getInstance(project).findFilesForFacade(FqName("foo.FooKt"), GlobalSearchScope.allScope(project))
assertEquals(0, facadeFiles.size)
}
override fun getTestDataPath(): String {
return PluginTestCaseBase.getTestDataPathBase() + "/asJava/fileLightClass/"
}
fun testInjectedCode() {
myFixture.configureByText("foo.kt", """
import org.intellij.lang.annotations.Language
fun foo(@Language("kotlin") a: String){a.toString()}
fun bar(){ foo("class<caret> A") }
""")
myFixture.testHighlighting("foo.kt")
val injectedFile = (editor as? EditorWindow)?.injectedFile
assertEquals("Wrong injection language", "kotlin", injectedFile?.language?.id)
assertEquals("Injected class should be `A`", "A", (injectedFile as KtFile).classes.single().name)
}
}