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 -S7

Or in builder.ini:

[profile: dev]
S7: true

What it catches

Builder walks the AST of every transpiled R file in two passes:

  1. 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, …).
  2. 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 callCls(foo = 1) when foo isn’t declared on Cls.
  • Unknown property via @instance@foo (or instance@foo <- ...) when foo isn’t a property of the class bound to instance.
  • 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 <- 3

Command:

builder -input srcr -output R -S7

Output:

[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 property

Type 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. If instance comes from a function call, an if/else branch, 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.