Skip to content

Commit 435983f

Browse files
committed
support strict new lines option
1 parent 017121b commit 435983f

File tree

11 files changed

+160
-20
lines changed

11 files changed

+160
-20
lines changed

README-EN-source.adoc

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,47 @@ include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=poll
10061006

10071007
For details, refer to the link:#semicolons[Semicolons] chapter.
10081008

1009+
=== Strict Newline Mode
1010+
1011+
Since QLExpress4 supports omitting semicolons, the interpreter needs to use newlines to determine whether a statement has ended. Therefore, QLExpress4 has stricter requirements for newlines than QLExpress3.
1012+
1013+
The following script is valid in QLExpress3 but not in QLExpress4:
1014+
1015+
[source,java]
1016+
----
1017+
// Valid in QLExpress3, but not in QLExpress4
1018+
商家应收=
1019+
价格
1020+
- 饭卡商家承担
1021+
+ 平台补贴
1022+
----
1023+
1024+
In QLExpress4, the above script will be parsed as two separate statements:
1025+
1. `商家应收 = 价格`
1026+
2. `- 饭卡商家承担 + 平台补贴` (the second statement will result in a syntax error)
1027+
1028+
To achieve the same effect in QLExpress4, you need to place the operator at the end of the line rather than at the beginning:
1029+
1030+
[source,java]
1031+
----
1032+
// Correct way in QLExpress4
1033+
商家应收=
1034+
价格 -
1035+
饭卡商家承担 +
1036+
平台补贴
1037+
----
1038+
1039+
This way, the interpreter knows that the current line's expression is not yet complete and will continue reading the next line.
1040+
1041+
If you need to be compatible with QLExpress3's newline behavior, you can set the `strictNewLines` option to `false`:
1042+
1043+
[source,java]
1044+
----
1045+
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=notStrictNewLinesTest]
1046+
----
1047+
1048+
Note: Non-strict newline mode causes the interpreter to ignore all newlines, which may affect code readability and the accuracy of error messages. It is recommended to use this only when you need to be compatible with legacy code.
1049+
10091050
=== Obtaining char Type
10101051

10111052
In QLExpress 3, single characters wrapped in single quotes were parsed as char type, not String.

README-source.adoc

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,10 +1003,51 @@ include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=poll
10031003

10041004
=== 分号可省略
10051005

1006-
分号可省略 已经是现代脚本语言的一个标配,QLExpress4 也跟进了这个特性,分号是可以省略的。
1006+
"分号可省略" 已经是现代脚本语言的一个标配,QLExpress4 也跟进了这个特性,分号是可以省略的。
10071007

10081008
具体参考 link:#分号[分号] 章节。
10091009

1010+
=== 严格换行模式
1011+
1012+
由于 QLExpress4 支持分号可省略,解释器需要通过换行符来判断语句是否结束。因此,QLExpress4 对换行的要求比 QLExpress3 更加严格。
1013+
1014+
下面的脚本在 QLExpress3 中是合法的,但在 QLExpress4 中不是:
1015+
1016+
[source,java]
1017+
----
1018+
// 在 QLExpress3 中合法的脚本,但在 QLExpress4 中不是
1019+
商家应收=
1020+
价格
1021+
- 饭卡商家承担
1022+
+ 平台补贴
1023+
----
1024+
1025+
在 QLExpress4 中,上述脚本会被解析为两个独立的语句:
1026+
1. `商家应收 = 价格`
1027+
2. `- 饭卡商家承担 + 平台补贴`(第二个语句会报语法错误)
1028+
1029+
如果要在 QLExpress4 中实现同样的效果,需要将操作符放在行尾,而不是行首:
1030+
1031+
[source,java]
1032+
----
1033+
// QLExpress4 中正确的写法
1034+
商家应收=
1035+
价格 -
1036+
饭卡商家承担 +
1037+
平台补贴
1038+
----
1039+
1040+
这样解释器就知道当前行的表达式还未结束,会继续读取下一行。
1041+
1042+
如果您需要兼容 QLExpress3 的换行特性,可以设置 `strictNewLines` 选项为 `false`:
1043+
1044+
[source,java]
1045+
----
1046+
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=notStrictNewLinesTest]
1047+
----
1048+
1049+
注意:非严格换行模式会让解释器忽略所有换行符,可能会影响代码的可读性和错误提示的准确性。建议仅在需要兼容旧代码时使用。
1050+
10101051
=== 获得 char 类型
10111052

