S7 Type Checks
Builder can statically validate code written against the S7 object system
with the -S7 flag. S7 is R’s struct-style OO
system: classes have declared properties with declared types,
and Builder uses that declared shape to catch mistakes before
the code ever runs.
⚠️ Warning: this feature is vibe-coded to some extent and static analysis of R code is notoriously difficult given the capabilities of the language, these are the reasons this feature is not enabled by default. False positives may be reported.
Usage
builder -input srcr -output R -S7Or in builder.ini:
[profile: dev]
S7: trueWhat it catches
Builder walks the AST of every transpiled R file in two passes:
- Pass 1: collects S7 class definitions —
both bare (
new_class(...)) and namespaced (S7::new_class(...)) — and records each class’s declared property names and type constructors (class_character,class_numeric,class_integer,class_logical,class_function,class_list, …). - Pass 2: walks every expression. When it
sees a constructor call, an
@access, or an@<-assignment it cross-checks against the class table.
Three categories of mistake are reported:
- Unknown property in a constructor call —
Cls(foo = 1)whenfooisn’t declared onCls. - Unknown property via
@—instance@foo(orinstance@foo <- ...) whenfooisn’t a property of the class bound toinstance. - Type mismatch on a literal — for both
constructor args and
@<-assignments, where the literal value clearly disagrees with the declared property type. Only unambiguous literal mismatches are reported; values that come from a variable, a function call, or any expression Builder cannot resolve are skipped silently.
Example
Input:
# srcr/S7.R
Dog <- S7::new_class("Dog", properties = list(
name = S7::class_character(),
age = S7::class_numeric(),
good_boy = S7::class_logical()
))
rex <- Dog(name = "Rex", age = 4, good_boy = TRUE)
# error: property 'nme' does not exist
fido <- Dog(nme = "Fido")
# error: 'name' expects character, got integer literal
buddy <- Dog(name = 1L)
# error: 'good_boy' expects logical, got character
spot <- Dog(good_boy = "yes")
# ok
greeting <- rex@name
# error: 'colour' not declared on Dog
colour <- rex@colour
# error: assigning numeric to logical property
rex@good_boy <- 3Command:
builder -input srcr -output R -S7Output:
[INFO] Running S7 type checks...
[WARNING] Unknown S7 property 'nme' for class 'Dog' - R/S7.R:3
[WARNING] S7 property 'name' of class 'Dog' expects class_character but got integer - R/S7.R:4
[WARNING] S7 property 'good_boy' of class 'Dog' expects class_logical but got character - R/S7.R:5
[WARNING] Unknown S7 property 'colour' for class 'Dog' - R/S7.R:7
[WARNING] S7 property 'good_boy' of class 'Dog' expects class_logical but got numeric - R/S7.R:8
The valid uses (rex <- Dog(...),
greeting <- rex@name) emit nothing.
Cross-File Detection
Class definitions and instance bindings are tracked across
all files in a single run, so a class declared in
srcr/classes.R and used from
srcr/main.R is checked end-to-end:
# srcr/classes.R
Dog <- S7::new_class("Dog", properties = list(name = S7::class_character()))# srcr/main.R
rex <- Dog(name = "Rex") # ok — Builder resolves Dog from classes.R
hmm <- Dog(nme = "Rex") # flagged — unknown propertyType Constructor Mapping
Literal values are matched against declared property types as follows:
| S7 constructor | Accepts (literal) | Rejects (literal) |
|---|---|---|
class_character |
"..." (STRSXP) |
logical, numeric, integer |
class_numeric, class_double |
1, 1L, 1.5 |
character, logical |
class_integer |
1L (INTSXP) |
character, logical |
class_logical |
TRUE, FALSE, NA |
character, numeric, integer |
class_function |
function(...) ... |
character, numeric, integer, logical |
class_list |
list(...) |
character, numeric, integer, logical |
Bare numeric literals like 1 parse as
REALSXP, so they are accepted for
class_numeric/class_double but
not flagged against class_integer
(since they may be intended as integers — only clearly wrong
types are reported).
Anything else — a variable, a function call,
NULL, a c(...) expression — is
unknowable from the AST alone and is skipped without a
warning.
Limitations
- Cross-file flow: only
instance <- Cls(...)bindings are tracked. Ifinstancecomes from a function call, anif/elsebranch, or any expression that doesn’t directly call a known class constructor,@checks against it are skipped. method()registration:method(generic, Class) <- function(...) ...is not yet validated.- Accessor functions:
prop(),props(),prop<-are not checked. - Validators: validator functions are not analysed.
- Line numbers: warnings report top-level
expression indices in the transpiled output (same convention as
-deadcode) since the post-strip output’s lines no longer match the original source.