Class: Mutineer::Mutators::ChainLink

Inherits:
Base
  • Object
show all
Defined in:
lib/mutineer/mutators/chain_link.rb

Overview

Chain-link mutator (Tier-2).

Drops one dotted call from a chain of calls, with its arguments and block: user.account.owner.name becomes user.owner.name and user.account.name. The chain's receiver and its final call stay, so a chain of n dotted calls gives at most n - 1 mutations. A chain begins at a receiver that is not a dotted call: a local, a constant, self, list[i], (a + b). A call in an argument or a block starts a chain of its own.

The mutant survives when no test tells the chain apart from the same chain without that step: a scope, a filter or a lookup the tests never see.

Links in SKIPPED are never dropped. On a value that already has the target type or needs no copy, a conversion (name.to_s.strip) or a copy (list.dup.sort) is a no-op, so dropping it most often makes an equivalent mutant. Dropping new sends the next call to the class, which raises (killed by any test that runs the line) or reaches a class method that builds the instance itself (equivalent); neither says anything about the tests.

Constant Summary collapse

SKIPPED =

Method names whose link is never dropped: core conversions and copies, plus new.

%i[
  to_s to_str to_sym to_i to_int to_f to_r to_c to_a to_ary to_h to_hash to_proc to_set
  dup clone freeze itself
  new
].freeze

Instance Method Summary collapse

Methods inherited from Base

#heredoc?

Instance Method Details

#chain(top) ⇒ void (private)

This method returns an undefined value.

Walks a final call's receiver chain, recording each link so it is not taken for the final call of a chain of its own, and dropping each link not in SKIPPED.

Parameters:

  • top (Prism::Node) —

    the chain's final call; responds to receiver.



106
107
108
109
110
111
112
113
# File 'lib/mutineer/mutators/chain_link.rb', line 106

def chain(top)
  link = top.receiver
  while dotted?(link)
    @links[link] = true
    drop(link) unless SKIPPED.include?(link.name)
    link = link.receiver
  end
end

#dotted?(node) ⇒ Boolean (private)

Returns whether a node is a call through ., &. or ::.

Parameters:

  • node (Prism::Node, nil) —

    node to inspect.

Returns:

  • (Boolean) —

    true for a call node with a call operator.



134
135
136
# File 'lib/mutineer/mutators/chain_link.rb', line 134

def dotted?(node)
  node.is_a?(Prism::CallNode) && !node.call_operator_loc.nil?
end

#drop(link) ⇒ void (private)

This method returns an undefined value.

Emits the removal of one link: from the end of its receiver to the end of its arguments and block, so a chain split across lines keeps the layout of the lines that remain.

Parameters:

  • link (Prism::CallNode) —

    the dotted call to drop.



121
122
123
124
125
126
127
128
# File 'lib/mutineer/mutators/chain_link.rb', line 121

def drop(link)
  @mutations << Mutation.new(
    start_offset: link.receiver.location.end_offset,
    end_offset: link.location.end_offset,
    replacement: "",
    operator: :chain_link
  )
end

#mutations_for(subject, source) ⇒ Array<Mutineer::Mutation>

Resets the per-subject record of calls already placed in a chain.

Parameters:

  • subject (Mutineer::Subject) —

    subject whose body is visited.

  • source (String) —

    full source text for byte-based slicing.

Returns:



40
41
42
43
# File 'lib/mutineer/mutators/chain_link.rb', line 40

def mutations_for(subject, source)
  @links = {}.compare_by_identity
  super
end

#visit_call_and_write_node(node) ⇒ void

This method returns an undefined value.

Visits a.b.c &&= 1.

Parameters:

  • node (Prism::CallAndWriteNode) —

    node to inspect.



77
78
79
80
# File 'lib/mutineer/mutators/chain_link.rb', line 77

def visit_call_and_write_node(node)
  chain(node)
  super
end

#visit_call_node(node) ⇒ void

This method returns an undefined value.

Visits a call. The outermost dotted call of a chain is its final call; the dotted calls below it in the receiver are its links.

Parameters:

  • node (Prism::CallNode) —

    call node to inspect.



50
51
52
53
# File 'lib/mutineer/mutators/chain_link.rb', line 50

def visit_call_node(node)
  chain(node) if dotted?(node) && !@links.key?(node)
  super
end

#visit_call_operator_write_node(node) ⇒ void

This method returns an undefined value.

Visits a.b.c += 1, whose final call Prism parses as its own node.

Parameters:

  • node (Prism::CallOperatorWriteNode) —

    node to inspect.



59
60
61
62
# File 'lib/mutineer/mutators/chain_link.rb', line 59

def visit_call_operator_write_node(node)
  chain(node)
  super
end

#visit_call_or_write_node(node) ⇒ void

This method returns an undefined value.

Visits a.b.c ||= 1.

Parameters:

  • node (Prism::CallOrWriteNode) —

    node to inspect.



68
69
70
71
# File 'lib/mutineer/mutators/chain_link.rb', line 68

def visit_call_or_write_node(node)
  chain(node)
  super
end

#visit_call_target_node(node) ⇒ void

This method returns an undefined value.

Visits a call used as an assignment target, as in a.b.c, d = 1, 2.

Parameters:

  • node (Prism::CallTargetNode) —

    node to inspect.



86
87
88
89
# File 'lib/mutineer/mutators/chain_link.rb', line 86

def visit_call_target_node(node)
  chain(node)
  super
end

#visit_def_node(node) ⇒ void

This method returns an undefined value.

Nested method definitions are discovered as their own subjects; do not recurse into them (prevents double-counting their chains).

Parameters:

  • node (Prism::DefNode) —

    nested definition node.



96
# File 'lib/mutineer/mutators/chain_link.rb', line 96

def visit_def_node(node); end