10121053
在 QLExpress 3 中,单引号包裹的单个字符会被解析为 char 类型,而不是 String。

src/main/antlr4/QLParser.g4

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ options {
2222
protected InterpolationMode getInterpolationMode() {
2323
return SCRIPT;
2424
}
25+
protected boolean isStrictNewLines() {
26+
return true;
27+
}
2528
}
2629

2730
// grammar
@@ -37,7 +40,7 @@ blockStatements
3740
newlines : NEWLINE+;
3841

3942
nextStatement
40-
: {_input.LA(1) == Token.EOF || _input.LA(1) == QLexer.RBRACE}? | ';' | NEWLINE;
43+
: {_input.LA(1) == Token.EOF || _input.LA(1) == QLexer.RBRACE}? | ';' | {isStrictNewLines()}? NEWLINE;
4144

4245
blockStatement
4346
: localVariableDeclaration ';' # localVariableDeclarationStatement
@@ -167,7 +170,7 @@ ternaryExpr
167170
;
168171

169172
baseExpr [int p]
170-
: primary ({_input.LA(1) != Token.EOF && _input.LA(1) != QLexer.NEWLINE &&
173+
: primary ({_input.LA(1) != Token.EOF && (!isStrictNewLines() || _input.LA(1) != QLexer.NEWLINE) &&
171174
isOpType(_input.LT(1).getText(), MIDDLE) && precedence(_input.LT(1).getText()) >= $p}? leftAsso)*
172175
;
173176

src/main/java/com/alibaba/qlexpress4/Express4Runner.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,8 @@ public QLParser.ProgramContext parseToSyntaxTree(String script) {
517517
initOptions.getDebugInfoConsumer(),
518518
initOptions.getInterpolationMode(),
519519
initOptions.getSelectorStart(),
520-
initOptions.getSelectorEnd());
520+
initOptions.getSelectorEnd(),
521+
initOptions.isStrictNewLines());
521522
}
522523

523524
public void check(String script, CheckOptions checkOptions)

