tests/testthat/test-query.R

# ------------------------------------------------------------------------------
# query()

test_that("can create basic query with correct structure", {
  source <- r'[
  (
    (identifier) @id
    (identifier) @id2
    (#eq? @id "blah")
    (#eq? @id @id2)
  )
  (
    (identifier) @id3
    (#match? @id3 "^\\s$")
  )
  ]'

  query <- query(r(), source)

  expect_identical(query$capture_names, c("id", "id2", "id3"))

  # First pattern
  predicates <- query$pattern_predicates[[1]]

  eq_string <- predicates[[1]]
  expect_true(is_predicate_eq_string(eq_string))
  expect_identical(eq_string$capture_name_value_id, 0)
  expect_identical(eq_string$capture_value, "blah")
  expect_identical(eq_string$capture_invert, FALSE)

  eq_capture <- predicates[[2]]
  expect_true(is_predicate_eq_capture(eq_capture))
  expect_identical(eq_capture$capture_name_value_id, 0)
  expect_identical(eq_capture$capture_value_id, 1)
  expect_identical(eq_capture$capture_invert, FALSE)

  # Second pattern
  predicates <- query$pattern_predicates[[2]]

  # Note - tree sitter does the escaping, so the `capture_value` now only
  # has 1 backtick
  match_string <- predicates[[1]]
  expect_true(is_predicate_match_string(match_string))
  expect_identical(match_string$capture_name_value_id, 2)
  expect_identical(match_string$capture_value, r"[^\s$]")
  expect_identical(match_string$capture_invert, FALSE)
})

test_that("can detect `not-` cases", {
  source <- '[((identifier) @id (#not-eq? @id "blah"))]'
  query <- query(r(), source)
  predicate <- query$pattern_predicates[[1]][[1]]
  expect_identical(predicate$capture_invert, TRUE)

  source <- '[((identifier) @id (#not-match? @id "blah"))]'
  query <- query(r(), source)
  predicate <- query$pattern_predicates[[1]][[1]]
  expect_identical(predicate$capture_invert, TRUE)
})

test_that("single quoted strings throw an error", {
  source <- "
(binary_operator
  operator: '+'
)
  "

  language <- r()

  expect_snapshot(error = TRUE, {
    query(language, source)
  })
})

# ------------------------------------------------------------------------------
# query_matches()

test_that("using `*` always results in a match, even if its capture count is 0", {
  # Because `*` is defined as "0 or more", so `0` counts!
  text <- "
# hi1
# hi2
fn <- function() {}
  "

  # Must get an escaped `\\s` into the string itself (so, two literal `\`
  # characters followed by an `s`, which tree-sitter then reinterprets), easiest
  # way is a raw string.
  source <- R'[
  (
    (comment)* @roxygen
    (binary_operator
      operator: "<-"
    )
    (#match? @roxygen "^#'\\s.*")
  )
  ]'

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  matches <- query_matches(query, node)

  # Only 1 pattern
  matches <- matches[[1]]

  expect_identical(matches, list())
})

test_that("using `*` with no other restricting condition results in 1 match per node", {
  # No comments here, so no matches
  text <- "
1 + x
fn(y)
  "

  source <- R'[
  (
    (comment)* @comment
  )
  ]'

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  matches <- query_matches(query, node)

  # Only 1 pattern
  matches <- matches[[1]]

  expect_identical(length(matches), as.integer(node_descendant_count(node)))

  # Each one is an empty match - the `*` results in a match even with 0 hits
  # because it is "zero or more"
  expect_identical(matches[[1]]$name, character())
  expect_identical(matches[[1]]$node, list())
})

test_that("can restrict `range`", {
  text <- "
# hi
# hi again
  "

  source <- "
  (
    (comment) @comment
  )
  "

  range <- range(0, point(0, 0), 5, point(1, 4))

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  matches <- query_matches(query, node, range = range)

  # Only 1 pattern
  matches <- matches[[1]]

  # Only finds first `# hi`
  expect_length(matches, 1L)
  expect_identical(matches[[1]]$name, "comment")
  expect_identical(node_text(matches[[1]]$node[[1]]), "# hi")
})

# ------------------------------------------------------------------------------
# query_captures()

test_that("returns ordered list of captures", {
  text <- "
a + b + a + ab
and(a)
  "

  source <- "
  (
    (identifier) @id
    (#eq? @id a)
  )
  (
    (identifier) @id2
    (#eq? @id2 b)
  )
  "

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  captures <- query_captures(query, node)

  # Returns ordered list of captures, regardless of pattern
  expect_identical(captures$name, c("id", "id2", "id", "id"))

  expect_snapshot(captures$node)
})

test_that("can restrict `range`", {
  text <- "
# hi
# hi again
  "

  source <- "
  (
    (comment) @comment
  )
  "

  range <- range(0, point(0, 0), 5, point(1, 4))

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  captures <- query_captures(query, node, range = range)

  # Only finds first `# hi`
  expect_identical(captures$name, "comment")
  expect_identical(node_text(captures$node[[1]]), "# hi")
})

# ------------------------------------------------------------------------------
# `#eq?` and `#not-eq?` - strings

test_that("can use `#eq?` with string", {
  text <- "
a + b + a + ab
and(a)
  "

  source <- '
  (
    (identifier) @id
    (#eq? @id "a")
  )
  '

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  captures <- query_captures(query, node)

  # Returns ordered list of captures, regardless of pattern
  expect_length(captures$name, 3)

  expect_identical(node_range(captures$node[[1]]), range(1, point(1, 0), 2, point(1, 1)))
  expect_identical(node_range(captures$node[[2]]), range(9, point(1, 8), 10, point(1, 9)))
  expect_identical(node_range(captures$node[[3]]), range(20, point(2, 4), 21, point(2, 5)))
})

test_that("can use `#not-eq?` with string", {
  text <- "
a + b + a + ab
and(a)
  "

  source <- "
  (
    (identifier) @id
    (#not-eq? @id a)
  )
  "

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  captures <- query_captures(query, node)

  expect_identical(
    vapply(captures$node, node_text, character(1)),
    c("b", "ab", "and")
  )
})

test_that("can repeat capture name across patterns", {
  text <- "b + a"

  source <- "
  (
    (identifier) @id
    (#eq? @id a)
  )
  (
    (identifier) @id
    (#eq? @id b)
  )
  "

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)

  matches <- query_matches(query, node)
  captures <- query_captures(query, node)

  # Split by pattern order
  pattern <- matches[[1]]
  expect_identical(pattern[[1]]$name, "id")
  expect_identical(node_text(pattern[[1]]$node[[1]]), "a")

  pattern <- matches[[2]]
  expect_identical(pattern[[1]]$name, "id")
  expect_identical(node_text(pattern[[1]]$node[[1]]), "b")

  # Returns list of captures ordered by node position
  expect_identical(captures$name, c("id", "id"))
  expect_identical(node_text(captures$node[[1]]), "b")
  expect_identical(node_text(captures$node[[2]]), "a")
})

test_that("can use alternations with `#eq?`", {
  text <- "
x + y
1 + y
z + y
fn(x) + y
  "

  source <- '
  (
    (binary_operator
      lhs: [
        (identifier) @id
        (call) @call
      ]
      operator: "+"
    )
    (#eq? @id x)
  )
  '

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  matches <- query_matches(query, node)

  # Only 1 pattern
  matches <- matches[[1]]

  # 2 matches
  expect_length(matches, 2)

  match <- matches[[1]]
  expect_identical(match$name, "id")
  expect_identical(node_text(match$node[[1]]), "x")

  match <- matches[[2]]
  expect_identical(match$name, "call")
  expect_identical(node_text(match$node[[1]]), "fn(x)")
})

# ------------------------------------------------------------------------------
# `#eq?` and `#not-eq?` - captures

test_that("can use `#eq?` and `#not-eq?` with capture", {
  text <- "
x + y
x + x
a + b
xy + xy
  "

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)

  source <- '
  (
    (binary_operator
      lhs: (identifier) @id1
      operator: "+"
      rhs: (identifier) @id2
    )
    (#eq? @id1 @id2)
  )
  '
  query <- query(language, source)
  matches <- query_matches(query, node)

  # Only 1 pattern
  matches <- matches[[1]]

  # 2 matches
  match <- matches[[1]]
  expect_identical(match$name, c("id1", "id2"))
  expect_identical(vapply(match$node, node_text, character(1)), c("x", "x"))
  expect_identical(node_start_point(match$node[[1]]), point(2, 0))
  expect_identical(node_start_point(match$node[[2]]), point(2, 4))

  match <- matches[[2]]
  expect_identical(match$name, c("id1", "id2"))
  expect_identical(vapply(match$node, node_text, character(1)), c("xy", "xy"))
  expect_identical(node_start_point(match$node[[1]]), point(4, 0))
  expect_identical(node_start_point(match$node[[2]]), point(4, 5))

  source <- '
  (
    (binary_operator
      lhs: (identifier) @id1
      operator: "+"
      rhs: (identifier) @id2
    )
    (#not-eq? @id1 @id2)
  )
  '
  query <- query(language, source)
  matches <- query_matches(query, node)

  # Only 1 pattern
  matches <- matches[[1]]

  # 2 matches
  match <- matches[[1]]
  expect_identical(match$name, c("id1", "id2"))
  expect_identical(vapply(match$node, node_text, character(1)), c("x", "y"))

  match <- matches[[2]]
  expect_identical(match$name, c("id1", "id2"))
  expect_identical(vapply(match$node, node_text, character(1)), c("a", "b"))
})

test_that("can use `#eq?` capture predicate on fairly complicated case", {
  text <- "
ab = abc + 1
def = de + 1
ghi = ghi + 1 # this
ghi = ghi - 1
x = x + match(a, b) # and this
  "

  source <- '
  (
    (binary_operator
      lhs: (identifier) @id1
      operator: "="
      rhs: (binary_operator
        lhs: (identifier) @id2
        operator: "+"
        rhs: (_) @expr
      )
    )
    (#eq? @id1 @id2)
  )
  '

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)
  query <- query(language, source)
  captures <- query_captures(query, node)

  expect_identical(captures$name, c("id1", "id2", "expr", "id1", "id2", "expr"))

  capture <- captures$node[[1]]
  expect_identical(node_text(capture), "ghi")
  expect_identical(node_start_point(capture), point(3, 0))
  expect_identical(node_end_point(capture), point(3, 3))

  capture <- captures$node[[2]]
  expect_identical(node_text(capture), "ghi")
  expect_identical(node_start_point(capture), point(3, 6))
  expect_identical(node_end_point(capture), point(3, 9))

  capture <- captures$node[[3]]
  expect_identical(node_text(capture), "1")

  capture <- captures$node[[4]]
  expect_identical(node_text(capture), "x")
  expect_identical(node_start_point(capture), point(5, 0))
  expect_identical(node_end_point(capture), point(5, 1))

  capture <- captures$node[[6]]
  expect_identical(node_text(capture), "match(a, b)")
})

# ------------------------------------------------------------------------------
# `#match?` and `#not-match?`

test_that("can use `#match?` and `#not-match?` with capture", {
  text <- "
# comment
#' roxygen
#' another
#' hi
# comment2
#' roxy
#' roxy again
  "

  language <- r()
  parser <- parser(language)
  tree <- parser_parse(parser, text)
  node <- tree_root_node(tree)

  source <- r'[
  (
    (comment) @roxygen
    (#match? @roxygen "^#'.*")
  )
  ]'
  query <- query(language, source)
  captures <- query_captures(query, node)

  expect_length(captures$node, 5)

  expect_identical(
    vapply(captures$node, node_text, character(1)),
    c("#' roxygen", "#' another", "#' hi", "#' roxy", "#' roxy again")
  )

  source <- r'[
    (
      (comment) @roxygen
      (#not-match? @roxygen "^#'.*")
    )
    ]'
    query <- query(language, source)
    captures <- query_captures(query, node)

    expect_length(captures$node, 2)

    expect_identical(
      vapply(captures$node, node_text, character(1)),
      c("# comment", "# comment2")
    )
})

# ------------------------------------------------------------------------------
# query_start_byte_for_pattern()

test_that("has OOB handling built in", {
  source <- "(identifier) @id"
  language <- r()
  query <- query(language, source)

  expect_snapshot(error = TRUE, {
    query_start_byte_for_pattern(query, 0)
  })

  expect_identical(query_start_byte_for_pattern(query, 1), 0)
  expect_identical(query_start_byte_for_pattern(query, 2), NA_real_)
  expect_identical(query_start_byte_for_pattern(query, 3), NA_real_)
})

Try the treesitter package in your browser

Any scripts or data that you put into this service are public.

treesitter documentation built on June 24, 2024, 5:07 p.m.