Summary
Engine.GenerateMutants does not produce unique mutant IDs. The same ID is handed to different mutants of the same file, pervasively — 87 of this repository's own 123 source files generate at least one repeated ID, 4530 duplicates across 21754 mutants.
Detail
internal/mutation/engine.go:137:
for i := range mutants {
mutants[i].FilePath = filePath
mutants[i].ID = fmt.Sprintf("%s_%d", filePath, len(allMutants)+i)
// Only add mutant if it passes type check
if typeChecker == nil || typeChecker.IsValidMutation(node, mutants[i]) {
allMutants = append(allMutants, mutants[i])
}
}
The ID combines len(allMutants) — re-evaluated on every iteration, and growing as the loop appends — with i, the candidate index within the same loop. Each accepted mutant is thus counted twice, so numbering advances in steps of two within a batch and then repeats values once the next batch resumes from the true slice length.
Type-check rejections skew it in the other direction: a rejected candidate still consumes an i without growing allMutants.
Minimal example — a nine-line file yields three collisions:
duplicate mutant ID test.go_3: 4:2 "n == 0"->"false" and 4:5 "=="->"!="
duplicate mutant ID test.go_9: 4:5 "=="->">" and 4:12 "{...}"->"{}"
duplicate mutant ID test.go_11: 4:5 "=="->">=" and 8:2 "false"->"true"
Impact
Nothing in gomu currently keys off the ID — history is keyed by file path and gated on file and test hashes (internal/history/store.go), and the report prints position, not ID. So this is a data-quality bug rather than a behavioural one:
Suggested fix
Assign the ID after the type check, from the number of mutants kept so far, so the number is unique and gapless per file. Since nothing reads the ID, no history or report compatibility handling is needed.
Summary
Engine.GenerateMutantsdoes not produce unique mutant IDs. The same ID is handed to different mutants of the same file, pervasively — 87 of this repository's own 123 source files generate at least one repeated ID, 4530 duplicates across 21754 mutants.Detail
internal/mutation/engine.go:137:The ID combines
len(allMutants)— re-evaluated on every iteration, and growing as the loop appends — withi, the candidate index within the same loop. Each accepted mutant is thus counted twice, so numbering advances in steps of two within a batch and then repeats values once the next batch resumes from the true slice length.Type-check rejections skew it in the other direction: a rejected candidate still consumes an
iwithout growingallMutants.Minimal example — a nine-line file yields three collisions:
Impact
Nothing in gomu currently keys off the ID — history is keyed by file path and gated on file and test hashes (
internal/history/store.go), and the report prints position, not ID. So this is a data-quality bug rather than a behavioural one:.gomu_history.jsonand the JSON report both serialise"id", so any external consumer that treats it as an identifier will silently conflate mutants.internal/execution/overlay.gounsafe, since that code did key off the ID — see Parallel workers inflate the mutation score: overlay directories collide on non-unique mutant IDs #1. That symptom is fixed independently.Suggested fix
Assign the ID after the type check, from the number of mutants kept so far, so the number is unique and gapless per file. Since nothing reads the ID, no history or report compatibility handling is needed.