Skip to content

Commit

Permalink
Upgrade to unparser 0.7.x interface
Browse files Browse the repository at this point in the history
  • Loading branch information
mbj committed Jan 14, 2025
1 parent 764bb20 commit 4114500
Show file tree
Hide file tree
Showing 57 changed files with 349 additions and 310 deletions.
23 changes: 23 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
# v0.13.0 unreleased

Significant unparser upgrade. Mutant now:

Avoids emitting mutations that do not round trip against unparsers API.

This change generates less mutations, and currently slightly increases boot time.
But the number of mutations that are hitting the test suite is much lower so it
should balance.

Also its a good step towards parallel mutation generation reducing boot times.

Also mutations that are currently not creating round trippable ASTS are removed:

* Negation of `if` conditions, as negating these needs operator precendence sensitive AST
mutations mutant does not have the infrastructure for right now. The mutation to `unless` is
still present so there is no real reduction of semantic coverage.

* All mutations that modify the local variable scope are removed. These generated ASTs that would
not round trip and are thus likely to be covered, execution wise these would move lvar reads to
implicit self receivers in the past. But this was not intended by these mutations, at least not
without explicitly changing the reads to send nodes explicitly.

# v0.12.5 unreleased

* [#1458](https://github.com/mbj/mutant/pull/1458)
Expand Down
45 changes: 23 additions & 22 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
PATH
remote: ../unparser
specs:
unparser (0.7.0)
diff-lcs (~> 1.3)
parser (>= 3.3.0)

PATH
remote: .
specs:
Expand All @@ -6,60 +13,53 @@ PATH
parser (~> 3.3.0)
regexp_parser (~> 2.9.0)
sorbet-runtime (~> 0.5.0)
unparser (~> 0.6.14)
unparser (~> 0.7.0)

GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
diff-lcs (1.5.1)
json (2.7.2)
json (2.7.5)
language_server-protocol (3.17.0.3)
parallel (1.25.1)
parser (3.3.2.0)
parallel (1.26.3)
parser (3.3.6.0)
ast (~> 2.4.1)
racc
racc (1.8.0)
racc (1.8.1)
rainbow (3.1.1)
regexp_parser (2.9.2)
rexml (3.2.9)
strscan
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.0)
rspec-core (3.13.2)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-its (1.3.0)
rspec-its (1.3.1)
rspec-core (>= 3.0.0)
rspec-expectations (>= 3.0.0)
rspec-mocks (3.13.1)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.1)
rubocop (1.64.1)
rubocop (1.67.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3)
rubocop-ast (1.33.0)
parser (>= 3.3.1.0)
ruby-progressbar (1.13.0)
sorbet-runtime (0.5.11422)
strscan (3.1.0)
unicode-display_width (2.5.0)
unparser (0.6.14)
diff-lcs (~> 1.3)
parser (>= 3.3.0)
sorbet-runtime (0.5.11625)
unicode-display_width (2.6.0)

PLATFORMS
ruby
Expand All @@ -70,6 +70,7 @@ DEPENDENCIES
rspec-core (~> 3.10)
rspec-its (~> 1.3.0)
rubocop (~> 1.7)
unparser!

BUNDLED WITH
2.5.6
1 change: 1 addition & 0 deletions Gemfile.shared
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gem 'unparser', path: '../unparser'
22 changes: 17 additions & 5 deletions lib/mutant/cli/command/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ def action
end
end

@targets.each(&method(:print_mutations))
Either::Right.new(nil)
if @targets.map(&method(:print_mutations)).all?
Either::Right.new(nil)
else
Either::Left.new('Invalid mutation detected!')
end
end

private
Expand Down Expand Up @@ -73,15 +76,24 @@ def add_target_options(parser)
def print_mutations(target)
world.stdout.puts(target.identification)

success = true

Mutator::Node.mutate(
config: Mutant::Mutation::Config::DEFAULT.with(ignore_patterns: @ignore_patterns),
node: target.node
).each do |mutation|
Reporter::CLI::Printer::Mutation.call(
object: Mutant::Mutation::Evil.new(subject: target, node: mutation),
output: world.stdout
Mutant::Mutation::Evil.from_node(subject: target, node: mutation).either(
->(violation) { world.stdout.puts(violation.report); success = false },
->(object) {
Reporter::CLI::Printer::Mutation.call(
object:,
output: world.stdout
)
}
)
end

success
end

def parse_remaining_arguments(arguments)
Expand Down
4 changes: 2 additions & 2 deletions lib/mutant/meta/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Expected
#
# @return [Verification]
def verification
Verification.new(example: self, mutations: generated)
Verification.from_mutations(example: self, mutations: generated)
end
memoize :verification

Expand Down Expand Up @@ -61,7 +61,7 @@ def generated
config: Mutation::Config::DEFAULT.with(operators:),
node:
).map do |node|
Mutation::Evil.new(subject: self, node:)
Mutation::Evil.from_node(subject: self, node:)
end
end
memoize :generated
Expand Down
46 changes: 30 additions & 16 deletions lib/mutant/meta/example/verification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@ module Meta
class Example
# Example verification
class Verification
include Adamantium, Anima.new(:example, :mutations)
include Adamantium, Anima.new(:example, :invalid, :valid)

