From 32735644dd1ca0030ad9975597424e9b16aa18b9 Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Fri, 18 Feb 2022 19:04:11 -0500 Subject: [PATCH] Accept `T.self_type` and `T.attached_class` for proc binding (#4829) * Allow binding blocks to attached_class and self_type * Add test case for trying to bind a proc's parameter * Look up attached class member on the singleton class * Prevent proc bind from being used in non-block arguments * Add more test cases and handle method level bind to attached_class --- core/GlobalState.cc | 7 ++ core/SymbolRef.h | 12 ++- core/errors/resolver.h | 1 + core/tools/generate_names.cc | 4 + infer/environment.cc | 21 +++++- resolver/resolver.cc | 22 +++++- resolver/type_syntax.cc | 24 +++++- test/testdata/resolver/bad_generic_bind.rb | 23 ++++++ test/testdata/resolver/bind.rb | 14 ++++ test/testdata/resolver/bind_attached_class.rb | 48 ++++++++++++ test/testdata/resolver/bind_self_type.rb | 75 +++++++++++++++++++ 11 files changed, 241 insertions(+), 10 deletions(-) create mode 100644 test/testdata/resolver/bad_generic_bind.rb create mode 100644 test/testdata/resolver/bind_attached_class.rb create mode 100644 test/testdata/resolver/bind_self_type.rb diff --git a/core/GlobalState.cc b/core/GlobalState.cc index c391532de01..b3c3a0c6438 100644 --- a/core/GlobalState.cc +++ b/core/GlobalState.cc @@ -490,6 +490,13 @@ void GlobalState::initEmpty() { klass = Symbols::T_Private_Compiler().data(*this)->singletonClass(*this); ENFORCE(klass == Symbols::T_Private_CompilerSingleton()); + // Magic classes for special proc bindings + klass = synthesizeClass(core::Names::Constants::BindToAttachedClass()); + ENFORCE(klass == Symbols::BindToAttachedClass()); + + klass = synthesizeClass(core::Names::Constants::BindToSelfType()); + ENFORCE(klass == Symbols::BindToSelfType()); + typeArgument = enterTypeArgument(Loc::none(), Symbols::noMethod(), Names::Constants::TodoTypeArgument(), Variance::CoVariant); ENFORCE(typeArgument == Symbols::todoTypeArgument()); diff --git a/core/SymbolRef.h b/core/SymbolRef.h index 9827a4abae5..b87bf71d0bf 100644 --- a/core/SymbolRef.h +++ b/core/SymbolRef.h @@ -970,6 +970,14 @@ class Symbols { return ClassOrModuleRef::fromRaw(88); } + static ClassOrModuleRef BindToAttachedClass() { + return ClassOrModuleRef::fromRaw(89); + } + + static ClassOrModuleRef BindToSelfType() { + return ClassOrModuleRef::fromRaw(90); + } + static constexpr int MAX_PROC_ARITY = 10; static ClassOrModuleRef Proc0() { return ClassOrModuleRef::fromRaw(MAX_SYNTHETIC_CLASS_SYMBOLS - MAX_PROC_ARITY * 2 - 2); @@ -992,11 +1000,11 @@ class Symbols { return ClassOrModuleRef::fromRaw(MAX_SYNTHETIC_CLASS_SYMBOLS - 1); } - static constexpr int MAX_SYNTHETIC_CLASS_SYMBOLS = 203; + static constexpr int MAX_SYNTHETIC_CLASS_SYMBOLS = 205; static constexpr int MAX_SYNTHETIC_METHOD_SYMBOLS = 45; static constexpr int MAX_SYNTHETIC_FIELD_SYMBOLS = 4; static constexpr int MAX_SYNTHETIC_TYPEARGUMENT_SYMBOLS = 4; - static constexpr int MAX_SYNTHETIC_TYPEMEMBER_SYMBOLS = 100; + static constexpr int MAX_SYNTHETIC_TYPEMEMBER_SYMBOLS = 102; }; template H AbslHashValue(H h, const SymbolRef &m) { diff --git a/core/errors/resolver.h b/core/errors/resolver.h index f904a0a8f22..a5d124ade01 100644 --- a/core/errors/resolver.h +++ b/core/errors/resolver.h @@ -76,6 +76,7 @@ constexpr ErrorClass NonClassSuperclass{5067, StrictLevel::False}; constexpr ErrorClass AmbiguousDefinitionError{5068, StrictLevel::False}; constexpr ErrorClass MultipleStatementsInSig{5069, StrictLevel::False}; constexpr ErrorClass NilableUntyped{5070, StrictLevel::False}; +constexpr ErrorClass BindNonBlockParameter{5071, StrictLevel::False}; } // namespace sorbet::core::errors::Resolver #endif diff --git a/core/tools/generate_names.cc b/core/tools/generate_names.cc index ab0ee3a3caa..93a1ef971f8 100644 --- a/core/tools/generate_names.cc +++ b/core/tools/generate_names.cc @@ -477,6 +477,10 @@ NameDef names[] = { {"Rational", "Rational", true}, // A magic non user-creatable class with methods to keep state between passes {"Magic", "", true}, + // A magic non user-creatable class for binding procs to attached_class + {"BindToAttachedClass", "", true}, + // A magic non user-creatable class for binding procs to self_type + {"BindToSelfType", "", true}, // A magic non user-creatable class for mimicing the decl builder during cfg // construction {"DeclBuilderForProcs", "", true}, diff --git a/infer/environment.cc b/infer/environment.cc index 5bcf72381f0..989709de0af 100644 --- a/infer/environment.cc +++ b/infer/environment.cc @@ -1364,10 +1364,25 @@ core::TypePtr Environment::processBinding(core::Context ctx, const cfg::CFG &inW }, [&](cfg::LoadSelf &l) { ENFORCE(l.link); - if (l.link->result->main.blockSpec.rebind.exists()) { - tp.type = l.link->result->main.blockSpec.rebind.data(ctx)->externalType(); - tp.origins.emplace_back(core::Loc(ctx.file, bind.loc)); + auto rebind = l.link->result->main.blockSpec.rebind; + + if (rebind.exists()) { + if (rebind == core::Symbols::BindToSelfType()) { + tp.type = l.link->result->main.receiver; + } else if (rebind == core::Symbols::BindToAttachedClass()) { + auto appliedType = core::cast_type(l.link->result->main.receiver); + auto attachedClass = + appliedType->klass.data(ctx)->findMember(ctx, core::Names::Constants::AttachedClass()); + auto lambdaParam = + core::cast_type(attachedClass.asTypeMemberRef().data(ctx)->resultType); + + tp.type = lambdaParam->upperBound; + } else { + tp.type = rebind.data(ctx)->externalType(); + } + + tp.origins.emplace_back(core::Loc(ctx.file, bind.loc)); } else { tp = getTypeAndOrigin(ctx, l.fallback); } diff --git a/resolver/resolver.cc b/resolver/resolver.cc index 99ea91b027c..c0f45cb93a0 100644 --- a/resolver/resolver.cc +++ b/resolver/resolver.cc @@ -2897,7 +2897,14 @@ class ResolveSignaturesWalk { method.data(ctx)->flags.isFinal = true; } if (sig.seen.bind) { - method.data(ctx)->rebind = sig.bind; + if (sig.bind == core::Symbols::BindToAttachedClass()) { + if (auto e = ctx.beginError(exprLoc, core::errors::Resolver::BindNonBlockParameter)) { + e.setHeader("Using `{}` is not permitted here", "bind"); + e.addErrorNote("Only block arguments can use `{}`", "bind"); + } + } else { + method.data(ctx)->rebind = sig.bind; + } } auto methodInfo = method.data(ctx); @@ -2952,9 +2959,21 @@ class ResolveSignaturesWalk { defParams.push_back(local); auto spec = absl::c_find_if(sig.argTypes, [&](const auto &spec) { return spec.name == treeArgName; }); + bool isBlkArg = arg.name == core::Names::blkArg(); if (spec != sig.argTypes.end()) { ENFORCE(spec->type != nullptr); + + // It would be nice to remove the restriction on more than these two specific binds, but that would + // raise a lot more errors + if (!isBlkArg && (spec->rebind == core::Symbols::BindToAttachedClass() || + spec->rebind == core::Symbols::BindToSelfType())) { + if (auto e = ctx.state.beginError(spec->loc, core::errors::Resolver::BindNonBlockParameter)) { + e.setHeader("Using `{}` is not permitted here", "bind"); + e.addErrorNote("Only block arguments can use `{}`", "bind"); + } + } + arg.type = std::move(spec->type); arg.loc = spec->loc; arg.rebind = spec->rebind; @@ -2965,7 +2984,6 @@ class ResolveSignaturesWalk { } // We silence the "type not specified" error when a sig does not mention the synthesized block arg. - bool isBlkArg = arg.name == core::Names::blkArg(); if (!isOverloaded && !isBlkArg && (sig.seen.params || sig.seen.returns || sig.seen.void_)) { // Only error if we have any types if (auto e = ctx.state.beginError(arg.loc, core::errors::Resolver::InvalidMethodSignature)) { diff --git a/resolver/type_syntax.cc b/resolver/type_syntax.cc index d34223ecf02..8e5de2ea668 100644 --- a/resolver/type_syntax.cc +++ b/resolver/type_syntax.cc @@ -168,8 +168,10 @@ ParsedSig parseSigWithSelfTypeParams(core::Context ctx, const ast::Send &sigSend sig.origSend = const_cast(&sigSend); const ast::Send *send = nullptr; + bool isProc = false; if (isTProc(ctx, &sigSend)) { send = &sigSend; + isProc = true; } else { sig.seen.sig = true; ENFORCE(sigSend.fun == core::Names::sig()); @@ -290,6 +292,14 @@ ParsedSig parseSigWithSelfTypeParams(core::Context ctx, const ast::Send &sigSend sig.bind = appType->klass; validBind = true; } + } else if (auto arg = ast::cast_tree(send->getPosArg(0))) { + if (arg->fun == core::Names::selfType()) { + sig.bind = core::Symbols::BindToSelfType(); + validBind = true; + } else if (arg->fun == core::Names::attachedClass()) { + sig.bind = core::Symbols::BindToAttachedClass(); + validBind = true; + } } if (!validBind) { @@ -361,8 +371,16 @@ ParsedSig parseSigWithSelfTypeParams(core::Context ctx, const ast::Send &sigSend auto *lit = ast::cast_tree(key); if (lit && lit->isSymbol(ctx)) { core::NameRef name = lit->asSymbol(ctx); - auto resultAndBind = - getResultTypeAndBindWithSelfTypeParams(ctx, value, *parent, args.withRebind()); + TypeSyntax::ResultType resultAndBind; + + if (isProc) { + resultAndBind = + getResultTypeAndBindWithSelfTypeParams(ctx, value, *parent, args.withoutRebind()); + } else { + resultAndBind = + getResultTypeAndBindWithSelfTypeParams(ctx, value, *parent, args.withRebind()); + } + sig.argTypes.emplace_back(ParsedSig::ArgSpec{core::Loc(ctx.file, key.loc()), name, resultAndBind.type, resultAndBind.rebind}); } @@ -991,7 +1009,7 @@ TypeSyntax::ResultType getResultTypeAndBindWithSelfTypeParams(core::Context ctx, }, [&](const ast::Send &s) { if (isTProc(ctx, &s)) { - auto sig = parseSigWithSelfTypeParams(ctx, s, &sigBeingParsed, args.withoutSelfType()); + auto sig = parseSigWithSelfTypeParams(ctx, s, &sigBeingParsed, args); if (sig.bind.exists()) { if (!args.allowRebind) { if (auto e = ctx.beginError(s.loc, core::errors::Resolver::InvalidTypeDeclaration)) { diff --git a/test/testdata/resolver/bad_generic_bind.rb b/test/testdata/resolver/bad_generic_bind.rb new file mode 100644 index 00000000000..b38e7e3cfeb --- /dev/null +++ b/test/testdata/resolver/bad_generic_bind.rb @@ -0,0 +1,23 @@ +# typed: true + +class TypeTemplate + extend T::Sig + extend T::Generic + + Elem = type_template + + sig { params(block: T.proc.bind(Elem).void).void } + # ^^^^^^^^^^^^^^^^^ error: Malformed `bind`: Can only bind to simple class names + def self.before_create(&block); end +end + +class TypeMember + extend T::Sig + extend T::Generic + + Elem = type_member + + sig { params(block: T.proc.bind(Elem).void).void } + # ^^^^^^^^^^^^^^^^^ error: Malformed `bind`: Can only bind to simple class names + def before_create(&block); end +end diff --git a/test/testdata/resolver/bind.rb b/test/testdata/resolver/bind.rb index 1dbb35d2700..b5b5d40bb19 100644 --- a/test/testdata/resolver/bind.rb +++ b/test/testdata/resolver/bind.rb @@ -13,8 +13,22 @@ def double_bind; 1; end sig {bind(Bar).returns(Integer)} def bar; 1; end + sig {bind(T.attached_class).returns(Integer)} +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Using `bind` is not permitted here + def self.bar; 1; end + sig {bind(T.any(Integer, String)).void} # error: Malformed `bind`: Can only bind to simple class names def too_complex; end + + sig {params(blk: T.proc.params(x: T.proc.bind(T.attached_class).void).void).void} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Using `bind` is not permitted here + def self.lambda_argument(&blk); end + + sig {returns(T.proc.returns(T.proc.bind(T.attached_class).void))} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Using `bind` is not permitted here + def self.lambda_returns + T.unsafe(nil) + end end class Bar diff --git a/test/testdata/resolver/bind_attached_class.rb b/test/testdata/resolver/bind_attached_class.rb new file mode 100644 index 00000000000..3c5bf18feaa --- /dev/null +++ b/test/testdata/resolver/bind_attached_class.rb @@ -0,0 +1,48 @@ +# typed: true + +class Base + extend T::Sig + + sig { params(block: T.proc.bind(T.attached_class).void).void } + def self.before_create(&block); end + + before_create do + T.reveal_type(self) # error: Revealed type: `Base` + end +end + +Base.before_create do + T.reveal_type(self) # error: Revealed type: `Base` +end + +class Model < Base + before_create do + T.reveal_type(self) # error: Revealed type: `Model` + + does_exist + does_not_exist # error: Method `does_not_exist` does not exist on `Model` + end + + def does_exist; end +end + +Model.before_create do + T.reveal_type(self) # error: Revealed type: `Model` + + does_exist + does_not_exist # error: Method `does_not_exist` does not exist on `Model` +end + +class BadBinds + extend T::Sig + + sig {params(it: T.proc.bind(T.attached_class).void).void} + # ^^ error: Using `bind` is not permitted here + def self.non_block_argument(it); end + + sig {returns(T.proc.bind(T.attached_class).void)} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Using `bind` is not permitted here + def self.attached_class_return + T.unsafe(nil) + end +end diff --git a/test/testdata/resolver/bind_self_type.rb b/test/testdata/resolver/bind_self_type.rb new file mode 100644 index 00000000000..50c1e3b34fe --- /dev/null +++ b/test/testdata/resolver/bind_self_type.rb @@ -0,0 +1,75 @@ +# typed: true + +module Mixin + extend T::Sig + + sig { params(block: T.proc.bind(T.self_type).void).void } + def foo(&block); end +end + +class IncludesMixin + include Mixin + + def does_exist; end + + def bar + foo do + T.reveal_type(self) # error: Revealed type: `IncludesMixin` + + does_exist + does_not_exist # error: Method `does_not_exist` does not exist on `IncludesMixin` + end + end +end + +IncludesMixin.new.foo do + T.reveal_type(self) # error: Revealed type: `IncludesMixin` + + does_exist + does_not_exist # error: Method `does_not_exist` does not exist on `IncludesMixin` +end + +class Base + extend T::Sig + + sig { params(block: T.proc.bind(T.self_type).void).void } + def self.foo(&block); end + + sig { params(block: T.proc.bind(T.self_type).void).void } + def instance_foo(&block); end +end + +class Model < Base + def self.does_exist; end + def does_exist; end + + foo do + T.reveal_type(self) # error: Revealed type: `T.class_of(Model)` + + does_exist + does_not_exist # error: Method `does_not_exist` does not exist on `T.class_of(Model)` + end + + def bar + instance_foo do + T.reveal_type(self) # error: Revealed type: `Model` + + does_exist + does_not_exist # error: Method `does_not_exist` does not exist on `Model` + end + end +end + +Model.foo do + T.reveal_type(self) # error: Revealed type: `T.class_of(Model)` + + does_exist + does_not_exist # error: Method `does_not_exist` does not exist on `T.class_of(Model)` +end + +Model.new.instance_foo do + T.reveal_type(self) # error: Revealed type: `Model` + + does_exist + does_not_exist # error: Method `does_not_exist` does not exist on `Model` +end