comby-tools/comby

Question about writing patterns for Python

Closed this issue · 3 comments

First of all, thanks for this great tool. This tool really helps me a lot in my daily work.

So my question is about writing patterns for Python.
I want to apply some PEP-8 idiomatic patterns for my code base, so I want a pattern for:

# previous pattern
if a == False:
  return

# pattern I want
if not a:
  return

and I simply write a comby rule: if :[x] == False: -> if not :[x]:.
However, this rule will also wrongly change the code below:

# pattern I want to change correctly
if b and a == False:
  return

# this pattern will be wrongly changed as follow:
if not b and a:
  return

# what I want to do:
if b and not a:
  return

How can I write a correct rule for this pattern?
What I've tried before is ...

  • if :[head] :[con~(and|or|==|!=|...)] :[x] == False: -> if :[head] :[con] not :[x]:: This is simple and reasonable, but (1) I have to write all the binary operators like and and or, and (2) this rule ignores the first pattern above.
  • if :[x~(?!and|or|...)\w+] == False: -> if not :[x]:: To supplement the rule above, use this pattern too. However, because this rule uses regular expressions to filter out binary operators and to capture variables with \w, it cannot utilize the powerful capturing of comby e.g. function calls like if foo() == False:. Since the comby document says to avoid regular expressions that match special syntax like ) or .*, I don't know how to capture this pattern in a smart way.

Is there a shorter and smarter way to write a rule for this pattern?
Thank you!

Hey @sangwoo-joh I'm glad you find comby helpful :-) Let's break this down:

it cannot utilize the powerful capturing of comby e.g. function calls like if foo() == False

True, this is probably the main thing stopping you from getting what you want. There is an undocumented experimental way to match this sort of thing. but you probably need this for things to work out. What you can do is write :[x:e]. The :e part is just a label that says treat this "like" an expression. Now, comby will match identifiers like foo, but also things like foo(a, b). Example.

At some point it will be "official", it's just that I'm not sure about introducing the syntax in this exact way. The :e part is a bit awkward, and it may make sense to make this behavior the default.


Now it becomes a bit easier to tackle your problem, but I think it would be difficult to create a single match statement to capture subexpressions in the if-conditionals. Something like if :[ignore] :[x:e] == False: won't really work because :[ignore] will continue matching across newlines if it can't satisfy the match. What you want to do is limit the matching to a single if-condition, so we will start simply with if :[cond]:. This will always match just one if conditional because in Python it's guaranteed to stop with a :.

Then what I'd do is write a rewrite rule using that :[x:e] syntax, here's an example. So you end up with something like:

comby 'if :[cond]:' 'if :[cond]:' -rule 'where rewrite :[cond] { ":[x:e] == False" -> "not :[x]" }' 

I don't know if this is perfect for what you're doing but it probably gets closer.

@rvantonder Wow, thank you for the excellent explanation!!
Although I have to test this rule with my codebase to check whether there are some corner cases or not, the rewrite rule using :[x:e] seems a perfect solution for me. Also, I'm glad to know (1) the hidden syntax for the expression :[x:e] and (2) that I can use this syntax in the rewrite rule.
Many thanks!!

Oh, and one more thing, I think either way is ok making the expression-capturing syntax :[x:e] official or as a default behaviour. At least it is documented. I will wait for the official version of this!