Obsah
1. Krátké zopakování – průchod uzly CST stromu
2. Průchod atributy všech uzlů ve stromu
3. Vytištění zdrojového kódu získaného zpětným převodem z CST
4. Modifikace stromu při jeho průchodu s využitím třídy CSTTransformer
5. Speciální metody volané při vstupu či opuštění uzlů určitých typů
6. Záměna jména zvolené proměnné za jiné jméno
7. Průběh transformace, zobrazení výsledného výrazu po transformaci
8. Zobrazení rozdílů mezi původní a transformovaným kódem s využitím knihovny difflib
9. Vylepšení třídy pro přejmenování symbolů
11. Komplikovanější transformace
12. Záměna zvolených binárních aritmetických operátorů
13. Průběh transformace a tvar výsledného zdrojového kódu
14. Přejmenování jména volané funkce
15. Průběh transformace a tvar výsledného zdrojového kódu
16. Přejmenování jména funkce v její definici
17. Spuštění příkladu a ukázka modifikovaného zdrojového kódu
18. Obsah poslední části článku
19. Repositář s demonstračními příklady
1. Krátké zopakování – průchod uzly CST stromu
Připomeňme si nejdříve, jakým způsobem se prochází CST stromem s využitím třídy odvozené od třídy CSTVisitor. Tímto tématem jsme se podrobněji zabývali v úvodním článku o knihovně LibCST. Ze třídy CSTVisitor si odvodíme vlastní třídu nazvanou například Visitor. Tato třída bude (samozřejmě kromě konstruktoru __init__) obsahovat metody nazvané on_visit a on_leave. První z těchto metod je volána při návštěvě uzlu při průchodu CST, druhá metoda je volána naopak při opuštění tohoto uzlu, tedy ve chvíli, kdy již došlo k průchodu všemi uzlu podstromu daného uzlu (pochopitelně za předpokladu, že takový podstrom existuje):
class Visitor(CSTVisitor): def __init__(self): ... ... ... def on_visit(self, node): ... ... ... return True nebo False def on_leave(self, node): self.nest_level -= 1
Význam první metody on_visit spočívá v tom, že její návratovou (pravdivostní) hodnotou lze řídit, zda se má projít i všemi poduzly či nikoli (někdy nás totiž například nezajímá průchod všemi prvky výrazu či těla funkce). Metoda on_leave zde zdánlivě postrádá smysl, ale v dalších kapitolách uvidíme, že pokud nahradíme CSTVisitor za CSTTransformer, umožní nám modifikaci uzlu, jeho náhradu či v některých případech dokonce jeho odstranění ze stromu (nahradí se za sentinel).
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTVisitor class Visitor(CSTVisitor): def __init__(self): self.nest_level = 0 def on_visit(self, node): indent = " " * self.nest_level * 2 print(indent, node.__class__.__name__) self.nest_level += 1 return True def on_leave(self, node): self.nest_level -= 1 expression = "1 + 2 * 3 - 4 / 5" parsed = parse_module(expression) visitor = Visitor() parsed.visit(visitor)
Průchod stromem (CST) vypadá následovně (odsazení je řízeno atributem nest_level):
Module SimpleStatementLine Expr BinaryOperation BinaryOperation Integer Add SimpleWhitespace SimpleWhitespace BinaryOperation Integer Multiply SimpleWhitespace SimpleWhitespace Integer Subtract SimpleWhitespace SimpleWhitespace BinaryOperation Integer Divide SimpleWhitespace SimpleWhitespace Integer TrailingWhitespace SimpleWhitespace Newline
2. Průchod atributy všech uzlů ve stromu
Kromě metod on_visit a on_leave je možné definovat i metody nazvané on_visit_attribute a on_leave_attribute. Jak již názvy těchto metod naznačují, jsou tyto metody zavolány pro všechny atributy každého uzlu, kterým se prochází. Kromě vlastního uzlu je těmto metodám předán i příslušný atribut. A je zde ještě jedna výjimka – návratová hodnota metody on_visit_attribute neurčuje způsob průchodu atributy – metoda se zavolá pro všechny atributy uzlu (naproti tomu návratovou hodnotou metody on_visit je možné řídit způsob průchodu stromem).
Zkusme si tedy předchozí demonstrační příklad nepatrně upravit tak, aby se procházelo všemi uzly stromu a všemi atributy uzlů:
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTVisitor class Visitor(CSTVisitor): def __init__(self): self.nest_level = 0 def on_visit(self, node): indent = " " * self.nest_level * 2 print(indent, node.__class__.__name__) self.nest_level += 1 return True def on_leave(self, node): self.nest_level -= 1 def on_visit_attribute(self, node, attribute): indent = " " * (self.nest_level + 1) * 2 print(indent, "-> attribute", attribute) def on_leave_attribute(self, node, attribute): pass expression = "1 + 2 * 3 - 4 / 5" parsed = parse_module(expression) visitor = Visitor() parsed.visit(visitor)
V tomto případě bude výsledek při průchodu celým stromem mnohem delší, protože prakticky každý uzel má jeden či více atributů:
Module -> attribute header -> attribute body SimpleStatementLine -> attribute leading_lines -> attribute body Expr -> attribute value BinaryOperation -> attribute lpar -> attribute left BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Add -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Multiply -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute operator Subtract -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Divide -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute semicolon -> attribute trailing_whitespace TrailingWhitespace -> attribute whitespace SimpleWhitespace -> attribute comment -> attribute newline Newline -> attribute footer
3. Vytištění zdrojového kódu získaného zpětným převodem z CST
Knihovna LibCST dokáže, jak jsme již ostatně viděli, převést zdrojový kód na CST (tedy na derivační strom). S tímto stromem lze provádět různé manipulace spočívající v modifikaci uzlů, přidání uzlů, odstranění uzlů a popř. úpravě atributů. A nakonec pochopitelně potřebujeme provést i opak první operace – tedy převod CST zpět na zdrojový kód, a to se zachováním formátování i poznámek. I tuto operaci pochopitelně knihovna LibCST podporuje. Zdrojový kód lze z CST vygenerovat přečtením atributu code (to není zcela přesné, protože třída CSTVisitor neumožňuje modifikace stromu, ale jak uvidíme dále, třída CSTTransformer již ano).
Pokusme se tedy upravit skript z předchozí kapitoly takovým způsobem, aby na konci vypsal i kód získaný z CST:
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTVisitor class Visitor(CSTVisitor): def __init__(self): self.nest_level = 0 def on_visit(self, node): indent = " " * self.nest_level * 2 print(indent, node.__class__.__name__) self.nest_level += 1 return True def on_leave(self, node): self.nest_level -= 1 def on_visit_attribute(self, node, attribute): indent = " " * (self.nest_level + 1) * 2 print(indent, "-> attribute", attribute) def on_leave_attribute(self, node, attribute): pass expression = "1 + 2 * 3 - 4 / 5" parsed = parse_module(expression) visitor = Visitor() parsed.visit(visitor) print() print("-" * 60) print(parsed.code)
Tento skript nejdříve vypíše obsah stromu tak, jak jsme to již viděli:
Module -> attribute header -> attribute body SimpleStatementLine -> attribute leading_lines -> attribute body Expr -> attribute value BinaryOperation -> attribute lpar -> attribute left BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Add -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Multiply -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute operator Subtract -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Divide -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute semicolon -> attribute trailing_whitespace TrailingWhitespace -> attribute whitespace SimpleWhitespace -> attribute comment -> attribute newline Newline -> attribute footer
A nakonec se vypíše zdrojový kód reprezentovaný daným stromem:
1 + 2 * 3 - 4 / 5
4. Modifikace stromu při jeho průchodu s využitím třídy CSTTransformer
Nyní se dostáváme k nejdůležitější funkcionalitě nabízené knihovnou LibCST, tj. k podpoře modifikace stromu a tím pádem i řízené modifikace zdrojového kódu, který je stromem reprezentován. Aby bylo možné CST modifikovat, musíme naši třídu odvodit nikoli ze třídy CSTVisitor, ale ze třídy CSTTransformer. V této třídě došlo ke změně významu metody on_leave. Nyní jsou totiž této metodě předány dva uzly – původní uzel a uzel, který lze modifikovat nebo nahradit za uzel jiný. Návratovou hodnotou této metody je uzel, který je vložen do CST namísto uzlu původního. Tento koncept nám umožňuje měnit atributy uzlů, náhradu uzlu za uzel jiný, změnu podstromu (k tomu se dostaneme příště), popř. je možné uzel nahradit za sentinel a tak ho ve výsledku odstranit.
Předchozí demonstrační příklad nejdříve upravíme takovým způsobem, že namísto třídy Visitor odvozené od třídy CSTVisitor nadeklarujeme třídu nazvanou například Transformer a odvodíme ji od třídy CSTTransformer. Další změna spočívá v odlišných parametrech metody on_leave i v tom, že z této metody budeme vracet původní uzel (žádný strom tedy prozatím nebudeme modifikovat, i když používáme CSTTransformer):
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTTransformer class Transformer(CSTTransformer): def __init__(self): self.nest_level = 0 def on_visit(self, node): indent = " " * self.nest_level * 2 print(indent, node.__class__.__name__) self.nest_level += 1 return True def on_leave(self, original_node, updated_node): self.nest_level -= 1 return original_node def on_visit_attribute(self, node, attribute): indent = " " * (self.nest_level + 1) * 2 print(indent, "-> attribute", attribute) def on_leave_attribute(self, node, attribute): pass expression = "1 + 2 * 3 - 4 / 5" parsed = parse_module(expression) transformer = Transformer() transformed = parsed.visit(transformer) print() print("-" * 60) print(transformed.code)
Po spuštění skriptu bychom měli získat naprosto stejný výsledek, jako při použití třídy CSTVisitor:
Module -> attribute header -> attribute body SimpleStatementLine -> attribute leading_lines -> attribute body Expr -> attribute value BinaryOperation -> attribute lpar -> attribute left BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Add -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Multiply -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute operator Subtract -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Divide -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute semicolon -> attribute trailing_whitespace TrailingWhitespace -> attribute whitespace SimpleWhitespace -> attribute comment -> attribute newline Newline -> attribute footer ------------------------------------------------------------ 1 + 2 * 3 - 4 / 5
5. Speciální metody volané při vstupu či opuštění uzlů určitých typů
V předchozích demonstračních příkladech jsme využívali obecné metody nazvané on_visit a on_leave. Tyto metody byly volány při návštěvě každého uzlu při traverzaci CST, popř. naopak při opouštění uzlu s tím, že právě při opouštění uzlu bylo možné provést jeho záměnu či modifikaci. Ovšem v těchto metodách bylo (většinou) nutné zjišťovat typ uzlu, tj. například to, zda se jedná o uzel reprezentující řetězec, jméno proměnné, jméno volané funkce, typ operátoru atd. A to vede ke zbytečně dlouhému a repetitivnímu kódu. Z tohoto důvodu nalezneme v knihovně LibCST ještě jednu možnost – pro každý typ uzlu jsou totiž definovány specializované metody volané pouze při vstupu či naopak výstupu z konkrétního typu uzlu.
Příkladem může být uzel typu Multiply, tedy uzel reprezentující operaci násobení. Při vstupu do tohoto uzlu se volá metoda visit_Multiply (pokud existuje) a při výstupu metoda leave_Multiply:
def visit_Multiply(self, original_node): print("multiply node visit") return True def leave_Divide(self, original_node, updated_node): print("multiply node leave") return original_node
Jména všech metod volaných při vstupu do uzlu jsem extrahoval do tohoto souboru. A jména všech metod volaných naopak při opouštění uzlu daného typu naleznete zde.
6. Záměna jména zvolené proměnné za jiné jméno
Nyní již máme k dispozici všechny informace proto, abychom realizovali průchod uzly CST s tím, že pokud budeme opouštět uzel typu Name a hodnota jména bude nastavena na „foo“, nahradíme tento uzel uzlem, v němž bude hodnota jména nastavená na „bar“. Jinými slovy provedeme takovou transformaci zdrojového kódu, při níž dojde k náhradě všech identifikátorů foo na bar. Tato záměna se však nebude týkat ani řetězců, ani komentářů. Samotná realizace popsané transformace vypadá takto:
def leave_Name(self, original_node, updated_node): if original_node.value == "foo": print("Renaming 'foo' to 'bar'") return updated_node.with_changes(value="bar") return original_node
Transformovat přitom budeme tento výraz:
expression = "1 + foo * 3 - 4 / foo"
Celý skript, který tuto transformaci provádí, bude vypadat následovně:
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTTransformer from libcst import SimpleWhitespace, Name class Transformer(CSTTransformer): def __init__(self): pass def on_visit(self, node): print(node.__class__.__name__) return True def leave_Name(self, original_node, updated_node): if original_node.value == "foo": print("Renaming 'foo' to 'bar'") return updated_node.with_changes(value="bar") return original_node def on_visit_attribute(self, node, attribute): print("-> attribute", attribute) def on_leave_attribute(self, node, attribute): pass expression = "1 + foo * 3 - 4 / foo" parsed = parse_module(expression) transformer = Transformer() transformed = parsed.visit(transformer) print() print("-" * 60) print(parsed.code) print(transformed.code)
7. Průběh transformace, zobrazení výsledného výrazu po transformaci
V případě, že výše uvedený skript spustíme, vypíšou se postupně informace o tom, že se prochází jednotlivými uzly i jejich atributy:
Module -> attribute header -> attribute body SimpleStatementLine -> attribute leading_lines -> attribute body Expr -> attribute value BinaryOperation -> attribute lpar -> attribute left BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Add -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Name -> attribute lpar -> attribute rpar
Jakmile se opustí uzel typu Name s hodnotou jména „foo“, provede se záměna tohoto uzlu, o čemž jsme taktéž informováni:
Renaming 'foo' to 'bar'
Pokračování v průchodu CST:
-> attribute operator Multiply -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute operator Subtract -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Divide -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Name -> attribute lpar -> attribute rpar
Další přejmenování:
Renaming 'foo' to 'bar'
Skript posléze dokončí průchod CST:
-> attribute rpar -> attribute rpar -> attribute semicolon -> attribute trailing_whitespace TrailingWhitespace -> attribute whitespace SimpleWhitespace -> attribute comment -> attribute newline Newline -> attribute footer
V samotném závěru se vypíše původní tvar zdrojového kódu a následně tvar upravený (tedy s přejmenovanými identifikátory):
------------------------------------------------------------ 1 + foo * 3 - 4 / foo 1 + bar * 3 - 4 / bar
8. Zobrazení rozdílů mezi původní a transformovaným kódem s využitím knihovny difflib
V případě, že se přes CST provádí složitější transformace kódu, nebude dostačující pouze zobrazit původní kód a pod ním kód modifikovaný. Výhodnější je v takovém případě použít zobrazení formou diffu, resp. přesněji unifikovaného diffu (viz též tento článek o nástroji Diff. V programovacím jazyku Python můžeme pro tento účel použít standardní knihovnu difflib tak, jak je to ukázáno v dalším demonstračním příkladu. Ten se od příkladu předchozího odlišuje pouze posledními příkazy, v nichž zobrazíme původní kód, pod ním modifikovaný kód a na konci rozdíly mezi oběma kódy formou unifikovaného diffu:
print(parsed.code) print(transformed.code) diff = "".join(unified_diff(parsed.code.splitlines(1), transformed.code.splitlines(1))) print(diff)
Úplný zdrojový kód takto upraveného demonstračního příkladu vypadá následovně:
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTTransformer from libcst import SimpleWhitespace, Name from difflib import unified_diff class Transformer(CSTTransformer): def __init__(self): pass def on_visit(self, node): print(node.__class__.__name__) return True def leave_Name(self, original_node, updated_node): if original_node.value == "foo": print("Renaming 'foo' to 'bar'") return updated_node.with_changes(value="bar") return original_node def on_visit_attribute(self, node, attribute): print("-> attribute", attribute) def on_leave_attribute(self, node, attribute): pass expression = "1 + foo * 3 - 4 / foo\n" parsed = parse_module(expression) transformer = Transformer() transformed = parsed.visit(transformer) print() print("-" * 60) print(parsed.code) print(transformed.code) diff = "".join(unified_diff(parsed.code.splitlines(1), transformed.code.splitlines(1))) print(diff)
Výsledky získané po spuštění skriptu (pro stručnost vypustím zprávy o návštěvách jednotlivých uzlů):
------------------------------------------------------------ 1 + foo * 3 - 4 / foo 1 + bar * 3 - 4 / bar --- +++ @@ -1 +1 @@ -1 + foo * 3 - 4 / foo +1 + bar * 3 - 4 / bar
Řádky „---“ a „+++“ začíná porovnání souborů ve formě unifikovaného diffu (což je pro jednořádkový „program“ sice poněkud zbytečné, ale v dalších kapitolách již budeme modifikovat delší zdrojové kódy).
9. Vylepšení třídy pro přejmenování symbolů
První varianta třídy určené pro přejmenování symbolů obsahovala jak původní jméno symbolu, tak i nové jméno symbolu přímo ve svém programovém kódu, což není příliš obecné:
class SymbolRenamer(CSTTransformer): ... ... ... def leave_Name(self, original_node, updated_node): if original_node.value == "foo": print("Renaming 'foo' to 'bar'") return updated_node.with_changes(value="bar") return original_node ... ... ...
Výhodnější a obecnější bude, aby bylo nahrazované jméno i jeho náhrada uloženy v atributech objektu:
class SymbolRenamer(CSTTransformer): ... ... ... def leave_Name(self, original_node, updated_node): if original_node.value == self.orig_name: print(f"Renaming '{self.orig_name}' to '{self.new_name}'") return updated_node.with_changes(value=self.new_name) return original_node ... ... ...
Příslušné atributy se nastaví v konstruktoru:
def __init__(self, orig_name, new_name): self.orig_name = orig_name self.new_name = new_name
A samotná transformace (což je vlastně jednoduchý refaktoring) bude vyvolána takto:
transformer = SymbolRenamer("foo", "baz") transformed = parsed.visit(transformer)
Pro úplnost si ukažme, jak bude vypadat zdrojový kód takto upraveného skriptu:
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTTransformer from libcst import SimpleWhitespace, Name from difflib import unified_diff class SymbolRenamer(CSTTransformer): def __init__(self, orig_name, new_name): self.orig_name = orig_name self.new_name = new_name def on_visit(self, node): print(node.__class__.__name__) return True def leave_Name(self, original_node, updated_node): if original_node.value == self.orig_name: print(f"Renaming '{self.orig_name}' to '{self.new_name}'") return updated_node.with_changes(value=self.new_name) return original_node def on_visit_attribute(self, node, attribute): print("-> attribute", attribute) def on_leave_attribute(self, node, attribute): pass expression = "1 + foo * 3 - 4 / bar\n" parsed = parse_module(expression) transformer = SymbolRenamer("foo", "baz") transformed = parsed.visit(transformer) print() print("-" * 60) print(parsed.code) print(transformed.code) diff = "".join(unified_diff(parsed.code.splitlines(1), transformed.code.splitlines(1))) print(diff)
10. Otestování funkcionality
Demonstrační příklad z předchozí kapitoly by se měl po svém spuštění chovat naprosto stejným způsobem jako původní třída SymbolRenamer. Opět si to můžeme velmi snadno ověřit spuštěním příslušného skriptu:
Module -> attribute header -> attribute body SimpleStatementLine -> attribute leading_lines -> attribute body Expr -> attribute value BinaryOperation -> attribute lpar -> attribute left BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Add -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Name -> attribute lpar -> attribute rpar Renaming 'foo' to 'baz' -> attribute operator Multiply -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute operator Subtract -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Divide -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right Name -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute semicolon -> attribute trailing_whitespace TrailingWhitespace -> attribute whitespace SimpleWhitespace -> attribute comment -> attribute newline Newline -> attribute footer ------------------------------------------------------------ 1 + foo * 3 - 4 / bar 1 + baz * 3 - 4 / bar --- +++ @@ -1 +1 @@ -1 + foo * 3 - 4 / bar +1 + baz * 3 - 4 / bar
11. Komplikovanější transformace
Prozatím jsme si ukázali pouze ten nejtriviálnější způsob transformace programového kódu na úrovni CST. Tato transformace spočívala v modifikaci nějakého atributu nalezeného uzlu. Ovšem v praxi lze provádět i složitější transformace, zejména pak:
- Náhradu jednoho typu uzlu za uzel odlišného typu. Tato operace ovšem ne vždy dává smysl, ovšem i takové příklady lze najít.
- Odstranění uzlu ze stromu (CST), což se provádí náhradou původního uzlu za takzvaný sentinel. Opět platí, že tato náhrada není vždy možná; záleží na konkrétním typu uzlu a na jeho umístění v CST.
- Namísto uzlu se do CST vloží celý podstrom, což odpovídá složitějšímu refaktoringu.
Dnes si ukážeme ještě způsob náhrady uzlu jednoho typu za uzel odlišného typu. Složitějšími transformacemi se budeme podrobněji zabývat v závěrečném článku této série.
12. Záměna zvolených binárních aritmetických operátorů
Vyzkoušejme si nyní poněkud umělý příklad. Budeme v něm provádět záměnu dvou zvolených binárních aritmetických operátorů, konkrétně operátoru součinu a operátoru podílu. To tedy znamená, že každý součin bude nahrazen za podíl a naopak každý podíl bude nahrazen součinem. Realizace takové náhrady bude v tomto případě jednoduchá, protože namísto původního uzlu vrátíme uzel odlišný (typu Multiply či Divide):
def leave_Multiply(self, original_node, updated_node): print("Replacing multiply by divide") return Divide() def leave_Divide(self, original_node, updated_node): print("Replacing divide by multiply") return Multiply()
Transformaci si otestujeme na výrazu, v němž je použita jak operace součinu, tak i operace podílu:
1 + 2 * 3 - 4 / 5
Podívejme se nyní na úplný zdrojový kód skriptu, který tuto transformaci provádí:
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTTransformer from libcst import SimpleWhitespace, Name from libcst import Multiply, Divide from difflib import unified_diff class BinaryOpReplacer(CSTTransformer): def __init__(self): pass def on_visit(self, node): print(node.__class__.__name__) return True def leave_Multiply(self, original_node, updated_node): print("Replacing multiply by divide") return Divide() def leave_Divide(self, original_node, updated_node): print("Replacing divide by multiply") return Multiply() def on_visit_attribute(self, node, attribute): print("-> attribute", attribute) def on_leave_attribute(self, node, attribute): pass expression = "1 + 2 * 3 - 4 / 5\n" parsed = parse_module(expression) transformer = BinaryOpReplacer() transformed = parsed.visit(transformer) print() print("-" * 60) print(parsed.code) print(transformed.code) diff = "".join(unified_diff(parsed.code.splitlines(1), transformed.code.splitlines(1))) print(diff)
13. Průběh transformace a tvar výsledného zdrojového kódu
Opět pochopitelně můžeme sledovat průběh transformace:
Module -> attribute header -> attribute body SimpleStatementLine -> attribute leading_lines -> attribute body Expr -> attribute value BinaryOperation -> attribute lpar -> attribute left BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Add -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Multiply -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace
Ve zprávách se objevuje i informace o náhradě uzlu za jiný typ:
Replacing multiply by divide
-> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute operator Subtract -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace -> attribute right BinaryOperation -> attribute lpar -> attribute left Integer -> attribute lpar -> attribute rpar -> attribute operator Divide -> attribute whitespace_before SimpleWhitespace -> attribute whitespace_after SimpleWhitespace
Další náhrada, tentokrát ovšem odlišná:
Replacing divide by multiply
-> attribute right Integer -> attribute lpar -> attribute rpar -> attribute rpar -> attribute rpar -> attribute semicolon -> attribute trailing_whitespace TrailingWhitespace -> attribute whitespace SimpleWhitespace -> attribute comment -> attribute newline Newline -> attribute footer
Nakonec se vypíše původní tvar výrazu a jeho nový tvar (s korektně prohozenými operátory):
1 + 2 * 3 - 4 / 5 1 + 2 / 3 - 4 * 5
A na úplný závěr pak unifikovaný diff původního a modifikovaného zdrojového kódu:
--- +++ @@ -1 +1 @@ -1 + 2 * 3 - 4 / 5 +1 + 2 / 3 - 4 * 5
14. Přejmenování jména volané funkce
Mnohem užitečnější, než náhrada operátorů, bude mnohem častěji prováděná transformace zdrojového kódu. Ta bude spočívat ve změně jména volané funkce. Můžeme například chtít nahradit volání funkce foo za volání funkce bar. Pro tento účel postačuje do transformátoru přidat metodu leave_Call volanou při opouštění uzlu typu Call. A tento uzel v atributu func.value obsahuje jméno volané metody:
class FunctionRenamer(CSTTransformer): ... ... ... def leave_Call(self, original_node, updated_node): print("Function call: ", original_node.func.value) if original_node.func.value == self.orig_name: print(f"Renaming '{self.orig_name}' to '{self.new_name}'") return updated_node.with_changes( func=Name(self.new_name)) return original_node
Budeme nahrazovat jméno funkce v tomto kódu:
def A(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return A(m - 1, 1) return A(m - 1, A(m, n - 1))
Jméno volané funkce A nahradíme za ackermann:
parsed = parse_module(code) transformer = FunctionRenamer("A", "ackermann") transformed = parsed.visit(transformer)
A takto vypadá úplný tvar skriptu, který tuto transformaci provede:
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTTransformer from libcst import SimpleWhitespace, Name from difflib import unified_diff class FunctionRenamer(CSTTransformer): def __init__(self, orig_name, new_name): self.orig_name = orig_name self.new_name = new_name def on_visit(self, node): return True def leave_Call(self, original_node, updated_node): print("Function call: ", original_node.func.value) if original_node.func.value == self.orig_name: print(f"Renaming '{self.orig_name}' to '{self.new_name}'") return updated_node.with_changes( func=Name(self.new_name)) return original_node code = ''' def A(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return A(m - 1, 1) return A(m - 1, A(m, n - 1)) ''' parsed = parse_module(code) transformer = FunctionRenamer("A", "ackermann") transformed = parsed.visit(transformer) print() print("-" * 60) print(parsed.code) print(transformed.code) diff = "".join(unified_diff(parsed.code.splitlines(1), transformed.code.splitlines(1))) print(diff)
15. Průběh transformace a tvar výsledného zdrojového kódu
Nyní se podívejme na průběh transformace zdrojového kódu ve chvíli, kdy spustíme skript z předchozí kapitoly. Nejdříve se vypíšou tři zprávy o tom, že bylo nalezeno volání funkce A a toto volání bylo nahrazeno za volání funkce ackermann:
Function call: A Renaming 'A' to 'ackermann' Function call: A Renaming 'A' to 'ackermann' Function call: A Renaming 'A' to 'ackermann'
Po těchto třech modifikacích CST se vypíše původní zdrojový kód (získaný vygenerováním z původního CST) i modifikovaný zdrojový kód (získaný vygenerováním z nového CST):
def A(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return A(m - 1, 1) return A(m - 1, A(m, n - 1)) def A(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return ackermann(m - 1, 1) return ackermann(m - 1, ackermann(m, n - 1))
Následně se zobrazí rozdíly mezi oběma kódy ve formě unifikovaného diffu. Tyto rozdíly vypadají takto:
--- +++ @@ -4,5 +4,5 @@ if m == 0: return n + 1 if n == 0: - return A(m - 1, 1) - return A(m - 1, A(m, n - 1)) + return ackermann(m - 1, 1) + return ackermann(m - 1, ackermann(m, n - 1))
16. Přejmenování jména funkce v její definici
Předchozí demonstrační příklad transformoval zdrojový kód takovým způsobem, že nahradil všechny výskyty volání funkce A za volání funkce ackermann. Jenže musíme ještě nahradit i jméno této funkce přímo v její definici. K tomu můžeme využít modifikaci uzlu typu FunctionDef, a to při opouštění tohoto uzlu při traverzaci stromem. Vytvoříme si tedy metodu nazvanou leave_FunctionDef, která slouží přesně k tomuto účelu. V této metodě posléze otestujeme, zda atribut name obsahuje jméno A a pokud tomu tak je, nahradíme toto jméno za ackermann a vrátíme takto upravený uzel:
class FunctionRenamer(CSTTransformer): ... ... ... def leave_FunctionDef(self, original_node, updated_node): print("Function definition: ", original_node.name.value) if original_node.name.value == self.orig_name: print(f"Renaming '{self.orig_name}' to '{self.new_name}'") return updated_node.with_changes( name=Name(self.new_name)) return original_node ... ... ...
Úplný zdrojový kód takto upraveného příkladu vypadá následovně:
#!/usr/bin/python # vim: set fileencoding=utf-8 from libcst import parse_module, CSTTransformer from libcst import SimpleWhitespace, Name from difflib import unified_diff class FunctionRenamer(CSTTransformer): def __init__(self, orig_name, new_name): self.orig_name = orig_name self.new_name = new_name def on_visit(self, node): return True def leave_FunctionDef(self, original_node, updated_node): print("Function definition: ", original_node.name.value) if original_node.name.value == self.orig_name: print(f"Renaming '{self.orig_name}' to '{self.new_name}'") return updated_node.with_changes( name=Name(self.new_name)) return original_node def leave_Call(self, original_node, updated_node): print("Function call: ", original_node.func.value) if original_node.func.value == self.orig_name: print(f"Renaming '{self.orig_name}' to '{self.new_name}'") return updated_node.with_changes( func=Name(self.new_name)) return original_node code = ''' def A(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return A(m - 1, 1) return A(m - 1, A(m, n - 1)) ''' parsed = parse_module(code) transformer = FunctionRenamer("A", "ackermann") transformed = parsed.visit(transformer) print() print("-" * 60) print(parsed.code) print(transformed.code) diff = "".join(unified_diff(parsed.code.splitlines(1), transformed.code.splitlines(1))) print(diff)
17. Spuštění příkladu a ukázka modifikovaného zdrojového kódu
Pokud transformaci kódu popsanou v předchozí kapitole spustíme, vypíšou se nejdříve zprávy o tom, že se našly a modifikovaly tři volání funkce A a že se tato volání nahradila za funkci ackermann. Dále, na posledním místě, je zobrazena zpráva o nalezení definice funkce A s tím, že i toto jméno bylo nahrazeno. Proč se ovšem tato zpráva vypíše jako poslední? V kódu totiž reagujeme na operaci opuštění uzlu, nikoli vstupu do uzlu (a nejdříve se opustí všechny poduzly a až poté nadřazený uzel):
Function call: A Renaming 'A' to 'ackermann' Function call: A Renaming 'A' to 'ackermann' Function call: A Renaming 'A' to 'ackermann' Function definition: A Renaming 'A' to 'ackermann'
Poté se vypíše originální zdrojový kód získaný zpětnou transformací původního CST:
def A(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return A(m - 1, 1) return A(m - 1, A(m, n - 1))
A následně se vypíše kód získaný zpětnou transformací nového CST. Povšimněte si, že došlo k náhradě jména funkce ve všech částech programu:
def ackermann(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: return ackermann(m - 1, 1) return ackermann(m - 1, ackermann(m, n - 1))
A v posledním kroku se zobrazí rozdíly mezi původním kódem a kódem novým; používá se zde unifikovaný diff:
--- +++ @@ -1,8 +1,8 @@ -def A(m: int, n: int) -> int: +def ackermann(m: int, n: int) -> int: """Ackermannova funkce.""" if m == 0: return n + 1 if n == 0: - return A(m - 1, 1) - return A(m - 1, A(m, n - 1)) + return ackermann(m - 1, 1) + return ackermann(m - 1, ackermann(m, n - 1))
18. Obsah poslední části článku
Ve třetí a současně i závěrečné části článku o knihovně LibCST se seznámíme s některými složitějšími transformacemi kódu. Transformace, které jsme prováděli až doposud, totiž nepotřebovaly znát kontext (náhrady se prováděly pro celý zdrojový kód bez ohledu na předchozí transformace atd.). Ovšem v praxi si s těmito typy transformací nevystačíme, takže se příště podíváme na sice složitější, ale o to užitečnější realizaci sofistikovanějšího refaktoringu.
19. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro programovací jazyk Python 3 a knihovnu libcst byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs:
20. Odkazy na Internetu
- Lexikální a syntaktická analýza zdrojových kódů programovacího jazyka Python
https://www.root.cz/clanky/lexikalni-a-syntakticka-analyza-zdrojovych-kodu-programovaciho-jazyka-python/ - Lexikální a syntaktická analýza zdrojových kódů programovacího jazyka Python (2.část)
https://www.root.cz/clanky/lexikalni-a-syntakticka-analyza-zdrojovych-kodu-programovaciho-jazyka-python-2-cast/ - Lexikální a syntaktická analýza zdrojových kódů programovacího jazyka Python (3.část)
https://www.root.cz/clanky/lexikalni-a-syntakticka-analyza-zdrojovych-kodu-programovaciho-jazyka-python-3-cast/ - Lexikální a syntaktická analýza zdrojových kódů jazyka Python (4.část)
https://www.root.cz/clanky/lexikalni-a-syntakticka-analyza-zdrojovych-kodu-jazyka-python-4-cast/ - Knihovna LibCST umožňující snadnou modifikaci zdrojových kódů Pythonu
https://www.root.cz/clanky/knihovna-libcst-umoznujici-snadnou-modifikaci-zdrojovych-kodu-pythonu/ - LibCST – dokumentace
https://libcst.readthedocs.io/en/latest/index.html - libCST na PyPi
https://pypi.org/project/libcst/ - libCST na GitHubu
https://github.com/Instagram/LibCST - Inside The Python Virtual Machine
https://leanpub.com/insidethepythonvirtualmachine - module-py_compile
https://docs.python.org/3.8/library/py_compile.html - Given a python .pyc file, is there a tool that let me view the bytecode?
https://stackoverflow.com/questions/11141387/given-a-python-pyc-file-is-there-a-tool-that-let-me-view-the-bytecode - The structure of .pyc files
https://nedbatchelder.com/blog/200804/the_structure_of_pyc_files.html - Python Bytecode: Fun With Dis
http://akaptur.github.io/blog/2013/08/14/python-bytecode-fun-with-dis/ - Python's Innards: Hello, ceval.c!
http://tech.blog.aknin.name/category/my-projects/pythons-innards/ - Byterun
https://github.com/nedbat/byterun - Python Byte Code Instructions
http://document.ihg.uni-duisburg.de/Documentation/Python/lib/node56.html - Python Byte Code Instructions
https://docs.python.org/3.2/library/dis.html#python-bytecode-instructions - dis – Python module
https://docs.python.org/2/library/dis.html - Comparison of Python virtual machines
http://polishlinux.org/apps/cli/comparison-of-python-virtual-machines/ - O-code
http://en.wikipedia.org/wiki/O-code_machine - Abstract syntax tree
https://en.wikipedia.org/wiki/Abstract_syntax_tree - Lexical analysis
https://en.wikipedia.org/wiki/Lexical_analysis - Parser
https://en.wikipedia.org/wiki/Parsing#Parser - Parse tree
https://en.wikipedia.org/wiki/Parse_tree - Derivační strom
https://cs.wikipedia.org/wiki/Deriva%C4%8Dn%C3%AD_strom - Python doc: ast — Abstract Syntax Trees
https://docs.python.org/3/library/ast.html - Python doc: tokenize — Tokenizer for Python source
https://docs.python.org/3/library/tokenize.html - SymbolTable
https://docs.python.org/3.8/library/symtable.html - 5 Amazing Python AST Module Examples
https://www.pythonpool.com/python-ast/ - Intro to Python ast Module
https://medium.com/@wshanshan/intro-to-python-ast-module-bbd22cd505f7 - Golang AST Package
https://golangdocs.com/golang-ast-package - AP8, IN8 Regulární jazyky
http://statnice.dqd.cz/home:inf:ap8 - AP9, IN9 Konečné automaty
http://statnice.dqd.cz/home:inf:ap9 - AP10, IN10 Bezkontextové jazyky
http://statnice.dqd.cz/home:inf:ap10 - AP11, IN11 Zásobníkové automaty, Syntaktická analýza
http://statnice.dqd.cz/home:inf:ap11 - Introduction to YACC
https://www.geeksforgeeks.org/introduction-to-yacc/ - Introduction of Lexical Analysis
https://www.geeksforgeeks.org/introduction-of-lexical-analysis/?ref=lbp - Využití knihovny Pygments (nejenom) pro obarvení zdrojových kódů
https://www.root.cz/clanky/vyuziti-knihovny-pygments-nejenom-pro-obarveni-zdrojovych-kodu/ - Pygments – Python syntax highlighter
http://pygments.org/ - Pygments (dokumentace)
http://pygments.org/docs/ - Write your own filter
http://pygments.org/docs/filterdevelopment/ - Write your own lexer
http://pygments.org/docs/lexerdevelopment/ - Write your own formatter
http://pygments.org/docs/formatterdevelopment/ - Jazyky podporované knihovnou Pygments
http://pygments.org/languages/ - Pygments FAQ
http://pygments.org/faq/ - Compiler Construction/Lexical analysis
https://en.wikibooks.org/wiki/Compiler_Construction/Lexical_analysis - Compiler Design – Lexical Analysis
https://www.tutorialspoint.com/compiler_design/compiler_design_lexical_analysis.htm - Lexical Analysis – An Intro
https://www.scribd.com/document/383765692/Lexical-Analysis - Python AST Visualizer
https://github.com/pombredanne/python-ast-visualizer - What is an Abstract Syntax Tree
https://blog.bitsrc.io/what-is-an-abstract-syntax-tree-7502b71bde27 - Why is AST so important
https://medium.com/@obernardovieira/why-is-ast-so-important-b1e7d6c29260 - Emily Morehouse-Valcarcel – The AST and Me – PyCon 2018
https://www.youtube.com/watch?v=XhWvz4dK4ng - Python AST Parsing and Custom Linting
https://www.youtube.com/watch?v=OjPT15y2EpE - Chase Stevens – Exploring the Python AST Ecosystem
https://www.youtube.com/watch?v=Yq3wTWkoaYY - Full Grammar specification
https://docs.python.org/3/reference/grammar.html