Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[question] Exclude If in elif position #1286

Open
Feuermurmel opened this issue Jan 26, 2025 · 0 comments
Open

[question] Exclude If in elif position #1286

Feuermurmel opened this issue Jan 26, 2025 · 0 comments

Comments

@Feuermurmel
Copy link

Feuermurmel commented Jan 26, 2025

I'm trying to write a transformer that inserts a certain statement before existing statements that contains some code pattern, but I'm hitting a problem with If nodes that represent the elif part of an if statement.

More specifically, I'm trying to insert an additional statement whenever an attribute with a specific name on any object is accessed. The attribute access could be anywhere, inside a simple expression statement, the right side of an assignment, the condition of an if, etc.

Below is a simplified version of the code I'm using. It works well in almost all cases. Shown is the case of the attribute access happening in the condition of an if statement:

from textwrap import dedent
from libcst import Attribute, CSTNode, BaseStatement, CSTTransformer, \
    FlattenSentinel, parse_module, parse_statement
import libcst.matchers as m

class Transformer(CSTTransformer):
    def __init__(self):
        super().__init__()

        # Attribute accesses within each nested BaseStatement node.
        self.attr_accesses_stack: list[list[Attribute]] = []

    def on_visit(self, node: CSTNode) -> bool:
        if isinstance(node, BaseStatement):
            self.attr_accesses_stack.append([])

        if m.matches(node, m.Attribute(attr=m.Name("special_attribute"))):
            self.attr_accesses_stack[-1].append(node)

        return True

    def on_leave(
        self, original_node: CSTNode, updated_node: CSTNode
    ) -> CSTNode | FlattenSentinel[CSTNode]:
        if isinstance(updated_node, BaseStatement):
            attr_accesses = self.attr_accesses_stack.pop()
            new_nodes = [updated_node]

            # The actual code does something more complex with the collected attribute nodes.
            for i in attr_accesses:
                new_nodes.insert(0, parse_statement("print('attribute accessed')"))

            return FlattenSentinel(new_nodes)

        return updated_node

example_code = dedent(
    """\
    if x.special_attribute:
        pass
    # elif y.special_attribute:
    #     pass
    """
)

print(parse_module(example_code).visit(Transformer()).code)

Output:

print('attribute accessed')
if x.special_attribute:
    pass
# elif y.special_attribute:
#     pass

The problem arises when the attribute access is in the elif's condition. Uncommenting the two lines in example_code produces the following error:

  File [...]/venv/lib/python3.12/site-packages/libcst/_nodes/internal.py:112 in visit_optional
    raise TypeError(

TypeError: We got a FlattenSentinel while visiting a If. This node's parent does not allow for it to be it to be replaced with a sequence.

The problem is clear: An If node is used in two distinct cases:

  • As an item in a list of statements to represent the a whole if statement including any elif and else parts.
  • To represent an elif part of an if statement. There can only be an If, an Else or no node in that place.

The second case is where my code fails because it returns a FlattenSentinel instance.

My question: What is the best way to handle this case? What I'd like to do is to ignore these If nodes in the traversal so that the attribute accesses are instead collected for the top-level If node of each if statement, i.e. so that the resulting code would look like this:

print('attribute accessed')
print('attribute accessed')
if x.special_attribute:
    pass
elif y.special_attribute:
    pass

Is there any way to do this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant