///|
pub(all) enum MutationKind {
BooleanLiteral
EqualityOperator
RelationalOperator
ArithmeticOperator
LogicalOperator
NumericLiteral
} derive(Debug, Eq, ToJson, FromJson)
///|
pub(all) struct MutationRule {
kind : MutationKind
from : String
to : String
label : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct SourceSpan {
file : String
start : Int
end : Int
line : Int
column : Int
} derive(Debug, Eq, ToJson)
///|
pub(all) struct MutationCandidate {
id : Int
rule : MutationRule
span : SourceSpan
original : String
replacement : String
line_text : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct MutationSummary {
file : String
candidate_count : Int
boolean_count : Int
equality_count : Int
relational_count : Int
arithmetic_count : Int
logical_count : Int
numeric_count : Int
} derive(Debug, Eq, ToJson)
///|
pub fn default_rules() -> Array[MutationRule] {
[
{ kind: EqualityOperator, from: "==", to: "!=", label: "eq-to-ne" },
{ kind: EqualityOperator, from: "!=", to: "==", label: "ne-to-eq" },
{ kind: RelationalOperator, from: "<=", to: "<", label: "le-to-lt" },
{ kind: RelationalOperator, from: "<", to: "<=", label: "lt-to-le" },
{ kind: RelationalOperator, from: ">=", to: ">", label: "ge-to-gt" },
{ kind: RelationalOperator, from: ">", to: ">=", label: "gt-to-ge" },
{ kind: ArithmeticOperator, from: "+", to: "-", label: "add-to-sub" },
{ kind: ArithmeticOperator, from: "-", to: "+", label: "sub-to-add" },
{ kind: ArithmeticOperator, from: "*", to: "/", label: "mul-to-div" },
{ kind: ArithmeticOperator, from: "/", to: "*", label: "div-to-mul" },
{ kind: LogicalOperator, from: "&&", to: "||", label: "and-to-or" },
{ kind: LogicalOperator, from: "||", to: "&&", label: "or-to-and" },
{ kind: BooleanLiteral, from: "true", to: "false", label: "true-to-false" },
{ kind: BooleanLiteral, from: "false", to: "true", label: "false-to-true" },
]
}
///|
pub fn discover(
source : String,
file? : String = "",
) -> Array[MutationCandidate] {
discover_with_rules(source, default_rules(), file~)
}
///|
pub fn discover_with_rules(
source : String,
rules : ArrayView[MutationRule],
file? : String = "",
) -> Array[MutationCandidate] {
let found : Array[MutationCandidate] = []
for i = 0, line = 1, column = 1; i < source.length(); {
if starts_with_at(source, i, "//") {
let (next_i, next_line, next_column) = skip_line_comment(
source, i, line, column,
)
continue next_i, next_line, next_column
} else if starts_with_at(source, i, "/*") {
let (next_i, next_line, next_column) = skip_block_comment(
source, i, line, column,
)
continue next_i, next_line, next_column
} else if source[i] == '"' {
let (next_i, next_line, next_column) = skip_string(
source, i, line, column,
)
continue next_i, next_line, next_column
} else if source[i] == '\'' {
let (next_i, next_line, next_column) = skip_char(source, i, line, column)
continue next_i, next_line, next_column
} else {
let matched_rules = rules_at(source, i, rules)
if !line_at(source, i).contains("mutest:ignore") {
for rule in matched_rules {
let end = i + rule.from.length()
found.push({
id: found.length(),
rule,
span: { file, start: i, end, line, column },
original: rule.from,
replacement: rule.to,
line_text: line_at(source, i),
})
}
}
let (next_i, next_line, next_column) = advance(source, i, line, column)
continue next_i, next_line, next_column
}
}
found
}
///|
pub fn apply_mutation(source : String, candidate : MutationCandidate) -> String {
source[0:candidate.span.start].to_owned() +
candidate.replacement +
source[candidate.span.end:source.length()].to_owned()
}
///|
pub fn summarize(
source : String,
file? : String = "",
) -> MutationSummary {
let candidates = discover(source, file~)
summarize_candidates(file, candidates)
}
///|
pub fn summarize_candidates(
file : String,
candidates : ArrayView[MutationCandidate],
) -> MutationSummary {
let mut boolean_count = 0
let mut equality_count = 0
let mut relational_count = 0
let mut arithmetic_count = 0
let mut logical_count = 0
let mut numeric_count = 0
for candidate in candidates {
match candidate.rule.kind {
BooleanLiteral => boolean_count += 1
EqualityOperator => equality_count += 1
RelationalOperator => relational_count += 1
ArithmeticOperator => arithmetic_count += 1
LogicalOperator => logical_count += 1
NumericLiteral => numeric_count += 1
}
}
{
file,
candidate_count: candidates.length(),
boolean_count,
equality_count,
relational_count,
arithmetic_count,
logical_count,
numeric_count,
}
}
///|
fn rules_at(
source : String,
index : Int,
rules : ArrayView[MutationRule],
) -> Array[MutationRule] {
let matched : Array[MutationRule] = []
let mut longest_token = 0
for rule in rules {
if starts_with_at(source, index, rule.from) &&
token_boundary(source, index, rule.from) &&
rule.from.length() > longest_token {
longest_token = rule.from.length()
}
}
for rule in rules {
if starts_with_at(source, index, rule.from) &&
token_boundary(source, index, rule.from) &&
rule.from.length() == longest_token {
matched.push(rule)
}
}
matched
}
///|
fn token_boundary(source : String, index : Int, token : String) -> Bool {
if is_keyword_token(token) {
let before = if index == 0 { None } else { source.get_char(index - 1) }
let after = source.get_char(index + token.length())
!is_ident_char(before) && !is_ident_char(after)
} else if is_numeric_token(token) {
let before = if index == 0 { None } else { source.get_char(index - 1) }
let after = source.get_char(index + token.length())
!is_numeric_boundary_char(before) && !is_numeric_boundary_char(after)
} else {
structural_token_allowed(source, index, token)
}
}
///|
fn is_numeric_token(token : String) -> Bool {
for ch in token {
match ch {
'-' | '0'..='9' => ()
_ => return false
}
}
token.contains("0") ||
token.contains("1") ||
token.contains("2") ||
token.contains("3") ||
token.contains("4") ||
token.contains("5") ||
token.contains("6") ||
token.contains("7") ||
token.contains("8") ||
token.contains("9")
}
///|
fn is_numeric_boundary_char(ch : Char?) -> Bool {
match ch {
Some('a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-') => true
_ => false
}
}
///|
fn structural_token_allowed(
source : String,
index : Int,
token : String,
) -> Bool {
match token {
"-" => !starts_with_at(source, index, "->")
">" => index == 0 || (source[index - 1] != '-' && source[index - 1] != '=')
"<" => index == 0 || source[index - 1] != '.'
_ => true
}
}
///|
fn is_keyword_token(token : String) -> Bool {
token == "true" || token == "false"
}
///|
fn is_ident_char(ch : Char?) -> Bool {
match ch {
Some('a'..='z' | 'A'..='Z' | '0'..='9' | '_') => true
_ => false
}
}
///|
fn starts_with_at(source : String, index : Int, needle : String) -> Bool {
index + needle.length() <= source.length() &&
source[index:index + needle.length()].to_owned() == needle
}
///|
fn advance(
source : String,
index : Int,
line : Int,
column : Int,
) -> (Int, Int, Int) {
if source[index] == '\n' {
(index + 1, line + 1, 1)
} else {
(index + 1, line, column + 1)
}
}
///|
fn skip_line_comment(
source : String,
index : Int,
line : Int,
column : Int,
) -> (Int, Int, Int) {
for i = index, c = column; i < source.length(); {
if source[i] == '\n' {
break (i + 1, line + 1, 1)
} else {
continue i + 1, c + 1
}
} nobreak {
(source.length(), line, c)
}
}
///|
fn skip_block_comment(
source : String,
index : Int,
line : Int,
column : Int,
) -> (Int, Int, Int) {
for i = index + 2, l = line, c = column + 2; i < source.length(); {
if starts_with_at(source, i, "*/") {
break (i + 2, l, c + 2)
} else if source[i] == '\n' {
continue i + 1, l + 1, 1
} else {
continue i + 1, l, c + 1
}
} nobreak {
(source.length(), l, c)
}
}
///|
fn skip_string(
source : String,
index : Int,
line : Int,
column : Int,
) -> (Int, Int, Int) {
for i = index + 1, l = line, c = column + 1, escaped = false; i <
source.length(); {
if escaped {
let (next_i, next_line, next_column) = advance(source, i, l, c)
continue next_i, next_line, next_column, false
} else if source[i] == '\\' {
continue i + 1, l, c + 1, true
} else if source[i] == '"' {
break (i + 1, l, c + 1)
} else if source[i] == '\n' {
continue i + 1, l + 1, 1, false
} else {
continue i + 1, l, c + 1, false
}
} nobreak {
(source.length(), l, c)
}
}
///|
fn skip_char(
source : String,
index : Int,
line : Int,
column : Int,
) -> (Int, Int, Int) {
for i = index + 1, c = column + 1, escaped = false; i < source.length(); {
if escaped {
continue i + 1, c + 1, false
} else if source[i] == '\\' {
continue i + 1, c + 1, true
} else if source[i] == '\'' {
break (i + 1, line, c + 1)
} else {
continue i + 1, c + 1, false
}
} nobreak {
(source.length(), line, c)
}
}
///|
fn line_at(source : String, index : Int) -> String {
let start = for i = index; i > 0; {
if source[i - 1] == '\n' {
break i
} else {
continue i - 1
}
} nobreak {
0
}
let end = for i = index; i < source.length(); {
if source[i] == '\n' {
break i
} else {
continue i + 1
}
} nobreak {
source.length()
}
source[start:end].to_owned()
}