Skip to content

Commit 903538e

Browse files
committed
use_claude_code()
1 parent e91fe6b commit 903538e

4 files changed

Lines changed: 262 additions & 6 deletions

File tree

.Rbuildignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@
2323
^CRAN-SUBMISSION$
2424
^[.]?air[.]toml$
2525
^\.vscode$
26+
^\.claude$

.claude/CLAUDE.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Rscript -e "devtools::test_active_file('R/{name}.R')"
1818
# To run a single test "blah" for R/{name}.R
1919
Rscript -e "devtools::test_active_file('R/{name}.R', desc = 'blah')"
2020
21-
# To document the package
21+
# To redocument the package
2222
Rscript -e "devtools::document()"
2323
2424
# To check pkgdown documentation
@@ -35,7 +35,7 @@ air format .
3535

3636
* Always run `air format .` after generating code
3737
* Use the base pipe operator (`|>`) not the magrittr pipe (`%>%`)
38-
* Don't use `_$x` or `_$[["x"]]` since dbplyr must work on R 4.1.
38+
* Don't use `_$x` or `_$[["x"]]` since this package must work on R 4.1.
3939
* Use `\() ...` for single-line anonymous functions. For all other cases, use `function() {...}`
4040

4141
### Testing
@@ -51,16 +51,21 @@ air format .
5151
- Wrap roxygen comments at 80 characters.
5252
- Internal functions should not have roxygen documentation.
5353
- Whenever you add a new (non-internal) documentation topic, also add the topic to `_pkgdown.yml`.
54+
- Always re-document the package after changing a roxygen2 comment.
5455
- Use `pkgdown::check_pkgdown()` to check that all topics are included in the reference index.
5556

5657
### `NEWS.md`
5758

