Skip to content

Commit fd25e40

Browse files
feat: Add support for hover docs. (#35)
1 parent 08ef96d commit fd25e40

File tree

5 files changed

+640
-52
lines changed

5 files changed

+640
-52
lines changed

scip_indexer/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ cc_library(
4242
"//cfg",
4343
"//common",
4444
"//core",
45+
"//main/lsp",
4546
"//proto",
4647
"//sorbet_version",
4748
"@com_google_absl//absl/synchronization",

scip_indexer/SCIPIndexer.cc

Lines changed: 106 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "absl/status/status.h"
1515
#include "absl/status/statusor.h"
1616
#include "absl/strings/str_cat.h"
17+
#include "absl/strings/str_replace.h"
1718
#include "absl/strings/str_split.h"
1819
#include "absl/synchronization/mutex.h"
1920
#include "spdlog/fmt/fmt.h"
@@ -27,6 +28,7 @@
2728
#include "core/Loc.h"
2829
#include "core/SymbolRef.h"
2930
#include "core/Symbols.h"
31+
#include "main/lsp/lsp.h"
3032
#include "main/pipeline/semantic_extension/SemanticExtension.h"
3133
#include "sorbet_version/sorbet_version.h"
3234

@@ -165,6 +167,7 @@ class GemMetadata final {
165167
class NamedSymbolRef final {
166168
core::SymbolRef selfOrOwner;
167169
core::NameRef name;
170+
core::TypePtr type;
168171

169172
public:
170173
enum class Kind {
@@ -175,7 +178,7 @@ class NamedSymbolRef final {
175178
};
176179

177180
private:
178-
NamedSymbolRef(core::SymbolRef s, core::NameRef n, Kind k) : selfOrOwner(s), name(n) {
181+
NamedSymbolRef(core::SymbolRef s, core::NameRef n, core::TypePtr t, Kind k) : selfOrOwner(s), name(n), type(t) {
179182
switch (k) {
180183
case Kind::ClassOrModule:
181184
ENFORCE(s.isClassOrModule());
@@ -209,19 +212,19 @@ class NamedSymbolRef final {
209212
}
210213

211214
static NamedSymbolRef classOrModule(core::SymbolRef self) {
212-
return NamedSymbolRef(self, {}, Kind::ClassOrModule);
215+
return NamedSymbolRef(self, {}, {}, Kind::ClassOrModule);
213216
}
214217

215-
static NamedSymbolRef undeclaredField(core::SymbolRef owner, core::NameRef name) {
216-
return NamedSymbolRef(owner, name, Kind::UndeclaredField);
218+
static NamedSymbolRef undeclaredField(core::SymbolRef owner, core::NameRef name, core::TypePtr type) {
219+
return NamedSymbolRef(owner, name, type, Kind::UndeclaredField);
217220
}
218221

219222
static NamedSymbolRef staticField(core::SymbolRef self) {
220-
return NamedSymbolRef(self, {}, Kind::StaticField);
223+
return NamedSymbolRef(self, {}, {}, Kind::StaticField);
221224
}
222225

223226
static NamedSymbolRef method(core::SymbolRef self) {
224-
return NamedSymbolRef(self, {}, Kind::Method);
227+
return NamedSymbolRef(self, {}, {}, Kind::Method);
225228
}
226229

227230
Kind kind() const {
@@ -257,8 +260,61 @@ class NamedSymbolRef final {
257260
return this->selfOrOwner;
258261
}
259262

263+
vector<string> docStrings(const core::GlobalState &gs, core::Loc loc) {
264+
vector<string> docs;
265+
string markdown = "";
266+
switch (this->kind()) {
267+
case Kind::UndeclaredField: {
268+
markdown = fmt::format("{} = T.let(_, {})", this->name.show(gs), this->type.show(gs));
269+
break;
270+
}
271+
case Kind::StaticField: {
272+
auto fieldRef = this->selfOrOwner.asFieldRef();
273+
markdown =
274+
fmt::format("{} = T.let(_, {})", fieldRef.showFullName(gs), fieldRef.data(gs)->resultType.show(gs));
275+
break;
276+
}
277+
case Kind::ClassOrModule: {
278+
auto ref = this->selfOrOwner.asClassOrModuleRef();
279+
auto classOrModule = ref.data(gs);
280+
if (classOrModule->isClass()) {
281+
auto super = classOrModule->superClass();
282+
if (super.exists() && super != core::Symbols::Object()) {
283+
markdown = fmt::format("class {} < {}", ref.show(gs), super.show(gs));
284+
} else {
285+
markdown = fmt::format("class {}", ref.show(gs));
286+
}
287+
} else {
288+
markdown = fmt::format("module {}", ref.show(gs));
289+
}
290+
break;
291+
}
292+
case Kind::Method: {
293+
auto ref = this->selfOrOwner.asMethodRef();
294+
markdown = realmain::lsp::prettyTypeForMethod(gs, ref, ref.data(gs)->owner.data(gs)->resultType,
295+
nullptr, nullptr);
296+
// FIXME(varun): For some reason, it looks like a bunch of public methods
297+
// get marked as private here. Avoid printing misleading info until we fix that.
298+
// https://github.com/sourcegraph/scip-ruby/issues/33
299+
markdown = absl::StrReplaceAll(markdown, {{"private def", "def"}, {"; end", ""}});
300+
break;
301+
}
302+
}
303+
if (!markdown.empty()) {
304+
docs.push_back(fmt::format("```ruby\n{}\n```", markdown));
305+
}
306+
auto whatFile = loc.file();
307+
if (whatFile.exists()) {
308+
if (auto doc = realmain::lsp::findDocumentation(whatFile.data(gs).source(), loc.beginPos())) {
309+
docs.push_back(doc.value());
310+
}
311+
}
312+
return docs;
313+
}
314+
260315
// Returns OK if we were able to compute a symbol for the expression.
261-
absl::Status symbolForExpr(const core::GlobalState &gs, const GemMetadata &metadata, scip::Symbol &symbol) const {
316+
absl::Status symbolForExpr(const core::GlobalState &gs, const GemMetadata &metadata, scip::Symbol &symbol,
317+
optional<core::Loc> loc) const {
262318
// Don't set symbol.scheme and package.manager here because those are hard-coded to 'scip-ruby' and 'gem'
263319
// anyways.
264320
scip::Package package;
@@ -364,6 +420,7 @@ core::Loc trimColonColonPrefix(const core::GlobalState &gs, core::Loc baseLoc) {
364420
class SCIPState {
365421
string symbolScratchBuffer;
366422
UnorderedMap<NamedSymbolRef, string> symbolStringCache;
423+
UnorderedMap<uint32_t, core::TypePtr> localTypes;
367424
GemMetadata gemMetadata;
368425

369426
public:
@@ -408,7 +465,7 @@ class SCIPState {
408465
status = scip::utils::emitSymbolString(*symbol, this->symbolScratchBuffer);
409466
} else {
410467
scip::Symbol symbol;
411-
status = symRef.symbolForExpr(gs, this->gemMetadata, symbol);
468+
status = symRef.symbolForExpr(gs, this->gemMetadata, symbol, nullopt);
412469
if (!status.ok()) {
413470
return status;
414471
}
@@ -423,11 +480,14 @@ class SCIPState {
423480

424481
private:
425482
absl::Status saveDefinitionImpl(const core::GlobalState &gs, core::FileRef file, const string &symbolString,
426-
core::Loc occLoc) {
483+
core::Loc occLoc, const vector<string> &docs) {
427484
ENFORCE(!symbolString.empty());
428485
occLoc = trimColonColonPrefix(gs, occLoc);
429486
scip::SymbolInformation symbolInfo;
430487
symbolInfo.set_symbol(symbolString);
488+
for (auto &doc : docs) {
489+
symbolInfo.add_documentation(doc);
490+
}
431491
this->symbolMap[file].push_back(symbolInfo);
432492

433493
scip::Occurrence occurrence;
@@ -492,11 +552,18 @@ class SCIPState {
492552
}
493553

494554
public:
495-
absl::Status saveDefinition(const core::GlobalState &gs, core::FileRef file, OwnedLocal occ) {
555+
absl::Status saveDefinition(const core::GlobalState &gs, core::FileRef file, OwnedLocal occ, core::TypePtr type) {
496556
if (this->cacheOccurrence(gs, file, occ, scip::SymbolRole::Definition)) {
497557
return absl::OkStatus();
498558
}
499-
return this->saveDefinitionImpl(gs, file, occ.toString(gs, file), core::Loc(file, occ.offsets));
559+
vector<string> docStrings;
560+
auto loc = core::Loc(file, occ.offsets);
561+
if (type) {
562+
auto var = loc.source(gs);
563+
ENFORCE(var.has_value(), "Failed to find source text for definition of local variable");
564+
docStrings.push_back(fmt::format("```ruby\n{} = T.let(_, {})\n```", var.value(), type.show(gs)));
565+
}
566+
return this->saveDefinitionImpl(gs, file, occ.toString(gs, file), loc, docStrings);
500567
}
501568

502569
// Save definition when you have a sorbet Symbol.
@@ -506,7 +573,8 @@ class SCIPState {
506573
std::optional<core::LocOffsets> loc = std::nullopt) {
507574
// TODO:(varun) Should we cache here too to avoid emitting duplicate definitions?
508575
scip::Symbol symbol;
509-
auto status = symRef.symbolForExpr(gs, this->gemMetadata, symbol);
576+
auto occLoc = loc.has_value() ? core::Loc(file, loc.value()) : symRef.symbolLoc(gs);
577+
auto status = symRef.symbolForExpr(gs, this->gemMetadata, symbol, occLoc);
510578
if (!status.ok()) {
511579
return status;
512580
}
@@ -515,10 +583,7 @@ class SCIPState {
515583
return valueOrStatus.status();
516584
}
517585
string &symbolString = *valueOrStatus.value();
518-
519-
auto occLoc = loc.has_value() ? core::Loc(file, loc.value()) : symRef.symbolLoc(gs);
520-
521-
return this->saveDefinitionImpl(gs, file, symbolString, occLoc);
586+
return this->saveDefinitionImpl(gs, file, symbolString, occLoc, symRef.docStrings(gs, occLoc));
522587
}
523588

524589
absl::Status saveReference(const core::GlobalState &gs, core::FileRef file, OwnedLocal occ, int32_t symbol_roles) {
@@ -617,7 +682,8 @@ class AliasMap final {
617682
if (sym == core::Symbols::Magic_undeclaredFieldStub()) {
618683
ENFORCE(!bind.loc.empty());
619684
this->map.insert( // no trim(...) because undeclared fields shouldn't have ::
620-
{bind.bind.variable, {NamedSymbolRef::undeclaredField(klass, instr->name), bind.loc, false}});
685+
{bind.bind.variable,
686+
{NamedSymbolRef::undeclaredField(klass, instr->name, bind.bind.type), bind.loc, false}});
621687
continue;
622688
}
623689
if (sym.isStaticField(gs)) {
@@ -685,6 +751,7 @@ class CFGTraversal final {
685751
// in the block.
686752
UnorderedMap<const cfg::BasicBlock *, UnorderedSet<cfg::LocalRef>> blockLocals;
687753
UnorderedMap<cfg::LocalRef, uint32_t> functionLocals;
754+
UnorderedMap<uint32_t, core::TypePtr> localTypes;
688755
AliasMap aliasMap;
689756

690757
// Local variable counter that is reset for every function.
@@ -697,9 +764,11 @@ class CFGTraversal final {
697764
: blockLocals(), functionLocals(), aliasMap(), scipState(scipState), ctx(ctx) {}
698765

699766
private:
700-
void addLocal(const cfg::BasicBlock *bb, cfg::LocalRef localRef) {
767+
uint32_t addLocal(const cfg::BasicBlock *bb, cfg::LocalRef localRef) {
768+
this->counter++;
701769
this->blockLocals[bb].insert(localRef);
702-
this->functionLocals[localRef] = ++this->counter;
770+
this->functionLocals[localRef] = this->counter;
771+
return this->counter;
703772
}
704773

705774
static core::LocOffsets lhsLocIfPresent(const cfg::Binding &binding) {
@@ -717,8 +786,10 @@ class CFGTraversal final {
717786
// Emit an occurrence for a local variable if applicable.
718787
//
719788
// Returns true if an occurrence was emitted.
789+
//
790+
// The type should be provided if we have an lvalue.
720791
bool emitLocalOccurrence(const cfg::CFG &cfg, const cfg::BasicBlock *bb, cfg::LocalOccurrence local,
721-
ValueCategory category) {
792+
ValueCategory category, core::TypePtr type) {
722793
auto localRef = local.variable;
723794
auto localVar = localRef.data(cfg);
724795
auto symRef = this->aliasMap.try_consume(localRef);
@@ -733,7 +804,9 @@ class CFGTraversal final {
733804
if (!this->functionLocals.contains(localRef)) {
734805
isDefinition = true; // If we're seeing this for the first time in topological order,
735806
// The current block must have a definition for the variable.
736-
this->addLocal(bb, localRef);
807+
auto id = this->addLocal(bb, localRef);
808+
ENFORCE(type, "missing type for lvalue");
809+
this->localTypes[id] = type;
737810
}
738811
// The variable wasn't passed in as an argument, and hasn't already been recorded
739812
// as a local in the block. So this must be a definition line.
@@ -774,9 +847,14 @@ class CFGTraversal final {
774847
}
775848
} else {
776849
uint32_t localId = this->functionLocals[localRef];
850+
auto it = this->localTypes.find(localId);
851+
core::TypePtr type{};
852+
if (it != this->localTypes.end()) {
853+
type = it->second;
854+
}
777855
if (isDefinition) {
778856
status = this->scipState.saveDefinition(this->ctx.state, this->ctx.file,
779-
OwnedLocal{this->ctx.owner, localId, loc});
857+
OwnedLocal{this->ctx.owner, localId, loc}, type);
780858
} else {
781859
status = this->scipState.saveReference(this->ctx.state, this->ctx.file,
782860
OwnedLocal{this->ctx.owner, localId, loc}, referenceRole);
@@ -856,12 +934,12 @@ class CFGTraversal final {
856934
if (binding.value.tag() != cfg::Tag::Alias && binding.value.tag() != cfg::Tag::ArgPresent) {
857935
// Emit occurrence information for the LHS
858936
auto occ = cfg::LocalOccurrence{binding.bind.variable, lhsLocIfPresent(binding)};
859-
this->emitLocalOccurrence(cfg, bb, occ, ValueCategory::LValue);
937+
this->emitLocalOccurrence(cfg, bb, occ, ValueCategory::LValue, binding.bind.type);
860938
}
861939
// Emit occurrence information for the RHS
862940
auto emitLocal = [this, &cfg, &bb, &binding](cfg::LocalRef local) -> void {
863941
(void)this->emitLocalOccurrence(cfg, bb, cfg::LocalOccurrence{local, binding.loc},
864-
ValueCategory::RValue);
942+
ValueCategory::RValue, core::TypePtr());
865943
};
866944
switch (binding.value.tag()) {
867945
case cfg::Tag::Literal: {
@@ -879,7 +957,8 @@ class CFGTraversal final {
879957

880958
// Emit reference for the receiver, if present.
881959
if (send->recv.loc.exists() && !send->recv.loc.empty()) {
882-
this->emitLocalOccurrence(cfg, bb, send->recv.occurrence(), ValueCategory::RValue);
960+
this->emitLocalOccurrence(cfg, bb, send->recv.occurrence(), ValueCategory::RValue,
961+
core::TypePtr());
883962
}
884963

885964
// Emit reference for the method being called
@@ -919,7 +998,8 @@ class CFGTraversal final {
919998
// and the first one is a write. Instead of emitting two occurrences, it'd be nice to emit
920999
// a combined read-write occurrence. However, that would require complicating the code a
9211000
// bit, so let's leave it as-is for now.
922-
this->emitLocalOccurrence(cfg, bb, arg.occurrence(), ValueCategory::RValue);
1001+
this->emitLocalOccurrence(cfg, bb, arg.occurrence(), ValueCategory::RValue,
1002+
core::TypePtr());
9231003
}
9241004

9251005
break;

0 commit comments

Comments
 (0)