Skip to content

Conversation

@0xLoopTheory
Copy link
Contributor

Fixes #480.

Summary

This change adds the same human-readable mutant name string that
cargo mutants --list prints to the JSON output:

  • cargo mutants --list --json
  • mutants.out/mutants.json
  • mutants.out/outcomes.json

The goal is to make it easier for tools to work with a stable,
human-readable identifier for each mutant, without needing to reconstruct
it from other fields.

Implementation details

  • Add a name: String field to Mutant:

    /// A precomputed human-readable name for this mutant, including the file,
    /// location, and change description.
    ///
    /// This is used in CLI output, filtering, and JSON.
    pub name: String,
  • After discovery, compute the canonical name once, including line/col,
    reusing the existing Mutant::name(true) formatting:

    // in walk_tree(...)
    for mutant in &mut mutants {
        mutant.name = mutant.name(true);
    }
  • Extend the custom Serialize for Mutant implementation to include the
    name field so it appears in:

    • --list --json
    • mutants.out/mutants.json
  • Make the same name available in outcomes.json entries so tools can
    join outcomes back to mutants by this human-readable identifier.

  • CLI behavior is unchanged:

    • Mutant::name(show_line_col: bool) and
      Mutant::to_styled_string(show_line_col: bool) are kept as-is and
      still used for textual output.
    • The stored name field follows the same format as the existing
      name(true) output (file path, line, column, description).

This is an additive JSON schema change: existing fields are preserved and
a new name string is added.

Testing

  • cargo test (workspace) – all tests pass.

  • Updated insta snapshot tests to account for the new name field in
    JSON output:

    • list_mutants_in_cfg_attr_test_skip_json
    • list_mutants_in_factorial_json
    • list_mutants_json_well_tested
    • list_mutants_regex_filters_json
    • uncaught_mutant_in_factorial
  • Manual verification on a small test project:

    • Running cargo mutants --output mutants.out produces
      mutants.out/mutants.json with a name for each mutant, matching
      the --list output.
    • mutants.out/outcomes.json also includes the same name for each
      outcome entry.

