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:
+127
-22
@@ -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)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
-47
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user