Instagram / LibCST

A concrete syntax tree parser and serializer library for Python that preserves many aspects of Python's abstract syntax tree
https://libcst.readthedocs.io/
Other
1.47k stars 176 forks source link

Extract properties from class defs to common superclass #427

Open malsmith opened 3 years ago

malsmith commented 3 years ago

I've been looking at codemods and visitor examples but not finding a clear path to a solution. Any help would be appreciated. Assume I have already determined that a specific set of class names and property names need to be refactored. The refactoring involves adding the properties to a super class and removing the properties from the "source" classes.

For example:

before

class A(object):
  foo = "foo value"
  bar = "someothervalue"
  other = "3rd other value"

class B(object):
  baz = "someothervalue"
  foo = "foo value"
  bar = "someothervalue"

after

class common_class(object):
  foo = "foo value"
  bar = "someothervalue"

class A(object,common_class):
  other = "3rd other value"

class B(object):
  baz = "someothervalue"

Basically, I want to be able to extract properties to a common superclass. I'm thinking this is a codemod that takes a args for the superclass name, subclass names and property names to move

I am also thinking there are at least 3 passes to consider -

  1. Identify which nodes are the property ASSIGN statements in the subclass targets and property names (which needs a lookup to check these). Also to remember if the soon-to-be subclasses already have a reference to the superclass name.
  2. A pass to update or add the superclass block with one of the properties that match the subclass properties found in pass 1.
  3. A pass to actually remove the properties from the sub classes where they were found (optionally comment out these)
  4. A pass to update the subclass CLASSDEF to include the superclass name if not present

I'm just not sure what combination of metadata wrapper, transform and codemode (or matches) I need for this solution and I don't find a comprehensive example that does this sort of thing. I would have thought that subclass or member extraction refactoring was a common need in a refactoring tool.

malsmith commented 3 years ago

I’m making progress now. But left with a question about how to replace an ASSIGN statement with a commented out version of that statement. BTW simply removing the statement is super easy - with a sentinel remove return value.

Commenting out a line of code seems to be super complicated - I’ve tried EMPTYLINE and leading and trailing lines but these are not working. Errors include complaints about Missing semicolons in the AST.

thatch commented 3 years ago

Can you share some concrete code and what you're testing on? I don't have a snippet handy, but would expect this to look something like this, modifying on the body of the class:

def leave_ClassDef(...):
  assert isinstance(node.body, cst.IndentedBlock)
  children = list(node.body.children)
  i = <somehow find your assignment index>
  commented = EmptyLine("#" + mod.code_for_node(children[i]))
  if i < len(children) - 1:
    # there is a subsequent statement
    children[i+1] = children[i+1].with_changes(leading_lines=(commented, *children[i+1].leading_lines))
  else:
    # the IndentedBlock needs to own it
    footer = (commented, *node.body.footer)
  return node.with_changes(body=body.with_changes(children=children, footer=footer))
malsmith commented 3 years ago

This example is super helpful - will share the working example in the next few days.