[kssr] KTIJ-20631 Rework object matching

Removes companion object matching by default and adds the `Also match companion object` modifier to allow matching companion objects. Also introduces a [0, 1] count filter for the object identifier to match unnamed/named companion objects and object expressions.

GitOrigin-RevId: bd5176c8a28f9afbd91d4ca8117e2f309cd0d35b
This commit is contained in:
Bart van Helvert
2022-01-26 20:16:38 +00:00
committed by intellij-monorepo-bot
parent f1907e692b
commit b5b48c828b
17 changed files with 233 additions and 85 deletions
@@ -1960,13 +1960,15 @@ error.type.filter.node=Kotlin matching element should either be an expression or
modifier.match.val=Match val
modifier.match.var=Match var
modifier.match.companion.object=Match companion object
modifier.also.match.val=Also match val
modifier.also.match.var=Also match var
modifier.also.match.companion.object=Also match companion object
predefined.configuration.all.methods.of.the.class=All methods of a class
predefined.configuration.all.vars.of.the.class=All vars of a class
predefined.configuration.all.vars.of.the.object=All vars of an object or companion object
predefined.configuration.all.vars.of.the.object=All vars of an object
predefined.configuration.annotations=Annotations
predefined.configuration.anonymous.class=Anonymous class
predefined.configuration.array.access=Array access
@@ -1974,6 +1976,7 @@ predefined.configuration.assert.not.null=Not-null assertion operators
predefined.configuration.assignments=Assignments
predefined.configuration.casts=Casts
predefined.configuration.class.annotation=Annotated classes
predefined.configuration.object.companion.object=Object and companion object
predefined.configuration.comments.containing.word=Comments containing a given word
predefined.configuration.do.while=Do...while loops
predefined.configuration.elvis=Elvis operators
@@ -1,5 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.structuralsearch
import com.intellij.structuralsearch.PatternContext
@@ -9,6 +8,7 @@ import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchCompanionObjectModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchValModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.OneStateFilter
@@ -52,7 +52,7 @@ object KotlinPredefinedConfigurations {
),
searchTemplate(
KotlinBundle.message("predefined.configuration.all.vars.of.the.object"),
"all vars of an object or companion object",
"all vars of an object",
"""
object '_Object {
var 'Field+ = '_Init?
@@ -73,6 +73,14 @@ object KotlinPredefinedConfigurations {
""".trimIndent(),
CLASS_TYPE
),
searchTemplate(
KotlinBundle.message("predefined.configuration.object.companion.object"),
"object also match companion object",
"""
object '_Object{0,1}:[_${AlsoMatchCompanionObjectModifier.CONSTRAINT_NAME}(${OneStateFilter.ENABLED})]
""".trimIndent(),
CLASS_TYPE
),
// Expressions
searchTemplate(
@@ -1,5 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.structuralsearch
import com.intellij.dupLocator.util.NodeFilter
@@ -28,9 +27,11 @@ import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.liveTemplates.KotlinTemplateContextType
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchCompanionObjectModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchValModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchVarModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.OneStateFilter
import org.jetbrains.kotlin.idea.structuralsearch.predicates.KotlinAlsoMatchCompanionObjectPredicate
import org.jetbrains.kotlin.idea.structuralsearch.predicates.KotlinAlsoMatchValVarPredicate
import org.jetbrains.kotlin.idea.structuralsearch.predicates.KotlinExprTypePredicate
import org.jetbrains.kotlin.idea.structuralsearch.visitor.KotlinCompilingVisitor
@@ -207,6 +208,8 @@ class KotlinStructuralSearchProfile : StructuralSearchProfile() {
UIUtil.REFERENCE -> isApplicableReference(variableNode)
AlsoMatchVarModifier.CONSTRAINT_NAME -> variableNode.parent is KtProperty && !(variableNode.parent as KtProperty).isVar
AlsoMatchValModifier.CONSTRAINT_NAME -> variableNode.parent is KtProperty && (variableNode.parent as KtProperty).isVar
AlsoMatchCompanionObjectModifier.CONSTRAINT_NAME -> variableNode.parent is KtObjectDeclaration &&
!(variableNode.parent as KtObjectDeclaration).isCompanion()
else -> super.isApplicableConstraint(constraintName, variableNode, completePattern, target)
}
@@ -260,6 +263,7 @@ class KotlinStructuralSearchProfile : StructuralSearchProfile() {
private fun isApplicableMinCount(variableNode: PsiElement): Boolean {
val family = ancestors(variableNode)
return when {
family[0] is KtObjectDeclaration -> true
family[0] !is KtNameReferenceExpression -> false
family[1] is KtProperty -> true
family[1] is KtDotQualifiedExpression -> true
@@ -288,9 +292,9 @@ class KotlinStructuralSearchProfile : StructuralSearchProfile() {
*/
private fun isApplicableMinMaxCount(variableNode: PsiElement): Boolean {
val family = ancestors(variableNode)
// println(family.map { if (it == null) "null" else it::class.java.toString().split(".").last() })
return when {
// Containers (lists, bodies, ...)
family[0] is KtObjectDeclaration -> false
family[1] is KtClassBody -> true
family[0] is KtParameter && family[1] is KtParameterList -> true
family[0] is KtTypeParameter && family[1] is KtTypeParameterList -> true
@@ -334,6 +338,9 @@ class KotlinStructuralSearchProfile : StructuralSearchProfile() {
if (getAdditionalConstraint(AlsoMatchValModifier.CONSTRAINT_NAME) == OneStateFilter.ENABLED ||
getAdditionalConstraint(AlsoMatchVarModifier.CONSTRAINT_NAME) == OneStateFilter.ENABLED
) result.add(KotlinAlsoMatchValVarPredicate())
if (getAdditionalConstraint(AlsoMatchCompanionObjectModifier.CONSTRAINT_NAME) == OneStateFilter.ENABLED) {
result.add(KotlinAlsoMatchCompanionObjectPredicate())
}
}
return result
}
@@ -0,0 +1,15 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.structuralsearch.filters
import org.jetbrains.annotations.NonNls
import org.jetbrains.kotlin.idea.KotlinBundle
class AlsoMatchCompanionObjectModifier : OneStateFilter(
KotlinBundle.lazyMessage("modifier.match.companion.object"),
KotlinBundle.message("modifier.also.match.companion.object"),
CONSTRAINT_NAME
) {
companion object {
const val CONSTRAINT_NAME: @NonNls String = "kotlinAlsoMatchCompanionObject"
}
}
@@ -5,5 +5,9 @@ import com.intellij.structuralsearch.plugin.ui.filters.FilterAction
import com.intellij.structuralsearch.plugin.ui.filters.FilterProvider
class KotlinFilterProvider : FilterProvider {
override fun getFilters(): List<FilterAction> = listOf(AlsoMatchValModifier(), AlsoMatchVarModifier())
override fun getFilters(): List<FilterAction> = listOf(
AlsoMatchValModifier(),
AlsoMatchVarModifier(),
AlsoMatchCompanionObjectModifier()
)
}
@@ -1,5 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.structuralsearch.filters
import com.intellij.psi.PsiElement
@@ -0,0 +1,10 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.structuralsearch.predicates
import com.intellij.psi.PsiElement
import com.intellij.structuralsearch.impl.matcher.MatchContext
import com.intellij.structuralsearch.impl.matcher.predicates.MatchPredicate
class KotlinAlsoMatchCompanionObjectPredicate : MatchPredicate() {
override fun match(matchedNode: PsiElement, start: Int, end: Int, context: MatchContext): Boolean = true
}
@@ -33,6 +33,7 @@ import org.jetbrains.kotlin.idea.search.declarationsSearch.HierarchySearchReques
import org.jetbrains.kotlin.idea.search.declarationsSearch.searchInheritors
import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor
import org.jetbrains.kotlin.idea.structuralsearch.*
import org.jetbrains.kotlin.idea.structuralsearch.predicates.KotlinAlsoMatchCompanionObjectPredicate
import org.jetbrains.kotlin.idea.structuralsearch.predicates.KotlinAlsoMatchValVarPredicate
import org.jetbrains.kotlin.idea.util.safeAnalyzeNonSourceRootCode
import org.jetbrains.kotlin.kdoc.lexer.KDocTokens
@@ -113,7 +114,18 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
private fun matchTextOrVariable(el1: PsiElement?, el2: PsiElement?): Boolean {
if (el1 == null) return true
if (el2 == null) return el1 == el2
if (el2 == null) return false
return substituteOrMatchText(el1, el2)
}
private fun matchTextOrVariableEq(el1: PsiElement?, el2: PsiElement?): Boolean {
if (el1 == null && el2 == null) return true
if (el1 == null) return false
if (el2 == null) return false
return substituteOrMatchText(el1, el2)
}
private fun substituteOrMatchText(el1: PsiElement, el2: PsiElement): Boolean {
return when (val handler = getHandler(el1)) {
is SubstitutionHandler -> handler.validate(el2, myMatchingVisitor.matchContext)
else -> myMatchingVisitor.matchText(el1, el2)
@@ -130,10 +142,10 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
KDocTokens.TEXT -> {
myMatchingVisitor.result = when (val handler = leafPsiElement.getUserData(CompiledPattern.HANDLER_KEY)) {
is LiteralWithSubstitutionHandler -> handler.match(leafPsiElement, other, myMatchingVisitor.matchContext)
else -> matchTextOrVariable(leafPsiElement, other)
else -> substituteOrMatchText(leafPsiElement, other)
}
}
KDocTokens.TAG_NAME, KtTokens.IDENTIFIER -> myMatchingVisitor.result = matchTextOrVariable(leafPsiElement, other)
KDocTokens.TAG_NAME, KtTokens.IDENTIFIER -> myMatchingVisitor.result = substituteOrMatchText(leafPsiElement, other)
}
}
@@ -306,7 +318,7 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
override fun visitConstantExpression(expression: KtConstantExpression) {
val other = getTreeElementDepar<KtExpression>() ?: return
myMatchingVisitor.result = matchTextOrVariable(expression, other)
myMatchingVisitor.result = substituteOrMatchText(expression, other)
}
override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) {
@@ -337,7 +349,7 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
&& other is KtDotQualifiedExpression
&& myMatchingVisitor.match(expression, other.selectorExpression)
myMatchingVisitor.result = skipReceiver || matchTextOrVariable(
myMatchingVisitor.result = skipReceiver || substituteOrMatchText(
expression.getReferencedNameElement(),
if (other is KtSimpleNameExpression) other.getReferencedNameElement() else other
)
@@ -675,7 +687,7 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
override fun visitTypeParameter(parameter: KtTypeParameter) {
val other = getTreeElementDepar<KtTypeParameter>() ?: return
myMatchingVisitor.result = matchTextOrVariable(parameter.firstChild, other.firstChild) // match generic identifier
myMatchingVisitor.result = substituteOrMatchText(parameter.firstChild, other.firstChild) // match generic identifier
&& myMatchingVisitor.match(parameter.extendsBound, other.extendsBound)
&& parameter.variance == other.variance
parameter.nameIdentifier?.let { nameIdentifier ->
@@ -872,17 +884,21 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
override fun visitObjectDeclaration(declaration: KtObjectDeclaration) {
val other = getTreeElementDepar<KtObjectDeclaration>() ?: return
val otherIdentifier =
other.nameIdentifier ?: if (other.isCompanion()) (other.parent.parent as KtClass).nameIdentifier else null
myMatchingVisitor.result = myMatchingVisitor.match(declaration.modifierList, other.modifierList)
&& matchTextOrVariable(declaration.nameIdentifier, otherIdentifier)
val inferredNameIdentifier =
declaration.nameIdentifier ?: if (declaration.isCompanion()) (declaration.parent.parent as KtClass).nameIdentifier else null
val handler = inferredNameIdentifier?.let { getHandler(inferredNameIdentifier) }
val matchIdentifier = if (handler is SubstitutionHandler && handler.maxOccurs > 0 && handler.minOccurs == 0) {
true // match count filter with companion object without identifier
} else matchTextOrVariableEq(declaration.nameIdentifier, other.nameIdentifier)
myMatchingVisitor.result =
(declaration.isCompanion() == other.isCompanion() ||
(handler is SubstitutionHandler && handler.predicate is KotlinAlsoMatchCompanionObjectPredicate))
&& myMatchingVisitor.match(declaration.modifierList, other.modifierList)
&& matchIdentifier
&& myMatchingVisitor.match(declaration.getSuperTypeList(), other.getSuperTypeList())
&& myMatchingVisitor.match(declaration.body, other.body)
declaration.nameIdentifier?.let { declNameIdentifier ->
val handler = getHandler(declNameIdentifier)
if (myMatchingVisitor.result && handler is SubstitutionHandler) {
handler.handle(otherIdentifier, myMatchingVisitor.matchContext)
}
if (myMatchingVisitor.result && handler is SubstitutionHandler) {
handler.handle(other.nameIdentifier, myMatchingVisitor.matchContext)
}
}
@@ -1129,7 +1145,7 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
val other = myMatchingVisitor.element
myMatchingVisitor.result = when (val handler = entry.getUserData(CompiledPattern.HANDLER_KEY)) {
is LiteralWithSubstitutionHandler -> handler.match(entry, other, myMatchingVisitor.matchContext)
else -> matchTextOrVariable(entry, other)
else -> substituteOrMatchText(entry, other)
}
}
@@ -1140,7 +1156,7 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
override fun visitEscapeStringTemplateEntry(entry: KtEscapeStringTemplateEntry) {
val other = getTreeElementDepar<KtEscapeStringTemplateEntry>() ?: return
myMatchingVisitor.result = matchTextOrVariable(entry, other)
myMatchingVisitor.result = substituteOrMatchText(entry, other)
}
override fun visitBinaryWithTypeRHSExpression(expression: KtBinaryExpressionWithTypeRHS) {
@@ -1243,7 +1259,7 @@ class KotlinMatchingVisitor(private val myMatchingVisitor: GlobalMatchingVisitor
override fun visitKDocLink(link: KDocLink) {
val other = getTreeElementDepar<KDocLink>() ?: return
myMatchingVisitor.result = matchTextOrVariable(link, other)
myMatchingVisitor.result = substituteOrMatchText(link, other)
}
companion object {
@@ -1,31 +1,144 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.structuralsearch.search
import org.jetbrains.kotlin.idea.structuralsearch.KotlinSSResourceInspectionTest
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchCompanionObjectModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.OneStateFilter
class KotlinSSObjectDeclarationTest : KotlinSSResourceInspectionTest() {
override fun getBasePath(): String = "objectDeclaration"
fun testObject() {
doTest("object '_", """
<warning descr="SSR">object A { }</warning>
class B {
companion object { }
}
fun main() {
val x = object { }
println(x)
}
""".trimIndent())
}
fun testObject() { doTest("object '_") }
fun testNestedObject() {
doTest("object B", """
object A {
<warning descr="SSR">object B { }</warning>
}
class C {
companion object B { }
}
""".trimIndent())
}
fun testCompanionObject() { doTest("object A") }
fun testObjectAlsoMatchCompanionObject() {
doTest("object '_:[_${AlsoMatchCompanionObjectModifier.CONSTRAINT_NAME}(${OneStateFilter.ENABLED})]", """
class A {
companion object { }
}
class B {
<warning descr="SSR">companion object Factory { }</warning>
}
<warning descr="SSR">object C { }</warning>
""".trimIndent())
}
fun testNestedObject() { doTest("object B") }
fun testObjectCountModifier() {
doTest("object '_{0,1}", """
class A {
companion object { }
}
class B {
companion object Factory { }
}
<warning descr="SSR">object C { }</warning>
fun main() {
val x = <warning descr="SSR">object { }</warning>
println(x)
}
""".trimIndent())
}
fun testObjectAlsoMatchCompanionObjectCountModifier() {
doTest("object '_{0,1}:[_${AlsoMatchCompanionObjectModifier.CONSTRAINT_NAME}(${OneStateFilter.ENABLED})]", """
class A {
<warning descr="SSR">companion object { }</warning>
}
class B {
<warning descr="SSR">companion object Factory { }</warning>
}
<warning descr="SSR">object C { }</warning>
""".trimIndent())
}
fun testCompanionObject() { doTest("""
class '_ {
companion object '_ { }
}
""".trimIndent(), """
class A {
companion object { }
}
<warning descr="SSR">class B {
companion object Factory { }
}</warning>
object C { }
""".trimIndent())
}
fun testNamelessCompanionObject() { doTest("""
class '_ {
companion object { }
}
""".trimIndent(), """
<warning descr="SSR">class A {
companion object { }
}</warning>
class B {
companion object Factory { }
}
object C { }
""".trimIndent())
}
fun testCompanionObjectCountModifier() { doTest("""
class '_ {
companion object '_{0,1} { }
}
""".trimIndent(), """
<warning descr="SSR">class A {
companion object { }
}</warning>
<warning descr="SSR">class B {
companion object Factory { }
}</warning>
object C { }
""".trimIndent())
}
fun testNamedCompanionObject() {
doTest(
"""class '_ {
companion object Foo { }
}""".trimMargin()
doTest("""
class '_ {
companion object Foo { }
}
""".trimIndent(), """
<warning descr="SSR">class A {
companion object Foo { }
}</warning>
""".trimIndent()
)
}
fun testNestedNamedCompanionObject() {
doTest(
"""class '_ {
doTest("""
class '_ {
companion object Foo { }
}
""".trimIndent(), """
<warning descr="SSR">class ContainerForCompanion {
companion object Foo { }
}""".trimMargin()
}</warning>
""".trimIndent()
)
}
}
@@ -1,9 +1,12 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.structuralsearch.search
import org.jetbrains.kotlin.idea.structuralsearch.KotlinSSResourceInspectionTest
import org.jetbrains.kotlin.idea.structuralsearch.KotlinStructuralSearchProfile
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchValModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchVarModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.OneStateFilter
class KotlinSSPropertyTest : KotlinSSResourceInspectionTest() {
override fun getBasePath(): String = "property"
@@ -55,4 +58,20 @@ class KotlinSSPropertyTest : KotlinSSResourceInspectionTest() {
fun testReceiverTypeReference() { doTest("val Int.'_ : '_") }
fun testReceiverFqTypeReference() { doTest("val kotlin.Int.'_ : '_") }
fun testAlsoMatchValModifier() { doTest("var '_:[_${AlsoMatchValModifier.CONSTRAINT_NAME}(${OneStateFilter.ENABLED})]", """
fun main() {
<warning descr="SSR">var x = 1</warning>
<warning descr="SSR">val y = 1</warning>
print(x + y)
}
""".trimIndent()) }
fun testAlsoMatchVarModifier() { doTest("val '_:[_${AlsoMatchVarModifier.CONSTRAINT_NAME}(${OneStateFilter.ENABLED})]", """
fun main() {
<warning descr="SSR">var x = 1</warning>
<warning descr="SSR">val y = 1</warning>
print(x + y)
}
""".trimIndent()) }
}
@@ -1,26 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.structuralsearch.search.filters
import org.jetbrains.kotlin.idea.structuralsearch.KotlinSSResourceInspectionTest
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchValModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.AlsoMatchVarModifier
import org.jetbrains.kotlin.idea.structuralsearch.filters.OneStateFilter
class KotlinSSValVarModifierTests: KotlinSSResourceInspectionTest() {
fun testAlsoMatchValModifier() { doTest("var '_:[_${AlsoMatchValModifier.CONSTRAINT_NAME}(${OneStateFilter.ENABLED})]", """
fun main() {
<warning descr="SSR">var x = 1</warning>
<warning descr="SSR">val y = 1</warning>
print(x + y)
}
""".trimIndent()) }
fun testAlsoMatchVarModifier() { doTest("val '_:[_${AlsoMatchVarModifier.CONSTRAINT_NAME}(${OneStateFilter.ENABLED})]", """
fun main() {
<warning descr="SSR">var x = 1</warning>
<warning descr="SSR">val y = 1</warning>
print(x + y)
}
""".trimIndent()) }
}
@@ -1,3 +0,0 @@
class A {
<warning descr="SSR">companion object { }</warning>
}
@@ -1,3 +0,0 @@
<warning descr="SSR">class A {
companion object Foo { }
}</warning>
@@ -1,3 +0,0 @@
<warning descr="SSR">class ContainerForCompanion {
companion object Foo { }
}</warning>
@@ -1,3 +0,0 @@
object A {
<warning descr="SSR">object B { }</warning>
}
@@ -1,3 +0,0 @@
<warning descr="SSR">object A { }</warning>
class B { }
@@ -1,5 +0,0 @@
import java.awt.event.MouseAdapter
<warning descr="SSR">object A : MouseAdapter() { }</warning>
object B { }