Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements delegates_missing_methods_to #19

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/errors/resolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions core/tools/generate_names.cc
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ NameDef names[] = {
{"requiredAncestorsLin", "<required-ancestor-lin>"},
{"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"},
Expand Down
131 changes: 131 additions & 0 deletions resolver/resolver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConstantResolutionItem> todo_;
vector<AncestorResolutionItem> todoAncestors_;
vector<ClassAliasResolutionItem> todoClassAliases_;
vector<TypeAliasResolutionItem> todoTypeAliases_;
vector<ClassMethodsResolutionItem> todoClassMethods_;
vector<RequireAncestorResolutionItem> todoRequiredAncestors_;
vector<DelegatesMissingMethodsToResolutionItem> missingMethodsDelegatees_;

static core::SymbolRef resolveLhs(core::Context ctx, const shared_ptr<Nesting> &nesting, core::NameRef name) {
Nesting *scope = nesting.get();
Expand Down Expand Up @@ -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<ast::ConstantLit>(block->body)) {
if (constant->symbol.exists() && constant->symbol.isClassOrModule()) {
symbol = constant->symbol.asClassOrModuleRef();
}
} else if (isTClassOf(block->body)) {
send = ast::cast_tree<ast::Send>(block->body);

ENFORCE(send);

if (send->numPosArgs() == 1) {
if (auto *argClass = ast::cast_tree<ast::ConstantLit>(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();
Expand Down Expand Up @@ -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<ast::ConstantLit>(send.recv);
Expand Down Expand Up @@ -1775,6 +1895,7 @@ class ResolveConstantsWalk {
vector<ResolveItems<TypeAliasResolutionItem>> todoTypeAliases;
vector<ResolveItems<ClassMethodsResolutionItem>> todoClassMethods;
vector<ResolveItems<RequireAncestorResolutionItem>> todoRequiredAncestors;
vector<ResolveItems<DelegatesMissingMethodsToResolutionItem>> todoMissingMethodsDelegatees;

{
ResolveWalkResult threadResult;
Expand Down Expand Up @@ -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());
Expand Down
36 changes: 36 additions & 0 deletions test/testdata/resolver/delegates_missing_methods_to.rb
Original file line number Diff line number Diff line change
@@ -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