5859
- Every user-facing change should be given a bullet in `NEWS.md`. Do not add bullets for small documentation changes or internal refactorings.
5960
- Each bullet should briefly describe the change to the end user and mention the related issue in parentheses.
60-
- A bullet can consist of multiple sentences but should not contain any new lines (i.e. don't wrap the bullet).
61+
- A bullet can consist of multiple sentences but should not contain any new lines (i.e. DO NOT line wrap).
6162
- If the change is related to a function, put the name of the function early in the bullet.
6263
- Order bullets alphabetically by function name. Put all bullets that don't mention function names at the beginning.
6364

65+
### GitHub
66+
67+
- If you use `gh` to retrieve information about an issue, always use `--comments` to read all the comments.
68+
6469
### Writing
6570

6671
- Use sentence case for headings.
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
---
2+
name: types-check
3+
description: Validate function inputs in R using a standalone file of check_* functions. Use when writing exported R functions that need input validation, reviewing existing validation code, or when creating new input validation helpers.
4+
---
5+
6+
# Input validation in R functions
7+
8+
This skill describes tidyverse style for validating function inputs. It focuses on rlang's exported type checkers along with the standalone file of `check_*` functions. These functions are carefully designed to produce clear, actionable error messages:
9+
10+
```r
11+
check_string(123)
12+
#> Error: `123` must be a single string, not the number 123.
13+
14+
check_number_whole(3.14, min = 1, max = 10)
15+
#> Error: `3.14` must be a whole number, not the number 3.14.
16+
```
17+
18+
It assumes that the user has already run `usethis::use_standalone("r-lib/rlang", "types-check")`, and imports rlang in their package. If not, confirm with the user before continuing.
19+
20+
## Function reference
21+
22+
### Scalars (single values)
23+
24+
Use scalar checkers when arguments parameterise the function (configuration flags, names, single counts), rather than represent vectors of user data. They all assert a single value.
25+
26+
- `check_bool()`: Single TRUE/FALSE (use for flags/options)
27+
- `check_string()`: Single string (allows empty `""` by default)
28+
- `check_name()`: Single non-empty string (for variable names, symbols as strings)
29+
- `check_number_whole()`: Single integer-like numeric value
30+
- `check_number_decimal()`: Single numeric value (allows decimals)
31+
32+
By default, scalar checkers do _not_ allow `NA` elements (`allow_na = FALSE`). Set `allow_na = TRUE` when missing values are allowed.
33+
34+
With the number checkers you can use `min` and `max` arguments for range validation, and `allow_infinite` (default `TRUE` for decimals, `FALSE` for whole numbers).
35+
36+
### Vectors
37+
38+
- `check_logical()`: Logical vector of any length
39+
- `check_character()`: Character vector of any length
40+
- `check_data_frame()`: A data frame object
41+
42+
By default, vector checkers allow `NA` elements (`allow_na = TRUE`). Set `allow_na = FALSE` when missing values are not allowed.
43+
44+
### Optional values: `allow_null`
45+
46+
Use `allow_null = TRUE` when `NULL` represents a valid "no value" state, similar to `Option<T>` in Rust or `T | null` in TypeScript:
47+
48+
```r
49+
# NULL means "use default timeout"
50+
check_number_decimal(timeout, allow_null = TRUE)
51+
```
52+
53+
The tidyverse style guide recommends using `NULL` defaults instead of `missing()` defaults, so this pattern comes up often in practice.
54+
55+
## Other helpers
56+
57+
These functions are exported by rlang.
58+
59+
- `arg_match()`: Validates enumerated choices. Use when an argument must be one of a known set of strings.
60+
61+
```r
62+
# Validates and returns the matched value
63+
my_plot <- function(color = c("red", "green", "blue")) {
64+
color <- rlang::arg_match(color)
65+
# ...
66+
}
67+
68+
my_plot("redd")
69+
#> Error in `my_plot()`:
70+
#> ! `color` must be one of "red", "green", or "blue", not "redd".
71+
#> ℹ Did you mean "red"?
72+
```
73+
74+
Note that partial matching is an error, unlike `base::match.arg()`.
75+
76+
- `check_exclusive()` ensures only one of two arguments can be supplied. Supplying both together (i.e. both of them are non-`NULL`) is an error. Use `.require = TRUE` if both can be omitted.
77+
78+
- `check_required()`: Nice error message if required argument is not supplied.
79+
80+
## `call` and `arg` arguments
81+
82+
All check functions have `call` and `arg` arguments, but you should never use these unless you are creating your own `check_` function (see below for more details).
83+
84+
## When to validate inputs
85+
86+
**Validate at entry points, not everywhere.**
87+
88+
Input validation should happen at the boundary between user code and your package's internal implementation:
89+
90+
- **Exported functions**: Functions users call directly
91+
- **Functions accepting user data**: Even internal functions if they directly consume user input, or external data (e.g. unserialised data)
92+
93+
Once inputs are validated at these entry points, internal helper functions can trust the data they receive without checking again.
94+
95+
A good analogy to keep in mind is gradual typing. Think of input validation like TypeScript type guards. Once you've validated data at the boundary, you can treat it as "typed" within your internal functions. Additional runtime checks are not needed. The entry point validates once, and all downstream code benefits.
96+
97+
Exception: Validate when in doubt. Do validate in internal functions if:
98+
- The cost of invalid data is high (data corruption, security issues)
99+
- The function or context is complex and you want defensive checks
100+
101+
Example of validating arguments of an exported function:
102+
103+
```r
104+
# Exported function: VALIDATE
105+
#' @export
106+
create_report <- function(title, n_rows) {
107+
check_string(title)
108+
check_number_whole(n_rows, min = 1)
109+
110+
# Now call helpers with validated data
111+
data <- generate_data(n_rows)
112+
format_report(title, data)
113+
}
114+
```
115+
116+
Once data is validated at the entry point, internal helpers can skip validation:
117+
118+
```r
119+
# Internal helper: NO VALIDATION NEEDED
120+
generate_data <- function(n_rows) {
121+
# n_rows is already validated, just use it
122+
data.frame(
123+
id = seq_len(n_rows),
124+
value = rnorm(n_rows)
125+
)
126+
}
127+
128+
# Internal helper: NO VALIDATION NEEDED
129+
format_report <- function(title, data) {
130+
# title and data are already validated, just use them
131+
list(
132+
title = title,
133+
summary = summary(data),
134+
rows = nrow(data)
135+
)
136+
}
137+
```
138+
139+
Note how the `data` generated by `generate_data()` doesn't need validation either. Internal code creating data in a trusted way (e.g. because it's simple or because it's covered by unit tests) doesn't require internal checks.
140+
141+
## Early input checking
142+
143+
Always validate inputs at the start of user-facing functions, before doing any work:
144+
145+
```r
146+
my_function <- function(x, name, env = caller_env()) {
147+
check_logical(x)
148+
check_name(name)
149+
check_environment(env)
150+
151+
# ... function body
152+
}
153+
```
154+
155+
Benefits:
156+
157+
- This self-documents the types of the arguments
158+
- Eager evaluation also reduces the risk of confusing lazy evaluation effects
159+
160+
## Custom validation functions
161+
162+
Most packages will need one or more unique checker functions. Sometimes it's sufficient to wrap existing check functions with custom arguments. In this case you just need to carefully pass through the `arg` and `call` arguments. In other cases, you want a completely new check in which case you can call `stop_input_type` with your own arguments.
163+
164+
### Wrapping existing `check_` functions
165+
166+
When creating a wrapper or helper function that calls `check_*` functions on behalf of another function, you **must** propagate the caller context. Otherwise, errors will point to your wrapper function instead of the actual entry point.
167+
168+
Without proper propagation, error messages show the wrong function and argument names:
169+
170+
```r
171+
# WRONG: errors will point to check_positive's definition
172+
check_positive <- function(x) {
173+
check_number_whole(x, min = 1)
174+
}
175+
176+
my_function <- function(count) {
177+
check_positive(count)
178+
}
179+
180+
my_function(-5)
181+
#> Error in `check_positive()`: # Wrong! Should say `my_function()`
182+
#> ! `x` must be a whole number larger than or equal to 1. # Wrong! Should say `count`
183+
```
184+
185+
With proper propagation, errors correctly identify the entry point and argument:
186+
187+
```r
188+
# CORRECT: propagates context from the entry point
189+
check_positive <- function(x, arg = caller_arg(x), call = caller_env()) {
190+
check_number_whole(x, min = 1, arg = arg, call = call)
191+
}
192+
193+
my_function <- function(count) {
194+
check_positive(count)
195+
}
196+
197+
my_function(-5)
198+
#> Error in `my_function()`: # Correct!
199+
#> ! `count` must be a whole number larger than or equal to 1. # Correct!
200+
```
201+
202+
Note how `arg` and `call` are part of the function signature. That allows them to be wrapped again by another checking function that can pass down its own context.
203+
204+
### Creating a new `check_` function
205+
206+
When constructing your own `check_` function you can call `stop_input_type()` to take advantage of the existing infrastructure for generating error messages.
207+
For example, imagine we wanted to create a function that checked that the input was a single date:
208+
209+
```R
210+
check_date <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) {
211+
if (!missing(x) && is.Date(x) && length(x) == 1) {
212+
return(invisible())
213+
}
214+
215+
stop_input_type(
216+
x,
217+
"a single Date",
218+
...,
219+
allow_null = allow_null,
220+
arg = arg,
221+
call = call
222+
)
223+
}
224+
```
225+
226+
Note you must always check first that the input is not missing, as `stop_input_type()` handles this case specially.
227+
228+
Sometimes you want to check if something is a compound type:
229+
230+
```R
231+
check_string_or_bool <- function(x, ..., arg = caller_arg(x), call = caller_env()) {
232+
if (!missing(x)) {
233+
if (is_string(x) || isTRUE(x) || isFALSE(x)) {
234+
return(invisible())
235+
}
236+
}
237+
238+
stop_input_type(
239+
x,
240+
c("a string", "TRUE", "FALSE"),
241+
...,
242+
arg = arg,
243+
call = call
244+
)
245+
}
246+
```
247+
248+
Note that the second argument to `stop_input_type()` can take a vector, and it will automatically places commas and "and" in the appropriate locations.
249+
250+
Generally, you should place this `check_` function close to the function that is usually used to construct the object being checked (e.g. close to the S3/S4/S7 constructor.)

.claude/skills/tidy-deprecate-function/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Guide for deprecating R functions/arguments. Use when a user asks t
55

66
# Deprecate functions and function arguments
77

8-
Use this skill when deprecating functions or function parameters in dbplyr.
8+
Use this skill when deprecating functions or function parameters in this package.
99

1010
## Overview
1111

@@ -124,10 +124,10 @@ Then re-document the package.
124124

125125
### Step 5: Add NEWS entry
126126

127-
Add a bullet point to the top of the "# dbplyr (development version)" section in NEWS.md:
127+
Add a bullet point to the top of the "# packagename (development version)" section in NEWS.md:
128128

129129
```markdown
130-
# dbplyr (development version)
130+
# packagename (development version)
131131

132132
* `function_name(parameter)` is deprecated and will be removed in a future
133133
version.

0 commit comments

Comments
 (0)