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;
|
|
/**
|
* 规则文本 → 语法树。
|
* <p>
|
* 与前端 {@code engine/rule-engine.ts} 的 tokenize + Parser 一一对应,
|
* 支持的语法见 docs/智能质检报告平台-方案设计.md §13。
|
*/
|
public final class RuleParser {
|
|
private static final Set<String> KEYWORDS = Set.of(
|
"AND", "BETWEEN", "FALSE", "IN", "NOT", "NOT_IN", "NULL", "OR", "TRUE");
|
|
private static final Set<String> 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<RuleToken> tokenize(String source) {
|
List<RuleToken> 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<RuleToken> 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;
|
}
|
|
/**
|
* 读一个标识符/路径,返回结束后的下标。
|
* <p>
|
* 路径尾巴(.属性 与 [下标])在词法阶段就并进同一个词,取值时整条路径一次解析。
|
*/
|
private static int readWord(String source, int start, List<RuleToken> 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<RuleToken> tokens;
|
private int cursor;
|
|
private Parser(List<RuleToken> 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<RuleNode> parseList() {
|
expect(TokenType.LPAREN);
|
List<RuleNode> 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++;
|
}
|
}
|
|
}
|