Repository navigation
Support static member and constructor intelligence for R6, S4 and S7 - #784
renkun-ken merged 17 commits into
Conversation
|
The demos now live in this comment; Install the PR branch in the R library used by VS Code and restart the R language server. Keep For completion, temporarily delete the member name after a chosen These examples were checked using the actual completion, signature and hover providers on R 4.6.1 with Polars 1.16.0, without sourcing or evaluating the demo files. This is automated provider verification, not a manual VS Code recording. Package metadata loading can run package initialization, but member analysis does not execute document expressions, arguments, constructors, methods or active getters. Polars: query stages and expression namespaces# Open this file with this branch's languageserver. Execution is optional.
# Completion: put the cursor after a $, deleting the following member name.
# Signature help: put the cursor inside a method's parentheses.
# Hover: point at a method name or named argument.
library(polars)
csv_file <- tempfile(fileext = ".csv")
write.csv(iris, csv_file, row.names = FALSE)
q <- pl$scan_csv(csv_file, infer_schema_files = 10)
# After the last $: agg, head, map_groups, ... (the grouped receiver).
# Hover group_by or .maintain_order for method/argument documentation.
grouped <- q$group_by("Species", .maintain_order = TRUE)
grouped$agg(pl$all()$sum())
# After the last $: collect, filter, group_by, ... (a lazy frame again).
result <- grouped$agg(pl$all()$sum())
result$collect()
# Nested namespace: complete/hover to_uppercase; signature: to_uppercase().
expression <- pl$col("Species")$str$to_uppercase()
# The Series receiver retains its return shape while delegating sum's docs.
series <- pl$Series("values", 1:3)
series$sum()Base R: list factories, nested return shapes and quoted fields# No extra packages. The provider analyzes the factory body and its closures.
make_client <- function(endpoint) {
list(
config = list(endpoint = endpoint, retries = 3L, `api key` = NULL),
request = function(path, timeout = 30) {
list(
status = 200L,
decode = function(simplify = TRUE) {
list(data = list(endpoint = endpoint, path = path), ok = TRUE)
}
)
}
)
}
client <- make_client("https://example.invalid")
# After client$: config, request.
# Signature help inside request(): request(path, timeout = 30).
response <- client$request("/records", timeout = 10)
# After response$: decode, status. Hover status shows the literal 200L.
# Signature help inside decode(): decode(simplify = TRUE).
decoded <- response$decode(simplify = FALSE)
response$status
# After decoded$: data, ok. After decoded$data$: endpoint, path.
decoded$data$path
# Quoted fields retain their full name and replacement/hover range.
client$config$`api key`Base R: fluent environment methods# No extra packages. Methods return self or a different statically known shape.
make_pipeline <- function(source) {
self <- new.env(parent = emptyenv())
self$source <- source
self$filter <- function(predicate) self
self$limit <- function(n = 10L) self
self$collect <- function(format = "list") {
list(data = source, format = format, metadata = list(cached = FALSE))
}
self
}
pipeline <- make_pipeline(iris)
# After the last $: collect, filter, limit, source.
# Signature help inside limit(): limit(n = 10L).
limited <- pipeline$filter("Sepal.Length > 5")$limit(n = 5L)
# Signature help inside collect(): collect(format = "list").
output <- limited$collect(format = "list")
# After output$: data, format, metadata. After metadata$: cached.
output$metadata$cachedR6: inherited methods and active member declarations# R6 is a languageserver dependency. Public declarations supply the shape;
# the provider never calls initialize or the active getter to discover members.
Dataset <- R6::R6Class("Dataset", public = list(
source = NULL,
initialize = function(source) self$source <- source,
select = function(columns) self,
preview = function(n = 6L) list(data = self$source, rows = n)
))
LoggedDataset <- R6::R6Class("LoggedDataset",
inherit = Dataset,
public = list(log = function(message, level = "info") self),
active = list(expensive = function(value) {
stop("This getter must not run during language features")
})
)
dataset <- LoggedDataset$new(iris)
# After the last $: inherited preview/select/source plus log/expensive.
# Signature help inside preview(): preview(n = 6L).
preview <- dataset$select(c("Species"))$log("selected")$preview(n = 3L)
# After preview$: data, rows. Hover rows shows its statically inferred value.
preview$rows
# Hover log/preview shows their respective signatures.
# expensive is offered as a declared member, but its result stays unknown.The list example offers |
|
S4 slot support is now included in this PR. These examples use the same static completion, hover and signature providers as the # Source declarations, inheritance, nested slots and mixed @/$ chains
Leaf <- methods::setClass("Leaf", slots = c(value = "numeric"))
Widget <- methods::setClass(
"Widget",
contains = "Leaf",
slots = c(child = "Leaf", items = "list", run = "function"),
prototype = list(run = function(value, flag = TRUE) value)
)
x <- new("Widget", child = Leaf(value = 1), items = list(alpha = 1))
x@ # child, items, run, value; details show slot types
x@child@ # value
x@items$ # alpha
x@run(flag = # run(value, flag = TRUE); active parameter: flag
x@value # hover: double
# Constructor aliases and source factories
factory <- function(value) Widget(items = list(result = value))
factory(unresolved)@items$ # result; no need to execute factory()
# Installed package metadata and a constructor's source return expression
library(stats4)
fit <- stats4::mle(function(mean = 0) sum((1:3 - mean)^2))
fit@ # call, coef, details, fixed, fullcoef, method,
# min, minuslogl, nobs, vcov
fit@coef # hover: doubleThe lines ending in Validation: all 68 S4 assertions pass, including a live LSP test that immediately requests completion, signature help and hover after |
|
Multiline chains and R6 method-body references are now covered in ba8626b. These examples can be pasted into an R document without running them. Completion, signature help and hover were checked through the actual providers; the live LSP tests also check the first request after an edit with retries disabled. Multiline fluent environment methodsmake_pipeline <- function(source) {
self <- new.env(parent = emptyenv())
self$source <- source
self$filter <- function(predicate) self
self$limit <- function(n = 10L) self
self$collect <- function(format = "list") {
list(data = source, format = format, metadata = list(cached = FALSE))
}
self
}
pipeline <- make_pipeline(iris)
pipeline$
filter("Sepal.Length > 5")$
limit(n = 5)$
filter("Petal.Length < 2")$
collect()At each R6 self, super and private inside methodsParent <- R6::R6Class("Parent", public = list(
source = NULL,
filter = function(predicate) self
))
Pipeline <- R6::R6Class("Pipeline", inherit = Parent,
public = list(
limit = function(n = 10L) {
self$
filter("Sepal.Length > 5")
super$
filter("Petal.Length < 2")
private$remember(n)
self
},
collect = function(format = "list") {
list(data = self$source, format = format,
metadata = list(cached = FALSE))
}
),
private = list(
remember = function(n) self
)
)
Pipeline$new()$
limit(n = 5)$
collect()$
metadata$
cachedInside R6 context is inferred from supported declarative |
|
S7 constructors and Class constructor and propertieslibrary(S7)
Dog <- new_class("Dog", properties = list(
name = class_character,
age = class_numeric
))
Dog
lola <- Dog(name = "Lola", age = 11)
lola@age
Inheritance and nested propertiesNamedDog <- new_class("NamedDog", parent = Dog, properties = list(
metadata = class_list
))
named <- NamedDog(name = "Lola", age = 11,
metadata = list(cached = FALSE))
named@
metadata$
cachedCompletion after Custom constructors and function propertiesRunner <- new_class("Runner",
properties = list(run = class_function, items = class_list),
constructor = function(first, option = TRUE) {
new_object(S7_object(),
run = function(value, flag = FALSE) value,
items = list(done = first)
)
}
)
runner <- Runner(unresolved)
runner@run(flag = TRUE)
runner@items$doneSignature help and hover resolve The analyzer reads installed class/property descriptors and constructor formals, and summarizes source declarations and supported custom-constructor syntax. It does not execute document expressions, constructors, getters, setters, validators or defaults. Getter/setter properties retain their declared types; values that depend on those callbacks remain unknown. Dynamic class definitions, arbitrary generic dispatch and cross-file value flow remain outside this support. |
eitsupi
left a comment
There was a problem hiding this comment.
Reviewed with ChatGPT against the latest b40aa47.
Overall, this has become a very impressive and carefully tested implementation. I did not find a general design or correctness issue with the R6/S4/member inference itself.
I do have one blocking concern before merging: S7 1.0 compatibility.
S7 main is already preparing 1.0.0 with breaking changes, and several of them directly affect assumptions made here:
R/member-metadata.Rreadsattr(value, "S7_class")directly. S7 1.0 has moved this internal attribute to_S7_classand explicitly recommends usingS7_class()rather than accessing the attribute directly. The compatibility fallback is implemented inside S7, so it will not apply to this direct read.- S7 1.0 introduces the
Foo := new_class()form. The current document binding analysis only tracks<-/=/->, while the S7 inference requires an explicit stringname, so this new form does not appear to be recognized. - The default constructor semantics for subclasses of a class with a custom constructor have changed in 1.0: the child now takes
...followed by its own properties and forwards...to the parent. The static constructor synthesis here still appears to inherit the parent's formals directly, matching the 0.2.x behavior.
Since the current S7 regression tests exercise the released 0.2.2 behavior, I think we should test this feature against S7 main / the upcoming 1.0.0 before merging and adapt the introspection and source inference accordingly. Where possible, I would also prefer public S7 introspection APIs over direct reads of internal representation details.
One non-blocking architectural note from the ChatGPT review: this PR now establishes a substantial semantic-analysis subsystem shared by completion, hover, and signature help. I think that is coherent for this PR, but for future expansion it would be useful to keep a clear provider boundary so that alternative static-analysis engines, such as Arity, could eventually be integrated rather than duplicating increasingly broad semantic analysis inside languageserver.
Once the S7 1.0 compatibility issue is addressed, I would be comfortable with the overall direction of this PR.
|
Implemented the S7 1.0 development compatibility fixes identified in the audit. The source rules derive capabilities from the user's installed S7 function bodies/formals; installed classes supply their actual constructor formals. No versioned class/property API table is introduced, and inference still does not execute document expressions or callbacks. This update handles current and legacy instance storage (including inert class-reference environments), nested exported instances, custom/foreign-parent forwarding constructors, overridden defaults, R matching at the dots boundary, direct The new compatibility workflow tests S7 0.2.2 and development commit Try these examples with that S7 development revision. At library(S7)
Dog := new_class(properties = list(name = class_character, age = class_numeric))
lola <- Dog(name = "Lola", age = 11)
lola@age
Dog@constructor()@age
Parent := new_class(
properties = list(payload = class_list),
constructor = function(seed = 1L) {
new_object(S7_object(), payload = list(seed = seed))
}
)
Child := new_class(parent = Parent, properties = list(label = class_character))
# Signature: Child(..., label = character(0))
Child(3L, label = "child")@payload$seed
Box := new_class(properties = list(
items = new_property(class_list, default = quote(list(old = 1)))
))
# Completion after the last $ suggests new, rather than old.
set_props(Box(), list(items = list(new = 2)))@items$new
S4Base <- methods::setClass("S4Base", slots = c(value = "numeric"))
Hybrid := new_class(parent = S4Base, properties = list(label = class_character))
Hybrid(value = 1, label = "hybrid")@valueValidation: the focused S7 suite passes on stable 0.2.2 (83 assertions; new-version-only features are skipped) and the pinned development version (116 assertions). Stable S7 coverage tests pass; the development coverage run passed 112 assertions before the last four fixture assertions were added. S4/member-provider and language-feature handler checks pass. Changed-line lint and The full member suite reaches an existing 250 ms limit in the long Polars Final review also prevents setter properties from retaining stale declared-default member shapes. Both S7 CI matrix jobs (stable 0.2.2 and pinned development 0.2.2.9000) and lint passed on final commit |
eitsupi
left a comment
There was a problem hiding this comment.
Re-reviewed with ChatGPT against the latest 25b16ef.
The blocking S7 1.0 concerns from my previous review have been addressed comprehensively, and the compatibility CI now covers both S7 0.2.2 and the current S7 main revision. I also re-reviewed the abstraction boundaries in the member analysis more closely.
Overall, I think the architecture is sound for this PR. Completion, hover, and signature help correctly share the same receiver inference and abstract-value machinery, and the R6, S4, and S7-specific semantics are mostly kept in separate modules. I do not think further architectural refactoring should block this PR.
I did notice a few non-blocking abstraction issues that would be worth keeping in mind as this subsystem grows:
member_slot()is effectively the generic@dispatcher, but it currently lives inmember-s7.Rand falls back tomember_s4_slot(). The dependency direction would be clearer if the common layer dispatched to separate S4/S7 implementations, e.g.member_s4_slot()andmember_s7_slot().member_value()has explicit S4/R6 state, while S7-specific fields such ass7_descriptorands7_generatorare added later bymember_s7_value(). Common code such asmember_shape_key()already knows about those S7 fields, so the abstract-value schema is slightly asymmetric. If this grows further, it may be worth formalizing the object-system-specific payload.- The provider layer currently identifies constructor intelligence through
s7_generator. That is sufficient today, but if constructor support later expands to S4 generators, R6 factories, or other class systems, a generic constructor/callable capability would avoid accumulating object-system checks in the providers. member_context_index()also knows how to merge S4/S7 dependency metadata and attached-object-system state. This is still manageable, but package/object-system-specific index merging may deserve its own boundary if more semantic systems are added.
These are architectural follow-ups rather than correctness concerns. In particular, I would prefer not to over-generalize the internals just to anticipate another backend. A cleaner long-term direction would be to keep this native member engine cohesive, then place a provider boundary around it if languageserver later supports alternative static-analysis backends such as Arity.
With the previous S7 compatibility concerns resolved, LGTM. Approving.
|
@eitsupi Thanks for the review. Will take a closer look at the abstraction issues you mentioned in a follow-up work. |
Completion, signature help and hover currently lose receiver identity in chained
$calls. For example,q$group_by("Species")$can show unrelated document tokens on its first trigger, and a method such assum()is looked up by its bare name. This change infers the receiver from syntax, offers its members, and resolves method signatures and documentation through the inferred method identity. S4 slots and S7 properties accessed via@use the same analysis and providers. S7 class bindings also supply constructor hover, signature help and argument completion. Member names can follow$/@on later lines, and supported R6 method bodies resolveself,superandprivateagainst their declared class context.Implementation
:=, S4 parents, external references with export/version checks and bounded dependency snapshots, S3 defaults and deprecation wrappers. Getter/setter values remain conservative.@/$chains, show slot types, and resolve function slot signatures when source syntax is known.$or@trigger after an edit without adding a synchronous whole-document parse.Document expressions, arguments, constructors, methods and active getters are never executed for these features. Package metadata preparation loads requested namespaces and can run package initialization. Unknown receivers/results remain conservative. The existing
member_completionsetting controls the shared member analysis.Demos
The demo comment contains editable examples for:
selfor a collected list.self/super/privatemethod-body references are in the multiline and R6 demo comment. Both examples were checked through the actual static providers.All four examples were verified with the actual completion, signature and hover providers on R 4.6.1 with Polars 1.16.0, without executing their code. Experimental scripts, corpus output and standalone demos are excluded from the package; regression fixtures remain self-contained under
tests/testthat.Validation
021846758e5d4520a56330d477066c25bd46d4f9(0.2.2.9000, NEWS targets 1.0.0). Stable skips development-only features. Coverage-instrumented S7 tests pass on both versions; the development coverage run preceded the last two fixture assertions. CI now tests stable and that pinned development revision. This does not claim validation of a final unreleased 1.0 artifact.q$test reaches the existing 250 ms bound locally; the same failure reproduces on a fresh install of prior commitb40aa47. The latest full member suite is therefore not claimed to pass locally.git diff --checkpass.didChange.R CMD buildandR CMD check --no-tests --no-manual --no-build-vignettespass with_R_CHECK_FORCE_SUGGESTS_=false. The full suite is not claimed to pass; formatting/diagnostics/code-action/protocol failures also reproduced on the pre-implementation branch in this environment. VS Code itself was not manually tested.The analyzer is conservative and bounded: cursor recovery considers at most 129 lines / 64 KiB; inference has a 20,000-node, 64-level and 250 ms bound. Current limits include arbitrary cross-package/cross-file value flow, unsupported dynamic dispatch/NSE, schema-dependent names, dynamic R6 class mutations and declarations beyond the cursor recovery window. Unsupported patterns stay unknown; changed package architectures may require additional generic extraction rules.
An earlier production-engine corpus audit retained 133/168 article, 5,267/5,324 reference and 44/48 supplemental members. These are historical member-availability measurements at real
$nodes, not type precision or end-to-end editor coverage; research runners and captured corpus data are not part of this PR.