package cn.iocoder.yudao.module.qcreport.engine.rule;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* 规则文本 → 语法树。
*
* 与前端 {@code engine/rule-engine.ts} 的 tokenize + Parser 一一对应,
* 支持的语法见 docs/智能质检报告平台-方案设计.md §13。
*/
public final class RuleParser {
private static final Set KEYWORDS = Set.of(
"AND", "BETWEEN", "FALSE", "IN", "NOT", "NOT_IN", "NULL", "OR", "TRUE");
private static final Set TWO_CHAR_OPERATORS = Set.of("<=", ">=", "!=", "<>", "==");
private static final String SINGLE_CHAR_OPERATORS = "><=+-*/!";
private static final Pattern NUMBER_TEXT = Pattern.compile("^\\d+(?:\\.\\d+)?$");
private static final Pattern DIGIT_TEXT = Pattern.compile("^\\d+$");
private RuleParser() {
}
/** 解析规则文本,语法错误抛 {@link RuleSyntaxException} */
public static RuleNode parse(String expression) {
String trimmed = expression == null ? "" : expression.trim();
if (trimmed.isEmpty()) {
throw new RuleSyntaxException("规则表达式为空", 0);
}
return new Parser(tokenize(trimmed)).parse();
}
/* ------------------------------ 词法分析 ------------------------------ */
private static List tokenize(String source) {
List tokens = new ArrayList<>();
int length = source.length();
int index = 0;
while (index < length) {
char ch = source.charAt(index);
if (Character.isWhitespace(ch)) {
index++;
continue;
}
if (ch == '\'' || ch == '"') {
index = readString(source, index, ch, tokens);
continue;
}
if (ch >= '0' && ch <= '9') {
int cursor = index;
while (cursor < length && isDigitOrDot(source.charAt(cursor))) {
cursor++;
}
String text = source.substring(index, cursor);
if (!NUMBER_TEXT.matcher(text).matches()) {
throw new RuleSyntaxException("数字格式不正确:" + text, index);
}
tokens.add(new RuleToken(TokenType.NUMBER, text, index));
index = cursor;
continue;
}
if (isIdentStart(ch)) {
index = readWord(source, index, tokens);
continue;
}
if (index + 2 <= length && TWO_CHAR_OPERATORS.contains(source.substring(index, index + 2))) {
tokens.add(new RuleToken(TokenType.OPERATOR, source.substring(index, index + 2), index));
index += 2;
continue;
}
if (ch == '(' || ch == ')') {
tokens.add(new RuleToken(ch == '(' ? TokenType.LPAREN : TokenType.RPAREN, String.valueOf(ch), index));
index++;
continue;
}
if (ch == ',') {
tokens.add(new RuleToken(TokenType.COMMA, String.valueOf(ch), index));
index++;
continue;
}
if (SINGLE_CHAR_OPERATORS.indexOf(ch) >= 0) {
tokens.add(new RuleToken(TokenType.OPERATOR, String.valueOf(ch), index));
index++;
continue;
}
throw new RuleSyntaxException("无法识别的字符「" + ch + "」", index);
}
tokens.add(new RuleToken(TokenType.EOF, "", length));
return tokens;
}
/** 读一个字符串字面量,返回结束后的下标 */
private static int readString(String source, int start, char quote, List tokens) {
int length = source.length();
StringBuilder value = new StringBuilder();
int cursor = start + 1;
while (cursor < length && source.charAt(cursor) != quote) {
if (source.charAt(cursor) == '\\' && cursor + 1 < length) {
value.append(source.charAt(cursor + 1));
cursor += 2;
continue;
}
value.append(source.charAt(cursor));
cursor++;
}
if (cursor >= length) {
throw new RuleSyntaxException("字符串缺少结尾引号", start);
}
tokens.add(new RuleToken(TokenType.STRING, value.toString(), start));
return cursor + 1;
}
/**
* 读一个标识符/路径,返回结束后的下标。
*
* 路径尾巴(.属性 与 [下标])在词法阶段就并进同一个词,取值时整条路径一次解析。
*/
private static int readWord(String source, int start, List tokens) {
int length = source.length();
int cursor = start;
while (cursor < length && isIdentPart(source.charAt(cursor))) {
cursor++;
}
StringBuilder value = new StringBuilder(source.substring(start, cursor));
while (cursor < length) {
char next = source.charAt(cursor);
if (next == '.' && cursor + 1 < length && isIdentStart(source.charAt(cursor + 1))) {
int end = cursor + 1;
while (end < length && isIdentPart(source.charAt(end))) {
end++;
}
value.append(source, cursor, end);
cursor = end;
continue;
}
if (next == '[') {
int close = source.indexOf(']', cursor);
if (close == -1) {
throw new RuleSyntaxException("数组下标缺少 ]", cursor);
}
String inner = source.substring(cursor + 1, close).trim();
if (!DIGIT_TEXT.matcher(inner).matches()) {
throw new RuleSyntaxException("数组下标只能是数字:" + inner, cursor);
}
value.append('[').append(inner).append(']');
cursor = close + 1;
continue;
}
break;
}
tokens.add(new RuleToken(TokenType.WORD, value.toString(), start));
return cursor;
}
private static boolean isIdentStart(char ch) {
return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch == '_' || ch == '$';
}
private static boolean isIdentPart(char ch) {
return isIdentStart(ch) || ch >= '0' && ch <= '9';
}
private static boolean isDigitOrDot(char ch) {
return ch >= '0' && ch <= '9' || ch == '.';
}
/* ------------------------------ 语法分析 ------------------------------ */
private static final class Parser {
private final List tokens;
private int cursor;
private Parser(List tokens) {
this.tokens = tokens;
}
private RuleNode parse() {
RuleNode node = parseOr();
RuleToken token = peek();
if (token.type() != TokenType.EOF) {
throw new RuleSyntaxException("多余的内容:" + token.value(), token.position());
}
return node;
}
private RuleNode parseOr() {
RuleNode left = parseAnd();
while (matchWord("OR")) {
left = new RuleNode.Binary("OR", left, parseAnd());
}
return left;
}
private RuleNode parseAnd() {
RuleNode left = parseComparison();
while (matchWord("AND")) {
left = new RuleNode.Binary("AND", left, parseComparison());
}
return left;
}
private RuleNode parseComparison() {
RuleNode left = parseAdditive();
if (matchWord("BETWEEN")) {
RuleNode lower = parseAdditive();
if (!matchWord("AND")) {
throw new RuleSyntaxException("BETWEEN 缺少 AND 与上界", peek().position());
}
RuleNode upper = parseAdditive();
return new RuleNode.Between(left, lower, upper, false);
}
if (matchWord("NOT")) {
if (matchWord("IN") || matchWord("NOT_IN")) {
return new RuleNode.In(left, parseList(), true);
}
throw new RuleSyntaxException("NOT 只能用于 NOT IN", peek().position());
}
if (matchWord("IN")) {
return new RuleNode.In(left, parseList(), false);
}
RuleToken token = peek();
if (token.type() == TokenType.OPERATOR
&& "< <= = == != <> > >=".contains(token.value())) {
cursor++;
return new RuleNode.Binary(normalizeOperator(token.value()), left, parseAdditive());
}
return left;
}
/** == 统一成 =、<> 统一成 !=,求值阶段只认一种写法 */
private static String normalizeOperator(String operator) {
if ("==".equals(operator)) {
return "=";
}
return "<>".equals(operator) ? "!=" : operator;
}
private List parseList() {
expect(TokenType.LPAREN);
List items = new ArrayList<>();
if (peek().type() != TokenType.RPAREN) {
items.add(parseOr());
while (peek().type() == TokenType.COMMA) {
cursor++;
items.add(parseOr());
}
}
expect(TokenType.RPAREN);
return items;
}
private RuleNode parseAdditive() {
RuleNode left = parseMultiplicative();
for (; ; ) {
RuleToken token = peek();
if (token.type() != TokenType.OPERATOR || !"+".equals(token.value()) && !"-".equals(token.value())) {
return left;
}
cursor++;
left = new RuleNode.Binary(token.value(), left, parseMultiplicative());
}
}
private RuleNode parseMultiplicative() {
RuleNode left = parseUnary();
for (; ; ) {
RuleToken token = peek();
if (token.type() != TokenType.OPERATOR || !"*".equals(token.value()) && !"/".equals(token.value())) {
return left;
}
cursor++;
left = new RuleNode.Binary(token.value(), left, parseUnary());
}
}
private RuleNode parseUnary() {
RuleToken token = peek();
if (token.type() == TokenType.OPERATOR && ("-".equals(token.value()) || "!".equals(token.value()))) {
cursor++;
return new RuleNode.Unary(token.value(), parseUnary());
}
return parsePrimary();
}
private RuleNode parsePrimary() {
RuleToken token = peek();
if (token.type() == TokenType.NUMBER) {
cursor++;
return new RuleNode.Literal(Double.valueOf(token.value()));
}
if (token.type() == TokenType.STRING) {
cursor++;
return new RuleNode.Literal(token.value());
}
if (token.type() == TokenType.LPAREN) {
cursor++;
RuleNode node = parseOr();
expect(TokenType.RPAREN);
return node;
}
if (token.type() == TokenType.WORD) {
cursor++;
String upper = token.value().toUpperCase();
if ("TRUE".equals(upper)) {
return new RuleNode.Literal(Boolean.TRUE);
}
if ("FALSE".equals(upper)) {
return new RuleNode.Literal(Boolean.FALSE);
}
if ("NULL".equals(upper)) {
return new RuleNode.Literal(null);
}
if (peek().type() == TokenType.LPAREN) {
return new RuleNode.Call(upper, parseList());
}
if (KEYWORDS.contains(upper)) {
throw new RuleSyntaxException("关键字 " + upper + " 的位置不正确", token.position());
}
return new RuleNode.Path(token.value());
}
throw new RuleSyntaxException(
token.type() == TokenType.EOF ? "表达式不完整" : "无法解析的内容:" + token.value(),
token.position());
}
private RuleToken peek() {
return tokens.get(cursor);
}
private boolean matchWord(String word) {
RuleToken token = peek();
if (token.type() == TokenType.WORD && token.value().toUpperCase().equals(word)) {
cursor++;
return true;
}
return false;
}
private void expect(TokenType type) {
RuleToken token = peek();
if (token.type() != type) {
throw new RuleSyntaxException(
"应该是 " + type.label() + ",实际是「" + token.value() + "」", token.position());
}
cursor++;
}
}
}