27
27
# ' @seealso [linters] for a complete list of linters available in lintr.
28
28
# ' @export
29
29
sprintf_linter <- function () {
30
- xpath <- "
30
+ call_xpath <- "
31
31
//SYMBOL_FUNCTION_CALL[text() = 'sprintf' or text() = 'gettextf']
32
32
/parent::expr
33
33
/parent::expr[
@@ -39,14 +39,79 @@ sprintf_linter <- function() {
39
39
]
40
40
"
41
41
42
+ pipes <- setdiff(magrittr_pipes , " %$%" )
43
+ in_pipe_xpath <- glue(" self::expr[
44
+ preceding-sibling::*[1][self::PIPE or self::SPECIAL[{ xp_text_in_table(pipes) }]]
45
+ and (
46
+ preceding-sibling::*[2]/STR_CONST
47
+ or SYMBOL_SUB[text() = 'fmt']/following-sibling::expr[1]/STR_CONST
48
+ )
49
+ ]" )
50
+
51
+ is_missing <- function (x ) is.symbol(x ) && ! nzchar(x )
52
+
53
+ # ' Zap sprintf() call to contain only constants
54
+ # '
55
+ # ' Set all extra arguments to 0L if they aren't a constant
56
+ # '
57
+ # ' @param parsed_expr A parsed `sprintf()` call
58
+ # '
59
+ # ' @return A `sprintf()` call with all non-constants replaced by `0L`
60
+ # ' (which is compatible with all sprintf format specifiers)
61
+ zap_extra_args <- function (parsed_expr ) {
62
+ if (" fmt" %in% names(parsed_expr )) {
63
+ fmt_loc <- which(names(parsed_expr ) == " fmt" )
64
+ } else {
65
+ fmt_loc <- 2L
66
+ }
67
+
68
+ if (length(parsed_expr ) > = 3L ) {
69
+ for (i in setdiff(seq_along(parsed_expr ), c(1L , fmt_loc ))) {
70
+ if (! is_missing(parsed_expr [[i ]]) && ! is.atomic(parsed_expr [[i ]])) {
71
+ parsed_expr [[i ]] <- 0L
72
+ }
73
+ }
74
+ }
75
+ parsed_expr
76
+ }
77
+
78
+ # Anticipate warnings of a sprintf() call
79
+ #
80
+ # Try running a static sprintf() call to determine whether it will produce warnings or errors due to format
81
+ # misspecification
82
+ #
83
+ # @param xml An XML node representing a `sprintf()` call (i.e. the `<expr>` node containing the call)
84
+ #
85
+ # @return A string, either `NA_character_` or the text of generated errors and warnings from the `sprintf()` call when
86
+ # replacing all dynamic components by 0, which is compatible with all format specifiers.
87
+ capture_sprintf_warning <- function (xml ) {
88
+ parsed_expr <- xml2lang(xml )
89
+ # convert x %>% sprintf(...) to sprintf(x, ...)
90
+ if (length(xml_find_first(xml , in_pipe_xpath )) > 0L ) {
91
+ arg_names <- names(parsed_expr )
92
+ arg_idx <- 2L : length(parsed_expr )
93
+ parsed_expr [arg_idx + 1L ] <- parsed_expr [arg_idx ]
94
+ names(parsed_expr )[arg_idx + 1L ] <- arg_names [arg_idx ]
95
+ parsed_expr [[2L ]] <- xml2lang(xml_find_first(xml , " preceding-sibling::*[2]" ))
96
+ names(parsed_expr )[2L ] <- " "
97
+ }
98
+ parsed_expr <- zap_extra_args(parsed_expr )
99
+ res <- tryCatch(eval(parsed_expr , envir = baseenv()), warning = identity , error = identity )
100
+ if (inherits(res , " condition" )) {
101
+ conditionMessage(res )
102
+ } else {
103
+ NA_character_
104
+ }
105
+ }
106
+
42
107
Linter(function (source_expression ) {
43
108
if (! is_lint_level(source_expression , " file" )) {
44
109
return (list ())
45
110
}
46
111
47
112
xml <- source_expression $ full_xml_parsed_content
48
113
49
- sprintf_calls <- xml_find_all(xml , xpath )
114
+ sprintf_calls <- xml_find_all(xml , call_xpath )
50
115
51
116
message <- vapply(sprintf_calls , capture_sprintf_warning , character (1L ))
52
117
@@ -59,57 +124,3 @@ sprintf_linter <- function() {
59
124
)
60
125
})
61
126
}
62
-
63
- # ' Zap sprintf() call to contain only constants
64
- # '
65
- # ' Set all extra arguments to 0L if they aren't a constant
66
- # '
67
- # ' @param parsed_expr A parsed `sprintf()` call
68
- # '
69
- # ' @return A `sprintf()` call with all non-constants replaced by `0L`
70
- # ' (which is compatible with all sprintf format specifiers)
71
- # '
72
- # ' @noRd
73
- zap_extra_args <- function (parsed_expr ) {
74
- is_missing <- function (x ) {
75
- is.symbol(x ) && ! nzchar(x )
76
- }
77
-
78
- if (" fmt" %in% names(parsed_expr )) {
79
- fmt_loc <- which(names(parsed_expr ) == " fmt" )
80
- } else {
81
- fmt_loc <- 2L
82
- }
83
-
84
- if (length(parsed_expr ) > = 3L ) {
85
- for (i in setdiff(seq_along(parsed_expr ), c(1L , fmt_loc ))) {
86
- if (! is_missing(parsed_expr [[i ]]) && ! is.atomic(parsed_expr [[i ]])) {
87
- parsed_expr [[i ]] <- 0L
88
- }
89
- }
90
- }
91
- parsed_expr
92
- }
93
-
94
- # ' Anticipate warnings of a sprintf() call
95
- # '
96
- # ' Try running a static sprintf() call to determine whether it will produce warnings or errors due to format
97
- # ' misspecification
98
- # '
99
- # ' @param xml An XML node representing a `sprintf()` call (i.e. the `<expr>` node containing the call)
100
- # '
101
- # ' @return A string, either `NA_character_` or the text of generated errors and warnings from the `sprintf()` call when
102
- # ' replacing all dynamic components by 0, which is compatible with all format specifiers.
103
- # '
104
- # ' @noRd
105
- capture_sprintf_warning <- function (xml ) {
106
- text <- get_r_code(xml )
107
- parsed_expr <- try_silently(parse(text = text , keep.source = FALSE )[[1L ]])
108
- parsed_expr <- zap_extra_args(parsed_expr )
109
- res <- tryCatch(eval(parsed_expr , envir = baseenv()), warning = identity , error = identity )
110
- if (inherits(res , " condition" )) {
111
- conditionMessage(res )
112
- } else {
113
- NA_character_
114
- }
115
- }
0 commit comments