Capabilities
Qualitative overview of flowR's support for R.
Please keep in mind that the capabilities are a qualitative measure.
Statements like "flowR can fully handle 50/80 capabilities" are discouraged as some capabilities have a vastly different granularity (and different levels of real-world presence).
Missing something? Suggest a capability!
Nothing matches that.
Names and Identifiers
Every name an R program writes down, the Form it is written in, and the binding it resolves to.
20 fully8 partially2 notExpressions
The calls, Operators, control flow, and function definitions an R program is built from.
CallsIndex AccessOperatorsControl-FlowFunction DefinitionsImportant Built-InsLiteral Values
52 fully20 partially1 notNon-Standard Evaluations/Semantics
The places where R does not evaluate an expression the way it is written, so a name in it may mean something other than the binding it seems to read.
1 fully5 partially1 notObject-Oriented Programming
R's object systems, the class an object carries, and the dispatch that decides which function body a generic call runs.
S3S4Class-Based Dependency Attribution
2 fully9 partially1 notR File Structure
The lexical shape of a source file, down to its line endings and encoding.
5 fully4 partially0 notProject
The files around the R code, from the package metadata and the packages a version manager pins to what runs before a script does.
Package MetadataDependency ManagersStartup and DiscoveryPre-Processors/external Tooling
9 fully4 partially0 notSystem, I/O, FFI, and Other Files
Everything a program reaches for beyond its own code, from files it sources to calls it makes out of R.
0 fully9 partially0 notTypes
What a value is, how we infer it from the code, and the coercions R applies between types.
0 fully1 partially3 notNames and Identifiers
20 fully8 partially2 not
Every name an R program writes down, the Form it is written in, and the binding it resolves to.
Consider the following R code:
"f" <- function(x) { get("x") }
`y x` <- 2
print(`y x` + f(3))Identifiers of interest are:
- The symbols
x(Normal),f(Quoted), and`y x`(Escaped). - The function calls
<-,function,{,get,+, andprint(Calls, all given with Normal). Especially{is identified as a Grouping of the Function Definitions' body. - The quoted name created by a function call
get(Created).
Besides the parameter x, which is resolved in its Lexicographic Scope, the other identifiers are resolved in the Global Scope.
flowchart LR
10["`**function** (L. 1)
*RFunctionDefinition*`"]
subgraph "flow-10" ["function(x) #123; get(#34;x#34;) #125; (L. 1)"]
1["`**x** (L. 1)
*RSymbol*`"]
8-get-name(["`**#34;x#34;** (L. 1)
*RSymbol*`"])
8[["`base#58;#58;**get** (L. 1)
*RFunctionCall*`"]]
built-in:get["`Built-In:
get`"]
style built-in:get stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
9[["`base#58;#58;**#123;**
*RExpressionList*`"]]
built-in:_["`Built-In:
#123;`"]
style built-in:_ stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
end
0["`**#34;f#34;** (L. 1)
*RString*`"]
11[["`base#58;#58;**#60;#45;** (L. 1)
*RBinaryOp*`"]]
built-in:_-["`Built-In:
#60;#45;`"]
style built-in:_- stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
13{{"`**2** (L. 2)
*RNumber*`"}}
12["`**#96;y x#96;** (L. 2)
*RSymbol*`"]
14[["`base#58;#58;**#60;#45;** (L. 2)
*RBinaryOp*`"]]
16(["`**#96;y x#96;** (L. 3)
*RSymbol*`"])
18{{"`**3** (L. 3)
*RNumber*`"}}
20[["`**f** (L. 3)
*RFunctionCall*`"]]
21[["`base#58;#58;**#43;** (L. 3)
*RBinaryOp*`"]]
built-in:_["`Built-In:
#43;`"]
style built-in:_ stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
23[["`base#58;#58;**print** (L. 3)
*RFunctionCall*`"]]
built-in:print["`Built-In:
print`"]
style built-in:print stroke:gray,fill:gray,stroke-width:2px,opacity:.8;
1 -->|"def-by-on-call"| 18
8-get-name -->|"reads"| 1
8 -->|"reads, returns, arg"| 8-get-name
8 -.->|"reads, calls"| built-in:get
linkStyle 3 stroke:gray;
9 -->|"returns, arg"| 8
9 -.->|"reads, calls"| built-in:_
linkStyle 5 stroke:gray;
10 -.-|function| flow-10
0 -->|"defined-by, flow"| 11
0 -->|"defined-by"| 10
11 -->|"reads, arg"| 10
11 -->|"returns, arg"| 0
11 -.->|"reads, calls"| built-in:_-
linkStyle 11 stroke:gray;
12 -->|"defined-by, flow"| 14
12 -->|"defined-by"| 13
14 -->|"reads, arg"| 13
14 -->|"returns, arg"| 12
14 -.->|"reads, calls"| built-in:_-
linkStyle 16 stroke:gray;
16 -->|"reads"| 12
18 -->|"def-on-call"| 1
20 -->|"reads, arg"| 18
20 -->|"reads"| 0
20 -->|"returns"| 8
20 -->|"calls"| 10
21 -->|"reads, arg"| 16
21 -->|"reads, arg"| 20
21 -.->|"reads, calls"| built-in:_
linkStyle 25 stroke:gray;
23 -->|"reads, returns, arg"| 21
23 -.->|"reads, calls"| built-in:print
linkStyle 27 stroke:gray;R Code of the (simplified) Dataflow Graph
The analysis ran (including parse and normalize, using the tree-sitter engine) within the generation environment. No signature database is mounted for these generated graphs, so library() calls attach no package exports; base-R names are still qualified via the generated base-package store (e.g. acf as stats::acf). We encountered unknown side effects (with ids: 23 (linked)) during the analysis.
"f" <- function(x) { get("x") }
`y x` <- 2
print(`y x` + f(3))- #Form
A name is written plainly, as a string, in backticks, or as the argument of a call that reads it as one.
5 children500
A name written as R's grammar allows it, like
aorplot. Every other form ends up as one of these, and a lookup links it to the definitions that may reach it.A name given as a string, like
"a"or'plot'. Such a name may hold spaces, but R accepts the quotes only where it is defined, so reading it back needs backticks or a name-creating call.- #Escaped18 tests10 slice, 8 dataflow, 1 output and 1 more
signature tests
one distractor, without distractors, one hundred distractors
A name escaped in backticks, like
`a`or`my var`. It binds and resolves like a normal name, the syntax only lifts the grammar's restriction on the characters it may hold. A call that reads a string argument as the name it spells, such as
get,get0,mget,exists,match.fun, andassign. The string counts as a use of that name, or forassignas a definition. A name only known at runtime (get(Sys.getenv("V"))) is not linked.A created name whose string argument is not a literal but folds to one. Covers string literals, variables holding one, and
paste0/paste/file.pathover constants, each followed like a written-out name.
Which definitions a name may reach, through the scopes around it, the environments a program builds, and the packages below them.
25 children1582
Resolve a name against the bindings of the global environment, whenever no enclosing function definition binds it. Every top-level definition that may still be active is linked, so a use after an
ifreaches both branches' definitions.Resolve a name to the innermost enclosing definition, and only to the global scope when no enclosing one binds it. Where a function is written decides what its body sees, not where it is called.
Two closures from the same factory keep independent state in R, as every call of the factory makes an environment of its own. We keep one environment per definition instead, so a
<<-in one closure is read as reaching the binding the other captured.A function captures the environment it was defined in, not the values that were in it. So it sees a write made to that environment after its definition, a sibling closure's
<<-included.An environment a program builds and names itself, rather than one a definition opens. Covers
new.env,assign/get/localwithenvir=,e$x,attach,with, and aliasing such an environment through a variable.8 children440
Track environment assignments and reads across branches and loop bodies, a key built at run time such as
paste0("k", i)included.Specifying a parent for a newly-created environment from a dynamic or unknown expression (
new.env(parent = f())). Such a parent falls back to the default (parent.frame()) instead of resolving.Specifying a parent for a newly-created environment from a tracked environment variable or a constant (
new.env(parent = e),new.env(parent = emptyenv())).Aliasing a tracked environment variable (
alias <- e), which binds a second name to the same environment rather than a copy. A write to the original made after the alias is not reflected through it, unlike a read.Reading through an aliased tracked environment variable (
alias <- e). Every assign made up to the alias is visible through it.Evaluating an expression inside a named environment with
with(data, expr). Reads of names the tracked env defines resolve, including through a computeddataargument or a nested call.Reading via
parent.frame()$name, and a write escaping througheval.parent(quote(name <- value)). Works only one call away, and storing the frame first drops the binding.Support for
rm(list=..., envir=sys.frame(N))removing variables from a specific call frame. Currently handles negative and zero offsets from within depth-1 functions.
Handling side-effects through environments, which act as reference types and are not copied when modified. A write through a parameter is kept as an unknown side effect of the call (see Environment Alias and Side-Effects in Function Call).
Resolve a name in call position only against function definitions, a name in value position against every binding. This is why the
c <- 1below does not shadow the call toc.Resolve a name against the packages the search path holds, whenever no scope in the script binds it. Attached packages sit below
.GlobalEnv, so a global binding shadows an export.Programmatically inspecting or mutating the search path with
search(),searchpaths(), or detaching by position. None of this is modelled, andsearch()reads as an unknown call.Separate what a package's namespace holds from what attaching it puts on the search path. The imports environment a package carries for itself is not modelled.
Resolve a name written
pkg::nameto the export it names, no matter what the script binds. This depends on what the signature database knows, and an unresolved name stays unresolved rather than reported.Resolve a name written
pkg:::name, which reaches what the namespace keeps to itself. Whether the name is genuinely internal rather than exported is not checked (see Namespace Exports).Know which names a package's namespace declares exported versus keeps internal. The
namespace-accessrule checks a::/:::choice against what the signature database records, which omits most internal names to stay small.Attach a package named by
library,require,attachNamespace, ... to the search path. From that point on an unbound name may resolve to one of its exports, while a binding in the script still shadows it.Undo a library attach with
detach,unloadNamespace, ... Neither is modelled, so a name used afterdetachstill resolves to the package's export.Open a scope of its own with a plain
localblock. A name it binds is invisible outside, while a super assignment from within reaches the enclosing scope.Send
local's body to a specific environment.new.env()andglobalenv()work, but inside a function that already binds the name the write lands in that frame instead.Resolve a call that does not name the function it runs, as
Recalldoes. It is linked to the enclosing function definition, the same way a call through the function's own name is.
Expressions
52 fully20 partially1 not
The calls, Operators, control flow, and function definitions an R program is built from.
- #Calls646 tests280 slice, 272 dataflow, 72 desugar and 3 more
signature tests
higher-order sum origin, built-in function as a value, quoted call
Everything that is a call in R, from
f(x)over an operator to a group, with the arguments it binds and the side effects it may have.15 children1140
- #Grouping111 tests77 desugar, 22 slice, 13 dataflow and 1 more
signature tests
completely constant, simple constant for-loop, using loop variable in for-body
Read
(and{as the calls they are, mapped to their primitive implementations. A group hands on the value of its last expression, sox <- { 1; 2 }bindsxto2, and redefining`{`replaces that meaning. Link a call like
f(x)orfoo::bar(x, y)to the definitions its name may resolve to, and its arguments to the formals they bind. Every definition that may be active is linked, which over-approximates rather than picking one.7 children520
An argument given without a name, like
f(3), which binds to a formal by position. It is matched after every named argument has taken its formal.An argument left out entirely, as the second one in
foo::bar(3, ,42). It still takes a position, which is what makesm[1, ]an index access on one dimension.An argument given with a name, like
f(x = 3), which binds to the formal of that name. Inside a call=names an argument rather than binding a name.An argument whose name is written as a string, like
f('x' = 3). It binds the same formal a named argument would, the way a quoted name binds an ordinary one.Match every argument of a call to the formal it binds, exact names first, then unique prefixes (
pmatch), then position. A formal behind...matches only exactly, and an ambiguous prefix binds nothing.An argument that binds a name while it is evaluated, as
f(x <- 3)does. Whether the argument is ever forced is not modelled (see promises), sof <- function(a) 1; f(x <- 3)still believesxis 3.A call that binds a name outside itself, as
setXTo(3)does with a super assignment. Such a write reaches the caller through several call levels, but one through a shared environment is missed.
A call to
finside the definition off. The name resolves to the definition it sits in, so the call links back to it rather than staying unresolved.A call whose target is an expression rather than a name, like
(function(x) x)(3)orfactory(0)(). It links to the definition that expression yields, without a name being involved.An operator written between its arguments, like
x + yorx %>% f(y). It is the same call as`+`(x, y), which is why redefining the operator changes what it does.A name that R defines being bound in the program, as in
print <- function(x) xor`for` <- function(a,b,c) a. The redefinition wins wherever the built-in would have been used, respecting scope and order. Only::/:::are exempt.A call that changes state the rest of the program reads without naming it, as
setwdchanges where a path points. Only the working directory is interpreted, while other ambient state such as the options stays an unknown side effect.Track the effective working directory across
setwd, control-flow- and location-sensitive. Interprocedural, sourced, and loop cases are treated as unbounded.
- #Index Access
The bracket, double-bracket, dollar, and slot forms for picking an element out of a container, with names, empty positions, and multiple indices.
7 children700
The
[call, as inx[i],x[i, ,b], orx[3][y]. The container is read as a whole without separating the cell an index picks, and writing through it is a replacement call.The
[[call, as inx[[i]]orx[[i, b]]. Read like single-bracket access, without modelling the difference in what R returns.The
$call, as inx$y,x$"y", orx$y$z, whose right side is a name rather than a value. On a list$matches that name partially, sol$alreaches an element namedalpha.The
@call, which reads a slot of an S4 object. The slot name is read like any other access, without checking it against the classsetClassdeclared.An index given with a name, as in
x[i = 3]orx[[i=]]. It is matched like any other named argument of the underlying[call.An index left out, as in
x[]orx[2,,42]. The gap is an empty argument and still counts as a position.An index that is a vector or a condition, as in
x[i > 3]orx[c(1,3)]. The index expression is read, but which elements it selects stays unknown, as vectorized operations are not evaluated.
R's unary, binary, and special operators, the model formula, and every way a name can be bound to a value.
15 children1131
An operator written before its single argument, like
+3or-3. It is a call of the operator's name with one argument.An operator written between its two arguments, like
3 + 4or3 * 4. It is a call of the operator's name with both arguments, in the order R gives them.13 children931
An operator whose name is spelled between percent signs, like
3 %in% 4or3 %*% 4. The name binds like any other, so a program may define%between%itself and the infix call resolves to it.- #Model Formula14 tests7 desugar, 4 output, 4 slice and 1 more
signature tests
simple formula, Comment Breaks for Unary, call in the lhs of a formula
A formula written with
~, likey ~ xory ~ x + z. Its operands are non-standard evaluation: a barey ~ xnames columns rather than variables, so neither name is read. Every way to bind a name to a value, with the local and super operators, the calls doing the same, or a write through a replacement function.
11 children821
Bind a name in the current scope with
<-, as inx <- 3. A target that is an access (x$y <- 3) rebinds the whole container through a replacement function.- #Local Right Assignment19 tests7 dataflow, 6 desugar, 6 slice
signature tests
"1 -> x -> y", "x <- 1 -> y", local define with -> in function, read after
Bind a name with
->, as in3 -> x. Identical to left assignment with the sides swapped. Bind a name with
=, as inx = 3. This holds only at the start of an expression, as inside a call the same token gives a named argument (f(a = 3)).Bind a column in place with data.table's
:=, as inx[,a:=3,]. It is read as a write to the table, not to a name, and which columns exist stays unknown (see data masking).Bind a name in an enclosing scope with
<<-, as inx <<- 42. The write leaves the function it sits in, which is what makes it a side effect of the call.- #Super Right Assignment10 tests6 desugar, 4 dataflow
signature tests
global define with ->> in function, read after, Manual Max Function
Bind a name in an enclosing scope with
->>, as in42 ->> x. Identical to super left assignment with the sides swapped. An assignment is itself an expression that hands on the value it bound. This is what makes
x <- y <- 3bind both names andprint(x <- 4)print4.- #Assignment Functions459 tests284 slice, 152 dataflow, 62 output and 1 more
signature tests
global assign (no envir) is unchanged, "x <- y <- z", simple assign
Bind a name given as a string, with
assign,delayedAssign, ... The name has to be a resolvable string, and what a delayed assignment does when forced is not modelled.assign("x", 3) delayedAssign("y", x * 2) y Write to a range of a container, as in
x[1:3] <- 3. Like every replacement call it rebinds the whole container, as which cells the range covers is not tracked.Read a write to a call, like
x[i] <- 3orx$y <- 3, as the`[<-`(x, 3)R runs for it. The name in front is both read and rebound. A named argument in such a call (g(v, k = 2) <- 3) leaves its argument edge dangling.A binding
lockBinding(orlockEnvironment) makes constant. Neither is recognized as a built-in, so the assignment R would have rejected is read as an ordinary rebinding.
- #Control-Flow14 tests13 slice, 8 output, 3 dataflow
signature tests
useless branch I, useless branch II, useless branch (complete graph)
Conditionals, the three loop forms, their jumps, and how an error leaves a computation.
9 children810
The conditional
if (x) y else z, andif (x) ywithout an alternative. Both branches stay possible, so a name bound in either reaches a use below, and a missingelseyieldsNULL.- #for loop51 tests31 slice, 19 dataflow, 6 output and 1 more
signature tests
a loop body still calls, Single-vector for Loop, simple constant for-loop
The loop
for (i in 1:3) print(i), which binds its variable in the surrounding scope. The body is analyzed once for any number of iterations, so a name it binds also reaches its own next read, andistays bound after the loop. - #while loop41 tests20 slice, 17 dataflow, 4 desugar and 1 more
signature tests
a loop body is evaluated, Endless while loop with variables, Loop Definitions
The loop
while (x) b, whose condition is read before every iteration. Whether the body ever runs is unknown, so a name it binds and the one bound before it both reach a use below. The loop
repeat {b; if (x) break}, which only abreakor areturnleaves. Like every loop the body is analyzed once, for any number of iterations.Leave the innermost enclosing loop with
break(break()included). The rest of the body becomes unreachable on that path, so what it would have bound does not reach a use after the loop.Start the next iteration of the innermost enclosing loop with
next(next()included). The rest of the body is skipped on that path, while the loop itself continues.Pick a branch by name or position with
switch. A branch left empty falls through to the next one, and every branch stays possible, as the selector does not narrow which of them runs.- #return41 tests36 slice, 5 dataflow
signature tests
return parameter named, read of parameter in return, simple return
Leave the enclosing function with
return(3). Its argument becomes a value of the call, next to the implicit return of whatever path does not return early. - #Exceptions and Errors78 tests59 dataflow, 12 output, 12 slice and 2 more
signature tests
Call edges for error, Call edges for error with fn, Call edges with may built-in
A condition raised with
stop/warningand caught withtry/tryCatch. The path on which a call throws before a write is not kept open, sotryCatch({ risky(); x <- 2 }, ...)losesx. A handler is read as an ordinary function definition.
The parameters a function binds, their defaults,
..., promises, and the value the call hands back.7 children610
A function written with
function(x) x. It opens a scope of its own, so its body resolves names in its lexicographic scope rather than where the call happens.- #Formals
The parameters a definition declares, with their names, defaults,
..., and the promise each is bound to.4 children310
- #Named81 tests52 slice, 22 dataflow, 15 output and 1 more
signature tests
read of parameter, read of one parameter, return parameter named
A parameter written as a name, as
xinfunction(x) x. It binds that name in the body, where it shadows an enclosing binding of the same name. A parameter with a default, as in
function(x = 3) x. The default is evaluated in the function's own scope, so it may read another parameter (function(x, y = x * 2)).The variadic parameter
..., which collects every argument no other formal took. Passing it on keeps those arguments together, while a formal behind it is only matched by its exact name.An argument is a promise, evaluated where it was written but only when the body first reads it. When that happens is not modelled, nor are the writes that forcing it performs, so
function(x = y) { y <- 3; x }does not tell us whereyis read.
A function hands back the value of the last expression it evaluates, without a
return. Every path's last expression is such a value of the call.The short form
\(x) xof a function definition. It is purely syntax and binds its parameters and scopes its body exactly like the long form.
- #Important Built-Ins
The base-R functions we give a meaning of their own rather than treating as opaque calls, the ones that compute on the language included.
13 children2110
The operators
&&and||, which evaluate their right side only if the left one does not decide the result. The right side is read as a path that may not run, unlike the vectorized&/|.The native pipe
|>, which is syntax rather than a call: the parser rewrites it. The left-hand side becomes the first argument or fills the_placeholder.The experimental pipe-bind
=>, which names what the pipe hands on and which R only enables under_R_USE_PIPEBIND_. It is off by default and needsengine.r-shell.pipeBind. Tree-sitter's grammar has no production for it.Give
:,seq, ... the sequence they produce, gathered by abstract interpretation. That value lets us reason about a loop bound or an index, butseq,seq_len,seq_along, andrepare not folded even for literal arguments.The
.Internaland.Primitivecalls a base function reaches its C implementation through. The call is kept and its arguments read, but the name.Primitive("sum")spells is not resolved the way a created name would be.The global options
optionssets andgetOptionreads. Option values are not tracked, sogetOption("digits")does not reach a precedingoptions(digits = 3). Unlike the working directory they are not interpreted at all.The help calls
?,??,help, ...?and??are recognized, but their topic is read as an ordinary name where R only looks it up, andhelp/help.searchare not known at all.Code that reads or builds code, by quoting an expression, evaluating it elsewhere, parsing it from a string, or rewriting a function.
6 children060
Read a part of a function with
body,formals,args, orenvironment. What comes back is opaque, and the whole function is read, so asking only forformals(f)keeps all off's body in a slice.Rewrite a part of a function with
body<-,formals<-, orenvironment<-. Like every replacement function it rebinds the name, so a later call reaches the new part as well as the original one.Take an expression as a value with
quote,substitute,bquote, ... A quoted argument is non-standard evaluation: the names in it are not read, andsubstitutedoes not reach the caller's expression.Run an expression that is a value with
eval,evalq,eval.parent, ...eval(expr, envir)runs in an environment we may not know, so it is marked an unknown side effect.A string that reads the names inside it, as
glue::glue("{x}"),cli::cli_alert_info("{.val {x}}"), orstringr::str_gluedo. Those names resolve in the scope of the template, and one aimed at another scope (.envir,.con,glue_data) becomes an unknown side effect.x <- 2 glue::glue("x plus one is {x + 1}")Turn text into an expression with
parse, and an expression back into text withdeparse. Whatparseproduces is only reached through evaluation, anddeparseis not modelled beyond reading its argument.
- #Literal Values
The values written into the source itself, which is what a resolved name and an S3 class are read from.
7 children700
Recognize numbers like
3,3.14, the integer3L, hexadecimals such as0xFFand0x1p3, as well as the typed missingsNA_integer_/NA_real_, ...A string literal like
"a"or'b'. Its value is kept, which is what lets it stand for a name or fold into a resolved one.- #Logical107 tests56 desugar, 35 dataflow, 16 slice and 1 more
signature tests
completely constant, simple constant while, using variable in body
Recognize the logicals
TRUEandFALSE, ... Their short formsTandFare ordinary bindings and can be reassigned, whileTRUEandFALSEare reserved. The
NULLobject, R's empty value. It is what anifwithout an else branch yields, and assigning it to a list element removes that element.The constants
InfandNaN, which arithmetic produces rather than errors on. They are ordinary values here, and the predicates asking for them are not evaluated, as no type is inferred.
Non-Standard Evaluations/Semantics
1 fully5 partially1 not
The places where R does not evaluate an expression the way it is written, so a name in it may mean something other than the binding it seems to read.
A call that evaluates its arguments against the columns of a data frame first, as
subset(d, col > 1), the dplyr verbs,ggplot2::aes, and data.table's:=do. Which columns exist is unknown to us, so a column shadowing a variable still resolves to the variable.A shorter operand is repeated to the length of the longer one. This is not modelled, so the operands are read but the length of what an operator produces is not derived from them.
An operation applied to every element of a vector at once. Comparisons, the reducing functions, and
ifelsewiden to the unknown value, so an index built from one selects an unknown set of elements.Code registered to run at a point the program does not write out, with
on.exitoruserhooks. Anon.exitbody runs when the function it sits in ends, whilesetHookis not modelled.- #Precedence33 tests32 slice, 4 output, 1 dataflow
signature tests
def compare in loop, Must work with assigned custom pipes too, if-then
Which operator binds tighter, as the documentation lays it out. It decides the shape of the tree the parser hands us, so
-2^2is-(2^2)before anything else looks at it. - #Attributes
The data an object carries beside its value, which is where its S3 class and its shape live.
2 children020
An attribute a program sets itself, with
attrorattributes. Which attributes an object carries is not part of the value we track, so only the rebinding of the object itself is seen.- #Built-In23 tests11 call-graph, 7 dataflow, 7 slice
signature tests
useless branch I, useless branch II, Call edges with may built-in
An attribute R gives a meaning of its own, such as
dim,names, or theclassthat dispatch reads.dim<-,names<-, andclass<-track shape only on a data frame.
Object-Oriented Programming
2 fully9 partially1 not
R's object systems, the class an object carries, and the dispatch that decides which function body a generic call runs.
- #S319 tests10 output, 10 slice, 8 dataflow and 1 more
signature tests
Simple S3 dispatch, Respect Later-Defs, Two-Targets S3 dispatch
Classes and methods built on the
classattribute andUseMethoddispatch. A class is a string an object carries rather than a declaration, so what a call runs follows from the namesgeneric.classin scope.3 children120
Give an object its class with
structure(..., class =),class<-, oroldClass<-. A class written as a string literal is tracked and reaches the dispatch that follows it.- #Dispatch8 tests8 dataflow
signature tests
Simple S3 dispatch, Respect Later-Defs, Two-Targets S3 dispatch
Route a generic call to the method that runs.
UseMethodlinks to every definition namedgeneric.classin scope, so the class an object carries narrows the target only where it is known (heavily over-approximating). Walk the class vector with
NextMethod. It reaches the generic's methods, the one it stands in included, rather than only the next class in the vector.
Formal classes and methods declared with
setClass,setGeneric, andsetMethod. Unlike S3 the class is declared, with named slots read through@.3 children111
Declare a class with
setClassand build one withnew. Thenewcall is linked to thesetClassthat declared the class, and a slot is read through@like any other access.Route a generic call to the method
setMethodregistered. The generic reaches its methods through the chain they register in, but the signature does not narrow which of them runs, as no type is inferred for the argument.Reach a parent method with
callNextMethod, and inherit throughcontains.callNextMethodis left unresolved, so nothing links it to the method it would call, unlike S3 inheritance.
Reference classes made with
setRefClass, whose objects are mutable and thus behave like the shared environments they are built on.$new()and$method()on an instance are unknown side effects, and no call links to the body it runs.- #R6
Classes made with
R6::R6Class, read as one unit rather than as the environment behind them. Like RC an R6 object is mutable, and typing, inheritance, private/active bindings, and handling objects fully "as units" are not supported. Classes made with
S7::new_class, read as one unit, dispatch and inheritance included. Typing is not supported, nor are objects handled fully "as units."Attribute the use of a class to the package that owns it, so a class use implies a dependency for library detection and version guessing. It is the class counterpart of resolving a name to its package.
2 children020
Attribute an S3 class to the package that owns it. Only a class written as a string literal is attributed, not one from a variable or a
c(...)vector.Attribute an S4 class to the package that owns it. S4 ownership is not in the signature database, so a bare class use is not attributed.
R File Structure
5 fully4 partially0 not
The lexical shape of a source file, down to its line endings and encoding.
A comment like
# this is a comment, a shebang line included. It is never read as code, so no name in it resolves.Recognize
#line n "file"as its own node (r-shell only). It is parsed but never retargets a location, and tree-sitter reads it as a comment.A semicolon separating two expressions on one line, as in
a; b; c. It says the same as a newline: the expressions run in the order they are written.Recognize and resolve newlines like
a\nb\nc, where a newline ends an expression unless it is still incomplete. A trailing operator or an unclosed bracket continues on the next line.Recognize
\n(Unix),\r\n(Windows), and a lone\r(classic Mac). Normalized at the r-bridge boundary, with both engines.UTF-8 beyond ASCII in string literals, in a comment, and in names (plain as well as backtick-escaped). Such names bind and resolve like any other, with both engines.
Recognize a UTF-8 byte-order mark at the start of a file. The tree-sitter engine reads past it, the r-shell engine rejects the same input as unparsable.
- #Syntax Errors3 tests3 linter
signature tests
unbalanced brace, missing closing parenthesis, valid code has no syntax errors
Handle source that does not parse. The
syntactically-validrule locates the region and offers a fix. The strict parser rejects the file, while tree-sitter's lax mode (off by default) drops the region. Reject a syntactic keyword like
`if`or`function`where R's grammar requires an expression. The r-shell engine rejectsif <- 5, while tree-sitter's grammar parses it as an ordinary assignment.
Project
9 fully4 partially0 not
The files around the R code, from the package metadata and the packages a version manager pins to what runs before a script does.
- #Package Metadata
The files that describe the package itself, what it needs, and what it offers.
5 children410
Read a package's
DESCRIPTION. Its DCF records give the package name, version, R version, dependency fields, andCollateorder.Read a package's
NAMESPACE. Acts onimport/importFrom,importClassesFrom/importMethodsFromfor S4, andexport/S3method, which is what says whether a name is exported or internal.Read the
.Rdpages underman/, their macros, and the indices beside them. A documented name is tied back to the page that documents it.Read a package's
NEWS/NEWS.md. We parse the versions it announces and what each changed, which is what a version guess is checked against.Read the
R/sysdata.rdaa package keeps its internal data in, and thedata/files it exports. What those bindings hold is not reconstructed.
- #Dependency Managers
The lockfiles and manifests a version manager pins a project's packages with.
5 children410
Read the configuration of renv, the most widespread R project-library manager. The library it points at is neither installed nor restored.
Read the configuration of packrat, the predecessor of renv. The
packrat/liblibrary beside it is not loaded.Read the configuration of rv, a declarative project manager in the style of cargo. Parses
rproject.tomland the resolvedrv.lock.Read the configuration of uvr, an R project manager modelled on uv. Parses
uvr.tomland theuvr.lockbeside it.Read the package library a project installs into (
renv/library,packrat/lib, or the platform library). Their code is not read.
- #Startup and Discovery
What runs before a script does and what counts as part of the project at all.
2 children110
Read the files R runs or reads before a script (
.Rprofile,Rprofile.site,.Renviron,Renviron.site). An.Rprofileis read as R code whose bindings precede the script, but variables set by an environment file are not interpreted.Read the
.gitignoreand.Rbuildignorethat say which files are not part of the project. Follows gitignore globs and the regular expressionsR CMD builduses.
- #Pre-Processors/external Tooling
The tooling around R code rather than the R in it, such as roxygen2 blocks and woven documents.
1 child010
Handle the roxygen2 blocks that precede a definition. They are read as a comment and what a tag states does not reach how a name resolves, so an
importFromtag leaves the name below it unqualified.
System, I/O, FFI, and Other Files
0 fully9 partially0 not
Everything a program reaches for beyond its own code, from files it sources to calls it makes out of R.
Pull another file's code into the analysis with
source,sys.source, ... A sourced file's top-level bindings become visible to the code below the call. Onlysourceis handled so far, and only where its path resolves.source("helpers.R") sys.source("setup.R", envir = environment())Files a program writes objects to and reads them back from, with
save,load,readRDS, ... Aloadbinds names we cannot know, so neither the names nor the values behind them are reconstructed.Reading and writing data files with
read.csv,write.csv, ... What a file contains does not enter the analysis, which is also why the columns of a data frame stay unknown.Calling out to compiled code with
.C,.Call,.External,.Fortran, ... The call carries an unknown side effect, like a write through a shared environment, and the foreign code itself is not analyzed.Handle
system,system.*, ... An injectable command built from user input is flagged by theproblematic-inputsandunescaped-argumentsrules.Support R-Markdown files as R sources. Code chunks are extracted, but not inline
r expror theparamsof the YAML front matter.Support Jupyter Notebooks as R sources. Cells are read in document order, not execution order, and the kernel is not checked.
Support Quarto files as R sources. Code chunks are extracted, but not inline
r expror theparamsof the YAML front matter.Support for Sweave files as R sources. Code chunks are extracted,
\Sexpr{}inline expressions are not.
Types
0 fully1 partially3 not
What a value is, how we infer it from the code, and the coercions R applies between types.
The atomic types
numeric,character,logical, ...typeof,class, andmodeare not evaluated and resolve to the unknown top value, so not even a literal yields its type.The composite types a list, a data frame, or an object has. None of them is tracked, so
class(list(1, 2))resolves to the unknown top value.Derive the type of an expression from the code that produces it. A type predicate never narrows a branch, so
if(is.numeric(1)) y <- 1 else y <- "a"still leavesyas both alternatives.The type R silently converts a value to, as
c(1, "a")orTRUE + 1do. A vector is not unified, soc(1, "a")keeps a number beside a string, and theas.*converters are not evaluated.