Skip to content

Commit 4702549

Browse files
committed
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.
1 parent 287ee18 commit 4702549

7 files changed

Lines changed: 874 additions & 184 deletions

File tree

‎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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 answers the tree node's parent, which the object's {@code getParent()} then reports.
16+
*/
17+
public final class RangeNode extends Node {
18+
private final Node treeNode;
19+
20+
public 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+
}

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

Lines changed: 32 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 forgetMatchBegins()
52+
private int[] matchBegins = new int[24];
53+
private int matchBeginCount = 0;
5154
int bufsize;
5255
int available;
5356
int tokenBegin;
@@ -107,6 +110,33 @@ public final int getAbsoluteTokenBegin() {
107110
return absoluteTokenBegin;
108111
}
109112

113+
/**
114+
* The absolute position of the match that began at the given line and column since the last
115+
* {@link #forgetMatchBegins()}, or 0 when none did. Lets a comment, which no token action sees,
116+
* take its position from where its match began.
117+
*/
118+
public final int getAbsoluteBegin(int line, int column) {
119+
for (int i = 0; i < matchBeginCount; i += 3) {
120+
if (matchBegins[i] == line && matchBegins[i + 1] == column) {
121+
return matchBegins[i + 2];
122+
}
123+
}
124+
return 0;
125+
}
126+
127+
public final void forgetMatchBegins() {
128+
matchBeginCount = 0;
129+
}
130+
131+
private void recordMatchBegin() {
132+
if (matchBeginCount + 3 > matchBegins.length) {
133+
matchBegins = java.util.Arrays.copyOf(matchBegins, matchBegins.length * 2);
134+
}
135+
matchBegins[matchBeginCount++] = getBeginLine();
136+
matchBegins[matchBeginCount++] = getBeginColumn();
137+
matchBegins[matchBeginCount++] = absoluteTokenBegin;
138+
}
139+
110140
protected void ExpandBuff(boolean wrapAround) {
111141
final int newSize = bufsize * 2;
112142
char[] newbuffer = new char[newSize];
@@ -200,6 +230,7 @@ public final char BeginToken() throws java.io.IOException {
200230
tokenBegin = bufpos;
201231

202232
absoluteTokenBegin = totalCharsRead;
233+
recordMatchBegin();
203234

204235
if (c == '#' && featureConfiguration != null
205236
&& featureConfiguration.getAsBoolean(Feature.allowHashLineComments)) {
@@ -441,6 +472,7 @@ public final void backup(int amount) {
441472
* Reinitialise.
442473
*/
443474
public void ReInit(Provider dstream, int startline, int startcolumn, int buffersize) {
475+
matchBeginCount = 0;
444476
inputStream = dstream;
445477
line = startline;
446478
column = startcolumn - 1;

‎src/main/java/net/sf/jsqlparser/statement/create/table/ColDataType.java‎

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

1212
import net.sf.jsqlparser.expression.IntervalQualifier;
13+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1314
import net.sf.jsqlparser.statement.select.PlainSelect;
1415

15-
import java.io.Serializable;
1616
import java.math.BigInteger;
1717
import java.util.ArrayList;
1818
import java.util.Collection;
@@ -23,7 +23,7 @@
2323

2424
import static java.util.stream.Collectors.joining;
2525

26-
public class ColDataType implements Serializable {
26+
public class ColDataType extends ASTNodeAccessImpl {
2727

2828
public enum Signedness {
2929
SIGNED, UNSIGNED

‎src/main/java/net/sf/jsqlparser/statement/select/OrderByElement.java‎

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

12-
import java.io.Serializable;
13-
1412
import java.util.Locale;
1513
import net.sf.jsqlparser.expression.Expression;
14+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1615

17-
public class OrderByElement implements Serializable {
16+
public class OrderByElement extends ASTNodeAccessImpl {
1817

1918
private Expression expression;
2019
// postgres rollup is an ExpressionList

0 commit comments

Comments
 (0)