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:

refactoringng1

Práci s RefactoringNG si ukážeme na tomto kódu:

refactoringng2

Refactoring zahajíme výběrem položky z menu:

refactoringng3

RefactoringNG aplikuje vybraná pravidla a zobrazí navrhované změny. Pro provedení změn je třeba je potvrdit:

refactoringng4

RefactoringNG umí použít refaktorizační pravidlo také pro řádkový hint:

refactoringng5

A to je pro dnešek všechno. Pokud se chcete o RefactoringNG dozvědět více, podívejte se na Wiki projektu.