diff --git a/lib/ch_usi_si_seart_treesitter_Query.cc b/lib/ch_usi_si_seart_treesitter_Query.cc index 893c45c5..28a220d3 100644 --- a/lib/ch_usi_si_seart_treesitter_Query.cc +++ b/lib/ch_usi_si_seart_treesitter_Query.cc @@ -10,3 +10,13 @@ JNIEXPORT void JNICALL Java_ch_usi_si_seart_treesitter_Query_delete( ts_query_delete(query); __clearPointer(env, thisObject); } + +JNIEXPORT jint JNICALL Java_ch_usi_si_seart_treesitter_Query_getQuantifier( + JNIEnv* env, jobject thisObject, jint patternIndex, jint captureIndex) { + TSQuery* query = (TSQuery*)__getPointer(env, thisObject); + return (jint)ts_query_capture_quantifier_for_id( + query, + (uint32_t)patternIndex, + (uint32_t)captureIndex + ); +} diff --git a/lib/ch_usi_si_seart_treesitter_Query.h b/lib/ch_usi_si_seart_treesitter_Query.h index d391e513..ce1d8bd0 100644 --- a/lib/ch_usi_si_seart_treesitter_Query.h +++ b/lib/ch_usi_si_seart_treesitter_Query.h @@ -15,6 +15,14 @@ extern "C" { JNIEXPORT void JNICALL Java_ch_usi_si_seart_treesitter_Query_delete (JNIEnv *, jobject); +/* + * Class: ch_usi_si_seart_treesitter_Query + * Method: getQuantifier + * Signature: (II)I + */ +JNIEXPORT jint JNICALL Java_ch_usi_si_seart_treesitter_Query_getQuantifier + (JNIEnv *, jobject, jint, jint); + #ifdef __cplusplus } #endif diff --git a/src/main/java/ch/usi/si/seart/treesitter/Capture.java b/src/main/java/ch/usi/si/seart/treesitter/Capture.java index d6779b4e..29d58375 100644 --- a/src/main/java/ch/usi/si/seart/treesitter/Capture.java +++ b/src/main/java/ch/usi/si/seart/treesitter/Capture.java @@ -8,7 +8,9 @@ import lombok.experimental.NonFinal; import org.jetbrains.annotations.NotNull; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * Represents the named capture of a {@link Query}. Captures are used @@ -51,6 +53,33 @@ public class Capture { */ public native void disable(); + /** + * Get the capture quantifier for a given query {@link Pattern}. + * + * @param pattern the query pattern + * @return the quantifier + * @throws NullPointerException if pattern is {@code null} + * @throws IllegalArgumentException if the pattern is not present in the query + * @since 1.12.0 + */ + public Quantifier getQuantifier(@NotNull Pattern pattern) { + return query.getQuantifier(pattern, this); + } + + /** + * Get the capture quantifiers for all {@link Query} patterns. + * The order of the quantifiers in the returned list corresponds + * to the {@link Pattern} order in the query. + * + * @return the quantifiers + * @since 1.12.0 + */ + public List getQuantifiers() { + return query.getPatterns().stream() + .map(this::getQuantifier) + .collect(Collectors.toUnmodifiableList()); + } + @Override @Generated public boolean equals(Object o) { diff --git a/src/main/java/ch/usi/si/seart/treesitter/Quantifier.java b/src/main/java/ch/usi/si/seart/treesitter/Quantifier.java new file mode 100644 index 00000000..717c9484 --- /dev/null +++ b/src/main/java/ch/usi/si/seart/treesitter/Quantifier.java @@ -0,0 +1,51 @@ +package ch.usi.si.seart.treesitter; + +/** + * Represents the {@link Capture} quantifier in a {@link Pattern}, + * i.e. the number of nodes that a capture should contain. Within a + * query, a capture can have different quantifiers for each pattern. + * + * @since 1.12.0 + * @author Ozren Dabić + */ +public enum Quantifier { + + /** + * The capture will not match any nodes, + * as said capture is not present in a + * specific pattern. + */ + ZERO, + /** + * The capture will match at most one node. + * Example: + *
{@code
+     * ((_)? @capture)
+     * }
+ */ + ZERO_OR_ONE, + /** + * The capture will match any number of nodes. + * Example: + *
{@code
+     * ((_)* @capture)
+     * }
+ */ + ZERO_OR_MORE, + /** + * The capture will match exactly one node. + * Example: + *
{@code
+     * ((_) @capture)
+     * }
+ */ + ONE, + /** + * The capture will match at least one node. + * Example: + *
{@code
+     * ((_)+ @capture)
+     * }
+ */ + ONE_OR_MORE +} diff --git a/src/main/java/ch/usi/si/seart/treesitter/Query.java b/src/main/java/ch/usi/si/seart/treesitter/Query.java index a5cca927..8427c43a 100644 --- a/src/main/java/ch/usi/si/seart/treesitter/Query.java +++ b/src/main/java/ch/usi/si/seart/treesitter/Query.java @@ -230,4 +230,16 @@ public String getPattern() { .map(Pattern::toString) .collect(Collectors.joining(" ")); } + + Quantifier getQuantifier(@NotNull Pattern pattern, @NotNull Capture capture) { + Objects.requireNonNull(pattern, "Pattern must not be null!"); + Objects.requireNonNull(capture, "Capture must not be null!"); + if (!patterns.contains(pattern)) throw new IllegalArgumentException("Pattern not present in query!"); + if (!captures.contains(capture)) throw new IllegalArgumentException("Capture not present in query!"); + Quantifier[] quantifiers = Quantifier.values(); + int ordinal = getQuantifier(pattern.getIndex(), capture.getIndex()); + return quantifiers[ordinal]; + } + + private native int getQuantifier(int patternIndex, int captureIndex); } diff --git a/src/test/java/ch/usi/si/seart/treesitter/QuantifierTest.java b/src/test/java/ch/usi/si/seart/treesitter/QuantifierTest.java new file mode 100644 index 00000000..bf054f6a --- /dev/null +++ b/src/test/java/ch/usi/si/seart/treesitter/QuantifierTest.java @@ -0,0 +1,84 @@ +package ch.usi.si.seart.treesitter; + +import lombok.Cleanup; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +class QuantifierTest extends BaseTest { + + private static final Language language = Language.JAVA; + + private static final Map symbols = Map.of( + Quantifier.ONE, "", + Quantifier.ONE_OR_MORE, "+", + Quantifier.ZERO_OR_ONE, "?", + Quantifier.ZERO_OR_MORE, "*" + ); + + private static final class QuantifierArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return symbols.entrySet().stream() + .map(entry -> { + Quantifier quantifier = entry.getKey(); + String symbol = entry.getValue(); + String pattern = "((_)" + symbol + " @capture)"; + Query query = Query.getFor(language, pattern); + List captures = query.getCaptures(); + Capture capture = captures.stream().findFirst().orElseThrow(); + return Arguments.of(quantifier, capture, query); + }); + } + } + + @ParameterizedTest(name = "[{index}] {0}") + @ArgumentsSource(QuantifierArgumentsProvider.class) + void testGetQuantifiers(Quantifier expected, Capture capture, Query query) { + List quantifiers = capture.getQuantifiers(); + Assertions.assertNotNull(quantifiers); + Assertions.assertFalse(quantifiers.isEmpty()); + Assertions.assertEquals(1, quantifiers.size()); + Quantifier actual = quantifiers.stream() + .findFirst() + .orElseGet(Assertions::fail); + Assertions.assertEquals(expected, actual); + query.close(); + } + + @Test + void testGetQuantifier() { + @Cleanup Query query = Query.builder() + .language(language) + .pattern("((_) @capture)") + .pattern("(identifier)") + .build(); + List captures = query.getCaptures(); + List patterns = query.getPatterns(); + Capture capture = captures.stream().findFirst().orElseThrow(); + Pattern pattern = patterns.stream().skip(1).findFirst().orElseThrow(); + Quantifier quantifier = capture.getQuantifier(pattern); + Assertions.assertEquals(Quantifier.ZERO, quantifier); + } + + @Test + void testGetQuantifierThrows() { + @Cleanup Query original = Query.getFor(language, "((_) @capture)"); + @Cleanup Query copy = Query.getFor(language, "((_) @capture)"); + List captures = original.getCaptures(); + Capture capture = captures.stream().findFirst().orElseThrow(); + List patterns = copy.getPatterns(); + Pattern pattern = patterns.stream().findFirst().orElseThrow(); + Assertions.assertThrows(NullPointerException.class, () -> capture.getQuantifier(null)); + Assertions.assertThrows(IllegalArgumentException.class, () -> capture.getQuantifier(pattern)); + } +}