Skip to content

textDocument/prepareTypeHierarchy works, but typeHierarchy/supertypes and typeHierarchy/subtypes always return [] for S4 / RefClass #738

Description

@Hanatarou

Summary

For S4 and RefClass definitions using contains = "ParentClass", prepareTypeHierarchy
correctly resolves and reports the definition (classType: "S4" / "RefClass"), but the
follow-up typeHierarchy/supertypes and typeHierarchy/subtypes requests always return an
empty array, even when the inheritance relationship is unambiguous. There is also a smaller,
related range bug in the definitions that prepareTypeHierarchy itself returns.

Reproduction

setClass("BaseEntity",
  representation(
    id         = "character",
    created_at = "POSIXct"
  )
)

setClass("User",
  contains = "BaseEntity",
  representation(
    name  = "character",
    email = "character"
  )
)

setClass("AdminUser",
  contains = "User",
  representation(
    department = "character"
  )
)
  1. Trigger textDocument/prepareTypeHierarchy on User → correctly returns one item,
    classType: "S4".
  2. Trigger typeHierarchy/supertypes on that item → expected [BaseEntity],
    actual [].
  3. Trigger typeHierarchy/subtypes on that item → expected [AdminUser],
    actual [].

Root cause 1 — wrong parse-tree node types for named arguments

find_s4_supertypes, find_s4_subtypes, find_refclass_supertypes and
find_refclass_subtypes (in R/type_hierarchy.R) look for the contains argument with:

xml_find_first(setclass_call,
  ".//SYMBOL[text() = 'contains']/following-sibling::*[1][self::EQ_ASSIGN]/following-sibling::expr[1]")

SYMBOL / EQ_ASSIGN are the node types for top-level <-/= assignment. A named
argument inside a function call is tokenized differently. Confirmed with getParseData():

code <- 'setClass("User", contains = "BaseEntity", representation(name = "character"))'
pd <- getParseData(parse(text = code, keep.source = TRUE))
print(pd[pd$token %in% c("SYMBOL_SUB","SYMBOL","EQ_SUB","EQ_ASSIGN","STR_CONST"),
          c("token","text","col1","col2")])
      token         text col1 col2
  STR_CONST       "User"   10   15
 SYMBOL_SUB     contains   18   25
     EQ_SUB            =   27   27
  STR_CONST "BaseEntity"   29   40
 SYMBOL_SUB         name   58   61
     EQ_SUB            =   63   63
  STR_CONST  "character"   65   75

contains is SYMBOL_SUB, followed by EQ_SUB — never SYMBOL/EQ_ASSIGN. The XPath
never matches, so contains_param is always empty and the four functions return list()
unconditionally. (The same wrong node types are also used for slots/representation in
extract_s4_members, and for fields/methods in extract_refclass_members — those are
likely affected too, though not reproduced here.)

Root cause 2 — ancestor::expr[1] stops one level too early

Even after fixing the node types above, the four find_* functions still return nothing,
because of a second, independent scoping bug:

calls <- xml_find_all(xdoc, "//SYMBOL_FUNCTION_CALL[text() = 'setClass']/ancestor::expr[1]")

For a call like setClass("User", contains = "BaseEntity", ...), the parse tree looks like:

expr (id=37)                       <- the full call
├── expr (id=3)                    <- ancestor::expr[1] stops HERE
│   └── SYMBOL_FUNCTION_CALL "setClass"
├── '('
├── expr [STR_CONST "User"]
├── SYMBOL_SUB "contains"
├── EQ_SUB "="
├── expr [STR_CONST "BaseEntity"]
└── ...

SYMBOL_FUNCTION_CALL/ancestor::expr[1] returns expr(3), which wraps only the function
name — contains, the class-name string, and every other argument are siblings of
expr(3) inside expr(37), not descendants of it. Any .//... search scoped to expr(3)
can never find them. This needs ancestor::expr[2] to reach the full call expression.

The same following-sibling::expr[1] pattern used to locate the first positional argument
(the class-name string) inside these four functions has the identical bug — it's applied
directly to the SYMBOL_FUNCTION_CALL node instead of to its parent::expr — so even the
class-name lookup inside these functions fails independently of the contains bug. Compare
with detect_s4class, which does this correctly via
SYMBOL_FUNCTION_CALL[...]/parent::expr/following-sibling::expr[1].

Root cause 3 (minor) — returned ranges include the surrounding quotes

get_element_range() is called directly on the STR_CONST node for both
prepareTypeHierarchy definitions and the find_* results. A STR_CONST node's col1/col2
span the whole literal including the quote characters, e.g. "AdminUser" (11 chars) spans
columns 9–20 instead of the identifier's actual 10–19. Clients that move the caret to
range.start on selection therefore land on the opening quote rather than on the class name,
which breaks any editor action that expects the caret to be on the symbol.

Suggested fix

  • Use SYMBOL_SUB + EQ_SUB (not SYMBOL + EQ_ASSIGN) when locating named arguments
    inside a function call, throughout R/type_hierarchy.R.
  • Use ancestor::expr[2] (or equivalently parent::expr/parent::expr) to reach the full
    call expression from SYMBOL_FUNCTION_CALL, and parent::expr/following-sibling::expr[1]
    (not following-sibling::expr[1] directly on SYMBOL_FUNCTION_CALL) to reach the first
    positional argument.
  • Shrink ranges derived from a STR_CONST by 1 character on each side before returning them,
    so they point at the identifier text rather than the surrounding quotes.

Environment

  • languageserver version: (CRAN release in use at time of writing — please confirm against
    packageVersion("languageserver"); the master branch's find_s4_supertypes etc. still
    contain the same SYMBOL/EQ_ASSIGN/ancestor::expr[1] patterns as of this writing)
  • R version: 4.3.3
  • OS: Windows (reproduced), pattern is OS-independent (pure parse-tree issue)

I have a local monkey-patch (via assignInNamespace) that fixes all three issues and can
share it if useful as a starting point for a PR.

r_type_hierarchy_patch.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions