package jnpf.elnFormula; import jnpf.elnFormula.parser.ElnFormulaBaseVisitor; import jnpf.elnFormula.parser.ElnFormulaLexer; import jnpf.elnFormula.parser.ElnFormulaParser.AdditiveContext; import jnpf.elnFormula.parser.ElnFormulaParser.ArgumentsContext; import jnpf.elnFormula.parser.ElnFormulaParser.ComparisonContext; import jnpf.elnFormula.parser.ElnFormulaParser.EqualityContext; import jnpf.elnFormula.parser.ElnFormulaParser.ExpressionContext; import jnpf.elnFormula.parser.ElnFormulaParser.LogicalAndContext; import jnpf.elnFormula.parser.ElnFormulaParser.LogicalOrContext; import jnpf.elnFormula.parser.ElnFormulaParser.MultiplicativeContext; import jnpf.elnFormula.parser.ElnFormulaParser.PowerContext; import jnpf.elnFormula.parser.ElnFormulaParser.PrimaryContext; import jnpf.elnFormula.parser.ElnFormulaParser.UnaryContext; import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.LexerNoViableAltException; import org.antlr.v4.runtime.RecognitionException; import org.antlr.v4.runtime.Recognizer; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.TerminalNode; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; public final class FormulaParser { private static final Set FORBIDDEN_IDENTIFIERS; static { Set values = new HashSet<>(); Collections.addAll(values, "new", "class", "import", "package", "this", "super"); FORBIDDEN_IDENTIFIERS = Collections.unmodifiableSet(values); } private final FormulaLimits limits; public FormulaParser() { this(FormulaLimits.DEFAULTS); } public FormulaParser(FormulaLimits limits) { if (limits == null) { throw new IllegalArgumentException("limits must not be null"); } this.limits = limits; } public static boolean isReservedParameterCode(String code) { return "true".equals(code) || "false".equals(code) || "null".equals(code) || FORBIDDEN_IDENTIFIERS.contains(code); } public FormulaAst.Node parse(String expression) { String source = expression == null ? "" : expression; if (source.length() > limits.getMaxExpressionLength()) { throw error("EXPRESSION_TOO_LONG", "公式长度超过限制", 0, source.length()); } SourceOffsets offsets = new SourceOffsets(source); CollectingErrorListener lexerErrors = new CollectingErrorListener("LEXICAL_ERROR", offsets); ElnFormulaLexer lexer = new ElnFormulaLexer(CharStreams.fromString(source)); lexer.removeErrorListeners(); lexer.addErrorListener(lexerErrors); CommonTokenStream tokens = new CommonTokenStream(lexer); tokens.fill(); lexerErrors.throwFirst(); int tokenCount = 0; for (Token token : tokens.getTokens()) { if (token.getType() == Token.EOF) { continue; } tokenCount++; if ((token.getType() == ElnFormulaLexer.IDENTIFIER || token.getType() == ElnFormulaLexer.FUNCTION) && token.getText().length() > 80) { throw error("IDENTIFIER_TOO_LONG", "标识符长度超过限制", offsets.toUtf16(token.getStartIndex()), offsets.toUtf16(token.getStopIndex() + 1)); } if (token.getType() == ElnFormulaLexer.IDENTIFIER && FORBIDDEN_IDENTIFIERS.contains(token.getText())) { throw error("FORBIDDEN_IDENTIFIER", "标识符不允许使用", offsets.toUtf16(token.getStartIndex()), offsets.toUtf16(token.getStopIndex() + 1)); } } if (tokenCount > limits.getMaxTokens()) { throw error("TOKEN_LIMIT_EXCEEDED", "公式 token 数量超过限制", 0, source.length()); } tokens.seek(0); CollectingErrorListener parserErrors = new CollectingErrorListener("SYNTAX_ERROR", offsets); jnpf.elnFormula.parser.ElnFormulaParser generated = new jnpf.elnFormula.parser.ElnFormulaParser(tokens); generated.removeErrorListeners(); generated.addErrorListener(parserErrors); jnpf.elnFormula.parser.ElnFormulaParser.ParseContext tree = generated.parse(); parserErrors.throwFirst(); return new AstBuilder(limits, offsets).visitExpression(tree.expression()).node; } private static ParseException error(String code, String message, int start, int end) { int safeStart = Math.max(0, start); return new ParseException(code, message, safeStart, Math.max(safeStart, end)); } public static final class ParseException extends RuntimeException { private final String code; private final int startOffset; private final int endOffset; private ParseException(String code, String message, int startOffset, int endOffset) { super(message); this.code = code; this.startOffset = startOffset; this.endOffset = endOffset; } public String getCode() { return code; } public int getStartOffset() { return startOffset; } public int getEndOffset() { return endOffset; } } private static final class CollectingErrorListener extends BaseErrorListener { private final String code; private final SourceOffsets offsets; private ParseException first; private CollectingErrorListener(String code, SourceOffsets offsets) { this.code = code; this.offsets = offsets; } @Override public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String message, RecognitionException exception) { if (first != null) { return; } int start = offsets.absoluteCodePoint(line, charPositionInLine); int end = start + 1; if (exception instanceof LexerNoViableAltException) { start = ((LexerNoViableAltException) exception).getStartIndex(); end = start + 1; } else if (offendingSymbol instanceof Token) { Token token = (Token) offendingSymbol; start = Math.max(0, token.getStartIndex()); end = token.getType() == Token.EOF ? start : token.getStopIndex() + 1; } String safeMessage = "LEXICAL_ERROR".equals(code) ? "公式包含非法字符" : "公式语法错误"; first = error(code, safeMessage, offsets.toUtf16(start), offsets.toUtf16(end)); } private void throwFirst() { if (first != null) { throw first; } } } private static final class SourceOffsets { private final int[] codePointToUtf16; private final List lineStartCodePoints; private SourceOffsets(String source) { int codePointCount = source.codePointCount(0, source.length()); this.codePointToUtf16 = new int[codePointCount + 1]; this.lineStartCodePoints = new ArrayList<>(); lineStartCodePoints.add(0); int utf16Offset = 0; for (int codePointOffset = 0; codePointOffset < codePointCount; codePointOffset++) { codePointToUtf16[codePointOffset] = utf16Offset; int codePoint = source.codePointAt(utf16Offset); utf16Offset += Character.charCount(codePoint); if (codePoint == '\n') { lineStartCodePoints.add(codePointOffset + 1); } } codePointToUtf16[codePointCount] = source.length(); } private int toUtf16(int codePointOffset) { int safeOffset = Math.max(0, Math.min(codePointOffset, codePointToUtf16.length - 1)); return codePointToUtf16[safeOffset]; } private int absoluteCodePoint(int line, int charPositionInLine) { int lineIndex = Math.max(0, Math.min(line - 1, lineStartCodePoints.size() - 1)); return lineStartCodePoints.get(lineIndex) + Math.max(0, charPositionInLine); } } private static final class AstValue { private final FormulaAst.Node node; private final int depth; private AstValue(FormulaAst.Node node, int depth) { this.node = node; this.depth = depth; } } private static final class AstBuilder extends ElnFormulaBaseVisitor { private final FormulaLimits limits; private final SourceOffsets offsets; private AstBuilder(FormulaLimits limits, SourceOffsets offsets) { this.limits = limits; this.offsets = offsets; } @Override public AstValue visitExpression(ExpressionContext context) { return visit(context.logicalOr()); } @Override public AstValue visitLogicalOr(LogicalOrContext context) { return fold(context.logicalAnd(), context.OR()); } @Override public AstValue visitLogicalAnd(LogicalAndContext context) { return fold(context.equality(), context.AND()); } @Override public AstValue visitEquality(EqualityContext context) { return fold(context.comparison(), terminals(context.EQ(), context.NE())); } @Override public AstValue visitComparison(ComparisonContext context) { return fold(context.additive(), terminals(context.GT(), context.GE(), context.LT(), context.LE())); } @Override public AstValue visitAdditive(AdditiveContext context) { return fold(context.multiplicative(), terminals(context.PLUS(), context.MINUS())); } @Override public AstValue visitMultiplicative(MultiplicativeContext context) { return fold(context.unary(), terminals(context.STAR(), context.SLASH(), context.PERCENT())); } @Override public AstValue visitUnary(UnaryContext context) { if (context.unary() == null) { return visit(context.power()); } AstValue operand = visit(context.unary()); Token operator = context.getStart(); return checked(new FormulaAst.Unary(start(operator), operand.node.getEndOffset(), operator.getText(), operand.node), operand.depth + 1); } @Override public AstValue visitPower(PowerContext context) { AstValue left = visit(context.primary()); if (context.unary() == null) { return left; } return binary(left, context.CARET().getSymbol(), visit(context.unary())); } @Override public AstValue visitPrimary(PrimaryContext context) { if (context.DECIMAL() != null) { Token token = context.DECIMAL().getSymbol(); return leaf(new FormulaAst.Literal(start(token), end(token), FormulaAst.LiteralKind.DECIMAL, new BigDecimal(token.getText()))); } if (context.STRING() != null) { Token token = context.STRING().getSymbol(); return leaf(new FormulaAst.Literal(start(token), end(token), FormulaAst.LiteralKind.TEXT, unescape(token.getText()))); } if (context.TRUE() != null || context.FALSE() != null) { Token token = context.TRUE() != null ? context.TRUE().getSymbol() : context.FALSE().getSymbol(); return leaf(new FormulaAst.Literal(start(token), end(token), FormulaAst.LiteralKind.BOOLEAN, Boolean.valueOf(token.getText()))); } if (context.NULL() != null) { Token token = context.NULL().getSymbol(); return leaf(new FormulaAst.Literal(start(token), end(token), FormulaAst.LiteralKind.NULL, null)); } if (context.IDENTIFIER() != null) { Token token = context.IDENTIFIER().getSymbol(); return leaf(new FormulaAst.Parameter(start(token), end(token), token.getText())); } if (context.FUNCTION() != null) { return function(context); } AstValue grouped = visitExpression(context.expression()); return checked(grouped.node, grouped.depth + 1); } private AstValue function(PrimaryContext context) { Token function = context.FUNCTION().getSymbol(); List arguments = new ArrayList<>(); int depth = 1; ArgumentsContext argumentsContext = context.arguments(); if (argumentsContext != null) { if (argumentsContext.expression().size() > limits.getMaxFunctionArguments()) { throw error("FUNCTION_ARGUMENT_LIMIT_EXCEEDED", "函数参数数量超过限制", start(function), end(context.getStop())); } for (ExpressionContext item : argumentsContext.expression()) { AstValue value = visitExpression(item); arguments.add(value.node); depth = Math.max(depth, value.depth + 1); } } return checked(new FormulaAst.FunctionCall(start(function), end(context.getStop()), function.getText(), arguments), depth); } private AstValue fold( List operands, List operators) { AstValue result = visit(operands.get(0)); for (int i = 1; i < operands.size(); i++) { result = binary(result, operators.get(i - 1).getSymbol(), visit(operands.get(i))); } return result; } @SafeVarargs private final List terminals(List... groups) { List result = new ArrayList<>(); for (List group : groups) { result.addAll(group); } result.sort((left, right) -> Integer.compare( left.getSymbol().getTokenIndex(), right.getSymbol().getTokenIndex())); return result; } private AstValue binary(AstValue left, Token operator, AstValue right) { return checked(new FormulaAst.Binary(left.node.getStartOffset(), right.node.getEndOffset(), operator.getText(), left.node, right.node), Math.max(left.depth, right.depth) + 1); } private AstValue leaf(FormulaAst.Node node) { return checked(node, 1); } private AstValue checked(FormulaAst.Node node, int depth) { if (depth > limits.getMaxAstDepth()) { throw error("AST_DEPTH_EXCEEDED", "公式嵌套深度超过限制", node.getStartOffset(), node.getEndOffset()); } return new AstValue(node, depth); } private int start(Token token) { return offsets.toUtf16(token.getStartIndex()); } private int end(Token token) { return offsets.toUtf16(token.getStopIndex() + 1); } private static String unescape(String tokenText) { StringBuilder result = new StringBuilder(tokenText.length() - 2); for (int i = 1; i < tokenText.length() - 1; i++) { char current = tokenText.charAt(i); if (current != '\\') { result.append(current); continue; } char escaped = tokenText.charAt(++i); if (escaped == 'n') { result.append('\n'); } else if (escaped == 'r') { result.append('\r'); } else if (escaped == 't') { result.append('\t'); } else { result.append(escaped); } } return result.toString(); } } }