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++; } } }