Repository navigation
Replace evaluator decorators with construct flags - #1848
Draft
neilvcarvalho wants to merge 1 commit into
Draft
neilvcarvalho wants to merge 1 commit into
neilvcarvalho wants to merge 1 commit into
Conversation
neilvcarvalho
added this pull request to stack #1849
October 6, 2026 14:48
neilvcarvalho
marked this pull request as draft
October 6, 2026 14:51
This was referenced Oct 6, 2026
neilvcarvalho
force-pushed
the
nc/evaluator-flags
branch
from
October 6, 2026 15:31
286d592 to
59e2995
Compare
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.
neilvcarvalho
force-pushed
the
nc/evaluator-flags
branch
from
October 13, 2026 13:15
59e2995 to
f1e6b5b
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_withruns, the evaluator used to be wrapped in three decorators built onBasicObject:InvocationTrackerrecorded every method called so attributes read by the constructor would not be assigned a second time,NewConstructorsuppliednew, andAttributeHashsuppliedattributes. The chain was rebuilt for every constructed object, and which wrapper happened to beselfdecided what a call meant.That behavior now lives in
Evaluator:__construct__runs the constructor block with a constructing flag set.AttributeAssignercalls it instead of building decorators.initialize_with { new(name) }withname { email.gsub(...) }still assignsemail=afterwards.newandattributeslive inEvaluator::Construction, a module prepended to the evaluator while constructing. They shadow same-named attributes and overrides only insideinitialize_withand fall through tosuperotherwise, as the decorators did. The acceptance suite covers a factory with anattributesattribute that callsnew(**attributes)and a factory with anewattribute.Evaluator.attribute_blocks) soattributescan 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.rband their requires.spec/factory_bot/decorator/attribute_hash_spec.rb.AttributeAssigner(method_tracking_evaluator,decorated_evaluator,methods_invoked_on_evaluator).object,hashand the alias logic are untouched.lib/factory_bot/decorator.rbstays for now becauseDecorator::DisallowsDuplicatesRegistrystill inherits from it; folding that intoRegistryis step 6.Behavior
No acceptance spec or feature was edited. Beyond the suite, I compared this branch with
mainon a script covering anattributesattribute, overrides namedattributesandnew, reads inside nested plain blocks, reads through another attribute's block,newandattributesoutside the constructor, andattributes_for. All match except one case: a factory that declares an attribute namednewand callsattributesinsideinitialize_with. Onmainthe hash carries a freshly constructed instance under:new(theNewConstructordecorator 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 inspec/acceptance/activesupport_instrumentation_spec.rb:186described 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.