From daf357bb115f2cd2c28be4148e3cf70eeb53d6b5 Mon Sep 17 00:00:00 2001 From: Oliver Nybroe Date: Mon, 30 Nov 2020 08:38:42 +0100 Subject: [PATCH] Add support for autocompletion after higher order methods and properties --- CHANGELOG.md | 21 +----- gradle.properties | 2 +- src/main/kotlin/dev/nybroe/collector/Util.kt | 19 ++++- .../types/HigherOrderTypeProvider.kt | 72 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 4 ++ .../nybroe/collector/BaseCollectTestCase.kt | 13 ++++ .../kotlin/dev/nybroe/collector/UtilTest.kt | 29 ++++++++ .../collector/inspections/InspectionTest.kt | 11 +-- .../types/HigherOrderTypeProviderTest.kt | 29 ++++++++ src/test/resources/stubs.php | 13 ++++ .../higherOrderMethod.php | 11 +++ .../higherOrderProperty.php | 5 ++ 12 files changed, 199 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/dev/nybroe/collector/types/HigherOrderTypeProvider.kt create mode 100644 src/test/kotlin/dev/nybroe/collector/BaseCollectTestCase.kt create mode 100644 src/test/kotlin/dev/nybroe/collector/UtilTest.kt create mode 100644 src/test/kotlin/dev/nybroe/collector/types/HigherOrderTypeProviderTest.kt create mode 100644 src/test/resources/types/HigherOrderMethodsTypeProvider/higherOrderMethod.php create mode 100644 src/test/resources/types/HigherOrderMethodsTypeProvider/higherOrderProperty.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a39257a..9f19218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## [Unreleased] ### Added +- Type provider for higher order collection methods and properties ### Changed @@ -15,31 +16,13 @@ ### Security ## [0.1.0] -### Added - ### Changed +- Tagged first stable release. -### Deprecated - -### Removed - -### Fixed - -### Security ## [0.0.1-EAP.13] -### Added - -### Changed - -### Deprecated - -### Removed - ### Fixed - Fixed type detection recognizing `mixed` as a collection. -### Security - ## [0.0.1-EAP.12] ### Fixed - Fixed closure to arrow function not working on collections variables diff --git a/gradle.properties b/gradle.properties index c913274..079795e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ pluginGroup = dev.nybroe.collector pluginName_ = Collector -pluginVersion = 0.1.0 +pluginVersion = 0.2.0.EAP.1 pluginSinceBuild = 202 pluginUntilBuild = diff --git a/src/main/kotlin/dev/nybroe/collector/Util.kt b/src/main/kotlin/dev/nybroe/collector/Util.kt index ae144aa..d09dc24 100644 --- a/src/main/kotlin/dev/nybroe/collector/Util.kt +++ b/src/main/kotlin/dev/nybroe/collector/Util.kt @@ -19,10 +19,14 @@ private val collectionClasses = listOf( "\\Illuminate\\Support\\Traits\\EnumeratesValues", ) -private val collectionType = PhpType().apply { +val collectionType = PhpType().apply { collectionClasses.forEach { this.add(it) } } +private val higherOrderCollectionType = PhpType().apply { + this.add("\\Illuminate\\Support\\HigherOrderCollectionProxy") +} + val Method.isCollectionMethod: Boolean get() = this.containingClass?.type?.isCollection(this.project) ?: false @@ -47,3 +51,16 @@ fun PhpType.isCollection(project: Project): Boolean { PhpIndex.getInstance(project) ) } + +fun PhpType.isHigherOrderCollection(project: Project): Boolean { + val filteredType = this.filterMixed() + + if (filteredType.isEmpty) { + return false + } + + return higherOrderCollectionType.isConvertibleFrom( + filteredType, + PhpIndex.getInstance(project) + ) +} diff --git a/src/main/kotlin/dev/nybroe/collector/types/HigherOrderTypeProvider.kt b/src/main/kotlin/dev/nybroe/collector/types/HigherOrderTypeProvider.kt new file mode 100644 index 0000000..0ea67bf --- /dev/null +++ b/src/main/kotlin/dev/nybroe/collector/types/HigherOrderTypeProvider.kt @@ -0,0 +1,72 @@ +package dev.nybroe.collector.types + +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.jetbrains.php.PhpIndex +import com.jetbrains.php.lang.psi.elements.FieldReference +import com.jetbrains.php.lang.psi.elements.MethodReference +import com.jetbrains.php.lang.psi.elements.PhpNamedElement +import com.jetbrains.php.lang.psi.resolve.types.PhpType +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider4 +import dev.nybroe.collector.collectionType +import dev.nybroe.collector.isHigherOrderCollection +import gnu.trove.THashSet + +class HigherOrderTypeProvider : PhpTypeProvider4 { + override fun getKey(): Char { + return '\u0171' + } + + override fun getType(psiElement: PsiElement): PhpType? { + if (DumbService.isDumb(psiElement.project)) return null + + // Check that our current element is a field or method reference + // $collection->map->data, $collection->map->data() + val fieldOrMethodReference = psiElement as? FieldReference + ?: psiElement as? MethodReference + ?: return null + + // Check that parent is a field reference. + // $collection->map + val parentField = fieldOrMethodReference.classReference as? FieldReference ?: return null + + return PhpType().add("#${this.key}${parentField.type}") + } + + override fun complete(s: String, project: Project): PhpType? { + return null + } + + /** + * Here you can extend the signature lookups + * @param expression Signature expression to decode. use PhpIndex.getBySignature() to look up expression internals. + * @param visited Recursion guard: Pass this on into any phpIndex calls having same parameter + * @param depth Recursion guard: Pass this on into any phpIndex calls having same parameter + * @param project well so you can reach the PhpIndex + * @return null if no match + */ + override fun getBySignature( + expression: String, + visited: MutableSet, + depth: Int, + project: Project + ): MutableCollection? { + // Decode the expression into a php type + val type = PhpIndex.getInstance(project).completeType( + project, + PhpType().apply { + expression.split('|').filter { it.length != 1 }.forEach { it -> + this.add(it) + } + }, + null + ) + + if (!type.isHigherOrderCollection(project)) return null + + return collectionType.types + .flatMap { PhpIndex.getInstance(project).getClassesByFQN(it.toString()) } + .toCollection(THashSet()) + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 6dcb0d4..bbc45c6 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -78,6 +78,10 @@ + + + + diff --git a/src/test/kotlin/dev/nybroe/collector/BaseCollectTestCase.kt b/src/test/kotlin/dev/nybroe/collector/BaseCollectTestCase.kt new file mode 100644 index 0000000..4edf78e --- /dev/null +++ b/src/test/kotlin/dev/nybroe/collector/BaseCollectTestCase.kt @@ -0,0 +1,13 @@ +package dev.nybroe.collector + +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +@Suppress("UnnecessaryAbstractClass") +internal abstract class BaseCollectTestCase : BasePlatformTestCase() { + override fun getTestDataPath(): String = "src/test/resources" + + override fun setUp() { + super.setUp() + myFixture.copyFileToProject("stubs.php") + } +} diff --git a/src/test/kotlin/dev/nybroe/collector/UtilTest.kt b/src/test/kotlin/dev/nybroe/collector/UtilTest.kt new file mode 100644 index 0000000..45505ab --- /dev/null +++ b/src/test/kotlin/dev/nybroe/collector/UtilTest.kt @@ -0,0 +1,29 @@ +package dev.nybroe.collector + +import com.jetbrains.php.lang.psi.resolve.types.PhpType + +internal class UtilTest : BaseCollectTestCase() { + fun testIsCollection() { + assertTrue( + PhpType().add( + "\\Illuminate\\Support\\Collection" + ).isCollection(project) + ) + } + + fun testMixedIsNotCollection() { + assertFalse(PhpType.MIXED.isCollection(project)) + } + + fun testIterableIsNotCollection() { + assertFalse(PhpType.ITERABLE.isCollection(project)) + } + + fun testArrayIsNotCollection() { + assertFalse(PhpType.ARRAY.isCollection(project)) + } + + fun testEmptyIsNotCollection() { + assertFalse(PhpType.EMPTY.isCollection(project)) + } +} diff --git a/src/test/kotlin/dev/nybroe/collector/inspections/InspectionTest.kt b/src/test/kotlin/dev/nybroe/collector/inspections/InspectionTest.kt index 447bcd4..8393c7c 100644 --- a/src/test/kotlin/dev/nybroe/collector/inspections/InspectionTest.kt +++ b/src/test/kotlin/dev/nybroe/collector/inspections/InspectionTest.kt @@ -1,19 +1,12 @@ package dev.nybroe.collector.inspections import com.intellij.codeInspection.InspectionProfileEntry -import com.intellij.testFramework.fixtures.BasePlatformTestCase +import dev.nybroe.collector.BaseCollectTestCase -internal abstract class InspectionTest : BasePlatformTestCase() { +internal abstract class InspectionTest : BaseCollectTestCase() { protected abstract fun defaultInspection(): InspectionProfileEntry protected abstract fun defaultAction(): String - override fun getTestDataPath(): String = "src/test/resources" - - override fun setUp() { - super.setUp() - myFixture.copyFileToProject("stubs.php") - } - private fun defaultInspectionPath(): String { return "inspections/${defaultInspection()::class.simpleName}" } diff --git a/src/test/kotlin/dev/nybroe/collector/types/HigherOrderTypeProviderTest.kt b/src/test/kotlin/dev/nybroe/collector/types/HigherOrderTypeProviderTest.kt new file mode 100644 index 0000000..6f3d3b3 --- /dev/null +++ b/src/test/kotlin/dev/nybroe/collector/types/HigherOrderTypeProviderTest.kt @@ -0,0 +1,29 @@ +package dev.nybroe.collector.types + +import dev.nybroe.collector.BaseCollectTestCase + +internal class HigherOrderTypeProviderTest : BaseCollectTestCase() { + fun testHigherOrderPropertyReturnsCollection() { + myFixture.configureByFile( + "types/HigherOrderMethodsTypeProvider/higherOrderProperty.php" + ) + + assertCompletion("map", "each") + } + + fun testHigherOrderMethodReturnsCollection() { + myFixture.configureByFile( + "types/HigherOrderMethodsTypeProvider/higherOrderMethod.php" + ) + + assertCompletion("map", "each") + } + + private fun assertCompletion(vararg shouldContain: String) { + myFixture.completeBasic() + + val strings = myFixture.lookupElementStrings ?: return fail("empty completion result") + + assertContainsElements(strings, shouldContain.asList()) + } +} diff --git a/src/test/resources/stubs.php b/src/test/resources/stubs.php index ce7b6c1..195703c 100644 --- a/src/test/resources/stubs.php +++ b/src/test/resources/stubs.php @@ -1,6 +1,12 @@ map->data()-> diff --git a/src/test/resources/types/HigherOrderMethodsTypeProvider/higherOrderProperty.php b/src/test/resources/types/HigherOrderMethodsTypeProvider/higherOrderProperty.php new file mode 100644 index 0000000..31dd261 --- /dev/null +++ b/src/test/resources/types/HigherOrderMethodsTypeProvider/higherOrderProperty.php @@ -0,0 +1,5 @@ + 'works'] +])->map->data->