Repository navigation
Mutate methods wrapped by a prepended module - #1685
Merged
Merged
Conversation
sferik
force-pushed
the
prepended-method-wrappers
branch
from
October 29, 2026 06:27
7c0421a to
c0d9611
Compare
Gems such as memosa memoize a method by prepending a module with a wrapper of the same name that calls super. Reflection on the scope returns that wrapper, whose source location points into the gem, so mutant found no matching def and dropped the method without a warning. Follow super_method past prepended modules until reaching the method defined on the scope itself. Mutations replace that method and the wrapper keeps running around them. This applies to instance and singleton methods alike and needs no knowledge of memosa. When the chain never reaches the scope, as with the methods DelegateClass lists as its own, the method is not defined there and is skipped as before. For instance methods the walk runs inside the existing rescue, so a reflection failure warns and skips the method. Read visibility from the scope without its ancestors, since a wrapper can have a different visibility than the method it wraps. Otherwise a private method behind a public wrapper became a public subject, and inserting each mutation made the original method public. Detect module_function the same way, ignoring ancestors on both sides, so a wrapper prepended to the singleton class no longer hides the copy of the method there. Remove a nil source location guard in matched_view that call already handles, and extra spacing that RuboCop flagged in the instance methods spec. Fixes mbj#1682
sferik
force-pushed
the
prepended-method-wrappers
branch
from
October 29, 2026 14:29
c0d9611 to
76f1270
Compare
mbj
approved these changes
Oct 29, 2026
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.
memosa memoizes a method by prepending a
MemosaMethodsmodule that defines a wrapper of the same name, which callssuper. When mutant asks the class for that method, it gets the wrapper back, and the wrapper's source location points into memosa itself. Mutant parsesmemosa.rb, finds no matchingdef, and silently drops the method. No warning, it just never becomes a subject.For example:
On
main, mutant finds 2 subjects here (initializeandreveal) and never touchesdoubleorsecret. With this patch it finds all 4, and all 37 mutations are killed by a small spec, which also shows the mutated code really runs behind memosa's wrapper.The fix is in
Matcher::Methods: after looking up a method, followsuper_methoduntil the owner is the scope itself. The original method is still defined on the class, so mutations replace it and the prepended wrapper keeps working around them (caching included). Since it lives in the shared matcher, it covers singleton methods too.The fork linked in the issue special-cased memosa, but nothing here is memosa specific, so mutant doesn't need to know about it. Anything that wraps methods with
prepend(instrumentation, other memoizers) gets the same treatment, and mutating the method on the class while leaving the wrapper in place seems like the right behavior for all of them.I checked the case where a subclass changes the visibility of an inherited method (
private :foo), since that creates a method entry without a body. Ruby reports the subclass as the owner, so the loop stops right away.