check-R-code
Standards derived from the check-R-code skill
Derived from the check-R-code skill (project).
Source file: ~/.claude/skills/check-R-code/SKILL.md
Last synced: 2026-08-19
0.1 Purpose
Review R code for tidyverse style compliance, correctness, and modern best practices
0.2 Standard
1 Check R Code for Tidyverse Compliance
When reviewing R code, apply these standards systematically. Report findings grouped by category, most critical first.
1.1 Priority Tiers
Critical — breaks code or modern tidyverse practice: - Wrong pipe operator (%>% vs |>) - Incorrect join syntax (not using join_by()) - Inefficient grouping patterns - map_dfr() / map_dfc() instead of map() |> list_rbind() - Base R string functions instead of stringr - sapply() with implicit type coercion
High — violates current best practices: - Non-snake_case names - group_by() |> ... |> ungroup() pattern (should be .by =) - head() instead of slice_head() (strips attributes) - Labeled data not using modern ggplot2 4.0+ auto-labeling - Awkward data masking (missing {{}} or .data[[]]) - Code not formatted with the air formatter
Medium — style/readability concerns: - Spacing around operators or arguments - Comment quality (explains WHY not WHAT) - Function/variable naming inconsistency
Low — optimization opportunities: - Vectorization suggestions - Performance tweaks in non-critical code
1.2 Formatting
Standard: All R code must be formatted with the air formatter.
# Format a single file
air format path/to/file.R
# Format a whole project
air format .
# Check formatting without writing (CI-friendly)
air format --check .Flag any code that hasn’t been run through air — inconsistent spacing, indentation, or line-wrapping that air format would normalize is a High finding. Recommend running air format . on the project before/after review rather than manually reformatting by hand.
1.3 Pipe Operator
Standard: Native pipe |> (R 4.1+)
❌ Anti-patterns:
data %>% filter(x > 5) %>% mutate(y = x * 2)
x %>% map(~f(.x)) %>% unlist()✅ Correct:
data |> filter(x > 5) |> mutate(y = x * 2)
x |> map(\(x) f(x)) |> list_rbind()Exception: Older codebases locked on R < 4.1 or magrittr-only code. If you see %>%, flag it as CRITICAL unless the codebase declares a minimum R version < 4.1.
1.4 Joins
Standard: join_by() for all join conditions (dplyr 1.1+)
❌ Anti-patterns:
inner_join(x, y, by = c("id" = "user_id"))
left_join(a, b, by = c("year" = "fiscal_year", "amount" = "value"))✅ Correct:
inner_join(x, y, by = join_by(id == user_id))
left_join(a, b, join_by(year == fiscal_year, amount == value))Quality control: On 1:1 joins expected to match cleanly, add:
inner_join(x, y, join_by(id), multiple = "error", unmatched = "error")1.5 Grouping
Standard: .by = parameter (dplyr 1.1+) — always returns ungrouped
❌ Anti-patterns:
data |> group_by(category) |> summarise(mean_val = mean(x)) |> ungroup()
data |> group_by(a, b) |> mutate(cumsum = cumsum(x)) |> ungroup()
data |> group_by(year) |> slice_head(n = 2)✅ Correct:
data |> summarise(mean_val = mean(x), .by = category)
data |> mutate(cumsum = cumsum(x), .by = c(a, b))
data |> slice_head(n = 2, .by = year)When to use persistent grouping: Only when grouping applies to multiple consecutive operations AND you explicitly ungroup after. Flag if not intentional.
1.6 Map & purrr
Standard: map() + list_rbind() / list_cbind(); lambda shorthand with \(x)
❌ Anti-patterns:
map_dfr(splits, train_model) # superseded
map_dfc(cols, normalize) # superseded
sapply(x, sqrt) # implicit type coercion risky
map(x, ~f(.x)) # old formula syntax✅ Correct:
splits |> map(train_model) |> list_rbind()
cols |> map(normalize) |> list_cbind()
x |> map_dbl(sqrt) # explicit type
x |> map(\(x) f(x)) # lambda shorthandwalk2 for side effects:
walk2(data_list, names_list, \(df, name) ggsave(name, plot(df)))1.7 String Functions
Standard: stringr package (never base R string functions)
❌ Anti-patterns:
grepl("pattern", text) # use str_detect()
sub("old", "new", x) # use str_replace()
gsub("a", "b", text) # use str_replace_all()
substr(text, 1, 5) # use str_sub()
nchar(text) # use str_length()
toupper(text); tolower(text) # use str_to_upper(); str_to_lower()
paste0(a, b, c) # use str_c()
paste("Hello", name, sep = " ") # use str_glue("Hello {name}")
strsplit(text, ",") # use str_split()
trimws(text) # use str_trim()✅ Correct:
str_detect(text, "pattern")
str_replace(x, "old", "new")
str_replace_all(text, "a", "b")
str_sub(text, 1, 5)
str_length(text)
str_to_upper(text); str_to_lower(text)
str_c(a, b, c)
str_glue("Hello {name}")
str_split(text, ",")
str_trim(text)1.8 Naming
Standard: All names in snake_case; nouns for variables, verbs for functions
❌ Anti-patterns:
user_Data; UserCount; calculate.total; THRESHOLD✅ Correct:
user_data; user_count; calculate_total; threshold
my_function <- function(...) { }
daily_revenue <- data |> summarise(...)1.9 Data Masking & Quoting
Embrace {{}} for function arguments:
summarise_by <- function(.data, group_var, summary_var) {
.data |>
summarise(mean = mean({{ summary_var }}), .by = {{ group_var }})
}
# Call: data |> summarise_by(group_var = category, summary_var = sales)Character column names with .data[[]]:
for (col in names(df)) {
result <- df |> count(.data[[col]])
}
# Or across() for multiple columns:
df |> mutate(across(where(is.numeric), log, .names = "log_{.col}"))1.10 ggplot2 4.0+ Label Integration
Auto-labeling from SPSS variable labels (haven):
✅ Correct (labels come from var_label() automatically):
# Variable labels on axes automatically
ggplot(df, aes(satisfaction, engagement)) + geom_point()
# Bulk override:
ggplot(df, aes(var1, var2)) + geom_point() + labs(dictionary = unlist(var_label(df)))Value labels (factor categories):
# Convert labeled numeric to factor with value labels:
df |> mutate(across(where(is.labelled), to_factor))
df |> mutate(gender = to_factor(gender)) # selective conversion
# Strip labels, keep numeric:
zap_labels(x)Caveats (auto-labeling fails): - Transformations in aes(): aes(x = log(income)) breaks labeling — use pure variable names - aes(x = .data$varname) is fine — the .data pronoun still counts as a pure reference and preserves labeling - head() strips attributes — use slice_head() instead
Label priority (low → high): - aes() expression - labs(dictionary=) - Column label attribute - labs(<aes>=) - scale_*(name=) - guide_*(title=)
Related packages (not a formal dependency — ggplot2 just reads the generic label attribute; haven/labelled happen to set it):
# ggplot2 >= 4.0.0
# labelled >= 2.16.0 (for var_label, to_factor, val_labels, look_for)
# haven >= 2.5.5 (for zap_labels, as_factor, reading SPSS/Stata/SAS files)1.11 Labeled Data (labelled package)
var_label(df) # Get all variable labels
var_label(df$col) <- "Label" # Set label
val_labels(df$col) # Get value labels
to_factor(x) # Convert labeled numeric to factor
zap_labels(x) # Strip labels, keep numeric
look_for(df, "keyword") # Search variable names and labels1.12 Anti-Patterns Checklist
| Anti-Pattern | Correct | Category |
|---|---|---|
%>% |
\|> |
Critical |
by = c("a" = "b") |
join_by(a == b) |
Critical |
group_by() \|> ungroup() |
.by = |
Critical |
map_dfr(), map_dfc() |
map() \|> list_rbind/cbind() |
Critical |
grepl(), gsub() |
str_detect(), str_replace_all() |
Critical |
sapply() |
map_dbl(), map_chr() |
Critical |
head() on labeled data |
slice_head() |
High |
| camelCase, PascalCase | snake_case |
High |
| Base string functions | stringr |
Critical |
paste0() for templates |
str_glue() |
Medium |
nchar() |
str_length() |
Critical |
strsplit() |
str_split() |
Critical |
1.13 Reporting Format
When reviewing R code, structure findings as:
- Critical issues (breaks modern tidyverse or correctness)
- High-priority issues (violates best practices)
- Medium issues (style/readability)
- Low issues (optimization)
For each finding, cite: - File & line number - What: the anti-pattern found - Why: the standard and reasoning - Fix: the corrected code - Impact: whether it’s a code issue or style
Example:
### Critical: Wrong pipe operator
**File:** analysis.R:42
**Issue:** Using `%>%` instead of `|>`
**Why:** Native pipe is standard in R 4.1+; cleaner, no magrittr dependency
**Current:** data %>% filter(x > 5) %>% mutate(...)
**Fixed:** data |> filter(x > 5) |> mutate(...)
1.14 Context-Dependent Exceptions
- R < 4.1:
%>%is correct; flag if minimum version isn’t declared - Legacy codebases: Document why older patterns remain; deprecate incrementally
- Performance-critical code:
data.tableor base R optimizations may be justified; document trade-off - Non-tidyverse projects: Adapt standards to project’s declared approach
1.15 Notes for reviewers
This section is not auto-generated. Add concrete examples, the “why” behind each rule, and links to this project’s actual code (shared setup scripts, theme functions, etc.) that the skill above may only refer to abstractly.