diff --git a/core/errors/resolver.h b/core/errors/resolver.h index a53793f7abc..eafefd9237a 100644 --- a/core/errors/resolver.h +++ b/core/errors/resolver.h @@ -80,6 +80,7 @@ constexpr ErrorClass BindNonBlockParameter{5071, StrictLevel::False}; constexpr ErrorClass TypeMemberScopeMismatch{5072, StrictLevel::False}; constexpr ErrorClass AbstractClassInstantiated{5073, StrictLevel::True}; constexpr ErrorClass HasAttachedClassIncluded{5074, StrictLevel::False}; +constexpr ErrorClass InvalidDelegatesMissingMethodsTo{5075, StrictLevel::True}; } // namespace sorbet::core::errors::Resolver #endif diff --git a/core/tools/generate_names.cc b/core/tools/generate_names.cc index 82a6dee05a7..cce9f87cc7a 100644 --- a/core/tools/generate_names.cc +++ b/core/tools/generate_names.cc @@ -328,6 +328,9 @@ NameDef names[] = { {"requiredAncestorsLin", ""}, {"requiresAncestor", "requires_ancestor"}, + // Delegate methods + {"delegatesMissingMethodsTo", "delegates_missing_methods_to"}, + // This behaves like the above two names, in the sense that we use a member // on a class to lookup an associated symbol with some extra info. {"sealedSubclasses", "sealed_subclasses"}, diff --git a/resolver/resolver.cc b/resolver/resolver.cc index 9b6f4e2f97f..ea6bbe40fb4 100644 --- a/resolver/resolver.cc +++ b/resolver/resolver.cc @@ -237,12 +237,30 @@ class ResolveConstantsWalk { const RequireAncestorResolutionItem &operator=(const RequireAncestorResolutionItem &) = delete; }; + struct DelegatesMissingMethodsToResolutionItem { + core::FileRef file; + core::ClassOrModuleRef owner; + ast::Send *send; + + DelegatesMissingMethodsToResolutionItem(core::FileRef file, core::ClassOrModuleRef owner, ast::Send *send) + : file(file), owner(owner), send(send) {} + + DelegatesMissingMethodsToResolutionItem(DelegatesMissingMethodsToResolutionItem &&) noexcept = default; + DelegatesMissingMethodsToResolutionItem & + operator=(DelegatesMissingMethodsToResolutionItem &&rhs) noexcept = default; + + DelegatesMissingMethodsToResolutionItem(const DelegatesMissingMethodsToResolutionItem &) = delete; + const DelegatesMissingMethodsToResolutionItem & + operator=(const DelegatesMissingMethodsToResolutionItem &) = delete; + }; + vector todo_; vector todoAncestors_; vector todoClassAliases_; vector todoTypeAliases_; vector todoClassMethods_; vector todoRequiredAncestors_; + vector missingMethodsDelegatees_; static core::SymbolRef resolveLhs(core::Context ctx, const shared_ptr &nesting, core::NameRef name) { Nesting *scope = nesting.get(); @@ -1285,6 +1303,103 @@ class ResolveConstantsWalk { owner.data(gs)->recordRequiredAncestor(gs, symbol, blockLoc); } + static void resolveMissingMethodsDelegateesJob(core::GlobalState &gs, + const DelegatesMissingMethodsToResolutionItem &todo) { + auto owner = todo.owner; + auto send = todo.send; + auto loc = core::Loc(todo.file, send->loc); + + if (owner.data(gs)->flags.isAbstract) { + if (auto e = gs.beginError(loc, core::errors::Resolver::InvalidDelegatesMissingMethodsTo)) { + e.setHeader("`{}` can not be declared inside an abstract class", send->fun.show(gs)); + } + return; + } + + auto *block = send->block(); + + if (send->numPosArgs() > 0) { + if (auto e = gs.beginError(loc, core::errors::Resolver::InvalidDelegatesMissingMethodsTo)) { + e.setHeader("`{}` only accepts a block", send->fun.show(gs)); + e.addErrorNote("Use {} to auto-correct using the new syntax", + "--isolate-error-code 5075 -a --typed true"); + + if (block != nullptr) { + return; + } + + string replacement = ""; + int indent = core::Loc::offset2Pos(todo.file.data(gs), send->loc.beginPos()).column - 1; + int index = 1; + const auto numPosArgs = send->numPosArgs(); + for (auto i = 0; i < numPosArgs; ++i) { + auto &arg = send->getPosArg(i); + auto argLoc = core::Loc(todo.file, arg.loc()); + replacement += fmt::format("{:{}}{} {{ {} }}{}", "", index == 1 ? 0 : indent, send->fun.show(gs), + argLoc.source(gs).value(), index < numPosArgs ? "\n" : ""); + index += 1; + } + e.addAutocorrect( + core::AutocorrectSuggestion{fmt::format("Replace `{}` with `{}`", send->fun.show(gs), replacement), + {core::AutocorrectSuggestion::Edit{loc, replacement}}}); + } + return; + } + + if (block == nullptr) { + return; // The sig mismatch error will be emitted later by infer. + } + + ENFORCE(block->body); + + auto blockLoc = core::Loc(todo.file, block->body.loc()); + core::ClassOrModuleRef symbol = core::Symbols::StubModule(); + + if (auto *constant = ast::cast_tree(block->body)) { + if (constant->symbol.exists() && constant->symbol.isClassOrModule()) { + symbol = constant->symbol.asClassOrModuleRef(); + } + } else if (isTClassOf(block->body)) { + send = ast::cast_tree(block->body); + + ENFORCE(send); + + if (send->numPosArgs() == 1) { + if (auto *argClass = ast::cast_tree(send->getPosArg(0))) { + if (argClass->symbol.exists() && argClass->symbol.isClassOrModule()) { + if (argClass->symbol == owner) { + if (auto e = + gs.beginError(blockLoc, core::errors::Resolver::InvalidDelegatesMissingMethodsTo)) { + e.setHeader("Must not pass yourself to `{}` inside of `delegates_missing_methods_to`", + send->fun.show(gs)); + } + return; + } + + symbol = argClass->symbol.asClassOrModuleRef().data(gs)->lookupSingletonClass(gs); + } + } + } + } + + if (symbol == core::Symbols::StubModule()) { + if (auto e = gs.beginError(blockLoc, core::errors::Resolver::InvalidDelegatesMissingMethodsTo)) { + e.setHeader("Argument to `{}` must be statically resolvable to a class", send->fun.show(gs)); + } + return; + } + + if (symbol == owner) { + if (auto e = gs.beginError(blockLoc, core::errors::Resolver::InvalidDelegatesMissingMethodsTo)) { + e.setHeader("Must not pass yourself to `{}`", send->fun.show(gs)); + } + return; + } + + // STOPPED COPYPASTING HERE + owner.data(gs)->recordRequiredAncestor(gs, symbol, blockLoc); + } + static void tryRegisterSealedSubclass(core::MutableContext ctx, AncestorResolutionItem &job) { ENFORCE(job.ancestor->symbol.exists(), "Ancestor must exist, or we can't check whether it's sealed."); auto ancestorSym = job.ancestor->symbol.dealias(ctx).asClassOrModuleRef(); @@ -1658,6 +1773,11 @@ class ResolveConstantsWalk { if (ctx.state.requiresAncestorEnabled) { this->todoRequiredAncestors_.emplace_back(ctx.file, ctx.owner.asClassOrModuleRef(), &send); } + } else if (send.fun == core::Names::delegatesMissingMethodsTo()) { + // printf("!!!!!!!!!!!!!!!!!!!!!\n"); + // printf("delegatesMissingMethodsTo"); + // printf("!!!!!!!!!!!!!!!!!!!!!\n"); + this->missingMethodsDelegatees_.emplace_back(ctx.file, ctx.owner.asClassOrModuleRef(), &send); } } else { auto recvAsConstantLit = ast::cast_tree(send.recv); @@ -1775,6 +1895,7 @@ class ResolveConstantsWalk { vector> todoTypeAliases; vector> todoClassMethods; vector> todoRequiredAncestors; + vector> todoMissingMethodsDelegatees; { ResolveWalkResult threadResult; @@ -1917,6 +2038,16 @@ class ResolveConstantsWalk { todoRequiredAncestors.clear(); } + { + Timer timeit(gs.tracer(), "resolver.delegates_missing_methods_to"); + for (auto &job : todoMissingMethodsDelegatees) { + for (auto &todo : job.items) { + resolveMissingMethodsDelegateesJob(gs, todo); + } + } + todoRequiredAncestors.clear(); + } + // We can no longer resolve new constants. All the code below reports errors categoryCounterAdd("resolve.constants.nonancestor", "failure", todo.size()); diff --git a/test/testdata/resolver/delegates_missing_methods_to.rb b/test/testdata/resolver/delegates_missing_methods_to.rb new file mode 100644 index 00000000000..d8338ae9adf --- /dev/null +++ b/test/testdata/resolver/delegates_missing_methods_to.rb @@ -0,0 +1,36 @@ +# typed: true + +class Delegatee + def delegated_method; end +end + +class Direct + extend T::Sig + extend T::Helpers + + def method_missing(name) + Delegatee.new.send(name) + end + + delegates_missing_methods_to { Bar } +end + +Direct.new.delegated_method + + +class DelegatingModule + extend T::Sig + extend T::Helpers + + def method_missing(name) + Delegatee.new.send(name) + end + + delegates_missing_methods_to { Bar } +end + +class ViaModule + include DelegatingModule +end + +ViaModule.new.delegated_method