Support for implicit package prefixes

Now, if all Kotlin files directly under a source root have the same
package statement, it's used as an implicit package prefix for that
source root. It's honored by the package name/directory name mismatch
inspection and by new file creation actions;
support in refactorings is TODO.

 #KT-17306 Fixed
This commit is contained in:
Dmitry Jemerov
2018-03-06 14:01:06 +01:00
parent 753319a89d
commit 81adbcb7e6
8 changed files with 281 additions and 97 deletions
@@ -1,33 +1,26 @@
/*
* 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.
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.caches
import com.intellij.ProjectTopics
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.fileTypes.FileTypeRegistry
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.*
import com.intellij.psi.PsiManager
import com.intellij.psi.impl.PsiTreeChangeEventImpl
import com.intellij.psi.impl.PsiTreeChangePreprocessor
import com.intellij.psi.search.GlobalSearchScope
@@ -38,6 +31,8 @@ import org.jetbrains.kotlin.idea.caches.project.ModuleSourceInfo
import org.jetbrains.kotlin.idea.caches.project.getModuleInfoByVirtualFile
import org.jetbrains.kotlin.idea.caches.project.getNullableModuleInfo
import org.jetbrains.kotlin.idea.stubindex.PackageIndexUtil
import org.jetbrains.kotlin.idea.util.getSourceRoot
import org.jetbrains.kotlin.idea.util.sourceRoot
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtPackageDirective
@@ -58,8 +53,7 @@ class KotlinPackageContentModificationListener(private val project: Project) {
it is VFileMoveEvent || it is VFileCreateEvent || it is VFileCopyEvent || it is VFileDeleteEvent
fun onEvents(events: List<VFileEvent>) {
val service = ServiceManager.getService(project, PerModulePackageCacheService::class.java)
val service = PerModulePackageCacheService.getInstance(project)
if (events.size >= FULL_DROP_THRESHOLD) {
service.onTooComplexChange()
} else {
@@ -67,12 +61,20 @@ class KotlinPackageContentModificationListener(private val project: Project) {
.asSequence()
.filter { it.file != null }
.filter(::isRelevant)
.mapNotNull { it.file }
.filter { it.isDirectory || FileTypeRegistry.getInstance().getFileTypeByFileName(it.name) == KotlinFileType.INSTANCE }
.forEach { file -> service.notifyPackageChange(file) }
.filter {
val vFile = it.file!!
vFile.isDirectory || FileTypeRegistry.getInstance().getFileTypeByFileName(vFile.name) == KotlinFileType.INSTANCE
}
.forEach { event -> service.notifyPackageChange(event) }
}
}
})
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent?) {
PerModulePackageCacheService.getInstance(project).onTooComplexChange()
}
})
}
}
@@ -100,6 +102,96 @@ class KotlinPackageStatementPsiTreeChangePreprocessor(private val project: Proje
}
}
private typealias ImplicitPackageData = MutableMap<FqName, MutableList<VirtualFile>>
class ImplicitPackagePrefixCache(private val project: Project) {
private val implicitPackageCache = ConcurrentHashMap<VirtualFile, ImplicitPackageData>()
fun getPrefix(sourceRoot: VirtualFile): FqName {
val implicitPackageMap = implicitPackageCache.getOrPut(sourceRoot) { analyzeImplicitPackagePrefixes(sourceRoot) }
return implicitPackageMap.keys.singleOrNull() ?: FqName.ROOT
}
internal fun clear() {
implicitPackageCache.clear()
}
private fun analyzeImplicitPackagePrefixes(sourceRoot: VirtualFile): MutableMap<FqName, MutableList<VirtualFile>> {
val result = mutableMapOf<FqName, MutableList<VirtualFile>>()
val ktFiles = sourceRoot.children.filter { it.fileType == KotlinFileType.INSTANCE }
for (ktFile in ktFiles) {
result.addFile(ktFile)
}
return result
}
private fun ImplicitPackageData.addFile(ktFile: VirtualFile) {
synchronized(this) {
val psiFile = PsiManager.getInstance(project).findFile(ktFile) as? KtFile ?: return
addPsiFile(psiFile, ktFile)
}
}
private fun ImplicitPackageData.addPsiFile(
psiFile: KtFile,
ktFile: VirtualFile
) = getOrPut(psiFile.packageFqName) { mutableListOf() }.add(ktFile)
private fun ImplicitPackageData.removeFile(file: VirtualFile) {
synchronized(this) {
for ((key, value) in this) {
if (value.remove(file)) {
if (value.isEmpty()) remove(key)
break
}
}
}
}
private fun ImplicitPackageData.updateFile(file: KtFile) {
synchronized(this) {
removeFile(file.virtualFile)
addPsiFile(file, file.virtualFile)
}
}
internal fun update(event: VFileEvent) {
when (event) {
is VFileCreateEvent -> checkNewFileInSourceRoot(event.file)
is VFileDeleteEvent -> checkDeletedFileInSourceRoot(event.file)
is VFileCopyEvent -> checkNewFileInSourceRoot(event.newParent.findChild(event.newChildName))
is VFileMoveEvent -> {
checkNewFileInSourceRoot(event.file)
if (event.oldParent.getSourceRoot(project) == event.oldParent) {
implicitPackageCache[event.oldParent]?.removeFile(event.file)
}
}
}
}
private fun checkNewFileInSourceRoot(file: VirtualFile?) {
if (file == null) return
if (file.getSourceRoot(project) == file.parent) {
implicitPackageCache[file.parent]?.addFile(file)
}
}
private fun checkDeletedFileInSourceRoot(file: VirtualFile?) {
val directory = file?.parent
if (directory == null || !directory.isValid) return
if (directory.getSourceRoot(project) == directory) {
implicitPackageCache[directory]?.removeFile(file)
}
}
internal fun update(ktFile: KtFile) {
val parent = ktFile.virtualFile.parent
if (ktFile.sourceRoot == parent) {
implicitPackageCache[parent]?.updateFile(ktFile)
}
}
}
class PerModulePackageCacheService(private val project: Project) {
/*
@@ -107,8 +199,9 @@ class PerModulePackageCacheService(private val project: Project) {
* Actually an StrongMap<Module, SoftMap<ModuleSourceInfo, SoftMap<FqName, Boolean>>>
*/
private val cache = ConcurrentHashMap<Module, ConcurrentMap<ModuleSourceInfo, ConcurrentMap<FqName, Boolean>>>()
private val implicitPackagePrefixCache = ImplicitPackagePrefixCache(project)
private val pendingVFileChanges: MutableSet<VirtualFile> = mutableSetOf()
private val pendingVFileChanges: MutableSet<VFileEvent> = mutableSetOf()
private val pendingKtFileChanges: MutableSet<KtFile> = mutableSetOf()
private val projectScope = GlobalSearchScope.projectScope(project)
@@ -117,9 +210,10 @@ class PerModulePackageCacheService(private val project: Project) {
pendingVFileChanges.clear()
pendingKtFileChanges.clear()
cache.clear()
implicitPackagePrefixCache.clear()
}
internal fun notifyPackageChange(file: VirtualFile): Unit = synchronized(this) {
internal fun notifyPackageChange(file: VFileEvent): Unit = synchronized(this) {
pendingVFileChanges += file
}
@@ -138,7 +232,8 @@ class PerModulePackageCacheService(private val project: Project) {
onTooComplexChange()
} else {
pendingVFileChanges.forEach { vfile ->
pendingVFileChanges.forEach { event ->
val vfile = event.file ?: return@forEach
// When VirtualFile !isValid (deleted for example), it impossible to use getModuleInfoByVirtualFile
// For directory we must check both is it in some sourceRoot, and is it contains some sourceRoot
if (vfile.isDirectory || !vfile.isValid) {
@@ -155,6 +250,7 @@ class PerModulePackageCacheService(private val project: Project) {
invalidateCacheForModuleSourceInfo(it)
}
}
implicitPackagePrefixCache.update(event)
}
pendingVFileChanges.clear()
@@ -163,6 +259,7 @@ class PerModulePackageCacheService(private val project: Project) {
return@forEach
}
(file.getNullableModuleInfo() as? ModuleSourceInfo)?.let { invalidateCacheForModuleSourceInfo(it) }
implicitPackagePrefixCache.update(file)
}
pendingKtFileChanges.clear()
}
@@ -189,7 +286,15 @@ class PerModulePackageCacheService(private val project: Project) {
}
}
fun getImplicitPackagePrefix(sourceRoot: VirtualFile): FqName {
checkPendingChanges()
return implicitPackagePrefixCache.getPrefix(sourceRoot)
}
companion object {
val FULL_DROP_THRESHOLD = 1000
const val FULL_DROP_THRESHOLD = 1000
fun getInstance(project: Project): PerModulePackageCacheService =
ServiceManager.getService(project, PerModulePackageCacheService::class.java)
}
}
@@ -63,7 +63,7 @@ class PluginDeclarationProviderFactory(
private fun stubBasedPackageExists(name: FqName): Boolean {
// We're only looking for source-based declarations
val moduleSourceInfo = moduleInfo as? ModuleSourceInfo ?: return false
return ServiceManager.getService(project, PerModulePackageCacheService::class.java).packageExists(name, moduleInfo)
return PerModulePackageCacheService.getInstance(project).packageExists(name, moduleInfo)
}
private fun getStubBasedPackageMemberDeclarationProvider(name: FqName): PackageMemberDeclarationProvider? {
@@ -1,17 +1,6 @@
/*
* 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.
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.util
@@ -148,5 +137,10 @@ object ProjectRootsUtil {
val Module.rootManager: ModuleRootManager
get() = ModuleRootManager.getInstance(this)
val Module.sourceRoots: Array<VirtualFile>
get() = rootManager.sourceRoots
val PsiElement.module: Module?
get() = ModuleUtilCore.findModuleForPsiElement(this)
fun VirtualFile.findModule(project: Project) = ModuleUtilCore.findModuleForFile(this, project)
@@ -1,17 +1,6 @@
/*
* 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.
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core
@@ -20,6 +9,8 @@ import com.intellij.psi.JavaDirectoryService
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiPackage
import org.jetbrains.kotlin.idea.caches.PerModulePackageCacheService
import org.jetbrains.kotlin.idea.util.sourceRoot
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
@@ -30,4 +21,16 @@ fun PsiFile.getFqNameByDirectory(): FqName {
return qualifiedNameByDirectory?.let(::FqName) ?: FqName.ROOT
}
fun KtFile.packageMatchesDirectory(): Boolean = packageFqName == getFqNameByDirectory()
fun PsiDirectory.getFqNameWithImplicitPrefix(): FqName {
val packageFqName = getPackage()?.qualifiedName?.let(::FqName) ?: FqName.ROOT
sourceRoot?.let {
val implicitPrefix = PerModulePackageCacheService.getInstance(project).getImplicitPackagePrefix(it)
return FqName.fromSegments((implicitPrefix.pathSegments() + packageFqName.pathSegments()).map { it.asString() })
}
return packageFqName
}
fun KtFile.packageMatchesDirectory(): Boolean = packageFqName == getFqNameByDirectory()
fun KtFile.packageMatchesDirectoryOrImplicit() =
packageFqName == getFqNameByDirectory() || packageFqName == parent?.getFqNameWithImplicitPrefix()
+1
View File
@@ -777,6 +777,7 @@
<annotationSupport language="kotlin" implementationClass="com.intellij.psi.impl.source.tree.java.JavaAnnotationSupport"/>
<createFromTemplateHandler implementation="org.jetbrains.kotlin.idea.actions.KotlinCreateFromTemplateHandler"/>
<defaultTemplatePropertiesProvider implementation="org.jetbrains.kotlin.idea.actions.KotlinDefaultTemplatePropertiesProvider" order="last"/>
<nameSuggestionProvider implementation="org.jetbrains.kotlin.idea.core.KotlinNameSuggestionProvider"/>
@@ -0,0 +1,23 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.actions
import com.intellij.ide.fileTemplates.DefaultTemplatePropertiesProvider
import com.intellij.ide.fileTemplates.FileTemplate
import com.intellij.psi.PsiDirectory
import org.jetbrains.kotlin.idea.core.getFqNameWithImplicitPrefix
import java.util.*
class KotlinDefaultTemplatePropertiesProvider : DefaultTemplatePropertiesProvider {
override fun fillProperties(directory: PsiDirectory?, props: Properties?) {
if (directory != null && props != null) {
props.setProperty(
FileTemplate.ATTRIBUTE_PACKAGE_NAME,
directory.getFqNameWithImplicitPrefix().asString()
)
}
}
}
@@ -1,17 +1,6 @@
/*
* 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.
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.refactoring.move.changePackage
@@ -28,7 +17,8 @@ import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackages
import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil
import com.intellij.refactoring.util.RefactoringMessageUtil
import org.jetbrains.kotlin.idea.core.getFqNameByDirectory
import org.jetbrains.kotlin.idea.core.packageMatchesDirectory
import org.jetbrains.kotlin.idea.core.getFqNameWithImplicitPrefix
import org.jetbrains.kotlin.idea.core.packageMatchesDirectoryOrImplicit
import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.idea.refactoring.hasIdentifiersOnly
import org.jetbrains.kotlin.idea.refactoring.isInjectedFragment
@@ -40,34 +30,38 @@ import org.jetbrains.kotlin.psi.KtVisitorVoid
class PackageDirectoryMismatchInspection : AbstractKotlinInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession) =
object : KtVisitorVoid() {
override fun visitPackageDirective(directive: KtPackageDirective) {
super.visitPackageDirective(directive)
if (directive.text.isEmpty()) return
object : KtVisitorVoid() {
override fun visitPackageDirective(directive: KtPackageDirective) {
super.visitPackageDirective(directive)
if (directive.text.isEmpty()) return
val file = directive.containingKtFile
if (file.isInjectedFragment || file.packageMatchesDirectory()) return
val file = directive.containingKtFile
if (file.isInjectedFragment || file.packageMatchesDirectoryOrImplicit()) return
val fixes = mutableListOf<LocalQuickFix>()
val qualifiedName = directive.qualifiedName
val dirName = if (qualifiedName.isEmpty()) "source root" else "'${qualifiedName.replace('.', '/')}'"
fixes += MoveFileToPackageFix(dirName)
val fqNameByDirectory = file.getFqNameByDirectory()
when {
fqNameByDirectory.isRoot ->
fixes += ChangePackageFix("source root")
fqNameByDirectory.hasIdentifiersOnly() ->
fixes += ChangePackageFix("'${fqNameByDirectory.asString()}'")
}
holder.registerProblem(
directive,
"Package directive doesn't match file location",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
*fixes.toTypedArray()
)
val fixes = mutableListOf<LocalQuickFix>()
val qualifiedName = directive.qualifiedName
val dirName = if (qualifiedName.isEmpty()) "source root" else "'${qualifiedName.replace('.', '/')}'"
fixes += MoveFileToPackageFix(dirName)
val fqNameByDirectory = file.getFqNameByDirectory()
when {
fqNameByDirectory.isRoot ->
fixes += ChangePackageFix("source root", fqNameByDirectory)
fqNameByDirectory.hasIdentifiersOnly() ->
fixes += ChangePackageFix("'${fqNameByDirectory.asString()}'", fqNameByDirectory)
}
val fqNameWithImplicitPrefix = file.parent?.getFqNameWithImplicitPrefix()
if (fqNameWithImplicitPrefix != null && fqNameWithImplicitPrefix != fqNameByDirectory) {
fixes += ChangePackageFix("'${fqNameWithImplicitPrefix.asString()}'", fqNameWithImplicitPrefix)
}
holder.registerProblem(
directive,
"Package directive doesn't match file location",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
*fixes.toTypedArray()
)
}
}
private class MoveFileToPackageFix(val dirName: String) : LocalQuickFix {
override fun getFamilyName() = "Move file to package-matching directory"
@@ -84,10 +78,9 @@ class PackageDirectoryMismatchInspection : AbstractKotlinInspection() {
val packageWrapper = PackageWrapper(PsiManager.getInstance(project), directive.qualifiedName)
val fileToMove = directive.containingFile
val chosenRoot =
sourceRoots.singleOrNull()
?: MoveClassesOrPackagesUtil.chooseSourceRoot(packageWrapper, sourceRoots,
fileToMove.containingDirectory)
?: return
sourceRoots.singleOrNull()
?: MoveClassesOrPackagesUtil.chooseSourceRoot(packageWrapper, sourceRoots, fileToMove.containingDirectory)
?: return
val targetDirFactory = AutocreatingSingleSourceRootMoveDestination(packageWrapper, chosenRoot)
targetDirFactory.verify(fileToMove)?.let {
Messages.showMessageDialog(project, it, CommonBundle.getErrorTitle(), Messages.getErrorIcon())
@@ -108,7 +101,7 @@ class PackageDirectoryMismatchInspection : AbstractKotlinInspection() {
}
}
private class ChangePackageFix(val packageName: String) : LocalQuickFix {
private class ChangePackageFix(val packageName: String, val packageFqName: FqName) : LocalQuickFix {
override fun getFamilyName() = "Change file's package to match directory"
override fun getName() = "Change file's package to $packageName"
@@ -116,10 +109,7 @@ class PackageDirectoryMismatchInspection : AbstractKotlinInspection() {
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val directive = descriptor.psiElement as? KtPackageDirective ?: return
val file = directive.containingKtFile
val newFqName = file.getFqNameByDirectory()
if (!newFqName.hasIdentifiersOnly()) return
KotlinChangePackageRefactoring(file).run(newFqName)
KotlinChangePackageRefactoring(file).run(packageFqName)
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.caches
import com.intellij.psi.PsiDocumentManager
import com.intellij.testFramework.LightProjectDescriptor
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.test.KotlinLightProjectDescriptor
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.sourceRoots
class ImplicitPackagePrefixTest : KotlinLightCodeInsightFixtureTestCase() {
private lateinit var cacheService: PerModulePackageCacheService
override fun getProjectDescriptor(): LightProjectDescriptor = KotlinLightProjectDescriptor.INSTANCE
override fun setUp() {
super.setUp()
cacheService = PerModulePackageCacheService.getInstance(myFixture.project)
}
private fun prefix(): String {
return cacheService.getImplicitPackagePrefix(myFixture.module.sourceRoots[0]).asString()
}
fun testSimple() {
myFixture.configureByText("foo.kt", "package com.example.foo")
assertEquals("com.example.foo", prefix())
}
fun testAmbiguous() {
myFixture.configureByText("foo.kt", "package com.example.foo")
myFixture.configureByText("bar.kt", "package com.example.bar")
assertEquals("", prefix())
}
fun testUpdateOnCreate() {
myFixture.configureByText("foo.kt", "package com.example.foo")
assertEquals("com.example.foo", prefix())
myFixture.configureByText("bar.kt", "package com.example.bar")
assertEquals("", prefix())
}
fun testUpdateOnDelete() {
myFixture.configureByText("foo.kt", "package com.example.foo")
val bar = myFixture.configureByText("bar.kt", "package com.example.bar")
assertEquals("", prefix())
myFixture.project.executeWriteCommand("") {
bar.delete()
}
assertEquals("com.example.foo", prefix())
}
fun testUpdateOnChangeContent() {
val foo = myFixture.configureByText("foo.kt", "package com.example.foo")
assertEquals("com.example.foo", prefix())
myFixture.project.executeWriteCommand("") {
foo.viewProvider.document!!.let {
it.replaceString(0, it.textLength, "package com.example.bar")
PsiDocumentManager.getInstance(project).commitDocument(it)
}
}
assertEquals("com.example.bar", prefix())
}
}