def self.from_mutations(example:, mutations:)
valid, invalid = [], []

mutations.each do |mutation|
mutation.either(invalid.public_method(:<<), valid.public_method(:<<))
end

new(example:, invalid:, valid:)
end

# Test if mutation was verified successfully
#
# @return [Boolean]
def success?
[
original_verification,
original_verification_report,
invalid,
missing,
no_diffs,
Expand All @@ -29,12 +39,12 @@ def error_report

def reports
reports = [example.location]
reports.concat(original)
reports.concat(original_verification)
reports.concat(original_report)
reports.concat(original_verification_report)
reports.concat(make_report('Missing mutations:', missing))
reports.concat(make_report('Unexpected mutations:', unexpected))
reports.concat(make_report('No-Diff mutations:', no_diffs))
reports.concat(invalid)
reports.concat(invalid_report)
end

def make_report(label, mutations)
Expand All @@ -52,15 +62,15 @@ def report_mutation(mutation)
]
end

def original
def original_report
[
"Original: (operators: #{example.operators.class.operators_name})",
example.node,
example.original_source
]
end

def original_verification
def original_verification_report
validation = Unparser::Validation.from_string(example.original_source)
if validation.success?
[]
Expand All @@ -77,30 +87,34 @@ def prefix(prefix, string)
end.join
end

def invalid
mutations.each_with_object([]) do |mutation, aggregate|
validation = Unparser::Validation.from_node(mutation.node)
aggregate << prefix('[invalid-mutation]', validation.report) unless validation.success?
def invalid_report
invalid.map do |validation|
prefix('[invalid-mutation]', validation.report)
end
end
memoize :invalid
memoize :invalid_report

def unexpected
mutations.reject do |mutation|
valid.reject do |mutation|
example.expected.any? { |expected| expected.node.eql?(mutation.node) }
end
end
memoize :unexpected

def missing
(example.expected.map(&:node) - mutations.map(&:node)).map do |node|
Mutation::Evil.new(subject: example, node:)
example.expected.each_with_object([]) do |expected, aggregate|
next if valid.any? { |mutation| expected.node.eql?(mutation.node) }
aggregate << Mutation::Evil.new(
node: expected.node,
source: expected.original_source,
subject: example
)
end
end
memoize :missing

def no_diffs
mutations.select { |mutation| mutation.source.eql?(example.original_source_generated) }
valid.select { |mutation| mutation.source.eql?(example.original_source_generated) }
end
memoize :no_diffs

Expand Down
72 changes: 59 additions & 13 deletions lib/mutant/mutation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,69 @@ module Mutant
# Represent a mutated node with its subject
class Mutation
include AbstractType, Adamantium
include Anima.new(:subject, :node)
include Anima.new(:subject, :node, :source)

CODE_DELIMITER = "\0"
CODE_RANGE = (..4)

class GenerationError
include Anima.new(:subject, :node, :unparser_violation)

MESSAGE = <<~'MESSAGE'
=== Mutation-Generation-Error ===
This is a mutant internal issue detected by a mutant internal cross check.
Please report an issue with the details below.
Subject: %<subject_identification>s.
Mutation-Source-Diff:
%<mutation_source_diff>s
Mutation-Node-Diff:
%<mutation_node_diff>s
Unparser-Violation:
%<unparser_violation>s
MESSAGE

def report
MESSAGE % {
mutation_source_diff:,
mutation_node_diff:,
subject_identification: subject.identification,
unparser_violation: unparser_violation.report
}
end

private

def mutation_source_diff
mutation = Mutation::Evil.new(
subject:,
node:,
source: unparser_violation.original_source.from_right
)

mutation.diff.colorized_diff
end

def mutation_node_diff
Unparser::Diff.new(
subject.node.to_s.lines.map(&:chomp),
node.to_s.lines.map(&:chomp)
).colorized_diff
end
end # GenerationError

def self.from_node(subject:, node:)
ast = Unparser::AST.from_node(node:)

Unparser
.unparse_validate_ast_either(ast:)
.lmap { |unparser_violation| GenerationError.new(subject:, node:, unparser_violation:) }
.fmap { |source| new(node:, source:, subject:) }
end

# Mutation identification code
#
# @return [String]
Expand All @@ -17,14 +75,6 @@ def code
end
memoize :code

# Normalized mutation source
#
# @return [String]
def source
Unparser.unparse(node)
end
memoize :source

# Identification string
#
# @return [String]
Expand Down Expand Up @@ -75,10 +125,6 @@ def insert(kernel)
end
end

# Rendered mutation diff
#
# @return [String, nil]
# the diff, if present
def diff
Unparser::Diff.build(original_source, source)
end
Expand Down
1 change: 0 additions & 1 deletion lib/mutant/mutator/node/and_asgn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class AndAsgn < self
private

def dispatch
emit_singletons
emit_left_mutations do |node|
!n_self?(node)
end
Expand Down
Loading

0 comments on commit 4114500

Please sign in to comment.