From 66ffba985a8da6cca317a6c7983179c361c5866e Mon Sep 17 00:00:00 2001 From: Varun Gandhi Date: Thu, 21 Jul 2022 18:59:29 +0800 Subject: [PATCH] feat: Add support for hover docs. --- scip_indexer/BUILD | 1 + scip_indexer/SCIPIndexer.cc | 132 ++++++-- test/scip/testdata/hoverdocs.rb | 110 +++++++ test/scip/testdata/hoverdocs.snapshot.rb | 385 +++++++++++++++++++++++ test/scip_test_runner.cc | 64 ++-- 5 files changed, 640 insertions(+), 52 deletions(-) create mode 100644 test/scip/testdata/hoverdocs.rb create mode 100644 test/scip/testdata/hoverdocs.snapshot.rb diff --git a/scip_indexer/BUILD b/scip_indexer/BUILD index add60c5a04..a7f094f65a 100644 --- a/scip_indexer/BUILD +++ b/scip_indexer/BUILD @@ -42,6 +42,7 @@ cc_library( "//cfg", "//common", "//core", + "//main/lsp", "//proto", "//sorbet_version", "@com_google_absl//absl/synchronization", diff --git a/scip_indexer/SCIPIndexer.cc b/scip_indexer/SCIPIndexer.cc index b6e1ca99a4..a192b59a47 100644 --- a/scip_indexer/SCIPIndexer.cc +++ b/scip_indexer/SCIPIndexer.cc @@ -14,6 +14,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_replace.h" #include "absl/strings/str_split.h" #include "absl/synchronization/mutex.h" #include "spdlog/fmt/fmt.h" @@ -27,6 +28,7 @@ #include "core/Loc.h" #include "core/SymbolRef.h" #include "core/Symbols.h" +#include "main/lsp/lsp.h" #include "main/pipeline/semantic_extension/SemanticExtension.h" #include "sorbet_version/sorbet_version.h" @@ -165,6 +167,7 @@ class GemMetadata final { class NamedSymbolRef final { core::SymbolRef selfOrOwner; core::NameRef name; + core::TypePtr type; public: enum class Kind { @@ -175,7 +178,7 @@ class NamedSymbolRef final { }; private: - NamedSymbolRef(core::SymbolRef s, core::NameRef n, Kind k) : selfOrOwner(s), name(n) { + NamedSymbolRef(core::SymbolRef s, core::NameRef n, core::TypePtr t, Kind k) : selfOrOwner(s), name(n), type(t) { switch (k) { case Kind::ClassOrModule: ENFORCE(s.isClassOrModule()); @@ -209,19 +212,19 @@ class NamedSymbolRef final { } static NamedSymbolRef classOrModule(core::SymbolRef self) { - return NamedSymbolRef(self, {}, Kind::ClassOrModule); + return NamedSymbolRef(self, {}, {}, Kind::ClassOrModule); } - static NamedSymbolRef undeclaredField(core::SymbolRef owner, core::NameRef name) { - return NamedSymbolRef(owner, name, Kind::UndeclaredField); + static NamedSymbolRef undeclaredField(core::SymbolRef owner, core::NameRef name, core::TypePtr type) { + return NamedSymbolRef(owner, name, type, Kind::UndeclaredField); } static NamedSymbolRef staticField(core::SymbolRef self) { - return NamedSymbolRef(self, {}, Kind::StaticField); + return NamedSymbolRef(self, {}, {}, Kind::StaticField); } static NamedSymbolRef method(core::SymbolRef self) { - return NamedSymbolRef(self, {}, Kind::Method); + return NamedSymbolRef(self, {}, {}, Kind::Method); } Kind kind() const { @@ -257,8 +260,61 @@ class NamedSymbolRef final { return this->selfOrOwner; } + vector docStrings(const core::GlobalState &gs, core::Loc loc) { + vector docs; + string markdown = ""; + switch (this->kind()) { + case Kind::UndeclaredField: { + markdown = fmt::format("{} = T.let(_, {})", this->name.show(gs), this->type.show(gs)); + break; + } + case Kind::StaticField: { + auto fieldRef = this->selfOrOwner.asFieldRef(); + markdown = + fmt::format("{} = T.let(_, {})", fieldRef.showFullName(gs), fieldRef.data(gs)->resultType.show(gs)); + break; + } + case Kind::ClassOrModule: { + auto ref = this->selfOrOwner.asClassOrModuleRef(); + auto classOrModule = ref.data(gs); + if (classOrModule->isClass()) { + auto super = classOrModule->superClass(); + if (super.exists() && super != core::Symbols::Object()) { + markdown = fmt::format("class {} < {}", ref.show(gs), super.show(gs)); + } else { + markdown = fmt::format("class {}", ref.show(gs)); + } + } else { + markdown = fmt::format("module {}", ref.show(gs)); + } + break; + } + case Kind::Method: { + auto ref = this->selfOrOwner.asMethodRef(); + markdown = realmain::lsp::prettyTypeForMethod(gs, ref, ref.data(gs)->owner.data(gs)->resultType, + nullptr, nullptr); + // FIXME(varun): For some reason, it looks like a bunch of public methods + // get marked as private here. Avoid printing misleading info until we fix that. + // https://github.com/sourcegraph/scip-ruby/issues/33 + markdown = absl::StrReplaceAll(markdown, {{"private def", "def"}, {"; end", ""}}); + break; + } + } + if (!markdown.empty()) { + docs.push_back(fmt::format("```ruby\n{}\n```", markdown)); + } + auto whatFile = loc.file(); + if (whatFile.exists()) { + if (auto doc = realmain::lsp::findDocumentation(whatFile.data(gs).source(), loc.beginPos())) { + docs.push_back(doc.value()); + } + } + return docs; + } + // Returns OK if we were able to compute a symbol for the expression. - absl::Status symbolForExpr(const core::GlobalState &gs, const GemMetadata &metadata, scip::Symbol &symbol) const { + absl::Status symbolForExpr(const core::GlobalState &gs, const GemMetadata &metadata, scip::Symbol &symbol, + optional loc) const { // Don't set symbol.scheme and package.manager here because those are hard-coded to 'scip-ruby' and 'gem' // anyways. scip::Package package; @@ -364,6 +420,7 @@ core::Loc trimColonColonPrefix(const core::GlobalState &gs, core::Loc baseLoc) { class SCIPState { string symbolScratchBuffer; UnorderedMap symbolStringCache; + UnorderedMap localTypes; GemMetadata gemMetadata; public: @@ -408,7 +465,7 @@ class SCIPState { status = scip::utils::emitSymbolString(*symbol, this->symbolScratchBuffer); } else { scip::Symbol symbol; - status = symRef.symbolForExpr(gs, this->gemMetadata, symbol); + status = symRef.symbolForExpr(gs, this->gemMetadata, symbol, nullopt); if (!status.ok()) { return status; } @@ -423,11 +480,14 @@ class SCIPState { private: absl::Status saveDefinitionImpl(const core::GlobalState &gs, core::FileRef file, const string &symbolString, - core::Loc occLoc) { + core::Loc occLoc, const vector &docs) { ENFORCE(!symbolString.empty()); occLoc = trimColonColonPrefix(gs, occLoc); scip::SymbolInformation symbolInfo; symbolInfo.set_symbol(symbolString); + for (auto &doc : docs) { + symbolInfo.add_documentation(doc); + } this->symbolMap[file].push_back(symbolInfo); scip::Occurrence occurrence; @@ -492,11 +552,18 @@ class SCIPState { } public: - absl::Status saveDefinition(const core::GlobalState &gs, core::FileRef file, OwnedLocal occ) { + absl::Status saveDefinition(const core::GlobalState &gs, core::FileRef file, OwnedLocal occ, core::TypePtr type) { if (this->cacheOccurrence(gs, file, occ, scip::SymbolRole::Definition)) { return absl::OkStatus(); } - return this->saveDefinitionImpl(gs, file, occ.toString(gs, file), core::Loc(file, occ.offsets)); + vector docStrings; + auto loc = core::Loc(file, occ.offsets); + if (type) { + auto var = loc.source(gs); + ENFORCE(var.has_value(), "Failed to find source text for definition of local variable"); + docStrings.push_back(fmt::format("```ruby\n{} = T.let(_, {})\n```", var.value(), type.show(gs))); + } + return this->saveDefinitionImpl(gs, file, occ.toString(gs, file), loc, docStrings); } // Save definition when you have a sorbet Symbol. @@ -506,7 +573,8 @@ class SCIPState { std::optional loc = std::nullopt) { // TODO:(varun) Should we cache here too to avoid emitting duplicate definitions? scip::Symbol symbol; - auto status = symRef.symbolForExpr(gs, this->gemMetadata, symbol); + auto occLoc = loc.has_value() ? core::Loc(file, loc.value()) : symRef.symbolLoc(gs); + auto status = symRef.symbolForExpr(gs, this->gemMetadata, symbol, occLoc); if (!status.ok()) { return status; } @@ -515,10 +583,7 @@ class SCIPState { return valueOrStatus.status(); } string &symbolString = *valueOrStatus.value(); - - auto occLoc = loc.has_value() ? core::Loc(file, loc.value()) : symRef.symbolLoc(gs); - - return this->saveDefinitionImpl(gs, file, symbolString, occLoc); + return this->saveDefinitionImpl(gs, file, symbolString, occLoc, symRef.docStrings(gs, occLoc)); } absl::Status saveReference(const core::GlobalState &gs, core::FileRef file, OwnedLocal occ, int32_t symbol_roles) { @@ -617,7 +682,8 @@ class AliasMap final { if (sym == core::Symbols::Magic_undeclaredFieldStub()) { ENFORCE(!bind.loc.empty()); this->map.insert( // no trim(...) because undeclared fields shouldn't have :: - {bind.bind.variable, {NamedSymbolRef::undeclaredField(klass, instr->name), bind.loc, false}}); + {bind.bind.variable, + {NamedSymbolRef::undeclaredField(klass, instr->name, bind.bind.type), bind.loc, false}}); continue; } if (sym.isStaticField(gs)) { @@ -685,6 +751,7 @@ class CFGTraversal final { // in the block. UnorderedMap> blockLocals; UnorderedMap functionLocals; + UnorderedMap localTypes; AliasMap aliasMap; // Local variable counter that is reset for every function. @@ -697,9 +764,11 @@ class CFGTraversal final { : blockLocals(), functionLocals(), aliasMap(), scipState(scipState), ctx(ctx) {} private: - void addLocal(const cfg::BasicBlock *bb, cfg::LocalRef localRef) { + uint32_t addLocal(const cfg::BasicBlock *bb, cfg::LocalRef localRef) { + this->counter++; this->blockLocals[bb].insert(localRef); - this->functionLocals[localRef] = ++this->counter; + this->functionLocals[localRef] = this->counter; + return this->counter; } static core::LocOffsets lhsLocIfPresent(const cfg::Binding &binding) { @@ -717,8 +786,10 @@ class CFGTraversal final { // Emit an occurrence for a local variable if applicable. // // Returns true if an occurrence was emitted. + // + // The type should be provided if we have an lvalue. bool emitLocalOccurrence(const cfg::CFG &cfg, const cfg::BasicBlock *bb, cfg::LocalOccurrence local, - ValueCategory category) { + ValueCategory category, core::TypePtr type) { auto localRef = local.variable; auto localVar = localRef.data(cfg); auto symRef = this->aliasMap.try_consume(localRef); @@ -733,7 +804,9 @@ class CFGTraversal final { if (!this->functionLocals.contains(localRef)) { isDefinition = true; // If we're seeing this for the first time in topological order, // The current block must have a definition for the variable. - this->addLocal(bb, localRef); + auto id = this->addLocal(bb, localRef); + ENFORCE(type, "missing type for lvalue"); + this->localTypes[id] = type; } // The variable wasn't passed in as an argument, and hasn't already been recorded // as a local in the block. So this must be a definition line. @@ -774,9 +847,14 @@ class CFGTraversal final { } } else { uint32_t localId = this->functionLocals[localRef]; + auto it = this->localTypes.find(localId); + core::TypePtr type{}; + if (it != this->localTypes.end()) { + type = it->second; + } if (isDefinition) { status = this->scipState.saveDefinition(this->ctx.state, this->ctx.file, - OwnedLocal{this->ctx.owner, localId, loc}); + OwnedLocal{this->ctx.owner, localId, loc}, type); } else { status = this->scipState.saveReference(this->ctx.state, this->ctx.file, OwnedLocal{this->ctx.owner, localId, loc}, referenceRole); @@ -856,12 +934,12 @@ class CFGTraversal final { if (binding.value.tag() != cfg::Tag::Alias && binding.value.tag() != cfg::Tag::ArgPresent) { // Emit occurrence information for the LHS auto occ = cfg::LocalOccurrence{binding.bind.variable, lhsLocIfPresent(binding)}; - this->emitLocalOccurrence(cfg, bb, occ, ValueCategory::LValue); + this->emitLocalOccurrence(cfg, bb, occ, ValueCategory::LValue, binding.bind.type); } // Emit occurrence information for the RHS auto emitLocal = [this, &cfg, &bb, &binding](cfg::LocalRef local) -> void { (void)this->emitLocalOccurrence(cfg, bb, cfg::LocalOccurrence{local, binding.loc}, - ValueCategory::RValue); + ValueCategory::RValue, core::TypePtr()); }; switch (binding.value.tag()) { case cfg::Tag::Literal: { @@ -879,7 +957,8 @@ class CFGTraversal final { // Emit reference for the receiver, if present. if (send->recv.loc.exists() && !send->recv.loc.empty()) { - this->emitLocalOccurrence(cfg, bb, send->recv.occurrence(), ValueCategory::RValue); + this->emitLocalOccurrence(cfg, bb, send->recv.occurrence(), ValueCategory::RValue, + core::TypePtr()); } // Emit reference for the method being called @@ -919,7 +998,8 @@ class CFGTraversal final { // and the first one is a write. Instead of emitting two occurrences, it'd be nice to emit // a combined read-write occurrence. However, that would require complicating the code a // bit, so let's leave it as-is for now. - this->emitLocalOccurrence(cfg, bb, arg.occurrence(), ValueCategory::RValue); + this->emitLocalOccurrence(cfg, bb, arg.occurrence(), ValueCategory::RValue, + core::TypePtr()); } break; diff --git a/test/scip/testdata/hoverdocs.rb b/test/scip/testdata/hoverdocs.rb new file mode 100644 index 0000000000..c697ece0ab --- /dev/null +++ b/test/scip/testdata/hoverdocs.rb @@ -0,0 +1,110 @@ +# typed: true +# options: showDocs + +# Class doc comment +class C1 + def m1 + end + + sig { returns(T::Boolean) } + def m2 + true + end + + sig { params(C, T::Boolean).returns(T::Boolean) } + def m3(c, b) + c.m2 || b + end + + # _This_ is a + # **doc comment.** + def m4(xs) + xs[0] + end + + # Yet another.. + # ...doc comment + sig { returns(T::Boolean) } + def m5 + true + end + + # And... + # ...one more doc comment + sig { params(C, T::Boolean).returns(T::Boolean) } + def m6(c, b) + c.m2 || b + end +end + +class C2 # undocumented class +end + +# Module doc comment +# +# Some stuff +module M1 + # This class is nested inside M1 + class C3 + end + + # This module is nested inside M1 + module M2 + # This method is inside M1::M2 + sig { returns(T::Boolean) } + def n1 + true + end + + # This method is also inside M1::M2 + def n2 + end + end +end + +# This is a global function +def f1 + M1::M2::m6 + M1::M2::m7 +end + +# Yet another global function +sig { returns(T::Integer) } +def f2 + return 10 +end + +def f3 # undocumented global function +end + +sig { returns(T::Integer) } +def f4 # another undocumented global function + return 10 +end + +# Parent class +class K1 + # sets @x and @@y + def p1 + @x = 10 + @@y = 10 + end + + # lorem ipsum, you get it + def self.p2 + @z = 10 + end +end + +# Subclass +class K2 < K1 + # doc comment on class var ooh + @z = 9 + + # overrides K1's p1 + def p1 + @x = 20 + @@y = 20 + @z += @x + end +end diff --git a/test/scip/testdata/hoverdocs.snapshot.rb b/test/scip/testdata/hoverdocs.snapshot.rb new file mode 100644 index 0000000000..4648efdeff --- /dev/null +++ b/test/scip/testdata/hoverdocs.snapshot.rb @@ -0,0 +1,385 @@ + # typed: true + # options: showDocs + + # Class doc comment + class C1 +# ^^ definition scip-ruby gem TODO TODO C1# +# documentation +# | ```ruby +# | class C1 +# | ``` +# documentation +# | Class doc comment + def m1 +# ^^^^^^ definition scip-ruby gem TODO TODO C1#m1(). +# documentation +# | ```ruby +# | sig {returns(T.untyped)} +# | def m1 +# | ``` + end + + sig { returns(T::Boolean) } +# ^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static##sig(). +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T#Boolean. +# ^^^^^^^^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static#ResolvedSig# + def m2 +# ^^^^^^ definition scip-ruby gem TODO TODO C1#m2(). +# documentation +# | ```ruby +# | sig {returns(T::Boolean)} +# | def m2 +# | ``` + true + end + + sig { params(C, T::Boolean).returns(T::Boolean) } +# ^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static##sig(). +# ^ reference scip-ruby gem TODO TODO T.untyped# +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T#Boolean. +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T#Boolean. +# ^^^^^^^^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static#ResolvedSig# + def m3(c, b) +# ^^^^^^^^^^^^ definition scip-ruby gem TODO TODO C1#m3(). +# documentation +# | ```ruby +# | sig {params(c: T.untyped, b: T.untyped).returns(T::Boolean)} +# | def m3(c, b) +# | ``` +# ^ definition local 1~#2519626513 +# documentation +# | ```ruby +# | c = T.let(_, T.untyped) +# | ``` +# ^ definition local 2~#2519626513 +# documentation +# | ```ruby +# | b = T.let(_, T.untyped) +# | ``` + c.m2 || b +# ^ reference local 1~#2519626513 +# ^ reference local 2~#2519626513 + end + + # _This_ is a + # **doc comment.** + def m4(xs) +# ^^^^^^^^^^ definition scip-ruby gem TODO TODO C1#m4(). +# documentation +# | ```ruby +# | sig {params(xs: T.untyped).returns(T.untyped)} +# | def m4(xs) +# | ``` +# documentation +# | _This_ is a +# | **doc comment.** +# ^^ definition local 1~#2536404132 +# documentation +# | ```ruby +# | xs = T.let(_, T.untyped) +# | ``` + xs[0] +# ^^ reference local 1~#2536404132 + end + + # Yet another.. + # ...doc comment + sig { returns(T::Boolean) } +# ^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static##sig(). +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T#Boolean. +# ^^^^^^^^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static#ResolvedSig# + def m5 +# ^^^^^^ definition scip-ruby gem TODO TODO C1#m5(). +# documentation +# | ```ruby +# | sig {returns(T::Boolean)} +# | def m5 +# | ``` +# documentation +# | Yet another.. +# | ...doc comment + true + end + + # And... + # ...one more doc comment + sig { params(C, T::Boolean).returns(T::Boolean) } +# ^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static##sig(). +# ^ reference scip-ruby gem TODO TODO T.untyped# +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T#Boolean. +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T#Boolean. +# ^^^^^^^^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static#ResolvedSig# + def m6(c, b) +# ^^^^^^^^^^^^ definition scip-ruby gem TODO TODO C1#m6(). +# documentation +# | ```ruby +# | sig {params(c: T.untyped, b: T.untyped).returns(T::Boolean)} +# | def m6(c, b) +# | ``` +# documentation +# | And... +# | ...one more doc comment +# ^ definition local 1~#2569959370 +# documentation +# | ```ruby +# | c = T.let(_, T.untyped) +# | ``` +# ^ definition local 2~#2569959370 +# documentation +# | ```ruby +# | b = T.let(_, T.untyped) +# | ``` + c.m2 || b +# ^ reference local 1~#2569959370 +# ^ reference local 2~#2569959370 + end + end + + class C2 # undocumented class +# ^^ definition scip-ruby gem TODO TODO C2# +# documentation +# | ```ruby +# | class C2 +# | ``` + end + + # Module doc comment + # + # Some stuff + module M1 +# ^^ definition scip-ruby gem TODO TODO M1# +# documentation +# | ```ruby +# | module M1 +# | ``` +# documentation +# | Module doc comment +# | +# | Some stuff + # This class is nested inside M1 + class C3 +# ^^ definition scip-ruby gem TODO TODO M1#C3# +# documentation +# | ```ruby +# | class M1::C3 +# | ``` +# documentation +# | This class is nested inside M1 + end + + # This module is nested inside M1 + module M2 +# ^^ definition scip-ruby gem TODO TODO M1#M2# +# documentation +# | ```ruby +# | module M1::M2 +# | ``` +# documentation +# | This module is nested inside M1 + # This method is inside M1::M2 + sig { returns(T::Boolean) } +# ^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static##sig(). +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T#Boolean. +# ^^^^^^^^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static#ResolvedSig# + def n1 +# ^^^^^^ definition scip-ruby gem TODO TODO M1#M2#n1(). +# documentation +# | ```ruby +# | sig {returns(T::Boolean)} +# | def n1 +# | ``` +# documentation +# | This method is inside M1::M2 + true + end + + # This method is also inside M1::M2 + def n2 +# ^^^^^^ definition scip-ruby gem TODO TODO M1#M2#n2(). +# documentation +# | ```ruby +# | sig {returns(T.untyped)} +# | def n2 +# | ``` +# documentation +# | This method is also inside M1::M2 + end + end + end + + # This is a global function + def f1 +#^^^^^^ definition scip-ruby gem TODO TODO Object#f1(). +#documentation +#| ```ruby +#| sig {returns(T.untyped)} +#| def f1 +#| ``` +#documentation +#| This is a global function + M1::M2::m6 +# ^^ reference scip-ruby gem TODO TODO M1# +# ^^ reference scip-ruby gem TODO TODO M1#M2# + M1::M2::m7 +# ^^ reference scip-ruby gem TODO TODO M1# +# ^^ reference scip-ruby gem TODO TODO M1#M2# + end + + # Yet another global function + sig { returns(T::Integer) } +#^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static##sig(). +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T.untyped# +# ^^^^^^^^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static#ResolvedSig# + def f2 +#^^^^^^ definition scip-ruby gem TODO TODO Object#f2(). +#documentation +#| ```ruby +#| sig {returns(T::Integer (unresolved))} +#| def f2 +#| ``` +#documentation +#| Yet another global function + return 10 + end + + def f3 # undocumented global function +#^^^^^^ definition scip-ruby gem TODO TODO Object#f3(). +#documentation +#| ```ruby +#| sig {returns(T.untyped)} +#| def f3 +#| ``` + end + + sig { returns(T::Integer) } +#^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static##sig(). +# ^ reference scip-ruby gem TODO TODO T# +# ^^^^^^^ reference scip-ruby gem TODO TODO T.untyped# +# ^^^^^^^^^^ reference scip-ruby gem TODO TODO Sorbet#Private#Static#ResolvedSig# + def f4 # another undocumented global function +#^^^^^^ definition scip-ruby gem TODO TODO Object#f4(). +#documentation +#| ```ruby +#| sig {returns(T::Integer (unresolved))} +#| def f4 +#| ``` + return 10 + end + + # Parent class + class K1 +# ^^ definition scip-ruby gem TODO TODO K1# +# documentation +# | ```ruby +# | class K1 +# | ``` +# documentation +# | Parent class + # sets @x and @@y + def p1 +# ^^^^^^ definition scip-ruby gem TODO TODO K1#p1(). +# documentation +# | ```ruby +# | sig {returns(T.untyped)} +# | def p1 +# | ``` +# documentation +# | sets @x and @@y + @x = 10 +# ^^ definition scip-ruby gem TODO TODO K1#@x. +# documentation +# | ```ruby +# | @x = T.let(_, T.untyped) +# | ``` + @@y = 10 +# ^^^ definition scip-ruby gem TODO TODO K1#@@y. +# documentation +# | ```ruby +# | @@y = T.let(_, T.untyped) +# | ``` +# ^^^^^^^^ reference scip-ruby gem TODO TODO K1#@@y. + end + + # lorem ipsum, you get it + def self.p2 +# ^^^^^^^^^^^ definition scip-ruby gem TODO TODO #p2(). +# documentation +# | ```ruby +# | sig {returns(T.untyped)} +# | def self.p2 +# | ``` +# documentation +# | lorem ipsum, you get it + @z = 10 +# ^^ definition scip-ruby gem TODO TODO #@z. +# documentation +# | ```ruby +# | @z = T.let(_, T.untyped) +# | ``` +# ^^^^^^^ reference scip-ruby gem TODO TODO #@z. + end + end + + # Subclass + class K2 < K1 +# ^^ definition scip-ruby gem TODO TODO K2# +# documentation +# | ```ruby +# | class K2 < K1 +# | ``` +# documentation +# | Subclass +# ^^ definition scip-ruby gem TODO TODO K1# +# documentation +# | ```ruby +# | class K1 +# | ``` +# documentation +# | Parent class + # doc comment on class var ooh + @z = 9 +# ^^ definition scip-ruby gem TODO TODO #@z. +# documentation +# | ```ruby +# | @z = T.let(_, T.untyped) +# | ``` +# documentation +# | doc comment on class var ooh + + # overrides K1's p1 + def p1 +# ^^^^^^ definition scip-ruby gem TODO TODO K2#p1(). +# documentation +# | ```ruby +# | sig {returns(T.untyped)} +# | def p1 +# | ``` +# documentation +# | overrides K1's p1 + @x = 20 +# ^^ definition scip-ruby gem TODO TODO K2#@x. +# documentation +# | ```ruby +# | @x = T.let(_, T.untyped) +# | ``` + @@y = 20 +# ^^^ definition scip-ruby gem TODO TODO K2#@@y. +# documentation +# | ```ruby +# | @@y = T.let(_, T.untyped) +# | ``` + @z += @x +# ^^ reference scip-ruby gem TODO TODO K2#@z. +# ^^ reference (write) scip-ruby gem TODO TODO K2#@z. +# ^^^^^^^^ reference scip-ruby gem TODO TODO K2#@z. +# ^^ reference scip-ruby gem TODO TODO K2#@x. + end + end diff --git a/test/scip_test_runner.cc b/test/scip_test_runner.cc index 6ff51b77c5..28db7e7be5 100644 --- a/test/scip_test_runner.cc +++ b/test/scip_test_runner.cc @@ -152,7 +152,11 @@ struct SCIPRange final { } }; -void formatSnapshot(const scip::Document &document, std::ostream &out) { +struct FormatOptions { + bool showDocs; +}; + +void formatSnapshot(const scip::Document &document, FormatOptions options, std::ostream &out) { UnorderedMap symbolTable{}; symbolTable.reserve(document.symbols_size()); for (auto &symbolInfo : document.symbols()) { @@ -197,24 +201,23 @@ void formatSnapshot(const scip::Document &document, std::ostream &out) { ENFORCE(range.start.column < range.end.column, "We shouldn't be emitting empty ranges 🙅"); - out << '#' << string(range.start.column - 1, ' ') << string(range.end.column - range.start.column, '^') - << ' ' << string(isDefinition ? "definition" : "reference") << ' ' << symbolRole - << formatSymbol(occ.symbol()); + auto lineStart = absl::StrCat("#", string(range.start.column - 1, ' ')); + + out << lineStart << string(range.end.column - range.start.column, '^') << ' ' + << string(isDefinition ? "definition" : "reference") << ' ' << symbolRole << formatSymbol(occ.symbol()) + << '\n'; if (!(isDefinition && symbolTable.contains(occ.symbol()))) { - out << '\n'; occ_i++; continue; } auto &symbolInfo = symbolTable[occ.symbol()]; - string prefix = "\n#" + string(range.start.column - 1, ' '); - for (auto &doc : symbolInfo.documentation()) { - out << prefix << "documentation "; - auto iter = std::find(doc.begin(), doc.end(), '\n'); - if (iter != doc.end()) { - // TODO: Use the constructor with two iterators with C++20. - out << string_view(doc.data(), std::distance(doc.begin(), doc.end())); - } else { - out << doc; + if (options.showDocs) { + for (auto &doc : symbolInfo.documentation()) { + out << lineStart << "documentation" << '\n'; + auto docstream = istringstream(doc); + for (string docline; getline(docstream, docline);) { + out << lineStart << "| " << docline << '\n'; + } } } relationships.clear(); @@ -226,7 +229,7 @@ void formatSnapshot(const scip::Document &document, std::ostream &out) { return r1.symbol() < r2.symbol(); }); for (auto &rel : relationships) { - out << prefix << "relation " << formatSymbol(rel.symbol()); + out << lineStart << "relation " << formatSymbol(rel.symbol()); if (rel.is_implementation()) { out << " implementation"; } @@ -236,8 +239,8 @@ void formatSnapshot(const scip::Document &document, std::ostream &out) { if (rel.is_type_definition()) { out << " type_definition"; } + out << '\n'; } - out << '\n'; occ_i++; } } @@ -249,18 +252,18 @@ string snapshot_path(string rb_path) { return rb_path + ".snapshot.rb"; } -void updateSnapshots(const scip::Index &index, const std::filesystem::path &outputDir) { +void updateSnapshots(const scip::Index &index, FormatOptions options, const std::filesystem::path &outputDir) { for (auto &doc : index.documents()) { auto outputFilePath = snapshot_path(doc.relative_path()); ofstream out(outputFilePath); if (!out.is_open()) { FAIL(fmt::format("failed to open snapshot output file at {}", outputFilePath)); } - formatSnapshot(doc, out); + formatSnapshot(doc, options, out); } } -void compareSnapshots(const scip::Index &index, const std::filesystem::path &snapshotDir) { +void compareSnapshots(const scip::Index &index, FormatOptions options, const std::filesystem::path &snapshotDir) { for (auto &doc : index.documents()) { auto filePath = snapshot_path(doc.relative_path()); // TODO: Separate out folders! ifstream inputStream(filePath); @@ -271,7 +274,7 @@ void compareSnapshots(const scip::Index &index, const std::filesystem::path &sna input << inputStream.rdbuf(); ostringstream out; - formatSnapshot(doc, out); + formatSnapshot(doc, options, out); auto result = out.str(); CHECK_EQ_DIFF(input.str(), result, @@ -279,16 +282,25 @@ void compareSnapshots(const scip::Index &index, const std::filesystem::path &sna } } -optional readGemMetadataFromComment(string_view path) { +pair, FormatOptions> readMagicComments(string_view path) { + optional gemMetadata = nullopt; + FormatOptions options{.showDocs = false}; ifstream input(path); for (string line; getline(input, line);) { if (absl::StrContains(line, "# gem-metadata: ")) { auto s = absl::StripPrefix(line, "# gem-metadata: "); ENFORCE(!s.empty()); - return string(s); + gemMetadata = s; + } + if (absl::StrContains(line, "# options: ")) { + auto s = absl::StripPrefix(line, "# options: "); + ENFORCE(!s.empty()); + if (absl::StrContains(s, "showDocs")) { + options.showDocs = true; + } } } - return nullopt; + return {gemMetadata, options}; } TEST_CASE("SCIPTest") { @@ -296,7 +308,7 @@ TEST_CASE("SCIPTest") { ENFORCE(inputs.size() == 1); Expectations test = Expectations::getExpectations(inputs[0]); - optional gemMetadata = readGemMetadataFromComment(inputs[0]); + auto [gemMetadata, formatOptions] = readMagicComments(inputs[0]); vector> errors; auto inputPath = test.folder + test.basename; @@ -391,9 +403,9 @@ TEST_CASE("SCIPTest") { index.ParseFromIstream(&indexFile); if (update) { - updateSnapshots(index, test.folder); + updateSnapshots(index, formatOptions, test.folder); } else { - compareSnapshots(index, test.folder); + compareSnapshots(index, formatOptions, test.folder); } MESSAGE("PASS");