Skip to content

Commit dd03d6f

Browse files
fix: give every parsed object a node spanning its source (#2870)
* fix: give arithmetic expressions an AST node prattArithRest builds operator expressions in plain Java, outside any production, so JJTree never opened a node for them and `CURRENT_DATE - 7` or `a || b` had no source range. Each expression the loop builds is now linked to a node from the token its operand chain starts at to the last token consumed. The node stays out of the tree, so the operands' nodes keep their place. * refactor: let every statement class carry an AST node The statement classes that implemented Statement alone now extend ASTNodeAccessImpl, as Select and Import already did, so a parsed statement can hold the node that records its source range. Nothing links them yet. * fix: give every parsed object a node spanning its source Objects no grammar rule opens a node for, such as a column's table qualifier, a WHEN clause, nested AND/OR, IS NULL, BETWEEN, parameter lists, data types, ORDER BY elements and statements, are linked to a RangeNode: a node outside the parse tree that records the object's first and last token. getASTRoot() returns the same tree, and no tree node gains or changes its value. Ranges that missed text their object prints now cover it: a statement's WITH, Oracle PRIOR and (+), LIMIT ... BY, sequence options, IF ... ELSE and merged adjacent literals. A string literal no longer takes the node of the primary expression around it. Comments get positions, so an Oracle hint has a range too. getParent() returns null instead of throwing once it runs out of tree. ASTNodeCoverageTest parses the test resources' SQL and requires every ASTNodeAccess object to have a node of at least one character, inside its holder's range, never partly overlapping another, holding no parser, and, for expressions and statements, covering text that parses back to the same object. * fix: link statements nested in others, and position every comment Statements that never pass through SingleStatement get their node where they are built: data-modifying CTEs, statements inside an Oracle block, CREATE SCHEMA elements, CREATE TABLE ... AS EXECUTE and the GRANT or REVOKE of ALTER DEFAULT PRIVILEGES. An IF statement's range takes in the semicolon it prints. A RangeNode that widens a tree node answers its children, id and dump too, so a WITH statement's node still shows the tree beneath it. Comments are positioned in one backwards pass over the recorded match begins, and comments before the end of input are positioned once parsing ends, since the end-of-file token gets no token action. Values in a primary expression, string literals, expression lists and row patterns get a RangeNode only once something wraps them, so a production's own tree node is the only node most of them get. On a 20,000-row INSERT ... VALUES this cuts the added allocation from 12.9% to 3.2%. The coverage test parses the Oracle tests in the Oracle dialect, checks that getParent() encloses each range, and runs its checks over the statements above, which the test corpus does not contain. * test: split the coverage walk to settle Codacy's PMD findings `visit` and `checkRange` exceeded PMD's NPath threshold of 200, so the container walk and the parent check move into methods of their own. `isHeld` compares with `==` on purpose: a getter that answers a new object on every call computes its child rather than holding it. * perf: create the deque of NOT tokens only for a NOT condition `Condition()` created a deque for the tokens of a leading `NOT` on every call, though few conditions have one. Each empty `ArrayDeque` allocates its 16-slot array up front, so parsing `performance.sql` allocated 585 KB more per parse. The deque is now created at the first `NOT`. * refactor: keep RangeNode internal to the parser package Only the generated parser creates a `RangeNode`, and callers see it as the `Node` that `getASTNode()` returns. Keeping the class package-private leaves its design free to change before a release.
1 parent df97e40 commit dd03d6f

126 files changed

Lines changed: 1322 additions & 326 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java‎

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,22 +71,20 @@ public StringBuilder appendTo(StringBuilder builder) {
7171
return builder;
7272
}
7373

74+
/**
75+
* The nearest enclosing object in the tree, or null when there is none. A node that carries
76+
* only a token range, for an object no grammar rule opens a tree node for, has no parent.
77+
*/
7478
public ASTNodeAccess getParent() {
75-
Node parent = (Node) node.jjtGetParent();
76-
while (parent.jjtGetValue() == null) {
77-
parent = (Node) parent.jjtGetParent();
78-
}
79-
80-
return ASTNodeAccess.class.cast(parent.jjtGetValue());
79+
return getParent(ASTNodeAccess.class);
8180
}
8281

8382
public <T extends ASTNodeAccess> T getParent(Class<T> clazz) {
84-
Node parent = (Node) node.jjtGetParent();
85-
while (parent.jjtGetValue() == null || !clazz.isInstance(parent.jjtGetValue())) {
83+
Node parent = node == null ? null : (Node) node.jjtGetParent();
84+
while (parent != null && !clazz.isInstance(parent.jjtGetValue())) {
8685
parent = (Node) parent.jjtGetParent();
8786
}
88-
89-
return clazz.cast(parent.jjtGetValue());
87+
return parent == null ? null : clazz.cast(parent.jjtGetValue());
9088
}
9189

9290
@Override
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.parser;
11+
12+
/**
13+
* A node outside the parse tree that records the tokens an object spans. It serves an object no
14+
* grammar rule opens a tree node for, or one whose text runs past its tree node. In that second
15+
* case it stands in for the tree node, answering its parent, children and id, with the wider range.
16+
*/
17+
final class RangeNode extends Node {
18+
private final Node treeNode;
19+
20+
RangeNode(Token first, Token last, Node treeNode) {
21+
super(CCJSqlParserTreeConstants.JJTEXPRESSION);
22+
this.treeNode = treeNode;
23+
jjtSetFirstToken(first);
24+
jjtSetLastToken(last);
25+
}
26+
27+
@Override
28+
public Node jjtGetParent() {
29+
return treeNode == null ? null : treeNode.jjtGetParent();
30+
}
31+
32+
@Override
33+
public Node[] jjtGetChildren() {
34+
return treeNode == null ? super.jjtGetChildren() : treeNode.jjtGetChildren();
35+
}
36+
37+
@Override
38+
public Node jjtGetChild(int i) {
39+
return treeNode == null ? super.jjtGetChild(i) : treeNode.jjtGetChild(i);
40+
}
41+
42+
@Override
43+
public int jjtGetNumChildren() {
44+
return treeNode == null ? super.jjtGetNumChildren() : treeNode.jjtGetNumChildren();
45+
}
46+
47+
@Override
48+
public Object childrenAccept(CCJSqlParserVisitor visitor, Object data) {
49+
return treeNode == null ? super.childrenAccept(visitor, data)
50+
: treeNode.childrenAccept(visitor, data);
51+
}
52+
53+
@Override
54+
public int getId() {
55+
return treeNode == null ? super.getId() : treeNode.getId();
56+
}
57+
58+
@Override
59+
public String toString() {
60+
return treeNode == null ? super.toString() : treeNode.toString();
61+
}
62+
63+
@Override
64+
public void dump(String prefix) {
65+
if (treeNode == null) {
66+
super.dump(prefix);
67+
} else {
68+
treeNode.dump(prefix);
69+
}
70+
}
71+
}

‎src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public class SimpleCharStream {
4848
protected boolean trackLineColumn = true;
4949
protected int totalCharsRead = 0;
5050
protected int absoluteTokenBegin = 0;
51+
// line, column and absolute position of each match begun since the last positionSpecialTokens()
52+
private int[] matchBegins = new int[24];
53+
private int matchBeginCount = 0;
5154
int bufsize;
5255
int available;
5356
int tokenBegin;
@@ -107,6 +110,38 @@ public final int getAbsoluteTokenBegin() {
107110
return absoluteTokenBegin;
108111
}
109112

113+
/**
114+
* Gives the comments in front of {@code token} the positions where their matches began, from
115+
* the begins recorded since the last call, and forgets those. Comments come through no token
116+
* action, so this is how they get a position. The comments chain backwards from the token while
117+
* the begins are recorded forwards, so one backwards pass pairs them.
118+
*/
119+
public final void positionSpecialTokens(Token token) {
120+
int i = matchBeginCount - 3;
121+
for (Token comment = token.specialToken; comment != null; comment = comment.specialToken) {
122+
while (i >= 0 && (matchBegins[i] != comment.beginLine
123+
|| matchBegins[i + 1] != comment.beginColumn)) {
124+
i -= 3;
125+
}
126+
if (i < 0) {
127+
break;
128+
}
129+
comment.absoluteBegin = matchBegins[i + 2];
130+
comment.absoluteEnd = comment.absoluteBegin + comment.image.length();
131+
i -= 3;
132+
}
133+
matchBeginCount = 0;
134+
}
135+
136+
private void recordMatchBegin() {
137+
if (matchBeginCount + 3 > matchBegins.length) {
138+
matchBegins = java.util.Arrays.copyOf(matchBegins, matchBegins.length * 2);
139+
}
140+
matchBegins[matchBeginCount++] = getBeginLine();
141+
matchBegins[matchBeginCount++] = getBeginColumn();
142+
matchBegins[matchBeginCount++] = absoluteTokenBegin;
143+
}
144+
110145
protected void ExpandBuff(boolean wrapAround) {
111146
final int newSize = bufsize * 2;
112147
char[] newbuffer = new char[newSize];
@@ -200,6 +235,7 @@ public final char BeginToken() throws java.io.IOException {
200235
tokenBegin = bufpos;
201236

202237
absoluteTokenBegin = totalCharsRead;
238+
recordMatchBegin();
203239

204240
if (c == '#' && featureConfiguration != null
205241
&& featureConfiguration.getAsBoolean(Feature.allowHashLineComments)) {
@@ -441,6 +477,7 @@ public final void backup(int amount) {
441477
* Reinitialise.
442478
*/
443479
public void ReInit(Provider dstream, int startline, int startcolumn, int buffersize) {
480+
matchBeginCount = 0;
444481
inputStream = dstream;
445482
line = startline;
446483
column = startcolumn - 1;

‎src/main/java/net/sf/jsqlparser/statement/AssertStatement.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import net.sf.jsqlparser.expression.Expression;
1313
import net.sf.jsqlparser.expression.StringValue;
14+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1415

1516
import java.util.function.Consumer;
1617

@@ -22,7 +23,7 @@
2223
* "https://cloud.google.com/bigquery/docs/reference/standard-sql/debugging-statements">Debugging
2324
* statements</a>
2425
*/
25-
public class AssertStatement implements Statement {
26+
public class AssertStatement extends ASTNodeAccessImpl implements Statement {
2627
private Expression expression;
2728
private StringValue description;
2829

‎src/main/java/net/sf/jsqlparser/statement/AttachStatement.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package net.sf.jsqlparser.statement;
1111

12+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1213
import net.sf.jsqlparser.statement.select.PlainSelect;
1314

1415
import java.util.List;
@@ -19,7 +20,7 @@
1920
*
2021
* @see <a href="https://duckdb.org/docs/stable/sql/statements/attach">ATTACH</a>
2122
*/
22-
public class AttachStatement implements Statement {
23+
public class AttachStatement extends ASTNodeAccessImpl implements Statement {
2324
private String databasePath;
2425
private String alias;
2526
private boolean usingDatabaseKeyword;

‎src/main/java/net/sf/jsqlparser/statement/Block.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
package net.sf.jsqlparser.statement;
1111

1212
import java.util.function.Consumer;
13+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1314

14-
public class Block implements Statement {
15+
public class Block extends ASTNodeAccessImpl implements Statement {
1516
private boolean hasSemicolonAfterEnd = false;
1617

1718
private Statements statements;

‎src/main/java/net/sf/jsqlparser/statement/Commit.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
*/
1010
package net.sf.jsqlparser.statement;
1111

12-
public class Commit implements Statement {
12+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
13+
14+
public class Commit extends ASTNodeAccessImpl implements Statement {
1315
private TransactionKeyword keyword;
1416
private TransactionChain chain;
1517

‎src/main/java/net/sf/jsqlparser/statement/ConnectStatement.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
*/
1010
package net.sf.jsqlparser.statement;
1111

12+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
13+
1214
/**
1315
* DuckDB's {@code CONNECT target [AS alias]}, which routes the session's queries to a remote
1416
* database.
1517
*/
16-
public class ConnectStatement implements Statement {
18+
public class ConnectStatement extends ASTNodeAccessImpl implements Statement {
1719
private String target;
1820
private String alias;
1921

‎src/main/java/net/sf/jsqlparser/statement/CopyStatement.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package net.sf.jsqlparser.statement;
1111

1212
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
13+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1314
import net.sf.jsqlparser.schema.Column;
1415
import net.sf.jsqlparser.schema.Table;
1516
import net.sf.jsqlparser.statement.select.PlainSelect;
@@ -25,7 +26,7 @@
2526
*
2627
* @see <a href="https://duckdb.org/docs/stable/sql/statements/copy">COPY</a>
2728
*/
28-
public class CopyStatement implements Statement {
29+
public class CopyStatement extends ASTNodeAccessImpl implements Statement {
2930
private Table table;
3031
private ExpressionList<Column> columns;
3132
private Select select;

‎src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
import java.util.List;
1616
import java.util.Optional;
1717
import java.util.function.Consumer;
18+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1819
import net.sf.jsqlparser.statement.create.function.FunctionReturnType;
1920
import net.sf.jsqlparser.statement.create.table.TableElement;
2021

2122
/**
2223
* A base for the declaration of function like statements
2324
*/
24-
public abstract class CreateFunctionalStatement implements Statement {
25+
public abstract class CreateFunctionalStatement extends ASTNodeAccessImpl implements Statement {
2526

2627
private String kind;
2728
private boolean orReplace = false;

0 commit comments

Comments
 (0)