Merge branch 'requirement' into 'master'

Close #225
This commit is contained in:
Karlatemp 2020-11-29 10:09:48 +08:00
commit df3418ef5b
No known key found for this signature in database
GPG Key ID: 21FBDDF664FF06F8
9 changed files with 771 additions and 271 deletions

View File

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

View File

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

View File

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

View File

@ -51,6 +51,9 @@ object MiraiConsoleErrors {
@JvmField
val ILLEGAL_PERMISSION_REGISTER_USE = create<PsiElement, KtNamedDeclaration, String>(ERROR)
@JvmField
val ILLEGAL_VERSION_REQUIREMENT = create<PsiElement, String, String>(ERROR)
@Suppress("ObjectPropertyName", "unused")
@JvmField
@Deprecated("", level = DeprecationLevel.ERROR)

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.IL
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_NAMESPACE
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_REGISTER_USE
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_VERSION_REQUIREMENT
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.NOT_CONSTRUCTABLE_TYPE
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.UNSERIALIZABLE_TYPE
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages
@ -87,6 +88,13 @@ object MiraiConsoleErrorsRendering : DefaultErrorMessages.Extension {
Renderers.DECLARATION_NAME,
Renderers.STRING
)
put(
ILLEGAL_VERSION_REQUIREMENT,
"{1}",
Renderers.STRING,
Renderers.STRING
)
}
override fun getMap() = MAP

View File

@ -8,6 +8,7 @@ import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermissionService
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
import net.mamoe.mirai.console.util.SemVersion
const val T = "org.example" // 编译期常量
@ -24,6 +25,18 @@ object MyPluginMain : KotlinPlugin(
PermissionService.INSTANCE.register(permissionId("dvs"), "ok")
PermissionService.INSTANCE.register(permissionId("perm with space"), "error")
PermissionId("Namespace with space", "Name with space")
SemVersion.parseRangeRequirement("")
SemVersion.parseRangeRequirement("<br/>")
SemVersion.parseRangeRequirement("SB YELLOW")
SemVersion.parseRangeRequirement("1.0.0 || 2.0.0 || ")
SemVersion.parseRangeRequirement("1.0.0 || 2.0.0")
SemVersion.parseRangeRequirement("1.0.0 || 2.0.0 && 3.0.0")
SemVersion.parseRangeRequirement("{}")
SemVersion.parseRangeRequirement("||")
SemVersion.parseRangeRequirement(">= 114.514 || = 1919.810 || (1.1, 1.2)")
SemVersion.parseRangeRequirement("0.0.0 || {90.48}")
SemVersion.parseRangeRequirement("{114514.1919810}")
SemVersion.parseRangeRequirement("}")
}
fun test() {

View File

@ -15,11 +15,14 @@ import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.IL
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_NAME
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PERMISSION_NAMESPACE
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_PLUGIN_DESCRIPTION
import net.mamoe.mirai.console.compiler.common.diagnostics.MiraiConsoleErrors.ILLEGAL_VERSION_REQUIREMENT
import net.mamoe.mirai.console.compiler.common.resolve.ResolveContextKind
import net.mamoe.mirai.console.compiler.common.resolve.resolveContextKinds
import net.mamoe.mirai.console.intellij.resolve.resolveAllCalls
import net.mamoe.mirai.console.intellij.resolve.resolveStringConstantValues
import net.mamoe.mirai.console.intellij.resolve.valueParametersWithArguments
import net.mamoe.mirai.console.intellij.util.RequirementHelper
import net.mamoe.mirai.console.intellij.util.RequirementParser
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.psi.KtDeclaration
@ -45,8 +48,10 @@ class ContextualParametersChecker : DeclarationChecker {
fun checkPluginId(inspectionTarget: PsiElement, value: String): Diagnostic? {
if (value.isBlank()) return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件 Id 不能为空. \n插件 Id$syntax")
if (value.none { it == '.' }) return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget,
"插件 Id '$value' 无效. 插件 Id 必须同时包含 groupId 和插件名称. $syntax")
if (value.none { it == '.' }) return ILLEGAL_PLUGIN_DESCRIPTION.on(
inspectionTarget,
"插件 Id '$value' 无效. 插件 Id 必须同时包含 groupId 和插件名称. $syntax"
)
val lowercaseId = value.toLowerCase()
@ -115,9 +120,12 @@ class ContextualParametersChecker : DeclarationChecker {
@Suppress("UNUSED_PARAMETER")
fun checkVersionRequirement(inspectionTarget: PsiElement, value: String): Diagnostic? {
// TODO: 2020/10/23 checkVersionRequirement
// 实现: 先在 MiraiConsoleErrors 添加一个 error, 再检测 value 并 report 一个错误.
return null
return try {
RequirementHelper.RequirementChecker.processLine(RequirementParser.TokenReader(value))
null
} catch (err: Throwable) {
ILLEGAL_VERSION_REQUIREMENT.on(inspectionTarget, value, err.message ?: err.toString())
}
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.intellij.util
@Suppress("RegExpRedundantEscape")
object RequirementHelper {
private val directVersion = """^[0-9]+(\.[0-9]+)+(|[\-+].+)$""".toRegex()
private val versionSelect = """^[0-9]+(\.[0-9]+)*\.x$""".toRegex()
private val versionMathRange =
"""([\[\(])([0-9]+(\.[0-9]+)+(|[\-+].+))\s*\,\s*([0-9]+(\.[0-9]+)+(|[\-+].+))([\]\)])""".toRegex()
private val versionRule = """^((\>\=)|(\<\=)|(\=)|(\!\=)|(\>)|(\<))\s*([0-9]+(\.[0-9]+)+(|[\-+].+))$""".toRegex()
private val SEM_VERSION_REGEX =
"""^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$""".toRegex()
fun isValid(rule: String): Boolean {
return rule.trim().let {
directVersion.matches(it) ||
versionSelect.matches(it) ||
versionMathRange.matches(it) ||
versionRule.matches(it)
}
}
internal object RequirementChecker : RequirementParser.ProcessorBase<Unit>() {
override fun processLogic(isAnd: Boolean, chunks: Iterable<Unit>) {
}
override fun processString(reader: RequirementParser.TokenReader, token: RequirementParser.Token.Content) {
if (!isValid(token.content)) {
token.ia(reader, "`${token.content}` 无效.")
}
}
}
}

View File

@ -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.intellij.util
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)
"无效的关键字 `}`"
else "空规则组"
)
}
is Token.Logic -> {
nextToken.ia(reader, "规则不允许以逻辑操作符开始")
}
is Token.Ending -> {
nextToken.ia(
reader, if (nextToken is Token.Begin)
"规则为空"
else "需要更多内容"
)
}
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 "<结束>"
next.ia(reader, "需要 ${getType(isStartingOfGroup)}, 但是找到了 ${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, "为了避免语义混乱, 不允许在一层规则组混合使用 `&&` 和 `||`, 请显式使用 `{}` 分离. " +
"需要 ${getMode(mode)}, 但是找到了 ${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, "需要更多值.")
}
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 未完成解析")
}
}
}
}
}