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"
)
)
- Trigger
textDocument/prepareTypeHierarchy on User → correctly returns one item,
classType: "S4".
- Trigger
typeHierarchy/supertypes on that item → expected [BaseEntity],
actual [].
- 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
Summary
For S4 and RefClass definitions using
contains = "ParentClass",prepareTypeHierarchycorrectly resolves and reports the definition (
classType: "S4"/"RefClass"), but thefollow-up
typeHierarchy/supertypesandtypeHierarchy/subtypesrequests always return anempty array, even when the inheritance relationship is unambiguous. There is also a smaller,
related range bug in the definitions that
prepareTypeHierarchyitself returns.Reproduction
textDocument/prepareTypeHierarchyonUser→ correctly returns one item,classType: "S4".typeHierarchy/supertypeson that item → expected[BaseEntity],actual
[].typeHierarchy/subtypeson that item → expected[AdminUser],actual
[].Root cause 1 — wrong parse-tree node types for named arguments
find_s4_supertypes,find_s4_subtypes,find_refclass_supertypesandfind_refclass_subtypes(inR/type_hierarchy.R) look for thecontainsargument with:SYMBOL/EQ_ASSIGNare the node types for top-level<-/=assignment. A namedargument inside a function call is tokenized differently. Confirmed with
getParseData():containsisSYMBOL_SUB, followed byEQ_SUB— neverSYMBOL/EQ_ASSIGN. The XPathnever matches, so
contains_paramis always empty and the four functions returnlist()unconditionally. (The same wrong node types are also used for
slots/representationinextract_s4_members, and forfields/methodsinextract_refclass_members— those arelikely affected too, though not reproduced here.)
Root cause 2 —
ancestor::expr[1]stops one level too earlyEven after fixing the node types above, the four
find_*functions still return nothing,because of a second, independent scoping bug:
For a call like
setClass("User", contains = "BaseEntity", ...), the parse tree looks like:SYMBOL_FUNCTION_CALL/ancestor::expr[1]returnsexpr(3), which wraps only the functionname —
contains, the class-name string, and every other argument are siblings ofexpr(3)insideexpr(37), not descendants of it. Any.//...search scoped toexpr(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_CALLnode instead of to itsparent::expr— so even theclass-name lookup inside these functions fails independently of the
containsbug. Comparewith
detect_s4class, which does this correctly viaSYMBOL_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 theSTR_CONSTnode for bothprepareTypeHierarchydefinitions and thefind_*results. ASTR_CONSTnode'scol1/col2span the whole literal including the quote characters, e.g.
"AdminUser"(11 chars) spanscolumns 9–20 instead of the identifier's actual 10–19. Clients that move the caret to
range.starton 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
SYMBOL_SUB+EQ_SUB(notSYMBOL+EQ_ASSIGN) when locating named argumentsinside a function call, throughout
R/type_hierarchy.R.ancestor::expr[2](or equivalentlyparent::expr/parent::expr) to reach the fullcall expression from
SYMBOL_FUNCTION_CALL, andparent::expr/following-sibling::expr[1](not
following-sibling::expr[1]directly onSYMBOL_FUNCTION_CALL) to reach the firstpositional argument.
STR_CONSTby 1 character on each side before returning them,so they point at the identifier text rather than the surrounding quotes.
Environment
languageserverversion: (CRAN release in use at time of writing — please confirm againstpackageVersion("languageserver"); themasterbranch'sfind_s4_supertypesetc. stillcontain the same
SYMBOL/EQ_ASSIGN/ancestor::expr[1]patterns as of this writing)I have a local monkey-patch (via
assignInNamespace) that fixes all three issues and canshare it if useful as a starting point for a PR.
r_type_hierarchy_patch.zip