Module: Mutineer::MutantId

Defined in:
lib/mutineer/mutant_id.rb

Overview

Content-based stable id for a mutant — NOT byte offsets. Pure function, reused by the Runner (matching the ignore list), the Reporter (emitting a copy- pasteable id per survivor), and #13 baseline gating (diffing id-sets run to run). digest is stdlib, so zero new deps.

Offset-free by design: keyed on the subject's qualified_name (a method, not a byte position) + operator + the normalized mutated token + an occurrence ordinal among same-(operator, token) twins WITHIN the subject. So it survives any edit outside the subject method — where raw start/end offsets shift on every edit earlier in the file and would silently stop matching.

Class Method Summary collapse

Class Method Details

.for(subject, mutation, source, occurrence = 0) ⇒ String

Computes the stable id for a single mutant.

NUL-joined so token delimiters (||=, spaces, ::, #) can never collide with the separator; SHA256 gives a fixed-length, copy-pasteable key.

Parameters:

  • subject (Mutineer::Subject)

    the subject (method) the mutant lives in; its qualified_name anchors the id to a method rather than a byte position.

  • mutation (Mutineer::Mutation)

    the atomic edit whose operator is hashed.

  • source (String)

    the full, unmutated source the mutation indexes into.

  • occurrence (Integer) (defaults to: 0)

    0-based ordinal among twins sharing the same (operator, token) within the subject, disambiguating otherwise-identical mutants.

Returns:

  • (String)

    a 12-character hex id, stable across edits outside the subject.



31
32
33
34
35
36
# File 'lib/mutineer/mutant_id.rb', line 31

def for(subject, mutation, source, occurrence = 0)
  Digest::SHA256.hexdigest(
    [subject.qualified_name, mutation.operator,
     normalized_token(mutation, source), occurrence].join("\x00")
  )[0, 12]
end

.for_subject(subject, source, mutations) ⇒ Array<String>

Computes ids for a subject's full mutation list, in input order, assigning each its 0-based occurrence among twins sharing the same (operator, token). This is what disambiguates a + b + c's two + mutants without an offset.

Parameters:

  • subject (Mutineer::Subject)

    the subject the mutations belong to.

  • source (String)

    the full, unmutated source for token normalization.

  • mutations (Array<Mutineer::Mutation>)

    the subject's mutations, in order.

Returns:

  • (Array<String>)

    one 12-character id per mutation, positionally aligned.



46
47
48
49
50
51
52
53
54
# File 'lib/mutineer/mutant_id.rb', line 46

def for_subject(subject, source, mutations)
  seen = Hash.new(0)
  mutations.map do |m|
    key = [m.operator, normalized_token(m, source)]
    occ = seen[key]
    seen[key] += 1
    self.for(subject, m, source, occ)
  end
end

.normalized_token(mutation, source) ⇒ String

Extracts the exact code being mutated, whitespace-collapsed — the same normalization the Reporter's diff_for uses for its token label.

Parameters:

  • mutation (Mutineer::Mutation)

    supplies the byte range to slice.

  • source (String)

    the source to byteslice (byte offsets, never char).

Returns:

  • (String)

    the mutated token with runs of whitespace collapsed to one space.



62
63
64
# File 'lib/mutineer/mutant_id.rb', line 62

def normalized_token(mutation, source)
  source.byteslice(mutation.start_offset...mutation.end_offset).gsub(/\s+/, " ").strip
end