mirror of
https://github.com/unclebob/mutator.git
synced 2026-11-06 18:23:50 +00:00
Mutate ?? and ?. in TypeScript and JavaScript, including Express handlers.
This commit is contained in:
@@ -95,7 +95,7 @@ Tokens inside strings, comments, and quoted Clojure forms are left alone. A repl
|
||||
| Clojure | clj-mutate's head symbols: `+`/`-`, `*` to `/`, `inc`/`dec`, comparisons, `=`/`not=`, `if`/`if-not`, `when`/`when-not`, `and`/`or`, and the seq, predicate, and coercion pairs. `true`/`false` and `0`/`1` anywhere. |
|
||||
| Java | `true`/`false`, `==`/`!=`, `<`/`<=`, `>`/`>=`, `+`/`-`, `*`/`/`, `&&`/`||`, delete `!` and unary `-`, `0`/`1`. Constructors are not entries, matching crapper. |
|
||||
| Go | mutate4go: `+`/`-`, `*` to `/` only, comparisons, `==`/`!=`, `true`/`false`, `&&`/`||`, `0`/`1`. |
|
||||
| TypeScript | the Java set, plus `===`/`!==`. |
|
||||
| TypeScript | the Java set, plus `===`/`!==`, `??` to `||`, and `?.` to `.`. A call or index drops `?.` (`a?.()` becomes `a()`). `.js`, `.jsx`, `.mjs`, and `.cjs` use these rules. |
|
||||
| Rust | the Java set (`&&`/`||`, `!`, unary `-`). |
|
||||
| Python | the Java set, spelled `and`/`or`, `not`, and `True`/`False`. |
|
||||
|
||||
|
||||
@@ -115,11 +115,29 @@ def project_functions(source: str, path: Path, root: Path):
|
||||
)
|
||||
|
||||
|
||||
def _owner(functions, line: int):
|
||||
matches = [fn for fn in functions if fn.start_line <= line <= fn.end_line]
|
||||
def _contains(fn, line: int, byte: int | None) -> bool:
|
||||
if fn.start_line > line or fn.end_line < line:
|
||||
return False
|
||||
start = getattr(fn, "start_byte", -1)
|
||||
end = getattr(fn, "end_byte", -1)
|
||||
if byte is None or start < 0 or end < 0:
|
||||
return True
|
||||
return start <= byte < end
|
||||
|
||||
|
||||
def _tightness(fn) -> int:
|
||||
start = getattr(fn, "start_byte", -1)
|
||||
end = getattr(fn, "end_byte", -1)
|
||||
if start >= 0 and end >= start:
|
||||
return end - start
|
||||
return (fn.end_line - fn.start_line) * 1_000_000
|
||||
|
||||
|
||||
def _owner(functions, line: int, byte: int | None = None):
|
||||
matches = [fn for fn in functions if _contains(fn, line, byte)]
|
||||
if not matches:
|
||||
return None
|
||||
return min(matches, key=lambda fn: (fn.end_line - fn.start_line, fn.start_line))
|
||||
return min(matches, key=lambda fn: (_tightness(fn), fn.start_line))
|
||||
|
||||
|
||||
def sites_in_file(source: str, path: Path, root: Path, file_key: str) -> list[Site]:
|
||||
@@ -133,7 +151,7 @@ def sites_in_file(source: str, path: Path, root: Path, file_key: str) -> list[Si
|
||||
}
|
||||
found: list[Site] = []
|
||||
for raw in discover_raw(source, language, path.as_posix()):
|
||||
owner = _owner(functions, raw.line)
|
||||
owner = _owner(functions, raw.line, raw.start)
|
||||
if owner is None:
|
||||
continue
|
||||
private = privacy[id(owner)]
|
||||
|
||||
+33
-4
@@ -2,7 +2,9 @@
|
||||
|
||||
Clojure follows clj-mutate's symbol rules. Go follows mutate4go, including
|
||||
one-way `*` to `/`. Java follows mutate4java. TypeScript, Rust, and Python
|
||||
use the same decisions as Java, spelled in that language.
|
||||
use the same decisions as Java, spelled in that language. TypeScript also
|
||||
mutates `??` to `||` and `?.` to `.` (removed on a call or index). JavaScript
|
||||
files use the TypeScript rules.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,6 +26,7 @@ _BINARY = {
|
||||
"!==": "===",
|
||||
"&&": "||",
|
||||
"||": "&&",
|
||||
"??": "||",
|
||||
"and": "or",
|
||||
"or": "and",
|
||||
}
|
||||
@@ -100,8 +103,12 @@ class RawSite:
|
||||
|
||||
|
||||
def _grammar(language: str, path: str) -> str:
|
||||
if language == "typescript" and path.endswith(".tsx"):
|
||||
return "tsx"
|
||||
if language == "typescript":
|
||||
if path.endswith(".tsx"):
|
||||
return "tsx"
|
||||
if path.endswith((".js", ".jsx", ".mjs", ".cjs")):
|
||||
return "javascript"
|
||||
return "typescript"
|
||||
if language == "go":
|
||||
return "go"
|
||||
return language
|
||||
@@ -118,7 +125,7 @@ def _category(token: str) -> str:
|
||||
return "comparison"
|
||||
if token in {"==", "!=", "===", "!=="}:
|
||||
return "equality"
|
||||
if token in {"&&", "||", "and", "or"}:
|
||||
if token in {"&&", "||", "??", "and", "or"}:
|
||||
return "logical"
|
||||
return "arithmetic"
|
||||
|
||||
@@ -126,9 +133,27 @@ def _category(token: str) -> str:
|
||||
def _binary_mutant(language: str, token: str) -> str | None:
|
||||
if language == "go" and token == "/":
|
||||
return None
|
||||
if token == "??" and language != "typescript":
|
||||
return None
|
||||
return _BINARY.get(token)
|
||||
|
||||
|
||||
def _optional_mutant(node) -> str | None:
|
||||
"""`a?.b` becomes `a.b`. A call or index drops `?.`, so `a?.()` becomes `a()`."""
|
||||
|
||||
parent = node.parent
|
||||
if parent is None:
|
||||
return None
|
||||
host = parent.parent if parent.type == "optional_chain" else parent
|
||||
if host is None:
|
||||
return None
|
||||
if host.type == "member_expression":
|
||||
return "."
|
||||
if host.type in {"call_expression", "subscript_expression"}:
|
||||
return ""
|
||||
return None
|
||||
|
||||
|
||||
def _add(found: list[RawSite], node, original: str, mutant: str, category: str) -> None:
|
||||
if mutant == original:
|
||||
return
|
||||
@@ -217,6 +242,10 @@ def _visit(node, data: bytes, language: str, found: list[RawSite]) -> None:
|
||||
mutant = _binary_mutant(language, token)
|
||||
if mutant is not None:
|
||||
_add(found, node, token, mutant, _category(token))
|
||||
if language == "typescript" and token == "?.":
|
||||
optional = _optional_mutant(node)
|
||||
if optional is not None:
|
||||
_add(found, node, "?.", optional, "optional")
|
||||
if token in {"!", "not"} and parent in _UNARY_PARENTS | {"comparison_operator"}:
|
||||
_add(found, node, token, "", "unary")
|
||||
if token == "-" and parent in _UNARY_PARENTS:
|
||||
|
||||
@@ -219,6 +219,10 @@ def test_privacy_headers_digests_owners_and_mutation_fields(tmp_path):
|
||||
def test_grammar_guards_and_non_head_clojure_symbols(tmp_path):
|
||||
assert _grammar("typescript", "src/app.tsx") == "tsx"
|
||||
assert _grammar("typescript", "src/app.ts") == "typescript"
|
||||
assert _grammar("typescript", "src/app.js") == "javascript"
|
||||
assert _grammar("typescript", "src/app.mjs") == "javascript"
|
||||
assert _grammar("typescript", "src/app.cjs") == "javascript"
|
||||
assert _grammar("typescript", "src/app.jsx") == "javascript"
|
||||
assert _grammar("python", "src/app.py") == "python"
|
||||
|
||||
sites, pairs = _pairs(tmp_path, "src/demo/core.clj", "(ns demo.core)\n(defn place []\n [inc])\n")
|
||||
|
||||
@@ -163,3 +163,34 @@ def test_python_private_and_class_namespace(tmp_path):
|
||||
def test_string_operators_are_not_sites(tmp_path):
|
||||
source = 'def place():\n return "x > 0 and true"\n'
|
||||
assert _sites(tmp_path, "src/demo/app.py", source) == []
|
||||
|
||||
|
||||
def test_typescript_mutates_nullish_and_optional_chaining(tmp_path):
|
||||
source = """
|
||||
export function place(a, b) {
|
||||
return a ?? b?.c ?? b?.[0] ?? b?.();
|
||||
}
|
||||
"""
|
||||
sites = _sites(tmp_path, "src/demo/app.ts", source)
|
||||
assert sum(site.original == "??" and site.mutant == "||" for site in sites) == 3
|
||||
assert {site.mutant for site in sites if site.original == "?."} == {".", ""}
|
||||
assert {site.namespace for site in sites} == {"demo.app"}
|
||||
|
||||
|
||||
def test_javascript_express_handler_owns_its_nullish_site(tmp_path):
|
||||
source = """
|
||||
export function mount(app) {
|
||||
app.get("/users", (req, res) => {
|
||||
return req.body ?? false;
|
||||
});
|
||||
}
|
||||
"""
|
||||
sites = _sites(tmp_path, "src/demo/routes.js", source)
|
||||
assert {site.namespace for site in sites} == {"demo.routes"}
|
||||
assert ("GET /users", "??", "||") in {(site.name, site.original, site.mutant) for site in sites}
|
||||
assert all(site.name != "mount" for site in sites)
|
||||
|
||||
|
||||
def test_a_quoted_nullish_operator_is_not_a_site(tmp_path):
|
||||
source = 'export function place() {\n return "a ?? b?.c";\n}\n'
|
||||
assert _sites(tmp_path, "src/demo/app.ts", source) == []
|
||||
|
||||
Reference in New Issue
Block a user