Files
ast-project/part1/mutator_operators.py
T
2026-06-24 13:47:14 +02:00

124 lines
3.4 KiB
Python

import random
import re
from sqlite_static_helper import *
_TOKEN_RE = re.compile(
r"""
(?P<line_comment>--[^\n]*) |
(?P<block_comment>/\*.*?\*/) |
(?P<string>'(?:[^']|'')*') |
(?P<dquoted>"(?:[^"]|"")*") |
(?P<bracket>\[[^\]]*\]) |
(?P<backtick>`(?:[^`]|``)*`) |
(?P<blob>[xX]'[0-9a-fA-F]*') |
(?P<number>\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b) |
(?P<ident>[A-Za-z_][A-Za-z0-9_]*) |
(?P<op>[<>!=]=|<>|\|\||::|->>?|[+\-*/%<>=&|^~,.;()@]) |
(?P<ws>\s+) |
(?P<other>.)
""",
re.VERBOSE | re.DOTALL,
)
_UNSAFE_KINDS = {
"line_comment", "block_comment", "string", "dquoted", "bracket", "backtick",
"blob"
}
def _tokenize(s: str):
return [(m.lastgroup, m.group()) for m in _TOKEN_RE.finditer(s)]
def _sub_in_safe(s: str, pattern: re.Pattern, repl, max_subs: int = 1) -> str:
"""Run `pattern.sub(repl, …)` only outside strings/comments/blob literals."""
if max_subs <= 0:
return s
parts: list[str] = []
done = 0
for kind, text in _tokenize(s):
if done >= max_subs or kind in _UNSAFE_KINDS:
parts.append(text)
continue
new_text, n = pattern.subn(repl, text, count=max_subs - done)
parts.append(new_text)
done += n
return ''.join(parts)
_CMP_RE = re.compile(r'(?<![<>!=])(<=|>=|<>|!=|==|=|<|>)(?!=)')
def mut_swap_comparison(s: str) -> str:
"""Swap the left-most comparison operator with a different one"""
def repl(m: re.Match) -> str:
op = m.group(1)
return random.choice([o for o in CMP_OPS if o != op])
return _sub_in_safe(s, _CMP_RE, repl, 1)
_BOOL_RE = re.compile(r'\b(AND|OR)\b', re.I)
def mut_swap_boolean(s: str) -> str:
"""Swap the left-most boolean operator with a different one"""
def repl(m: re.Match) -> str:
return 'OR' if m.group(1).upper() == 'AND' else 'AND'
return _sub_in_safe(s, _BOOL_RE, repl, 1)
def mut_negate_where(s: str) -> str:
"""Swap the left-most WHERE with WHERE NOT"""
if not re.search(r'\bWHERE\b', s, re.I):
return s
return _sub_in_safe(s, re.compile(r'\bWHERE\b', re.I), 'WHERE NOT', 1)
def mut_change_type(s: str) -> str:
"""Swap the left-most data type with a different one"""
pat = re.compile(
r'\b(' + '|'.join(
re.escape(t)
for t in sorted(TYPES, key=len, reverse=True)) +
r')\b',
re.I,
)
def repl(m: re.Match) -> str:
cur = m.group(1).upper()
return random.choice(
[t for t in TYPES if t.upper() != cur])
return _sub_in_safe(s, pat, repl, 1)
def mut_swap_join_type(s: str) -> str:
"""Swap one join type to a different join type"""
join_pattern = '|'.join(
re.escape(j) for j in sorted(JOINS, key=len, reverse=True))
pat = re.compile(rf'\b({join_pattern})\b', re.I)
m = pat.search(s)
if not m:
return s
matched = m.group(1).upper()
alternatives = [j for j in JOINS if j.upper() != matched]
if not alternatives:
return s
replacement = random.choice(alternatives)
if m.group(1).isupper():
replacement = replacement.upper()
elif m.group(1).islower():
replacement = replacement.lower()
elif m.group(1)[0].isupper() and m.group(1)[1:].islower():
replacement = replacement.title()
return s[:m.start()] + replacement + s[m.end():]