You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Source effects can now target a directory instead of a single file, which covers the two idioms for loading a folder of scripts: targets::tar_source() and the sourceDir() example from ?source.
# _targets.R
library(targets)
tar_source() # defaults to files = "R"
tar_target(x, my_helper()) # goto-def lands in R/my_helper.R
The resolver expands the directory into one target per script, in list.files() order. Load order within the call is respected: code/b.R sees code/a.R but not the other way around. Combined with the previous PR this also means the sourced files see each other, through the sourcing file's scope, without relying on the R/ auto-collation of the first PR.
The PR also adds ad hoc support for sourceDir() with a total hack: any calls to sourceDir() with a resolvable string causes a Source effect. This is to handle the documented pattern in ?source that people are copying around: https://github.com/search?q=sourceDir+language%3AR&type=code. Ideally we'd infer the effects from the list.files() and source() calls. One day.
Covers oak-source/1-r-collation, oak-source/2-source-inheritance and oak-source/3-source-dir.
Setup
Section 5 covers experimental diagnostics, off by default. Turn them on up front, in settings.json:
{
"oak.diagnostics.experimental.enable": true
}
Everything here needs a non-package project. Open a plain folder with no DESCRIPTION. If one is present the package paths take over and none of this applies.
1. R/ directory collation
A non-package project with an R/ directory gets the same treatment as a package R/ with no Collate:: files load in basename order and each sees the ones before it.
ws/
R/
a.R # a_val <- 1
b.R # b_val <- 2
In b.R, a_val resolves and goto-def lands in a.R. Find-references on a_val from either file finds both sites.
Order matters at the top level. In a.R, a top-levelb_val should not resolve — b.R hasn't loaded yet:
# R/a.Rb_val# unresolved: b.R loads after this filef<-function() b_val# resolves: the body runs after everything has loaded
That eager/lazy split is the same one as elsewhere. A cursor at file scope sees only predecessors; a cursor in a function body sees the whole directory. local() counts as eager, so a cursor inside a local({ ... }) block at file scope still only sees predecessors.
No cross-collation between directories. Two separate R/ directories (say ws/R/ and ws/sub/R/) don't see each other.
The full search path is still there. This is the regression to watch for, since package code gets the rest from NAMESPACE:
# R/a.R
median(1:10) # `median` must still resolve (stats, on the default path)
A package R/ file left out of Collate: should keep resolving as a standalone script, not pick up directory collation.
Just-opened files. Open an R/ file in a fresh window and use it before the scan settles. It should behave as if it's in the collation from the start — in particular "unknown symbol" diagnostics shouldn't appear after indexing finishes, which is what a naive implementation does.
2. Sourced files inherit the sourcing file's context
A file loaded by source() sees what the sourcing file had at the call.
# helpers.Rconfig# resolves, goto-def lands in main.Rfilter# resolves to dplyr, inherited from main.R's library() callf<-function() result# resolves: runs later, after main.R finishedresult# top level: does NOT resolve, main.R hadn't run that line yet
That last pair is the point of the offset narrowing. The top-level result is a genuine runtime error and should read as unresolved; the one in f's body should not.
Transitive.main.R -> setup.R -> helpers.R should carry main.R's names all the way down.
Several sourcing files union. Each sourcing file is an alternative, not a priority order, so goto-def offers one target per file that binds the name:
Goto-def on x in foo.R gives two targets, file1.R and file2.R. On y it gives one. Note y is really only bound on one of the two paths, and we report it as certainly bound — the certainty join isn't built (see gaps).
Package and testthat files opt out. A data-raw/build.R that sources a package's R/foo.R must not cost foo.R its NAMESPACE imports, collation siblings, or base. Check that foo.R still resolves everything it did before the source() call existed.
Editing the sourcing file shouldn't churn. Type in main.R somewhere away from the source() call — adding a line at the top, editing a function body. helpers.R shouldn't flicker or lose resolution.
3. Directory sourcing
3.1 sourceDir()
The worked example from ?source. The implementation is a temporary stopgap and recognizes sourceDir()by name alone. Ideally we'd infer the effect from the code:
# main.RsourceDir<-function(path, trace=TRUE, ...) {
for (nmin list.files(path, pattern="[.][RrSsQq]$")) {
source(file.path(path, nm), ...)
}
}
sourceDir("code")
helper_from_code_dir() # should resolve, goto-def into code/*.R
The local sourceDir <- function(...) above the call is exactly what would normally suppress the effect, so if that resolves, the bypass is working.
Flip side of matching on the name: a sourceDir() that does something unrelated will be misread as sourcing a directory. Expected, and the reason the list has one entry.
3.2 targets::tar_source()
# _targets.R
library(targets)
tar_source() # defaults to files = "R"
tar_target(x, my_helper()) # `my_helper` from R/*.R should resolve
Try all of: bare tar_source(), targets::tar_source(), and a positional tar_source("code"). All three should work.
tar_source("R/utils.R") naming a script rather than a directory should also work — files takes both.
tar_source(files = "code") is not recognized. Deliberate: the path is matched positionally, and the alternative was silently defaulting to "R" and sourcing a directory the call explicitly overrode. A miss, not a wrong answer. Same for tar_source(c("R", "utils")).
3.3 Both
Subdirectories are included (R/models/fit.R counts), matching tar_source()'s recursion. sourceDir() isn't recursive in R, so it gets more than it would really load — the over-approximating direction.
The directory's files see each other, whatever the directory is called — this doesn't lean on section 1's collation, so code/ works as well as R/.
Load order is respected, so it's worth checking both directions with a top-level cursor. Given sourceDir("code") loading a.R then b.R:
# code/a.Rb_val# does NOT resolve: b.R hasn't run yetf<-function() b_val# resolves: bodies run after the whole call# code/b.Ra_val# resolves: a.R ran first
The middle case is the interesting one to poke at. The sourcing file's own state is snapshotted once, before any target runs, so nothing there can tell a.R from b.R — the ordering comes from the call's target list instead.
4. What this fixes in the issues
Worth checking the reported cases directly.
#15144. Clone https://github.com/benzipperer/ctrl_click. The three ctrl+click targets in main.R: greet_local and greet_static_helper already worked. greet_dynamic_helper is loaded by lapply(list.files("R"), source) and still won't resolve — that idiom needs dataflow we don't have yet. Swap the line for sourceDir("R") or tar_source() and it should.
What #15144 does get: the files inside R/ now resolve each other.
#14790. Functions spread across files directly in the workspace root with no R/ directory and no source() calls. Not fixed by any of this. Confirm it still reports "no definition found" so nobody thinks otherwise.
5. Diagnostics
Both experimental, gated on the setting above.
5.1 source-cycle (Warning)
# a.R
source("b.R")
# b.R
source("a.R")
Both files should report it, anchored at the start of the file (under recovery there's no recorded call to point at). Message says analysis will be incomplete until the cycle is resolved.
5.2 inherited-shadow (Info)
Fires when a name resolves differently because of an inherited layer than it would standalone. Reproducible with no packages involved:
base::source on the second line is load-bearing. A bare source("b.R") would be shadowed by a.R's own binding, so the call stops being effectful and the whole edge disappears along with the diagnostic.
The case that matters more in practice is the same shape via an attach: a package on main.R's search path exporting its own library, reaching helpers.R only through inheritance. Harder to set up by hand, but attach ordering is most of what inheritance actually contributes.
Reported in the sourced file, the one being edited, not at the source() call — diagnostics only run for open files, so putting it on the sourcing file would mean the user editing the sourced one sees nothing.
Shouldn't fire when the file's own attach ordering explains the shadow, nor when the sourcing file binds the name only after its source() call.
6. Known gaps
Things to notice and not file.
lapply(list.files("R"), source) isn't recognized. Needs an abstract-value layer. This is #15144's literal repro.
A file next to R/ doesn't see R/. In a Shiny app, app.R calling myModuleUI() from R/module.R won't resolve. Shiny's loadSupport() runs inside runApp(), so there's no call to attach an effect to, and we deliberately didn't add a "directory adjacency" rule. Same for a _targets.R that doesn't call tar_source().
Inheritance replaces collation rather than adding to it. A non-package R/ file that some script also source()s loses its collation siblings.
No certainty distinction across source sites. A name bound via one sourcing file and not another reads as certainly bound.
Out-of-workspace source() targets don't resolve at all; nothing tells you why.
First use may be slow. The first cross-file resolution in a sourced file pays a workspace-wide index sweep. Background priming isn't built. Worth reporting only if it's bad enough to be visible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses posit-dev/positron#14790
Progress towards #1338
Source effects can now target a directory instead of a single file, which covers the two idioms for loading a folder of scripts:
targets::tar_source()and thesourceDir()example from?source.The resolver expands the directory into one target per script, in
list.files()order. Load order within the call is respected:code/b.Rseescode/a.Rbut not the other way around. Combined with the previous PR this also means the sourced files see each other, through the sourcing file's scope, without relying on theR/auto-collation of the first PR.The PR also adds ad hoc support for
sourceDir()with a total hack: any calls tosourceDir()with a resolvable string causes aSourceeffect. This is to handle the documented pattern in?sourcethat people are copying around: https://github.com/search?q=sourceDir+language%3AR&type=code. Ideally we'd infer the effects from thelist.files()andsource()calls. One day.Positron Release Notes
New Features
targets::tar_source()and asourceDir()call as documented in?source(go to definition returns no definition found positron#14790).Bug Fixes