src/visit.rs Outdated
/// Record that we generated some mutants.
fn collect_mutant(&mut self, span: Span, replacement: &TokenStream, genre: Genre) {
self.mutants.push(Mutant {
name: String::new(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems sub-optimal to set it to "" in the constructor and fill it in later...?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you and actually had the same thoughts while implementing this.
Initializing name as an empty string and then filling it later felt a bit off to me as well.

I tried to think through the options and currently see three possible directions — maybe you see more, and I’d really appreciate your input before I change the PR.


1. What the PR currently does (compute name in walk_tree)

Right now, I construct all Mutant values with name: String::new() in the visitor, and then later in walk_tree I do:

for mutant in &mut mutants {
    mutant.name = mutant.name(true);
}

This has the advantage that there is a single place where the canonical name (with line/col) is set, without duplicating the naming logic. But I agree with you that it’s a bit sub‑optimal to have an invalid “empty” name during the lifetime of the Mutant and then patch it up later.


2. Compute name immediately at each construction site

An alternative would be to compute the canonical name right after constructing each Mutant in the visitor, for example in collect_mutant:

/// Record that we generated some mutants.
fn collect_mutant(&mut self, span: Span, replacement: &TokenStream, genre: Genre) {
    let mut mutant = Mutant {
        name: String::new(),
        source_file: self.source_file.clone(),
        function: self.fn_stack.last().cloned(),
        span,
        short_replaced: None,
        replacement: replacement.to_pretty_string(),
        genre,
        target: None,
    };
    // Needs a fully-initialized `Mutant`, so done as a second step.
    mutant.name = mutant.name(true);
    self.mutants.push(mutant);
}

and apply the same pattern to the two other places where we construct a Mutant (for match arms and struct fields).

Because Mutant::name(true) needs &self and uses several fields (source_file, span, function, etc.), it can’t be called inside the struct literal itself — we need a constructed Mutant first. So this two‑step (“construct then compute name”) seems unavoidable somewhere.

With this approach:

  • Every Mutant has a valid name from the moment it’s stored in self.mutants.
  • We can remove the loop in walk_tree that recomputes names.
  • The name formatting logic stays centralized in Mutant::name (no duplication of the string‑assembly code).

This is my current favourite because it’s relatively small and keeps the semantics clear: “discovery creates fully‑named mutants.”


3. A helper constructor like Mutant::new_discovered

A slightly more structured variant of (2) would be to introduce a helper constructor that encapsulates the two‑step process:

impl Mutant {
    /// Construct a mutant discovered while walking source code.
    ///
    /// This computes and stores a canonical human‑readable name that includes
    /// the file path, line/column, and change description. The name is used
    /// for filtering, reporting, and JSON output.
    pub fn new_discovered(
        source_file: SourceFile,
        function: Option<Arc<Function>>,
        span: Span,
        short_replaced: Option<String>,
        replacement: String,
        genre: Genre,
        target: Option<MutationTarget>,
    ) -> Self {
        let mut mutant = Mutant {
            name: String::new(),
            source_file,
            function,
            span,
            short_replaced,
            replacement,
            genre,
            target,
        };
        mutant.name = mutant.name(true);
        mutant
    }
}

Then the visitor sites would call:

self.mutants.push(Mutant::new_discovered(
    self.source_file.clone(),
    self.fn_stack.last().cloned(),
    span,
    None,
    replacement.to_pretty_string(),
    genre,
    None,
));

and similar for match arms and struct fields (with appropriate short_replaced / target).

This avoids repeating the “construct then compute name” pattern at each call site, at the cost of one more constructor‑like function on Mutant. I’m not sure if you’d consider that over‑abstracted for this codebase or acceptable.


Question / next step

Given these options, I’d be happy to rework the PR accordingly. My slight preference is option 2 (compute name immediately at the construction sites and remove the loop in walk_tree), but I’m also happy to go with option 3 if you’d rather keep the construction logic inside Mutant itself.

If you have a different pattern in mind that fits better with how you’d like Mutant to evolve, I’d be very interested to hear it and adjust the PR. 😊

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! It seems the third option is the best for now, because this confines it inside the Mutant.

Perhaps later the code in Mutant::name should be refactored to run at construction time and not need an instance, but I guess that's a little complex because of the variations for generating styled text and optionally including the line/column. Let's leave that out of this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for the quick feedback and the clear direction.

I’ve updated the PR to follow option 3 as you suggested:

  • Introduced Mutant::new_discovered(...) which:
    • Takes the fields needed to construct a Mutant.
    • Builds the struct.
    • Immediately computes and stores the canonical human-readable name using name(true).
  • Scoped the constructor as pub(crate) so it’s only used within the crate and remains internal implementation detail.

For consistency and readability, I also updated all three discovery sites to use the same pattern:

let mutant = Mutant::new_discovered(
    self.source_file.clone(),
    self.fn_stack.last().cloned(),
    span,
    short_replaced,
    replacement,
    genre,
    target,
);
self.mutants.push(mutant);

Concretely, this now applies to:

  • The generic collect_mutant helper (binary/unary ops, guards, etc.).
  • Match arm deletion mutants.
  • Struct field deletion mutants (when there is a base expression like ..Default::default()).

As a result:

  • Every Mutant discovered by the visitor has a fully initialized name as soon as it’s created.
  • There is no longer a post-processing loop in walk_tree that mutates the name field afterwards.
  • The naming logic is still centralized in Mutant::name, and the JSON serialization simply includes the stored name.

Verification

After the refactor I re-ran:

  • cargo test (full test suite) – all tests pass.
  • Manual checks on a small sample project:
    • cargo mutants --list --json to confirm the name field appears in the list JSON with the same human-readable string as the CLI output.
    • cargo mutants and inspection of mutants.json and outcomes.json to confirm both contain the name field and that the value matches the printed mutant name.

If you’d like me to tweak the constructor signature, visibility, or documentation style, I’m happy to adjust. Thanks again for the guidance – this was a fun one to work on!

Copy link
Owner

@sourcefrog sourcefrog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good aside from this one thing.

@sourcefrog sourcefrog merged commit 9b31451 into sourcefrog:main Dec 10, 2025
27 checks passed
@0xLoopTheory 0xLoopTheory deleted the add-mutant-name-json branch December 10, 2025 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Include mutant string names in json representation

2 participants