diff --git a/scip_indexer/SCIPIndexer.cc b/scip_indexer/SCIPIndexer.cc index 0b402bfc34..c93d27cef1 100644 --- a/scip_indexer/SCIPIndexer.cc +++ b/scip_indexer/SCIPIndexer.cc @@ -13,6 +13,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" #include "absl/synchronization/mutex.h" #include "spdlog/fmt/fmt.h" @@ -127,6 +128,32 @@ struct OwnedLocal { } }; +class GemMetadata final { + string _name; + string _version; + + GemMetadata(string name, string version) : _name(name), _version(version) {} + +public: + GemMetadata &operator=(const GemMetadata &) = default; + + static GemMetadata tryParseOrDefault(string metadata) { + vector v = absl::StrSplit(metadata, '@'); + if (v.size() != 2 || v[0].empty() || v[1].empty()) { + return GemMetadata{"TODO", "TODO"}; + } + return GemMetadata{v[0], v[1]}; + } + + const std::string &name() const { + return this->_name; + } + + const std::string &version() const { + return this->_version; + } +}; + // A wrapper type to handle both top-level symbols (like classes) as well as // "inner symbols" like fields (@x). In a statically typed language, field // symbols are like any other symbols, but in Ruby, they aren't declared @@ -212,12 +239,12 @@ class NamedSymbolRef final { } // Returns OK if we were able to compute a symbol for the expression. - absl::Status symbolForExpr(const core::GlobalState &gs, scip::Symbol &symbol) const { + absl::Status symbolForExpr(const core::GlobalState &gs, const GemMetadata &metadata, scip::Symbol &symbol) const { // Don't set symbol.scheme and package.manager here because those are hard-coded to 'scip-ruby' and 'gem' // anyways. scip::Package package; - package.set_name("TODO"); - package.set_version("TODO"); + package.set_name(metadata.name()); + package.set_version(metadata.version()); *symbol.mutable_package() = move(package); InlinedVector descriptors; @@ -319,6 +346,7 @@ core::Loc trimColonColonPrefix(const core::GlobalState &gs, core::Loc baseLoc) { class SCIPState { string symbolScratchBuffer; UnorderedMap symbolStringCache; + GemMetadata gemMetadata; public: UnorderedMap> occurrenceMap; @@ -337,7 +365,7 @@ class SCIPState { vector externalSymbols; public: - SCIPState() = default; + SCIPState(GemMetadata metadata) : symbolScratchBuffer(), symbolStringCache(), gemMetadata(metadata) {} ~SCIPState() = default; SCIPState(SCIPState &&) = default; SCIPState &operator=(SCIPState &&other) = default; @@ -362,7 +390,7 @@ class SCIPState { status = scip::utils::emitSymbolString(*symbol, this->symbolScratchBuffer); } else { scip::Symbol symbol; - status = symRef.symbolForExpr(gs, symbol); + status = symRef.symbolForExpr(gs, this->gemMetadata, symbol); if (!status.ok()) { return status; } @@ -459,7 +487,7 @@ 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, symbol); + auto status = symRef.symbolForExpr(gs, this->gemMetadata, symbol); if (!status.ok()) { return status; } @@ -954,6 +982,7 @@ using LocalSymbolTable = UnorderedMap; class SCIPSemanticExtension : public SemanticExtension { public: string indexFilePath; + scip_indexer::GemMetadata gemMetadata; using SCIPState = sorbet::scip_indexer::SCIPState; @@ -977,7 +1006,7 @@ class SCIPSemanticExtension : public SemanticExtension { // // We will move the state out later, so use a no-op deleter. return mutableState.states[this_thread::get_id()] = - shared_ptr(new SCIPState(), [](SCIPState *) {}); + shared_ptr(new SCIPState(gemMetadata), [](SCIPState *) {}); } } @@ -985,14 +1014,27 @@ class SCIPSemanticExtension : public SemanticExtension { auto classLoc = core::Loc(file, cd->name.loc()); } + bool doNothing() const { + return this->indexFilePath.empty(); + } + void run(core::MutableContext &ctx, ast::ClassDef *cd) const override { + if (this->doNothing()) { + return; + } // FIXME:(varun) This is a no-op??? emitSymbol(ctx.state, ctx.file, cd); }; virtual void finishTypecheckFile(const core::GlobalState &gs, const core::FileRef &file) const override { + if (this->doNothing()) { + return; + } getSCIPState()->saveDocument(gs, file); }; virtual void finishTypecheck(const core::GlobalState &gs) const override { + if (this->doNothing()) { + return; + } scip::ToolInfo toolInfo; toolInfo.set_name("scip-ruby"); toolInfo.set_version(sorbet_version); @@ -1045,6 +1087,9 @@ class SCIPSemanticExtension : public SemanticExtension { virtual void typecheck(const core::GlobalState &gs, core::FileRef file, cfg::CFG &cfg, ast::MethodDef &methodDef) const override { + if (this->doNothing()) { + return; + } auto scipState = this->getSCIPState(); if (methodDef.name != core::Names::staticInit()) { auto status = scipState->saveDefinition(gs, file, scip_indexer::NamedSymbolRef::method(methodDef.symbol)); @@ -1067,11 +1112,12 @@ class SCIPSemanticExtension : public SemanticExtension { } virtual unique_ptr deepCopy(const core::GlobalState &from, core::GlobalState &to) override { - return make_unique(this->indexFilePath); + return make_unique(this->indexFilePath, this->gemMetadata); }; virtual void merge(const core::GlobalState &from, core::GlobalState &to, core::NameSubstitution &subst) override{}; - SCIPSemanticExtension(string indexFilePath) : indexFilePath(indexFilePath), mutableState() {} + SCIPSemanticExtension(string indexFilePath, scip_indexer::GemMetadata metadata) + : indexFilePath(indexFilePath), gemMetadata(metadata), mutableState() {} ~SCIPSemanticExtension() {} }; @@ -1080,15 +1126,21 @@ class SCIPSemanticExtensionProvider : public SemanticExtensionProvider { void injectOptions(cxxopts::Options &optsBuilder) const override { optsBuilder.add_options("indexer")("index-file", "Output SCIP index to a directory, which must already exist", cxxopts::value()); + optsBuilder.add_options("name@version")( + "gem-metadata", "Name and version pair to be used for cross-repository code navigation.", + cxxopts::value()); }; unique_ptr readOptions(cxxopts::ParseResult &providedOptions) const override { if (providedOptions.count("index-file") > 0) { - return make_unique(providedOptions["index-file"].as()); + return make_unique( + providedOptions["index-file"].as(), + scip_indexer::GemMetadata::tryParseOrDefault( + providedOptions.count("gem-metadata") > 0 ? providedOptions["gem-metadata"].as() : "")); } return this->defaultInstance(); }; virtual unique_ptr defaultInstance() const override { - return make_unique("index.scip"); + return make_unique("", scip_indexer::GemMetadata::tryParseOrDefault("")); }; static vector getProviders(); virtual ~SCIPSemanticExtensionProvider() = default; diff --git a/test/scip/testdata/gem_metadata.rb b/test/scip/testdata/gem_metadata.rb new file mode 100644 index 0000000000..6c4c862645 --- /dev/null +++ b/test/scip/testdata/gem_metadata.rb @@ -0,0 +1,11 @@ +# typed: true +# gem-metadata: leet@1.3.3.7 + +class C + def m + n + end + def n + m + end +end diff --git a/test/scip/testdata/gem_metadata.snapshot.rb b/test/scip/testdata/gem_metadata.snapshot.rb new file mode 100644 index 0000000000..ea6188dab0 --- /dev/null +++ b/test/scip/testdata/gem_metadata.snapshot.rb @@ -0,0 +1,16 @@ + # typed: true + # gem-metadata: leet@1.3.3.7 + + class C +# ^ definition scip-ruby gem leet 1.3.3.7 C# + def m +# ^^^^^ definition scip-ruby gem leet 1.3.3.7 C#m(). + n +# ^ reference scip-ruby gem leet 1.3.3.7 C#n(). + end + def n +# ^^^^^ definition scip-ruby gem leet 1.3.3.7 C#n(). + m +# ^ reference scip-ruby gem leet 1.3.3.7 C#m(). + end + end diff --git a/test/scip_test_runner.cc b/test/scip_test_runner.cc index 31099f374d..e23ca1f12c 100644 --- a/test/scip_test_runner.cc +++ b/test/scip_test_runner.cc @@ -15,6 +15,7 @@ #include "absl/strings/match.h" #include "absl/strings/str_replace.h" +#include "absl/strings/str_split.h" #include "spdlog/sinks/stdout_color_sinks.h" #include "ast/ast.h" @@ -278,11 +279,25 @@ void compareSnapshots(const scip::Index &index, const std::filesystem::path &sna } } +optional readGemMetadataFromComment(string_view path) { + 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); + } + } + return nullopt; +} + TEST_CASE("SCIPTest") { // FIXME(varun): Add support for multifile tests. ENFORCE(inputs.size() == 1); Expectations test = Expectations::getExpectations(inputs[0]); + optional gemMetadata = readGemMetadataFromComment(inputs[0]); + vector> errors; auto inputPath = test.folder + test.basename; @@ -315,8 +330,14 @@ TEST_CASE("SCIPTest") { cxxopts::Options options{"scip-ruby-snapshot-test"}; scipProvider->injectOptions(options); - std::vector argv = {"scip-ruby-snapshot-test", "--index-file", indexFilePath.c_str(), nullptr}; - auto parseResult = options.parse(3, argv.data()); + std::vector argv = {"scip-ruby-snapshot-test", "--index-file", indexFilePath.c_str()}; + if (gemMetadata.has_value()) { + argv.push_back("--gem-metadata"); + ENFORCE(!gemMetadata.value().empty()); + argv.push_back(gemMetadata.value().data()); + } + argv.push_back(nullptr); + auto parseResult = options.parse(argv.size() - 1, argv.data()); gs.semanticExtensions.push_back(scipProvider->readOptions(parseResult)); {