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
se převede na
Main { }KEYWORD_CLASS,
. Této
IDENTIFIER, LEFT_BRACE, RIGHT_BRACE
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 =
, kde
0p
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.