Skip to content

Commit 65b25dd

Browse files
authored
Add new action: Use named constructor (#180)
* Add new action: Use named constructor * Fix readme * Update intellij plugin * Use text representation to check method signature
1 parent 95e8c85 commit 65b25dd

File tree

10 files changed

+259
-46
lines changed

10 files changed

+259
-46
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<!-- Keep a Changelog guide -> https://keepachangelog.com -->
22

33
# PhpClean Changelog
4-
## [2023.02.30]
4+
## [2023.03.01]
55
### Fixed
66
- #162 Skip "use assert" qf before variable declaration
7+
### Added
8+
- Introduce new action - use named constructor
79

810
## [2022.08.30]
911
### Changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ Hit `install` button.
1414

1515
![GitHub commits (since latest release)](https://img.shields.io/github/commits-since/funivan/PhpClean/latest.svg?style=flat-square)
1616

17+
[- Inspections](#Inspections)
1718

19+
[- Actions](#Actions)
1820

19-
## List of inspection:
20-
<!-- inspections -->
21+
22+
<!-- Autogenerated -->
23+
## Inspections
2124
#### AssignMisused
2225
Detects assignment and comparison operators in one statement.
2326
```php
@@ -137,3 +140,22 @@ Use assert to check variable type instead of doc comment.
137140
/** @var User $user */ // <-- Use assert to check variable type
138141
assert($user instanceof User);
139142
```
143+
144+
## Actions
145+
#### UseNamedConstructor
146+
Replace `new ClassName()` with selected named constructor.
147+
148+
```php
149+
class Text {
150+
public function __construct(string $name){ }
151+
public static fromName(string $n){}
152+
}
153+
```
154+
Invoke `refactor this` on method name `fromName`
155+
and all new statements with this class will be changed
156+
157+
```php
158+
new Text('User') // old code
159+
Text::fromName('User') // new code
160+
```
161+

build.gradle.kts

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
21
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
32

43
plugins {
54
id("java")
65
id("org.jetbrains.kotlin.jvm") version "1.7.10"
7-
id("org.jetbrains.intellij") version "1.13.0"
6+
id("org.jetbrains.intellij") version "1.13.3"
87
id("org.jetbrains.changelog") version "2.0.0"
98
// ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
109
// id("org.jlleitschuh.gradle.ktlint") version "10.1.0"
@@ -58,14 +57,15 @@ changelog {
5857
version.set(pluginVersion)
5958
headerParserRegex.set("""(\d+\.\d+\.\d+)""".toRegex())
6059
}
61-
6260
tasks {
63-
withType<JavaCompile> {
64-
sourceCompatibility = "11"
65-
targetCompatibility = "11"
66-
}
67-
withType<KotlinCompile> {
68-
kotlinOptions.jvmTarget = "11"
61+
properties("javaVersion").let {
62+
withType<JavaCompile> {
63+
sourceCompatibility = it
64+
targetCompatibility = it
65+
}
66+
withType<KotlinCompile> {
67+
kotlinOptions.jvmTarget = it
68+
}
6969
}
7070

7171
publishPlugin {
@@ -89,10 +89,10 @@ tasks {
8989

9090
register("copyInspections") {
9191
doLast {
92-
blocks().forEach {
92+
inspections().forEach {
9393
write(
9494
File("src/main/resources/inspectionDescriptions/" + it.file().name),
95-
it.full()
95+
it.content()
9696
)
9797
}
9898
}
@@ -113,7 +113,7 @@ tasks {
113113
}
114114
}
115115

116-
named("test"){
116+
named("test") {
117117
dependsOn("checkReadme")
118118
}
119119
named("buildPlugin") {
@@ -138,34 +138,48 @@ fun write(file: File, content: String): Boolean {
138138
return result
139139
}
140140

141-
class Block(private val file: File) {
141+
class Descriptor(
142+
private val file: File,
143+
private val uid: String
144+
) {
145+
fun uid() = uid
142146
fun file() = file
143-
fun uid() = file.name.replace("Inspection.html", "")
144-
fun full() = file.readText()
145-
fun short() = full()
147+
fun content() = file.readText()
148+
fun short() = content()
146149
.replace(Regex("<!-- main -->(.*)", RegexOption.DOT_MATCHES_ALL), "")
147150
.trim()
148151
}
149152

150-
fun blocks() = File("src/main/kotlin/com/funivan/idea/phpClean/inspections")
151-
.walkTopDown()
152-
.filter { it.name.contains("Inspection.kt") }
153-
.map { Block(File(it.path.replace(".kt", ".html"))) }
154-
155-
fun generatedReadmeContent(readme: File): String {
156-
var content = readme.readText()
157-
content = content.replace(
158-
Regex("(<!-- inspections -->)(.+)", RegexOption.DOT_MATCHES_ALL),
159-
"$1"
160-
)
161-
content = content + "\n" + blocks().sortedBy { it.uid() }
162-
.map {
163-
val description = it.short().replace("<pre>", "```php").replace("</pre>", "```")
164-
"#### ${it.uid()}\n$description\n"
165-
}
166-
.joinToString("")
167-
return content
168-
}
153+
fun actions() = projectHtmlFiles("Action")
154+
fun inspections() = projectHtmlFiles("Inspection")
155+
156+
157+
fun generatedReadmeContent(readme: File): String =
158+
readme.readText().replace(
159+
Regex("<!-- Autogenerated -->.+", RegexOption.DOT_MATCHES_ALL), ""
160+
) + "<!-- Autogenerated -->\n" + generateSections()
169161

170162
fun readmeFile() = File("README.md")
163+
fun projectHtmlFiles(type: String) = File("src/main/kotlin/com/funivan/idea/phpClean")
164+
.walkTopDown()
165+
.filter { it.name.contains("${type}.html") }
166+
.map {
167+
Descriptor(
168+
File(it.path),
169+
it.name.replace("${type}.html", "")
170+
)
171+
}
171172

173+
fun generateSections() = listOf(
174+
Pair("Inspections", inspections()),
175+
Pair("Actions", actions()),
176+
).fold("") { acc, pair ->
177+
acc + "## ${pair.first}\n" +
178+
pair.second.sortedBy { it.uid() }
179+
.map {
180+
val description = it.short().replace("<pre>", "```php").replace("</pre>", "```")
181+
"#### ${it.uid()}\n$description\n"
182+
}
183+
.joinToString("") +
184+
"\n"
185+
}

gradle.properties

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
pluginGroup = com.funivan.idea.phpClean
22
pluginName_ = PhpClean
33
name = PhpClean
4-
pluginVersion = 2022.08.30
5-
pluginSinceBuild = 211
4+
pluginVersion = 2023.03.01
5+
pluginSinceBuild = 221.5080.224
66
#pluginUntilBuild = 203.*
77
# Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl
88
# See https://jb.gg/intellij-platform-builds-list for available build versions
9-
pluginVerifierIdeVersions = 2020.3.2, 2021.1, 2022.1.4
9+
pluginVerifierIdeVersions = 2022.3.2
1010
platformType = IU
11-
platformVersion = 2022.1.4
11+
platformVersion = 2022.2.3
1212
platformDownloadSources = true
1313
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
14-
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
15-
platformPlugins = com.intellij.java, com.jetbrains.php:221.6008.13, PsiViewer:221-SNAPSHOT
16-
14+
platformPlugins = com.intellij.java, com.jetbrains.php:222.4345.15
15+
javaVersion = 17
1716
# Opt-out flag for bundling Kotlin standard library.
1817
# See https://kotlinlang.org/docs/reference/using-gradle.html#dependency-on-the-standard-library for details.
1918
kotlin.stdlib.default.dependency = false
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.funivan.idea.phpClean.actions.useNamedConstructor
2+
3+
import com.intellij.usageView.UsageViewBundle
4+
import com.intellij.usageView.UsageViewDescriptor
5+
import com.jetbrains.php.lang.psi.elements.Method
6+
7+
class NamedConstructorUsageViewDescriptor(
8+
val constructor: Method,
9+
val target: Method
10+
) : UsageViewDescriptor {
11+
override fun getElements() =
12+
arrayOf(constructor)
13+
14+
override fun getProcessedElementsHeader() =
15+
"Use named constructor: " + target.name
16+
17+
override fun getCodeReferencesText(usagesCount: Int, filesCount: Int) =
18+
UsageViewBundle.getReferencesString(usagesCount, filesCount)
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Replace `new ClassName()` with selected named constructor.
2+
3+
<pre>
4+
class Text {
5+
public function __construct(string $name){ }
6+
public static fromName(string $n){}
7+
}
8+
</pre>
9+
Invoke `refactor this` on method name `fromName`
10+
and all new statements with this class will be changed
11+
12+
<pre>
13+
new Text('User') // old code
14+
Text::fromName('User') // new code
15+
</pre>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.funivan.idea.phpClean.actions.useNamedConstructor
2+
3+
import com.intellij.lang.Language
4+
import com.intellij.openapi.actionSystem.DataContext
5+
import com.intellij.openapi.editor.Editor
6+
import com.intellij.psi.PsiElement
7+
import com.intellij.psi.PsiFile
8+
import com.intellij.refactoring.RefactoringActionHandler
9+
import com.intellij.refactoring.actions.BaseRefactoringAction
10+
import com.jetbrains.php.lang.PhpLanguage
11+
import com.jetbrains.php.lang.psi.elements.Method
12+
13+
class UseNamedConstructorAction : BaseRefactoringAction() {
14+
15+
override fun isAvailableInEditorOnly() = false
16+
17+
override fun isEnabledOnElements(elements: Array<out PsiElement>): Boolean {
18+
return elements.map(::isNamedConstructorCandidate).all { it == false }
19+
}
20+
21+
override fun isAvailableOnElementInEditorAndFile(
22+
element: PsiElement,
23+
editor: Editor,
24+
file: PsiFile,
25+
context: DataContext
26+
): Boolean {
27+
return isNamedConstructorCandidate(element)
28+
}
29+
30+
private fun isNamedConstructorCandidate(element: PsiElement): Boolean {
31+
if (element is Method && element.isStatic) {
32+
val constructorParameters = element.containingClass?.constructor?.let { it.parameters.map { it.typeDeclaration?.text ?: "" } }
33+
val methodParameters = element.parameters.map { it.typeDeclaration?.text ?: "" }
34+
return constructorParameters == methodParameters
35+
}
36+
return false
37+
}
38+
39+
override fun getHandler(dataContext: DataContext): RefactoringActionHandler {
40+
return UseNamedConstructorHandler()
41+
}
42+
43+
override fun isAvailableForFile(file: PsiFile): Boolean {
44+
return isAvailableForLanguage(file.language)
45+
}
46+
47+
override fun isAvailableForLanguage(language: Language): Boolean {
48+
return language.isKindOf(PhpLanguage.INSTANCE)
49+
}
50+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.funivan.idea.phpClean.actions.useNamedConstructor
2+
3+
import com.intellij.openapi.actionSystem.CommonDataKeys
4+
import com.intellij.openapi.actionSystem.DataContext
5+
import com.intellij.openapi.editor.Editor
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.psi.PsiElement
8+
import com.intellij.psi.PsiFile
9+
import com.intellij.refactoring.RefactoringActionHandler
10+
import com.jetbrains.php.lang.psi.elements.Method
11+
12+
class UseNamedConstructorHandler : RefactoringActionHandler {
13+
override fun invoke(project: Project, editor: Editor?, file: PsiFile?, dataContext: DataContext?) {
14+
if (editor == null) {
15+
return
16+
}
17+
dataContext
18+
?.let { CommonDataKeys.PSI_ELEMENT.getData(it) as Method? }
19+
?.let {
20+
val constructor = it.containingClass?.ownConstructor
21+
if (constructor != null) {
22+
UseNamedConstructorProcessor(constructor, it).run()
23+
}
24+
}
25+
}
26+
27+
override fun invoke(project: Project, elements: Array<PsiElement>, dataContext: DataContext?) {
28+
// Do nothing. This action can not be triggered in non-editor context
29+
}
30+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.funivan.idea.phpClean.actions.useNamedConstructor
2+
3+
import com.intellij.psi.search.searches.ReferencesSearch
4+
import com.intellij.psi.util.parentOfType
5+
import com.intellij.refactoring.BaseRefactoringProcessor
6+
import com.intellij.usageView.UsageInfo
7+
import com.intellij.usageView.UsageViewDescriptor
8+
import com.jetbrains.php.lang.psi.PhpPsiElementFactory
9+
import com.jetbrains.php.lang.psi.elements.Method
10+
import com.jetbrains.php.lang.psi.elements.NewExpression
11+
12+
class UseNamedConstructorProcessor(
13+
val constructor: Method,
14+
val namedConstructor: Method,
15+
) : BaseRefactoringProcessor(constructor.project) {
16+
17+
override fun createUsageViewDescriptor(usages: Array<out UsageInfo>): UsageViewDescriptor {
18+
return NamedConstructorUsageViewDescriptor(constructor, namedConstructor)
19+
}
20+
21+
override fun findUsages(): Array<UsageInfo> {
22+
return ReferencesSearch.search(constructor)
23+
.map { it.element.parent as? NewExpression }
24+
.filter {
25+
val inMethod = it?.parentOfType(Method::class)
26+
if (inMethod is Method) {
27+
val isSameM = inMethod.name == namedConstructor.name
28+
val isSameClass = inMethod.containingClass?.fqn == namedConstructor.containingClass?.fqn
29+
(isSameM && isSameClass) == false
30+
} else {
31+
true
32+
}
33+
}
34+
.filterNotNull()
35+
.map { UsageInfo(it) }.toTypedArray()
36+
}
37+
38+
override fun performRefactoring(usages: Array<UsageInfo>) {
39+
usages.forEach {
40+
(it.element as? NewExpression)?.let {
41+
val n = it.classReference?.element?.text
42+
if (n != null) {
43+
it.replace(
44+
PhpPsiElementFactory.createMethodReference(
45+
it.project,
46+
n + "::" + namedConstructor.name
47+
+ "(" + it.parameterList?.text.orEmpty() + ")"
48+
)
49+
)
50+
}
51+
}
52+
}
53+
}
54+
55+
override fun getCommandName() = "Use named constructor"
56+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@
177177
</extensions>
178178

179179
<actions>
180-
<!-- Add your actions here -->
180+
<action class="com.funivan.idea.phpClean.actions.useNamedConstructor.UseNamedConstructorAction"
181+
id="PhpClean.UseNamedConstructor"
182+
text="Use named constructor"
183+
description="Use named constructor instead of construct method"
184+
>
185+
<add-to-group group-id="RefactoringMenu" anchor="after" relative-to-action="SafeDelete"/>
186+
</action>
181187
</actions>
182188
</idea-plugin>

0 commit comments

Comments
 (0)