Skip to content

Commit 10c78f0

Browse files
export xp_call_name() for downstream use (#2096)
* export xp_call_name() for downstream use * Update _pkgdown.yaml * delint * fix NEWS, typo, document expr
1 parent c21600a commit 10c78f0

File tree

6 files changed

+97
-0
lines changed

6 files changed

+97
-0
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export(whitespace_linter)
143143
export(with_defaults)
144144
export(with_id)
145145
export(xml_nodes_to_lints)
146+
export(xp_call_name)
146147
export(yoda_test_linter)
147148
importFrom(cyclocomp,cyclocomp)
148149
importFrom(glue,glue)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* `conjunct_test_linter()` also lints usage like `dplyr::filter(x, A & B)` in favor of using `dplyr::filter(x, A, B)` (part of #884, @MichaelChirico).
3131
* `sort_linter()` checks for code like `x == sort(x)` which is better served by using the function `is.unsorted()` (part of #884, @MichaelChirico).
3232
* `paste_linter()` gains detection for file paths that are better constructed with `file.path()`, e.g. `paste0(dir, "/", file)` would be better as `file.path(dir, file)` (part of #884, @MichaelChirico).
33+
* New `xp_call_name()` helper to facilitate writing custom linters (#2023, @MichaelChirico). This helper converts a matched XPath to the R function to which it corresponds. This is useful for including the "offending" function in the lint's message.
3334

3435
### New linters
3536

R/xp_utils.R

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,41 @@ xp_and <- function(...) paren_wrap(..., sep = "and")
4444
#' @noRd
4545
xp_or <- function(...) paren_wrap(..., sep = "or")
4646

47+
#' Get the name of the function matched by an XPath
48+
#'
49+
#' Often, it is more helpful to custom-tailer the `message` of a lint to record
50+
#' which function was matched by the lint logic. This function encapsualtes
51+
#' the logic to pull out the matched call in common situations.
52+
#'
53+
#' @param expr An `xml_node` or `xml_nodeset`, e.g. from [xml2::xml_find_all()].
54+
#' @param depth Integer, default `1L`. How deep in the AST represented by `expr`
55+
#' should we look to find the call? By default, we assume `expr` is matched
56+
#' to an `<expr>` node under which the corresponding `<SYMBOL_FUNCTION_CALL>`
57+
#' node is found directly. `depth = 0L` means `expr` is matched directly
58+
#' to the `SYMBOL_FUNCTION_CALL`; `depth > 1L` means `depth` total `<expr>`
59+
#' nodes must be traversed before finding the call.
60+
#' @param condition An additional (XPath condition on the `SYMBOL_FUNCTION_CALL`
61+
#' required for a match. The default (`NULL`) is no condition. See examples.
62+
#'
63+
#' @examples
64+
#' xml_from_code <- function(str) {
65+
#' xml2::read_xml(xmlparsedata::xml_parse_data(parse(text = str, keep.source = TRUE)))
66+
#' }
67+
#' xml <- xml_from_code("sum(1:10)")
68+
#' xp_call_name(xml, depth = 2L)
69+
#'
70+
#' xp_call_name(xml2::xml_find_first(xml, "expr"))
71+
#'
72+
#' xml <- xml_from_code(c("sum(1:10)", "sd(1:10)"))
73+
#' xp_call_name(xml, depth = 2L, condition = "text() = 'sum'")
74+
#'
75+
#' @export
4776
xp_call_name <- function(expr, depth = 1L, condition = NULL) {
77+
stopifnot(
78+
inherits(expr, c("xml_node", "xml_nodeset")),
79+
is.numeric(depth), depth >= 0L,
80+
is.null(condition) || is.character(condition)
81+
)
4882
if (is.null(condition)) {
4983
node <- "SYMBOL_FUNCTION_CALL"
5084
} else {

_pkgdown.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ reference:
4141
- get_r_string
4242
- use_lintr
4343
- xml_nodes_to_lints
44+
- xp_call_name
4445

4546
- title: Meta-tooling
4647
contents:

man/xp_call_name.Rd

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-xp_utils.R

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
test_that("xp_call_name works", {
2+
xml_from_code <- function(str) {
3+
xml2::read_xml(xmlparsedata::xml_parse_data(parse(text = str, keep.source = TRUE)))
4+
}
5+
xml <- xml_from_code("sum(1:10)")
6+
expect_identical(xp_call_name(xml, depth = 2L), "sum")
7+
8+
expect_identical(xp_call_name(xml2::xml_find_first(xml, "expr")), "sum")
9+
10+
xml <- xml_from_code(c("sum(1:10)", "sd(1:10)"))
11+
expect_identical(xp_call_name(xml, depth = 2L, condition = "text() = 'sum'"), "sum")
12+
})
13+
14+
test_that("xp_call_name input validation works", {
15+
expect_error(xp_call_name(2L), "inherits(expr", fixed = TRUE)
16+
17+
xml <- xml2::read_xml("<a></a>")
18+
expect_error(xp_call_name(xml, depth = -1L), "depth >= 0", fixed = TRUE)
19+
expect_error(xp_call_name(xml, depth = "1"), "is.numeric(depth)", fixed = TRUE)
20+
expect_error(xp_call_name(xml, condition = 1L), "is.character(condition)", fixed = TRUE)
21+
})

0 commit comments

Comments
 (0)