Repository navigation
Evaluate attributes with a single Evaluator - #1852
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 15:19
neilvcarvalho
force-pushed
the
nc/single-evaluator
branch
from
October 6, 2026 15:31
43f3693 to
abd5bfd
Compare
Each factory defined an anonymous Evaluator subclass per snapshot, inheriting from the parent's, with one method per attribute and a singleton method per override, so that Ruby method lookup performed attribute inheritance. That needed EvaluatorClassDefiner, the class_attribute of attribute lists, the undef dance in define_attribute, and the attribute block registry. Under attributes_for a NullObject stood in for the instance so that blocks referencing build class methods would answer nil. The snapshot now carries an ordered Hash of attributes, merged from the parent's snapshot, the factory's own definition and the requested traits. A later definition wins but keeps the first definition's position, and a name declared transient anywhere stays transient, which reproduces the assignment order and transient handling of the class chain. Evaluator becomes a single BasicObject over that Hash: method_missing looks up the memo, then the attributes, then the instance, then SyntaxRunner. A hash mode flag replaces NullObject: with no instance, a method the build class defines answers nil. The memo is a copy of the overrides instead of the same Hash, so attributes evaluated before assignment no longer count as overrides in the alias logic. The initialize_with helpers new and attributes shadow attributes of the same name only while constructing, as before. Deletes evaluator_class_definer.rb and null_object.rb with their specs. The one acceptance example that inspected the evaluator through instance_values now reads the strategy with instance_eval, as the plan notes, since a BasicObject has no instance_values. Behavior is unchanged for every other acceptance example.
neilvcarvalho
force-pushed
the
nc/single-evaluator
branch
from
October 13, 2026 13:15
abd5bfd to
c826fb5
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.
Fourth step of the sequence of refactors toward a compiled, non-cloning core. Stacked on #1851 (step 3), #1848 (step 2) and #1847 (step 1); merge those first.
What changed
Each factory used to define an anonymous
Evaluatorsubclass per snapshot, inheriting from the parent's, with one method per attribute and a singleton method per override, so that Ruby method lookup performed attribute inheritance.CompiledFactorynow carries an ordered Hash of attributes instead of an evaluator class.Factory#attributes_overmerges the parent's Hash, the factory's own definition and the requested traits. A later definition wins but keeps the first definition's position, which reproduces the assignment order of the class chain, and a name declared transient anywhere stays transient, which the suite pins inspec/acceptance/transient_attributes_spec.rb("redefining a transient attribute without the transient block").Attribute#as_transientproduces the copy used for that case, alongside thetransientreader that Refactor remaining internal ignored state to transient terminology #1834 renamed fromignored.Evaluatoris a singleBasicObjectover that Hash.method_missinglooks up the memo, then the attributes, then the instance, thenSyntaxRunner. It is aBasicObjectso attribute names such ashash,display,method,formatandsystemare not shadowed byObject; inside the class, constants are written as::FactoryBot::...and::Kernel.raise.respond_to?,send,public_send,nil?andinspectare defined explicitly since callbacks receive the evaluator.NullObject: underattributes_forthere is no instance, and a method the build class defines answers nil, exactly what the null object did.beforecallback leaked into__override_names__and into the alias logic.initialize_withhelpersnewandattributesstill shadow attributes of the same name only while constructing, now as plain methods that fall through tomethod_missingotherwise.AttributeAssignertakes the attribute Hash instead of readingevaluator.class.attribute_list.Deleted
lib/factory_bot/evaluator_class_definer.rb,lib/factory_bot/null_object.rb, their requires and their specs.Evaluator.define_attribute,Evaluator.attribute_list, theattribute_listsandattribute_blocksclass attributes, theConstructionprepend module, the per-override singleton methods, andAttributeAssigner#build_hashand#hash_instance_methods_to_respond_to.Acceptance edit
One acceptance example changes, as the plan notes:
spec/acceptance/callbacks_spec.rbread the strategy throughcontext.instance_values["build_strategy"], which aBasicObjectdoes not have. It now readscontext.instance_eval { @build_strategy }. Nothing else in the acceptance suite or the features changed.Behavior
Beyond the suite, I compared this branch with
mainon attribute names that collide withObjectmethods underbuildandattributes_for, blocks referencing build class methods under both strategies, an unknown method underattributes_for,send,respond_to?andnil?on the evaluator inside a callback, and theinitialize_withedge cases from step 2. All match. One visible difference, which is the aliasing fix above: a factory declaring bothuseranduser_idwhosebefore(:build)callback readsevaluator.usernow assignsuser_id; onmainthe read madeuserlook like an override anduser_idwas skipped.Verification
All on Ruby 4.0.1.
bundle exec standardrb: no offenses.bundle exec rake all_specs(default Gemfile): 324 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: same counts, all passing.