Skip to content

Commit b1e47b0

Browse files
feat: Add support for manually specifiying gems for cross-repo.
1 parent e0cef5a commit b1e47b0

26 files changed

+495
-88
lines changed

core/Error.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ string Error::toString(const GlobalState &gs) const {
124124
stringstream buf;
125125
buf << RESET_STYLE << FILE_POS_STYLE << loc.filePosToString(gs) << RESET_STYLE << ": " << ERROR_COLOR
126126
<< restoreColors(header, ERROR_COLOR) << RESET_COLOR;
127-
if (what.code != 25900) { // SCIPRubyDebug
127+
if (what != scip_indexer::errors::SCIPRubyDebug && what != scip_indexer::errors::SCIPRuby) {
128128
buf << LOW_NOISE_COLOR << " " << gs.errorUrlBase << what.code << RESET_COLOR;
129129
}
130130
if (loc.exists()) {

core/GlobalState.cc

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "core/hashing/hashing.h"
1414
#include "core/lsp/Task.h"
1515
#include "core/lsp/TypecheckEpochManager.h"
16+
#include <filesystem>
1617
#include <utility>
1718

1819
#include "absl/strings/str_cat.h"
@@ -1663,14 +1664,21 @@ FileRef GlobalState::enterFile(const shared_ptr<File> &file) {
16631664
}
16641665

16651666
files.emplace_back(file);
1667+
// GlobalState initialization guarantees the 0 slot will be taken, so this is OK.
16661668
auto ret = FileRef(filesUsed() - 1);
16671669
fileRefByPath[string(file->path())] = ret;
16681670
return ret;
16691671
}
16701672

16711673
FileRef GlobalState::enterFile(string_view path, string_view source) {
1674+
std::string pathBuf;
1675+
if (this->isSCIPRuby) { // See [NOTE: scip-ruby-path-normalization]
1676+
pathBuf = string(std::filesystem::path(path).lexically_normal());
1677+
} else {
1678+
pathBuf = string(path);
1679+
}
16721680
return GlobalState::enterFile(
1673-
make_shared<File>(string(path.begin(), path.end()), string(source.begin(), source.end()), File::Type::Normal));
1681+
make_shared<File>(move(pathBuf), string(source.begin(), source.end()), File::Type::Normal));
16741682
}
16751683

16761684
FileRef GlobalState::enterNewFileAt(const shared_ptr<File> &file, FileRef id) {
@@ -1686,7 +1694,13 @@ FileRef GlobalState::enterNewFileAt(const shared_ptr<File> &file, FileRef id) {
16861694
}
16871695

16881696
FileRef GlobalState::reserveFileRef(string path) {
1689-
return GlobalState::enterFile(make_shared<File>(move(path), "", File::Type::NotYetRead));
1697+
std::string pathBuf;
1698+
if (this->isSCIPRuby) { // See [NOTE: scip-ruby-path-normalization]
1699+
pathBuf = string(std::filesystem::path(path).lexically_normal());
1700+
} else {
1701+
pathBuf = move(path);
1702+
}
1703+
return GlobalState::enterFile(make_shared<File>(move(pathBuf), "", File::Type::NotYetRead));
16901704
}
16911705

16921706
NameRef GlobalState::nextMangledName(ClassOrModuleRef owner, NameRef origName) {
@@ -2155,7 +2169,7 @@ bool GlobalState::shouldReportErrorOn(Loc loc, ErrorClass what) const {
21552169
return false;
21562170
}
21572171
if (this->isSCIPRuby && !this->unsilenceErrors) {
2158-
if (what.code != 25900) { // SCIPRubyDebug
2172+
if (what != scip_indexer::errors::SCIPRubyDebug && what != scip_indexer::errors::SCIPRuby) {
21592173
return false;
21602174
}
21612175
}

core/errors/errors.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
#include "core/errors/packager.h"
1616
#include "core/errors/parser.h"
1717
#include "core/errors/resolver.h"
18+
#include "core/errors/scip_ruby.h"

core/errors/scip_ruby.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#ifndef SORBET_CORE_ERRORS_SCIP_RUBY_H
2+
#define SORBET_CORE_ERRORS_SCIP_RUBY_H
3+
#include "core/Error.h"
4+
5+
namespace sorbet::scip_indexer::errors {
6+
constexpr sorbet::core::ErrorClass SCIPRubyDebug{25900, sorbet::core::StrictLevel::False};
7+
constexpr sorbet::core::ErrorClass SCIPRuby{25901, sorbet::core::StrictLevel::False};
8+
} // namespace sorbet::scip_indexer::errors
9+
10+
#endif // SORBET_CORE_ERRORS_SCIP_RUBY_H

docs/scip-ruby/CLI.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,60 @@ For example, with Git, you can use the last tag
2020
string. For repos which index every commit, you could also use the SHA
2121
instead (`git rev-parse HEAD`).
2222

23+
## `--gem-map-path <arg>`
24+
25+
At the moment, scip-ruby requires an extra step for cross-repo
26+
code navigation; you need to supply information about which.
27+
file belongs to which gem explicitly, using a newline-delimited
28+
JSON file in the following format:
29+
30+
<!--
31+
TODO: Uncomment this
32+
By default, `scip-ruby` will attempt to identify which gems the
33+
ingested files belong to based on the standard layout of paths
34+
as used by Bundler. If it can't identify the gems for certain files,
35+
it will print a warning. This may happen if you're using a custom
36+
build system or different filesystem layout.
37+
38+
To get correct cross-repo code navigation, you can explicitly
39+
supply information about files and gems using a supplementary
40+
newline-delimited JSON file in the following format:
41+
-->
42+
```json
43+
{"path": "a/b/c.rb", "gem": "[email protected]"}
44+
{"path": "a/b/d.rb", "gem": "[email protected]"}
45+
{"path": "a/x/y.rb", "gem": "[email protected]"}
46+
...
47+
```
48+
49+
Then pass the path to the JSON file:
50+
51+
```bash
52+
scip-ruby --gem-map-path path/to/cross-repo-metadata.json
53+
```
54+
55+
Paths are interpreted relative to the working directory
56+
for the `scip-ruby` invocation.
57+
58+
If information about the gem being indexed
59+
cannot be inferred from the filesystem, then you can supply
60+
the `--gem-metadata` argument as described earlier.
61+
62+
If you run into an error message where a path in the JSON file
63+
is not recognized by `scip-ruby`, you can re-run the indexing command
64+
with extra arguments `--log-recorded-filepaths --debug-log-file out.log`
65+
to identify differences between the JSON file
66+
and paths created by traversing directories.
67+
68+
## `--index-file <arg>`
69+
70+
The path for emitting the SCIP index. Defaults to `index.scip`.
71+
2372
## `--unquiet-errors`
2473

2574
scip-ruby defaults to running in Sorbet's quiet mode, as scip-ruby supports
2675
indexing `# typed: false` files on a best-effort basis, but Sorbet may
2776
rightfully flag many errors in those files. The number of errors can be
2877
overwhelming if there is a large amount of untyped code.
2978

30-
This flag restores Sorbet's default behavior.
79+
This flag restores Sorbet's default behavior.

scip_indexer/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ cc_library(
6161
"@com_google_absl//absl/strings",
6262
"@com_google_absl//absl/synchronization",
6363
"@cxxopts",
64+
"@rapidjson",
6465
"@spdlog",
6566
],
6667
)

scip_indexer/Debug.cc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33

44
#include "core/GlobalState.h"
55
#include "core/Loc.h"
6+
#include "core/errors/scip_ruby.h"
67

78
#include "scip_indexer/Debug.h"
89

910
namespace sorbet::scip_indexer {
1011

11-
constexpr sorbet::core::ErrorClass SCIPRubyDebug{25900, sorbet::core::StrictLevel::False};
12-
1312
void _log_debug(const sorbet::core::GlobalState &gs, sorbet::core::Loc loc, std::string s) {
14-
if (auto e = gs.beginError(loc, SCIPRubyDebug)) {
13+
if (auto e = gs.beginError(loc, errors::SCIPRubyDebug)) {
1514
auto lines = absl::StrSplit(s, '\n');
1615
for (auto line = lines.begin(); line != lines.end(); line++) {
1716
auto text = std::string(line->begin(), line->length());

scip_indexer/Debug.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ template <typename V, typename Fn> std::string showVec(const V &v, Fn f) {
5555
return out.str();
5656
}
5757

58-
extern const sorbet::core::ErrorClass SCIPRubyDebug;
59-
6058
void _log_debug(const sorbet::core::GlobalState &gs, sorbet::core::Loc loc, std::string s);
6159
} // namespace sorbet::scip_indexer
6260

scip_indexer/SCIPGemMetadata.cc

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
#include <vector>
99

1010
#include "absl/strings/str_replace.h"
11+
12+
#include "common/FileSystem.h"
13+
#include "common/common.h"
14+
#include "core/FileRef.h"
15+
#include "core/errors/scip_ruby.h"
16+
17+
// Uses ENFORCE as defined by common/common.h, so put this later
18+
#include "rapidjson/document.h"
19+
1120
#include "scip_indexer/SCIPGemMetadata.h"
1221

1322
using namespace std;
@@ -169,4 +178,95 @@ pair<GemMetadata, vector<GemMetadataError>> GemMetadata::readFromConfig(const Fi
169178
return {GemMetadata(name.value_or(currentDirName()), version.value_or("latest")), errors};
170179
}
171180

181+
GemMapping::GemMapping() {
182+
// The 'ruby' namespace is reserved by RubyGems.org, so we won't run into any
183+
// actual gem called 'ruby', so we use that here. 'stdlib' would not be appropriate
184+
// as Sorbet contains RBIs for both core and stdlib modules.
185+
// For the version, we're not really doing any versioning for core & stdlib RBIs,
186+
// (e.g. handling both 2.7 and 3) so let's stick to latest for now.
187+
this->stdlibGem = make_shared<GemMetadata>(GemMetadata::tryParse("ruby@latest").value());
188+
}
189+
190+
optional<shared_ptr<GemMetadata>> GemMapping::lookupGemForFile(const core::GlobalState &gs, core::FileRef file) const {
191+
ENFORCE(file.exists());
192+
auto it = this->map.find(file);
193+
if (it != this->map.end()) {
194+
return it->second;
195+
}
196+
auto filepath = file.data(gs).path();
197+
if (absl::StartsWith(filepath, core::File::URL_PREFIX)) {
198+
return this->stdlibGem;
199+
}
200+
if (this->currentGem.has_value()) {
201+
// TODO Should we enforce here in debug builds?
202+
// Fallback to this if set, to avoid collisions with other gems.
203+
return this->currentGem.value();
204+
}
205+
return nullopt;
206+
}
207+
208+
void GemMapping::populateFromNDJSON(const core::GlobalState &gs, const FileSystem &fs, const std::string &ndjsonPath) {
209+
istringstream input(fs.readFile(ndjsonPath));
210+
auto currentDir = filesystem::path(fs.getCurrentDir());
211+
unsigned errorCount = 0;
212+
for (string line; getline(input, line);) {
213+
auto jsonLine = std::string(absl::StripAsciiWhitespace(line));
214+
if (jsonLine.empty()) {
215+
continue;
216+
}
217+
rapidjson::Document document;
218+
document.Parse(jsonLine);
219+
if (document.HasParseError() || !document.IsObject()) {
220+
continue;
221+
}
222+
if (document.HasMember("path") && document["path"].IsString() && document.HasMember("gem") &&
223+
document["gem"].IsString()) {
224+
auto jsonPath = document["path"].GetString();
225+
auto fileRef = gs.findFileByPath(jsonPath);
226+
if (!fileRef.exists()) {
227+
vector<string> alternatePaths;
228+
auto path = filesystem::path(jsonPath);
229+
// [NOTE: scip-ruby-path-normalization]
230+
// We normalize paths upon entry into GlobalState, so it is sufficient to only check
231+
// different normalized combinations here. One common situation where normalization is
232+
// necessary is if you pass '.' as the directory argument, without normalization,
233+
// the entered paths would be './'-prefixed, but you could reasonably forget to add
234+
// the same in the JSON file.
235+
auto normalizedPath = path.lexically_normal();
236+
if (normalizedPath != path) {
237+
alternatePaths.push_back(normalizedPath);
238+
}
239+
if (normalizedPath.is_absolute()) {
240+
alternatePaths.push_back(string(normalizedPath.lexically_relative(currentDir).lexically_normal()));
241+
} else {
242+
alternatePaths.push_back(string((currentDir / normalizedPath).lexically_normal()));
243+
}
244+
bool foundFile = absl::c_any_of(alternatePaths, [&](auto &altPath) -> bool {
245+
fileRef = gs.findFileByPath(altPath);
246+
return fileRef.exists();
247+
});
248+
if (!foundFile) {
249+
errorCount++;
250+
if (errorCount < 5) {
251+
if (auto e = gs.beginError(core::Loc(), errors::SCIPRuby)) {
252+
e.setHeader("File path in JSON map doesn't match known paths: {}", jsonPath);
253+
e.addErrorNote("Use --debug-log-filepaths to check the paths being recorded by scip-ruby");
254+
}
255+
}
256+
continue;
257+
}
258+
}
259+
auto gemMetadata = GemMetadata::tryParse(document["gem"].GetString());
260+
if (!gemMetadata.has_value()) {
261+
continue;
262+
}
263+
this->map[fileRef] = make_shared<GemMetadata>(move(gemMetadata.value()));
264+
}
265+
}
266+
}
267+
268+
void GemMapping::markCurrentGem(GemMetadata gem) {
269+
this->currentGem = make_shared<GemMetadata>(gem);
270+
}
271+
172272
} // namespace sorbet::scip_indexer

scip_indexer/SCIPGemMetadata.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
#ifndef SORBET_SCIP_GEM_METADATA
22
#define SORBET_SCIP_GEM_METADATA
33

4+
#include <memory>
45
#include <optional>
56
#include <string>
67
#include <utility>
78

89
#include "absl/strings/str_split.h"
910

1011
#include "common/FileSystem.h"
12+
#include "common/common.h"
13+
#include "core/FileRef.h"
14+
#include "core/GlobalState.h"
1115

1216
namespace sorbet::scip_indexer {
1317

@@ -71,6 +75,23 @@ class GemMetadata final {
7175
static std::pair<GemMetadata, std::vector<GemMetadataError>> readFromGemspec(const std::string &);
7276
};
7377

78+
// Type carrying gem information for each file, which is used during
79+
// symbol emission to ensure correct symbol names for cross-repo.
80+
class GemMapping final {
81+
std::shared_ptr<GemMetadata> stdlibGem;
82+
std::optional<std::shared_ptr<GemMetadata>> currentGem;
83+
UnorderedMap<core::FileRef, std::shared_ptr<GemMetadata>> map;
84+
85+
public:
86+
GemMapping();
87+
88+
std::optional<std::shared_ptr<GemMetadata>> lookupGemForFile(const core::GlobalState &gs, core::FileRef file) const;
89+
90+
void populateFromNDJSON(const core::GlobalState &, const FileSystem &fs, const std::string &ndjsonPath);
91+
92+
void markCurrentGem(GemMetadata gem);
93+
};
94+
7495
} // namespace sorbet::scip_indexer
7596

7697
#endif // SORBET_SCIP_GEM_METADATA

0 commit comments

Comments
 (0)