Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
961911c
chore(docs): Source mathcing enrichment
misonijnik Apr 11, 2026
3a24e32
docs: Type-aware pattern matching design spec
misonijnik Apr 12, 2026
513c220
feat: add typeArgs field to TypeNamePattern.ClassName and FullyQualified
misonijnik Apr 12, 2026
cb14b7d
feat: add returnType field to MethodSignature action and predicate
misonijnik Apr 12, 2026
95f08fa
feat: stop discarding type args, array returns, concrete returns in p…
misonijnik Apr 12, 2026
b571c5c
feat: handle typeArgs in unifyTypeName for generic type unification
misonijnik Apr 12, 2026
6a71939
feat: recurse into typeArgs for metavar extraction in typeNameMetaVars
misonijnik Apr 12, 2026
a28d6d2
feat: add typeArgs field to SerializedTypeNameMatcher.ClassPattern
misonijnik Apr 12, 2026
6cc4c73
feat: propagate typeArgs and returnType through automata-to-taint con…
misonijnik Apr 12, 2026
df6bd0a
feat: add typeArgs to TypeMatchesPattern for deferred generic matching
misonijnik Apr 12, 2026
1d89709
feat: add matchType(JIRType) and pass typeArgs through resolveIsType()
misonijnik Apr 12, 2026
324f7c7
feat: resolve generic types in JIRBasicAtomEvaluator for call-site re…
misonijnik Apr 12, 2026
55440d4
test: add E2E samples for generic type args, array returns, concrete …
misonijnik Apr 12, 2026
91571e3
test: add E2E tests for type-aware pattern matching
misonijnik Apr 12, 2026
2936ef6
fix: thread returnType through pipeline and fix generic type matching
misonijnik Apr 12, 2026
77bc679
test: add ResponseEntity<$T> generic return type with metavar type ar…
misonijnik Apr 13, 2026
3ac5200
docs: add type pattern mathching plan
misonijnik Apr 13, 2026
2427e19
test(engine): cover type-aware feature matrix (A1-A6)
misonijnik Apr 20, 2026
a1b3d97
test(engine): convert A1/A3/A6 pins to honest Negatives
misonijnik Apr 20, 2026
5566863
fix: discriminate concrete type arguments at method-decl return position
misonijnik Apr 20, 2026
cf4d3ae
test(engine): A8/A10/A12/A13 generic-function-definition gap tests
misonijnik Apr 20, 2026
9c0a051
test(engine): A15/A17/A19-A23 generic-function-definition gap tests
misonijnik Apr 20, 2026
6a3eb37
refactor: consolidate type-matching logic into shared DSL primitive
misonijnik Apr 21, 2026
cea2439
fix: reject concrete type args against wildcard pattern
misonijnik Apr 21, 2026
a420e9d
fix: use declared erasure for type variables and wildcards
misonijnik Apr 21, 2026
fa76ba5
docs: Clean
misonijnik Apr 21, 2026
8baa50c
refactor: simplify type-arg matching with specialized matcher and con…
misonijnik Apr 30, 2026
562a987
Cleanup type matcher
Saloed May 4, 2026
510be7d
Cleanup type matcher in config loader
Saloed May 4, 2026
c730906
Remove incorrect test example
Saloed May 4, 2026
13d3721
Cleanup rule builder
Saloed May 4, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.opentaint.dataflow.configuration.jvm

import org.opentaint.dataflow.configuration.jvm.serialized.SerializedSimpleNameMatcher
import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTypeNameMatcher
import org.opentaint.ir.api.jvm.JIRArrayType
import org.opentaint.ir.api.jvm.JIRClassType
import org.opentaint.ir.api.jvm.JIRType
import org.opentaint.ir.api.jvm.JIRTypeVariable
import org.opentaint.ir.api.jvm.JIRUnboundWildcard

fun SerializedTypeNameMatcher.matchType(
erasedTypeName: String,
resolveType: () -> JIRType,
erasedMatch: SerializedTypeNameMatcher.(String) -> Boolean,
): Boolean {
if (!erasedMatch(erasedTypeName)) return false
return matchTypeArgs(resolveType, erasedMatch)
}

private fun SerializedTypeNameMatcher.matchTypeArgs(
resolveType: () -> JIRType?,
erasedMatch: SerializedTypeNameMatcher.(String) -> Boolean,
): Boolean {
return when (this) {
is SerializedSimpleNameMatcher -> true // no type args

is SerializedTypeNameMatcher.ClassPattern -> {
val args = typeArgs ?: return true

val type = resolveType()
if (type !is JIRClassType) return false

if (args.size != type.typeArguments.size) return false

args.zip(type.typeArguments).all { (m, a) ->
m.matchType(a.erasedName(), resolveType = { a }, erasedMatch)
}
}

is SerializedTypeNameMatcher.Array -> element.matchTypeArgs(
resolveType = { (resolveType() as? JIRArrayType)?.elementType },
erasedMatch
)
}
}

