diff --git a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java index 6ef61996b..983f99711 100644 --- a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java +++ b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.parser; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -26,6 +29,26 @@ public void setASTNode(Node node) { this.node = node; } + /** + * The comments preceding this node's first token, in source order, with the original token + * images. Line comments do not include their line terminator; callers must add a line break + * when prepending them to SQL. Empty for nodes built programmatically, which carry no AST. + */ + public List getLeadingComments() { + Node node = getASTNode(); + if (node == null) { + return new ArrayList<>(); + } + List comments = new ArrayList<>(); + for (Token specialToken = + node.jjtGetFirstToken().specialToken; specialToken != null; specialToken = + specialToken.specialToken) { + comments.add(specialToken.image); + } + Collections.reverse(comments); + return comments; + } + public StringBuilder appendTo(StringBuilder builder) { // don't add spaces around the following punctuation final Set punctuation = new TreeSet<>(Set.of(".", "[", "]")); diff --git a/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java b/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java new file mode 100644 index 000000000..6b83c46cb --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java @@ -0,0 +1,80 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class LeadingCommentsTest { + + @Test + public void blockCommentsBeforeStatementInSourceOrder() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* keep orderby */ /*+ FULL(t) */ SELECT * FROM mytable"); + assertEquals(List.of("/* keep orderby */", "/*+ FULL(t) */"), select.getLeadingComments()); + } + + @ParameterizedTest + @ValueSource(strings = {"\n", "\r\n", "\r"}) + public void lineCommentsExcludeLineTerminators(String lineTerminator) + throws JSQLParserException { + Select select = + (Select) CCJSqlParserUtil.parse("-- paging hint" + lineTerminator + "SELECT 1"); + assertEquals(List.of("-- paging hint"), select.getLeadingComments()); + } + + @Test + public void mixedCommentsRetainExactImagesAndSourceOrder() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* a */ -- b\r\n// c\r/* d */ SELECT 1"); + assertEquals(List.of("/* a */", "-- b", "// c", "/* d */"), select.getLeadingComments()); + } + + @Test + public void commentInsideStatementIsNotLeading() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT /* mid comment */ 1"); + assertEquals(List.of(), select.getLeadingComments()); + } + + @Test + public void statementWithoutComments() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM mytable"); + assertEquals(List.of(), select.getLeadingComments()); + } + + @Test + public void programmaticStatementHasNoLeadingComments() { + assertEquals(List.of(), new PlainSelect().getLeadingComments()); + } + + @Test + public void toStringDropsLeadingCommentsAsBefore() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* keep orderby */ SELECT * FROM mytable"); + assertFalse(select.toString().contains("/*")); + } + + @Test + public void commentsSplitPerStatement() throws JSQLParserException { + Statements statements = CCJSqlParserUtil + .parseStatements("/* first */ SELECT 1; /* second */ SELECT 2"); + assertEquals(List.of("/* first */"), ((Select) statements.get(0)).getLeadingComments()); + assertEquals(List.of("/* second */"), ((Select) statements.get(1)).getLeadingComments()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java b/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java index 380f8a4e6..8b23ebdc2 100644 --- a/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java @@ -15,6 +15,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.math.BigDecimal; +import java.util.List; import java.util.stream.Stream; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.SpannerInterleaveIn; @@ -34,6 +35,17 @@ import org.junit.jupiter.params.provider.ValueSource; public class SerializationTest { + @Test + void serializeSelectWithLeadingComments() throws Exception { + PlainSelect original = (PlainSelect) CCJSqlParserUtil + .parse("/* keep orderby */ SELECT id FROM mytable"); + Assertions.assertEquals(List.of("/* keep orderby */"), original.getLeadingComments()); + PlainSelect copy = roundTrip(original); + Assertions.assertEquals("SELECT id FROM mytable", copy.toString()); + Assertions.assertNull(copy.getASTNode()); + Assertions.assertEquals(List.of(), copy.getLeadingComments()); + } + @Test void serializeWithItem() throws JSQLParserException, IOException, ClassNotFoundException { String sqlStr =