Skip to content

Commit 5870c9f

Browse files
committed
WIP attempt to switch to libcst for AST manipulation
1 parent 12090d5 commit 5870c9f

File tree

4 files changed

+93
-83
lines changed

4 files changed

+93
-83
lines changed

mutmut/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
from collections import defaultdict
22

3-
from parso.python.tree import (
4-
Keyword,
5-
Name,
6-
Number,
7-
)
83

94
__version__ = '3.2.0'
105

6+
from libcst import Name
117

128
# We have a global whitelist for constants of the pattern __all__, __version__, etc
139

@@ -146,7 +142,7 @@ def argument_mutation(children, context, **_):
146142

147143
def arglist_mutation(children, node, **_):
148144
for i, child_node in enumerate(children):
149-
if child_node.type in ('name', 'argument'):
145+
if type(child_node).__name__ in ('name', 'argument'):
150146
offset = 1
151147
if len(children) > i+1:
152148
if children[i+1].type == 'operator' and children[i+1].value == ',':

mutmut/__main__.py

Lines changed: 71 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,15 @@
5050
)
5151

5252
import click
53-
from parso import (
54-
parse,
55-
ParserSyntaxError,
53+
from libcst import (
54+
ClassDef,
55+
FunctionDef,
56+
ImportFrom,
57+
Module,
58+
Name,
59+
parse_module,
60+
SimpleStatementLine,
61+
Yield,
5662
)
5763
from rich.text import Text
5864
from setproctitle import setproctitle
@@ -254,7 +260,7 @@ def create_mutants_for_file(filename, output_path):
254260
# validate no syntax errors of mutants
255261
with open(output_path) as f:
256262
try:
257-
ast.parse(f.read())
263+
ast.parse_module(f.read())
258264
except (IndentationError, SyntaxError) as e:
259265
print(output_path, 'has invalid syntax: ', e)
260266
exit(1)
@@ -286,14 +292,14 @@ def write_all_mutants_to_file(*, out, source, filename):
286292
hash_by_function_name = {}
287293
mutant_names = []
288294

289-
try:
290-
ast = parse(ensure_ends_with_newline(source), error_recovery=False)
291-
except ParserSyntaxError:
292-
print(f'Warning: unsupported syntax in {filename}, skipping')
293-
out.write(source)
294-
return [], {}
295+
# try:
296+
module = parse_module(ensure_ends_with_newline(source))
297+
# except ParserSyntaxError:
298+
# print(f'Warning: unsupported syntax in {filename}, skipping')
299+
# out.write(source)
300+
# return [], {}
295301

296-
for type_, x, name_and_hash, mutant_name in yield_mutants_for_module(ast, no_mutate_lines):
302+
for type_, x, name_and_hash, mutant_name in yield_mutants_for_module(node=module, no_mutate_lines=no_mutate_lines):
297303
out.write(x)
298304
if mutant_name:
299305
mutant_names.append(mutant_name)
@@ -368,17 +374,20 @@ def filter_funcdef_children(children):
368374

369375
def yield_mutants_for_node(*, func_node, class_name=None, context, node):
370376
# do not mutate static typing annotations
371-
if node.type == 'tfpdef':
377+
if type(node).__name__ == 'tfpdef':
372378
return
373379

380+
381+
print(type(node).__name__)
382+
374383
# Some functions should not be mutated
375-
if node.type == 'atom_expr' and node.children[0].type == 'name' and node.children[0].value in NEVER_MUTATE_FUNCTION_CALLS:
384+
if type(node).__name__ == 'atom_expr' and node.children[0].type == 'name' and node.children[0].value in NEVER_MUTATE_FUNCTION_CALLS:
376385
return
377386

378387
# The rest
379388
if hasattr(node, 'children'):
380389
children = node.children
381-
if node.type == 'funcdef':
390+
if isinstance(node, FunctionDef):
382391
children = filter_funcdef_children(children)
383392
for child_node in children:
384393
context.stack.append(child_node)
@@ -387,7 +396,7 @@ def yield_mutants_for_node(*, func_node, class_name=None, context, node):
387396
finally:
388397
context.stack.pop()
389398

390-
mutation = mutmut.mutation_by_ast_type.get(node.type)
399+
mutation = mutmut.mutation_by_ast_type.get(type(node).__name__)
391400
if not mutation:
392401
return
393402

@@ -416,7 +425,7 @@ def yield_mutants_for_node(*, func_node, class_name=None, context, node):
416425

417426
# noinspection PyArgumentList
418427
with rename_function_node(func_node, suffix=f'{context.count}', class_name=class_name):
419-
code = func_node.get_code()
428+
code = func_module.code_for_node(node)
420429
if valid_syntax(code):
421430
context.count += 1
422431

@@ -431,7 +440,7 @@ def yield_mutants_for_node(*, func_node, class_name=None, context, node):
431440

432441
def valid_syntax(code):
433442
try:
434-
ast.parse(dedent(code))
443+
ast.parse_module(dedent(code))
435444
return True
436445
except (SyntaxError, IndentationError):
437446
return False
@@ -452,25 +461,25 @@ def exclude_node(self, node):
452461

453462
def is_inside_annassign(self):
454463
for node in self.stack:
455-
if node.type == 'annassign':
464+
if type(node).__name__ == 'annassign':
456465
return True
457466
return False
458467

459468
def is_inside_dict_synonym_call(self):
460469
for node in self.stack:
461-
if node.type == 'atom_expr' and node.children[0].type == 'name' and node.children[0].value in self.dict_synonyms:
470+
if type(node).__name__ == 'atom_expr' and node.children[0].type == 'name' and node.children[0].value in self.dict_synonyms:
462471
return True
463472
return False
464473

465474

466475
def is_generator(node):
467-
assert node.type == 'funcdef'
476+
assert isinstance(node, FunctionDef)
468477

469478
def _is_generator(n):
470-
if n is not node and n.type in ('funcdef', 'classdef'):
479+
if n is not node and isinstance(n, (FunctionDef, ClassDef)):
471480
return False
472481

473-
if n.type == 'keyword' and n.value == 'yield':
482+
if isinstance(n, Yield):
474483
return True
475484

476485
for c in getattr(n, 'children', []):
@@ -480,29 +489,29 @@ def _is_generator(n):
480489
return _is_generator(node)
481490

482491

483-
def yield_mutants_for_function(node, *, class_name=None, no_mutate_lines):
484-
assert node.type == 'funcdef'
492+
def yield_mutants_for_function(*, module, node, class_name=None, no_mutate_lines):
493+
assert isinstance(node, FunctionDef)
485494

486495
if node.name.value in NEVER_MUTATE_FUNCTION_NAMES:
487-
yield 'filler', node.get_code(), None, None
496+
yield 'filler', module.code_for_node(node), None, None
488497
return
489498

490-
hash_of_orig = md5(node.get_code().encode()).hexdigest()
499+
hash_of_orig = md5(module.code_for_node(node).encode()).hexdigest()
491500

492501
orig_name = node.name.value
493502
# noinspection PyArgumentList
494503
with rename_function_node(node, suffix='orig', class_name=class_name):
495-
yield 'orig', node.get_code(), (orig_name, hash_of_orig), None
504+
yield 'orig', module.code_for_node(node), (orig_name, hash_of_orig), None
496505

497506
context = FuncContext(no_mutate_lines=no_mutate_lines)
498507

499508
return_annotation_started = False
500509

501510
for child_node in node.children:
502-
if child_node.type == 'operator' and child_node.value == '->':
511+
if type(child_node).__name__ == 'operator' and child_node.value == '->':
503512
return_annotation_started = True
504513

505-
if return_annotation_started and child_node.type == 'operator' and child_node.value == ':':
514+
if return_annotation_started and type(child_node).__name__ == 'operator' and child_node.value == ':':
506515
return_annotation_started = False
507516

508517
if return_annotation_started:
@@ -521,60 +530,61 @@ def yield_mutants_for_function(node, *, class_name=None, no_mutate_lines):
521530
yield 'filler', '\n\n', None, None
522531

523532

524-
def yield_mutants_for_class(node, no_mutate_lines):
525-
assert node.type == 'classdef'
533+
def yield_mutants_for_class(*, module, node, no_mutate_lines):
534+
assert isinstance(node, ClassDef)
526535
for child_node in node.children:
527-
if child_node.type == 'suite':
528-
yield from yield_mutants_for_class_body(child_node, no_mutate_lines=no_mutate_lines)
536+
if type(child_node).__name__ == 'suite':
537+
yield from yield_mutants_for_class_body(module=module, node=child_node, no_mutate_lines=no_mutate_lines)
529538
else:
530-
yield 'filler', child_node.get_code(), None, None
539+
yield 'filler', module.code_for_node(child_node), None, None
531540

532541

533-
def yield_mutants_for_class_body(node, no_mutate_lines):
534-
assert node.type == 'suite'
542+
def yield_mutants_for_class_body(*, module, node, no_mutate_lines):
543+
assert type(node).__name__ == 'suite'
535544
class_name = node.parent.name.value
536545

537546
for child_node in node.children:
538-
if child_node.type == 'funcdef':
539-
yield from yield_mutants_for_function(child_node, class_name=class_name, no_mutate_lines=no_mutate_lines)
547+
if isinstance(node, FunctionDef):
548+
yield from yield_mutants_for_function(module=module, node=child_node, class_name=class_name, no_mutate_lines=no_mutate_lines)
540549
else:
541-
yield 'filler', child_node.get_code(), None, None
550+
yield 'filler', module.code_for_node(child_node), None, None
542551

543552

544553
def is_from_future_import_node(c):
545-
if c.type == 'simple_stmt':
554+
if isinstance(c, SimpleStatementLine):
546555
if c.children:
547556
c2 = c.children[0]
548-
if c2.type == 'import_from' and c2.children[1].type == 'name' and c2.children[1].value == '__future__':
557+
if isinstance(c2, ImportFrom) and isinstance(c2.children[1], Name) and c2.children[1].value == '__future__':
549558
return True
550559
return False
551560

552561

553-
def yield_future_imports(node):
562+
def yield_future_imports(*, module, node):
554563
for c in node.children:
555564
if is_from_future_import_node(c):
556-
yield 'filler', c.get_code(), None, None
565+
yield 'filler', module.code_for_node(c), None, None
557566

558567

559-
def yield_mutants_for_module(node, no_mutate_lines):
560-
assert node.type == 'file_input'
568+
def yield_mutants_for_module(*, node, no_mutate_lines):
569+
assert isinstance(node, Module)
570+
module = node
561571

562572
# First yield `from __future__`, then the rest
563-
yield from yield_future_imports(node)
564-
573+
yield from yield_future_imports(module=module, node=node)
565574
yield 'trampoline_impl', trampoline_impl, None, None
566575
yield 'trampoline_impl', yield_from_trampoline_impl, None, None
567576
yield 'filler', '\n', None, None
577+
assert type(node).__name__ == 'Module'
568578
for child_node in node.children:
569-
if child_node.type == 'funcdef':
570-
yield from yield_mutants_for_function(child_node, no_mutate_lines=no_mutate_lines)
571-
elif child_node.type == 'classdef':
572-
yield from yield_mutants_for_class(child_node, no_mutate_lines=no_mutate_lines)
579+
if isinstance(child_node, FunctionDef):
580+
yield from yield_mutants_for_function(module=module, node=child_node, no_mutate_lines=no_mutate_lines)
581+
elif isinstance(child_node, ClassDef):
582+
yield from yield_mutants_for_class(module=module, node=child_node, no_mutate_lines=no_mutate_lines)
573583
elif is_from_future_import_node(child_node):
574584
# Don't yield `from __future__` after trampoline
575585
pass
576586
else:
577-
yield 'filler', child_node.get_code(), None, None
587+
yield 'filler', module.code_for_node(child_node), None, None
578588

579589

580590
class SourceFileMutationData:
@@ -1390,25 +1400,25 @@ def results(all):
13901400

13911401
def read_mutants_ast(path):
13921402
with open(Path('mutants') / path) as f:
1393-
return parse(f.read(), error_recovery=False)
1403+
return parse_module(f.read())
13941404

13951405

13961406
def read_orig_ast(path):
13971407
with open(path) as f:
1398-
return parse(f.read())
1408+
return parse_module(f.read())
13991409

14001410

14011411
def find_ast_node(ast, function_name, orig_function_name):
14021412
function_name = function_name.rpartition('.')[-1]
14031413
orig_function_name = orig_function_name.rpartition('.')[-1]
14041414

14051415
for node in ast.children:
1406-
if node.type == 'classdef':
1416+
if isinstance(node, ClassDef):
14071417
(body,) = [x for x in node.children if x.type == 'suite']
14081418
result = find_ast_node(body, function_name=function_name, orig_function_name=orig_function_name)
14091419
if result:
14101420
return result
1411-
if node.type == 'funcdef' and node.name.value == function_name:
1421+
if isinstance(node, FunctionDef) and node.name.value == function_name:
14121422
node.name.value = orig_function_name
14131423
return node
14141424

@@ -1457,9 +1467,9 @@ def get_diff_for_mutant(mutant_name, source=None, path=None):
14571467
if source is None:
14581468
ast = read_mutants_ast(path)
14591469
else:
1460-
ast = parse(source, error_recovery=False)
1461-
orig_code = read_original_ast_node(ast, mutant_name).get_code().strip()
1462-
mutant_code = read_mutant_ast_node(ast, mutant_name).get_code().strip()
1470+
ast = parse_module(source)
1471+
orig_code = module.code_for_node(read_original_ast_node(ast, mutant_name)).strip()
1472+
mutant_code = module.code_for_node(read_mutant_ast_node(ast, mutant_name)).strip()
14631473

14641474
path = str(path) # difflib requires str, not Path
14651475
return '\n'.join([
@@ -1500,14 +1510,14 @@ def apply_mutant(mutant_name):
15001510
mutant_ast_node.name.value = orig_function_name
15011511

15021512
for node in orig_ast.children:
1503-
if node.type == 'funcdef' and node.name.value == orig_function_name:
1513+
if isinstance(node, FunctionDef) and node.name.value == orig_function_name:
15041514
node.children = mutant_ast_node.children
15051515
break
15061516
else:
15071517
raise FileNotFoundError(f'Could not apply mutant {mutant_name}')
15081518

15091519
with open(path, 'w') as f:
1510-
f.write(orig_ast.get_code())
1520+
f.write(orig_ast.code_for_node(orig_ast))
15111521

15121522

15131523
# TODO: junitxml, html commands

0 commit comments

Comments
 (0)