V tomto článku se podíváme na to, jak je implementován modul RefactoringNG, a vysvětlíme si syntaxi jeho refaktorizačních pravidel.
Překladač javac
Nejprve něco o tom, jak funguje překladač javac
. Překladač
zpracovává
zdrojový kód v několika krocích. Nejprve se text převede na posloupnost
lexikálních
symbolů. Např. class Main { }
se převede na
KEYWORD_CLASS, IDENTIFIER, LEFT_BRACE, RIGHT_BRACE
.
Této fázi se říká lexikální analýza. V další fázi, kterou nazýváme
syntaktická analýza, se kontroluje, zda jsou lexikální symboly správně za sebou.
Lexikální analýza
tedy provádí předzpracování zdrojového kódu pro syntaktickou analýzu.
Během syntaktické analýzy dojde k postavení stromu, ze kterého se bude
později generovat
výstupní kód. Tento strom se nazývá abstraktní syntaktický strom
(zkráceně AST z anglického abstract syntax tree).
Na syntaktickou analýzu navazuje analýza sémantická, která kontroluje
např. to, zda byla použitá proměnná deklarována. Výstupem sémantické
analýzy je strom, ve kterém má každý identifikátor vazbu na svoji deklaraci. Tj. např.
pro každou
proměnnou známe její typ. Překladač javac
zpřístupňuje AST přes
Compiler
Tree API.
Protože v AST jsou všechny podstatné informace ze zdrojového
kódu, lze provést i opačnou transformaci a AST převést na zdrojový kód.
Toho využívají NetBeans, které implementují refactoring javovského
kódu na úrovni AST. Když v NetBeans např. přejmenujete
metodu, provede se přejmenování v AST a pak se z AST vygeneruje zdrojový
kód, ve kterém je nové jméno. Navíc NetBeans nabízejí API (balík
org.netbeans.api.java.source), přes které lze AST měnit.
RefactoringNG
RefactoringNG používá toto API pro refactoring javovského kódu. Zjednodušeně řečeno, RefactoringNG umí pouze přepsat jeden AST na jiný. Tomu odpovídá zápis refaktorizačních pravidel. Každé pravidlo má tvar
Pattern -> Rewrite
kde Pattern
je původní AST a Rewrite
je nový
AST.
Pattern
i Rewrite
mají stejnou strukturu:
Tree attributes content
kde attributes
jsou uzavřeny do hranatých závorek a
oddělují se čárkou. Atributy jsou pojmenovány stejně jako vlastnosti AST v javac
.
Např. atribut kind
v zápise
Literal [kind: NULL_LITERAL]
říká, že jde o literál null
. Pokud nějaký atribut není
uveden, může mít libovolnou hodnotu. Např.
Identifier
představuje libovolný identifikátor a
Literal [kind: INT_LITERAL]
je libovolný literál typu int
.
Naproti tomu
Identifier [name: "answer"]
je identifikátor answer
a
Literal [kind: INT_LITERAL, value: 42]
je literál 42
. V části Rewrite
musí být AST vždy popsán
tak, aby jej bylo možné vytvořit. Např. každý Identifier
musí obsahovat
atribut
name
.
V části content
uzlu uvádíme potomky tohoto uzlu. Seznam potomků
je uzavřen do složených závorek a položky seznamu jsou odděleny čárkou.
Např.
Binary [kind: PLUS] {
Literal [kind: INT_LITERAL],
Literal [kind: INT_LITERAL]
}
je sčítání dvou hodnot typu int
. Potomci daného uzlu musí
být správného typu a musí být uvedeni všichni, pokud má uzel nějaký obsah.
Např. Binary
musí mít vždy dva potomky (operandy), pokud má
stanoven obsah, a oba musí být typu Expression
nebo
libovolný podtyp. Pokud Binary
nemá žádný obsah, operandy mohou mít
libovolnou hodnotu. Např.
Binary [kind: MINUS]
je libovolné odčítání. To lze zapsat i takto:
Binary [kind: MINUS] {
Expression,
Expression
}
Expression
zde znamená libovolný výraz, protože nemá
stanoven žádný
atribut. Je-li očekáván uzel nějakého typu, lze použít také libovolný
podtyp.
Např. operandem uzlu Binary
může být libovolný podtyp
Expression
.
Binary [kind: MULTIPLY] {
Identifier,
Literal [kind: INT_LITERAL, value: 0]
}
V RefactoringNG se používá stejná hierarchie typů jako v javac
.
Některé atributy mohou mít více hodnot. Hodnoty se pak oddělují svislítkem. Např.
Binary [kind: PLUS | MINUS]
je buď sčítání nebo odčítání.
Každý uzel v části Pattern
může mít atribut id
.
Hodnota tohoto atributu musí být unikátní v celém pravidle a slouží
k odkazování na daný uzel z části Rewrite
. Např.
Assignment {
Identifier [id: p],
Literal [kind: NULL_LITERAL]
} ->
Assignment {
Identifier [ref: p],
Literal [kind: INT_LITERAL, value: 0]
}
přepíše p = null
na p = 0
, kde p
zastupuje libovolný identifikátor.
Odkazy na atributy se zapisují pomocí #
. Např. b#kind
odkazuje na atribut kind
uzlu b
. Odkaz na
atribut může být použit v části Rewrite
jako hodnota atributu.
Např. následující pravidlo prohodí argumenty operace dělení nebo zbytek po
dělení:
Binary [id: b, kind: DIVIDE | REMAINDER] {
Identifier [id: x],
Identifier [id: y]
} ->
Binary [kind: b#kind] {
Identifier [ref: y],
Identifier [ref: x]
}
Tj. x/y
se přepíše na y/x
a x%y
se přepíše y%x
, kde x
a y
zastupují libovolné identifikátory.
Hodnota null
znamená, že daný uzel v AST chybí. Např.
pravidlo
Variable [id: v] {
Modifiers [id: m],
PrimitiveType [primitiveTypeKind: INT],
null
} ->
Variable [name: v#name] {
Modifiers [ref: m],
PrimitiveType [primitiveTypeKind: INT],
Literal [kind: INT_LITERAL, value: 42]
}
doplní k deklaraci proměnné typu int
inicializaci na
hodnotu 42
. Tj. např.
int x;
se přepíše na
int x = 42;
Na závěr si ukážeme, jak RefactoringNG vypadá. Nejprve editor pravidel s kontrolou syntaxe a kontextovou nápovědou:
Práci s RefactoringNG si ukážeme na tomto kódu:
Refactoring zahajíme výběrem položky z menu:
RefactoringNG aplikuje vybraná pravidla a zobrazí navrhované změny. Pro provedení změn je třeba je potvrdit:
RefactoringNG umí použít refaktorizační pravidlo také pro řádkový hint:
A to je pro dnešek všechno. Pokud se chcete o RefactoringNG dozvědět více, podívejte se na Wiki projektu.