Skip to content

Commit

Permalink
Add support for autocompletion after higher order methods and properties
Browse files Browse the repository at this point in the history
  • Loading branch information
olivernybroe committed Nov 30, 2020
1 parent 22c8086 commit daf357b
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 30 deletions.
21 changes: 2 additions & 19 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## [Unreleased]
### Added
- Type provider for higher order collection methods and properties

### Changed

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

pluginGroup = dev.nybroe.collector
pluginName_ = Collector
pluginVersion = 0.1.0
pluginVersion = 0.2.0.EAP.1
pluginSinceBuild = 202
pluginUntilBuild =

Expand Down
19 changes: 18 additions & 1 deletion src/main/kotlin/dev/nybroe/collector/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
)
}
Original file line number Diff line number Diff line change
@@ -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<String>,
depth: Int,
project: Project
): MutableCollection<out PhpNamedElement>? {
// 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())
}
}
4 changes: 4 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
<projectService serviceImplementation="dev.nybroe.collector.services.ChecksInspectionsEnabledService"/>
</extensions>

<extensions defaultExtensionNs="com.jetbrains.php">
<typeProvider4 implementation="dev.nybroe.collector.types.HigherOrderTypeProvider"/>
</extensions>

<applicationListeners>
<listener class="dev.nybroe.collector.listeners.ChecksInspectionsEnabledProjectManagerListener"
topic="com.intellij.openapi.project.ProjectManagerListener"/>
Expand Down
13 changes: 13 additions & 0 deletions src/test/kotlin/dev/nybroe/collector/BaseCollectTestCase.kt
Original file line number Diff line number Diff line change
@@ -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")
}
}
29 changes: 29 additions & 0 deletions src/test/kotlin/dev/nybroe/collector/UtilTest.kt
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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}"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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())
}
}
13 changes: 13 additions & 0 deletions src/test/resources/stubs.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<?php

namespace Illuminate\Support {
/**
* Class Collection
*
* @package Illuminate\Support
* @property-read HigherOrderCollectionProxy $map
*/
class Collection {
/**
* Create a new collection.
Expand Down Expand Up @@ -92,6 +98,13 @@ public function where($key, $operator, $value = null)
{
}
}

/**
* @mixin \Illuminate\Support\Enumerable
*/
class HigherOrderCollectionProxy
{
}
}

namespace Illuminate\Database\Eloquent {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

class HigherOrderMethod {
public function data() {
return 'works';
}
}

collect([
new HigherOrderMethod()
])->map->data()-><caret>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

collect([
['data' => 'works']
])->map->data-><caret>

0 comments on commit daf357b

Please sign in to comment.