From 4a190d8a7adcd715a414219fba1f7f36ae0429ac Mon Sep 17 00:00:00 2001 From: fbeutel Date: Fri, 10 Jan 2025 12:07:45 +0100 Subject: [PATCH] improved tests and documentation of QueryCursor --- .../github/treesitter/jtreesitter/Query.java | 69 +++-- .../treesitter/jtreesitter/QueryCursor.java | 81 +++--- .../jtreesitter/QueryCursorConfig.java | 124 +++++++++ .../jtreesitter/QueryCursorOptions.java | 71 ----- .../jtreesitter/QueryCursorTest.java | 251 ++++++++++++++++++ .../treesitter/jtreesitter/QueryTest.java | 5 - 6 files changed, 444 insertions(+), 157 deletions(-) create mode 100644 src/main/java/io/github/treesitter/jtreesitter/QueryCursorConfig.java delete mode 100644 src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java create mode 100644 src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java diff --git a/src/main/java/io/github/treesitter/jtreesitter/Query.java b/src/main/java/io/github/treesitter/jtreesitter/Query.java index 95c6978..5b669dd 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/Query.java +++ b/src/main/java/io/github/treesitter/jtreesitter/Query.java @@ -12,7 +12,6 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.stream.Stream; - import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -25,7 +24,7 @@ @NullMarked public final class Query implements AutoCloseable { private final MemorySegment query; - private final QueryCursorOptions cursorOptions = new QueryCursorOptions(); + private final QueryCursorConfig cursorConfig = new QueryCursorConfig(); private final Arena arena; private final Language language; private final String source; @@ -261,7 +260,7 @@ private static boolean invalidPredicateChar(char c) { return !(Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.' || c == '?' || c == '!'); } - MemorySegment self(){ + MemorySegment self() { return query; } @@ -275,30 +274,30 @@ MemorySegment self(){ return ts_query_capture_count(query); } - public List> getPredicates(){ + public List> getPredicates() { return predicates.stream().map(Collections::unmodifiableList).toList(); } - public List getCaptureNames(){ + public List getCaptureNames() { return Collections.unmodifiableList(captureNames); } /** - * Get the maximum number of in-progress matches. + * Get the maximum number of in-progress matches of the default {@link QueryCursorConfig} * * @apiNote Defaults to {@code -1} (unlimited). */ public @Unsigned int getMatchLimit() { - return cursorOptions.getMatchLimit(); + return cursorConfig.getMatchLimit(); } /** - * Get the maximum number of in-progress matches. + * Set the maximum number of in-progress matches of the default {@link QueryCursorConfig} * * @throws IllegalArgumentException If {@code matchLimit == 0}. */ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentException { - cursorOptions.setMatchLimit(matchLimit); + cursorConfig.setMatchLimit(matchLimit); return this; } @@ -308,9 +307,11 @@ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentExcep * * @apiNote Defaults to {@code 0} (unlimited). * @since 0.23.1 + * @deprecated */ + @Deprecated(forRemoval = true) public @Unsigned long getTimeoutMicros() { - return cursorOptions.getTimeoutMicros(); + return cursorConfig.getTimeoutMicros(); } /** @@ -318,9 +319,11 @@ public Query setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentExcep * execution should be allowed to take before halting. * * @since 0.23.1 + * @deprecated */ + @Deprecated(forRemoval = true) public Query setTimeoutMicros(@Unsigned long timeoutMicros) { - cursorOptions.setTimeoutMicros(timeoutMicros); + cursorConfig.setTimeoutMicros(timeoutMicros); return this; } @@ -331,25 +334,22 @@ public Query setTimeoutMicros(@Unsigned long timeoutMicros) { *
Note that if a pattern includes many children, then they will still be checked. */ public Query setMaxStartDepth(@Unsigned int maxStartDepth) { - cursorOptions.setMaxStartDepth(maxStartDepth); + cursorConfig.setMaxStartDepth(maxStartDepth); return this; } /** Set the range of bytes in which the query will be executed. */ public Query setByteRange(@Unsigned int startByte, @Unsigned int endByte) { - cursorOptions.setStartByte(startByte); - cursorOptions.setEndByte(endByte); + cursorConfig.setByteRange(startByte, endByte); return this; } /** Set the range of points in which the query will be executed. */ public Query setPointRange(Point startPoint, Point endPoint) { - cursorOptions.setStartPoint(startPoint); - cursorOptions.setEndPoint(endPoint); + cursorConfig.setPointRange(startPoint, endPoint); return this; } - /** * Disable a certain pattern within a query. * @@ -477,14 +477,13 @@ public Map> getPatternAssertions(@Unsigned int index, b return Collections.unmodifiableMap(assertions.get(index)); } - /** - * Execute the query on a given node. + * Execute the query on a given node with the default {@link QueryCursorConfig}. * @param node The node that the query will run on. * @return A cursor that can be used to iterate over the matches. */ - public QueryCursor execute(Node node){ - return new QueryCursor(this, node, cursorOptions); + public QueryCursor execute(Node node) { + return new QueryCursor(this, node, cursorConfig); } /** @@ -493,22 +492,22 @@ public QueryCursor execute(Node node){ * @param options The options that will be used for this query. * @return A cursor that can be used to iterate over the matches. */ - public QueryCursor execute(Node node, QueryCursorOptions options){ + public QueryCursor execute(Node node, QueryCursorConfig options) { return new QueryCursor(this, node, options); } - /** * Iterate over all the matches in the order that they were found. The lifetime of the native memory of the returned * matches is bound to the lifetime of this query object. * * @param node The node that the query will run on. + * @implNote The stream is not created lazily such that there is no open {@link QueryCursor} instance left behind. + * For creating a lazy stream use {@link #execute(Node)} and {@link QueryCursor#matchStream()}. */ public Stream findMatches(Node node) { - return findMatches(node, null, arena); + return findMatches(node, arena, null); } - /** * Iterate over all the matches in the order that they were found. The lifetime of the native memory of the returned * matches is bound to the lifetime of this query object. @@ -526,13 +525,13 @@ public Stream findMatches(Node node) { * * @param node The node that the query will run on. * @param predicate A function that handles custom predicates. - * @implNote The stream is not created lazily such that there is no open {@link QueryCursor} instance left behind. For creating a lazy stream use {@link #execute(Node)} and {@link QueryCursor#stream(BiPredicate)}. + * @implNote The stream is not created lazily such that there is no open {@link QueryCursor} instance left behind. + * For creating a lazy stream use {@link #execute(Node)} and {@link QueryCursor#matchStream(BiPredicate)}. */ public Stream findMatches(Node node, @Nullable BiPredicate predicate) { - return findMatches(node, predicate, arena); + return findMatches(node, arena, predicate); } - /** * Like {@link #findMatches(Node, BiPredicate)} but the native memory of the returned matches is created using the * given allocator. @@ -540,12 +539,15 @@ public Stream findMatches(Node node, @Nullable BiPredicate findMatches(Node node, @Nullable BiPredicate predicate, SegmentAllocator allocator) { - try(QueryCursor cursor = this.execute(node)){ + public Stream findMatches( + Node node, SegmentAllocator allocator, @Nullable BiPredicate predicate) { + try (QueryCursor cursor = this.execute(node)) { // make sure to load the entire stream into memory before closing the cursor. - // Otherwise, we call for nextMatch after closing the cursor which leads to undefined behavior. - return cursor.stream(allocator, predicate).toList().stream(); + // Otherwise, we call for nextMatch after closing the cursor which leads to an exception. + return cursor.matchStream(allocator, predicate).toList().stream(); } } @@ -559,13 +561,10 @@ public String toString() { return "Query{language=%s, source=%s}".formatted(language, source); } - private void checkIndex(@Unsigned int index) throws IndexOutOfBoundsException { if (Integer.compareUnsigned(index, getPatternCount()) >= 0) { throw new IndexOutOfBoundsException( "Pattern index %s is out of bounds".formatted(Integer.toUnsignedString(index))); } } - - } diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java index 9202fd5..04f9e68 100644 --- a/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java @@ -1,11 +1,11 @@ package io.github.treesitter.jtreesitter; +import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; + import io.github.treesitter.jtreesitter.internal.TSNode; import io.github.treesitter.jtreesitter.internal.TSQueryCapture; import io.github.treesitter.jtreesitter.internal.TSQueryMatch; import io.github.treesitter.jtreesitter.internal.TreeSitter; -import org.jspecify.annotations.Nullable; - import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.foreign.SegmentAllocator; @@ -18,20 +18,16 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_did_exceed_match_limit; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_exec; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_match_limit; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_new; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_next_match; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_remove_match; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_byte_range; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_match_limit; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_max_start_depth; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_point_range; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_set_timeout_micros; -import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_timeout_micros; - -public class QueryCursor implements AutoCloseable{ +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Cursor for iterating over the matches produced by a {@link Query}. + *

+ * An instance of this class can be retrieved by calling {@link Query#execute(Node)}. + */ +@NullMarked +public class QueryCursor implements AutoCloseable { private final MemorySegment cursor; private final Arena arena; @@ -39,14 +35,13 @@ public class QueryCursor implements AutoCloseable{ private final Query query; private final Tree tree; - - QueryCursor(Query query, Node cursorRootNode, @Nullable QueryCursorOptions options){ + QueryCursor(Query query, Node cursorRootNode, @Nullable QueryCursorConfig config) { arena = Arena.ofConfined(); cursor = ts_query_cursor_new().reinterpret(arena, TreeSitter::ts_query_cursor_delete); this.query = query; this.tree = cursorRootNode.getTree(); - if(options != null){ - applyOptions(options); + if (config != null) { + applyConfig(config); } try (var alloc = Arena.ofConfined()) { @@ -54,7 +49,7 @@ public class QueryCursor implements AutoCloseable{ } } - private void applyOptions(QueryCursorOptions options){ + private void applyConfig(QueryCursorConfig options) { if (options.getStartByte() >= 0 && options.getEndByte() >= 0) { ts_query_cursor_set_byte_range(cursor, options.getStartByte(), options.getEndByte()); @@ -81,7 +76,6 @@ private void applyOptions(QueryCursorOptions options){ } } - /** * Get the maximum number of in-progress matches. * @@ -91,19 +85,19 @@ private void applyOptions(QueryCursorOptions options){ return ts_query_cursor_match_limit(cursor); } - /** * Get the maximum duration in microseconds that query * execution should be allowed to take before halting. * * @apiNote Defaults to {@code 0} (unlimited). * @since 0.23.1 + * @deprecated */ + @Deprecated(forRemoval = true) public @Unsigned long getTimeoutMicros() { return ts_query_cursor_timeout_micros(cursor); } - /** * Check if the query exceeded its maximum number of * in-progress matches during its last execution. @@ -112,39 +106,35 @@ public boolean didExceedMatchLimit() { return ts_query_cursor_did_exceed_match_limit(cursor); } - public void removeMatch(@Unsigned int matchId){ - ts_query_cursor_remove_match(cursor, matchId); - } - /** * Stream the matches produced by the query. The stream can not be consumed after the cursor is closed. The native * nodes backing the matches are bound to the lifetime of the cursor. * @return a stream of matches */ - public Stream stream() { - return stream(null); + public Stream matchStream() { + return matchStream(null); } /** - * Like {@link #stream()} but allows for custom predicates to be applied to the matches. + * Like {@link #matchStream()} but allows for custom predicates to be applied to the matches. * @param predicate a function to handle custom predicates. * @return a stream of matches */ - public Stream stream(@Nullable BiPredicate predicate) { - return stream(arena , predicate); + public Stream matchStream(@Nullable BiPredicate predicate) { + return matchStream(arena, predicate); } /** - * Like {@link #stream(BiPredicate)} but allows for a custom allocator to be used for allocating the native nodes. + * Like {@link #matchStream(BiPredicate)} but allows for a custom allocator to be used for allocating the native nodes. * @param allocator allocator to use for allocating the native nodes backing the matches * @param predicate a function to handle custom predicates. * @return a stream of matches */ - public Stream stream(SegmentAllocator allocator, @Nullable BiPredicate predicate) { + public Stream matchStream( + SegmentAllocator allocator, @Nullable BiPredicate predicate) { return StreamSupport.stream(new MatchesIterator(this, allocator, predicate), false); } - /** * Get the next match produced by the query. The native nodes backing the match are bound to the lifetime of the cursor. * @return the next match, if available @@ -168,7 +158,8 @@ public Optional nextMatch(@Nullable BiPredicate nextMatch(SegmentAllocator allocator, @Nullable BiPredicate predicate) { + public Optional nextMatch( + SegmentAllocator allocator, @Nullable BiPredicate predicate) { var hasNoText = tree.getText() == null; MemorySegment match = arena.allocate(TSQueryMatch.layout()); @@ -183,9 +174,6 @@ public Optional nextMatch(SegmentAllocator allocator, @Nullable BiPr captureList.add(new QueryCapture(name, new Node(node, tree))); } var patternIndex = TSQueryMatch.pattern_index(match); - var matchId = TSQueryMatch.id(match); - // we copy all the data. So we can directly remove the match from the cursor to free memory. - ts_query_cursor_remove_match(cursor, matchId); var result = new QueryMatch(patternIndex, captureList); if (hasNoText || matches(predicate, result)) { return Optional.of(result); @@ -201,7 +189,6 @@ private boolean matches(@Nullable BiPredicate predic }); } - @Override public void close() { arena.close(); @@ -214,24 +201,26 @@ private static final class MatchesIterator extends Spliterators.AbstractSplitera private final QueryCursor cursor; - public MatchesIterator(QueryCursor cursor, SegmentAllocator allocator, @Nullable BiPredicate predicate) { + public MatchesIterator( + QueryCursor cursor, + SegmentAllocator allocator, + @Nullable BiPredicate predicate) { super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL); this.predicate = predicate; this.allocator = allocator; this.cursor = cursor; } + @Override public boolean tryAdvance(Consumer action) { - if(!cursor.arena.scope().isAlive()){ - throw new IllegalStateException("Cursor is closed. Cannot produce more matches."); + if (!cursor.arena.scope().isAlive()) { + throw new IllegalStateException("The underlying QueryCursor is closed. Cannot produce more matches."); } Optional queryMatch = cursor.nextMatch(allocator, predicate); queryMatch.ifPresent(action); return queryMatch.isPresent(); } - } - } diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursorConfig.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursorConfig.java new file mode 100644 index 0000000..4d57bdc --- /dev/null +++ b/src/main/java/io/github/treesitter/jtreesitter/QueryCursorConfig.java @@ -0,0 +1,124 @@ +package io.github.treesitter.jtreesitter; + +import org.jspecify.annotations.Nullable; + +/** + * Configuration for creating a {@link QueryCursor}. + * + * @see Query#execute(Node, QueryCursorConfig) + */ +public class QueryCursorConfig { + private int matchLimit = -1; // Default to unlimited + private long timeoutMicros = 0; // Default to unlimited + private int maxStartDepth = -1; + private int startByte = -1; + private int endByte = -1; + private Point startPoint; + private Point endPoint; + + /** + * Get the maximum number of in-progress matches. + * + * @apiNote Defaults to {@code -1} (unlimited). + */ + public int getMatchLimit() { + return matchLimit; + } + + /** + * Set the maximum number of in-progress matches. + * + * @apiNote Defaults to {@code -1} (unlimited). + */ + public void setMatchLimit(int matchLimit) throws IllegalArgumentException { + if (matchLimit == 0) { + throw new IllegalArgumentException("The match limit cannot equal 0"); + } + this.matchLimit = matchLimit; + } + + /** + * Get the maximum duration in microseconds that query + * execution should be allowed to take before halting. + * + * @return the timeout in microseconds + * @deprecated + */ + @Deprecated(forRemoval = true) + public long getTimeoutMicros() { + return timeoutMicros; + } + + /** + * Set the maximum duration in microseconds that query execution + * should be allowed to take before halting. + * + * @param timeoutMicros the timeout in microseconds + * @deprecated + */ + @Deprecated(forRemoval = true) + public void setTimeoutMicros(long timeoutMicros) { + this.timeoutMicros = timeoutMicros; + } + + /** + * Get the maximum start depth for the query cursor + */ + public int getMaxStartDepth() { + return maxStartDepth; + } + + /** + * Set the maximum start depth for the query cursor. + * + *

This prevents cursors from exploring children nodes at a certain depth. + *
Note that if a pattern includes many children, then they will still be checked. + */ + public void setMaxStartDepth(int maxStartDepth) { + this.maxStartDepth = maxStartDepth; + } + + /** + * Set the range of bytes in which the query will be executed. + */ + public void setByteRange(@Unsigned int startByte, @Unsigned int endByte) { + this.startByte = startByte; + this.endByte = endByte; + } + + /** + * Get the start byte of the range of bytes in which the query will be executed or -1 if not set. + */ + public int getStartByte() { + return startByte; + } + + /** + * Get the end byte of the range of bytes in which the query will be executed or -1 if not set. + */ + public int getEndByte() { + return endByte; + } + + /** + * Set the range of points in which the query will be executed. + */ + public void setPointRange(Point startPoint, Point endPoint) { + this.startPoint = startPoint; + this.endPoint = endPoint; + } + + /** + * Get the start point of the range of points in which the query will be executed or {@code null} if not set. + */ + public @Nullable Point getStartPoint() { + return startPoint; + } + + /** + * Get the end point of the range of points in which the query will be executed or {@code null} if not set. + */ + public @Nullable Point getEndPoint() { + return endPoint; + } +} diff --git a/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java b/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java deleted file mode 100644 index 36941bf..0000000 --- a/src/main/java/io/github/treesitter/jtreesitter/QueryCursorOptions.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.github.treesitter.jtreesitter; - -public class QueryCursorOptions { - private int matchLimit = -1; // Default to unlimited - private long timeoutMicros = -1; // Default to unlimited - private int maxStartDepth = -1; - private int startByte = -1; - private int endByte = -1; - private Point startPoint; - private Point endPoint; - - - public int getMatchLimit() { - return matchLimit; - } - - public void setMatchLimit(int matchLimit) throws IllegalArgumentException { - if (matchLimit == 0) { - throw new IllegalArgumentException("The match limit cannot equal 0"); - } - this.matchLimit = matchLimit; - } - - public long getTimeoutMicros() { - return timeoutMicros; - } - - public void setTimeoutMicros(long timeoutMicros) { - this.timeoutMicros = timeoutMicros; - } - - public int getMaxStartDepth() { - return maxStartDepth; - } - - public void setMaxStartDepth(int maxStartDepth) { - this.maxStartDepth = maxStartDepth; - } - - public int getStartByte() { - return startByte; - } - - public void setStartByte(int startByte) { - this.startByte = startByte; - } - - public int getEndByte() { - return endByte; - } - - public void setEndByte(int endByte) { - this.endByte = endByte; - } - - public Point getStartPoint() { - return startPoint; - } - - public void setStartPoint(Point startPoint) { - this.startPoint = startPoint; - } - - public Point getEndPoint() { - return endPoint; - } - - public void setEndPoint(Point endPoint) { - this.endPoint = endPoint; - } -} diff --git a/src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java b/src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java new file mode 100644 index 0000000..418ac9b --- /dev/null +++ b/src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java @@ -0,0 +1,251 @@ +package io.github.treesitter.jtreesitter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import io.github.treesitter.jtreesitter.languages.TreeSitterJava; +import java.lang.foreign.Arena; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class QueryCursorTest { + + private static Language language; + private static Parser parser; + + @BeforeAll + static void beforeAll() { + language = new Language(TreeSitterJava.language()); + parser = new Parser(language); + } + + @AfterAll + static void afterAll() { + parser.close(); + } + + private static void assertQueryCursor(String querySource, String source, Consumer assertions) { + assertQueryCursor(querySource, source, null, assertions); + } + + private static void assertQueryCursor( + String querySource, String source, QueryCursorConfig config, Consumer assertions) { + try (var tree = parser.parse(source).orElseThrow(); + Query query = new Query(language, querySource)) { + assertQueryCursor(query, tree.getRootNode(), config, assertions); + } catch (QueryError e) { + fail("Unexpected query error", e); + } + } + + private static void assertQueryCursor( + Query query, Node node, QueryCursorConfig config, Consumer assertions) { + try (var queryCursor = query.execute(node, config)) { + assertions.accept(queryCursor); + } + } + + @Test + void nextMatch() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + """; + + assertQueryCursor(querySource, source, qc -> { + Optional currentMatch = qc.nextMatch(); + + assertTrue(currentMatch.isPresent()); + assertEquals("a", currentMatch.get().captures().getFirst().node().getText()); + + currentMatch = qc.nextMatch(); + assertTrue(currentMatch.isPresent()); + assertEquals("b", currentMatch.get().captures().getFirst().node().getText()); + + currentMatch = qc.nextMatch(); + assertFalse(currentMatch.isPresent()); + }); + } + + @Test + void matchStream() { + + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + assertQueryCursor(querySource, source, qc -> { + List queryMatches = qc.matchStream().toList(); + assertEquals(3, queryMatches.size()); + + List texts = queryMatches.stream() + .flatMap(qm -> qm.captures().stream()) + .map(cap -> cap.node().getText()) + .toList(); + assertEquals(List.of("a", "b", "c"), texts); + }); + } + + @Test + void didExceedMatchLimit() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + assertQueryCursor(querySource, source, qc -> { + assertFalse(qc.didExceedMatchLimit()); + }); + } + + @Test + void testByteRange() { + + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + QueryCursorConfig config = new QueryCursorConfig(); + config.setByteRange(6, 20); // should ignore a + + assertQueryCursor(querySource, source, config, qc -> { + List queryMatches = qc.matchStream().toList(); + assertEquals(2, queryMatches.size()); + + List texts = queryMatches.stream() + .flatMap(qm -> qm.captures().stream()) + .map(cap -> cap.node().getText()) + .toList(); + assertEquals(List.of("b", "c"), texts); + }); + } + + @Test + void testPointRange() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + QueryCursorConfig config = new QueryCursorConfig(); + config.setPointRange(new Point(0, 0), new Point(1, 0)); // should ignore c + + assertQueryCursor(querySource, source, config, qc -> { + List queryMatches = qc.matchStream().toList(); + assertEquals(2, queryMatches.size()); + + List texts = queryMatches.stream() + .flatMap(qm -> qm.captures().stream()) + .map(cap -> cap.node().getText()) + .toList(); + assertEquals(List.of("a", "b"), texts); + }); + } + + @Test + void testStartDepth() { + + String queryString = "(local_variable_declaration) @decl"; + + String source = + """ + int a = b; + void foo() { + int c = 3; + } + """; + + QueryCursorConfig config = new QueryCursorConfig(); + config.setMaxStartDepth(1); // should ignore second declaration as it is nested in method body + + assertQueryCursor(queryString, source, config, qc -> { + List queryMatches = qc.matchStream().toList(); + assertEquals(1, queryMatches.size()); + + List texts = queryMatches.stream() + .flatMap(qm -> qm.captures().stream()) + .map(cap -> cap.node().getText()) + .toList(); + assertEquals(List.of("int a = b;"), texts); + }); + } + + @Test + void testCustomAllocator() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + try (Arena arena = Arena.ofConfined()) { + + List matches = new ArrayList<>(); + + try (var tree = parser.parse(source).orElseThrow(); + Query query = new Query(language, querySource)) { + + try (var queryCursor = query.execute(tree.getRootNode())) { + List queryMatches = + queryCursor.matchStream(arena, null).toList(); + matches.addAll(queryMatches); + } + + // check that we can still work with the nodes after closing the cursor + List parentTexts = matches.stream() + .flatMap(qm -> qm.captures().stream()) + .flatMap(cap -> cap.node().getParent().stream()) + .map(Node::getText) + .toList(); + + assertEquals(List.of("a = b", "a = b", "c = 3"), parentTexts); + } catch (QueryError e) { + fail("Unexpected query error", e); + } + } + } + + @Test + void testUseMatchStreamAfterClose() { + String querySource = "(identifier) @identifier"; + + String source = """ + int a = b; + int c = 3; + """; + + try (var tree = parser.parse(source).orElseThrow(); + Query query = new Query(language, querySource)) { + + Stream queryStream = null; + try (var queryCursor = query.execute(tree.getRootNode())) { + queryStream = queryCursor.matchStream(); + } + // we cannot use the stream after closing the cursor + assertThrows(IllegalStateException.class, queryStream::toList); + } catch (QueryError e) { + fail("Unexpected query error", e); + } + } +} diff --git a/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java b/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java index f5a8bc0..6b4de54 100644 --- a/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java +++ b/src/test/java/io/github/treesitter/jtreesitter/QueryTest.java @@ -132,11 +132,6 @@ void setPointRange() { }); } - @Test - void didExceedMatchLimit() { - assertQuery(query -> assertFalse(query.didExceedMatchLimit())); - } - @Test void disablePattern() { assertQuery(query -> {