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 +4 -4
- data/.beads/issues.jsonl +9 -9
- data/CHANGELOG.md +22 -0
- data/lib/evilution/ast/inheritance_scanner.rb +70 -0
- data/lib/evilution/ast/parser.rb +10 -6
- data/lib/evilution/cli.rb +5 -1
- data/lib/evilution/equivalent/detector.rb +5 -1
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +5 -2
- data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
- data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
- data/lib/evilution/mutator/base.rb +16 -0
- data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
- data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
- data/lib/evilution/mutator/registry.rb +3 -1
- data/lib/evilution/reporter/suggestion.rb +29 -1
- data/lib/evilution/runner.rb +82 -8
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +3 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5cc9d6a79710b327b773b23138b767bb377260031a679d9d3ed1e8fc1308654
|
|
4
|
+
data.tar.gz: 7389805db21c5a7e406d388f517616b6df4b1bc0e139ebd83e907ec1093f9e9f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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":"
|
|
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":"
|
|
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":"
|
|
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":"
|
|
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":"
|
|
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":"
|
|
116
|
-
{"id":"EV-198","title":"Add RSpec suggestion templates for class/module mutations","description":"Add concrete suggestion templates for survived class/module mutations.","status":"
|
|
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":"
|
|
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":"
|
|
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
|
data/lib/evilution/ast/parser.rb
CHANGED
|
@@ -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
|
-
"
|
|
67
|
+
"#{separator}#{node.name}"
|
|
60
68
|
else
|
|
61
|
-
"#{scope}
|
|
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
|
|
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
|
|
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
|
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
data/lib/evilution/version.rb
CHANGED
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.
|
|
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
|