mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-10 18:40:15 +08:00
Redesign Requirement parsing
This commit is contained in:
parent
3b39be6bdc
commit
635d0bfdec
@ -1,250 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.mamoe.mirai.console.internal.util.semver
|
||||
|
||||
import net.mamoe.mirai.console.util.SemVersion
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
internal object RangeTokenReader {
|
||||
enum class TokenType {
|
||||
STRING,
|
||||
|
||||
/* 左括号 */
|
||||
LEFT,
|
||||
|
||||
/* 右括号 */
|
||||
RIGHT,
|
||||
|
||||
/* || */
|
||||
OR,
|
||||
|
||||
/* && */
|
||||
AND,
|
||||
GROUP
|
||||
}
|
||||
|
||||
sealed class Token {
|
||||
abstract val type: TokenType
|
||||
abstract val value: String
|
||||
abstract val position: Int
|
||||
|
||||
class LeftBracket(override val position: Int) : Token() {
|
||||
override val type: TokenType get() = TokenType.LEFT
|
||||
override val value: String get() = "{"
|
||||
|
||||
override fun toString(): String = "LB{"
|
||||
}
|
||||
|
||||
class RightBracket(override val position: Int) : Token() {
|
||||
override val type: TokenType get() = TokenType.RIGHT
|
||||
override val value: String get() = "}"
|
||||
|
||||
override fun toString(): String = "RB}"
|
||||
}
|
||||
|
||||
class Or(override val position: Int) : Token() {
|
||||
override val type: TokenType get() = TokenType.OR
|
||||
override val value: String get() = "||"
|
||||
override fun toString(): String = "OR||"
|
||||
}
|
||||
|
||||
class And(override val position: Int) : Token() {
|
||||
override val type: TokenType get() = TokenType.AND
|
||||
override val value: String get() = "&&"
|
||||
|
||||
override fun toString(): String = "AD&&"
|
||||
}
|
||||
|
||||
class Group(val values: List<Token>, override val position: Int) : Token() {
|
||||
override val type: TokenType get() = TokenType.GROUP
|
||||
override val value: String get() = ""
|
||||
}
|
||||
|
||||
class Raw(val source: String, val start: Int, val end: Int) : Token() {
|
||||
override val value: String get() = source.substring(start, end)
|
||||
override val position: Int
|
||||
get() = start
|
||||
override val type: TokenType get() = TokenType.STRING
|
||||
|
||||
override fun toString(): String = "R:$value"
|
||||
}
|
||||
}
|
||||
|
||||
fun parseToTokens(source: String): List<Token> = ArrayList<Token>(
|
||||
max(source.length / 3, 16)
|
||||
).apply {
|
||||
var index = 0
|
||||
var position = 0
|
||||
fun flushOld() {
|
||||
if (position > index) {
|
||||
val id = index
|
||||
index = position
|
||||
for (i in id until position) {
|
||||
if (!source[i].isWhitespace()) {
|
||||
add(Token.Raw(source, id, position))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val iterator = source.indices.iterator()
|
||||
for (i in iterator) {
|
||||
position = i
|
||||
when (source[i]) {
|
||||
'{' -> {
|
||||
flushOld()
|
||||
add(Token.LeftBracket(i))
|
||||
index = i + 1
|
||||
}
|
||||
'|' -> {
|
||||
if (source.getOrNull(i + 1) == '|') {
|
||||
flushOld()
|
||||
add(Token.Or(i))
|
||||
index = i + 2
|
||||
iterator.nextInt()
|
||||
}
|
||||
}
|
||||
'&' -> {
|
||||
if (source.getOrNull(i + 1) == '&') {
|
||||
flushOld()
|
||||
add(Token.And(i))
|
||||
index = i + 2
|
||||
iterator.nextInt()
|
||||
}
|
||||
}
|
||||
'}' -> {
|
||||
flushOld()
|
||||
add(Token.RightBracket(i))
|
||||
index = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
position = source.length
|
||||
flushOld()
|
||||
}
|
||||
|
||||
fun collect(source: String, tokens: Iterator<Token>, root: Boolean): List<Token> = ArrayList<Token>().apply {
|
||||
tokens.forEach { token ->
|
||||
if (token is Token.LeftBracket) {
|
||||
add(Token.Group(collect(source, tokens, false), token.position))
|
||||
} else if (token is Token.RightBracket) {
|
||||
if (root) {
|
||||
throw IllegalArgumentException("Syntax error: Unexpected }, ${buildMsg(source, token.position)}")
|
||||
} else {
|
||||
return@apply
|
||||
}
|
||||
} else add(token)
|
||||
}
|
||||
if (!root) {
|
||||
throw IllegalArgumentException("Syntax error: Excepted }, ${buildMsg(source, source.length)}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMsg(source: String, position: Int): String {
|
||||
val ed = min(position + 10, source.length)
|
||||
val st = max(0, position - 10)
|
||||
return buildString {
|
||||
append('`')
|
||||
if (st != 0) append("...")
|
||||
append(source, st, ed)
|
||||
if (ed != source.length) append("...")
|
||||
append("` at ").append(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun check(source: String, tokens: Iterator<Token>, group: Token.Group?) {
|
||||
if (!tokens.hasNext()) {
|
||||
throw IllegalArgumentException("Syntax error: empty rule, ${buildMsg(source, group?.position ?: 0)}")
|
||||
}
|
||||
var type = false
|
||||
do {
|
||||
val next = tokens.next()
|
||||
if (type) {
|
||||
if (next is Token.Group || next is Token.Raw) {
|
||||
throw IllegalArgumentException("Syntax error: Except logic but got expression, ${buildMsg(source, next.position)}")
|
||||
}
|
||||
} else {
|
||||
if (next is Token.Or || next is Token.And) {
|
||||
throw IllegalArgumentException("Syntax error: Except expression but got logic, ${buildMsg(source, next.position)}")
|
||||
}
|
||||
if (next is Token.Group) {
|
||||
check(source, next.values.iterator(), next)
|
||||
}
|
||||
}
|
||||
type = !type
|
||||
} while (tokens.hasNext())
|
||||
if (!type) {
|
||||
throw IllegalArgumentException("Syntax error: Except more expression, ${buildMsg(source, group?.values?.last()?.position ?: source.length)}")
|
||||
}
|
||||
}
|
||||
|
||||
fun parse(source: String, token: Token): RequirementInternal {
|
||||
return when (token) {
|
||||
is Token.Group -> {
|
||||
if (token.values.size == 1) {
|
||||
parse(source, token.values.first())
|
||||
} else {
|
||||
val logic = token.values.asSequence().map { it.type }.filter {
|
||||
it == TokenType.OR || it == TokenType.AND
|
||||
}.toSet()
|
||||
if (logic.size == 2) {
|
||||
throw IllegalArgumentException("Syntax error: || and && cannot use in one group, ${buildMsg(source, token.position)}")
|
||||
}
|
||||
val rules = token.values.asSequence().filter {
|
||||
it is Token.Raw || it is Token.Group
|
||||
}.map { parse(source, it) }.toList()
|
||||
when (logic.first()) {
|
||||
TokenType.OR -> {
|
||||
return object : RequirementInternal {
|
||||
override fun test(version: SemVersion): Boolean {
|
||||
rules.forEach { if (it.test(version)) return true }
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
TokenType.AND -> {
|
||||
return object : RequirementInternal {
|
||||
override fun test(version: SemVersion): Boolean {
|
||||
rules.forEach { if (!it.test(version)) return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
}
|
||||
is Token.Raw -> SemVersionInternal.parseRule(token.value)
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
fun StringBuilder.dump(prefix: String, token: Token) {
|
||||
when (token) {
|
||||
is Token.LeftBracket -> append("${prefix}LF {\n")
|
||||
|
||||
is Token.RightBracket -> append("${prefix}LR }\n")
|
||||
|
||||
is Token.Or -> append("${prefix}OR ||\n")
|
||||
|
||||
is Token.And -> append("${prefix}AND &&\n")
|
||||
is Token.Group -> {
|
||||
append("${prefix}GROUP {\n")
|
||||
token.values.forEach { dump("$prefix ", it) }
|
||||
append("${prefix}}\n")
|
||||
}
|
||||
is Token.Raw -> append("${prefix}RAW ${token.value}\n")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright 2019-2020 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link.
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.console.internal.util.semver
|
||||
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
internal class RequirementParser {
|
||||
sealed class Token {
|
||||
open var line: Int = -1
|
||||
open var pos: Int = -1
|
||||
open var sourcePos: Int = -1
|
||||
open lateinit var content: String
|
||||
|
||||
sealed class GroupBod : Token() {
|
||||
class Left : GroupBod() {
|
||||
override var content: String
|
||||
get() = "{"
|
||||
set(_) {}
|
||||
}
|
||||
|
||||
class Right : GroupBod() {
|
||||
override var content: String
|
||||
get() = "}"
|
||||
set(_) {}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Logic : Token() {
|
||||
class And : Logic() {
|
||||
override var content: String
|
||||
get() = "&&"
|
||||
set(_) {}
|
||||
}
|
||||
|
||||
class Or : Logic() {
|
||||
override var content: String
|
||||
get() = "||"
|
||||
set(_) {}
|
||||
}
|
||||
}
|
||||
|
||||
class Content : Token()
|
||||
class Ending : Token() {
|
||||
override var content: String
|
||||
get() = ""
|
||||
set(_) {}
|
||||
}
|
||||
|
||||
object Begin : Token() {
|
||||
override var content: String
|
||||
get() = ""
|
||||
set(_) {}
|
||||
override var line: Int
|
||||
get() = 0
|
||||
set(_) {}
|
||||
override var pos: Int
|
||||
get() = 0
|
||||
set(_) {}
|
||||
override var sourcePos: Int
|
||||
get() = 0
|
||||
set(_) {}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return javaClass.canonicalName.substringAfterLast('.') + " - $content [$line, $pos]"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val END = '\u0000'
|
||||
}
|
||||
|
||||
class TokenReader(
|
||||
@JvmField val content: String
|
||||
) {
|
||||
@JvmField
|
||||
var pos: Int = 0
|
||||
|
||||
@JvmField
|
||||
var line: Int = 0
|
||||
|
||||
@JvmField
|
||||
var posi: Int = 0
|
||||
|
||||
@JvmField
|
||||
var latestToken: Token = Token.Begin
|
||||
|
||||
@JvmField
|
||||
var insertToken: Token? = Token.Begin
|
||||
fun peekChar(): Char {
|
||||
if (pos < content.length)
|
||||
return content[pos]
|
||||
return END
|
||||
}
|
||||
|
||||
fun peekNextChar(): Char {
|
||||
if (pos + 1 < content.length)
|
||||
return content[pos + 1]
|
||||
return END
|
||||
}
|
||||
|
||||
fun nextChar(): Char {
|
||||
val char = peekChar()
|
||||
pos++
|
||||
if (char == '\n') {
|
||||
line++
|
||||
posi = 0
|
||||
} else {
|
||||
posi++
|
||||
}
|
||||
return char
|
||||
}
|
||||
|
||||
fun nextToken(): Token {
|
||||
insertToken?.let { insertToken = null; return it }
|
||||
return nextToken0().also { latestToken = it }
|
||||
}
|
||||
|
||||
private fun nextToken0(): Token {
|
||||
if (pos < content.length) {
|
||||
while (peekChar().isWhitespace()) {
|
||||
nextChar()
|
||||
}
|
||||
val startIndex = pos
|
||||
if (startIndex >= content.length) {
|
||||
return Token.Ending().also {
|
||||
it.line = line
|
||||
it.pos = posi
|
||||
it.sourcePos = content.length
|
||||
}
|
||||
}
|
||||
val pline = line
|
||||
val ppos = posi
|
||||
nextChar()
|
||||
when (content[startIndex]) {
|
||||
'&' -> {
|
||||
if (peekChar() == '&') {
|
||||
return Token.Logic.And().also {
|
||||
it.pos = ppos
|
||||
it.line = pline
|
||||
it.sourcePos = startIndex
|
||||
nextChar()
|
||||
}
|
||||
}
|
||||
}
|
||||
'|' -> {
|
||||
if (peekChar() == '|') {
|
||||
return Token.Logic.Or().also {
|
||||
nextChar()
|
||||
it.pos = ppos
|
||||
it.line = pline
|
||||
it.sourcePos = startIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
'{' -> {
|
||||
return Token.GroupBod.Left().also {
|
||||
it.pos = ppos
|
||||
it.line = pline
|
||||
it.sourcePos = startIndex
|
||||
}
|
||||
}
|
||||
'}' -> {
|
||||
return Token.GroupBod.Right().also {
|
||||
it.pos = ppos
|
||||
it.line = pline
|
||||
it.sourcePos = startIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
when (val c = peekChar()) {
|
||||
'&', '|' -> {
|
||||
if (c == peekNextChar()) {
|
||||
break
|
||||
}
|
||||
nextChar()
|
||||
}
|
||||
'{', '}' -> {
|
||||
break
|
||||
}
|
||||
END -> break
|
||||
else -> nextChar()
|
||||
}
|
||||
}
|
||||
val endIndex = pos
|
||||
return Token.Content().also {
|
||||
it.content = content.substring(startIndex, endIndex)
|
||||
it.pos = ppos
|
||||
it.line = pline
|
||||
it.sourcePos = startIndex
|
||||
}
|
||||
}
|
||||
return Token.Ending().also {
|
||||
it.line = line
|
||||
it.pos = posi
|
||||
it.sourcePos = content.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TokensProcessor<R> {
|
||||
fun process(reader: TokenReader): R
|
||||
fun processLine(reader: TokenReader): R
|
||||
fun processLogic(isAnd: Boolean, chunks: Iterable<R>): R
|
||||
}
|
||||
|
||||
abstract class ProcessorBase<R> : TokensProcessor<R> {
|
||||
fun Token.ia(reader: TokenReader, msg: String, cause: Throwable? = null): Nothing {
|
||||
throw IllegalArgumentException("$msg (at [$line, $pos], ${cutSource(reader, sourcePos)})", cause)
|
||||
}
|
||||
|
||||
fun cutSource(reader: TokenReader, index: Int): String {
|
||||
val content = reader.content
|
||||
val s = max(0, index - 10)
|
||||
val e = min(content.length, index + 10)
|
||||
return content.substring(s, e)
|
||||
}
|
||||
|
||||
override fun process(reader: TokenReader): R {
|
||||
return when (val nextToken = reader.nextToken()) {
|
||||
is Token.Begin,
|
||||
is Token.GroupBod.Left -> {
|
||||
val first = when (val next = reader.nextToken()) {
|
||||
is Token.Content -> {
|
||||
processString(reader, next)
|
||||
}
|
||||
is Token.GroupBod.Right -> {
|
||||
nextToken.ia(
|
||||
reader, if (nextToken is Token.Begin)
|
||||
"Invalid token `}`"
|
||||
else "The first token cannot be Group Ending"
|
||||
)
|
||||
}
|
||||
is Token.Logic -> {
|
||||
nextToken.ia(reader, "The first token cannot be Token.Logic")
|
||||
}
|
||||
is Token.Ending -> {
|
||||
nextToken.ia(
|
||||
reader, if (nextToken is Token.Begin)
|
||||
"Requirement cannot be blank"
|
||||
else "Except more tokens"
|
||||
)
|
||||
}
|
||||
is Token.GroupBod.Left -> {
|
||||
reader.insertToken = next
|
||||
process(reader)
|
||||
}
|
||||
else -> {
|
||||
next.ia(reader, "Bad token $next")
|
||||
}
|
||||
}
|
||||
// null -> not set
|
||||
// true -> AND mode
|
||||
// false-> OR mode
|
||||
var mode: Boolean? = null
|
||||
val chunks = arrayListOf(first)
|
||||
while (true) {
|
||||
when (val next = reader.nextToken()) {
|
||||
is Token.Ending,
|
||||
is Token.GroupBod.Right -> {
|
||||
val isEndingOfGroup = next is Token.GroupBod.Right
|
||||
val isStartingOfGroup = nextToken is Token.GroupBod.Left
|
||||
if (isStartingOfGroup != isEndingOfGroup) {
|
||||
fun getType(type: Boolean) = if (type) "`}`" else "<EOF>"
|
||||
next.ia(reader, "Except ${getType(isStartingOfGroup)} but got ${getType(isEndingOfGroup)}")
|
||||
} else {
|
||||
// reader.insertToken = next
|
||||
break
|
||||
}
|
||||
}
|
||||
is Token.Logic -> {
|
||||
val stx = next is Token.Logic.And
|
||||
if (mode == null) mode = stx
|
||||
else if (mode != stx) {
|
||||
fun getMode(type: Boolean) = if (type) "`&&`" else "`||`"
|
||||
next.ia(
|
||||
reader, "Cannot change logic mode after setting. " +
|
||||
"Except ${getMode(mode)} but got ${getMode(stx)}"
|
||||
)
|
||||
}
|
||||
chunks.add(process(reader))
|
||||
}
|
||||
else -> {
|
||||
next.ia(
|
||||
reader, "Except ${
|
||||
when (mode) {
|
||||
null -> "`&&` or `||`"
|
||||
true -> "`&&`"
|
||||
false -> "`||`"
|
||||
}
|
||||
} but get `${next.content}`"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mode == null) {
|
||||
first
|
||||
} else {
|
||||
processLogic(mode, chunks)
|
||||
}
|
||||
}
|
||||
is Token.Content -> {
|
||||
processString(reader, nextToken)
|
||||
}
|
||||
is Token.Ending -> {
|
||||
nextToken.ia(reader, "Except more values.")
|
||||
}
|
||||
else -> {
|
||||
nextToken.ia(reader, "Assert Error: $nextToken")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun processString(reader: TokenReader, token: Token.Content): R
|
||||
|
||||
|
||||
override fun processLine(reader: TokenReader): R {
|
||||
return process(reader).also {
|
||||
val tok = reader.nextToken()
|
||||
if (reader.nextToken() !is Token.Ending) {
|
||||
tok.ia(reader, "Token reader stream not done")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@
|
||||
|
||||
package net.mamoe.mirai.console.internal.util.semver
|
||||
|
||||
import net.mamoe.mirai.console.internal.util.semver.RangeTokenReader.dump
|
||||
import net.mamoe.mirai.console.util.SemVersion
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@ -166,21 +165,27 @@ internal object SemVersionInternal {
|
||||
|
||||
|
||||
@JvmStatic
|
||||
fun parseRangeRequirement(requirement: String): RequirementInternal {
|
||||
if (requirement.isBlank()) {
|
||||
throw IllegalArgumentException("Invalid requirement: Empty requirement rule.")
|
||||
}
|
||||
val tokens = RangeTokenReader.parseToTokens(requirement)
|
||||
val collected = RangeTokenReader.collect(requirement, tokens.iterator(), true)
|
||||
RangeTokenReader.check(requirement, collected.iterator(), null)
|
||||
return kotlin.runCatching {
|
||||
RangeTokenReader.parse(requirement, RangeTokenReader.Token.Group(collected, 0))
|
||||
}.onFailure { error ->
|
||||
throw IllegalArgumentException("Exception in parsing $requirement\n\n" + buildString {
|
||||
collected.forEach { dump("", it) }
|
||||
}, error)
|
||||
}.getOrThrow()
|
||||
}
|
||||
fun parseRangeRequirement(requirement: String): RequirementInternal =
|
||||
object : RequirementParser.ProcessorBase<RequirementInternal>() {
|
||||
override fun processLogic(isAnd: Boolean, chunks: Iterable<RequirementInternal>): RequirementInternal {
|
||||
return if (isAnd) object : RequirementInternal {
|
||||
override fun test(version: SemVersion): Boolean {
|
||||
return chunks.all { it.test(version) }
|
||||
}
|
||||
} else object : RequirementInternal {
|
||||
override fun test(version: SemVersion): Boolean {
|
||||
return chunks.any { it.test(version) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun processString(
|
||||
reader: RequirementParser.TokenReader,
|
||||
token: RequirementParser.Token.Content
|
||||
): RequirementInternal = kotlin.runCatching {
|
||||
parseRule(token.content)
|
||||
}.getOrElse { token.ia(reader, "Error in parsing rule `${token.content}`", it) }
|
||||
}.processLine(RequirementParser.TokenReader(requirement))
|
||||
|
||||
@JvmStatic
|
||||
fun compareInternal(source: SemVersion, other: SemVersion): Int {
|
||||
|
Loading…
Reference in New Issue
Block a user