Skip to content

Commit 7fa5c45

Browse files
WIP: Add support for fields in mixins.
1 parent 9f4b36e commit 7fa5c45

File tree

9 files changed

+1676
-108
lines changed

9 files changed

+1676
-108
lines changed

scip_indexer/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ cc_library(
3030
srcs = [
3131
"Debug.cc",
3232
"Debug.h",
33+
"SCIPFieldResolve.cc",
34+
"SCIPFieldResolve.h",
3335
"SCIPIndexer.cc",
3436
"SCIPSymbolRef.cc",
3537
"SCIPSymbolRef.h",

scip_indexer/SCIPFieldResolve.cc

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#include <vector>
2+
3+
#include "absl/strings/ascii.h"
4+
5+
#include "common/common.h"
6+
#include "core/GlobalState.h"
7+
#include "core/SymbolRef.h"
8+
#include "core/Symbols.h"
9+
10+
#include "scip_indexer/Debug.h"
11+
#include "scip_indexer/SCIPFieldResolve.h"
12+
13+
using namespace std;
14+
15+
namespace sorbet::scip_indexer {
16+
17+
string FieldQueryResult::Data::showRaw(const core::GlobalState &gs) const {
18+
switch (this->kind()) {
19+
case Kind::FromDeclared:
20+
return this->originalSymbol().showRaw(gs);
21+
case Kind::FromUndeclared:
22+
return fmt::format("In({})", absl::StripAsciiWhitespace(this->originalClass().showFullName(gs)));
23+
}
24+
}
25+
26+
string FieldQueryResult::showRaw(const core::GlobalState &gs) const {
27+
if (this->mixedIn->empty()) {
28+
return fmt::format("FieldQueryResult(inherited: {})", this->inherited.showRaw(gs));
29+
}
30+
return fmt::format("FieldQueryResult(inherited: {}, mixins: {})", this->inherited.showRaw(gs),
31+
showVec(*this->mixedIn.get(), [&gs](const auto &mixin) -> string { return mixin.showRaw(gs); }));
32+
}
33+
34+
void FieldResolver::resetMixins() {
35+
this->mixinQueue.clear();
36+
}
37+
38+
// Compute all transitively included modules which mention the field being queried.
39+
//
40+
// If an include chain for a field looks like class C.@f <- module M2.@f <- module M1.@f,
41+
// both M1 and M2 will be included in the results (this avoids any kind of postprocessing
42+
// of a transitive closure of relationships at the cost of a larger index).
43+
void FieldResolver::findUnresolvedFieldInMixinsTransitive(const core::GlobalState &gs, FieldQuery query,
44+
vector<FieldQueryResult::Data> &out) {
45+
this->mixinQueue.clear();
46+
for (auto mixin : query.start.data(gs)->mixins()) {
47+
this->mixinQueue.push_back(mixin);
48+
}
49+
auto field = query.field;
50+
using Data = FieldQueryResult::Data;
51+
while (auto m = this->mixinQueue.try_pop_front()) {
52+
auto mixin = m.value();
53+
auto sym = mixin.data(gs)->findMember(gs, field);
54+
if (sym.exists()) {
55+
out.push_back(Data(sym));
56+
continue;
57+
}
58+
auto it = gs.unresolvedFields.find(mixin);
59+
if (it != gs.unresolvedFields.end() && it->second.contains(field)) {
60+
out.push_back(Data(mixin));
61+
}
62+
}
63+
}
64+
65+
FieldQueryResult::Data FieldResolver::findUnresolvedFieldInInheritanceChain(const core::GlobalState &gs, core::Loc loc,
66+
FieldQuery query) {
67+
auto start = query.start;
68+
auto field = query.field;
69+
70+
auto fieldText = query.field.shortName(gs);
71+
auto isInstanceVar = fieldText.size() >= 2 && fieldText[0] == '@' && fieldText[1] != '@';
72+
auto isClassInstanceVar = isInstanceVar && start.data(gs)->isSingletonClass(gs);
73+
// Class instance variables are not inherited, unlike ordinary instance
74+
// variables or class variables.
75+
if (isClassInstanceVar) {
76+
return FieldQueryResult::Data(start);
77+
}
78+
auto isClassVar = fieldText.size() >= 2 && fieldText[0] == '@' && fieldText[1] == '@';
79+
if (isClassVar && !start.data(gs)->isSingletonClass(gs)) {
80+
// Triggered when undeclared class variables are accessed from instance methods.
81+
start = start.data(gs)->lookupSingletonClass(gs);
82+
}
83+
84+
if (gs.unresolvedFields.find(start) == gs.unresolvedFields.end() ||
85+
!gs.unresolvedFields.find(start)->second.contains(field)) {
86+
// Triggered by code patterns like:
87+
// # top-level
88+
// def MyClass.method
89+
// # blah
90+
// end
91+
// which is not supported by Sorbet.
92+
LOG_DEBUG(gs, loc,
93+
fmt::format("couldn't find field {} in class {};\n"
94+
"are you using a code pattern like def MyClass.method which is unsupported by Sorbet?",
95+
field.exists() ? field.toString(gs) : "<non-existent>",
96+
start.exists() ? start.showFullName(gs) : "<non-existent>"));
97+
// As a best-effort guess, assume that the definition is
98+
// in this class but we somehow missed it.
99+
return FieldQueryResult::Data(start);
100+
}
101+
102+
auto best = start;
103+
auto cur = start;
104+
while (cur.exists()) {
105+
auto klass = cur.data(gs);
106+
auto sym = klass->findMember(gs, field);
107+
if (sym.exists()) { // TODO(varun): Is this early exit justified?
108+
// Maybe it is possible to hit this in multiple ancestors?
109+
return FieldQueryResult::Data(sym);
110+
}
111+
auto it = gs.unresolvedFields.find(cur);
112+
if (it != gs.unresolvedFields.end() && it->second.contains(field)) {
113+
best = cur;
114+
}
115+
116+
if (cur == klass->superClass()) { // FIXME(varun): Handle mix-ins
117+
break;
118+
}
119+
cur = klass->superClass();
120+
}
121+
return FieldQueryResult::Data(best);
122+
}
123+
124+
pair<FieldQueryResult, bool> FieldResolver::findUnresolvedFieldTransitive(const core::GlobalState &gs, core::Loc loc,
125+
FieldQuery query) {
126+
auto cacheIt = this->cache.find(query);
127+
if (cacheIt != this->cache.end()) {
128+
return {cacheIt->second, true};
129+
}
130+
auto inherited = this->findUnresolvedFieldInInheritanceChain(gs, loc, query);
131+
using Data = FieldQueryResult::Data;
132+
vector<Data> mixins;
133+
findUnresolvedFieldInMixinsTransitive(gs, query, mixins);
134+
auto [it, inserted] =
135+
this->cache.insert({query, FieldQueryResult{inherited, make_shared<vector<Data>>(move(mixins))}});
136+
ENFORCE(inserted);
137+
return {it->second, false};
138+
}
139+
140+
} // namespace sorbet::scip_indexer

scip_indexer/SCIPFieldResolve.h

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
2+
#ifndef SORBET_SCIP_FIELD_RESOLVE
3+
#define SORBET_SCIP_FIELD_RESOLVE
4+
5+
#include <memory>
6+
#include <optional>
7+
#include <vector>
8+
9+
#include "core/NameRef.h"
10+
#include "core/SymbolRef.h"
11+
12+
namespace sorbet::scip_indexer {
13+
14+
struct FieldQuery final {
15+
sorbet::core::ClassOrModuleRef start;
16+
sorbet::core::NameRef field;
17+
18+
bool operator==(const FieldQuery &other) const noexcept {
19+
return this->start == other.start && this->field == other.field;
20+
}
21+
};
22+
23+
template <typename H> H AbslHashValue(H h, const FieldQuery &q) {
24+
return H::combine(std::move(h), q.start, q.field);
25+
}
26+
27+
struct FieldQueryResult final {
28+
enum class Kind : bool {
29+
FromDeclared,
30+
FromUndeclared,
31+
};
32+
33+
class Data {
34+
union Storage {
35+
core::ClassOrModuleRef owner;
36+
core::SymbolRef symbol;
37+
Storage() {
38+
memset(this, 0, sizeof(Storage));
39+
}
40+
} storage;
41+
Kind _kind;
42+
43+
public:
44+
Data(Data &&) = default;
45+
Data(const Data &) = default;
46+
Kind kind() const {
47+
return this->_kind;
48+
}
49+
core::ClassOrModuleRef originalClass() const {
50+
ENFORCE(this->kind() == Kind::FromUndeclared);
51+
return this->storage.owner;
52+
}
53+
core::SymbolRef originalSymbol() const {
54+
ENFORCE(this->kind() == Kind::FromUndeclared);
55+
return this->storage.symbol;
56+
}
57+
Data(core::ClassOrModuleRef klass) : _kind(Kind::FromUndeclared) {
58+
this->storage.owner = klass;
59+
}
60+
Data(core::SymbolRef sym) : _kind(Kind::FromDeclared) {
61+
this->storage.symbol = sym;
62+
}
63+
64+
std::string showRaw(const core::GlobalState &) const;
65+
};
66+
67+
Data inherited;
68+
std::shared_ptr<std::vector<Data>> mixedIn;
69+
70+
std::string showRaw(const core::GlobalState &gs) const;
71+
};
72+
73+
// Non-shrinking queue for cheap-to-copy types.
74+
template <typename T> class BasicQueue final {
75+
std::vector<T> storage;
76+
size_t current;
77+
78+
public:
79+
BasicQueue() = default;
80+
BasicQueue(BasicQueue &&) = default;
81+
BasicQueue &operator=(BasicQueue &&) = default;
82+
BasicQueue(const BasicQueue &) = delete;
83+
BasicQueue &operator=(const BasicQueue &) = delete;
84+
85+
void clear() {
86+
this->storage.clear();
87+
this->current = 0;
88+
}
89+
void push_back(T val) {
90+
this->storage.push_back(val);
91+
}
92+
std::optional<T> try_pop_front() {
93+
if (this->current >= this->storage.size()) {
94+
return {};
95+
}
96+
auto ret = this->storage[this->current];
97+
this->current++;
98+
return ret;
99+
}
100+
};
101+
102+
class FieldResolver final {
103+
sorbet::UnorderedMap<FieldQuery, FieldQueryResult> cache;
104+
BasicQueue<sorbet::core::ClassOrModuleRef> mixinQueue;
105+
106+
public:
107+
std::pair<FieldQueryResult, /*cacheHit*/ bool> findUnresolvedFieldTransitive(const core::GlobalState &gs,
108+
core::Loc loc, FieldQuery query);
109+
110+
private:
111+
void resetMixins();
112+
113+
void findUnresolvedFieldInMixinsTransitive(const sorbet::core::GlobalState &gs, FieldQuery query,
114+
std::vector<FieldQueryResult::Data> &out);
115+
116+
FieldQueryResult::Data findUnresolvedFieldInInheritanceChain(const core::GlobalState &gs, core::Loc loc,
117+
FieldQuery query);
118+
};
119+
120+
} // namespace sorbet::scip_indexer
121+
122+
#endif // SORBET_SCIP_FIELD_RESOLVE

0 commit comments

Comments
 (0)