Skip to content

Replace evaluator decorators with construct flags - #1848

Draft
neilvcarvalho wants to merge 1 commit into
nc/definition-hierarchy-foldfrom
nc/evaluator-flags
Draft

neilvcarvalho wants to merge 1 commit into
nc/definition-hierarchy-foldfrom
nc/evaluator-flags

Conversation

@neilvcarvalho

Copy link
Copy Markdown
Member

Second step of the sequence of refactors toward a compiled, non-cloning core. Stacked on #1847 (step 1, the definition hierarchy fold); merge that one first.

What changed

While initialize_with runs, the evaluator used to be wrapped in three decorators built on BasicObject: InvocationTracker recorded every method called so attributes read by the constructor would not be assigned a second time, NewConstructor supplied new, and AttributeHash supplied attributes. The chain was rebuilt for every constructed object, and which wrapper happened to be self decided what a call meant.

That behavior now lives in Evaluator:

  • __construct__ runs the constructor block with a constructing flag set. AttributeAssigner calls it instead of building decorators.
  • An attribute read is recorded only while that flag is set and no attribute block is on the evaluation stack. That is exactly when the decorators used to see the call, so initialize_with { new(name) } with name { email.gsub(...) } still assigns email= afterwards.
  • new and attributes live in Evaluator::Construction, a module prepended to the evaluator while constructing. They shadow same-named attributes and overrides only inside initialize_with and fall through to super otherwise, as the decorators did. The acceptance suite covers a factory with an attributes attribute that calls new(**attributes) and a factory with a new attribute.
  • Attribute blocks are kept in a per-class registry (Evaluator.attribute_blocks) so attributes can read every assignable key through an internal path and mark it as read, instead of calling back through the public method it shadows.

Deleted

  • lib/factory_bot/decorator/attribute_hash.rb, lib/factory_bot/decorator/invocation_tracker.rb, lib/factory_bot/decorator/new_constructor.rb and their requires.
  • spec/factory_bot/decorator/attribute_hash_spec.rb.
  • The decorator wiring in AttributeAssigner (method_tracking_evaluator, decorated_evaluator, methods_invoked_on_evaluator). object, hash and the alias logic are untouched.

lib/factory_bot/decorator.rb stays for now because Decorator::DisallowsDuplicatesRegistry still inherits from it; folding that into Registry is step 6.

Behavior

No acceptance spec or feature was edited. Beyond the suite, I compared this branch with main on a script covering an attributes attribute, overrides named attributes and new, reads inside nested plain blocks, reads through another attribute's block, new and attributes outside the constructor, and attributes_for. All match except one case: a factory that declares an attribute named new and calls attributes inside initialize_with. On main the hash carries a freshly constructed instance under :new (the NewConstructor decorator intercepted the read) and the attribute was assigned again afterwards. Now the hash carries the attribute's value and it is not assigned a second time. This is the "reads through the internal path" behavior the plan calls for.

Verification

All on Ruby 4.0.1.

  • bundle exec standardrb: no offenses.
  • bundle exec rake all_specs (default Gemfile): 332 unit examples, 445 acceptance examples, 4 Cucumber scenarios, all passing.
  • BUNDLE_GEMFILE=gemfiles/7.0.gemfile bundle exec rake all_specs: same counts, all passing.
  • BUNDLE_GEMFILE=gemfiles/main.gemfile bundle exec rake all_specs: unit passing; the acceptance run hit the pre-existing timing flake in spec/acceptance/activesupport_instrumentation_spec.rb:186 described in Fold definition hierarchy into Factory #1847, which aborted the task before Cucumber. Rerunning acceptance three times and Cucumber once under the same gemfile: 445 examples passing each time, 4 scenarios passing.

While initialize_with runs, the evaluator was wrapped in three
decorators built on BasicObject: InvocationTracker recorded every
method called so the attributes read by the constructor would not be
assigned a second time, NewConstructor supplied new, and
AttributeHash supplied attributes. The chain was rebuilt for every
object constructed and made the evaluator's behavior depend on which
wrapper happened to be self.

Move that behavior into Evaluator. A __construct__ method runs the
constructor block with a constructing flag set, and attribute reads
are recorded only while that flag is set and no attribute block is
on the evaluation stack, which is exactly when the decorators used to
see the call. The new and attributes helpers live in a module
prepended while constructing, so they shadow same-named attributes and
overrides only inside initialize_with, as the decorators did.
Attribute blocks are kept in a per-class registry so attributes can
read every key through an internal path and mark it as read.

Deletes decorator/attribute_hash.rb, decorator/invocation_tracker.rb
and decorator/new_constructor.rb with their specs. decorator.rb stays
because DisallowsDuplicatesRegistry still inherits from it.

Behavior is unchanged for every acceptance example. The one difference
is a factory that declares an attribute named new and calls attributes
inside initialize_with: the hash now carries that attribute's value
instead of a freshly constructed instance, and the attribute is no
longer assigned a second time afterwards.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant