feat(selector): add methods/props

This commit is contained in:
lisonge 2024-07-22 22:54:10 +08:00
parent d68573d728
commit 3dca7789e3
9 changed files with 278 additions and 107 deletions

View File

@ -275,7 +275,9 @@ fun clearNodeCache(t: Long = System.currentTimeMillis()) {
lastCacheTime = t
if (BuildConfig.DEBUG) {
val sizeList = defaultCacheTransform.cache.sizeList
Log.d("cache", "clear cache, sizeList=$sizeList")
if (sizeList.any { it > 0 }) {
Log.d("cache", "clear cache, sizeList=$sizeList")
}
}
defaultTransform.cache.clear()
defaultCacheTransform.cache.clear()

View File

@ -147,10 +147,7 @@ val getChildren: (AccessibilityNodeInfo) -> Sequence<AccessibilityNodeInfo> = {
}
private val typeInfo by lazy {
initDefaultTypeInfo().apply {
nodeType.props = nodeType.props.filter { !it.name.startsWith('_') }.toTypedArray()
contextType.props = contextType.props.filter { !it.name.startsWith('_') }.toTypedArray()
}.contextType
initDefaultTypeInfo().globalType
}
fun Selector.checkSelector(): String? {
@ -357,6 +354,7 @@ fun createCacheTransform(): CacheTransform {
when (target) {
is Context<*> -> when (name) {
"prev" -> target.prev
"current" -> target.current
else -> getNodeAttr(target.current as AccessibilityNodeInfo, name)
}
@ -369,7 +367,7 @@ fun createCacheTransform(): CacheTransform {
when (target) {
is AccessibilityNodeInfo -> when (name) {
"getChild" -> {
args.getIntOrNull()?.let { index ->
args.getInt().let { index ->
cache.getChild(target, index)
}
}
@ -379,11 +377,11 @@ fun createCacheTransform(): CacheTransform {
is Context<*> -> when (name) {
"getPrev" -> {
args.getIntOrNull()?.let { target.getPrev(it) }
args.getInt().let { target.getPrev(it) }
}
"getChild" -> {
args.getIntOrNull()?.let { index ->
args.getInt().let { index ->
cache.getChild(target.current as AccessibilityNodeInfo, index)
}
}
@ -545,9 +543,7 @@ fun createCacheTransform(): CacheTransform {
return CacheTransform(transform, cache)
}
private fun List<Any>.getIntOrNull(i: Int = 0): Int? {
return getOrNull(i) as? Int ?: return null
}
private fun List<Any>.getInt(i: Int = 0) = get(i) as Int
fun createNoCacheTransform(): CacheTransform {
val cache = NodeCache()
@ -569,7 +565,7 @@ fun createNoCacheTransform(): CacheTransform {
when (target) {
is AccessibilityNodeInfo -> when (name) {
"getChild" -> {
args.getIntOrNull()?.let { index ->
args.getInt().let { index ->
cache.getChild(target, index)
}
}
@ -579,11 +575,11 @@ fun createNoCacheTransform(): CacheTransform {
is Context<*> -> when (name) {
"getPrev" -> {
args.getIntOrNull()?.let { target.getPrev(it) }
args.getInt().let { target.getPrev(it) }
}
"getChild" -> {
args.getIntOrNull()?.let { index ->
args.getInt().let { index ->
cache.getChild(target.current as AccessibilityNodeInfo, index)
}
}

View File

@ -58,14 +58,18 @@ sealed class CompareOperator(val key: String) : Stringify {
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return compare(left, right)
}
override fun allowType(left: ValueExpression, right: ValueExpression) = true
fun compare(left: Any?, right: Any?): Boolean {
return if (left is CharSequence && right is CharSequence) {
left.contentReversedEquals(right)
} else {
left == right
}
}
override fun allowType(left: ValueExpression, right: ValueExpression) = true
}
data object NotEqual : CompareOperator("!=") {

View File

@ -28,7 +28,7 @@ data class UnknownMemberMethodException(
@JsExport
data class MismatchParamTypeException(
val call: ValueExpression.CallExpression,
val argument: ValueExpression.LiteralExpression,
val argument: ValueExpression,
val type: PrimitiveType
) : SelectorCheckException("Mismatch Param Type: ${argument.value} should be ${type.key}")

View File

@ -138,76 +138,129 @@ private fun getExpType(exp: ValueExpression, typeInfo: TypeInfo): PrimitiveType?
is ValueExpression.BooleanLiteral -> PrimitiveType.BooleanType
is ValueExpression.IntLiteral -> PrimitiveType.IntType
is ValueExpression.StringLiteral -> PrimitiveType.StringType
is ValueExpression.Variable -> checkVariable(exp, typeInfo).type
is ValueExpression.Variable -> checkVariable(exp, typeInfo, typeInfo).type
}
}
private fun checkVariable(value: ValueExpression.Variable, typeInfo: TypeInfo): TypeInfo {
private fun checkMethod(
method: MethodInfo,
value: ValueExpression.CallExpression,
globalTypeInfo: TypeInfo
): TypeInfo {
method.params.forEachIndexed { index, argTypeInfo ->
when (val argExp = value.arguments[index]) {
is ValueExpression.NullLiteral -> {}
is ValueExpression.BooleanLiteral -> {
if (argTypeInfo.type != PrimitiveType.BooleanType) {
throw MismatchParamTypeException(
value,
argExp,
PrimitiveType.BooleanType
)
}
}
is ValueExpression.IntLiteral -> {
if (argTypeInfo.type != PrimitiveType.IntType) {
throw MismatchParamTypeException(value, argExp, PrimitiveType.IntType)
}
}
is ValueExpression.StringLiteral -> {
if (argTypeInfo.type != PrimitiveType.StringType) {
throw MismatchParamTypeException(
value,
argExp,
PrimitiveType.StringType
)
}
}
is ValueExpression.Variable -> {
val type = checkVariable(argExp, argTypeInfo, globalTypeInfo)
if (type.type != argTypeInfo.type) {
throw MismatchParamTypeException(
value,
argExp,
type.type
)
}
}
}
}
return method.returnType
}
private fun checkVariable(
value: ValueExpression.Variable,
currentTypeInfo: TypeInfo,
globalTypeInfo: TypeInfo,
): TypeInfo {
return when (value) {
is ValueExpression.CallExpression -> {
val method = when (value.callee) {
val methods = when (value.callee) {
is ValueExpression.CallExpression -> {
throw IllegalArgumentException("Unsupported nested call")
}
is ValueExpression.Identifier -> {
// getChild(0)
typeInfo.methods.find { it.name == value.callee.value && it.params.size == value.arguments.size }
?: throw UnknownIdentifierMethodException(value.callee)
globalTypeInfo.methods.filter { it.name == value.callee.value && it.params.size == value.arguments.size }
.apply {
if (isEmpty()) {
throw UnknownIdentifierMethodException(value.callee)
}
}
}
is ValueExpression.MemberExpression -> {
// parent.getChild(0)
checkVariable(
value.callee.object0, typeInfo
).methods.find { it.name == value.callee.property && it.params.size == value.arguments.size }
?: throw UnknownMemberMethodException(value.callee)
value.callee.object0,
currentTypeInfo,
globalTypeInfo
).methods.filter { it.name == value.callee.property && it.params.size == value.arguments.size }
.apply {
if (isEmpty()) {
throw UnknownMemberMethodException(value.callee)
}
}
}
}
method.params.forEachIndexed { index, argTypeInfo ->
when (val argExp = value.arguments[index]) {
is ValueExpression.NullLiteral -> {}
is ValueExpression.BooleanLiteral -> {
if (argTypeInfo.type != PrimitiveType.BooleanType) {
throw MismatchParamTypeException(
value,
argExp,
PrimitiveType.BooleanType
)
}
}
is ValueExpression.IntLiteral -> {
if (argTypeInfo.type != PrimitiveType.IntType) {
throw MismatchParamTypeException(value, argExp, PrimitiveType.IntType)
}
}
is ValueExpression.StringLiteral -> {
if (argTypeInfo.type != PrimitiveType.StringType) {
throw MismatchParamTypeException(
value,
argExp,
PrimitiveType.StringType
)
}
}
is ValueExpression.Variable -> {
checkVariable(argExp, argTypeInfo)
if (methods.size == 1) {
checkMethod(methods[0], value, globalTypeInfo)
return methods[0].returnType
}
methods.forEachIndexed { i, method ->
try {
checkMethod(method, value, globalTypeInfo)
return method.returnType
} catch (e: SelectorCheckException) {
if (i == methods.size - 1) {
throw e
}
// ignore
}
}
return method.returnType
if (value.callee is ValueExpression.Identifier) {
throw UnknownIdentifierMethodException(value.callee)
} else if (value.callee is ValueExpression.MemberExpression) {
throw UnknownMemberMethodException(value.callee)
}
throw IllegalArgumentException("Unsupported nested call")
}
is ValueExpression.Identifier -> {
typeInfo.props.find { it.name == value.value }?.type
globalTypeInfo.props.find { it.name == value.value }?.type
?: throw UnknownIdentifierException(value)
}
is ValueExpression.MemberExpression -> {
checkVariable(value.object0, typeInfo).props.find { it.name == value.property }?.type
checkVariable(
value.object0,
currentTypeInfo,
globalTypeInfo
).props.find { it.name == value.property }?.type
?: throw UnknownMemberException(value)
}
}

View File

@ -15,7 +15,11 @@ data class MethodInfo(
val name: String,
val returnType: TypeInfo,
val params: Array<TypeInfo> = emptyArray(),
) {
) : Stringify {
override fun stringify(): String {
return "$name(${params.joinToString(", ") { it.stringify() }}): ${returnType.stringify()}"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
@ -48,7 +52,15 @@ data class TypeInfo(
val type: PrimitiveType,
var props: Array<PropInfo> = arrayOf(),
var methods: Array<MethodInfo> = arrayOf(),
) {
) : Stringify {
override fun stringify(): String {
return if (type is PrimitiveType.ObjectType) {
type.name
} else {
type.key
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

View File

@ -30,6 +30,9 @@ sealed class ValueExpression(open val value: Any?, open val type: String) : Posi
get() = arrayOf(value)
override val methods: Array<String>
get() = emptyArray()
val isEqual = name == "equal"
val isNotEqual = name == "notEqual"
}
data class MemberExpression internal constructor(
@ -52,6 +55,10 @@ sealed class ValueExpression(open val value: Any?, open val type: String) : Posi
get() = arrayOf(*object0.properties, property)
override val methods: Array<String>
get() = object0.methods
val isPropertyOr = property == "or"
val isPropertyAnd = property == "and"
val isPropertyIfElse = property == "ifElse"
}
data class CallExpression internal constructor(
@ -74,23 +81,66 @@ sealed class ValueExpression(open val value: Any?, open val type: String) : Posi
}
is Identifier -> {
transform.getInvoke(
context,
callee.name,
arguments.map {
it.getAttr(context, transform).whenNull { return null }
when {
callee.isEqual -> {
CompareOperator.Equal.compare(
arguments[0].getAttr(context, transform),
arguments[1].getAttr(context, transform)
)
}
)
callee.isNotEqual -> {
!CompareOperator.Equal.compare(
arguments[0].getAttr(context, transform),
arguments[1].getAttr(context, transform)
)
}
else -> {
transform.getInvoke(
context,
callee.name,
arguments.map {
it.getAttr(context, transform).whenNull { return null }
}
)
}
}
}
is MemberExpression -> {
transform.getInvoke(
callee.object0.getAttr(context, transform).whenNull { return null },
callee.property,
arguments.map {
it.getAttr(context, transform).whenNull { return null }
val objectValue =
callee.object0.getAttr(context, transform).whenNull { return null }
when {
callee.isPropertyOr -> {
(objectValue as Boolean) ||
(arguments[0].getAttr(context, transform)
.whenNull { return null } as Boolean)
}
)
callee.isPropertyAnd -> {
(objectValue as Boolean) &&
(arguments[0].getAttr(context, transform)
.whenNull { return null } as Boolean)
}
callee.isPropertyIfElse -> {
if (objectValue as Boolean) {
arguments[0].getAttr(context, transform)
} else {
arguments[1].getAttr(context, transform)
}
}
else -> transform.getInvoke(
objectValue,
callee.property,
arguments.map {
it.getAttr(context, transform).whenNull { return null }
}
)
}
}
}
}

View File

@ -77,17 +77,36 @@ class DefaultTypeInfo(
val stringType: TypeInfo,
val contextType: TypeInfo,
val nodeType: TypeInfo,
val globalType: TypeInfo
)
@JsExport
fun initDefaultTypeInfo(): DefaultTypeInfo {
fun initDefaultTypeInfo(webField: Boolean = false): DefaultTypeInfo {
val booleanType = TypeInfo(PrimitiveType.BooleanType)
val intType = TypeInfo(PrimitiveType.IntType)
val stringType = TypeInfo(PrimitiveType.StringType)
val nodeType = TypeInfo(PrimitiveType.ObjectType("node"))
val contextType = TypeInfo(PrimitiveType.ObjectType("context"))
val globalType = TypeInfo(PrimitiveType.ObjectType("global"))
fun buildMethods(name: String, returnType: TypeInfo, paramsSize: Int): Array<MethodInfo> {
return arrayOf(
MethodInfo(name, returnType, Array(paramsSize) { booleanType }),
MethodInfo(name, returnType, Array(paramsSize) { intType }),
MethodInfo(name, returnType, Array(paramsSize) { stringType }),
MethodInfo(name, returnType, Array(paramsSize) { nodeType }),
MethodInfo(name, returnType, Array(paramsSize) { contextType }),
)
}
booleanType.methods = arrayOf(
MethodInfo("toInt", intType),
MethodInfo("or", booleanType, arrayOf(booleanType)),
MethodInfo("and", booleanType, arrayOf(booleanType)),
MethodInfo("not", booleanType),
*buildMethods("ifElse", booleanType, 2),
)
intType.methods = arrayOf(
MethodInfo("toString", stringType),
MethodInfo("toString", stringType, arrayOf(intType)),
@ -96,6 +115,10 @@ fun initDefaultTypeInfo(): DefaultTypeInfo {
MethodInfo("times", intType, arrayOf(intType)),
MethodInfo("div", intType, arrayOf(intType)),
MethodInfo("rem", intType, arrayOf(intType)),
MethodInfo("more", booleanType, arrayOf(intType)),
MethodInfo("moreEqual", booleanType, arrayOf(intType)),
MethodInfo("less", booleanType, arrayOf(intType)),
MethodInfo("lessEqual", booleanType, arrayOf(intType)),
)
stringType.props = arrayOf(
PropInfo("length", intType),
@ -110,13 +133,15 @@ fun initDefaultTypeInfo(): DefaultTypeInfo {
MethodInfo("indexOf", intType, arrayOf(stringType)),
MethodInfo("indexOf", intType, arrayOf(stringType, intType)),
)
val contextType = TypeInfo(PrimitiveType.ObjectType("context"))
val nodeType = TypeInfo(PrimitiveType.ObjectType("node"))
nodeType.props = arrayOf(
PropInfo("_id", intType),
PropInfo("_pid", intType),
* (if (webField) {
arrayOf(
PropInfo("_id", intType),
PropInfo("_pid", intType),
)
} else {
emptyArray()
}),
PropInfo("id", stringType),
PropInfo("vid", stringType),
@ -155,13 +180,21 @@ fun initDefaultTypeInfo(): DefaultTypeInfo {
contextType.props = arrayOf(
*nodeType.props,
PropInfo("prev", contextType),
PropInfo("current", contextType),
)
globalType.methods = arrayOf(
*contextType.methods,
*buildMethods("equal", booleanType, 2),
*buildMethods("notEqual", booleanType, 2),
)
globalType.props = arrayOf(*contextType.props)
return DefaultTypeInfo(
booleanType = booleanType,
intType = intType,
stringType = stringType,
contextType = contextType,
nodeType = nodeType
nodeType = nodeType,
globalType = globalType
)
}
@ -169,23 +202,39 @@ fun initDefaultTypeInfo(): DefaultTypeInfo {
fun getIntInvoke(target: Int, name: String, args: List<Any>): Any? {
return when (name) {
"plus" -> {
target + (args.getIntOrNull() ?: return null)
target + args.getInt()
}
"minus" -> {
target - (args.getIntOrNull() ?: return null)
target - args.getInt()
}
"times" -> {
target * (args.getIntOrNull() ?: return null)
target * args.getInt()
}
"div" -> {
target / (args.getIntOrNull()?.also { if (it == 0) return null } ?: return null)
target / args.getInt().also { if (it == 0) return null }
}
"rem" -> {
target % (args.getIntOrNull()?.also { if (it == 0) return null } ?: return null)
target % args.getInt().also { if (it == 0) return null }
}
"more" -> {
target > args.getInt()
}
"moreEqual" -> {
target >= args.getInt()
}
"less" -> {
target < args.getInt()
}
"lessEqual" -> {
target <= args.getInt()
}
else -> null
@ -193,11 +242,7 @@ fun getIntInvoke(target: Int, name: String, args: List<Any>): Any? {
}
internal fun List<Any>.getIntOrNull(i: Int = 0): Int? {
val v = getOrNull(i)
if (v is Int) return v
return null
}
internal fun List<Any>.getInt(i: Int = 0) = get(i) as Int
@JsExport
fun getStringInvoke(target: String, name: String, args: List<Any>): Any? {
@ -208,6 +253,7 @@ fun getStringInvoke(target: String, name: String, args: List<Any>): Any? {
fun getBooleanInvoke(target: Boolean, name: String, args: List<Any>): Any? {
return when (name) {
"toInt" -> if (target) 1 else 0
"not" -> !target
else -> null
}
}
@ -215,11 +261,11 @@ fun getBooleanInvoke(target: Boolean, name: String, args: List<Any>): Any? {
fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any>): Any? {
return when (name) {
"get" -> {
target.getOrNull(args.getIntOrNull() ?: return null).toString()
target.getOrNull(args.getInt()).toString()
}
"at" -> {
val i = args.getIntOrNull() ?: return null
val i = args.getInt()
if (i < 0) {
target.getOrNull(target.length + i).toString()
} else {
@ -230,7 +276,7 @@ fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any>):
"substring" -> {
when (args.size) {
1 -> {
val start = args.getIntOrNull() ?: return null
val start = args.getInt()
if (start < 0) return null
if (start >= target.length) return ""
target.substring(
@ -240,10 +286,10 @@ fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any>):
}
2 -> {
val start = args.getIntOrNull() ?: return null
val start = args.getInt()
if (start < 0) return null
if (start >= target.length) return ""
val end = args.getIntOrNull(1) ?: return null
val end = args.getInt(1)
if (end < start) return null
target.substring(
start,
@ -260,7 +306,7 @@ fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any>):
"toInt" -> when (args.size) {
0 -> target.toString().toIntOrNull()
1 -> {
val radix = args.getIntOrNull() ?: return null
val radix = args.getInt()
if (radix !in 2..36) {
return null
}
@ -279,7 +325,7 @@ fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any>):
2 -> {
val str = args[0] as? CharSequence ?: return null
val startIndex = args.getIntOrNull(1) ?: return null
val startIndex = args.getInt(1)
target.indexOf(str.toString(), startIndex)
}

View File

@ -37,7 +37,7 @@ class ParserTest {
private fun getNodeInvoke(target: TestNode, name: String, args: List<Any>): Any? {
when (name) {
"getChild" -> {
val arg = (args.getIntOrNull() ?: return null)
val arg = args.getInt()
return target.children.getOrNull(arg)
}
}
@ -49,6 +49,7 @@ class ParserTest {
when (target) {
is Context<*> -> when (name) {
"prev" -> target.prev
"current" -> target.current
else -> getNodeAttr(target.current as TestNode, name)
}
@ -66,7 +67,7 @@ class ParserTest {
is TestNode -> getNodeInvoke(target, name, args)
is Context<*> -> when (name) {
"getPrev" -> {
args.getIntOrNull()?.let { target.getPrev(it) }
args.getInt().let { target.getPrev(it) }
}
else -> getNodeInvoke(target.current as TestNode, name, args)
@ -168,7 +169,8 @@ class ParserTest {
@Test
fun check_query() {
val text = "@TextView[getPrev(0).text=`签到提醒`] - [text=`签到提醒`] <<n [vid=`webViewContainer`]"
val text =
"@TextView[getPrev(0).text=`签到提醒`] - [text=`签到提醒`] <<n [vid=`webViewContainer`]"
val selector = Selector.parse(text)
println("selector: $selector")
println(selector.targetIndex)
@ -231,11 +233,16 @@ class ParserTest {
@Test
fun check_regex() {
val source = "[1<parent.getChild][vid=`im_cover`]"
val source = "[vid=`im_cover`][top.more(319).not()=true]"
println("source:$source")
val selector = Selector.parse(source)
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/14445410")
println("selector:$selector")
val error = selector.checkType(typeInfo)
if (error != null) {
println("error:$error")
return
}
println("selector:${selector.stringify()}")
println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id }
.toList())
}
@ -256,20 +263,21 @@ class ParserTest {
)
}
private val typeInfo by lazy { initDefaultTypeInfo(webField = true).globalType }
@Test
fun check_type() {
val source =
"[prev!=null&&visibleToUser=true][((parent.getChild(0,).getChild( (0), )=null) && (((2 >= 1)))) || (name=null && desc=null)]"
"[prev!=null&&visibleToUser=true&&equal(index, depth)=true][((parent.getChild(0,).getChild( (0), )=null) && (((2 >= 1)))) || (name=null && desc=null)]"
val selector = Selector.parse(source)
val typeInfo = initDefaultTypeInfo().contextType
val error = selector.checkType(typeInfo)
println("useCache: ${selector.useCache}")
println("error: $error")
println("check_type: $selector")
println("check_type: ${selector.stringify()}")
}
@Test
fun check_qf(){
fun check_qf() {
val source = "@UIView[clickable=true] -3 FlattenUIText[text=`a`||text=`b`||vid=`233`]"
val selector = Selector.parse(source)
println("fastQuery: ${selector.fastQueryList}")