瀏覽代碼

Redesign Requirement parsing

Karlatemp 5 年之前
父節點
當前提交
635d0bfdec

+ 0 - 250
backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt

@@ -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")
-        }
-    }
-}

+ 335 - 0
backend/mirai-console/src/internal/util/semver/RequirementParser.kt

@@ -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")
+                }
+            }
+        }
+    }
+}

+ 21 - 16
backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt

@@ -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 {