Skip to content

Commit 4114500

Browse files
committed
Upgrade to unparser 0.7.x interface
1 parent 764bb20 commit 4114500

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+349
-310
lines changed

Changelog.md

+23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
# v0.13.0 unreleased
2+
3+
Significant unparser upgrade. Mutant now:
4+
5+
Avoids emitting mutations that do not round trip against unparsers API.
6+
7+
This change generates less mutations, and currently slightly increases boot time.
8+
But the number of mutations that are hitting the test suite is much lower so it
9+
should balance.
10+
11+
Also its a good step towards parallel mutation generation reducing boot times.
12+
13+
Also mutations that are currently not creating round trippable ASTS are removed:
14+
15+
* Negation of `if` conditions, as negating these needs operator precendence sensitive AST
16+
mutations mutant does not have the infrastructure for right now. The mutation to `unless` is
17+
still present so there is no real reduction of semantic coverage.
18+
19+
* All mutations that modify the local variable scope are removed. These generated ASTs that would
20+
not round trip and are thus likely to be covered, execution wise these would move lvar reads to
21+
implicit self receivers in the past. But this was not intended by these mutations, at least not
22+
without explicitly changing the reads to send nodes explicitly.
23+
124
# v0.12.5 unreleased
225

326
* [#1458](https://github.com/mbj/mutant/pull/1458)

Gemfile.lock

+23-22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
PATH
2+
remote: ../unparser
3+
specs:
4+
unparser (0.7.0)
5+
diff-lcs (~> 1.3)
6+
parser (>= 3.3.0)
7+
18
PATH
29
remote: .
310
specs:
@@ -6,60 +13,53 @@ PATH
613
parser (~> 3.3.0)
714
regexp_parser (~> 2.9.0)
815
sorbet-runtime (~> 0.5.0)
9-
unparser (~> 0.6.14)
16+
unparser (~> 0.7.0)
1017

1118
GEM
1219
remote: https://rubygems.org/
1320
specs:
1421
ast (2.4.2)
1522
diff-lcs (1.5.1)
16-
json (2.7.2)
23+
json (2.7.5)
1724
language_server-protocol (3.17.0.3)
18-
parallel (1.25.1)
19-
parser (3.3.2.0)
25+
parallel (1.26.3)
26+
parser (3.3.6.0)
2027
ast (~> 2.4.1)
2128
racc
22-
racc (1.8.0)
29+
racc (1.8.1)
2330
rainbow (3.1.1)
2431
regexp_parser (2.9.2)
25-
rexml (3.2.9)
26-
strscan
2732
rspec (3.13.0)
2833
rspec-core (~> 3.13.0)
2934
rspec-expectations (~> 3.13.0)
3035
rspec-mocks (~> 3.13.0)
31-
rspec-core (3.13.0)
36+
rspec-core (3.13.2)
3237
rspec-support (~> 3.13.0)
33-
rspec-expectations (3.13.0)
38+
rspec-expectations (3.13.3)
3439
diff-lcs (>= 1.2.0, < 2.0)
3540
rspec-support (~> 3.13.0)
36-
rspec-its (1.3.0)
41+
rspec-its (1.3.1)
3742
rspec-core (>= 3.0.0)
3843
rspec-expectations (>= 3.0.0)
39-
rspec-mocks (3.13.1)
44+
rspec-mocks (3.13.2)
4045
diff-lcs (>= 1.2.0, < 2.0)
4146
rspec-support (~> 3.13.0)
4247
rspec-support (3.13.1)
43-
rubocop (1.64.1)
48+
rubocop (1.67.0)
4449
json (~> 2.3)
4550
language_server-protocol (>= 3.17.0)
4651
parallel (~> 1.10)
4752
parser (>= 3.3.0.2)
4853
rainbow (>= 2.2.2, < 4.0)
49-
regexp_parser (>= 1.8, < 3.0)
50-
rexml (>= 3.2.5, < 4.0)
51-
rubocop-ast (>= 1.31.1, < 2.0)
54+
regexp_parser (>= 2.4, < 3.0)
55+
rubocop-ast (>= 1.32.2, < 2.0)
5256
ruby-progressbar (~> 1.7)
5357
unicode-display_width (>= 2.4.0, < 3.0)
54-
rubocop-ast (1.31.3)
58+
rubocop-ast (1.33.0)
5559
parser (>= 3.3.1.0)
5660
ruby-progressbar (1.13.0)
57-
sorbet-runtime (0.5.11422)
58-
strscan (3.1.0)
59-
unicode-display_width (2.5.0)
60-
unparser (0.6.14)
61-
diff-lcs (~> 1.3)
62-
parser (>= 3.3.0)
61+
sorbet-runtime (0.5.11625)
62+
unicode-display_width (2.6.0)
6363

6464
PLATFORMS
6565
ruby
@@ -70,6 +70,7 @@ DEPENDENCIES
7070
rspec-core (~> 3.10)
7171
rspec-its (~> 1.3.0)
7272
rubocop (~> 1.7)
73+
unparser!
7374

7475
BUNDLED WITH
7576
2.5.6

Gemfile.shared

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
gem 'unparser', path: '../unparser'

lib/mutant/cli/command/util.rb

+17-5
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ def action
2020
end
2121
end
2222

23-
@targets.each(&method(:print_mutations))
24-
Either::Right.new(nil)
23+
if @targets.map(&method(:print_mutations)).all?
24+
Either::Right.new(nil)
25+
else
26+
Either::Left.new('Invalid mutation detected!')
27+
end
2528
end
2629

2730
private
@@ -73,15 +76,24 @@ def add_target_options(parser)
7376
def print_mutations(target)
7477
world.stdout.puts(target.identification)
7578

79+
success = true
80+
7681
Mutator::Node.mutate(
7782
config: Mutant::Mutation::Config::DEFAULT.with(ignore_patterns: @ignore_patterns),
7883
node: target.node
7984
).each do |mutation|
80-
Reporter::CLI::Printer::Mutation.call(
81-
object: Mutant::Mutation::Evil.new(subject: target, node: mutation),
82-
output: world.stdout
85+
Mutant::Mutation::Evil.from_node(subject: target, node: mutation).either(
86+
->(violation) { world.stdout.puts(violation.report); success = false },
87+
->(object) {
88+
Reporter::CLI::Printer::Mutation.call(
89+
object:,
90+
output: world.stdout
91+
)
92+
}
8393
)
8494
end
95+
96+
success
8597
end
8698

8799
def parse_remaining_arguments(arguments)

lib/mutant/meta/example.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class Expected
2323
#
2424
# @return [Verification]
2525
def verification
26-
Verification.new(example: self, mutations: generated)
26+
Verification.from_mutations(example: self, mutations: generated)
2727
end
2828
memoize :verification
2929

@@ -61,7 +61,7 @@ def generated
6161
config: Mutation::Config::DEFAULT.with(operators:),
6262
node:
6363
).map do |node|
64-
Mutation::Evil.new(subject: self, node:)
64+
Mutation::Evil.from_node(subject: self, node:)
6565
end
6666
end
6767
memoize :generated

lib/mutant/meta/example/verification.rb

+30-16
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,24 @@ module Meta
55
class Example
66
# Example verification
77
class Verification
8-
include Adamantium, Anima.new(:example, :mutations)
8+
include Adamantium, Anima.new(:example, :invalid, :valid)
9+
10+
def self.from_mutations(example:, mutations:)
11+
valid, invalid = [], []
12+
13+
mutations.each do |mutation|
14+
mutation.either(invalid.public_method(:<<), valid.public_method(:<<))
15+
end
16+
17+
new(example:, invalid:, valid:)
18+
end
919

1020
# Test if mutation was verified successfully
1121
#
1222
# @return [Boolean]
1323
def success?
1424
[
15-
original_verification,
25+
original_verification_report,
1626
invalid,
1727
missing,
1828
no_diffs,
@@ -29,12 +39,12 @@ def error_report
2939

3040
def reports
3141
reports = [example.location]
32-
reports.concat(original)
33-
reports.concat(original_verification)
42+
reports.concat(original_report)
43+
reports.concat(original_verification_report)
3444
reports.concat(make_report('Missing mutations:', missing))
3545
reports.concat(make_report('Unexpected mutations:', unexpected))
3646
reports.concat(make_report('No-Diff mutations:', no_diffs))
37-
reports.concat(invalid)
47+
reports.concat(invalid_report)
3848
end
3949

4050
def make_report(label, mutations)
@@ -52,15 +62,15 @@ def report_mutation(mutation)
5262
]
5363
end
5464

55-
def original
65+
def original_report
5666
[
5767
"Original: (operators: #{example.operators.class.operators_name})",
5868
example.node,
5969
example.original_source
6070
]
6171
end
6272

63-
def original_verification
73+
def original_verification_report
6474
validation = Unparser::Validation.from_string(example.original_source)
6575
if validation.success?
6676
[]
@@ -77,30 +87,34 @@ def prefix(prefix, string)
7787
end.join
7888
end
7989

80-
def invalid
81-
mutations.each_with_object([]) do |mutation, aggregate|
82-
validation = Unparser::Validation.from_node(mutation.node)
83-
aggregate << prefix('[invalid-mutation]', validation.report) unless validation.success?
90+
def invalid_report
91+
invalid.map do |validation|
92+
prefix('[invalid-mutation]', validation.report)
8493
end
8594
end
86-
memoize :invalid
95+
memoize :invalid_report
8796

8897
def unexpected
89-
mutations.reject do |mutation|
98+
valid.reject do |mutation|
9099
example.expected.any? { |expected| expected.node.eql?(mutation.node) }
91100
end
92101
end
93102
memoize :unexpected
94103

95104
def missing
96-
(example.expected.map(&:node) - mutations.map(&:node)).map do |node|
97-
Mutation::Evil.new(subject: example, node:)
105+
example.expected.each_with_object([]) do |expected, aggregate|
106+
next if valid.any? { |mutation| expected.node.eql?(mutation.node) }
107+
aggregate << Mutation::Evil.new(
108+
node: expected.node,
109+
source: expected.original_source,
110+
subject: example
111+
)
98112
end
99113
end
100114
memoize :missing
101115

102116
def no_diffs
103-
mutations.select { |mutation| mutation.source.eql?(example.original_source_generated) }
117+
valid.select { |mutation| mutation.source.eql?(example.original_source_generated) }
104118
end
105119
memoize :no_diffs
106120

lib/mutant/mutation.rb

+59-13
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,69 @@ module Mutant
44
# Represent a mutated node with its subject
55
class Mutation
66
include AbstractType, Adamantium
7-
include Anima.new(:subject, :node)
7+
include Anima.new(:subject, :node, :source)
88

99
CODE_DELIMITER = "\0"
1010
CODE_RANGE = (..4)
1111

12+
class GenerationError
13+
include Anima.new(:subject, :node, :unparser_violation)
14+
15+
MESSAGE = <<~'MESSAGE'
16+
=== Mutation-Generation-Error ===
17+
This is a mutant internal issue detected by a mutant internal cross check.
18+
Please report an issue with the details below.
19+
20+
Subject: %<subject_identification>s.
21+
22+
Mutation-Source-Diff:
23+
%<mutation_source_diff>s
24+
25+
Mutation-Node-Diff:
26+
%<mutation_node_diff>s
27+
28+
Unparser-Violation:
29+
%<unparser_violation>s
30+
MESSAGE
31+
32+
def report
33+
MESSAGE % {
34+
mutation_source_diff:,
35+
mutation_node_diff:,
36+
subject_identification: subject.identification,
37+
unparser_violation: unparser_violation.report
38+
}
39+
end
40+
41+
private
42+
43+
def mutation_source_diff
44+
mutation = Mutation::Evil.new(
45+
subject:,
46+
node:,
47+
source: unparser_violation.original_source.from_right
48+
)
49+
50+
mutation.diff.colorized_diff
51+
end
52+
53+
def mutation_node_diff
54+
Unparser::Diff.new(
55+
subject.node.to_s.lines.map(&:chomp),
56+
node.to_s.lines.map(&:chomp)
57+
).colorized_diff
58+
end
59+
end # GenerationError
60+
61+
def self.from_node(subject:, node:)
62+
ast = Unparser::AST.from_node(node:)
63+
64+
Unparser
65+
.unparse_validate_ast_either(ast:)
66+
.lmap { |unparser_violation| GenerationError.new(subject:, node:, unparser_violation:) }
67+
.fmap { |source| new(node:, source:, subject:) }
68+
end
69+
1270
# Mutation identification code
1371
#
1472
# @return [String]
@@ -17,14 +75,6 @@ def code
1775
end
1876
memoize :code
1977

20-
# Normalized mutation source
21-
#
22-
# @return [String]
23-
def source
24-
Unparser.unparse(node)
25-
end
26-
memoize :source
27-
2878
# Identification string
2979
#
3080
# @return [String]
@@ -75,10 +125,6 @@ def insert(kernel)
75125
end
76126
end
77127

78-
# Rendered mutation diff
79-
#
80-
# @return [String, nil]
81-
# the diff, if present
82128
def diff
83129
Unparser::Diff.build(original_source, source)
84130
end

lib/mutant/mutator/node/and_asgn.rb

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ class AndAsgn < self
1414
private
1515

1616
def dispatch
17-
emit_singletons
1817
emit_left_mutations do |node|
1918
!n_self?(node)
2019
end

0 commit comments

Comments
 (0)