Skip to content

Commit

Permalink
Merge pull request #27 from olivernybroe/closure-to-arrow
Browse files Browse the repository at this point in the history
Closure to arrow function
  • Loading branch information
olivernybroe authored Nov 8, 2020
2 parents ee2f40f + 9acb754 commit 68364fd
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 18 deletions.
8 changes: 1 addition & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,11 @@

## [Unreleased]
### Added

### Changed

### Deprecated

### Removed
- Added closure to arrow function inside collection

### Fixed
- Fixed foreach refactoring. `use` statement contains empty variable when expression inside string interpolation.

### Security
## [0.0.1-EAP.6]
### Fixed
- Fixed foreach refactoring. `use` statement missing array variable when used in foreach.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ However, if you would like to enforce yourself to never use `foreach`, you can c
### Remove nested collections
![recursive-collection-example](https://github.com/olivernybroe/collector-intellij/blob/main/art/usage/recursiveCollection.gif)

## Closure to arrow functions
![closure-to-arrow-example](https://github.com/olivernybroe/collector-intellij/blob/main/art/usage/closureToArrow.gif)


## Credits

- [Oliver Nybroe](https://github.com/olivernybroe)
Expand Down
Binary file added art/usage/closureToArrow.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 22 additions & 10 deletions src/main/kotlin/dev/nybroe/collector/Util.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
package dev.nybroe.collector

import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.MethodReference
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.jetbrains.php.lang.psi.elements.PhpReference
import com.jetbrains.php.lang.psi.elements.impl.FunctionImpl
import com.jetbrains.php.lang.psi.resolve.types.PhpType

fun FunctionReference.isGlobalFunctionCallWithName(name: String): Boolean {
return this.name == name
}

fun PhpClass.isCollectionClass(): Boolean {
return this.fqn == "\\Illuminate\\Support\\Collection"
}
val PhpClass.isCollectionClass: Boolean
get() = this.fqn == "\\Illuminate\\Support\\Collection"

fun Method.isCollectionMethod(): Boolean {
return this.containingClass?.isCollectionClass() ?: false
}
val Method.isCollectionMethod: Boolean
get() = this.containingClass?.isCollectionClass ?: false

fun MethodReference.isCollectionMethod(): Boolean {
return (this.resolve() as? Method)?.isCollectionMethod() ?: false
}
val MethodReference.isCollectionMethod: Boolean
get() = (this.resolve() as? Method)?.isCollectionMethod ?: false

val Function.returnsCollection: Boolean
get() = this.type.global(this.project).isCollection

val FunctionReference.returnsCollection: Boolean
get() = (this.resolve() as? Function)?.returnsCollection ?: false

val PhpReference.isCollectionType: Boolean
get() = this.type.global(this.project).types.contains("\\Illuminate\\Support\\Collection")
get() = this.type.global(this.project).isCollection

val Function.isShortArrowFunction: Boolean
get() = FunctionImpl.isShortArrowFunction(this)

val PhpType.isCollection: Boolean
get() = this.types.contains("\\Illuminate\\Support\\Collection")
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package dev.nybroe.collector.inspections

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.elementType
import com.jetbrains.php.config.PhpLanguageFeature
import com.jetbrains.php.lang.inspections.PhpInspection
import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.FunctionReference
import com.jetbrains.php.lang.psi.elements.GroupStatement
import com.jetbrains.php.lang.psi.elements.ParameterList
import com.jetbrains.php.lang.psi.elements.PhpUseList
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor
import dev.nybroe.collector.MyBundle
import dev.nybroe.collector.isShortArrowFunction
import dev.nybroe.collector.quickFixes.ClosureToArrowFunctionQuickFix
import dev.nybroe.collector.returnsCollection

class ClosureToArrowFunctionInspection : PhpInspection() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return object : PhpElementVisitor() {
override fun visitPhpFunction(closure: Function) {
// Check that the PHP version supports arrow functions
if (!PhpLanguageFeature.ARROW_FUNCTION_SYNTAX.isSupported(closure.project)) {
return
}

// Make sure we are in a closure
if (!closure.isClosure) {
return
}

// And not already using arrow functions
if (closure.isShortArrowFunction) {
return
}

// And no use arguments are by reference
if (useByReferenceExists(closure)) {
return
}

// And closure is inside a collection.
val methodReference = PhpPsiUtil
.getParentByCondition<ParameterList>(closure, ParameterList.INSTANCEOF)?.parent ?: return
val functionReference = PhpPsiUtil
.getChildByCondition<FunctionReference>(methodReference, FunctionReference.INSTANCEOF) ?: return
if (!functionReference.returnsCollection) {
return
}

// Get the closure body and make sure the size is 1
val body = PhpPsiUtil.getChildByCondition<GroupStatement>(closure, GroupStatement.INSTANCEOF) ?: return

if (body.statements.size != 1) {
return
}

// And cannot be done on echo call
if (body.statements[0].firstChild.elementType === PhpTokenTypes.kwECHO) {
return
}

holder.registerProblem(
closure,
MyBundle.message("closureToArrowFunctionDescription"),
ProblemHighlightType.WEAK_WARNING,
ClosureToArrowFunctionQuickFix()
)
}
}
}

private fun useByReferenceExists(closure: Function): Boolean {
val useList = PhpPsiUtil
.getChildByCondition<PsiElement>(closure, PhpUseList.INSTANCEOF) as PhpUseList? ?: return false

return useList.children
.any { it.prevSibling.elementType === PhpTokenTypes.opBIT_AND }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class MapFlattenToFlatMapInspection : PhpInspection() {
return object : PhpElementVisitor() {
override fun visitPhpMethodReference(reference: MethodReference) {
// Check that method reference is collection method.
if (!reference.isCollectionMethod()) {
if (!reference.isCollectionMethod) {
return
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.nybroe.collector.quickFixes

import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.util.elementType
import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.psi.PhpPsiElementFactory
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.GroupStatement
import com.jetbrains.php.lang.psi.elements.PhpUseList

class ClosureToArrowFunctionQuickFix : LocalQuickFix {
companion object {
const val QUICK_FIX_NAME = "Closure can be converted to arrow function"
}

override fun getFamilyName(): String {
return QUICK_FIX_NAME
}

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val closure = descriptor.psiElement as? Function ?: return

val body = PhpPsiUtil.getChildByCondition<GroupStatement>(closure, GroupStatement.INSTANCEOF) ?: return

// Replace `function` with `fn`
val functionKeyword = closure.node.findChildByType(PhpTokenTypes.kwFUNCTION)?.psi ?: return
functionKeyword.replace(PhpPsiElementFactory.createFromText(project, PhpTokenTypes.kwFN, "fn()"))

// Replace body with dummy arrow function
val arrow = body.replace(PhpPsiElementFactory.createFromText(project, PhpTokenTypes.opHASH_ARRAY, "fn() => 1"))

// Add body
arrow.parent.addAfter(copyBody(body), arrow)

// Delete use list
PhpPsiUtil.getChildByCondition<PhpUseList>(closure, PhpUseList.INSTANCEOF)?.delete()
}

private fun copyBody(body: GroupStatement): PsiElement {
val statement = body.statements[0].copy()

// Delete semi colon
if (statement.lastChild.elementType === PhpTokenTypes.opSEMICOLON) {
statement.lastChild.delete()
}

// Delete return keyword if exist
if (statement.firstChild.elementType === PhpTokenTypes.kwRETURN) {
statement.firstChild.delete()
}

return statement
}
}
11 changes: 11 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,16 @@
key="collectFunctionInCollectionInspectionDisplayName"
implementationClass="dev.nybroe.collector.inspections.CollectFunctionInCollectionInspection"
/>

<localInspection
language="PHP"
groupPath="PHP"
groupKey="collectionGroupName"
shortName="ClosureToArrowFunctionInspection"
enabledByDefault="true"
bundle="messages.MyBundle"
key="closureToArrowFunctionDisplayName"
implementationClass="dev.nybroe.collector.inspections.ClosureToArrowFunctionInspection"
/>
</extensions>
</idea-plugin>
3 changes: 3 additions & 0 deletions src/main/resources/messages/MyBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ arrayMapToCollectionDescription='array_map' used instead of 'Collection'
mapFlattenToFlatMapDisplayName='map()->flatten()' used instead of 'flatmap()'
mapFlattenToFlatMapDescription='map()->flatten()' used instead of 'flatmap()'

closureToArrowFunctionDisplayName=Closure can be converted to arrow function
closureToArrowFunctionDescription=Closure can be converted to arrow function

collectFunctionInCollectionInspectionDisplayName=Recursive call on collection
collectFunctionInCollectionInspectionDescription=Recursive call on collection
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dev.nybroe.collector.inspections

import com.intellij.codeInspection.InspectionProfileEntry
import com.jetbrains.php.config.PhpLanguageLevel
import com.jetbrains.php.config.PhpProjectConfigurationFacade
import dev.nybroe.collector.quickFixes.ClosureToArrowFunctionQuickFix

internal class ClosureToArrowFunctionInspectionTest : InspectionTest() {
override fun defaultInspection(): InspectionProfileEntry = ClosureToArrowFunctionInspection()
override fun defaultAction(): String = ClosureToArrowFunctionQuickFix.QUICK_FIX_NAME

fun testEachFunctionCallToArrow() {
PhpProjectConfigurationFacade.getInstance(project).languageLevel = PhpLanguageLevel.PHP740
doTest("each-function-call-to-arrow-function")
}

fun testEachFunctionCallToArrowWithUses() {
PhpProjectConfigurationFacade.getInstance(project).languageLevel = PhpLanguageLevel.PHP740
doTest("each-function-call-to-arrow-function-with-uses")
}

fun testEachFunctionCallToArrowNotOnPHP730() {
PhpProjectConfigurationFacade.getInstance(project).languageLevel = PhpLanguageLevel.PHP730
doNotMatchTest("each-function-call-to-arrow-function-php730")
}

fun testEachFunctionCallToArrowWithReturn() {
PhpProjectConfigurationFacade.getInstance(project).languageLevel = PhpLanguageLevel.PHP740
doTest("each-function-call-to-arrow-function-with-return")
}

fun testEachFunctionCallToArrowNotOnUsesByReference() {
PhpProjectConfigurationFacade.getInstance(project).languageLevel = PhpLanguageLevel.PHP740
doNotMatchTest("each-function-call-to-arrow-function-with-uses-by-reference")
}

fun testEachFunctionCallToArrowNotOnEchoCall() {
PhpProjectConfigurationFacade.getInstance(project).languageLevel = PhpLanguageLevel.PHP740
doNotMatchTest("each-function-call-to-arrow-function-echo")
}

fun testFunctionCallToArrowFunctionNotOnNonCollectionFunctions() {
PhpProjectConfigurationFacade.getInstance(project).languageLevel = PhpLanguageLevel.PHP740
doNotMatchTest("function-call-to-arrow-function-not-in-collection")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

$array = [
'one',
'two',
'tree'
];

collect($array)->each(fn($item) => doAction($item));
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

$array = [
'one',
'two',
'tree'
];

collect($array)->each(<weak_warning descr="Closure can be converted to arrow function">function ($item) {
return doAction($item);
}</weak_warning>);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

$array = [
'one',
'two',
'tree'
];

$name = 'one';

collect($array)->each(fn($item) => doAction($item, $name));
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

$array = [
'one',
'two',
'tree'
];

$name = 'one';

collect($array)->each(<weak_warning descr="Closure can be converted to arrow function">function ($item) use ($name) {
doAction($item, $name);
}</weak_warning>);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

$array = [
'one',
'two',
'tree'
];

collect($array)->each(fn($item) => doAction($item));
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

$array = [
'one',
'two',
'tree'
];

collect($array)->each(<weak_warning descr="Closure can be converted to arrow function">function ($item) {
doAction($item);
}</weak_warning>);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

$array = [
'one',
'two',
'tree'
];

collect($array)->each(function ($item) {
echo doAction($item);
});
Loading

0 comments on commit 68364fd

Please sign in to comment.