Skip to content

feat(sess): resolve hover and completion targets without evaluating them - #1949

Merged
eitsupi merged 9 commits into
mainfrom
feat/sess-inspection-resolver
Nov 15, 2026
Merged

eitsupi merged 9 commits into
mainfrom
feat/sess-inspection-resolver

Conversation

@eitsupi

@eitsupi eitsupi commented Nov 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Hover and completion requests send the text in front of the cursor to R, which runs eval(parse(text = expr)) in the global environment. The text is extracted in TypeScript with [a-zA-Z0-9._$@ ], and R trusts that extraction. Because spaces are included, valid R code other than a name can still reach eval(): hovering counter in repeat counter <- counter + 1 sends repeat counter, which parses and loops forever, so the R session hangs. Completion after repeat x$ does the same.

This PR adds a shared parser and resolver in sess that answers the same requests without evaluating the request text, so R no longer depends on what the client extracted. The existing socket handlers are not switched over yet, so behavior does not change in this PR; switching them is a follow-up.

Why requests must not be evaluated

  • Hover and completion run without an explicit action. Evaluating the request runs code just because the cursor moved or $ was typed, as the repeat example shows. This concern was raised for vscode-R before in Improve $-completion of method-chaining syntax and more #1469, whose author concluded that a safe allowlist of functions could not be built. R's own completion (utils rcompgen) does not evaluate a prefix that contains an explicit function call, ark forbids function calls in completion evaluation, and IPython limits completion to side-effect-free evaluation by default.
  • The handler should not trust its caller. Validating the request in R keeps the guarantee independent of how a client extracts the text. This also prepares these handlers to be exposed to other clients later (for example as arf IPC handlers, which run outside arf's evaluation policy and approval), where an evaluating handler would let any client of the same user run arbitrary code.
  • The current extraction never contains parentheses, so rejecting function calls removes no editor feature.

What is accepted

The request text is parsed in R and accepted only when it is a single expression that is one of:

  • a name, looked up from the global environment along the search path (as before, so mtcars and pi still work);
  • a constant: every length-one constant the parser produces (numbers, integers, complex, logical, NULL, typed NA, Inf, NaN, strings). Evaluating a constant runs no code, and the editor already requests hover for them. T and F are names;
  • a chain of $ and @ accesses on a name, with a name or non-empty string after each operator. Spaces such as a $ b and trailing comments are fine.

Anything else, including function calls, [[, ::, assignments, unary operators and multiple expressions, is rejected with an error of class sess_inspection_invalid. Previously only the first of several expressions was evaluated. Parsing in R, rather than splitting the text in TypeScript, keeps R's syntax (for example 0x10, 1L, 1i) and its validation in one place.

How accessors are applied

The parsed request is not evaluated. The resolver looks up the name and applies each accessor as a fixed call: base::$ or base::@ is embedded as a function object, the member is inserted as a symbol (the operators do not evaluate their member argument), and the call is evaluated in a private environment that binds the current object. As a result:

  • $ or @ defined in the global environment do not change the operators;
  • the environment's parent is the global environment, so S3 $ methods defined there still dispatch as they do at the console;
  • base::@ is used instead of methods::slot() so that its own dispatch (S4 slots, S7 properties) is kept;
  • the current object is never re-evaluated, even if it is a language object.

Side effects that remain

Not evaluating the request does not make inspection free of side effects. Promises, active bindings (including R6 active fields), S3 $ methods and methods for str() or .DollarNames() still run code that is already defined in the session, as they do now. They are kept so that hover and completion keep working for lazy data, R6 objects and classes with custom accessors. Each binding on the path is read once.

Results and errors

  • Hover returns list(str = ...) from the existing capture_str(). Completion returns the same items as before; the item builder is extracted from handle_complete() into completion_items() without changing it. Completion without a $ or @ trigger returns NULL before reading any binding.
  • Properties of S7 objects can be hovered through @, but are not offered as completion items yet, because completion_items() lists @ members only for S4 objects. This is unchanged and noted with a TODO.
  • A missing name, $ on a plain atomic vector, and @ on a missing S4 slot or on an object without a class return NULL.
  • For S4 objects, @ accepts what methods::.hasSlot() reports plus the pseudo-slots that base @ reads: .S3Class, .Data and, on list-based objects, names. Like base @, .Data gives NULL for an object without a data part rather than an error.
  • Invalid requests, other errors and interrupts are signalled rather than turned into NULL, so that a caller with an execution deadline can tell them apart.

Other changes

  • sess is bumped to 3.4.9000.9000.
  • R6 and S7 are added to Suggests so that their inspection tests run in CI. The managed sess installation installs only Imports, so users do not get them.
  • arity.toml ignores description-malformed-version, which flags tidyverse-style development versions such as 3.4.9000.9000.

Extract the member listing after `$` and `@` from handle_complete so that a
completion handler which resolves its target differently can return the same
items. Behavior is unchanged.
Add a shared parser and resolver for hover and completion requests. The
request text is parsed but never evaluated: only a single name, a single
constant, or a chain of `$` and `@` accesses on a name is accepted. The name is
looked up along the search path and each accessor is applied with the base
operator, so other clients cannot use these handlers to run arbitrary code and
moving the cursor never runs an expression.

Promises, active bindings and S3 `$` methods still run as before, and each
binding on the path is read once. Missing names and members return NULL, while
invalid requests, errors and interrupts are signalled.

The existing socket handlers are not switched over yet.
@eitsupi
eitsupi requested a review from renkun-ken November 15, 2026 07:12
Explain first that the editor's extraction can send code such as `repeat x`,
and mention exposing the handlers to other clients only as a later use.
… members

Completion has no items unless the trigger is `$` or `@`, so return before
reading any binding instead of running promises or active bindings for
nothing. Reject quoted members that cannot be symbols, such as `x$""`, as
invalid requests. Collect the accessor chain without repeated prepending.

The test now restores `T`, `$`, `@` and the S3 methods it masks if they
existed before, so it can run in a live session.
The inspection tests for R6 active fields and S7 properties are skipped when
the packages are missing. Listing them in Suggests makes CI install them; the
managed sess installation only installs Imports.

@renkun-ken renkun-ken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed aa188ec. The parser and fixed-accessor approach look sound within the stated scope, including retaining session-defined methods and deferring the socket-handler switch. I found one accessor compatibility issue below: the S4 slot precheck rejects a valid base @ pseudo-slot.

Validation: installed the exact PR source into an isolated library and ran the full sess tinytest suite on R 4.6.1: all 1,440 assertions passed, including the 99 new inspection assertions with R6 and S7 installed. All 10 GitHub checks are successful. No other actionable findings.

Comment thread sess/R/inspection.R Outdated
return(list(found = FALSE))
}
if (operator == "@") {
if (isS4(object) && !methods::.hasSlot(object, name)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve base @ access to S4 pseudo-slots

methods::.hasSlot() is false for .S3Class on an ordinary S4 object, even though base @ successfully reads it. This precheck therefore reports a valid accepted target as missing before the fixed accessor can run. For example, on R 4.6.1:

methods::setClass("InspectionS4", slots = c(items = "list"))
inspection_s4 <- methods::new("InspectionS4", items = list(a = 1L))
inspection_s4@.S3Class                          # "InspectionS4"
methods::.hasSlot(inspection_s4, ".S3Class")    # FALSE
sess:::handle_hover("inspection_s4@.S3Class")   # list(str = ' chr "InspectionS4"')
sess:::inspection_hover("inspection_s4@.S3Class") # NULL

The same discrepancy occurs for S4 classes extending numeric or list. Please preserve base @'s special-slot behavior when deciding that an S4 member is missing, and add a regression test for .S3Class. This keeps the new resolver compatible with the existing hover handler when it is adopted.

methods::.hasSlot() is FALSE for .S3Class, for .Data when it is not
stored as an attribute, and for names of a list-based object, although
base @ reads all of them, so hovering such a member reported nothing.
Accept these names in the precheck. Accessor errors still propagate.
@eitsupi
eitsupi requested a review from renkun-ken November 15, 2026 11:15

@renkun-ken renkun-ken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 196d847. The S4 pseudo-slot fix addresses my previous P2 finding: inspection_hover("obj@.S3Class") now matches the existing hover handler. The new precheck preserves base @ behavior for .S3Class, .Data, and list-based names, while missing slots still return NULL and other errors propagate. The added regression tests cover the fix. No remaining actionable findings.

Validation: installed the exact updated source into an isolated library and ran the full sess tinytest suite on R 4.6.1: all 1,444 assertions passed, including 103 inspection assertions with R6 and S7 installed. Additional comparisons against base @ passed across S4 superclass types, and repeat inspection-test runs left no test bindings or classes behind. All 10 GitHub checks are successful.

@eitsupi
eitsupi merged commit d28ee5b into main Nov 15, 2026
10 checks passed
@eitsupi
eitsupi deleted the feat/sess-inspection-resolver branch November 15, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants