mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(mutation report): warn when multiple functions in a file share a name
Addresses the Greptile review concern: ast.walk's first-match-wins behavior could embed the wrong function body when a file defines the same name in multiple places (e.g., a module-level helper and a class method). mutmut's mutant identifier does not carry class context, so we can't always determine which definition was mutated. find_function_in_file now returns the start line of every matching definition; render() surfaces a "Note: N functions named X" warning in the report when there is more than one match. The first match is still embedded as the body — the warning tells the reader to verify manually instead of silently using the wrong context. Smoke-tested against the existing artifact: single-match files render unchanged.
This commit is contained in:
parent
0ccdc91dc5
commit
c4d964f460
1 changed files with 40 additions and 14 deletions
|
|
@ -77,21 +77,35 @@ def module_to_file(module_path: str) -> Path | None:
|
|||
|
||||
def find_function_in_file(
|
||||
file_path: Path, function_name: str
|
||||
) -> tuple[int, int, str] | None:
|
||||
) -> tuple[int, int, str, list[int]] | None:
|
||||
"""Find a top-level or nested function by name; returns the first match.
|
||||
|
||||
Returns ``(start_line, end_line, source, all_match_lines)`` or ``None``.
|
||||
``all_match_lines`` is the start line of every function (any nesting
|
||||
level) in the file with this name. When ``len(all_match_lines) > 1`` the
|
||||
file defines the same name in multiple places (e.g., a module-level
|
||||
helper and a class method) — mutmut's mutant identifier does not carry
|
||||
class context, so we can't determine which definition was mutated.
|
||||
Callers surface a disambiguation note in that case.
|
||||
"""
|
||||
src = file_path.read_text()
|
||||
tree = ast.parse(src)
|
||||
for node in ast.walk(tree):
|
||||
if (
|
||||
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
and node.name == function_name
|
||||
):
|
||||
lines = src.splitlines()
|
||||
return (
|
||||
node.lineno,
|
||||
node.end_lineno,
|
||||
"\n".join(lines[node.lineno - 1 : node.end_lineno]),
|
||||
)
|
||||
return None
|
||||
matches = [
|
||||
node
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
and node.name == function_name
|
||||
]
|
||||
if not matches:
|
||||
return None
|
||||
first = matches[0]
|
||||
lines = src.splitlines()
|
||||
return (
|
||||
first.lineno,
|
||||
first.end_lineno,
|
||||
"\n".join(lines[first.lineno - 1 : first.end_lineno]),
|
||||
[m.lineno for m in matches],
|
||||
)
|
||||
|
||||
|
||||
def collect_test_files(tests_dir: list[str]) -> list[Path]:
|
||||
|
|
@ -274,9 +288,21 @@ def render(config: dict, survivors: list[str], stats: dict | None) -> str:
|
|||
out.append("")
|
||||
found = find_function_in_file(file_path, function_name)
|
||||
if found:
|
||||
start, end, fn_src = found
|
||||
start, end, fn_src, all_lines = found
|
||||
out.append(f"### Original function (lines {start}-{end})")
|
||||
out.append("")
|
||||
if len(all_lines) > 1:
|
||||
line_list = ", ".join(str(line) for line in all_lines)
|
||||
out.append(
|
||||
f"> **Note:** {len(all_lines)} functions named "
|
||||
f"`{function_name}` are defined in this file at lines "
|
||||
f"{line_list}. Showing the first match. mutmut's "
|
||||
f"mutant identifier does not carry class context, so "
|
||||
f"the body below may not correspond to the function "
|
||||
f"that was actually mutated — verify manually before "
|
||||
f"writing the killing test."
|
||||
)
|
||||
out.append("")
|
||||
out.append("```python")
|
||||
out.append(fn_src)
|
||||
out.append("```")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue