evilution 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07cd7bdd77da17dd201aa8e598707c326f4a9763ded14edfcd871a67de1ab2f5
4
- data.tar.gz: 47b0a007f57cc93f1a143555db3600c92dc38a116b58004a9ce7d1e4a0aadd5c
3
+ metadata.gz: c5cc9d6a79710b327b773b23138b767bb377260031a679d9d3ed1e8fc1308654
4
+ data.tar.gz: 7389805db21c5a7e406d388f517616b6df4b1bc0e139ebd83e907ec1093f9e9f
5
5
  SHA512:
6
- metadata.gz: ec518cde7ff3380d303ded60d7ea30b507d4b4d363d22211c06789de4cec6af8ad92e8b86e77a5a0d9551c9051cbf8afde4dc3eb261de8404260c22ada861648
7
- data.tar.gz: 2e60811f5ebe09e8958024f8edbf2c1bd825dd82b5c1e94a044e097108141ecbba4c90dcee86a9c2934b53b3bab4c0afc0bcfb2080d337e4fc26675b46140231
6
+ metadata.gz: 376eb8895c863791d581d2565c4551af7f8868a543cf17aecc88444101d11f6c7fffb2c80e1b1110526a2e6d0ac57750c3a170c3e73cd45cb7833df24c1fdd72
7
+ data.tar.gz: 8e244c54f10d5871a4d82f1be0aefc3ab276b850cf53800d28d4606ea401ca92b5e21aa88d6ece2e6e1b1d779550c4e7251c3c11f895ec0e8c13264aa9c5b80f
data/.beads/issues.jsonl CHANGED
@@ -97,23 +97,23 @@
97
97
  {"id":"EV-180","title":"Add 'evilution tests list' command","description":"List all detected test files and test examples that would be run. Useful for verifying test discovery is working correctly.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:19:56.667492485+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:19:56.667492485+07:00","dependencies":[{"issue_id":"EV-180","depends_on_id":"EV-170","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
98
98
  {"id":"EV-181","title":"Sorbet type signature awareness for mutations","description":"Detect and unwrap Sorbet type signatures (sig { ... }) before mutation. Mutant automatically skips mutations inside sig blocks and handles typed codebases without special configuration. Evilution should skip sig blocks to avoid generating useless mutations on type annotations.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:07.254888519+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:07.254888519+07:00"}
99
99
  {"id":"EV-182","title":"Add 'evilution util mutation' preview command","description":"Add a utility command that previews mutations for a code snippet without running tests: evilution util mutation -e 'def foo; x + y; end'. Shows all mutations that would be generated. Mutant has this. Useful for understanding operator behavior and debugging mutation generation.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:08.026842032+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:08.026842032+07:00"}
100
- {"id":"EV-183","title":"Implement namespace wildcard matching (Foo::Bar*)","description":"Support wildcard patterns in --target that match all classes/modules under a namespace. E.g., Foo::Bar* matches Foo::Bar, Foo::BarBaz, Foo::Bar::Qux.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:09.50500441+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:09.50500441+07:00","dependencies":[{"issue_id":"EV-183","depends_on_id":"EV-178","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
100
+ {"id":"EV-183","title":"Implement namespace wildcard matching (Foo::Bar*)","description":"Support wildcard patterns in --target that match all classes/modules under a namespace. E.g., Foo::Bar* matches Foo::Bar, Foo::BarBaz, Foo::Bar::Qux.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:09.50500441+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T23:30:28.435713182+07:00","dependencies":[{"issue_id":"EV-183","depends_on_id":"EV-178","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
101
101
  {"id":"EV-184","title":"Detect Sorbet sig blocks in source","description":"Identify Sorbet sig { ... } blocks (send nodes with receiver T and method sig) in source files. Store their byte ranges for exclusion.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:18.062074984+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:18.062074984+07:00","dependencies":[{"issue_id":"EV-184","depends_on_id":"EV-181","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
102
102
  {"id":"EV-185","title":"Cross-run comparison and diffing","description":"Add ability to diff between two session results to show mutation coverage changes over time. Specifically requested in comparison testing feedback. Show: new mutations, removed mutations, changed results (killed→survived or vice versa).","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:18.414861607+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:18.414861607+07:00"}
103
- {"id":"EV-186","title":"Implement method-type selectors (Foo#, Foo.)","description":"Support targeting all instance methods of a class (Foo#) or all class methods (Foo.) without specifying individual method names.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:21.178494367+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:21.178494367+07:00","dependencies":[{"issue_id":"EV-186","depends_on_id":"EV-178","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
103
+ {"id":"EV-186","title":"Implement method-type selectors (Foo#, Foo.)","description":"Support targeting all instance methods of a class (Foo#) or all class methods (Foo.) without specifying individual method names.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:21.178494367+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T23:02:33.070915314+07:00","dependencies":[{"issue_id":"EV-186","depends_on_id":"EV-178","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
104
104
  {"id":"EV-187","title":"Implement session result diffing engine","description":"Create a diff engine that compares two session result JSON files and produces: new mutations (in B but not A), removed mutations (in A but not B), status changes (killed→survived, survived→killed), and score delta.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:26.707985667+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:26.707985667+07:00","dependencies":[{"issue_id":"EV-187","depends_on_id":"EV-185","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
105
105
  {"id":"EV-188","title":"Skip mutation generation inside sig blocks","description":"Filter out any mutation whose source location falls within a Sorbet sig block. Report skipped count separately in verbose output.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:28.716852636+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:28.716852636+07:00","dependencies":[{"issue_id":"EV-188","depends_on_id":"EV-181","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
106
- {"id":"EV-189","title":"Implement descendant matching (descendants:Foo)","description":"Support descendants:Foo syntax to match Foo and all classes that inherit from Foo. Requires scanning the codebase for class inheritance.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:30.943382606+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:30.943382606+07:00","dependencies":[{"issue_id":"EV-189","depends_on_id":"EV-178","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
106
+ {"id":"EV-189","title":"Implement descendant matching (descendants:Foo)","description":"Support descendants:Foo syntax to match Foo and all classes that inherit from Foo. Requires scanning the codebase for class inheritance.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:30.943382606+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T22:39:55.192457857+07:00","dependencies":[{"issue_id":"EV-189","depends_on_id":"EV-178","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
107
107
  {"id":"EV-19","title":"Add method-name targeting (--target flag)","description":"Users can target a specific method for mutation testing via --target METHOD flag. Matches against Subject.name.","status":"closed","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-10T00:09:57.091870734+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-10T00:10:02.637447023+07:00","closed_at":"2026-03-10T00:10:02.637447023+07:00","close_reason":"Implemented --target flag in CLI, Config.target? predicate, Runner.filter_by_target, README docs, and specs"}
108
108
  {"id":"EV-190","title":"Add 'evilution session diff' CLI command","description":"Add CLI subcommand: evilution session diff <session-a> <session-b>. Output the comparison in a readable format with color-coded changes.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:38.753251207+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:38.753251207+07:00","dependencies":[{"issue_id":"EV-190","depends_on_id":"EV-185","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
109
109
  {"id":"EV-191","title":"Verify and ensure Ruby 4.0 compatibility","description":"Test Evilution against Ruby 4.0 (when available) and fix any compatibility issues. Mutant already supports Ruby 4.0. Key areas: Prism parser compatibility with new syntax, frozen string literals by default, any deprecated API usage.","status":"closed","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:39.786215101+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T17:15:21.121499298+07:00","closed_at":"2026-03-23T17:15:21.121499298+07:00","close_reason":"No action needed — Ruby 4.0 already supported and tested in CI (matrix includes 4.0.2). The gap analysis claim of 'not yet tested' was outdated."}
110
- {"id":"EV-192","title":"Implement source glob matching (source:lib/**/*.rb)","description":"Support source: prefix for file glob patterns in --target, matching mutant's source:lib/**/*.rb syntax.","status":"open","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:41.326339048+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:41.326339048+07:00","dependencies":[{"issue_id":"EV-192","depends_on_id":"EV-178","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
110
+ {"id":"EV-192","title":"Implement source glob matching (source:lib/**/*.rb)","description":"Support source: prefix for file glob patterns in --target, matching mutant's source:lib/**/*.rb syntax.","status":"in_progress","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:41.326339048+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T22:21:33.599617614+07:00","dependencies":[{"issue_id":"EV-192","depends_on_id":"EV-178","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
111
111
  {"id":"EV-193","title":"Add cross-run comparison to HTML report","description":"Extend the HTML reporter to optionally include comparison data from a baseline session, highlighting regressions (newly survived mutations) in red.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:49.187589087+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:49.187589087+07:00","dependencies":[{"issue_id":"EV-193","depends_on_id":"EV-185","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
112
112
  {"id":"EV-194","title":"Epic: Class and module definition mutations","description":"Add mutations for class definitions, module definitions, inheritance changes, and singleton classes. Mutant mutates these; they test structural code organization.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:20:53.918581132+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:20:53.918581132+07:00"}
113
- {"id":"EV-195","title":"Implement superclass removal mutation","description":"Mutate class Foo < Bar to class Foo (remove inheritance). Tests whether the superclass is actually needed. Prism class_node with superclass.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:02.559769802+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:21:02.559769802+07:00","dependencies":[{"issue_id":"EV-195","depends_on_id":"EV-194","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
113
+ {"id":"EV-195","title":"Implement superclass removal mutation","description":"Mutate class Foo < Bar to class Foo (remove inheritance). Tests whether the superclass is actually needed. Prism class_node with superclass.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:02.559769802+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-29T00:16:40.526192467+07:00","dependencies":[{"issue_id":"EV-195","depends_on_id":"EV-194","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
114
114
  {"id":"EV-196","title":"Show code diffs for survived mutations in text output","description":"Show the exact code diff (original vs mutated) for each survived mutation in the text reporter output, similar to how Mutant displays survived mutants. Currently Evilution shows the mutation description but not the visual diff. This was specifically requested in comparison testing feedback as it helps users quickly understand what test is missing.","status":"open","priority":2,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:02.574157928+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:21:02.574157928+07:00"}
115
- {"id":"EV-197","title":"Implement module include/extend removal","description":"Mutate include/extend/prepend statements: remove each one individually. Tests whether the mixin is actually used.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:13.978482592+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:21:13.978482592+07:00","dependencies":[{"issue_id":"EV-197","depends_on_id":"EV-194","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
116
- {"id":"EV-198","title":"Add RSpec suggestion templates for class/module mutations","description":"Add concrete suggestion templates for survived class/module mutations.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:23.284834592+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:21:23.284834592+07:00","dependencies":[{"issue_id":"EV-198","depends_on_id":"EV-194","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
115
+ {"id":"EV-197","title":"Implement module include/extend removal","description":"Mutate include/extend/prepend statements: remove each one individually. Tests whether the mixin is actually used.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:13.978482592+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T23:57:39.685072781+07:00","dependencies":[{"issue_id":"EV-197","depends_on_id":"EV-194","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
116
+ {"id":"EV-198","title":"Add RSpec suggestion templates for class/module mutations","description":"Add concrete suggestion templates for survived class/module mutations.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:23.284834592+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T23:47:56.899533924+07:00","dependencies":[{"issue_id":"EV-198","depends_on_id":"EV-194","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
117
117
  {"id":"EV-199","title":"Implement keyword argument mutations","description":"Add mutations for keyword arguments and optional keyword argument defaults. Mutations: remove keyword arg default value, swap keyword arg with positional, remove optional keyword. Prism keyword_hash_node, keyword_rest_parameter_node.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:33.74262984+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:21:33.74262984+07:00"}
118
118
  {"id":"EV-2","title":"Phase 1: Foundation — End-to-End Single Mutation","description":"Build the core pipeline: parse Ruby with Prism, generate mutations, fork-based isolation, RSpec integration, JSON reporting. Milestone: Runner.new(files: ['lib/user.rb']).call produces JSON output.","status":"closed","priority":2,"issue_type":"epic","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:04:58.737191467+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T11:02:00.342745637+07:00","closed_at":"2026-03-02T11:02:00.342745637+07:00","close_reason":"Phase 1 Foundation complete: Config, AST::Parser, Subject, SourceSurgeon, Mutation, Mutator::Base+Registry, ComparisonReplacement, Isolation::Fork, Integration::RSpec, Result objects, Reporter::JSON, Runner — all 13 tasks done"}
119
119
  {"id":"EV-2.1","title":"Implement Evilution::Config","description":"Immutable configuration value object. Fields: target_files, jobs (default: Etc.nprocessors), timeout (default: 10s), format (:json/:text), diff_base (nil), min_score (0.0), integration (:rspec), config_file path. Merge from defaults + YAML + CLI flags. File: lib/evilution/config.rb + spec.","status":"closed","priority":2,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-02T00:05:50.275297792+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-02T10:43:14.688620834+07:00","closed_at":"2026-03-02T10:43:14.688620834+07:00","close_reason":"Closed","dependencies":[{"issue_id":"EV-2.1","depends_on_id":"EV-2","type":"parent-child","created_at":"0001-01-01T00:00:00Z"}]}
@@ -134,8 +134,8 @@
134
134
  {"id":"EV-201","title":"Implement yield statement mutations","description":"Add mutations for yield statements. Mutations: remove yield, remove yield arguments, replace yield value with nil. Prism yield_node.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:21:53.437749245+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:21:53.437749245+07:00"}
135
135
  {"id":"EV-202","title":"Implement splat operator mutations","description":"Add mutations for splat (*) and double-splat (**) operators. Mutations: remove splat (pass array directly), remove double-splat (pass hash directly). Prism splat_node, assoc_splat_node.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:01.958187896+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:22:01.958187896+07:00"}
136
136
  {"id":"EV-203","title":"Epic: Extended equivalent mutation detection","description":"Extend equivalent mutation detection with more heuristics and manual marking support. Evilution's equivalent detection is already praised in feedback; extending it further strengthens a competitive advantage.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:11.400012571+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:22:11.400012571+07:00"}
137
- {"id":"EV-204","title":"Add more automatic equivalence heuristics","description":"Add new equivalence detection strategies: frozen string mutations (frozen strings are immutable, mutation is equivalent), private method rename (if only called internally with same signature), constant folding equivalences.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:20.07787144+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:22:20.07787144+07:00","dependencies":[{"issue_id":"EV-204","depends_on_id":"EV-203","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
138
- {"id":"EV-205","title":"Support # evilution:equivalent manual marking","description":"Allow users to mark specific mutations as equivalent using source comments: # evilution:equivalent. These should be excluded from the score denominator like auto-detected equivalents.","status":"open","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:31.543822152+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:22:31.543822152+07:00","dependencies":[{"issue_id":"EV-205","depends_on_id":"EV-203","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
137
+ {"id":"EV-204","title":"Add more automatic equivalence heuristics","description":"Add new equivalence detection strategies: frozen string mutations (frozen strings are immutable, mutation is equivalent), private method rename (if only called internally with same signature), constant folding equivalences.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:20.07787144+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T21:21:19.35465527+07:00","dependencies":[{"issue_id":"EV-204","depends_on_id":"EV-203","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
138
+ {"id":"EV-205","title":"Support # evilution:equivalent manual marking","description":"Allow users to mark specific mutations as equivalent using source comments: # evilution:equivalent. These should be excluded from the score denominator like auto-detected equivalents.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:31.543822152+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T22:08:20.967272294+07:00","dependencies":[{"issue_id":"EV-205","depends_on_id":"EV-203","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
139
139
  {"id":"EV-206","title":"Epic: MCP server enhancements","description":"Enhance the MCP server with session history browsing, cross-run diffs, and test suggestion streaming. MCP integration is Evilution's unique competitive advantage — investing here widens the moat.","status":"open","priority":3,"issue_type":"feature","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:41.847761146+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-23T11:22:41.847761146+07:00"}
140
140
  {"id":"EV-207","title":"Add session history browsing to MCP server","description":"Add MCP tools for listing and viewing past session results. Enables AI agents to review mutation testing history without CLI.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:22:50.697338124+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T01:23:57.262720975+07:00","dependencies":[{"issue_id":"EV-207","depends_on_id":"EV-206","type":"blocks","created_at":"0001-01-01T00:00:00Z"},{"issue_id":"EV-207","depends_on_id":"EV-150","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
141
141
  {"id":"EV-208","title":"Add cross-run diff to MCP server","description":"Add MCP tool for comparing two sessions and returning the diff. Enables AI agents to detect regressions.","status":"in_progress","priority":3,"issue_type":"task","owner":"denis.kiselyov@gmail.com","created_at":"2026-03-23T11:23:00.558002099+07:00","created_by":"Denis Kiselev","updated_at":"2026-03-28T10:58:31.301603302+07:00","dependencies":[{"issue_id":"EV-208","depends_on_id":"EV-206","type":"blocks","created_at":"0001-01-01T00:00:00Z"}]}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.15.0] - 2026-03-29
4
+
5
+ ### Added
6
+
7
+ - **SuperclassRemoval operator** — mutates `class Foo < Bar` to `class Foo` (removes inheritance) to test whether the superclass is actually needed (#342)
8
+ - **MixinRemoval operator** — removes `include`, `extend`, and `prepend` statements individually to test whether each mixin is actually used; supports both class and module scopes (#343)
9
+ - **Suggestion templates for class/module mutations** — static and concrete RSpec suggestion templates for `superclass_removal` and `mixin_removal` operators (#344)
10
+ - **Namespace wildcard matching** (`Foo::Bar*`) — `--target` now supports trailing `*` to match all classes under a namespace prefix (#329)
11
+ - **Method-type selectors** (`Foo#`, `Foo.`) — `--target` now supports `Foo#` for all instance methods and `Foo.` for all class methods; class methods (`def self.foo`) are now captured by the parser with `.` separator (#332)
12
+ - **Descendant matching** (`descendants:Foo`) — `--target` now supports inheritance-based filtering via `Evilution::AST::InheritanceScanner` Prism visitor (#335)
13
+ - **Source glob matching** (`source:lib/**/*.rb`) — `--target` now supports file glob patterns (#338)
14
+ - **CommentMarking heuristic** — `# evilution:equivalent` inline comment marks mutations as equivalent (#384)
15
+ - **ArithmeticIdentity heuristic** — detects equivalent mutations for arithmetic identity operations like `x + 0`, `x * 1`, `x ** 1` (#383)
16
+ - **AliasSwap heuristic expansion** — added `count`/`length` and `detect`/`find` alias pairs for `collection_replacement` operator (#384)
17
+
18
+ ### Changed
19
+
20
+ - **Operator count** — 30 operators (up from 28), with new structural mutation operators for class/module definitions
21
+ - **Parse tree caching** — shared parse cache on `Mutator::Base` for structural operators, cleared after each run to prevent unbounded memory growth
22
+ - **Class method support in parser** — `AST::Parser` now distinguishes instance methods (`Foo#bar`) from class methods (`Foo.bar`) via Prism's `DefNode#receiver`
23
+ - **Descendant filter** — uses `/[#.]/` split to include both instance and class methods in inheritance-based matching
24
+
3
25
  ## [0.14.0] - 2026-03-28
4
26
 
5
27
  ### Added
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../ast"
6
+
7
+ class Evilution::AST::InheritanceScanner < Prism::Visitor
8
+ attr_reader :inheritance
9
+
10
+ def initialize
11
+ @inheritance = {}
12
+ @context = []
13
+ super
14
+ end
15
+
16
+ def self.call(files)
17
+ scanner = new
18
+ files.each do |file|
19
+ source = File.read(file)
20
+ result = Prism.parse(source)
21
+ next if result.failure?
22
+
23
+ scanner.visit(result.value)
24
+ rescue SystemCallError
25
+ next
26
+ end
27
+ scanner.inheritance
28
+ end
29
+
30
+ def visit_class_node(node)
31
+ class_name = qualified_name(node.constant_path)
32
+
33
+ @inheritance[class_name] = (qualified_superclass(node.superclass) if node.superclass)
34
+
35
+ @context.push(constant_name(node.constant_path))
36
+ super
37
+ @context.pop
38
+ end
39
+
40
+ def visit_module_node(node)
41
+ @context.push(constant_name(node.constant_path))
42
+ super
43
+ @context.pop
44
+ end
45
+
46
+ private
47
+
48
+ def qualified_name(node)
49
+ name = constant_name(node)
50
+ @context.empty? ? name : "#{@context.join("::")}::#{name}"
51
+ end
52
+
53
+ def qualified_superclass(node)
54
+ name = constant_name(node)
55
+ return name if name.include?("::")
56
+ return name if @context.empty?
57
+
58
+ "#{@context.join("::")}::#{name}"
59
+ end
60
+
61
+ def constant_name(node)
62
+ if node.respond_to?(:full_name)
63
+ node.full_name
64
+ elsif node.respond_to?(:name)
65
+ node.name.to_s
66
+ else
67
+ node.slice
68
+ end
69
+ end
70
+ end
@@ -54,11 +54,19 @@ module Evilution::AST
54
54
  end
55
55
 
56
56
  def visit_def_node(node)
57
+ separator = node.receiver ? "." : "#"
58
+ add_subject(node, separator)
59
+ super
60
+ end
61
+
62
+ private
63
+
64
+ def add_subject(node, separator)
57
65
  scope = @context.join("::")
58
66
  name = if scope.empty?
59
- "##{node.name}"
67
+ "#{separator}#{node.name}"
60
68
  else
61
- "#{scope}##{node.name}"
69
+ "#{scope}#{separator}#{node.name}"
62
70
  end
63
71
 
64
72
  loc = node.location
@@ -71,12 +79,8 @@ module Evilution::AST
71
79
  source: method_source,
72
80
  node: node
73
81
  )
74
-
75
- super
76
82
  end
77
83
 
78
- private
79
-
80
84
  def constant_name(node)
81
85
  if node.respond_to?(:full_name)
82
86
  node.full_name
data/lib/evilution/cli.rb CHANGED
@@ -146,7 +146,11 @@ class Evilution::CLI
146
146
  def add_filter_options(opts)
147
147
  opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
148
148
  opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
149
- opts.on("--target METHOD", "Only mutate the named method (e.g. Foo::Bar#calculate)") { |m| @options[:target] = m }
149
+ opts.on("--target EXPR",
150
+ "Filter: method (Foo#bar), type (Foo#/Foo.), namespace (Foo*),",
151
+ "class (Foo), glob (source:**/*.rb), hierarchy (descendants:Foo)") do |m|
152
+ @options[:target] = m
153
+ end
150
154
  end
151
155
 
152
156
  def add_flag_options(opts)
@@ -4,6 +4,8 @@ require_relative "heuristic/noop_source"
4
4
  require_relative "heuristic/method_body_nil"
5
5
  require_relative "heuristic/alias_swap"
6
6
  require_relative "heuristic/dead_code"
7
+ require_relative "heuristic/arithmetic_identity"
8
+ require_relative "heuristic/comment_marking"
7
9
 
8
10
  require_relative "../equivalent"
9
11
 
@@ -34,7 +36,9 @@ class Evilution::Equivalent::Detector
34
36
  Evilution::Equivalent::Heuristic::NoopSource.new,
35
37
  Evilution::Equivalent::Heuristic::MethodBodyNil.new,
36
38
  Evilution::Equivalent::Heuristic::AliasSwap.new,
37
- Evilution::Equivalent::Heuristic::DeadCode.new
39
+ Evilution::Equivalent::Heuristic::DeadCode.new,
40
+ Evilution::Equivalent::Heuristic::ArithmeticIdentity.new,
41
+ Evilution::Equivalent::Heuristic::CommentMarking.new
38
42
  ]
39
43
  end
40
44
  end
@@ -6,11 +6,14 @@ class Evilution::Equivalent::Heuristic::AliasSwap
6
6
  ALIAS_PAIRS = Set[
7
7
  Set[:detect, :find],
8
8
  Set[:length, :size],
9
- Set[:collect, :map]
9
+ Set[:collect, :map],
10
+ Set[:count, :length]
10
11
  ].freeze
11
12
 
13
+ MATCHING_OPERATORS = Set["send_mutation", "collection_replacement"].freeze
14
+
12
15
  def match?(mutation)
13
- return false unless mutation.operator_name == "send_mutation"
16
+ return false unless MATCHING_OPERATORS.include?(mutation.operator_name)
14
17
 
15
18
  diff = mutation.diff
16
19
  removed = extract_method(diff, "- ")
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../heuristic"
4
+
5
+ class Evilution::Equivalent::Heuristic::ArithmeticIdentity
6
+ # Patterns where the original expression is an arithmetic identity operation.
7
+ # "x + 0" is identity (equals x), so mutating the 0 to something else means
8
+ # the original was a no-op — if the test doesn't catch it, it's likely equivalent.
9
+ ADDITIVE_IDENTITY = /[\w)\].]+\s*[+-]\s*0\b|\b0\s*\+\s*[\w(\[]/
10
+ MULTIPLICATIVE_IDENTITY = %r{[\w)\].]+\s*[*/]\s*1\b|\b1\s*\*\s*[\w(\[]}
11
+ EXPONENT_IDENTITY = /[\w)\].]+\s*\*\*\s*1\b/
12
+
13
+ def match?(mutation)
14
+ return false unless mutation.operator_name == "integer_literal"
15
+
16
+ removed = diff_line(mutation.diff, "- ")
17
+ return false unless removed
18
+
19
+ content = removed.sub(/^- /, "")
20
+ content.match?(ADDITIVE_IDENTITY) ||
21
+ content.match?(MULTIPLICATIVE_IDENTITY) ||
22
+ content.match?(EXPONENT_IDENTITY)
23
+ end
24
+
25
+ private
26
+
27
+ def diff_line(diff, prefix)
28
+ diff.split("\n").find { |l| l.start_with?(prefix) }
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../heuristic"
4
+
5
+ class Evilution::Equivalent::Heuristic::CommentMarking
6
+ MARKER = /#\s*evilution:equivalent\b/
7
+
8
+ def match?(mutation)
9
+ source = mutation.original_source
10
+ return false unless source
11
+
12
+ lines = source.lines
13
+ line_index = mutation.line - 1
14
+ return false if line_index.negative? || line_index >= lines.length
15
+
16
+ return true if lines[line_index].match?(MARKER)
17
+ return true if line_index.positive? && lines[line_index - 1].match?(MARKER)
18
+
19
+ false
20
+ end
21
+ end
@@ -49,4 +49,20 @@ class Evilution::Mutator::Base < Prism::Visitor
49
49
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
50
50
  .downcase
51
51
  end
52
+
53
+ @parse_cache = {}
54
+
55
+ def self.parsed_tree_for(file_path, file_source)
56
+ cache = Evilution::Mutator::Base.instance_variable_get(:@parse_cache)
57
+ entry = cache[file_path]
58
+ return entry[:tree] if entry && entry[:source_hash] == file_source.hash
59
+
60
+ tree = Prism.parse(file_source).value
61
+ cache[file_path] = { source_hash: file_source.hash, tree: tree }
62
+ tree
63
+ end
64
+
65
+ def self.clear_parse_cache!
66
+ Evilution::Mutator::Base.instance_variable_set(:@parse_cache, {})
67
+ end
52
68
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../operator"
6
+
7
+ class Evilution::Mutator::Operator::MixinRemoval < Evilution::Mutator::Base
8
+ MIXIN_METHODS = %i[include extend prepend].freeze
9
+
10
+ def call(subject)
11
+ @subject = subject
12
+ @file_source = File.read(subject.file_path)
13
+ @mutations = []
14
+
15
+ tree = self.class.parsed_tree_for(subject.file_path, @file_source)
16
+ enclosing = find_enclosing_scope(tree, subject.line_number)
17
+ return @mutations unless enclosing
18
+
19
+ first_method_line = find_first_method_line(enclosing)
20
+ return @mutations unless first_method_line == subject.line_number
21
+
22
+ find_mixin_calls(enclosing).each do |call_node|
23
+ add_mutation(
24
+ offset: call_node.location.start_offset,
25
+ length: call_node.location.length,
26
+ replacement: "",
27
+ node: call_node
28
+ )
29
+ end
30
+
31
+ @mutations
32
+ end
33
+
34
+ private
35
+
36
+ def find_enclosing_scope(tree, target_line)
37
+ finder = ScopeFinder.new(target_line)
38
+ finder.visit(tree)
39
+ finder.result
40
+ end
41
+
42
+ def find_first_method_line(scope_node)
43
+ return nil unless scope_node.body
44
+
45
+ scope_node.body.body.each do |node|
46
+ return node.location.start_line if node.is_a?(Prism::DefNode)
47
+ end
48
+ nil
49
+ end
50
+
51
+ def find_mixin_calls(scope_node)
52
+ return [] unless scope_node.body
53
+
54
+ scope_node.body.body.select do |node|
55
+ node.is_a?(Prism::CallNode) &&
56
+ MIXIN_METHODS.include?(node.name) &&
57
+ node.receiver.nil?
58
+ end
59
+ end
60
+
61
+ # Visitor to find the ClassNode or ModuleNode enclosing a given line number.
62
+ class ScopeFinder < Prism::Visitor
63
+ attr_reader :result
64
+
65
+ def initialize(target_line)
66
+ @target_line = target_line
67
+ @result = nil
68
+ end
69
+
70
+ def visit_class_node(node)
71
+ @result = node if @target_line.between?(node.location.start_line, node.location.end_line)
72
+ super
73
+ end
74
+
75
+ def visit_module_node(node)
76
+ @result = node if @target_line.between?(node.location.start_line, node.location.end_line)
77
+ super
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../operator"
6
+
7
+ class Evilution::Mutator::Operator::SuperclassRemoval < Evilution::Mutator::Base
8
+ def call(subject)
9
+ @subject = subject
10
+ @file_source = File.read(subject.file_path)
11
+ @mutations = []
12
+
13
+ tree = self.class.parsed_tree_for(subject.file_path, @file_source)
14
+ enclosing = find_enclosing_class(tree, subject.line_number)
15
+ return @mutations unless enclosing
16
+ return @mutations unless enclosing.superclass
17
+
18
+ first_method_line = find_first_method_line(enclosing)
19
+ return @mutations unless first_method_line == subject.line_number
20
+
21
+ name_end = enclosing.constant_path.location.start_offset + enclosing.constant_path.location.length
22
+ superclass_end = enclosing.superclass.location.start_offset + enclosing.superclass.location.length
23
+
24
+ add_mutation(
25
+ offset: name_end,
26
+ length: superclass_end - name_end,
27
+ replacement: "",
28
+ node: enclosing
29
+ )
30
+
31
+ @mutations
32
+ end
33
+
34
+ private
35
+
36
+ def find_enclosing_class(tree, target_line)
37
+ finder = ClassFinder.new(target_line)
38
+ finder.visit(tree)
39
+ finder.result
40
+ end
41
+
42
+ def find_first_method_line(class_node)
43
+ return nil unless class_node.body
44
+
45
+ class_node.body.body.each do |node|
46
+ return node.location.start_line if node.is_a?(Prism::DefNode)
47
+ end
48
+ nil
49
+ end
50
+
51
+ # Visitor to find the ClassNode enclosing a given line number.
52
+ class ClassFinder < Prism::Visitor
53
+ attr_reader :result
54
+
55
+ def initialize(target_line)
56
+ @target_line = target_line
57
+ @result = nil
58
+ end
59
+
60
+ def visit_class_node(node)
61
+ @result = node if @target_line.between?(node.location.start_line, node.location.end_line)
62
+ super
63
+ end
64
+ end
65
+ end
@@ -33,7 +33,9 @@ class Evilution::Mutator::Registry
33
33
  Evilution::Mutator::Operator::ReceiverReplacement,
34
34
  Evilution::Mutator::Operator::SendMutation,
35
35
  Evilution::Mutator::Operator::ArgumentNilSubstitution,
36
- Evilution::Mutator::Operator::CompoundAssignment
36
+ Evilution::Mutator::Operator::CompoundAssignment,
37
+ Evilution::Mutator::Operator::MixinRemoval,
38
+ Evilution::Mutator::Operator::SuperclassRemoval
37
39
  ].each { |op| registry.register(op) }
38
40
  registry
39
41
  end
@@ -24,7 +24,9 @@ class Evilution::Reporter::Suggestion
24
24
  "collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects",
25
25
  "method_call_removal" => "Add a test that depends on the return value or side effect of this method call",
26
26
  "argument_removal" => "Add a test that verifies the correct arguments are passed to this method call",
27
- "compound_assignment" => "Add a test that verifies the side effect of this compound assignment (the accumulated value matters)"
27
+ "compound_assignment" => "Add a test that verifies the side effect of this compound assignment (the accumulated value matters)",
28
+ "superclass_removal" => "Add a test that exercises inherited behavior from the superclass",
29
+ "mixin_removal" => "Add a test that exercises behavior provided by the included/extended module"
28
30
  }.freeze
29
31
 
30
32
  CONCRETE_TEMPLATES = {
@@ -289,6 +291,32 @@ class Evilution::Reporter::Suggestion
289
291
  expect(result).to be_nil
290
292
  end
291
293
  RSPEC
294
+ },
295
+ "superclass_removal" => lambda { |mutation|
296
+ method_name = parse_method_name(mutation.subject.name)
297
+ original_line, _mutated_line = extract_diff_lines(mutation.diff)
298
+ <<~RSPEC.strip
299
+ # Mutation: removed superclass from `#{original_line}` in #{mutation.subject.name}
300
+ # #{mutation.file_path}:#{mutation.line}
301
+ it 'depends on inherited behavior in ##{method_name}' do
302
+ # Assert behavior that comes from the superclass
303
+ result = subject.#{method_name}(input_value)
304
+ expect(result).to eq(expected)
305
+ end
306
+ RSPEC
307
+ },
308
+ "mixin_removal" => lambda { |mutation|
309
+ method_name = parse_method_name(mutation.subject.name)
310
+ original_line, _mutated_line = extract_diff_lines(mutation.diff)
311
+ <<~RSPEC.strip
312
+ # Mutation: removed `#{original_line}` in #{mutation.subject.name}
313
+ # #{mutation.file_path}:#{mutation.line}
314
+ it 'depends on behavior from the included module in ##{method_name}' do
315
+ # Assert behavior provided by the mixin
316
+ result = subject.#{method_name}(input_value)
317
+ expect(result).to eq(expected)
318
+ end
319
+ RSPEC
292
320
  }
293
321
  }.freeze
294
322
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "config"
4
4
  require_relative "ast/parser"
5
+ require_relative "ast/inheritance_scanner"
5
6
  require_relative "memory"
6
7
  require_relative "mutator/registry"
7
8
  require_relative "isolation/fork"
@@ -35,9 +36,7 @@ class Evilution::Runner
35
36
  def call
36
37
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
38
 
38
- subjects = parse_subjects
39
- subjects = filter_by_target(subjects) if config.target?
40
- subjects = filter_by_line_ranges(subjects) if config.line_ranges?
39
+ subjects = parse_and_filter_subjects
41
40
  log_memory("after parse_subjects", "#{subjects.length} subjects")
42
41
 
43
42
  baseline_result = run_baseline(subjects)
@@ -45,6 +44,7 @@ class Evilution::Runner
45
44
  mutations = generate_mutations(subjects)
46
45
  equivalent_mutations, mutations = filter_equivalent(mutations)
47
46
  release_subject_nodes(subjects)
47
+ clear_operator_caches
48
48
  results, truncated = run_mutations(mutations, baseline_result)
49
49
  results += equivalent_mutations.map do |m|
50
50
  m.strip_sources!
@@ -65,28 +65,98 @@ class Evilution::Runner
65
65
 
66
66
  attr_reader :parser, :registry, :isolator, :cache, :on_result
67
67
 
68
+ def parse_and_filter_subjects
69
+ subjects = parse_subjects
70
+ subjects = filter_by_descendants(subjects) if descendants_target?
71
+ subjects = filter_by_target(subjects) if method_target?
72
+ subjects = filter_by_line_ranges(subjects) if config.line_ranges?
73
+ subjects
74
+ end
75
+
68
76
  def parse_subjects
69
77
  files = resolve_target_files
70
78
  files.flat_map { |file| parser.call(file) }
71
79
  end
72
80
 
73
81
  def resolve_target_files
82
+ return resolve_source_glob if source_glob_target?
74
83
  return config.target_files unless config.target_files.empty?
75
84
 
76
85
  Evilution::Git::ChangedFiles.new.call
77
86
  end
78
87
 
88
+ def source_glob_target?
89
+ config.target&.start_with?("source:")
90
+ end
91
+
92
+ def descendants_target?
93
+ config.target&.start_with?("descendants:")
94
+ end
95
+
96
+ def method_target?
97
+ config.target? && !source_glob_target? && !descendants_target?
98
+ end
99
+
100
+ def resolve_source_glob
101
+ pattern = config.target.delete_prefix("source:")
102
+ files = Dir.glob(pattern)
103
+ raise Evilution::Error, "no files found matching '#{pattern}'" if files.empty?
104
+
105
+ files.sort
106
+ end
107
+
108
+ def filter_by_descendants(subjects)
109
+ base_name = config.target.delete_prefix("descendants:")
110
+ files = resolve_target_files
111
+ inheritance = Evilution::AST::InheritanceScanner.call(files)
112
+ class_names = resolve_descendant_set(base_name, inheritance)
113
+ raise Evilution::Error, "no classes found matching '#{config.target}'" if class_names.empty?
114
+
115
+ subjects.select { |s| class_names.include?(s.name.split(/[#.]/).first) }
116
+ end
117
+
118
+ def resolve_descendant_set(base_name, inheritance)
119
+ descendants = Set.new
120
+ known = inheritance.key?(base_name) || inheritance.value?(base_name)
121
+ return descendants unless known
122
+
123
+ descendants.add(base_name)
124
+ changed = true
125
+ while changed
126
+ changed = false
127
+ inheritance.each do |child, parent|
128
+ next unless descendants.include?(parent)
129
+ next if descendants.include?(child)
130
+
131
+ descendants.add(child)
132
+ changed = true
133
+ end
134
+ end
135
+ descendants
136
+ end
137
+
79
138
  def filter_by_target(subjects)
80
- matched = if config.target.include?("#")
81
- subjects.select { |s| s.name == config.target }
82
- else
83
- subjects.select { |s| s.name.start_with?("#{config.target}#") }
84
- end
139
+ matched = subjects.select(&target_matcher)
85
140
  raise Evilution::Error, "no method found matching '#{config.target}'" if matched.empty?
86
141
 
87
142
  matched
88
143
  end
89
144
 
145
+ def target_matcher
146
+ target = config.target
147
+ if target.end_with?("*")
148
+ prefix = target.chomp("*")
149
+ ->(s) { s.name.split(/[#.]/).first.start_with?(prefix) }
150
+ elsif target.end_with?("#", ".")
151
+ prefix = target
152
+ ->(s) { s.name.start_with?(prefix) }
153
+ elsif target.include?("#") || target.include?(".")
154
+ ->(s) { s.name == target }
155
+ else
156
+ ->(s) { s.name.start_with?("#{target}#") || s.name.start_with?("#{target}.") }
157
+ end
158
+ end
159
+
90
160
  def filter_by_line_ranges(subjects)
91
161
  subjects.select do |subject|
92
162
  range = config.line_ranges[subject.file_path]
@@ -112,6 +182,10 @@ class Evilution::Runner
112
182
  subjects.each(&:release_node!)
113
183
  end
114
184
 
185
+ def clear_operator_caches
186
+ Evilution::Mutator::Base.clear_parse_cache!
187
+ end
188
+
115
189
  def equivalent_result(mutation)
116
190
  Evilution::Result::MutationResult.new(mutation: mutation, status: :equivalent, duration: 0.0)
117
191
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evilution
4
- VERSION = "0.14.0"
4
+ VERSION = "0.15.0"
5
5
  end
data/lib/evilution.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "evilution/ast"
10
10
  require_relative "evilution/parallel"
11
11
  require_relative "evilution/ast/source_surgeon"
12
12
  require_relative "evilution/ast/parser"
13
+ require_relative "evilution/ast/inheritance_scanner"
13
14
  require_relative "evilution/mutator"
14
15
  require_relative "evilution/mutator/base"
15
16
  require_relative "evilution/mutator/operator"
@@ -41,6 +42,8 @@ require_relative "evilution/mutator/operator/receiver_replacement"
41
42
  require_relative "evilution/mutator/operator/send_mutation"
42
43
  require_relative "evilution/mutator/operator/argument_nil_substitution"
43
44
  require_relative "evilution/mutator/operator/compound_assignment"
45
+ require_relative "evilution/mutator/operator/mixin_removal"
46
+ require_relative "evilution/mutator/operator/superclass_removal"
44
47
  require_relative "evilution/mutator/registry"
45
48
  require_relative "evilution/equivalent"
46
49
  require_relative "evilution/equivalent/heuristic"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evilution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Kiselev
@@ -79,6 +79,7 @@ files:
79
79
  - exe/evilution
80
80
  - lib/evilution.rb
81
81
  - lib/evilution/ast.rb
82
+ - lib/evilution/ast/inheritance_scanner.rb
82
83
  - lib/evilution/ast/parser.rb
83
84
  - lib/evilution/ast/source_surgeon.rb
84
85
  - lib/evilution/baseline.rb
@@ -89,6 +90,8 @@ files:
89
90
  - lib/evilution/equivalent/detector.rb
90
91
  - lib/evilution/equivalent/heuristic.rb
91
92
  - lib/evilution/equivalent/heuristic/alias_swap.rb
93
+ - lib/evilution/equivalent/heuristic/arithmetic_identity.rb
94
+ - lib/evilution/equivalent/heuristic/comment_marking.rb
92
95
  - lib/evilution/equivalent/heuristic/dead_code.rb
93
96
  - lib/evilution/equivalent/heuristic/method_body_nil.rb
94
97
  - lib/evilution/equivalent/heuristic/noop_source.rb
@@ -130,6 +133,7 @@ files:
130
133
  - lib/evilution/mutator/operator/integer_literal.rb
131
134
  - lib/evilution/mutator/operator/method_body_replacement.rb
132
135
  - lib/evilution/mutator/operator/method_call_removal.rb
136
+ - lib/evilution/mutator/operator/mixin_removal.rb
133
137
  - lib/evilution/mutator/operator/negation_insertion.rb
134
138
  - lib/evilution/mutator/operator/nil_replacement.rb
135
139
  - lib/evilution/mutator/operator/range_replacement.rb
@@ -139,6 +143,7 @@ files:
139
143
  - lib/evilution/mutator/operator/send_mutation.rb
140
144
  - lib/evilution/mutator/operator/statement_deletion.rb
141
145
  - lib/evilution/mutator/operator/string_literal.rb
146
+ - lib/evilution/mutator/operator/superclass_removal.rb
142
147
  - lib/evilution/mutator/operator/symbol_literal.rb
143
148
  - lib/evilution/mutator/registry.rb
144
149
  - lib/evilution/parallel.rb