/**
* Erased class name for matching — drops any generic decoration that
* [JIRType.typeName] may carry (e.g. `Map<String, Object>` → `java.util.Map`)
* and reduces a type variable / unbound wildcard to its declared erasure
* (e.g. `E` → `java.lang.Object`) so string-based matchers can match against
* pass-through rules whose return/parameter types show up as type variables
* when resolved via the declaring class (e.g. `List.get` returns `E`).
*/
fun JIRType.erasedName(): String = when (this) {
is JIRClassType -> jIRClass.name
is JIRTypeVariable -> jIRClass.name
is JIRUnboundWildcard -> jIRClass.name
is JIRArrayType -> {
val el = elementType
when (el) {
is JIRClassType -> el.jIRClass.name + "[]"
is JIRTypeVariable -> el.jIRClass.name + "[]"
is JIRUnboundWildcard -> el.jIRClass.name + "[]"
else -> typeName
}
}
else -> typeName
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ sealed interface ConditionNameMatcher {
data class TypeMatchesPattern(
val position: Position,
val pattern: ConditionNameMatcher,
val typeArgs: List<TypeArgMatcher>? = null,
) : Condition {
override fun <R> accept(conditionVisitor: ConditionVisitor<R>): R = conditionVisitor.visit(this)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.opentaint.dataflow.configuration.jvm

/**
* A type-argument matcher that has been pre-resolved during rule resolution:
* the erased-name matchers are already compiled to [ConditionNameMatcher],
* so runtime evaluation only needs to dispatch on the structure.
*/
sealed interface TypeArgMatcher {

data class Class(
val name: ConditionNameMatcher,
// null = no type-args constraint (matches raw / declared erasure).
val typeArgs: List<TypeArgMatcher>?,
) : TypeArgMatcher

data class Array(val element: TypeArgMatcher) : TypeArgMatcher
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ sealed interface SerializedTypeNameMatcher {
@Serializable
data class ClassPattern(
val `package`: SerializedSimpleNameMatcher,
val `class`: SerializedSimpleNameMatcher
val `class`: SerializedSimpleNameMatcher,
// null = no type-args constraint (matches raw / declared erasure).
// empty list is reserved for an explicit zero-arg parameterization.
val typeArgs: List<SerializedTypeNameMatcher>? = null,
) : SerializedTypeNameMatcher

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import org.opentaint.dataflow.configuration.jvm.Not
import org.opentaint.dataflow.configuration.jvm.Or
import org.opentaint.dataflow.configuration.jvm.Position
import org.opentaint.dataflow.configuration.jvm.PositionResolver
import org.opentaint.dataflow.configuration.jvm.TypeArgMatcher
import org.opentaint.dataflow.configuration.jvm.TypeMatches
import org.opentaint.dataflow.configuration.jvm.TypeMatchesPattern
import org.opentaint.dataflow.configuration.jvm.erasedName
import org.opentaint.dataflow.jvm.ap.ifds.CallPositionValue
import org.opentaint.dataflow.jvm.ap.ifds.JIRFactTypeChecker
import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis
Expand All @@ -31,7 +33,10 @@ import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis.AliasApInfo
import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis.AliasInfo
import org.opentaint.ir.api.common.cfg.CommonInst
import org.opentaint.ir.api.common.cfg.CommonValue
import org.opentaint.ir.api.jvm.JIRArrayType
import org.opentaint.ir.api.jvm.JIRClassType
import org.opentaint.ir.api.jvm.JIRRefType
import org.opentaint.ir.api.jvm.JIRType
import org.opentaint.ir.api.jvm.cfg.JIRBool
import org.opentaint.ir.api.jvm.cfg.JIRCallExpr
import org.opentaint.ir.api.jvm.cfg.JIRConstant
Expand Down Expand Up @@ -329,21 +334,35 @@ class JIRBasicAtomEvaluator(
val type = value.type as? JIRRefType ?: return false

val pattern = condition.pattern
if (pattern.match(type.typeName)) return true
val erasedMatch = pattern.match(type.typeName)

if (pattern !is ConditionNameMatcher.Concrete) {
// todo: check super classes?
return false
}
if (!erasedMatch) {
if (pattern !is ConditionNameMatcher.Concrete) {
// todo: check super classes?
return false
}

if (negated) return false
if (negated) return false

if (type.typeName == "java.lang.Object") {
// todo: hack to avoid explosion
return false
}

if (type.typeName == "java.lang.Object") {
// todo: hack to avoid explosion
return false
if (!typeChecker.typeMayHaveSubtypeOf(type.typeName, pattern.name)) {
return false
}
}

return typeChecker.typeMayHaveSubtypeOf(type.typeName, pattern.name)
val typeArgs = condition.typeArgs
?: return true

if (type !is JIRClassType) return true

if (type.typeArguments.size != typeArgs.size) return false
return typeArgs.zip(type.typeArguments).all { (matcher, arg) ->
matcher.matchType(arg)
}
}

private fun ConditionNameMatcher.match(name: String): Boolean = when (this) {
Expand All @@ -352,7 +371,7 @@ class JIRBasicAtomEvaluator(
is ConditionNameMatcher.Simple -> match(name)
}

private fun ConditionNameMatcher.Simple.match(name: String): Boolean = when (this) {
private fun ConditionNameMatcher.Simple.match(name: String): Boolean = when (this) {
is ConditionNameMatcher.Pattern -> pattern.containsMatchIn(name)
is ConditionNameMatcher.Concrete -> this.name == name
is ConditionNameMatcher.AnyName -> true
Expand All @@ -367,4 +386,24 @@ class JIRBasicAtomEvaluator(
is CallPositionValue.Value -> value(res.value)
is CallPositionValue.VarArgValue -> callVarArgValue(res.value)
}

private fun TypeArgMatcher.matchType(type: JIRType): Boolean = when (this) {
is TypeArgMatcher.Class -> matchType(type)
is TypeArgMatcher.Array -> matchType(type)
}

private fun TypeArgMatcher.Class.matchType(type: JIRType): Boolean {
if (!name.match(type.erasedName())) return false

val args = typeArgs
if (args == null || type !is JIRClassType) {
return true
}

if (args.size != type.typeArguments.size) return false
return args.zip(type.typeArguments).all { (m, a) -> m.matchType(a) }
}

private fun TypeArgMatcher.Array.matchType(type: JIRType): Boolean =
type is JIRArrayType && element.matchType(type.elementType)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package example;

import base.RuleSample;
import base.RuleSet;
import java.util.List;

/**
* A15. Array of parameterized type — {@code List<String>[]}.
*
* Rule return type {@code List<String>[] $METHOD(...)}.
*
* Expected behavior:
* <ul>
* <li>Positive: method returns {@code List<String>[]}.</li>
* <li>Negative: method returns {@code List<String>} — missing array
* dimension; rule must NOT fire.</li>
* <li>Negative: method returns {@code List<Integer>[]} — inner type arg
* differs; rule must NOT fire.</li>
* <li>Negative: method returns {@code String[]} — wrong outer type.</li>
* </ul>
*
* {@code List<String>[]} is a legal return-type declaration in Java; we use
* {@code @SuppressWarnings("unchecked")} to silence the generic-array
* creation warning on the negative helpers.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@RuleSet("example/RuleWithArrayOfParameterized.yaml")
public abstract class RuleWithArrayOfParameterized implements RuleSample {

void sink(String data) {}

List<String>[] methodReturningListStringArray(String data) {
sink(data);
return null;
}

List<String> methodReturningListString(String data) {
sink(data);
return null;
}

List<Integer>[] methodReturningListIntegerArray(String data) {
sink(data);
return null;
}

String[] methodReturningStringArray(String data) {
sink(data);
return null;
}

final static class PositiveListStringArray extends RuleWithArrayOfParameterized {
@Override
public void entrypoint() {
String data = "tainted";
methodReturningListStringArray(data);
}
}

/**
* Honest Negative: missing the outer array dimension — return is
* {@code List<String>}, not {@code List<String>[]}.
*/
final static class NegativeListStringNoArray extends RuleWithArrayOfParameterized {
@Override
public void entrypoint() {
String data = "tainted";
methodReturningListString(data);
}
}

/**
* Honest Negative: inner type argument is {@code Integer}, not the
* required {@code String}.
*/
final static class NegativeListIntegerArray extends RuleWithArrayOfParameterized {
@Override
public void entrypoint() {
String data = "tainted";
methodReturningListIntegerArray(data);
}
}

/**
* Honest Negative: outer type is {@code String[]}, not {@code List<...>[]}.
*/
final static class NegativeStringArray extends RuleWithArrayOfParameterized {
@Override
public void entrypoint() {
String data = "tainted";
methodReturningStringArray(data);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package example;

import base.RuleSample;
import base.RuleSet;

@RuleSet("example/RuleWithArrayReturnType.yaml")
public abstract class RuleWithArrayReturnType implements RuleSample {

void sink(String data) {}

String[] methodReturningStringArray(String data) {
sink(data);
return new String[] { data };
}

int[] methodReturningIntArray(String data) {
sink(data);
return new int[] { 1 };
}

String methodReturningString(String data) {
sink(data);
return data;
}

final static class PositiveStringArrayReturn extends RuleWithArrayReturnType {
@Override
public void entrypoint() {
String data = "tainted";
methodReturningStringArray(data);
}
}

final static class NegativeIntArrayReturn extends RuleWithArrayReturnType {
@Override
public void entrypoint() {
String data = "tainted";
methodReturningIntArray(data);
}
}

final static class NegativeStringReturn extends RuleWithArrayReturnType {
@Override
public void entrypoint() {
String data = "tainted";
methodReturningString(data);
}
}
}
Loading
Loading