diff --git a/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt b/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt deleted file mode 100644 index 17064132b..000000000 --- a/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt +++ /dev/null @@ -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, 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 = ArrayList( - 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, root: Boolean): List = ArrayList().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, 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") - } - } -} \ No newline at end of file diff --git a/backend/mirai-console/src/internal/util/semver/RequirementParser.kt b/backend/mirai-console/src/internal/util/semver/RequirementParser.kt new file mode 100644 index 000000000..011f18954 --- /dev/null +++ b/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 { + fun process(reader: TokenReader): R + fun processLine(reader: TokenReader): R + fun processLogic(isAnd: Boolean, chunks: Iterable): R + } + + abstract class ProcessorBase : TokensProcessor { + 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 "" + 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") + } + } + } + } +} diff --git a/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt b/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt index 1d9c1af03..bb9f9a947 100644 --- a/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt +++ b/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() { + override fun processLogic(isAnd: Boolean, chunks: Iterable): 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 { diff --git a/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt b/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt index 7f981069c..53df01f1d 100644 --- a/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt +++ b/tools/compiler-common/src/diagnostics/MiraiConsoleErrors.kt @@ -51,6 +51,9 @@ object MiraiConsoleErrors { @JvmField val ILLEGAL_PERMISSION_REGISTER_USE = create(ERROR) + @JvmField + val ILLEGAL_VERSION_REQUIREMENT = create(ERROR) + @Suppress("ObjectPropertyName", "unused") @JvmField @Deprecated("", level = DeprecationLevel.ERROR) diff --git a/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt b/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt index 5858bea53..73e60cd4b 100644 --- a/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt +++ b/tools/compiler-common/src/diagnostics/MiraiConsoleErrorsRendering.kt @@ -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 diff --git a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt index 5f3ad337a..64c9095ad 100644 --- a/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt +++ b/tools/intellij-plugin/run/projects/test-project/src/main/kotlin/org/example/myplugin/MyPluginMain.kt @@ -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("
") + 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() { diff --git a/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt b/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt index 4163b04d4..86a906bf0 100644 --- a/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt +++ b/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt @@ -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()) + } } } diff --git a/tools/intellij-plugin/src/util/RequirementHelper.kt b/tools/intellij-plugin/src/util/RequirementHelper.kt new file mode 100644 index 000000000..f3e99c56c --- /dev/null +++ b/tools/intellij-plugin/src/util/RequirementHelper.kt @@ -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() { + override fun processLogic(isAnd: Boolean, chunks: Iterable) { + } + + override fun processString(reader: RequirementParser.TokenReader, token: RequirementParser.Token.Content) { + if (!isValid(token.content)) { + token.ia(reader, "`${token.content}` 无效.") + } + } + + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/util/RequirementParser.kt b/tools/intellij-plugin/src/util/RequirementParser.kt new file mode 100644 index 000000000..892cd75cc --- /dev/null +++ b/tools/intellij-plugin/src/util/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.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 { + fun process(reader: TokenReader): R + fun processLine(reader: TokenReader): R + fun processLogic(isAnd: Boolean, chunks: Iterable): R + } + + abstract class ProcessorBase : TokensProcessor { + 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 未完成解析") + } + } + } + } +}