|
24 | 24 | #include "cfg/CFG.h"
|
25 | 25 | #include "common/common.h"
|
26 | 26 | #include "common/sort.h"
|
| 27 | +#include "core/Error.h" |
27 | 28 | #include "core/ErrorQueue.h"
|
28 | 29 | #include "core/Loc.h"
|
29 | 30 | #include "core/SymbolRef.h"
|
@@ -57,6 +58,29 @@ static uint32_t fnv1a_32(const string &s) {
|
57 | 58 |
|
58 | 59 | namespace sorbet::scip_indexer {
|
59 | 60 |
|
| 61 | +constexpr sorbet::core::ErrorClass SCIPRubyDebug{400, sorbet::core::StrictLevel::False}; |
| 62 | + |
| 63 | +void _log_debug(const sorbet::core::GlobalState &gs, sorbet::core::Loc loc, std::string s) { |
| 64 | + if (auto e = gs.beginError(loc, SCIPRubyDebug)) { |
| 65 | + auto lines = absl::StrSplit(s, '\n'); |
| 66 | + for (auto line = lines.begin(); line != lines.end(); line++) { |
| 67 | + auto text = string(line->begin(), line->length()); |
| 68 | + if (line == lines.begin()) { |
| 69 | + e.setHeader("[scip-ruby] {}", text); |
| 70 | + } else { |
| 71 | + e.addErrorNote("{}", text); |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +#ifndef NDEBUG |
| 78 | +#define LOG_DEBUG(__gs, __loc, __s) _log_debug(__gs, __loc, __s) |
| 79 | +#else |
| 80 | +#define LOG_DEBUG(__gs, __s) \ |
| 81 | + {} |
| 82 | +#endif |
| 83 | + |
60 | 84 | // TODO(varun): This is an inline workaround for https://github.com/sorbet/sorbet/issues/5925
|
61 | 85 | // I've not changed the main definition because I didn't bother to rerun the tests with the change.
|
62 | 86 | static bool isTemporary(const core::GlobalState &gs, const core::LocalVariable &var) {
|
@@ -778,6 +802,63 @@ string format_ancestry(const core::GlobalState &gs, core::SymbolRef sym) {
|
778 | 802 | return out.str();
|
779 | 803 | }
|
780 | 804 |
|
| 805 | +static absl::variant</*owner*/ core::ClassOrModuleRef, core::SymbolRef> |
| 806 | +findUnresolvedFieldTransitive(const core::GlobalState &gs, core::Loc loc, core::ClassOrModuleRef start, |
| 807 | + core::NameRef field) { |
| 808 | + auto fieldText = field.shortName(gs); |
| 809 | + auto isInstanceVar = fieldText.size() >= 2 && fieldText[0] == '@' && fieldText[1] != '@'; |
| 810 | + auto isClassInstanceVar = isInstanceVar && start.data(gs)->isSingletonClass(gs); |
| 811 | + // Class instance variables are not inherited, unlike ordinary instance |
| 812 | + // variables or class variables. |
| 813 | + if (isClassInstanceVar) { |
| 814 | + return start; |
| 815 | + } |
| 816 | + auto isClassVar = fieldText.size() >= 2 && fieldText[0] == '@' && fieldText[1] == '@'; |
| 817 | + if (isClassVar && !start.data(gs)->isSingletonClass(gs)) { |
| 818 | + // Triggered when undeclared class variables are accessed from instance methods. |
| 819 | + start = start.data(gs)->lookupSingletonClass(gs); |
| 820 | + } |
| 821 | + |
| 822 | + // TODO(varun): Should we add a cache here? It seems wasteful to redo |
| 823 | + // work for every occurrence. |
| 824 | + if (gs.unresolvedFields.find(start) == gs.unresolvedFields.end() || |
| 825 | + !gs.unresolvedFields.find(start)->second.contains(field)) { |
| 826 | + // Triggered by code patterns like: |
| 827 | + // # top-level |
| 828 | + // def MyClass.method |
| 829 | + // # blah |
| 830 | + // end |
| 831 | + // which is not supported by Sorbet. |
| 832 | + LOG_DEBUG(gs, loc, |
| 833 | + fmt::format("couldn't find field {} in class {};\n" |
| 834 | + "are you using a code pattern like def MyClass.method which is unsupported by Sorbet?", |
| 835 | + field.exists() ? field.toString(gs) : "<non-existent>", |
| 836 | + start.exists() ? start.showFullName(gs) : "<non-existent>")); |
| 837 | + // As a best-effort guess, assume that the definition is |
| 838 | + // in this class but we somehow missed it. |
| 839 | + return start; |
| 840 | + } |
| 841 | + |
| 842 | + auto best = start; |
| 843 | + auto cur = start; |
| 844 | + while (cur.exists()) { |
| 845 | + auto klass = cur.data(gs); |
| 846 | + auto sym = klass->findMember(gs, field); |
| 847 | + if (sym.exists()) { |
| 848 | + return sym; |
| 849 | + } |
| 850 | + auto it = gs.unresolvedFields.find(cur); |
| 851 | + if (it != gs.unresolvedFields.end() && it->second.contains(field)) { |
| 852 | + best = cur; |
| 853 | + } |
| 854 | + if (cur == klass->superClass()) { // FIXME(varun): Handle mix-ins |
| 855 | + break; |
| 856 | + } |
| 857 | + cur = klass->superClass(); |
| 858 | + } |
| 859 | + return best; |
| 860 | +} |
| 861 | + |
781 | 862 | // Loosely inspired by AliasesAndKeywords in IREmitterContext.cc
|
782 | 863 | class AliasMap final {
|
783 | 864 | public:
|
@@ -816,9 +897,27 @@ class AliasMap final {
|
816 | 897 | }
|
817 | 898 | if (sym == core::Symbols::Magic_undeclaredFieldStub()) {
|
818 | 899 | ENFORCE(!bind.loc.empty());
|
819 |
| - this->map.insert( // no trim(...) because undeclared fields shouldn't have :: |
820 |
| - {bind.bind.variable, |
821 |
| - {NamedSymbolRef::undeclaredField(klass, instr->name, bind.bind.type), bind.loc, false}}); |
| 900 | + ENFORCE(klass.isClassOrModule()); |
| 901 | + auto result = findUnresolvedFieldTransitive(ctx, ctx.locAt(bind.loc), klass.asClassOrModuleRef(), |
| 902 | + instr->name); |
| 903 | + if (absl::holds_alternative<core::ClassOrModuleRef>(result)) { |
| 904 | + auto klass = absl::get<core::ClassOrModuleRef>(result); |
| 905 | + if (klass.exists()) { |
| 906 | + this->map.insert( // no trim(...) because undeclared fields shouldn't have :: |
| 907 | + {bind.bind.variable, |
| 908 | + {NamedSymbolRef::undeclaredField(klass, instr->name, bind.bind.type), bind.loc, |
| 909 | + false}}); |
| 910 | + } |
| 911 | + } else if (absl::holds_alternative<core::SymbolRef>(result)) { |
| 912 | + auto fieldSym = absl::get<core::SymbolRef>(result); |
| 913 | + if (fieldSym.exists()) { |
| 914 | + this->map.insert( |
| 915 | + {bind.bind.variable, |
| 916 | + {NamedSymbolRef::declaredField(fieldSym, bind.bind.type), trim(bind.loc), false}}); |
| 917 | + } |
| 918 | + } else { |
| 919 | + ENFORCE(false, "Should've handled all cases of variant earlier"); |
| 920 | + } |
822 | 921 | continue;
|
823 | 922 | }
|
824 | 923 | if (sym.isFieldOrStaticField()) {
|
@@ -1019,9 +1118,18 @@ class CFGTraversal final {
|
1019 | 1118 | } else {
|
1020 | 1119 | uint32_t localId = this->functionLocals[localRef];
|
1021 | 1120 | auto it = this->localDefinitionType.find(localId);
|
1022 |
| - ENFORCE(it != this->localDefinitionType.end(), "file:{}, code:\n{}\naliasMap: {}\n", file.data(gs).path(), |
1023 |
| - core::Loc(file, loc).toString(gs), this->aliasMap.showRaw(gs, file, cfg)); |
1024 |
| - auto overrideType = computeOverrideType(it->second, type); |
| 1121 | + optional<core::TypePtr> overrideType; |
| 1122 | + if (it != this->localDefinitionType.end()) { |
| 1123 | + overrideType = computeOverrideType(it->second, type); |
| 1124 | + } else { |
| 1125 | + LOG_DEBUG( |
| 1126 | + gs, core::Loc(file, loc), |
| 1127 | + fmt::format( |
| 1128 | + "failed to find type info; are you using a code pattern unsupported by Sorbet?\ndebugging " |
| 1129 | + "information: aliasMap: {}", |
| 1130 | + this->aliasMap.showRaw(gs, file, cfg))); |
| 1131 | + overrideType = type; |
| 1132 | + } |
1025 | 1133 | if (isDefinition) {
|
1026 | 1134 | status = this->scipState.saveDefinition(gs, file, OwnedLocal{this->ctx.owner, localId, loc}, type);
|
1027 | 1135 | } else {
|
|
0 commit comments