Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/kotlin/com/aopphp/go/pointcut/AndPointcut.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ open class AndPointcut(

protected fun isMatchesPointcut(point: PhpNamedElement, pointcut: Pointcut): Boolean {
if (point !is PhpClassMember) return false
if (!pointcut.getKind().supports(point)) return false
val containingClass = point.containingClass ?: return false
return pointcut.matches(point) && pointcut.getClassFilter().matches(containingClass)
}
Expand Down
11 changes: 1 addition & 10 deletions src/main/kotlin/com/aopphp/go/pointcut/AttributePointcut.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import com.aopphp.go.index.AttributePhpNamedElementIndex
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.FileBasedIndex
import com.jetbrains.php.PhpIndex
import com.jetbrains.php.lang.psi.elements.Field
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.PhpAttributesOwner
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
Expand All @@ -23,18 +21,11 @@ class AttributePointcut(
override fun getClassFilter() = _classFilter

override fun matches(element: PhpNamedElement): Boolean {
if (!canMatchElement(element)) return false
if (!filterKind.supports(element)) return false
if (element !is PhpAttributesOwner) return false
return element.getAttributes(expectedClass).isNotEmpty()
}

private fun canMatchElement(element: PhpNamedElement) = when (element) {
is Method -> filterKind.contains(KindFilter.KIND_METHOD)
is Field -> filterKind.contains(KindFilter.KIND_PROPERTY)
is PhpClass -> filterKind.contains(KindFilter.KIND_CLASS)
else -> false
}

override fun getKind() = filterKind

override fun equals(other: Any?): Boolean {
Expand Down
18 changes: 18 additions & 0 deletions src/main/kotlin/com/aopphp/go/pointcut/KindFilter.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.aopphp.go.pointcut

import com.jetbrains.php.lang.psi.elements.Field
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
import java.io.Serializable

enum class KindFilter : Serializable {
Expand All @@ -12,3 +16,17 @@ enum class KindFilter : Serializable {
KIND_STATIC_INIT,
KIND_DYNAMIC
}

/**
* Checks whether this set of kinds can match the given element's join point kind.
*
* This is the matching "context" from goaop/framework#274: without it, each part of a
* combined pointcut (e.g. `execution(...) || access(...)`) would be consulted for every
* element kind and could match by name pattern alone.
*/
fun Set<KindFilter>.supports(element: PhpNamedElement): Boolean = when (element) {
is Method -> KindFilter.KIND_METHOD in this
is Field -> KindFilter.KIND_PROPERTY in this
is PhpClass -> KindFilter.KIND_CLASS in this
else -> false
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class SignaturePointcut(
override fun getClassFilter() = _classFilter

override fun matches(element: PhpNamedElement): Boolean {
if (!filterKind.supports(element)) return false
if (element is PhpClassMember && !modifierFilter.matches(element)) return false

val elementName = when (element) {
Expand Down
33 changes: 28 additions & 5 deletions src/test/kotlin/com/aopphp/go/pointcut/AndPointcutTest.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.aopphp.go.pointcut

import com.jetbrains.php.lang.psi.elements.Field
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.jetbrains.php.lang.psi.elements.PhpClassMember
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

class AndPointcutTest {
Expand Down Expand Up @@ -47,13 +52,26 @@ class AndPointcutTest {

@Test
fun `matches returns false when containingClass is null`() {
val member = mock<PhpClassMember>()
val member = mock<Method>()
whenever(member.containingClass).thenReturn(null)
val first = mockTruePointcut()
val second = mockTruePointcut()
assertFalse(AndPointcut(first, second).matches(member))
}

@Test
fun `matches returns false when child pointcut kind does not support element kind`() {
val cls = mock<PhpClass>()
val field = mock<Field>()
whenever(field.containingClass).thenReturn(cls)
val first = mockPointcutMatching(field, cls, true, setOf(KindFilter.KIND_METHOD))
val second = mockPointcutMatching(field, cls, true, setOf(KindFilter.KIND_METHOD))
assertFalse(AndPointcut(first, second).matches(field))
// The kind gate must short-circuit before the child pointcut is consulted
verify(first, never()).matches(any())
verify(second, never()).matches(any())
}

@Test
fun `getKind returns intersection of both pointcut kinds`() {
val first = mockPointcutWithKind(setOf(KindFilter.KIND_METHOD, KindFilter.KIND_CLASS))
Expand Down Expand Up @@ -92,19 +110,24 @@ class AndPointcutTest {
// ---- Helpers ----

private fun mockMemberInClass(cls: PhpClass): PhpClassMember {
val member = mock<PhpClassMember>()
val member = mock<Method>()
whenever(member.containingClass).thenReturn(cls)
return member
}

private fun mockPointcutMatching(member: PhpClassMember, cls: PhpClass, matches: Boolean): Pointcut {
private fun mockPointcutMatching(
member: PhpClassMember,
cls: PhpClass,
matches: Boolean,
kinds: Set<KindFilter> = KindFilter.entries.toSet()
): Pointcut {
val classFilter = mock<PointFilter>()
whenever(classFilter.matches(cls)).thenReturn(matches)
whenever(classFilter.getKind()).thenReturn(KindFilter.entries.toSet())
whenever(classFilter.getKind()).thenReturn(kinds)
val pointcut = mock<Pointcut>()
whenever(pointcut.matches(member)).thenReturn(matches)
whenever(pointcut.getClassFilter()).thenReturn(classFilter)
whenever(pointcut.getKind()).thenReturn(KindFilter.entries.toSet())
whenever(pointcut.getKind()).thenReturn(kinds)
return pointcut
}

Expand Down
49 changes: 49 additions & 0 deletions src/test/kotlin/com/aopphp/go/pointcut/KindFilterTest.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.aopphp.go.pointcut

import com.jetbrains.php.lang.psi.elements.Field
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock

class KindFilterTest {

Expand Down Expand Up @@ -35,4 +40,48 @@ class KindFilterTest {
val allKinds = KindFilter.entries.toSet()
assertEquals(8, allKinds.size)
}

// ---- supports ----

@Test
fun `supports returns true for Method when set contains KIND_METHOD`() {
assertTrue(setOf(KindFilter.KIND_METHOD).supports(mock<Method>()))
}

@Test
fun `supports returns false for Method when set lacks KIND_METHOD`() {
assertFalse(setOf(KindFilter.KIND_PROPERTY, KindFilter.KIND_CLASS).supports(mock<Method>()))
}

@Test
fun `supports returns true for Field when set contains KIND_PROPERTY`() {
assertTrue(setOf(KindFilter.KIND_PROPERTY).supports(mock<Field>()))
}

@Test
fun `supports returns false for Field when set lacks KIND_PROPERTY`() {
assertFalse(setOf(KindFilter.KIND_METHOD, KindFilter.KIND_CLASS).supports(mock<Field>()))
}

@Test
fun `supports returns true for PhpClass when set contains KIND_CLASS`() {
assertTrue(setOf(KindFilter.KIND_CLASS).supports(mock<PhpClass>()))
}

@Test
fun `supports returns false for PhpClass when set lacks KIND_CLASS`() {
assertFalse(setOf(KindFilter.KIND_METHOD, KindFilter.KIND_PROPERTY).supports(mock<PhpClass>()))
}

@Test
fun `supports returns false for plain PhpNamedElement even with all kinds`() {
assertFalse(KindFilter.entries.toSet().supports(mock<PhpNamedElement>()))
}

@Test
fun `empty kind set supports nothing`() {
assertFalse(emptySet<KindFilter>().supports(mock<Method>()))
assertFalse(emptySet<KindFilter>().supports(mock<Field>()))
assertFalse(emptySet<KindFilter>().supports(mock<PhpClass>()))
}
}
106 changes: 106 additions & 0 deletions src/test/kotlin/com/aopphp/go/pointcut/OrPointcutKindMatchingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.aopphp.go.pointcut

import com.jetbrains.php.lang.psi.elements.Field
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.jetbrains.php.lang.psi.elements.PhpModifier
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

/**
* Regression tests for issue #9: a combination with `||` must not let a sub-pointcut
* of one join point kind match elements of another kind.
*
* Models the pointcut `execution(public **->data*(*)) || access(public **->$id*)`
* with real [SignaturePointcut]s: before the fix, a public field named `dataMap`
* was matched by the execution clause purely by name pattern (and a method named
* `idGenerator` by the access clause).
*/
class OrPointcutKindMatchingTest {

private val executionClause = SignaturePointcut(
setOf(KindFilter.KIND_METHOD),
"data*",
MemberAccessMatcherFilter(setOf(PhpModifier.Access.PUBLIC))
)

private val accessClause = SignaturePointcut(
setOf(KindFilter.KIND_PROPERTY),
"id*",
MemberAccessMatcherFilter(setOf(PhpModifier.Access.PUBLIC))
)

private val orPointcut = OrPointcut(executionClause, accessClause)

@Test
fun `field matching execution name pattern is not matched`() {
// The issue #9 false positive: field "dataMap" matches "data*" by name,
// but the execution clause is KIND_METHOD and must not apply to fields
val field = mockField("dataMap")
assertFalse(orPointcut.matches(field))
}

@Test
fun `method matching access name pattern is not matched`() {
// Symmetric case: method "idGenerator" matches "id*" of the access clause
val method = mockMethod("idGenerator")
assertFalse(orPointcut.matches(method))
}

@Test
fun `method matching execution clause still matches`() {
val method = mockMethod("dataLoader")
assertTrue(orPointcut.matches(method))
}

@Test
fun `field matching access clause still matches`() {
val field = mockField("idNumber")
assertTrue(orPointcut.matches(field))
}

@Test
fun `class-kind true pointcut in or does not leak members`() {
// Models `execution(public **->data*(*)) || initialization(**)`: the
// initialization clause compiles to TruePointcut(KIND_CLASS) whose matches()
// is always true — it must not make every method match through the OR branch
val orWithInit = OrPointcut(executionClause, TruePointcut(setOf(KindFilter.KIND_CLASS)))
assertFalse(orWithInit.matches(mockMethod("unrelated")))
assertFalse(orWithInit.matches(mockField("unrelated")))
assertTrue(orWithInit.matches(mockMethod("dataLoader")))
}

@Test
fun `and of cross-kind pointcuts matches nothing`() {
// No single element can be both a method and a property
val andPointcut = AndPointcut(executionClause, accessClause)
assertFalse(andPointcut.matches(mockMethod("dataLoader")))
assertFalse(andPointcut.matches(mockField("idNumber")))
}

@Test
fun `or kind is still the union of both clause kinds`() {
// PointcutAdvisor.getMatchedElements relies on the union to collect members
assertEquals(setOf(KindFilter.KIND_METHOD, KindFilter.KIND_PROPERTY), orPointcut.getKind())
}

// ---- Helpers ----

private fun mockMethod(name: String): Method {
val method = mock<Method>()
whenever(method.name).thenReturn(name)
whenever(method.containingClass).thenReturn(mock<PhpClass>())
whenever(method.modifier).thenReturn(PhpModifier.PUBLIC_IMPLEMENTED_DYNAMIC)
return method
}

private fun mockField(name: String): Field {
val field = mock<Field>()
whenever(field.name).thenReturn(name)
whenever(field.containingClass).thenReturn(mock<PhpClass>())
whenever(field.modifier).thenReturn(PhpModifier.PUBLIC_IMPLEMENTED_DYNAMIC)
return field
}
}
Loading
Loading