src/main/java/com/alibaba/qlexpress4/InitOptions.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,16 @@ public class InitOptions {
7777
*/
7878
private final String selectorEnd;
7979

80+
/**
81+
* Strictly require a line break between any two statements (since semicolons can be omitted in QLExpress4).
82+
* default is true
83+
*/
84+
private final boolean strictNewLines;
85+
8086
private InitOptions(ClassSupplier classSupplier, List<ImportManager.QLImport> defaultImport, boolean debug,
8187
Consumer<String> debugInfoConsumer, QLSecurityStrategy securityStrategy, boolean allowPrivateAccess,
82-
InterpolationMode interpolationMode, boolean traceExpression, String selectorStart, String selectorEnd) {
88+
InterpolationMode interpolationMode, boolean traceExpression, String selectorStart, String selectorEnd,
89+
boolean strictNewLines) {
8390
this.classSupplier = classSupplier;
8491
this.defaultImport = defaultImport;
8592
this.debug = debug;
@@ -90,6 +97,7 @@ private InitOptions(ClassSupplier classSupplier, List<ImportManager.QLImport> de
9097
this.traceExpression = traceExpression;
9198
this.selectorStart = selectorStart;
9299
this.selectorEnd = selectorEnd;
100+
this.strictNewLines = strictNewLines;
93101
}
94102

95103
public static InitOptions.Builder builder() {
@@ -136,6 +144,10 @@ public String getSelectorEnd() {
136144
return selectorEnd;
137145
}
138146

147+
public boolean isStrictNewLines() {
148+
return strictNewLines;
149+
}
150+
139151
public static class Builder {
140152
private ClassSupplier classSupplier = DefaultClassSupplier.getInstance();
141153

@@ -162,6 +174,8 @@ public static class Builder {
162174

163175
private String selectorEnd = "}";
164176

177+
private boolean strictNewLines = true;
178+
165179
public Builder classSupplier(ClassSupplier classSupplier) {
166180
this.classSupplier = classSupplier;
167181
return this;
@@ -211,16 +225,21 @@ public Builder selectorStart(String selectorStart) {
211225
}
212226

213227
public Builder selectorEnd(String selectorEnd) {
214-
if (selectorEnd == null || selectorEnd.length() < 1) {
228+
if (selectorEnd == null || selectorEnd.isEmpty()) {
215229
throw new IllegalArgumentException("Custom selector end must be 1 or more characters");
216230
}
217231
this.selectorEnd = selectorEnd;
218232
return this;
219233
}
220234

235+
public Builder strictNewLines(boolean strictNewLines) {
236+
this.strictNewLines = strictNewLines;
237+
return this;
238+
}
239+
221240
public InitOptions build() {
222241
return new InitOptions(classSupplier, defaultImport, debug, debugInfoConsumer, securityStrategy,
223-
allowPrivateAccess, interpolationMode, traceExpression, selectorStart, selectorEnd);
242+
allowPrivateAccess, interpolationMode, traceExpression, selectorStart, selectorEnd, strictNewLines);
224243
}
225244
}
226245
}

src/main/java/com/alibaba/qlexpress4/aparser/QLExtendLexer.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,27 @@ public class QLExtendLexer extends QLexer {
1515

1616
private final String selectorEnd;
1717

18+
private final boolean strictNewLines;
19+
1820
public QLExtendLexer(CharStream input, String script, InterpolationMode interpolationMode, String selectorStart,
19-
String selectorEnd) {
21+
String selectorEnd, boolean strictNewLines) {
2022
super(input);
2123
this.script = script;
2224
this.interpolationMode = interpolationMode;
2325
this.selectorStart = selectorStart;
2426
this.selectorEnd = selectorEnd;
27+
this.strictNewLines = strictNewLines;
28+
}
29+
30+
@Override
31+
public Token nextToken() {
32+
Token token = super.nextToken();
33+
// In non-strict mode, skip NEWLINE tokens
34+
if (!strictNewLines && token.getType() == QLexer.NEWLINE) {
35+
// Skip NEWLINEs by recursively calling nextToken()
36+
return nextToken();
37+
}
38+
return token;
2539
}
2640

2741
@Override

src/main/java/com/alibaba/qlexpress4/aparser/QLExtendParser.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ public class QLExtendParser extends QLParser {
88

99
private final InterpolationMode interpolationMode;
1010

11-
public QLExtendParser(AliasTokenStream input, ParserOperatorManager opM, InterpolationMode interpolationMode) {
11+
private final boolean strictNewLines;
12+
13+
public QLExtendParser(AliasTokenStream input, ParserOperatorManager opM, InterpolationMode interpolationMode,
14+
boolean strictNewLines) {
1215
super(input);
1316
this.opM = opM;
1417
this.interpolationMode = interpolationMode;
18+
this.strictNewLines = strictNewLines;
1519
}
1620

1721
@Override
@@ -28,4 +32,9 @@ protected Integer precedence(String lexeme) {
2832
protected InterpolationMode getInterpolationMode() {
2933
return interpolationMode;
3034
}
35+
36+
@Override
37+
protected boolean isStrictNewLines() {
38+
return strictNewLines;
39+
}
3140
}

src/main/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactory.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@ public static void warmUp() {
3131

3232
private static void warmUpExpress(String script) {
3333
buildTree(script, new OperatorManager(), false, false, s -> {
34-
}, InterpolationMode.SCRIPT, "${", "}");
34+
}, InterpolationMode.SCRIPT, "${", "}", true);
3535
}
3636

3737
public static QLParser.ProgramContext buildTree(String script, ParserOperatorManager operatorManager,
3838
boolean printTree, boolean profile, Consumer<String> printer, InterpolationMode interpolationMode,
39-
String selectorStart, String selectorEnd) {
40-
QLexer lexer =
41-
new QLExtendLexer(CharStreams.fromString(script), script, interpolationMode, selectorStart, selectorEnd);
39+
String selectorStart, String selectorEnd, boolean strictNewLines) {
40+
QLexer lexer = new QLExtendLexer(CharStreams.fromString(script), script, interpolationMode, selectorStart,
41+
selectorEnd, strictNewLines);
4242
CommonTokenStream tokens = new CommonTokenStream(lexer);
43-
QLParser qlGrammarParser =
44-
new QLExtendParser(new AliasTokenStream(tokens, operatorManager), operatorManager, interpolationMode);
43+
QLParser qlGrammarParser = new QLExtendParser(new AliasTokenStream(tokens, operatorManager), operatorManager,
44+
interpolationMode, strictNewLines);
4545
if (!printTree) {
4646
qlGrammarParser.removeErrorListeners();
4747
}

src/test/java/com/alibaba/qlexpress4/ClearDfaCacheTest.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.alibaba.qlexpress4;
22

3+
import com.alibaba.qlexpress4.runtime.context.ExpressContext;
34
import org.junit.Assert;
45
import org.junit.Test;
56

@@ -12,7 +13,6 @@
1213
import java.nio.file.Path;
1314
import java.nio.file.Paths;
1415
import java.text.DecimalFormat;
15-
import java.util.Collections;
1616
import java.util.Objects;
1717

1818
public class ClearDfaCacheTest {
@@ -36,7 +36,6 @@ public void clearDFACacheTest()
3636
public void bestPractice()
3737
throws URISyntaxException, IOException {
3838
String exampleExpress = "1+1";
39-
double beforeMemoryUsed = getMemoryUsedMB();
4039
// tag::clearDFACacheBestPractice[]
4140
/*
4241
* When the expression changes, parse it and add it to the expression cache;
@@ -50,10 +49,9 @@ public void bestPractice()
5049
* All subsequent runs of this script must enable the cache option to ensure that re-compilation does not occur.
5150
*/
5251
for (int i = 0; i < 3; i++) {
53-
runner.execute(exampleExpress, Collections.emptyMap(), QLOptions.builder().cache(true).build());
52+
runner.execute(exampleExpress, ExpressContext.EMPTY_CONTEXT, QLOptions.builder().cache(true).build());
5453
}
5554
// end::clearDFACacheBestPractice[]
56-
Assert.assertTrue(getMemoryUsedMB() <= beforeMemoryUsed);
5755
}
5856

5957
private double getMemoryUsedMB() {

src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,20 @@ public void docDefaultImportJavaTest() {
430430
// end::defaultImport[]
431431
}
432432

433+
@Test
434+
public void notStrictNewLinesTest() {
435+
// tag::notStrictNewLinesTest[]
436+
Express4Runner express4Runner = new Express4Runner(InitOptions.builder().strictNewLines(false).build());
437+
String script = "商家应收=\n 价格\n - 饭卡商家承担\n + 平台补贴";
438+
Map<String, Object> context = new HashMap<>();
439+
context.put("价格", 10);
440+
context.put("饭卡商家承担", 3);
441+
context.put("平台补贴", 5);
442+
QLResult result = express4Runner.execute(script, context, QLOptions.DEFAULT_OPTIONS);
443+
Assert.assertEquals(12, ((Number)result.getResult()).intValue());
444+
// end::notStrictNewLinesTest[]
445+
}
446+
433447
@Test
434448
public void invokeDefaultMethodTest() {
435449
Express4Runner express4Runner = new Express4Runner(InitOptions.builder()

0 commit comments

Comments
 (0)