FernUniversität in Hagen Fakultät für Mathematik und Informatik Lehrgebiet Programmiersysteme Prof. Dr. Friedrich Steimann Constraintbasiertes Refaktorisieren von Identifiern in Java Abschlussarbeit im Studiengang Bachelor of Science in Wirtschaftsinformatik Betreuer: Dipl.-Inf. Andreas Thies 10. Juli 2013 Hermine Spengler Hütwiesstrasse 8 86510 Ried Matrikelnummer: 7707118 Danksagung Herrn Prof. Dr. Steimann danke ich für die Möglichkeit, meine Bachlorarbeit am Lehrgebiet Programmiersysteme der FernUniversität in Hagen anfertigen zu können. Ganz besonders bedanken möchte ich mich bei meinem Betreuer Dipl.-Inf. Andreas Thies für seine Unterstützung in allen Fragen und die vielen hilfreichen Tipps und Anmerkungen. Ebenso möchte ich mich an dieser Stelle ganz herzlich bei meiner Familie, Freunden, Bekannten und Kollegen bedanken für deren Verständnis und die Unterstützung der letzten Jahre. Zusammenfassung Bestehende Software wird in vielen Fällen weiterentwickelt. Damit Änderungen im Laufe der Zeit nicht das ursprüngliche Design negativ beeinflussen, ist ein regelmäßiges Umstrukturieren des Quelltextes nötig. Gerade das Überarbeiten von Identifiern ist sinnvoll, da sprechende Bezeichnungen im Quelltext wichtig sind für ein schnelles Programmverständnis. Dennoch ist es nicht leicht, sofort einen geeigneten Namen zu finden. Daher ist häufig ein nachträgliches Umbenennen erforderlich. Spezielle Refaktorisierungswerkzeuge können diese Tätigkeit wirkungsvoll unterstützen, wenn sie zuverlässig und fehlerfrei arbeiten. Die Erstellung eines solchen Werkzeugs ist allerdings nicht trivial. Ein Refaktorisierunswerkzeug muss eine Vielzahl an Bedingungen beachten, damit es keine Fehler in das zu ändernde Programm einbringt. Diese Arbeit untersucht die notwendigen Bedingungen für die Refaktorisierung von Identifiern in der Programmiersprache Java. Bei der Umsetzung wird auf das Framework REFACOLA aufgesetzt, welches die Erstellung von programmiersprachenunabhängigen Refaktorisierungswerkzeugen unterstützt. Inhaltsverzeichnis I Inhaltsverzeichnis Abbildungsverzeichnis .........................................................................................................III Listingverzeichnis.................................................................................................................IV Regelverzeichnis ..................................................................................................................VI Tabellenverzeichnis ............................................................................................................VII Abkürzungsverzeichnis.......................................................................................................VIII Hinweise zur Benutzung.......................................................................................................IX 1 Einleitung .......................................................................................................................1 2 Problembeschreibung .....................................................................................................3 3 2.1 Beitrag der Arbeit............................................................................................................ 4 2.2 Aufbau der Arbeit ........................................................................................................... 5 Grundlagen.....................................................................................................................6 3.1 3.2 4 Die Java-Sprachspezifikation........................................................................................... 6 3.1.1 Allgemeines ....................................................................................................... 6 3.1.2 Namen in Java.................................................................................................... 7 3.1.3 Wichtige Sprachkonzepte in Java ...................................................................... 8 Constraintbasierte Refaktorisierung mit REFACOLA..................................................... 13 3.2.1 Definition Refaktorisierungsmodule................................................................ 14 3.2.2 Ablauf einer Refaktorisierung in REFACOLA.................................................... 17 3.2.3 Eclipse - Integration......................................................................................... 17 Implementierung.......................................................................................................... 19 4.1 Ausgangssituation ......................................................................................................... 19 4.2 Das Regelset im Überblick............................................................................................. 19 4.2.1 Allgemeines ..................................................................................................... 19 4.2.2 Eindeutige Namen für Typen, Felder und Methoden...................................... 19 4.2.3 Namensgleichheit von Typ und Konstruktor ................................................... 23 4.2.4 Namensgleichheit von Topleveltyp und Quelltextdatei .................................. 23 4.2.5 Referenzen....................................................................................................... 24 4.2.6 Überschreiben und Verbergen von Methoden ............................................... 25 4.2.7 Eager Interface ................................................................................................ 28 4.2.8 Eindeutige Namen für lokale Variablen und formale Parameter .................... 29 4.2.9 Pakete und Importanweisungen ..................................................................... 31 Inhaltsverzeichnis 4.3 5 Einschränkungen bei der Regeldefinition ..................................................................... 33 4.3.1 Ergänzung von Qualifiern ................................................................................ 33 4.3.2 Ergänzung Qualifizierte Namen....................................................................... 37 4.3.3 Erweiterte Prüffunktionen............................................................................... 37 4.3.4 Offene Themen................................................................................................ 40 4.4 Identifier in Fremdbibliotheken .................................................................................... 42 4.5 Tests .............................................................................................................................. 42 4.5.1 Unit-Tests......................................................................................................... 43 4.5.2 Mutationstests................................................................................................. 43 4.5.3 RTT-Tests ......................................................................................................... 45 4.5.4 Testgetriebene Entwicklung ............................................................................ 46 Diskussion .................................................................................................................... 47 5.1 5.2 5.3 6 II Ergebnisse ..................................................................................................................... 47 5.1.1 Prototyp zur Ermittlung umschließender lokaler Variablen............................ 47 5.1.2 Einbindung Locked Bindings ............................................................................ 49 5.1.3 Auswertung der Tests...................................................................................... 50 Verwandte Arbeiten...................................................................................................... 53 5.2.1 Reflektive Aufrufe............................................................................................ 53 5.2.2 Sprachübergreifendes Refaktorisieren............................................................ 53 Ausblick ......................................................................................................................... 54 5.3.1 Unterstützung durch Benutzerschnittstelle .................................................... 54 5.3.2 Java-Versionen................................................................................................. 55 Schlußbetrachtung........................................................................................................ 56 Literaturverzeichnis ............................................................................................................ 57 Anhang .............................................................................................................................. 60 A) Referenz Constraintregeln .............................................................................................. 61 B) Refaktorisierungsdefinition RenameMember .................................................................. 64 C) JUnit-Testsuite................................................................................................................ 65 D) Inhalt der CD .................................................................................................................. 66 Eidesstattliche Versicherung ............................................................................................... 67 Abbildungsverzeichnis III Abbildungsverzeichnis Abbildung 1.1: modifizierter Software-Lebenszyklus.............................................................. 1 Abbildung 3.1: Gültigkeitsbereiche am Beispiel von Variablen .............................................. 9 Abbildung 3.2: Module eines REFACOLA-Refaktorisierungswerkzeugs................................ 14 Abbildung 4.1: Eager Interface.............................................................................................. 28 Abbildung 4.2: Refacola-Eingabedialog „Rename“ - Prüfung gültiger Identifier .................. 39 Abbildung 4.3: Menü RTT-Tests für Rename in Eclipse Package Explorer ............................ 46 Abbildung 5.1: gleichnamige lokale Variablen in verschiedenen Anweisungsblöcken......... 47 Abbildung 5.2: lokale Klasse und lokale Variablen................................................................ 49 Listingverzeichnis IV Listingverzeichnis Listing 2.1: Refaktorisierungsbeschreibung „Rename Method“....................................... 3 Listing 2.2: Umbenennen eines Klassennamens ............................................................... 4 Listing 3.1: Varianten bei der Definition von Variablendeklaration und –initialiserung.. 7 Listing 3.2: Deklaration und Referenzierung in Java ........................................................ 7 Listing 3.3: Namensaufbau in Java ................................................................................... 8 Listing 3.4: Vermeidung Namenskonflikt durch Verwendung qualifizierter Namen ........ 8 Listing 3.5: Abschatten einer Typdeklaration.................................................................. 10 Listing 3.6: Verdunkeln durch eine Typdeklaration ........................................................ 11 Listing 3.7: Methodenbindung ........................................................................................ 12 Listing 3.8: unvollständige Refaktorisierung ................................................................... 14 Listing 3.9: Ausschnitt REFACOLA-Sprachdefinition für Java .......................................... 15 Listing 3.10: Ausschnitt Regelset Names.ruleset.refacola ................................................ 16 Listing 3.11: Ausschnitt Refaktorisierungsdefinition........................................................ 16 Listing 4.1: gleichnamige Elemente in einem Typen....................................................... 20 Listing 4.2: Verletzung Eindeutigkeit eines Typ-Identifiers............................................. 20 Listing 4.3: eindeutige Namen bei verschachtelten Typen ............................................. 21 Listing 4.4: gleichnamige Konstruktor- und Methodendeklaration in einer Klasse ........ 23 Listing 4.5: Typreferenz in extends-Klausel..................................................................... 24 Listing 4.6: Überschreiben einer Methode durch Umbenennen .................................... 26 Listing 4.7: Methode mit final-Modifier.......................................................................... 28 Listing 4.8: gleichnamiger Topleveltyp und Single-Type-Import..................................... 32 Listing 4.9: Abschattung einer Instanzvariablen nach Refaktorisierung I ....................... 33 Listing 4.10: Abschattung einer Instanzvariablen nach Refaktorisierung II ...................... 33 Listing 4.11: Qualifizierter Referenzname in Zuweisung................................................... 35 Listing 4.12: geschachtelte Expression in Zuweisung........................................................ 35 Listing 4.13: Problematik von fehlendem Qualifier bei lokaler Variable .......................... 35 Listing 4.14: Ergänzung Quelltext mit explizitem this-Qualifier........................................ 36 Listing 4.15: diverse Aufrufe mit Qualifiern ...................................................................... 36 Listing 4.16: Einfügen von qualifizierten Namen .............................................................. 37 Listing 4.17: Methodenaufruf bei überladenen Methoden .............................................. 38 Listing 4.18: Methodenaufruf mit Autoboxing.................................................................. 38 Listing 4.19: Funktion für casesensitivem Stringvergleich ................................................ 40 Listing 4.20: Kommentare in Java ..................................................................................... 42 Listingverzeichnis V Listing 5.1: Ausschnitt REFACOLA-Sprachspezifikation für Java - lokale Variablen......... 48 Listing 5.2: Klasse mit reflektivem Aufruf ....................................................................... 53 Listing 5.3: Ausblick voraussichtliche Lambda-Syntax .................................................... 55 Regelverzeichnis VI Regelverzeichnis Regel 1: Names.UniqueTopLevelTypeIdentifier ......................................................... 20 Regel 2: Names.UniqueMemberTypeNames2 ........................................................... 21 Regel 3: Names.UniqueMemberTypeNames1* ......................................................... 21 Regel 4: Names.UniqueFieldIdentifier........................................................................ 22 Regel 5: Names.UniqueMethodIdentifier................................................................... 22 Regel 6: Names.ConstructorNames............................................................................ 23 Regel 7 : Names.TopLevelTypeNames......................................................................... 24 Regel 8: Names.TypeReferenceIdentifier ................................................................... 25 Regel 9: Names.ReferenceIdentifier*......................................................................... 25 Regel 10: Names.OverridingMethodNames................................................................. 26 Regel 11: Accessibility.AccidentalOverriding*.............................................................. 27 Regel 12: Names.AccidentalHidingFinalMethod .......................................................... 28 Regel 13: Names.EagerInterfaceMethodNames .......................................................... 29 Regel 14: Names.UniqueLocalVariableNames1............................................................ 30 Regel 15: Names.UniqueLocalVariableNames2............................................................ 30 Regel 16: Names.UniqueLocalVariableNames3............................................................ 31 Regel 17: Names.PackageDeclarationNames* ............................................................. 31 Regel 18: Names.ImportDeclarationNames1 ............................................................... 32 Regel 19: Names.ImportDeclarationNames2 ............................................................... 32 Regel 20: Names.UniqueEnclosingMemberTypeNamesInAssignment1 .................... 34 Regel 21: Names.UniqueEnclosingMemberTypeNamesInAssignment2 .................... 34 * Autor A. Thies Tabellenverzeichnis VII Tabellenverzeichnis Tabelle 4.1: Mutationstest für das JUnit-Test RenameMember ...................................... 44 Tabelle 5.1: JUnit-Testsuite AllRenameMember .............................................................. 50 Tabelle 5.2: RTT-Test Rename Only Type ......................................................................... 51 Tabelle 5.3: RTT-Test Rename Only Field ......................................................................... 51 Tabelle 5.4: RTT-Test Rename Only Method.................................................................... 51 Tabelle 5.5: RTT-Test Rename Only LocalVariable ........................................................... 52 Tabelle 5.6: Parameter Testumgebung ............................................................................ 52 Abkürzungsverzeichnis Abkürzungsverzeichnis API Application Programming Interface AST Abstract Syntax Tree CCD Clean Code Developer DSL Domain specific Language IDE Integrated Development Environment JDT Java Development Tools JLS Java Language Specification JSP Java Server Pages JVM Java Virtual Machine REFACOLA REFActoring COnstraint LAnguage RTT Refactoring Tool Tester TDD Test Driven Development XML Extensible Markup Language XP Extreme Programming VIII Hinweise zur Benutzung IX Hinweise zur Benutzung Sprache In der Informatik haben sich im täglichen Sprachgebrauch viele fachspezifische Begriffe in englischer Sprache etabliert, wie zum Beispiel Interface oder Sourcecode. Da diese sich teilweise nur unzureichend übersetzen lassen und zudem das Verständnis dadurch eher erschwert wird, verwendet diese Arbeit an vielen Stellen die englischen Begriffe. Auf eine besondere Hervorhebung wird aus Gründen der Lesbarkeit verzichtet. Darstellung Quelltext Diese Arbeit verwendet zwei Varianten für die Darstellung von Quelltexten. Die eine dient ausschließlich der Erläuterung einer bestimmten Ausgangssituation. Die andere veranschaulicht die Transformation eines Programms und seines Refaktorisierungsergebnisses. Quelltext zur Erläuterung Quelltext, welcher der Erläuterung dient, stellt einen Programmausschnitt dar und kann ein- oder zweispaltig angegeben sein. package a; package a; class A { class B { public B (){} void m() { B b2 = new B(); } } } Refaktorisierter Quelltext Hier befindet sich das Originalprogamm jeweils auf der linken Seite, das refaktorisierte Ergebnis jeweils rechts. Ein Pfeil weist auf die Transformation hin. package a; package a; public class A{ public class A{ int i; int k; int j=i; } int j=k; } Einleitung 1 1 Einleitung Eine wichtige Eigenschaft von Software ist, dass sie relativ leicht änderbar ist. Denn je erfolgreicher ein Produkt ist, desto eher wird es weiterentwickelt. So stehen, wenn die erste Version eines Programms fertig ist, häufig schon die nächsten Themen auf der meist langen Wunschliste der Auftraggeber [We06, S. 5]. Aber auch ohne Erweiterungen endet die Entwicklung in der Regel nicht mit der Inbetriebnahme der Software. Um die geforderten Aufgaben über viele Jahre erfüllen zu können, müssen die meist fehlerhaften Systeme gewartet werden. Abbildung 1.1: modifizierter Software-Lebenszyklus Im Gegensatz zur initialen Entwicklung eines Produktes werden Änderungen oftmals ohne zeitaufwändige Analyse oder Konzeptionierung eingefügt (vgl. blauer Kreislauf in Abbildung 1.1). So wächst eine Applikation und mit jeder Anpassung steigt die Gefahr, dass das ursprüngliche Design verloren geht. Vergleichbar mit einem Haus, bei dem Erweiterungen und Anbauten vorgenommen werden, die nicht ohne weiteres ins Gesamtkonzept passen [MCo07, S. 17], fängt die bestehende Architektur an, mehr und mehr zu „verfallen“. Was bei einem Haus schnell offensichtlich wird, ist in der Softwareentwicklung oftmals ein schleichender Prozess, der erst sehr spät wahrgenommen wird. Analysen, Fehlerbehebung und Erweiterungen werden erschwert, weil der Quelltext immer unverständlicher und undurchsichtiger wird und irgendwann einem „Flickenteppich“ gleicht. Dies hat unmittelbar Auswirkung auf die Qualität der Software. Werden keine Gegenmaßnahmen ergriffen, muss das Produkt im Extremfall von Grund auf neu programmiert werden. Ist dies nicht mehr möglich, wird die Entwicklung eingestellt [MCo07, S. 575 ff.]. Um diesem schleichenden Niedergang entgegenzuwirken, fordert die Initiative Clean Code Developer1 ein „pro-aktives Handeln“. Quelltexte sollten rechtzeitig überprüft und, falls nötig, überarbeitet werden, um deren Qualität zu gewährleisten oder gar zu verbessern [CCD]. 1 Informationen finden sich unter http://www.clean-code-developer.de/. Die Initiative beruft sich dabei unter anderem auf [Ma11]. Einleitung 2 Auch Fowler beschreibt bereits 1999 in [Fo11, S.53 ff.] die Notwendigkeit zur regelmäßigen Verbesserung der bestehenden Quelltextstruktur. Mithilfe von rein strukturellen Änderungen sollen Quelltexte leichter les-, wart- und erweiterbar gemacht werden, um damit die Qualität in der Programmierung zu erhöhen. Er empfiehlt Refaktorisierung als grundlegende Technik. Das Ziel dabei ist, „ein Softwaresystem so zu verändern, dass das externe Verhalten nicht verändert wird, es jedoch eine bessere interne Struktur erhält“ [Fo11, S. XVi]. Westphal schreibt, ohne ständige Code-Pflege würde die Qualität von Quelltexten mit zunehmender Entwicklung für gewöhnlich abnehmen, mit der Gefahr, dass jegliche Weiterentwicklung be- oder sogar verhindert wird. Als einzig existierende Gegenmaßnahme sieht er stetiges Refaktorisieren [We06, S. 3]. Es ist daher sinnvoll, diese fest in den täglichen Arbeitsablauf zu integrieren, um so die Qualität dauerhaft zu erhalten oder sogar zu verbessern [CCD]. Damit ist Refaktorisierung nicht an ein festes Vorgehensmodell gebunden, sondern ein Prozess, der in allen Phasen der Implementierung unterstützen kann. Die Entscheidung, welche Stellen nun verbesserungswürdig sind, hängt zunächst von der subjektiven Einschätzung des jeweiligen Entwicklers ab. Häufig sind es Stellen im Quelltext, die ohne zusätzliche Informationen oder Kommentare nur schwer verständlich sind [Ma11, S. 54ff.]. In den letzten Jahren haben sich eine Reihe von Beschreibungen etabliert, die helfen, solche Stellen zu identifizieren, sogenannte Codesmells. Beispiele sind „Duplication“ oder „Commented Out Code“ [Ma11, S. 285ff.]. Ein weiterer Codesmell „Choose Descriptive Names“ weist auf das Problem von schlecht gewählten Benamungen hin. Diese und die inkonsistenter Handhabung von Begriffen2 erschweren das Programmverständnis erheblich. Angesichts dessen, dass Identifier ca. 70% des gesamten Quelltextes ausmachen [DP06], tragen sie wesentlich zur Lesbarkeit des Programmes bei, was Fowler [Fo11, S.15] wie folgt dokumentiert: „Guter Code sollte klar kommunizieren was er tut… und Namen sind ein Schlüssel dazu …“. Je treffender Bezeichner also sind, desto leichter fallen Einarbeitung und Wartung [DP06, Ma11, S309 ff.]. Nur leider ist die spontane Suche nach einem geeigneten Namen oftmals schwierig, weshalb nachträgliches Umbenennen eine häufige Refaktorisierung ist. 2 Der „Codesmell“ dazu ist „Same Name Different Meaning“ Online unter: http://c2.com/cgi/wiki?SameNameDifferentMeaning Problembeschreibung 3 2 Problembeschreibung Selbst wenn die Notwendigkeit von Strukturverbesserungen gesehen wird, ist die Hemmschwelle, Änderungen an einem funktionierenden System vorzunehmen, oftmals groß, weil es mit jeder Änderung auch immer zu unerwünschten Nebeneffekten kommen kann. Inwieweit es sinnvoll ist, diese manuell durchzuführen, lässt sich hinterfragen. Selbst eine vermeintlich einfache Refaktorisierung kann zu einer aufwändigen Prozedur werden und ist nicht immer risikolos [Fo11, S.15]. Fowler stellt in [Fo11] eine Reihe von Refaktorisierungsmustern für die manuelle Umstrukturierung von Quelltexten vor.3 Exemplarisch wird in Listing 2.1 anhand des Musters „Rename Method“ gezeigt, welche Einzelschritte von Hand für das Umbenennen einer Methode durchzuführen sind. Rename Method 1. Prüfen, ob eine gleichnamige Methode mit derselben Signatur bereits besteht (inkl. Super-/Subklassen) 2. Eine zusätzliche Methode mit neuem Namen deklarieren und den Inhalt der alten Methode hineinkopieren 3. Kompilieren 4. Anschließend die neue Methode im Rumpf der alten Methode aufrufen (bei wenigen Referenzen kann der Punkt übersprungen werden) 5. Kompilieren und Testen 6. Jeweils eine Referenz der alten Methode suchen und auf den neuen Namen ändern, kompilieren und testen (Schritt 6 für alle Referenzen wiederholen) 7. Alte Methode entfernen, wenn sie kein Teil einer Schnittstelle (interface) ist, andernfalls kann sie nicht entfernt werden, sondern muss auf deprecated gesetzt werden 8. Kompilieren und Testen Listing 2.1: Refaktorisierungsbeschreibung „Rename Method“ [Fo11, S. 273 ff.] 4 Das Beispiel zeigt, wie der Umfang dieser vergleichsweise einfachen Refaktorisierung davon abhängt, wie viele Stellen im Programm angepasst werden müssen (vgl. Listing 2.1, Schritt 6). Dadurch kann diese Refaktorisierung unter Umständen sehr zeitaufwändig werden. Wird erst zu einem fortgeschrittenen Zeitpunkt bemerkt, dass die Umstrukturierung doch nicht durchgeführt werden kann, weil es beispielsweise zu einem Namenskonflikt von Methoden kommt, der übersehen wurde, müssen die bereits geänderten Quelltextfragmente zurückgesetzt werden. Insgesamt ist dies nicht nur fehleranfällig, sondern auch ineffizient. 3 Viele dieser Refaktorisierungsmuster und Zusatzinformationen finden sich auch auf der Internetpräsenz von Martin Fowler http://refactoring.com/catalog/index.html 4 Anmerkung: Der englische Text ist frei übersetzt Problembeschreibung 4 Für die automatisierte Durchführung einer Refaktorisierung bieten sich spezielle Werkzeuge an. Diese werden von Entwicklungsumgebungen5, wie Eclipse [Ecl], IntelliJ IDEA [InJ] oder NetBeans IDE [NeB] bereits seit Jahren integriert [SKP11, Sc10, Wa12]. Allerdings zeigte 2009 eine Umfrage [MPB09], dass Refaktorisierungswerkzeuge noch nicht in dem Maße genutzt werden, wie es Entwicklern möglich wäre. Laut den Autoren werden trotz einer Vielzahl verfügbarer Werkzeuge immer noch erstaunlich viele Refaktorisierungen von Hand durchgeführt. Einen Grund, warum Entwickler den manuellen Weg wählen oder eine „Suchen und Ersetzen“-Funktion bevorzugen, sehen einige Autoren darin, dass die Werkzeuge zum Teil immer noch fehlerhaft arbeiten [SKP11, SEM08]. Zu den am häufigsten genutzten Refaktorisierungswerkzeugen gehören solche, die Programmelemente umbenennen [MPB09, Sc10]. Diese Umstrukturierung ist jedoch nicht immer so einfach, wie es auf den ersten Blick erscheinen mag. Einen ersten Eindruck soll Listing 2.2 vermitteln. Es zeigt ein Javaprogramm, in welchem der Name der Klasse B in C umbenannt werden soll. package a; import b.B; class A { B b1; B n() { B b2 = new B(); return b; } } package b; //umbenennen B -> C class B { public B (){ } void m(B b){ } } Listing 2.2 Umbenennen eines Klassennamens Im Beispiel wird deutlich, dass viele Stellen im Quelltext die Klasse B referenzieren. Diese müssen beim Umbenennen der Klasse ebenfalls geändert werden. Eine nicht offensichtliche Voraussetzung ist, dass die Änderung nur durchgeführt werden kann, wenn es im Paket b nicht bereits eine gleichnamige Klasse C gibt. Dies würde zu einem Namenskonflikt führen (vgl. Kapitel 4.2). 2.1 Beitrag der Arbeit Abhängig von der Programmiersprache gibt es für ein Refaktorisierungswerkzeug eine Vielzahl an Vorbedingungen zu beachten, um sicherzustellen, dass sich das Verhalten des Programms durch die Umstrukturierung nicht ändert [SKP11, St10]. Sind diese zu schwach, kann es nach der Refaktorisierung zu Übersetzungsfehlern oder einer Veränderung im Programmverhalten kommen [SMG11]. Lehnt ein 5 Die Entwicklungsumgebungen finden sich unter: http://www.eclipse.org/, http://netbeans.org/, http://www.jetbrains.com/idea/ Problembeschreibung 5 Werkzeug eine Änderung aufgrund zu strenger Prüfungen ab, ist dies zwar weniger fatal, aber für den Benutzer sehr ärgerlich [SKP11]. Beides ist für die Akzeptanz eines solchen Werkzeuges nicht hilfreich. Am Lehrgebiet Programmiersysteme der FernUniversität in Hagen wird aktuell ein Framework namens REFACOLA (REFActoring COnstraint LAnguage)6 für die Erstellung von Refaktorisierungswerkzeugen entwickelt, mit dem Ziel korrekte Refaktorisierungen zu erreichen, indem deren Bedingungen korrekt und vollständig beschrieben werden. Zielsetzung Ziel der Arbeit ist es, Vorbedingungen für die Refaktorisierung von Identifiern in Java-Quelltexten zu identifizieren und in einem Regelwerk in REFACOLA abzubilden. Im Fokus stehen Möglichkeiten, aber auch Einschränkungen, die sich ausschließlich auf das Umbenennen von Identifiern im Java-Quelltext beziehen. Ein Problem in diesem Zusammenhang ist das Umbenennen von Identifiern in reflektiven Ausdrücken (vgl. [TB12]). Eine weitere, spezielle Problematik ergibt sich daraus, dass ein Identifier nicht nur im Quelltext, sondern auch in anderen Dateien außerhalb des Quelltextes verwendet werden kann (vgl. [Kl10]). Auf Probleme, die sich bei der Refaktorisierung aufgrund dieser beider Sachverhalte ergeben, kann im Rahmen dieser Arbeit nur kurz theoretisch eingegangen werden. Da sich das REFACOLA-Framework noch in der Entwicklung befindet, sind bei Bedarf Erweiterungen vorzunehmen. Die Entwicklung setzt auf ein vorhandenes Refaktorisierungswerkzeug für das Umbenennen von Javaelementen auf, welches bereits über eine Benutzerschnittstelle „Rename“ verfügt. Zum Zeitpunkt der Arbeit basiert das REFACOLA-Framework auf der Java-Version 1.6 [GJSB05]. Neuere Versionen sind für diese Arbeit nicht nicht vorgesehen. Refaktorisierungswerkzeuge zählen, wie auch Compiler und Debugger, zu den Metaprogrammen, weil sie ihre Funktionen direkt auf dem Quelltext eines Programms ausführen und diesen ändern. Dementsprechend groß ist der Anspruch für den Bau und Test an ein solches Programm [St10]. Zur Überprüfung der Refaktorisierungsergebnisse beim Umbenennen von Identifiern ist eine Möglichkeit zu schaffen, mit der die Funktionsweise des Regelwerks getestet werden kann. 2.2 Aufbau der Arbeit Kapitel 3 fasst wichtige Eigenschaften und Konzepte der Sprache Java zusammen und führt in die Grundlagen der constraintbasierten Refaktorisierung auf Basis des REFACOLA-Frameworks ein. Kapitel 4 erläutert Details der Entwicklung des Regelwerks für das Refaktorisieren von Identifiern. Ebenso werden Problematiken bei der Definition der Bedingungen aufgezeigt. Abschließend wird eine Testmöglichkeit für das entwickelte Regelset vorgestellt. Kapitel 5 reflektiert Ergebnisse der Arbeit und greift ergänzende Themen auf. Kapitel 6 fasst abschließend die wichtigsten Punkte zusammen. 6 Siehe auch Webpräsenz: http://www.fernuni-hagen.de/ps/prjs/refacola/ Grundlagen 6 3 Grundlagen 3.1 Die Java-Sprachspezifikation Damit sich das Verhalten eines Programms bei einer Refaktorisierung nicht ungewollt verändert müssen die syntaktischen und semantischen Vorgaben einer Sprache beachtet werden [SKP11, St10]. Diese geben vor, wie ein Java-Programm strukturiert sein muss, damit es vom Compiler übersetzbar ist. Für Java sind diese in der Java Language Specification7 [GJSB05] beschrieben, nachfolgend mit JLS abgekürzt. Wichtige Aspekte der Sprache Java, die für die weiteren Ausführungen als wichtig erachtet werden, sind in den anschließenden Kapiteln kurz zusammengefasst. Verweise auf die Sprachspezifikation werden jeweils durch ein §-Zeichen gekennzeichnet, gefolgt von der Angabe des zugehörigen Kapitels. So bezieht sich die Angabe JLS §4.1 auf die Java-Sprachspezifikation Kapitel 4.1. 3.1.1 Allgemeines Die Deklaration von Javaelementen findet in der Quelltextdatei statt, hier auch als Compilationunit bezeichnet. Javaelemente können hierarchisch geordnet bzw. ineinander geschachtelt werden, beispielsweise Pakete oder Typen. Java unterscheidet zwischen primitiven Datentypen, wie boolean oder int, und Referenztypen (JLS §4). Zu letzteren zählen Klassen, Interfaces und Arrays (JLS §4.3). Enumerationen sind spezielle Klassen (JLS §8.1). Im Folgenden sollen Klassen, Interfaces und Enumerationen auch unter dem Begriff Typen zusammengefasst werden.8 Anhand ihrer hierarchischen Ordnung lassen sie sich weiter unterscheiden in (vgl. [Wa12]): Ein Topleveltyp wird selbst von keinem anderen Typen umschlossen. In einer Compilationunit kann es mehrere geben (JLS §7.6). Nestedtypes sind Typen, die innerhalb eines anderen Typen deklariert sind. Dieser Begriff fasst eine Reihe von spezielleren Typen zusammen, nämlich Membertypen bzw. innere, lokale oder anonyme Klassen (JLS §8, §9, §14.3, §15.9.5). Während ein Membertyp auch statisch sein kann, gilt dies für die anderen nicht. Innere Klassen sind wie der Name es bereits andeutet, kein Interface und keine Enumeration (§8.1.3). Lokale und anonyme Klassen sind spezielle innere Klassen und werden innerhalb von Methoden, Konstruktoren oder Initialisierungsblöcken definiert (JLS §14.3, §15.9.5). Anonyme Klassen haben selbst keinen Namen. Sie erweitern bei der Erzeugung implizit die Klasse oder das Interface, welches im Instanziierungsausdruck angegeben ist. Konstruktoren gleichen vom Aufbau her stark Methoden und können daher an vielen Stellen gleich behandelt werden. Im Wesentlichen unterscheiden sich sich darin, dass sie den gleichen Namen haben wie der sie deklarierende Typ und über keinen Rückgabetyp verfügen (JLS §8.8). 7 8 Hier in der 3. Version der Java-Sprachspezifikation (vgl. [GJSB05]) Arrays werden für diese Arbeit in diesem Zusammenhang vernachlässigt Grundlagen 7 Des Weiteren werden diverse Arten von Variablen unterschieden, nämlich Klassenoder Instanzvariablen, formale Parameter von Methoden oder Konstruktoren und lokale Variablen (JLS §4.12.3).9 Neben den vielfältigen Möglichkeiten, welche der Sprachumfang von Java an sich bietet, sind für die Formulierung eines bestimmten Sachverhalts häufig mehrere Varianten denkbar. Es ist beispielsweise zulässig Deklaration und Initialisierung getrennt oder in einer Anweisung vorzunehmen. Listing 3.1 zeigt drei Varianten für die Deklaration und Initialisierung einer Instanzvariablen. Für ein Refaktorisierungswerkzeug müssen alle Varianten berücksichtigt werden. package a; public class A { int x; x = 0; int y = 0; int a=0, b=0; } Listing 3.1: Varianten bei der Definition von Variablendeklaration und –initialiserung 3.1.2 Namen in Java In Java besitzen Programmelemente, wie Pakete, Typen, Variablen, Methoden oder Typparameter einen Namen.10 Dieser wird bei der Deklaration an das jeweilige Objekt gebunden, um anhand dessen auf das Programmelement zugreifen zu können (JLS §6). Diese Namen können bei einer Refaktorisierung geändert werden. Listing 3.2 zeigt die Deklaration für den Typen B. Über den Namen b kann das Objekt in der Methode m() referenziert werden. Aber auch der Typ der Deklaration, eine sogenannte Typreferenz, hat einen eigenen Namen, nämlich B. package a; public class A { B b; void m(){ B newB = b; } //deklaration //referenz auf b } Listing 3.2: Deklaration und Referenzierung in Java In dieser Arbeit werden die Begriffe Identifier, Name und Bezeichner synonym gebraucht. 9 Weitere Variablenarten wie einzelne Array Komponenten und die Parameter der Exception-Handler können für diese Arbeit vernachlässigt werden. 10 Der Auszug aus der JLS (§6) lautet dazu: „NAMES are used to refer to entities declared in a program. A declared entity (§6.1) is a, class type (normal or enum), interface type (normal or annotation type), member (class, interface, field, or method) of a reference type, type parameter (of a class, interface, method or constructor) (§4.4), parameter (to a method, constructor, or exception handler), or local variable.“ Grundlagen 8 Aufbau von Namen in Java Java unterscheidet zwischen einfachen und qualifizierten Namen. Erstere bestehen lediglich aus einem einzelnen Identifier, qualifizierte Namen hingegen setzen sich aus mehreren durch „.“ getrennte Identifier zusammen (vgl. Listing 3.3). Ein qualifizierter Name setzt sich aus Paket- und Typnamen zusammen und spiegelt damit auch die hierarchische Organisation eines Javaelements wieder. Man spricht auch von vollqualifizierten Namen. Einfacher Name: Identifier Qualifizierter Name: Identifier { . Identifier } Listing 3.3: Namensaufbau in Java (JLS §18.1) Die Angabe eines qualifizierten Namens ist nötig, wenn der einfache Name des Elements nicht eindeutig ist. Dies ist zum Beispiel der Fall, wenn, wie in Listing 3.4 dargestellt, mehrere gleichnamige Typen in einer Klasse verwendet werden sollen. package examples; public class NeedQualifiedName { a.I i1; b.I i2; } Listing 3.4: Vermeidung Namenskonflikt durch Verwendung qualifizierter Namen Gültige Identifier und Konventionen Ein Identifier muss in Java einem bestimmten Aufbau genügen, um gültig zu sein. Er kann beliebig lang sein und aus Unterstrichen, diversen Zeichen11 und Zahlen bestehen. Allerdings darf er nicht mit einer Zahl beginnen. Schlüsselwörter12 und Literale wie true, false oder null sind als Identifier ebenfalls nicht erlaubt (JLS §3.8). Java unterscheidet Groß- und Kleinschreibung, damit sind aAa und AaA zwei unterschiedliche Bezeichner. Zusätzlich zu den syntaktischen und semantischen Vorgaben schlägt die JLS auch einige Konventionen für die Namensgebung vor (JLS §6.8). Beispielsweise soll der Name einer Klasse mit einem Großbuchstaben beginnen, während Pakete kleingeschrieben werden sollten. Die Einhaltung ist jedoch nicht verpflichtend. 3.1.3 Wichtige Sprachkonzepte in Java Weiterhin gibt es eine Reihe von Sprachkonzepten, die Einfluss auf die NamensBindung von Referenzen an eine deklariertes Javaelement hat, oder darauf ob ein Identifier in einem bestimmten Kontext eindeutig sein muss. Die einzelnen Konzepte stehen zum Teil in engen Abhängigkeiten zueinander und können sich daher beeinflussen. 11 Es gibt einige Ausnahmen für Zeichen mit besonderer Bedeutung, wie Prozent, Gleichheits- oder Ausrufezeichen, sowie Leerzeichen. Diese sind nicht erlaubt, Umlaute dagegen sehr wohl. 12 Die vollständige Liste findet sich in JLS §3.9, Beispiele sind class, enum oder void Grundlagen 9 Zugriffsmodifier (JLS §6.6) Java bietet durch sogenannte Zugriffsmodifier einen Mechanismus, über den der Zugriff auf Umsetzungsdetails eines Pakets oder eines Typs geregelt werden kann. Anstatt von Zugreifbarkeit wird auch von Sichtbarkeit gesprochen.13 Die vier Modifier können geordnet werden, dabei ist public weniger restriktiv als protected. public > protected > package private14 > private Javaelemente, welche die Zugreifbarkeit private haben sind nur innerhalb des Typs, in dem sie deklariert wurden, einschließlich der Nestedtypes zugreifbar. Ist kein Modifier angegeben wird dies auch als default access oder package private bezeichnet. Javaelemente sind dann nur innerhalb des gleichen Pakets sichtbar. Haben diese dagegen den Modifier protected, können zusätzlich auch erbende Elemente darauf zugreifen. Für public gibt es keine Einschränkung. Die Zugriffskontrolle hat zum Beispiel Auswirkungen auf Vererben, Verbergen oder Überschreiben von Methoden (JLS §6.6). Die Eindeutigkeit eines Identifiers hängt unter anderem von dessen Sichtbarkeit ab. Gültigkeitsbereiche (JLS § 6.3) Der Gültigkeitsbereich (scope) eines Javaelements ist davon abhängig, wo es deklariert wurde. Innerhalb von diesem Bereich kann es über seinen einfachen Namen referenziert werden. Abbildung 3.1 illustriert das Prinzip. Der innerste Bereich, hier eine For-Schleife, beinhaltet eine Variable y, die nur innerhalb des Blocks gültig ist. Für außerhalb liegende Javaelemente ist sie nicht referenzierbar. Allerdings können Elemente eines innenliegenden Block auf die Elemente in den sie umschließenden Blöcken zugreifen. Daher kann beispielsweise auf die Membervariable x in der ganzen Klasse zugegriffen werden. class A{ private int x=0; void m(){ int y = x; for(int i=0; i<5; i++){ int x; //abschatten der instanzvariable x } } } Abbildung 3.1: Gültigkeitsbereiche am Beispiel von Variablen 13 TM Vgl. Java Tutorial (http://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html) 14 In der Sprachspezifikation für Java wird von default access anstatt von package private gesprochen (JLS §6.6.1). Zur besseren Unterscheidung der einzelnen Modifier wird in dieser ArTM beit jedoch der Begriff package private verwendet, wie er auch im Java Tutorial von Oracle zu finden ist (vgl. http://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html) Grundlagen 10 Für Identifier hat der Gültigkeitsbereich eines Javaelements zwei Konsequenzen. Zum einen müssen die Bezeichner innerhalb bestimmter Bereiche eindeutig sein. Wird diese Eindeutigkeit verletzt, kommt es zu Übersetzungsfehlern im Programm. Zum anderen können sich gleichnamige Identifier „überdecken“. Abschatten (Shadowing, JLS §6.3.1) Gleichnamige Deklarationen können sich abschatten, wenn die eine innerhalb des Gültigkeitsbereichs der anderen liegt und beide vom gleichen Elementtyp sind. Eine Methode kann folglich nicht durch eine gleichnamige lokale Variable abgeschattet werden, eine Instanzvariable von einer lokalen Variable aber sehr wohl (vgl. Abbildung 3.1). Über den einfachen Bezeichner kann nicht mehr auf das abgeschattete Javaelement zugegriffen werden. Auch Methoden oder Typen können abgeschattet werden. package a; import b.*; class A { class B{ void n(){} } package b; public class B{ void xy(){} } void m(){ B b = new B(); b.n(); } } Listing 3.5: Abschatten einer Typdeklaration Listing 3.5 zeigt die Abschattung einer Typdeklaration. Im Beispiel gibt es zwei Klassen, die denselben einfachen Namen haben, aber in unterschiedlichen Paketen liegen. Die Klasse b.B wird zwar in die Klasse a.A importiert, jedoch von der Deklaration der inneren Klasse a.B abgeschattet. Abschatten unterscheidet sich außerdem von den Konzepten Verdunkeln und Verbergen. Diese werden in den folgenden Absätzen beschrieben. Verdunkeln (Obscuring, JLS §6.3.2) Es ist zulässig, dass eine Variable, ein Typ oder ein Paket denselben einfachen Namen im gleichen Kontext hat. Damit ähnelt Verdunkeln dem Abschatten, nur dass hier die Javaelemente von unterschiedlichem Elementtyp sind. Für solche Situationen definiert die Java-Sprachspezifikation eine Reihenfolge, wie die Javaelemente ausgewählt werden. Beispielsweise werden Variablen gegenüber Typen bevorzugt. Namenskonventionen (JLS §6.8) können helfen, Verdunklung zu vermeiden. Listing 3.6 zeigt ein Beispiel, bei dem anhand des einfachen Namens nicht mehr auf das statische Element PI der Klasse java.lang.Math zugegriffen werden kann, weil es in der Klasse A eine gleichnamige Typdeklaration gibt. Deshalb muss der Zugriff über den qualifizierten Namen Math.PI erfolgen. Grundlagen 11 package a; import static java.lang.Math.PI; class A { PI PI = new PI(); double x = PI; //Typkonflikt, qualifizierter Aufruf nötig double y = Math.PI; class PI{} } Listing 3.6: Verdunkeln durch eine Typdeklaration Verbergen (Hiding, JLS §8, §9). Verbergen tritt in Kombination mit Vererbung auf. Javaelemente eines Supertyps können durch eine gleichnamige Deklaration aus dem Subtyp verborgen werden, vorausgesetzt, beide haben den gleichen Elementtyp und eine entsprechende Zugreifbarkeit.15 Verbergen ist möglich bei Klassenmethoden, Instanz- und Klassenvariablen. Bei Instanzmethoden wird nicht von Verbergen, sondern von Überschreiben gesprochen. Statische Methoden können keine Instanzmethoden verbergen. Überschreiben (Overriding, JLS §8.4.8) Eine Instanzmethode überschreibt eine Instanzmethode des Supertyps, wenn diese die gleiche Signatur besitzt und für den Subtyp sichtbar ist. Sie kann keine statische Methode überschreiben, dies würde zu einem Kompilierungsfehler führen. Bei Rückgabetypen von überschriebenen Methoden ist eine Besonderheit zu beachten. Bis Java 1.4 musste der Rückgabetyp von Methoden beim Überschreiben genau übereinstimmen. Seit Java 1.5 sind auch Subtypen des in der Supermethode angegebenen Rückgabetyps erlaubt, sogenannte kovariante Rückgabetypen (JLS § 8.4.5). Mit dem Schlüsselwort final gekennzeichnete Methoden können ebenso nicht überschrieben werden, wie Instanzmethoden mit dem Zugriffsmodifier private. Im zweiten Fall kann aber im Subtyp eine Methode mit gleicher Signatur definiert werden. Dies gilt gleichermaßen für statische Methoden im Zusammenhang mit Verbergen. Überladen (Overload, JLS §8.4.9) Gibt es mehrere gleichnamige Methoden innerhalb eines Typs oder seines Supertyps, die sich anhand ihrer Signatur unterscheiden, spricht man von Überladen. Qualifizierung von Namen (JLS §15.8.3, §15.8.4, §15.11.2) Der einfache Name eines Programmelements kann über die Qualifier this oder super qualifiziert werden. Um zwischen gleichnamigen überschriebenen oder versteckten Programmelementen in Sub- und Supertyp zu unterscheiden, wird super verwendet. Eine Unterscheidung innerhalb eines Typs, zum Beispiel von Instanzva- 15 Beispielsweise können Programmelemente im Supertyp mit dem Modifier private nicht verborgen werden, da sie im Subtyp nicht sichtbar sind. Grundlagen 12 riablen und lokalen Variablen ist mit dem Qualifier this möglich. Bei verschachtelten Klassen kann zur Unterscheidung der verschiedenen Klassen ein qualifiziertes this verwendet werden. Dies hat die Form ClassName.this. Statische Javaelemente können über den Namen des Typs, in dem sie deklariert sind, qualifiziert werden. Methodenbindung (JLS §15.12) Konzepte wie Überschrieben bzw. Überladen machen die Methodenbindung in Java nicht ganz einfach. Zum einen erfolgt die Bindung einer Methode statisch zur Compilezeit. Gibt es in einer Klasse mehrere gleichnamige Methoden, die anhand der Parameterliste zu einem Aufruf passen könnten, wird die speziellere verwendet (JLS §15.12.2.5), wie es in Listing 3.7 dargestellt wird. In dem Beispiel wird eine überladene Methode mit einem Argument i vom Typ int aufgerufen. Die aufzurufende Methode ist daher m(int i), da diese den spezielleren Parametertyp hat. package a; public class A { void m(int i) {} //aufgerufene Methode void m(float f) {} public static void main(String [] args){ A a = new A(); int i = 1; a.m(i); //methodenaufruf } } Listing 3.7: Methodenbindung Bei überschriebenen Methoden hängt der Methodenaufruf zusätzlich vom aktuell geladenen Objekt ab und wird daher dynamisch zur Laufzeit ermittelt. Ausgehend von diesem wird solange die Vererbungshierarchie aufsteigend jedes Objekt nach einer passenden Methode durchsucht, bis eine Methode gefunden ist, welche anhand ihrer Signatur für den Aufruf passt (JLS 15.12.4). Typumwandlung (JLS §5) Typumwandlungen können in Java explizit oder implizit durchgeführt werden. Erstere können durch eine Cast-Anweisung erzwungen werden. Letztere werden vom Java-Compiler selbstständig durchgeführt, wenn sie zu keinem Informationsverlust führen. Ein primitiver Datentyp short kann beispielsweise ohne Probleme in die Datentypen int, long, float oder double konvertiert werden (JLS §5.1.2). Seit Java 1.5 gibt es eine spezielle Form der Typwandlung, das Autoboxing (JLS §5.1.7). Hierbei werden primitive Typen automatisch in deren Wrappertypen gewandelt. Der umgekehrte Weg ist Autounboxing (JLS §5.1.8). Grundlagen 13 3.2 Constraintbasierte Refaktorisierung mit REFACOLA Für den Bau von Refaktorisierungswerkzeugen gibt es verschiedene Ansätze. Steimann nennt in [St10] den Ansatz der constraintbasierten Refaktorisierung, auf dem bereits einige Refaktorisierungswerkzeuge beruhen, als vielversprechend.16 Ein Framework, welches die Erstellung von constraintbasierten Refaktorisierungswerkzeugen unterstützt, ist REFACOLA (REFActoring COnstraint LAnguage)17. Dieses wird am Lehrgebiet Programmiersysteme der FernUniversität in Hagen entwickelt. Damit eine Refaktorisierung zu einem korrekten Ergebnis führt, muss ein Refaktorisierungswerkzeug die „Einschränkungen und spezifischen Regeln“ einer Programmiersprache, wie sie zum Beispiel in der Java-Sprachspezifikation beschrieben sind, beachten [St10]. Sie bilden die Vorbedingungen für eine Refaktorisierung. Diese Arbeit soll nun die Definition der Vorbedingungen für das Umbenennen von Identifiern auf Basis des constraintbasierten Ansatzes diskutieren. Obwohl das REFACOLA-Framework unterschiedliche Programmiersprachen18 unterstützt, wird für diese Arbeit ausschließlich der Java-spezifische Teil betrachtet. Die Idee bei der constraintbasierte Programmierung ist, ein Problem deklarativ zu beschreiben, um die Lösung dann von einem speziellen Programm, dem Constraintlöser, berechnen zu lassen [HW07, S. 53]. In REFACOLA wird dies genutzt, um Bedingungen für eine Refaktorisierung auf einfache Art in einer Constraintregel zu beschreiben. Dafür wird die zum REFACOLAFramework gehörende, gleichnamige, domänenspezifische Sprache verwendet. Ein Constraint legt „Bedingungen oder Einschränkungen auf Objekten oder Beziehungen zwischen diesen fest“ [HW07, S. 53]. Eine Constraintregel könnte dann sinngemäß folgendes aussagen: „der deklarierte Name einer Variablen muss mit dem Referenznamen für die Variablen übereinstimmen“, oder kurz Variablendeklaration.identifier = Variablenreferenz.identifier. Durch Anwendung der Constraintregeln auf das zu ändernde Programm können Constraints erzeugt werden, die im Gesamten ein sogenanntes Constraintsystem bilden [SKP11]. Dieses kann vom Constraintlöser auf Erfüllbarkeit geprüft werden. Kann der Constraintlöser für ein Problem keine Lösung finden, bedeutet dies, dass eine Refaktorisierung nicht durchgeführt werden kann. Es kann aber auch mehrere Lösungen geben, die dann alle gleichermaßen richtig sind und von denen eine beliebige ausgewählt werden kann. Aufgrund der oben dargestellten Regel ergibt sich für eine korrekte Änderung, dass alle Referenznamen geändert werden müssen, wenn der deklarierte Name geändert wird. Ohne die Regel kommt es zu einem Übersetzungsfehler, wie es in Listing 3.8 dargestellt wird, da nur der Name der Deklaration x geändert wird, nicht aber der Name der Referenz auf die Variable. 16 Ein anderer Ansatz fasst „Programme als Graphen“ auf. Die notwendigen Bedingungen einer Programmiersprache werden ebenfalls deklarativ als Graph-Transformationsregeln beschrieben 17 Eine detaillierte Darstellung findet sich unter anderem in [SKP11]. 18 Beispielsweise werden die Programmiersprachen Java und Eiffel unterstützt vgl. [SKP11] Grundlagen 14 package a; public class A { int x; int y = x; } package a; public class A { int xNew; //ok int y = x; //compile fehler } Listing 3.8 unvollständige Refaktorisierung 3.2.1 Definition Refaktorisierungsmodule In REFACOLA benötigt man drei verschiedene Module für die Erstellung eines konkreten Werkzeugs [SKP11]: eine Sprachdefinition (language specification) ein Regelset (ruleset) und eine Refaktorisierungsdefinition (refactoring definition) Abbildung 3.2: Module eines REFACOLA-Refaktorisierungswerkzeugs Die Module müssen, damit sie genutzt werden können, vom REFACOLA eigenen Compiler in Quelltext übersetzt werden. Nachfolgend werden zunächst die einzelnen Komponenten erläutert. Die weiteren Beschreibungen der einzelnen Module beruhen auf [SKP11]. Die REFACOLA-Sprachdefinition - Java.language.refacola Um ein korrektes Ergebnis zu erzielen, benötigt ein Refaktorisierungswerkzeug Informationen über die Programmelemente und ihre spezifischen Gegebenheiten. Diese werden in der REFACOLA-Sprachdefinition abgebildet und von den Regelsets und Refaktorisierungsdefinitionen eingebunden. Damit stellt sie sozusagen einen Baukasten für diese dar. Da sich die aktuelle Sprachdefinition für Java noch in der Entwicklung befindet, sind noch nicht alle benötigten Elemente vollständig abgebildet. Ein Ausschnitt findet sich in Listing 3.9 und wird kurz beschrieben. Nach Festlegung der Programmiersprache werden die einzelnen Programmelemente kinds mit ihren Eigenschaften properties beschrieben, wie beispielsweise der Identifier identifier oder die Zugreifbarkeit accessibility. Beim Refaktorisieren werden eben diese Eigenschaften geändert. Für diese können weiterhin Wertebereiche festgelegt werden. Für Zugriffsmodifier sind dies zum Beispiel die Werte private, package, protected und public. Grundlagen 15 language Java kinds abstract Entity <: ENTITY NamedEntity <: Entity{identifier} LocalVariable <: NamedEntity,TypedEntity properties identifier: Identifier hostPackage: Packages accessibility: AccessModifierDomain //Element mit Eigenschaft //Mehrfachvererbung // Eigenschaften domains //Wertebereiche AccessModifierDomain = {private, package, protected, public} queries mainMethod(mainMethod: StaticMethod) //Query Listing 3.9: Ausschnitt REFACOLA-Sprachdefinition für Java (Java.language.refacola)19 kinds können voneinander erben, auch mehrfach. Interessant für die Refaktorisierung von Identifiern sind vor allem die kinds NamedEntity bzw. NamedReference und alle die von ihnen erben. Abschließend können noch queries definiert werden, auf diese wird im Folgeabschnitt eingegangen. Das Regelset - Names.ruleset.refacola Im Regelset werden die einzelnen Constraintregeln beschrieben. Ziel ist es die synthaktischen und semantischen Vorgaben einer Programmiersprache vollständig in Constraintregeln abzubilden. Damit wären korrekte Refaktorisierungen ohne zusätzliche Sonderfallbehandlungen, die häufig die Ursache für Fehler sind, möglich [St10]. Die Constraintregeln bilden die Basis für die Constraintgenerierung und haben nach [SKP11] den allgemeinen Aufbau: programm queries constraints Vor der Constraintregel werden zunächst im for all–Block die Regelvariablen definiert. Diese entsprechen den Programmelementtypen, für welche eine Constraintregel gelten soll. In dem Beispiel in Listing 3.10 sind dies alle Typen mit einem Namen (NamedType) und alle Konstruktoren (Constructor) eines Programms. Anhand einer Query kann die Anzahl der Programmelemente eingegrenzt werden, welche bei der Constraintprüfung und -generierung berücksichtigt werden müssen. Queries werden in der Sprachdefinition festgelegt. Ist keine Query vorhanden, werden alle Programmelemente selektiert, die dem Typ der Regelvariablen entsprechen. Der constraint-Teil beschreibt die Bedingungen, welche bei einer Refaktorisie- 19 Einträge im Listing 3.9 wurden aus Platzgründen teilweise modifiziert Grundlagen 16 rung für die zu ändernde Eigenschaft einzuhalten sind und dient als Vorlage zur Generierung der Constraints. Um die einzelnen Constraintregeln besser zu strukturieren, sind sie auf mehrere Regelsets aufgeteilt. import "Java.language.refacola" ruleset Names //eindeutiger Name language Java rules ConstructorNames for all type: Java.NamedType //Regelvariablen constructor: Java.Constructor do if Java.instantiates(constructor, type) //Query then type.identifier = constructor.identifier //Constraint end Listing 3.10: Ausschnitt Regelset Names.ruleset.refacola Die Refaktorisierungsdefinition - RenameMember.refactoring.refacola Abschließend wird die Refaktorisierungsdefinition beschrieben. In diesem Modul erfolgt die Festlegung, welche Eigenschaft bei einer Refaktorisierung geändert werden soll. Diese wird unter forced changes angegeben. Beim Umbenennen eines Programmelements ist dies die Eigenschaft identifier. Allerdings reicht es häufig nicht aus, nur den Bezeichner eines Programmelements zu ändern. Damit das Programm korrekt bleibt, müssen auch Referenzen auf das Element, beispielsweise in Importen, angepasst werden [St10]. Diese zusätzlichen Änderungen müssen unter allowed changes explizit für die einzelnen kinds erlaubt werden [SKP11]. Nicht zuletzt müssen die Regelsets angegeben werden, welche für die Refaktorisierung verwendet werden sollen. import import import import "Java.language.refacola" "Accessibility.ruleset.refacola" "Types.ruleset.refacola" "Names.ruleset.refacola" refactoring RenameMember languages Java uses Accessibility, Types, Names forced changes identifier of Java.NamedEntity as NewName allowed changes identifier of Java.NamedReference {initial, NewName @ forced} identifier of Java.Constructor {initial, NewName @ forced} Listing 3.11: Ausschnitt Refaktorisierungsdefinition (RenameMember.refactoring.refacola) Grundlagen 17 Abhängig von der Komplexität einer Refaktorisierung leisten Refaktorisierungswerkzeuge unterschiedlich viele Einzelschritte. Je komplizierter die Umstrukturierung ist, desto mehr Einzelaktionen sind nötig. Vergleicht man Werkzeuge miteinander, lassen sich einige Parallelen feststellen. Beispielsweise müssen beim Umbenennen einer Klasse alle Deklarationen oder Importe für den Typ mit angepasst werden. Beim Verschieben eines Typs in ein anderes Paket müssen Paketdeklaration und ebenfalls Importe angepasst werden. Für beide Aktionen ist die Eindeutigkeit von Namen zu prüfen. REFACOLA löst dies, indem in die einzelnen Refaktorisierungsdefinitionen mehrere Regelsets eingebunden werden können. Ein Regelset wird folglich nicht speziell für ein einzelnes Werkzeug definiert, sondern kann von mehreren Werkzeugen genutzt werden. Die einzelnen Regeldefinitionen ergänzen sich. 3.2.2 Ablauf einer Refaktorisierung in REFACOLA Um eine Refaktorisierung mit REFACOLA durchzuführen, wird zunächst der komplette Quelltext eines Programms eingelesen. Hierbei werden die einzelnen Programmelemente und deren Eigenschaften, Beziehungen und Abhängigkeiten identifiziert. REFACOLAAlle relevanten Informationen werden, entsprechend der Sprachdefinition, als Fakten gespeichert. Anschließend wird geprüft, ob eine vordefinierte Constraintregel die für die zu ändernde Eigenschaft angewendet werden kann. Wenn dem so ist, werden mittels Queries, diejenigen Fakten ermittelt, die für die Refaktorisierung in Frage kommen. Sind die Bedingungen erfüllt werden entsprechende Constraints generiert. Ein Constraintlöser versucht, eine Lösung für das Problem zu berechnen.20 Wird keine Lösung gefunden, wird der Vorgang abgebrochen. Anderenfalls werden die Änderungsanweisungen erstellt. Anschließend führt eine Rückschreibekomponente die Änderungen, wie Einfügen, Verschieben, Löschen oder Ändern am bestehenden Quelltext durch. 3.2.3 Eclipse - Integration Die aktuelle Version von REFACOLA für Java ist in die Entwicklungsumgebung Eclipse21 integriert [SKP11]. Unter dem Namen Eclipse wird nicht nur eine Entwicklungsumgebung (IDE), sondern ein ganzes „Framework für die Anwendungsentwicklung“ [TH12] bereitgestellt. Zu diesem gehören mehrere Projekte. Eines davon sind die Java Development Tools (JDT).22 Diese stellen Bibliotheken zu Verfügung, mit denen es möglich ist, bestehenden Java-Quellcode einzulesen und dessen Elemente in einem Baum darzustellen. Zum einen ist dies das Java Model, zum anderen der Abstrakte Syntaxbaum (AST). Darüber kann auf einzelne Programmelemente und deren Eigenschaften zugegriffen werden. Dies wiederum nutzt das Refaktorisierungswerkzeug, um die Elemente zu analysieren oder ändern. Das Java Model entspricht in etwa der Ansicht im Eclipse Package Explorer bzw. der Outline und ist nicht so detailliert wie der AST. Um auf Elemente innerhalb von Methoden zuzugreifen, wie beispielsweise lokale Variablen oder innere Klassen, benötigt man 20 Der Algorithmus zur Ermittlung der Constaints wird in [SKP11] vorgestellt (vgl. Kapitel 4) Hier Eclipse Juno in der Version 4.2 , unter http://www.eclipse.org/juno/ 22 http://www.eclipse.org/jdt/ 21 Grundlagen 18 den AST. Die JDT-Bibliotheken werden für die Implementierung von Faktengenerierung und Rückschreibekomponente ebenso eingesetzt wie für die Testerstellung. Des Weiteren erlaubt es Eclipse dem Entwickler, über seinen Plugin-Mechanismus die IDE individuell zu erweitern. Dies wird in REFACOLA durch eine Reihe von eigenen Plugins genutzt, unter anderem für Tests. Implementierung 19 4 Implementierung 4.1 Ausgangssituation REFACOLA wird seit geraumer Zeit am Lehrstuhl Programmiersysteme der FernUniversität in Hagen entwickelt, deshalb sind eine Vielzahl von Komponenten bereits zu einem großen Teil vorhanden. Dazu gehören die REFACOLA-Sprachdefinition für Java, die Faktengenerierung und eine Rückschreibekomponente. Dagegen waren Regelset und Refaktorisierungsdefinition für das Ändern von Identifiern jeweils nur mit einem Eintrag vorhanden und bildeten damit den Ausgangspunkt für diese Arbeit. Ein Dialog für das Umbenennen von Programmelementen existierte ebenfalls. Zudem sind in REFACOLA eine Reihe von Test-Plugins vorhanden, die individuell erweitert werden können. 4.2 Das Regelset im Überblick Die im Laufe dieser Arbeit entstandenen Regeln lassen sich grob in zwei Gruppen einteilen. Ein Teil stellt die Eindeutigkeit eines Identifiers in dessen Gültigkeitsbereich sicher. Der andere Teil gewährleistet den Erhalt der Bindung von Referenzen an das richtige Programmelement. Das gilt beispielsweise für Methodenaufrufe oder Referenzen auf Variablen. In den Constraintregeln werden Bedingungen formuliert, die verhindern sollen, dass nach einer Refaktorisierung die semantische und syntaktische Vorgaben der Programmiersprache Java verletzt werden. Darüber hinaus sollen sie für die Erhaltung des ursprünglichen Programmverhaltens sorgen [SKP11]. Einige der im Anschluss vorgestellten Regeln wurden nicht im Rahmen dieser Arbeit entwickelt. Sie werden aber zur Ergänzung mit aufgeführt. Um sie zu unterscheiden, sind sie mit dem Kommentar „Autor: Name“ gekennzeichnet 4.2.1 Allgemeines Ein wesentlicher Aspekt bei der Entwicklung der Regeln ist, inwieweit die benötigten Komponenten in der REFACOLA-Sprachdefinition vorhanden sind. Gleiches gilt für Faktengenerierung und Rückschreibekomponente. Nur wenn die Fakten richtig ermittelt und die Änderungen korrekt in das zu refaktorisierende Programm übertragen werden, kann eine Refaktorisierung erfolgreich durchgeführt werden. Falls Erweiterungen in diesen Komponenten nötig waren, werden diese mit der jeweiligen Regelbeschreibung vorgestellt. 4.2.2 Eindeutige Namen für Typen, Felder und Methoden Es gibt in der Java-Sprachspezifikation einige Vorgaben die bestimmen, wann der einfache Name eines Programmelements in einem bestimmten Kontext eindeutig sein muss. Ist diese nach einer Refaktorisierung nicht mehr gegeben, führt dies zu Kompilierungsfehlern. Da Typen, Felder und Methoden in unterschiedlichen Kontexten verwendet werden und damit eindeutig zu unterscheiden sind, erlaubt Java hier die Deklaration gleichnamiger Programmelemente in einem Typen (JLS §8.3). So ist es möglich, in einer Klasse sowohl eine Methode und eine Variable zu deklarieren, die den gleichen Namen wie sie selbst haben (vgl. Listing 4.1). Implementierung 20 package a; public class A{ String A; void A(){} } Listing 4.1: gleichnamige Elemente in einem Typen Handelt es sich dagegen um gleiche Programmelementtypen, müssen bei der Refaktorisierung Bedingungen für die Eindeutigkeit von Identifiern beachtet werden. Zunächst soll dies bei Klassen, Enumerationen und Interfaces betrachtet werden, anschließend werden Felder und Methoden diesbezüglich untersucht. Eindeutigkeit für Typnamen Innerhalb desselben Paketes darf es keine zwei gleichnamigen Topleveltypen geben (JLS §7.6). Dieses wird über die Regel Names.UniqueTopLevelTypeIdentifier23 sichergestellt. Dabei ist es unerheblich, ob die Typen innerhalb der gleichen Compilationunit definiert werden oder nicht. Auch deren Zugreifbarkeit hat keinen Einfluss auf diese Bedingung. UniqueTopLevelTypeIdentifier for all tlt1: Java.TopLevelType tlt2: Java.TopLevelType do if tlt1 != tlt2 then (tlt1.hostPackage = tlt2.hostPackage) -> (tlt1.identifier != tlt2.identifier) end Regel 1: Names.UniqueTopLevelTypeIdentifier Membertypen müssen eindeutige Identifier haben, wenn sie in einem Typen auf gleicher Ebene deklariert werden. Listing 4.2 zeigt ein Beispiel, in dem in einer Klasse A zwei gleichnamige Membertypen deklariert sind. Auch wenn es sich hierbei um verschiedene Arten von Typen handelt, ist dies nicht erlaubt (JLS §8.5). Das Problem wird durch die Regel Names.UniqueMemberTypeNames2 vermieden. package a; public class A{ class B{} interface B{} //compile fehler, Namenskonflikt mit Klasse B } Listing 4.2: Verletzung Eindeutigkeit eines Typ-Identifiers 23 Verweise auf Regeln werden immer in der Form Regelsetname.Regelname vorgenommen. Für Names.UniqueTopLevelTypeIdentifier ist Names der Name des Regelsets in dem die Regel definiert ist und UniqueTopLevelTypeIdentifier ist der eigentliche Regelname. Implementierung 21 UniqueMemberTypeNames2 for all memberType1: Java.MemberType memberType2: Java.MemberType do if memberType1 != memberType2 then (memberType1.owner = memberType2.owner) -> (memberType1.identifier != memberType2.identifier) end Regel 2: Names.UniqueMemberTypeNames2 Weiterhin können Typen innerhalb einer Compilationunit beliebig ineinander geschachtelt werden. Dabei darf ein eingeschlossener Typ nicht den gleichen Namen haben wie ein ihn umschließender Typ, einschließlich des Topleveltyps (JLS §8.1, §9.1, §8.5). Gleichnamige Membertypen innerhalb eines Typen sind erlaubt, wenn sie nicht auf gleicher Ebene deklariert und nicht ineinander verschachtelt sind. Listing 4.3 illustriert die Problematik. In Klasse A gibt es zwei Namenskonflikte die jeweils zu Übersetzungsproblemen führen. Zum einen haben zwei Membertypen denselben Namen B, zum anderen hat das Interface A den gleichen Namen wie der Topleveltyp. Beides ist nicht erlaubt. package a; public class A { class B { class B {} //namenskonflikt mit Membertyp B } class C { interface A {} //namenskonflikt mit Topleveltyp A } } Listing 4.3: eindeutige Namen bei verschachtelten Typen Damit die Überprüfung der Namen von umschließenden Typen möglich ist, müssen diese während der Faktengenerierung von REFACOLA für jeden Membertyp ermittelt werden. Gespeichert werden sie dann in der Sequenz enclosingNamedTypes. UniqueMemberTypeNames1 /* Autor: A.Thies */ for all t: Java.MemberType do if all(t) then t.identifier != t.enclosingNamedTypes.identifier, //1.Constraint t.identifier != t.tlowner.identifier //2.Constraint end Regel 3: Names.UniqueMemberTypeNames1 Implementierung 22 Bei der Regel Names.UniqueMemberTypeNames1 fällt auf, dass diese zwei Constraints abbildet, nämlich zum einen die Prüfung der eindeutigen Namen für die umschließenden Typen und zum anderen die Prüfung für den Topleveltypnamen, da enclosingNamedTypes den Namen des Topleveltypen nicht enthält. Wenn Regeln über dieselben Regelvariablen verfügen und auch die Queries gleich sind, können diese in einer Regel mit mehreren Constaints zusammengefasst werden. Alternativ könnten aber auch zwei einzelne Regeln erstellt werden. Obwohl Java eine sogenannte casesensitive Sprache ist, darf bei Namen von Topleveltypen eines Paketes ausnahmsweise nicht zwischen Groß- und Kleinschreibung unterschieden werden. Dies aber nur indirekt ein Problem gleichnamiger Typnamen. Das eigentliche Problem ist der Name der Compilationunit. Die Problematik wird in Kapitel 4.3.3 näher erläutert. Eindeutigkeit für Felder und Methoden Innerhalb eines Typen dürfen keine gleichnamigen Felder definiert sein, der Typ der Deklaration spielt dabei keine Rolle (JLS §8.3). Dies wird über die Regel Names.UniqueFieldIdentifier sichergestellt. Methoden können nur dann denselben Namen haben, wenn sich ihre Signaturen unterscheiden (JLS §8.4.2). Dafür sorgt die Regel Names.UniqueMethodIdentifier. UniqueFieldIdentifier for all field1: Java.Field field2: Java.Field do if field1 != field2 then field1.owner = field2.owner ->(field1.identifier != field2.identifier) end Regel 4: Names.UniqueFieldIdentifier UniqueMethodIdentifier for all method1: Java.RegularTypedMethod method2: Java.RegularTypedMethod do if method1 != method2 then (method1.owner = method2.owner) and (method1.parameters.declaredParameterType = method2.parameters.declaredParameterType) -> (method1.identifier != method2.identifier) end Regel 5: Names.UniqueMethodIdentifier Implementierung 4.2.3 23 Namensgleichheit von Typ und Konstruktor Wird in einer Klasse oder einer Enumeration ein Konstruktor deklariert, muss der einfache Name eines Konstruktors mit dem einfachen Namen des ihn definierenden Typen übereinstimmen (JLS §8.8, §8.9). Interfaces besitzen keine Konstruktoren. Ein Konstruktor wird bei der Instanzierung eines Objekts aufgerufen. Die hier verwendete Query Java.instantiates ermittelt hierfür alle Fakten aus dem eingelesenen Programm. ConstructorNames for all type: Java.NamedType constructor: Java.Constructor do if Java.instantiates(constructor, type) then type.identifier = constructor.identifier end Regel 6: Names.ConstructorNames Bei der Entwicklung der Regeln fiel das in Listing 4.4 dargestellte Problem auf. Konstruktor und Methode haben hier den gleichen Namen. Der Versuch, die Methode A umzubenennen, führt aufgrund eines Fehlers in den JDT-Bibliotheken zu Problemen, da beim Ermitteln der Methode fälschlicherweise der Konstruktor zurückgeliefert wird.24 package a; public class A{ A(){} void A(){} } Listing 4.4: gleichnamige Konstruktor- und Methodendeklaration in einer Klasse 4.2.4 Namensgleichheit von Topleveltyp und Quelltextdatei Ein Topleveltyp kann die Zugreifbarkeit public oder package private haben, andere Zugreifbarkeiten sind für Topleveltypen nicht erlaubt (JLS §7.6). Hat ein Topleveltyp die Zugreifbarkeit public, muss der Typname mit dem der Quelltextdatei übereinstimmen. Dies wird über die Regel Names.TopLevelTypeNames sichergestellt. Innerhalb einer Compilationunit darf es nur einen Typen mit dieser Zugreifbarkeit geben. Ist diese mit package private angegeben, darf sich der Name der Datei unterscheiden. (vgl. JLS §7.6). 24 Da die Ursache in den JDT-Bibliotheken liegt, hat das Rename-Werkzeug von Eclipse dasselbe Problem. Ein entsprechender Fehler wurde erfasst (vgl. EclipseBug 399476). Implementierung 24 TopLevelTypeNames for all tlt: Java.TopLevelType tr: Java.TypeRoot do if all(tlt), all(tr) then (tlt.typeRoot = tr and tlt.accessibility = #public) -> (tlt.identifier = tr.identifier) End Regel 7: Names.TopLevelTypeNames 4.2.5 Referenzen Bei einem Großteil der Stellen, die beim Umbenennen eines Identifiers zu prüfen sind, handelt es sich um Referenzen. Zu unterscheiden sind zum einen Referenzen, die dem Zugriff auf ein Element dienen und deren Name bei der Deklaration festgelegt wurde. Zum anderen sind es Referenzen auf einen Typ selbst. Typreferenz Bei einer Typreferenz muss der Referenzname mit dem Namen des Typs übereinstimmen, dies wird über die Regel Names.TypeReferenceIdentifier sichergestellt. Zu den Typreferenzen gehören: Typangaben einer Deklaration Typangaben in implements-, extends-, throws-Klauseln Typangaben in Cast- oder instanceOf-Expressions Typangaben für den Returntyp einer Methode Typangaben für formale Parameter in Methoden und Konstruktoren Listing 4.5 zeigt eine Typreferenz der Klasse B in der extends-Klausel der Klasse A. Wird nun beispielsweise die Klasse B umbenannt, muss auch diese Typreferenz umbenannt werden. package a; public class A extends B { package a; public class B{ } } Listing 4.5: Typreferenz in extends-Klausel Für die Auswahl der Programmelemente gibt es hier keine spezielle Query, daher werden hier alle Typen und allen Referenzen des eingelesenen Programms ausgewählt. Im Rahmen dieser Arbeit wurde die Ermittlung von einigen Typreferenzen in der Faktengenerierung ergänzt, beispielsweise für Parametertypen oder throwsKlauseln. Implementierung 25 TypeReferenceIdentifier for all type: Java.NamedType ref: Java.TypeReference do if all(type), all(ref) then (type = ref.typeBinding) -> (type.identifier = ref.identifier) end Regel 8: Names.TypeReferenceIdentifier Referenz einer Deklaration Auch für den Bezeichner einer Deklaration muss sichergestellt werden, dass nach einer Refaktorisierung dessen Referenzen noch namensgleich sind. Ansonsten geht die Bindung an das deklarierte Programmelement verloren. Dies gewährleistet die Regel Names.ReferenceIdentifier. Betroffen sind neben Deklarationen von Variablen und formalen Parametern auch Methoden und Konstruktoren. ReferenceIdentifier /* Autor: A.Thies */ for all reference: Java.NamedTypedReference entity: Java.NamedEntity do if Java.binds(reference, entity) then reference.identifier = entity.identifier end Regel 9: Names.ReferenceIdentifier 4.2.6 Überschreiben und Verbergen von Methoden Die Regel Names.OverridingMethodNames sorgt dafür, dass der Name einer überschiebenen Methode im Supertyp mit der des Subtyps übereinstimmt. Sie kann sehr einfach gehalten werden, weil die Ermittlung überschriebener Methoden über die Query Java.overrides erfolgt. Daher ist es unnötig, beispielsweise explizit zu prüfen, ob die Parameterlisten der beiden Methoden übereinstimmen oder der Rückgabewert zulässig ist. Implementierung 26 OverridingMethodNames for all superMethod: Java.InstanceMethod subMethod: Java.InstanceMethod do if Java.overrides(subMethod, superMethod) then subMethod.identifier = superMethod.identifier end Regel 10: Names.OverridingMethodNames Für ein Refaktorisierungswerkzeug ist es wichtig, dass sich bei der Änderung eines Methodennamens die Methodenbindungen nicht verändern, ansonsten kann dies zu einem veränderten Programmverhalten führen. In Listing 4.6 darf daher die Methode n() nicht in m() umbenannt werden, weil ansonsten die Methode der Superklasse überschrieben wird und somit die Methode m() der Klasse C aufgerufen wird anstatt der Methode in Klasse A. package a; public class A{ class B{ int m(){return 0;} } class C extends B{ int n(){ //umbennenen -> m nicht erlaubt ... return 1; } void getM(){ int myM = m(); //...sonst Änderung der Methodenbindung } } } Listing 4.6: Überschreiben einer Methode durch Umbenennen Daher ist vor der Umstrukturierung ist zu prüfen, ob eine Methode im Vorfeld bereits überschrieben wurde oder nicht. Die Refaktorisierung darf an diesem Umstand nichts ändern. Für das Überschreiben von Methoden sind lt. JLS folgende Vorgaben zu beachten Ein Überschreiben von Methoden ist nur für Instanzmethoden zulässig (JLS §8.4.8.1). Eine Instanzmethode darf keine statische Methode überschreiben und umgekehrt (JLS §8.4.8.1, §8.4.8.2). Implementierung 27 Das Regelset Accessibility25 enthält hierfür bereits entsprechende Regeln. Die erste Vorgabe wird durch die Regel Accessibility.AccidentalOverriding umgesetzt. Des Weiteren gibt es für die zweite Vorgabe die Regeln Accessibility.InstanceMethodOverridingStaticMethod und Accessibility.StaticMethodHidesInstanceMethod.26 Exemplarisch wird Accessibility.AccidentalOverriding vorgestellt, auf eine detaillierte Darstellung der anderen Regeln wird jedoch verzichtet. Die Regel Accessibility.AccidentalOverriding gibt einen guten Eindruck, dass die Formulierung eines Constraints durchaus kompliziert werden kann. Hier wird eine ganze Reihe an Bedingungen in dem Constraint formuliert, die alle erfüllt sein müssen. So ist zum Beispiel eine Bedingung, dass die beiden Methoden für die die Regel gelten soll, sich bereits überschreiben, oder dass es keine Vererbungsbeziehung zwischen den Methodenbesitzern (beispielsweise Klassen) gibt, oder die Bezeichner der Methoden unterschiedlich sind, usw. AccidentalOverriding /* Autor: A.Thies */ for all superMethod: Java.InstanceMethod subMethod: Java.InstanceMethod do if all (subMethod), all(superMethod) then Java.overrides(subMethod, superMethod) or (not Java.sub(subMethod.owner, superMethod.owner)) or (superMethod.identifier != subMethod.identifier) or (not (subMethod.parameters.declaredParameterType = superMethod.parameters.declaredParameterType)) or (superMethod.accessibility = #private) or ((superMethod.accessibility = #package) and (superMethod.hostPackage != subMethod.hostPackage)) end Regel 11: Accessibility.AccidentalOverriding Alle vorgestellten Regeln behandeln allerdings noch nicht den Fall, dass es sich bei einer Methode im Supertyp um eine finale Methode handelt. Daher wird zusätzlich die Regel Names.AccidentalHidingFinalMethod benötigt. Die Regel gilt sowohl für statische als auch für Instanzmethoden, weil eine finale Methode weder überschrieben noch versteckt werden darf (JLS §8.4.3.3). Soll die Methode n() der Klasse B im Listing 4.7 nach m() umbenannt werden, kommt es zu einem Kompilierungsfehler, weil dies eine finale Methode der Superklasse verbergen würde.27 25 Regeln aus diesem Regelset wurden bereits vor dieser Arbeit erstellt (Autor A.Thies). 26 Es gibt weitere Constraintregeln im Regelset Accessibility welche das unerwünschte Überladen von Methoden verhindern. Auf eine detaillierte Darstellung wird hier verzichtet. 27 Interessanterweise lautet die Kompilermeldung von Sun „cannot override m“ obwohl es sich um eine statische Methode handelt lt. JLS wird eigentlich von Hiding gesprochen. Implementierung 28 package a; public class A { static final void m(){} } package a; public class B extends A { static void m(){} //compilefehler } Listing 4.7: Methode mit final-Modifier Für die Ermittlung von Methoden mit dem Modifier final wurde zusätzlich die Sprachdefinition in REFACOLA um die Query Java.finalMethod erweitert. AccidentalHidingFinalMethod for all method1: Java.Method method2: Java.Method do if Java.finalMethod(method2), all(method1) then (method1.identifier != method2.identifier) or (not Java.sub(method1.owner, method2.owner)) or (not(method1.parameters.declaredParameterType = method2.parameters.declaredParameterType)) or ((method1.tlowner != method2.tlowner) and (method2.accessibility = #private)) or ((method1.hostPackage != method2.hostPackage) and (method2.accessibility <= #package)) end Regel 12: Names.AccidentalHidingFinalMethod Konstruktoren sind von den vorgestellten Problematiken nicht betroffen, weil sie weder überschrieben noch versteckt werden können (JLS §8.8). 4.2.7 Eager Interface Supertyp m() <<interface>> Subtyp <<realize>> EagerIF m() Abbildung 4.1: Eager Interface Die Einbindung eines Interfaces in eine nicht abstrakte Klasse verlangt, dass von dieser alle in dem Interface deklarierten Methoden implementiert (JLS §8.1.5). Es wird jedoch nicht vorgeschrieben, ob sie dieses selbst tut. Ebenso kann eine Superklasse diese Aufgabe übernehmen, unabhängig davon, ob sie selbst das Interface Implementierung 29 einbindet (vgl. Abb. 4.1). Zudem muss die implementierende Methode die Zugreifbarkeit public haben, weil alle Methoden eines Interface implizit public sind. Soll nun der Name der Methode im Interface geändert werden, muss sichergestellt werden, dass auch der Name der implementierenden Methode angepasst wird. Ansonsten käme es nach der Refaktorisierung zu einem Kompilierungsfehler. Dies gilt auch im umgekehrten Fall, wenn der Name der Methode im Supertyp geändert wird. EagerInterfaceMethodNames for all type: Java.Class typeMethod: Java.InstanceMethod interfaceMethod: Java.InstanceMethod do if Java.eagerInterfaceImplementation(type, typeMethod, interfaceMethod) then typeMethod.identifier = interfaceMethod.identifier end Regel 13: Names.EagerInterfaceMethodNames Für die Regel Names.EagerInterfaceMethodNames, welche die oben beschriebenen Bedingungen umsetzt, werden die Fakten anhand der Query Java.eagerInterfaceImplementation nach solchen Programmkonstellationen durchsucht. Diese war bereits vorhanden, musste aber um den Parameter interfaceMethod erweitert. 4.2.8 Eindeutige Namen für lokale Variablen und formale Parameter Aus Gründen der Lesbarkeit gelten die folgenden Ausführungen für Methoden ebenso für Konstruktoren. Auf Besonderheiten oder Unterschiede wird explizit hingewiesen. Innerhalb des Deklarationsbereichs einer Methode müssen die darin definierten lokalen Variablen eindeutige Namen haben. Formale Parameter und lokale Variablen Obwohl formale Parameter auch als spezielle lokale Variablen gelten können, wird hier für die Regeldefinition zwischen beiden unterschieden. Die formalen Parameter einer Methode müssen innerhalb ihrer Parameterliste eindeutige Bezeichner haben (JLS § 8.4.1). Dies drückt sich in der Constraintregel Names.UniqueLocalVariableNames1 aus. UniqueLocalVariableNames2 stellt dagegen sicher, dass keine lokale Variable namensgleich mit einem der formalen Parameter einer Methode ist. Um abfragen zu können, ob eine lokale Variable oder ein formaler Parameter zur gleichen Methode gehört, wurde eine neue Query in die Sprachdefinition von REFACOLA aufgenommen nämlich Java.declaresLocalVariableOrParameter. Implementierung 30 UniqueLocalVariableNames1 for all parameter1: Java.FormalParameter parameter2: Java.FormalParameter method: Java.MethodOrConstructor do if Java.declaresLocalVariableOrParameter(method, parameter1), Java.declaresLocalVariableOrParameter(method, parameter2), (parameter1 != parameter2) then (parameter1.identifier != parameter2.identifier) end Regel 14: Names.UniqueLocalVariableNames1 UniqueLocalVariableNames2 for all parameter: Java.FormalParameter localVariable: Java.LocalVariable methodOrConstructor: Java.MethodOrConstructor do if Java.declaresLocalVariableOrParameter(methodOrConstructor, parameter), Java.declaresLocalVariableOrParameter(methodOrConstructor, localVariable) then (parameter.identifier != localVariable.identifier) end Regel 15: Names.UniqueLocalVariableNames2 Locale Variablen in Anweisungsblöcken Initialisierungsblöcke besitzen keine formalen Parameter, daher finden sie für die ersten beiden Regeln keine Berücksichtigung. Anders ist dies für die nächste Regel Names.UniqueLocalVariableNames3. Weil innerhalb einer Methode verschiedene Anweisungsblöcke deklariert werden können, muss die hierarchische Ordnung der einzelnen Variablen beachtet werden es, wie es bei verschachtelten Typen der Fall war (vgl. Kapitel 4.2.2). Auch in verschachtelten Anweisungsblöcken dürfen lokale Variablen nicht den gleichen Identifier besitzen. Implementierung 31 UniqueLocalVariableNames3 for all localVariable: Java.LocalVariable do if Java.hasEnclosingVariables(localVariable) then localVariable.identifier != localVariable.enclosingLocalVariables.identifier end Regel 16: Names.UniqueLocalVariableNames3 Allerdings ist die Ermittlung von umschließenden Blöcken nicht einfach, da es eine Vielzahl von verschiedenen Anweisungsblöcken gibt, in denen lokale Variablen deklariert werden können. Da diese bei der Ermittlung jeweils eigens behandelt werden müssen, ist dies komplizierter als für Typen. Eine entsprechende Funktion fehlte in REFACOLA. Es wurde zunächst eine prototypische Implementierung vorgenommen, die in Kapitel 5.1.1 zur Diskussion gestellt wird. Außerdem wurde eine Query Java.hasEnclosingVariables eingeführt. Sie prüft, obe es für eine lokale Variable überhaupt Variablen aus umschließenden Blöcken gibt. Lokale Klassen wurden noch nicht berücksichtigt. 4.2.9 Pakete und Importanweisungen Die Paketdeklaration einer Compilationunit muss mit dem Paketnamen übereinstimmen, in welchem sie abgelegt ist (JLS §7.4.1). Liegt sie im default-Paket gibt es keine Paketdeklaration. Wird ein Typ von einem Paket a in ein Paket b verschoben, muss auch die Paketdeklaration angepasst werden. Dieses wird durch die Regel Names.PackageDeclarationNames sichergestellt. PackageDeclarationNames /* Autor: A.Thies */ for all packageDeclaration: Java.PackageDeclaration typeRoot: Java.TypeRoot do if Java.packageDeclarationInTypeRoot( packageDeclaration, typeRoot) then packageDeclaration.identifier = typeRoot.hostPackage.identifier end Regel 17: Names.PackageDeclarationNames Importe wurden in REFACOLA bisher weder in der Sprachspezifikation noch in Faktengenerierung oder Rückschreibekomponente unterstützt. Für einen ersten Schritt wurden diese Komponenten insoweit erweitert, dass damit das Ändern für einen konkreten Typ in einem Single-Type-Import (JLS §7.5.1) möglich ist. import examples.refacola.BeispielTyp; Implementierung 32 Über die Regel Names.ImportDeclarationNames1 wird sicherstellt, dass der Name eines verwendeten Typs mit dessen Bezeichnung in der Importanweisung übereinstimmt. ImportDeclarationNames1 for all importDeclaration: Java.ImportDeclaration clazz: Java.NamedType do if Java.importDeclarationInTypeRoot(importDeclaration, clazz) then (importDeclaration.typeBinding = clazz) -> (importDeclaration.identifier = clazz.identifier) end Regel 18: Names.ImportDeclarationNames1 Des Weiteren darf der Name eines Topleveltypen nicht mit dem Typnamen in einem Single-Type-Import übereinstimmen, wenn der Topleveltypen in einer anderen Compilationunit deklariert wurde (JLS §6). Listing 4.8 zeigt eine Klasse, die einen Kompilefehler aufweist, da sie einen gleichnamigen Typ aus einem anderen Pakt importiert package a; import b.A; //compilefehler public class A { } package b; public class A { } Listing 4.8: gleichnamiger Topleveltyp und Single-Type-Import Die Regel Names.ImportDeclarationNames2 setzt diese Vorbedingung um. ImportDeclarationNames2 for all importDeclaration: Java.ImportDeclaration tlt: Java.TopLevelType do if all(importDeclaration), all(tlt) then (importDeclaration.typeBinding != tlt) -> (importDeclaration.identifier != tlt.identifier) end Regel 19: Names.ImportDeclarationNames2 Aufgrund der zeitlichen Begrenzung und fehlender Komponenten konnte die Thematik rund um Pakete und Importe an dieser Stelle jedoch nicht weiterverfolgt werden. Die Unterstützung für statische Importe fehlt daher genauso wie das Ändern einzelner Fragmente einer Import- oder Paketanweisung. Implementierung 4.3 33 Einschränkungen bei der Regeldefinition Nicht alle Bedingungen für eine korrekte Refaktorisierung von Identifiern konnten über das Regelwerk vollständig sichergestellt werden. Die Problematiken werden in diesem Kapitel erläutert. Zudem werden Themen aufgeführt, die aus zeitlichen Gründen nicht umgesetzt werden konnten. 4.3.1 Ergänzung von Qualifiern Konzepte wie Abschatten, Verbergen oder Überschreiben machen es möglich, dass namensgleiche Deklarationen sich „überlagern“. Probleme, die dabei entstehen, werden exemplarisch für die Verschattung einer Instanzvariablen gezeigt. In Listing 4.9 wird die Instanzvariable i der Toplevelklasse A der Instanzvariablen j in der inneren Klasse B zugewiesen. Wird nun die Instanzvariable i nach j umbenannt, wird sie durch die gleichnamige Variable der inneren Klasse abgeschattet. Dadurch es kommt zu einem Kompilierungsfehler, da nun die Variable j sich selbst zugewiesen wird, bevor sie initialisiert ist. package a; public class A{ private int i; class B{ int j=i; } } package a; public class A{ private int j; class B{ int j=j; //compile fehler } } Listing 4.9: Abschattung einer Instanzvariablen nach Refaktorisierung I Im Gegensatz zu Listing 4.9, wo es sich um eine initiale Zuweisung handelt, wurde in Listing 4.10 die lokale Variable j der Klasse B bereits im Vorfeld initialisiert. Hier kommt es zu keinem Kompilierungsfehler. Jedoch hat sich unerwünschterweise der Ablauf in dem Programm verändert, was sich unter Umständen erst zur Laufzeit bemerkbar macht. package a; public class A{ private int i; class B{ int j=0; j = i; } } package a; public class A{ private int j; class B{ int j=0; j = j; //geändertes Laufzeit//verhalten möglich } } Listing 4.10: Abschattung einer Instanzvariablen nach Refaktorisierung II Um die oben beschriebenen Fehler zu vermeiden, wurden die beiden Regeln Names.UniqueEnclosingMemberTypeNamesInAssignment1 und Names.UniqueEnclosingMemberTypeNamesInAssignment2 erstellt. Sie stellen für verschachtelte Typen sicher, dass Instanzvariablen, welche in verschiedenen Typen deklariert sind, in Zuweisungen nicht namensgleich sein dürfen. Um differenzieren zu können, ob es sich, wie in Listing 4.9, um eine initiale Zuweisung handelt, kann anhand der Implementierung 34 Query Java.initialAssignment abgefragt werden. Im anderen Fall wird Java.assignment verwendet. UniqueEnclosingMemberTypeNamesInAssignment1 for all field1: Java.InstanceField field2: Java.RegularTypedInstanceField fieldReference: Java.FieldReference expression: Java.Expression do if field1 != field2, Java.binds(fieldReference, field1), Java.isExpression(fieldReference, expression), Java.initialAssignment(field2, expression) then field1.tlowner = field2.tlowner and field1.owner != field2.owner and Java.sub!(field2.owner,field1.owner) ->fieldReference.identifier != field2.identifier end Regel 20: Names.UniqueEnclosingMemberTypeNamesInAssignment1 UniqueEnclosingMemberTypeNamesInAssignment2 for all field1: Java.InstanceField field2: Java.InstanceField fieldReference1: Java.FieldReference fieldReference2: Java.FieldReference expression1: Java.Expression expression2: Java.Expression do if field1 != field2, Java.binds(fieldReference1, field1), Java.binds(fieldReference2, field2), Java.isExpression(fieldReference1, expression1), Java.isExpression(fieldReference2, expression2), Java.assignment(expression2, expression1) then field1.tlowner = field2.tlowner and field1.owner != field2.owner and Java.sub!(field2.owner,field1.owner) ->fieldReference2.identifier != fieldReference1.identifier end Regel 21: Names.UniqueEnclosingMemberTypeNamesInAssignment2 Implementierung 35 Problematik Leider verhindern die Regeln auch ein Umbenennen, wenn es sich im Zuweisungsausdruck um eine qualifizierte Namensangabe handelt (vgl. Listing 4.11). Dies ist unnötig und bedeutet eine stärkere Restriktion als gewollt, da hier eine eindeutige Unterscheidung der verschiedenen Variablen eigentlich problemlos möglich ist. package a; public class A{ private int j; class B{ A a = new A(); int j=a.j; } } Listing 4.11: Qualifizierter Referenzname in Zuweisung Ein Problem ist, dass die beiden Regeln nur einen speziellen Fall behandeln. Es gibt allerdings eine Vielzahl ähnlicher Probleme. Um alle abzubilden, wären zusätzliche Regeln nötig. Handelt es sich beispielsweise statt einer einfachen Variablenzuweisung um einen geschachtelten Ausdruck, wird dies durch die Regeln nicht mit abgedeckt (vgl. Listing 4.12). package a; public class A{ private int i; class B{ int j=i; } } package a; public class A{ private int j; class B{ int j= new Integer(j); //compile //fehler } } Listing 4.12: geschachtelte Expression in Zuweisung Auch in dem Beispiel in Listing 4.13 kommt es zu einem Kompilierungsfehler nachdem die lokale Variable y und Instanzvariablen x nach dem Umbenennen des Feldes denselben Namen haben. package a; package a; public class A { int x = 0; public class A { int y = 0; void m() { int y = x; } } void m() { int y = y; //compile fehler } } Listing 4.13. Problematik von fehlendem Qualifier bei lokaler Variable Obwohl es sich in allen Fällen um die Verschattung einer Instanzvariable handelt, die durch eine andere Variable in einer Zuweisung abgeschattet wird, hat jeder Fall eigene Bedingungen, die letztlich in eigenen Regeln abgebildet werden müssten. Im ersten Fall sind die Bedingungen der vorhandenen Regeln sogar zu restriktiv. Implementierung 36 Ergänzung Qualifier im Quelltext Eine einfache Lösung für die Problematik bietet Java durch das Einfügen des Qualifiers this an, wie es in Listing 4.14 für das Beispiel aus Listing 4.13 dargestellt ist. Trotz identischer Namen ist jetzt eine Unterscheidung zwischen lokaler und Instanzvariable möglich und das Programm funktioniert auch nach wie vor korrekt.28 Der Qualifier müsste jedoch aktiv beim Refaktorisierungsvorgang ergänzt werden. Eine Möglichkeit für das automatische Einsetzen eines Qualifiers gibt es in REFACOLA aktuell nicht. package a; package a; public class A { int x = 0; public class A { int y = 0; void m() { int y = x; } } void m() { int y = this.y; } } Listing 4.14: Ergänzung Quelltext mit explizitem this-Qualifier Schäfer, Ekman und de Moor stellen in [SEM08] den Ansatz locked names vor, bei dem sie solche Ersetzungen vornehmen. In ihrem Konzept werden Referenzen über symbolische Namen fest an ihre Deklarationen gebunden. Treten Namenskonflikte auf, werden Qualifizierungen eingefügt und nur wenn die Refaktorisierung ein korrektes Ergebnis liefern kann, wird sie auch durchgeführt. Listing 4.15 zeigt Beispiele für den Einsatz von Qualifiern.29 Über den Qualifier super kann beispielsweise auf versteckte oder überschriebene Elemente des Supertyps zugegriffen werden. package a; class SuperA{ int x; void m(){} package a; class B{ int x; static void m(){} } class C { void n(){ B.m(); } package a; class SupA extends SuperA{ void m(){ super.m(); this.x; } void o(){ int x = B.this.x; } } } Listing 4.15 diverse Aufrufe mit Qualifiern 28 Um das Problem bei verschachtelten Typen zu lösen, müsste ein qualifiziertes this ergänzt werden (vgl. Listing 4.9 und Listing 4.10). 29 Ein ausführliches Beispiel findet sich in [STST12] Figure 7: Example for name lookup in Java Implementierung 37 Namen von statischen Programmelementen können grundsätzlich mit dem Typnamen in dem das Element deklariert ist, qualifiziert werden. Das Verfahren eignet sich nach Steimann, Kollee und von Pilgrim [SKP11] gut für Java mit seinen komplizierten Konzepten Verbergen, Abschatten und Verdunkeln, die ansonsten schwer über Constraintregeln zu behandeln sind. Um für REFACOLA die beschriebene Problematik speziell zu untersuchen, wurde vom Lehrstuhl parallel eine eigene Abschlussarbeit [Ni13] vergeben. Daher wurden Probleme, die sich durch die Einführung von Qualifiern lösen lassen, nicht weiter bei der Regeldefinition berücksichtigt. 4.3.2 Ergänzung Qualifizierte Namen In einem Programmteil kann für ein Element solange der einfache Name verwendet werden, wie er in diesem Kontext eindeutig ist. Geht die Eindeutigkeit bei Umbenennung eines Elements verloren, kommt es zu einem Kompilierungsproblem. Durch Verwendung des qualifizierten Namens kann der Fehler vermieden werden. Das Beispiel in Listing 4.16 zeigt eine Compilationunit, welche ein Interface mit Namen I importiert. Außerdem deklariert sie selbst mehrere Schnittstellen. Wird nun eines dieser Interfaces ebenfalls in I umbenannt, führt dies zu einem Namenskonflikt. Das Programm ist nicht mehr übersetzbar, wenn nicht der qualifizierte Name ergänzt wird. Allerdings reicht dies für dieses Beispiel nicht, hier muss zusätzlich der Import entfernt werden. package a; public interface I{} package a; public interface I{} package b; import a.I; package b; public interface J{} interface JJ{} interface K extends I,JJ {} public interface J{} interface I{} interface K extends a.I,b.I {} Listing 4.16 Einfügen von qualifizierten Namen Eine Möglichkeit, dies über eine Regel zu lösen, besteht nur darin, die Refaktorisierung zu verbieten, da es in REFACOLA keine Anweisung gibt, die bedingt, dass ein einfacher Name durch einen qualifizierten ersetzt wird. Dies bedeutet wiederum eine unnötige Restriktion für den Benutzer. Auch hier wäre eine automatische Anpassung wünschenswert, wie es für Qualifier vorgesehen ist. Das Thema ist ebenso Teil der vom Lehrstuhl vergebenen Abschlussarbeit [Ni13] und wurde für die Regeldefinition daher ebenfalls nicht weiter berücksichtigt. 4.3.3 Erweiterte Prüffunktionen Bei der Umsetzung des Regelsets wurden einige Punkte identifiziert, die eine erweiterte Prüffunktionalität benötigen, um eine Constraintregel zu formulieren. Ungewolltes Überladen von Methoden Handelt es sich bei den formalen Parametern einer Methode um primitive Typen, gibt es Konstellationen, in denen das Umbenennen zum ungewollten Überladenen einer Methoden führt. Dadurch kann es zu einer veränderten Methodenbindung kommen. Listing 4.17 zeigt ein Beispiel. Das Umbenennen der Methode n(short s) nach m(short s) führt zum Überladen der bereits vorhandenen gleichnamigen Implementierung 38 Methode. Da im Methodenaufruf a.m(s) das Argument vom Typ short ist ändert sich hier die Methodenbindung, da der Argumenttyp nun besser zur Methode m(short s) passt als zu m(int i). package a; public class A { package a; public class A { void m(int i) { } //aufruf void n(short s) {} void m(long l) {) void m(int i) { } void m(short s) {} //aufruf void m(long l) {) void n() { short s = 1; a.m(s); } void n() { short s = 1; a.m(s); } } } Listing 4.17: Methodenaufruf bei überladenen Methoden REFACOLA stellt derzeit keine Möglichkeit bereit, das Problem mittels einer Regel zu lösen. Primitive Datentypen können anhand ihres Wertebereichs in eine Reihenfolge gebracht werden, beispielsweise byte < short < int < long. Dabei kann ein short sowohl in einen int, wie auch in einen long konvertiert werden, ohne dass Informationen verloren gehen. Ob dieses Wissen für eine Prüfung ausreicht, wäre zu untersuchen. Verkompliziert wird das Problem noch durch das sogenannte Autoboxing wie es in Listing 4.18 gezeigt wird. Auch hier ändert sich die Methodenbindung nach Umbenennen der Methode n(int i). Auch hier wird nach dem Refaktorisieren die Methode gebunden, die für den Aufruf die am besten passende Parameterliste bietet (vgl. 3.1.3). package a; public class A { void m(Integer i) {} package a; public class A { void m(Integer i) {} void p() { m(1); //aufruf m(Integer) } void p() { m(1); //aufruf m(int) } void n(int i) {} } void m(int i) {} } Listing 4.18: Methodenaufruf mit Autoboxing Für das Vermeiden von ungewolltem Überladen von Methoden, welche zum Beispiel Referenztypen als formale Parameter haben, gibt es im Regelset Accessibility einige Regeln, welche das unerwünschte Überladen von Methoden verhindern. Auf eine detaillierte Darstellung wird hier verzichtet. Gültige Bezeichner Um ein korrektes Umbenennen durchführen zu können, müssen die Bezeichner gültig sein. Der Aufbau eines gültigen Bezeichners wurde bereits in Kapitel 3.1.2 kurz erläutert. Diese Aussagen werden hier etwas Stelle präzisiert. Die Java-Sprachspezifikation definiert einen gültigen Identifier als eine unbegrenzt lange Sequenz von Java letters und Java digits. Des Weiteren unterstützt Implementierung 39 Java den Unicode-Zeichensatz (JLS §3.8). Dies erlaubt es Entwicklern, Identifier selbst in Sprachen wie beispielsweise Chinesisch zu schreiben. REFACOLA selbst stellt aktuell keine Funktion für die Überprüfung von gültigen Identifiern innerhalb der Regeldefinition zur Verfügung. Anstatt eine explizite Prüfung für alle Regeldefinitionen eigens zu formulieren, scheint eine zentrale Prüfung für gültige Identifier auch sinnvoller. Um nun prüfen zu können, ob es sich auch um zulässige Java letters handelt, gibt es in der Klasse java.lang.Character spezielle Prüfmethoden30 (JLS §3.8). Allerdings reichen diese für eine Überprüfung nicht immer aus. Da REFACOLA in Eclipse integriert ist, muss hier zusätzlich das eingestellte Encoding der IDE beachtet werden. REFACOLA führt diese Prüfung aktuell direkt in der Benutzerschnittstelle durch (vgl. Abbildung 4.2). Es werden dazu Validierungsmethoden aus den JDTBibliotheken der Klasse org.eclipse.jdt.core.JavaConventions31 verwendet, mit dem Vorteil, dass diese das Encoding der IDE bereits berücksichtigen. Da die anderen REFACOLA-Refaktorisierungswerkzeuge im Benutzerdialog keine Eingabemöglichkeit für Identifier zur Verfügung stellen, wurde die Lösung zunächst als ausreichend erachtet. Abbildung 4.2: REFACOLA-Eingabedialog „Rename“ - Prüfung gültiger Identifier Casesensitve Compilationunitnamen Java beachtet für die Unterscheidung von Namen Groß- und Kleinschreibung bei der Schreibweise (vgl. Kapitel 3.1.2). Ein Sonderfall, auf den bereits im Rahmen der Regelvorstellung hingewiesen wurde (vgl. Kapitel 4.2.2), betrifft Compilationunits. Wenn sie innerhalb des gleichen Pakets definiert werden, muss deren Name eindeutig sein. Allerdings wird darf dabei nicht casesensitiv unterschieden werden, obwohl Java eine sogenannte casesensitive Sprache ist 32. 30 Die Prüfmethoden heißen Character.isJavaIdentifierStart(int) , Character.isJavaIdentifierPart(int); Javadoc zu Klasse unter http://help.eclipse.org/juno/ topic/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/core/JavaConventions.html 32 In einem Windows XP–System könnten zwei Javadateien a.java und A.java auch nicht in einem Ordner gespeichert werden. 31 Implementierung 40 Das Problem kann über die Regeldefinition aktuell nicht entsprechend behandelt werden. Eine Erweiterung der Eingabeprüfung in der Benutzerschnittstelle wäre möglich, erscheint auf den zweiten Blick jedoch wenig praktikabel, da nicht nur der oben dargestellte Fall zu beachten ist, sondern auch welche, bei denen ein Typ „indirekt“ umbenannt wird, beispielsweise durch ein „Move Type“. Idealerweise sollte die Prüfung in einer Regel formuliert werden, damit sie von allen Werkzeugen gleichermaßen eingebunden kann. REFACOLA stellt derzeit keine Funktion für einen nicht-casesensitiven Vergleich von Bezeichnern zur Verfügung, wie es beispielsweise Java mit der Methode equalsIgnoreCase33 tut. Mit einer solchen Funktion könnte eine Regel wie folgt formuliert werden. UniqueCasesensitiveTyperootIdentifier for all tr1: Java.TypeRoot tr2: Java.TypeRoot do if tr1 != tr2 then (tr1.hostPackage = tr2.hostPackage) ->(not equalsIgnoreCase(tr1.identifier,tr2.identifier)) end Listing 4.19: Funktion für casesensitivem Stringvergleich 4.3.4 Offene Themen Aufgrund der zeitlichen Begrenzung und fehlender Komponenten konnten einige Themen nicht oder nur teilweise berücksichtigt werden. Paketen und Importen wurden bereits in Kapitel 4,2.9 angesprochen. Im engen Zusammenhang mit Paketen und Importen steht der Umgang mit Qualifizierten Namen. Einzelne Identifer eines Qualifizierten Namens Im Rahmen dieser Arbeit wurden einfache Erweiterungen für das Umbenennen von vollqualifizierten Klasseninstanziierungen und Importen eingefügt, die es bedingt ermöglichen, qualifizierte Namen zu schreiben34. Hierbei kann allerdings jeweils nur das letzte Fragment eines qualifizierten Namens geändert werden. Diese Erweiterung wurde vor allem aus Gründen der Testbarkeit beim Umbenennen von Typen vorgenommen, um auch paketübergreifend testen zu können. Bei einem qualifizierten Namen stellt jedes einzelne Fragment einen eigenen Identifier dar (vgl. Kapitel 3.1.2). Daraus resultiert die Anforderung, sie auch einzeln umbenennen zu können. Dies ist zum Beispiel für Pakete nötig, weil jedes Fragment einer Paketdeklaration in einer Compilationunit einem einzelnen Verzeichnis entspricht. Wird ein solches umbenannt, müssen alle Paketdeklarationen der Compila- 33 34 Methode der Klasse java.lang.String Die Änderungen betreffen in de.feu.ps.refacola.lang.java.jdt.manipulation.NodeChangeRequest die Methoden rename(ClassInstanceCreation creation) und rename(QualifiedName qualifiedName). Die Stellen wurden entsprechend dokumentiert. Implementierung 41 tionunits, welche in dem Paket liegen, angepasst werden. Ebenso müssen die Fragmente in Importanweisung und qualifizierten Namen geändert werden können. Annotationen (JLS §9.6, §9.7) Ein Annotationstyp ist eine spezielle Interface-Form (JLS §9.6). Mithilfe einer Annotation kann der Quelltext um Metainformationen erweitert werden.35 Es gibt bereits vordefinierte Annotationstypen in Java, wie beispielsweise @Override. Ist eine Methode damit annotiert, kommt es zu einem Kompilierfehler, wenn diese nicht tatsächlich eine Methode ihres Supertyps überschreibt (JLS §9.6.1). Es ist zudem möglich, Annotationen selbst zu definieren. Auch für Annotationen gelten eine Reihe von Bedingungen, die bei der Refaktorisierung von Identifiern zu beachten sind. Beispielsweise muss der Name einer Annotationsdeklaration mit dem Namen des Annotationstyps übereinstimmen. Außerdem dürfen Annotationen nicht den gleichen Namen besitzen wie der sie umgebende Typ. Weil REFACOLA Annotationen derzeit an vielen Stellen nur sporadisch bis gar nicht unterstützt, konnte die Thematik nicht mehr berücksichtigt werden. Generische Typen Obwohl REFACOLA generische Typen bereits in weiten Teilen unterstützt, gibt es für Typparamter noch keine Eigenschaft identifier in der REFACOLA-Sprachdefinition. Daher können diese noch nicht umbenannt werden. Eine Erweiterung war zeitlich nicht mehr möglich. Außerdem gibt es für die Vielzahl an Möglichkeiten, die generische Typen bieten, ebenso viele Bedingungen. Diese müssen noch weiter analysiert werden. Typparameter in extends-Klauseln werden beispielsweise bereits richtig refaktorisiert. class A <T extends B> {} Anonyme Klassen (JLS 15.9.5.1) Anonyme Klassen erweitern implizit die Klasse oder das Interface, welches im Instanziierungsausdruck angegeben ist. Sie selbst haben keinen eigenen Namen. Wird nun der Supertyp umbenannt, muss auch dieser Instanziierungsausdruck angepasst werden. In der Faktengenerierung werden bisher nur Fakten für die Anonyme Klasse und deren ClassInstanceCreation berücksichtigt. Benötigt wird allerdings ein Fakt, welches die Referenz der anonymen Klasse auf die Superklasse repräsentiert. Auch diese Erweiterung war zeitlich nicht mehr möglich, daher fehlt eine entsprechende Constraintregel. Dokumentation im Quelltext Mens und Tourwé weisen in [MT04, S. 5] darauf hin, dass zu einer korrekten Refaktorisierung nicht nur die Sicherstellung des Programmverhaltens gehört, sondern, auch die „Konsistenz zu anderen Softwareartefakten, wie beispielsweise Pflichtenheft oder Entwurfsdokumente“. Dies gilt insbesondere für Dokumentation, die di- 35 Vgl. http://docs.oracle.com/javase/tutorial/java/annotations/ Implementierung 42 rekt im Quelltext hinterlegt ist. In Java ist es möglich, aus speziellen Kommentaren, der Javadoc, eine Dokumentation generieren zu lassen, die häufig mit öffentlichen Bibliotheken ausgeliefert wird. Wenn in Kommentaren nun beispielsweise Verweise zu Programmelementen enthalten sind, ist es umso ärgerlicher, wenn nach einer Refaktorisierung zwar alle Codestellen richtig angepasst wurden, die Dokumentation jedoch von Hand geändert werden muss. Dies erfordert letztendlich wieder ein „Suchen und Ersetzen“. Bisher ist die Refaktorisierung von Kommentaren in REFACOLA noch gar nicht vorgesehen. Um das Regelwerk entsprechend erweitern zu können, müssen zuvor sowohl REFACOLA-Sprachdefinition, Faktengenerierung wie auch Rückschreibekomponente erweitert werden. Listing 4.20 zeigt die verschiedenen Arten von Kommentaren in einem Java-Quelltext. package examples; public class JavadocExample { class A {} /*Blockkommentar*/ /** Javadoc * @param a * @see examples.A */ void m(A a) {} /** Javadoc * @return A */ A n() { return new A(); //zeilenkommentar } } Listing 4.20: Kommentare in Java 4.4 Identifier in Fremdbibliotheken Neben den vielen Programmelementen, die explizit umbenannt werden sollen, gibt es in Projekten auch Stellen, an denen dieses unerwünscht ist. Dazu zählen Fremdbibliotheken, beispielsweise die Java-Bibliotheken. REFACOLA verhindert dies, indem die Programmelemente bereits in der Faktengenerierung als nicht änderbar (readonly) gekennzeichnet werden. Eigene Regeln sind daher für diese Fälle nicht nötig. 4.5 Tests Nach der Refaktorisierung eines Programmes erfolgt zunächst die syntaktische und semantische Überprüfung des Quelltextes durch den Compiler. Treten hier Fehler auf, werden diese sofort erkannt und können behoben werden. Steimann nennt sie daher auch „gutartige Fehler“ (vgl. [St10]). Weiterhin muss aber auch überprüft werden, ob sich das beobachtbare Verhalten des refaktorisierten Programmes geändert hat. Ein Nachweis der Korrektheit durch eine formale Verifikation ist zwar theoretisch denkbar, jedoch meist aufgrund des hohen Aufwandes nicht durchführbar. Stattdessen wird Software in der Regel mithilfe von Tests auf korrektes Verhalten geprüft (vgl. [St10]). Diese helfen, die vorhandene Funktionalität vor und nach der Änderung zu prüfen, um Veraltensänderungen aufzudecken [We06]. Allerdings können sie keine Fehlerfreiheit garantieren, da jeweils nur geprüft wird, ob für eine vorgegebene Eingabe Implementierung 43 auch das gewünschte Ergebnis geliefert wird. Außerdem besteht bei der Entwicklung von Testfällen häufig das Dilemma, dass nicht alle Konstellationen in einer vorgegebenen Zeit erschöpfend geprüft werden können. Allein für das Umbenennen eines Identifiers alle Möglichkeiten zu prüfen ist mit vertretbarem Aufwand kaum möglich (vgl. [St10]). Eine wichtige Anforderung an Testverfahren ist die effiziente Ausführung von Tests, schnelles Feedback und reproduzierbare Ergebnisse. Manuelles Testen kann dieses nur selten leisten, deshalb sind automatisierte Tests von Vorteil [We06, S.2]. 4.5.1 Unit-Tests In der Softwareentwicklung haben sich sogenannte Unit-Tests etabliert, die gezielt begrenzte Funktionalitäten eines Programms in einzelnen Testmethoden überprüfen. Einmal geschrieben, lassen sie sich jederzeit automatisch ausführen. Es gibt diverse Frameworks, die die Erstellung von Entwicklertests unterstützen. Der „Defacto-Standard“ für Java ist das JUnit-Framework36 (vgl. [We06, S. 21]). Vorteilhaft daran ist die einfache Handhabung und eine gute Integration in die Entwicklungsumgebung Eclipse (vgl. [We06, S. 21ff.]). Dieses Framework wird bereits in der REFACOLA-Entwicklung eingesetzt. Mit Hilfe eines speziellen Hilfsplugins in REFACOLA ist es möglich die Refaktorisierungswerkzeuge zu testen. Dazu wird für den Test eine weitere Eclipse-Instanz gestartet, welche dann bereits die REFACOLA-Refaktorisierungswerkzeuge integriert. In dieser Umgebung lassen sich außerdem JUnit-Tests starten (vgl. [Wa12, S. 47]). Dies wird für Tests des Regelwerks genutzt. Jeder einzelne Testfall37 simuliert ein kleines Programm, auf dem eine Refaktorisierung durchgeführt wird. Dies hat zudem den positiven Nebeneffekt, dass hierbei der komplette Refaktorisierungszyklus mit überprüft wird. Ziel ist es, möglichst Fehler aufzudecken, um daraus neue Constraintregeln abzuleiten, oder bestehende abzuändern. Neben den Funktionstests, die das erstellte Regelset testen, gibt es eine Reihe von Tests, welche offene Punkte dokumentieren. Außerdem gibt es für Erweiterungen an Faktengenerierung und Rückschreibekomponente zur besseren Nachvollziehbarkeit „Referenztestfälle“. Eine Übersicht findet sich in Anhang C. 4.5.2 Mutationstests Testergebnisse können nur so gut sein wie der Test selbst. Dabei ist es denkbar, dass nicht der zu testende Gegenstand fehlerhaft ist, sondern der Test selbst [We06, S. 154ff.], weshalb es sinnvoll ist, auch die Tests einer Prüfung zu unterziehen, beispielsweise durch Mutationstesten. Dabei werden gezielt Fehler in ein Programm eingebracht und anschließend kontrolliert, ob die Fehler beim Testlauf entdeckt werden. Ist dies nicht der Fall, lässt dies Rückschlüsse auf fehlerhafte Tests oder eine unzureichende Testabdeckung zu. Für die Überprüfung der JUnit-Tests wurde daher schrittweise jede einzelne Constraintregel auskommentiert und jeweils anschließend alle Tests ausgeführt. Werden keine Fehler entdeckt, kann dies zwei Ursachen haben. Entweder liegen für die Regel keine aussagekräftigen bzw. fehlerhaften Testfälle vor, oder eine Problem- 36 37 http://junit.sourceforge.net/ Die Testfälle befinden sich im Projekt de.feu.ps.refacola.lang.java.jdt.ui.tests. Implementierung 44 stellung wird bereits mittels einer anderen Regel behandelt. In letzterem Fall kann eine der Regeln entfernt werden. Über die Ergebnisse der zuletzt durchgeführten Mutationstests gibt Tabelle 4.1 Auskunft. In Spalte „Test Rename“ wurden JUnit-Tests bewertet, die im Rahmen dieser Arbeit entstanden. Für jede Regel aus Names.ruleset.refacola wurden Fehler erkannt, mit Ausnahme von Names.UniqueMethodIdentifier. Hier wurde festgestellt, dass der Constraint für kovariante Rückgabetypen nicht benötigt wird. Für die Änderung eines kovarianten Rückgabetypen gibt es bereits die Regel Types.OverridingCovariantReturnType. Ein Umbenennen des Typen ist unproblematisch, wenn die Subtypbeziehung bestehen bleibt. Der Constraint wurde daraufhin aus der Regel entfernt. Die zweite Spalte gibt die Auswertung von JUnit-Tests wider, die für andere Refaktorisierungswerkzeuge in REFACOLA erstellt wurden. Beim Vergleich fällt auf, dass hier kaum Fehler aufgetreten sind. Dies lässt auf eine mangelnde Testabdeckung schließen. Hier wurde begonnen weitere Testfälle zu erfassen. Status Anzahl Fehler Tests Andere38 Anzahl Fehler F: 22 F: 26 F:0 F:0 OK F: 19 F:0 UniqueMemberTypeNames2 F: 11 F: 0 OK UniqueEnclosingMemberTypeNamesInAssignment1 F: 4 F: 0 OK UniqueEnclosingMemberTypeNamesInAssignment2 F: 1 F: 0 OK TopLevelTypeNames F: 78 F: 3 OK ConstructorNames F: 73 F: 0 OK UniqueTopLevelTypeIdentifier F: 1 E:7 F: 0 OK TypeReferenceIdentifier F: 105 F: 0 OK ReferenceIdentifier F: 256 F: 0 OK UniqueFieldIdentifier F: 17 F: 0 OK UniqueMethodIdentifier Teil 1 -> ok F: 15 F: 0 Teil2 Prüfen Regelname UniqueMemberTypeNames1 Teil 1 ->ok Test Rename (t.identifier != t.enclosingNamedTypes.identifier) Teil 2-> ok ( t.identifier != t.tlowner.identifier) ((method1.owner = method2.owner) and(method1.parameters.declaredParameterType =method2.parameters.declaredParameterType) ->(method1.identifier != method2.identifier)) Teil 2 -> kovarianter Rückgabetyp prüfen 38 „Tests Andere“ bezieht sich auf die JUnit-Testsuiten: AllChangeAccessibilityTests, AllPullUpTests, AllChangeDeclaredTypeTests, AllMoveCompilationUnitTests Implementierung 45 OverridingMethodNames F: 51 F: 0 OK PackageDeclarationNames F: 15 F: 5 OK ImportDeclarationNames1 F: 35 F: 0 OK AccidentalHidingFinalMethod F: 19 F: 0 OK UniqueLocalVariableNames1 F: 17 F: 0 OK UniqueLocalVariableNames2 F: 16 F: 0 OK UniqueLocalVariableNames3 F: 20 F: 0 OK EagerInterfaceMethodNames F: 27 F: 0 OK (Stand 01.06.2013) Tabelle 4.1: Mutationstest für das JUnit-Test RenameMember 4.5.3 RTT-Tests JUnit-Tests können nur einfache Beispielprogramme simulieren. Darüber hinaus ist es aber nötig, die Auswirkungen einer Refaktorisierung anhand einer umfangreicheren Quelltextbasis prüfen zu können. Das Test-Framework Refactoring Tool Tester (RTT) bietet die Möglichkeit, ein zu testendes Refaktorisierungswerkzeug auf einer beliebigen Codebasis anzuwenden. Es wurde ebenfalls am Lehrgebiet Programmiersysteme der FernUniversität in Hagen entwickelt [Ho10].39 Vorteilhaft daran ist, dass für den Test von Refaktorisierungswerkzeugen jegliche Codebasis verwendet werden kann, solange sie vor der Umstrukturierung ein ausführbares Verhalten zeigt [St10]. Hier bietet sich die Verwendung von OpensourceProjekten an, die in einer Vielzahl frei zur Verfügung stehen. Ein weiterer Vorteil von Opensource-Projekten ist, dass diese häufig über eine sehr gute Testabdeckung verfügen. Deren Tests können nach der Refaktorisierung genutzt werden, um die Refaktorisierungsergebnisse zu überprüfen. Gerade Fehler die zu verändertem Verhalten führen können, sind so leichter zu entdecken. Für diese Arbeit wurde der RTT um neue Funktionalitäten für das Umbenennen von Typen, Methoden, Feldern und lokalen Variablen ergänzt (vgl. Abbildung. 4.4). 39 Siehe auch Webpräsenz: http://www.fernuni-hagen.de/ps/prjs/rtt/ Für diese Arbeit wurde eine Neuentwicklung des RTT verwendet. Implementierung 46 Abbildung 4.3: Menü RTT-Tests für Rename in Eclipse Package Explorer 4.5.4 Testgetriebene Entwicklung Als Vorgehensweise bei der Entwicklung wurde ein testgetriebener Ansatz gewählt. Die Idee der sogenannten Test-First-Programmierung, auch bekannt als Test Driven Development (TDD), ist, dass die Tests vor der eigentlichen Entwicklung spezifiziert und bereitgestellt werden. Das geforderte Ergebnis muss folglich im Vorfeld bekannt sein, um es in einem Testfall zu „dokumentieren“. Ein Vorteil dabei ist, dass der Testfall nicht ungewollt oder unterbewusst an das Programm angepasst wird, sondern die Funktionalität sich an dem geforderten Ergebnis orientiert (vgl. [We06, 2ff.]). Für die Erstellung des Regelsets wurden daher zunächst für bestimmte Schwerpunkte diverse Testfälle identifiziert. Dadurch wurde schnell offenbar, ob es bereits Constraintregeln gibt, oder ob eine neue Regel nötig ist. Das weitere iterative Vorgehen unterstützte die Erweiterung und Verfeinerung der Testfälle und Regeln. Entsprechend des TDD-Ansatzes nehmen die Tests einen hohen Stellenwert für diese Arbeit ein. Diskussion 47 5 Diskussion 5.1 Ergebnisse Im Zuge der Definition des Regelsets wurden einige Erweiterungen in der REFACOLASprachdefinition, der Faktengenerierung und der Rückschreibekomponente benötigt. Diese wurden größtenteils bei der Vorstellung des Regelsets angesprochen (vgl. Kapitel 4.2). Exemplarisch soll hier der Prototyp für die Ermittlung umschließender lokaler Variablen zur Diskussion gestellt werden. 5.1.1 Prototyp zur Ermittlung umschließender lokaler Variablen In Kapitel 4.2.8 wurden Constraintregeln40 vorgestellt, welche die Eindeutigkeit von Namen für lokale Variablen und formale Parameter in Methoden und Konstruktoren sichern sollen. Dieses Kaptitel stellt eine prototypische Implementierung zur Diskussion. Formale Parameter werden nicht berücksichtigt, da über die Regel Names.UniqueLocalVariableNames2 bereits die Namenseindeutigkeit zu lokalen Variablen sichergestellt wird. Für die weiteren Ausführungen in diesem Kapitel gelten Aussagen, die für Methoden gemacht werden ebenso wie für Konstruktoren und Initialisierungsblöcke. Auf Unterschiede wird explizit hingewiesen. Lokale Variablen können in unterschiedlichen Anweisungsblöcken deklariert sein (vgl. Kapitel 3.1.3). Dabei gilt, dass ein Identifier innerhalb der ihn umschließenden Blöcke eindeutig sein muss. Liegt ein Anweisungsblock außerhalb der Hierarchie, kann es auch innerhalb einer Methode mehrere gleichnamige Identifier geben, wie es in Abbildung 5.1 dargestellt ist. Das Beispiel zeigt eine Methode, in der zwei gleichnamige Variablen i deklariert werden. Dies ist möglich, weil die beiden ForSchleifen zwei voneinander getrennte Anweisungsblöcke sind. class A{ private int y = 0; void m(int x){ for(int i=0; i<5; i++){ int x; //fehler konflikt mit Parametername } for(int i=0; i<5; i++){ int y; //abschatten instanzvariable y } } } Abbildung 5.1: gleichnamige lokale Variablen in verschiedenen Anweisungsblöcken 40 Es handelt sich hier um die Regeln Names.UniqueLocalVariableNames1, Names.UniqueLocalVariableNames2 und Names.UniqueLocalVariableNames3 Diskussion 48 REFACOLA bietet die Möglichkeit, einem Element eine Liste von Entitäten mitzugeben, sogenannte Sequences. Befüllt werden diese während der Faktengenerierung. In der REFACOLA-Sprachdefinition wurde LocalVariable um die Eigenschaft enclosingLocalVariables erweitert (vgl. Listing 5.1). Names.UniqueLocalVariableNames3 verwendet diese für den Vergleich der Variablennamen. language Java kinds abstract Entity <: ENTITY abstract LocalVariable <: TypedEntity, LocalVariableOrParameter {enclosingLocalVariables} properties identifier: Identifier enclosingLocalVariables : Sequence(LocalVariables) domains AccessModifierDomain = {private, package, protected, public} LocalVariables = [ LocalVariable ] queries declaresLocalVariableOrParameter(method: MethodOrConstructor, localVariableOrParameter: LocalVariableOrParameter) hasEnclosingVariables(localVariable : LocalVariable) Listing 5.1: Ausschnitt REFACOLA-Sprachspezifikation für Java - lokale Variablen Es gibt eine Vielzahl unterschiedlicher Anweisungsblöcke. Dazu zählen die unterschiedlichen Schleifen ebenso wie break- oder return-Anweisungen. Für die korrekte Ermittlung der lokalen Variablen müssen diese unterschieden werden. Die prototypische Implementierung löst dieses in einer eigenen Klasse41, welche in die Faktengenerierung eingebunden ist. Hier werden für jede lokale Variable alle umschließenden Variablen ermittelt und jeweils in der Sequenz enclosingLocalVariables der Entität gespeichert. Die vorgeschlagene Lösung stellt einen einfachen Weg zur Behandlung von Namenseindeutigkeit für lokale Variablen vor. Ein Vorteil der Lösung ist die einfache Integration in die bisherige Faktengenerierung und die zentrale Behandlung aller Fälle. Da es sich um einen Prototypen handelt, werden bislang nur wenige Anweisungsblock-Arten unterstützt und daher noch nicht alle Variablen gefunden. Ein Nachteil daran ist, dass in der Faktengenerierung die Anweisungsblöcke mehrmals durchlaufen werden, da für jede lokale Variable alle umgebenden Variablen ermittelt und gespeichert werden. Alternativ könnten die Variablen bei der Ermittlung der Fakten für die Methoden einmalig gespeichert werden, die dafür nötigen Mechanismen zur Speicherung und Abfrage wären dann aber komplizierter. Daher ist mehr Speicherkapazität und Laufzeit notwendig. Wenn die Ermittlung aller Anwei- 41 Es handelt sich um die Klasse de.feu.ps.refacola.lang.java.jdt.util.LocalVariableParseTool Diskussion 49 sungsblöcke vollständig implementiert ist und folglich alle lokalen Variablen verfügbar sind, sollte dieser Aspekt kritisch betrachtet werden. Die Lösung berücksichtigt noch nicht, dass innerhalb von Methoden lokale Klassen deklariert werden können. Dies verkompliziert die Ermittlung nochmals, da die Eindeutigkeit der Namen lokaler Variabeln wiederum für alle Variablen der der umschließenden Anweisungsblöcke gelten muss (JLS §8.1.3). Allerdings besitzen auch lokale Klassen Instanzvariablen, welche abgeschattet werden können. Das Beispiel in Abbildung 5.2. zeigt dies für die Instanzvariable z der Klasse LocalA. In der Methode n() in der Klasse LocalA werden sowohl Variablen der eigenen Klasse wie auch der äußeren Klasse A verwendet. class A{ private int x = 0; void m(int x){ final int y = x; class LocalA{ private int z; void n(int x){ int localInnerVar = y+x+z; } } } } Abbildung 5.2: lokale Klasse und lokale Variablen 5.1.2 Einbindung Locked Bindings Viele Refaktorisierungsprobleme können aufgrund fehlender Qualifier nicht vollständig gelöst werden. Erstellte Constraintregeln sind daher teilweise zu restriktiv (vgl. Kapitel 4.3.1). Die Einbindung der Locked Bindings [Ni13] verspricht eine wichtige Ergänzung zum Regelset zu werden. Gegen Ende dieser Arbeit wurden bereits erste Tests erfolgreich durchgeführt. Da hiermit auch die Limitierungen in den Regeln obsolet werden, sollte das Regelset noch einmal kritisch daraufhin geprüft werden, ob und welche Regeln vereinfacht oder entfernt werden können. Die Regel UniqueEnclosingMemberTypeNamesInAssignment142 zum Beispiel sollte komplett entfallen können. Die JUnit-Tests aus Kapitel 4.5.1 verfügen über eine Reihe an Tests, die speziell das Einfügen von Qualifiern prüfen. Sie wurden entsprechend gekennzeichnet und können die Integration der Locked Bindings unterstützen. Dies gilt ebenso für die Ergänzung qualifizierter Namen. 42 Dies gilt ebenso für die Regel UniqueEnclosingMemberTypeNamesInAssignment2. Diskussion 5.1.3 50 Auswertung der Tests In diesem Kapitel werden exemplarisch Beispiele für Testergebnisse vorgestellt. Bei den vorgestellten Statistiken handelt es sich um tagesaktuelle Ergebnisse, die sich durch die laufende Weiterentwicklung von REFACOLA ändern können. JUnit - Testsuite Die Testsuite AllRenameMember enthält derzeit noch viele deaktivierte Tests (Ignore). Dies liegt zum einen daran, dass einige Tests speziell für die Erweiterung Locked Bindings vorbereitet wurden und daher erst mit deren Einbindung aktiviert werden können. Die anderen sind Problemen zuzuordnen, die über das Regelwerk noch nicht gelöst werden konnten, wie zum Beispiel das Umbenennen generischer Typen. Ein weiterer Teil dient zur Dokumentation der Referenztestfälle oder offener Fehler. Letztere sind bei der Zählung in Tabelle 5.2 nicht enthalten. Testsuite Gesamt Failure/Errors Ignore Locked Bindings Ignore Sonstige 896 0 48 59 AllRenameMember (Stand 07.06.2013 , ohne Tests für Dokumentation, vgl. Anhang C) Tabelle 5.1: JUnit-Testsuite AllRenameMember RTT-Tests Bei der Auswahl der RTT-Testprojekte zeigen sich zum Teil deutliche Einschränkungen, die auf noch fehlende Funktionalitäten in REFACOLA zurückzuführen sind. So können viele Opensource-Projekte noch nicht verwendet werden, da beispielsweise Vararg-Argumente oder Generics noch nicht vollständig unterstützt werden. Ein Vorteil der RTT-Tests ist deren Flexibilität. Mit wenigen Änderungen werden diverse Testkonstellationen geschaffen. Jedes Testwerkzeug kann sehr individuell gestaltet werden. Beispielsweise gibt es für RenameOnlyType eine Konfigurationsmöglichkeit, nur diejenigen Fälle zu testen, bei denen eine Refaktorisierung nicht möglich sein darf (Unsolved). Tabelle 5.3 zeigt exemplarisch Ergebnisse für RenameOnlyType. Bei 1270 Einzelrefaktorisierungen für das Projekt jaxen1.1.1 wurden insgeamt 27 Kompilierungsprobleme entdeckt. Bei näherer Betrachtet konnte festgestellt werden, dass es sich immer um das selbe Problem handelte. Außerdem schwanken die Ergebnisse für die Einzelnen Projekte zum Teil deutlich, wie der Vergleich zwischen junit3.8 und regexp zeigt. Um eine gute Testbasis zu bekommen ist es daher sinnvoll, verschiedene Projekte mit unterschiedlichem Umfang oder Einsatzzweck zu verwenden. Des Weiteren ist es sinnvoll, gezielt zwei gleiche Projekte in verschiedenen Versionen zu vergleichen, um beispielsweise das Verhalten für verschiedene Java-Versionen testen zu können. 43 43 Zum Beispiel JUnit3 und JUnit4 für den Wechsel der Javaversionen von 1.4 auf 1.5. Dies konnte jedoch wegen der fehlenden Unterstützung von Annotationen, Varargs und Generics noch nicht durchgeführt werden. Diskussion 51 Rename Only Type Anzahl Anzahl OK Exception Unsolved Compile Dauer ∅ jaxen 1.1.1 1270 98% 0 0 2,%* 1860 junit3.8 232 93% 0 0 6% 943 regexp 54 100% 0 0 0 439 (Stand 16.06.2013) * 27 -mal gleiche Fehlermeldung Tabelle 5.2: RTT-Test Rename Only Type Tests für die Refaktorisierung von Feldern liefern bereits sehr gute Ergebnisse, wie Tabelle 5.4 zeigt. Rename Only Field Anzahl Anzahl OK Exception Unsolved Compile Dauer ∅ jaxen 1.1.1 315 100% 0 0 0 880 junit3.8 119 100% 0 0 0 842 regexp 134 100% 0 0 0 802 (Stand 03.07.2013) Tabelle 5.3: RTT-Test Rename Only Field Sowohl in den Tests für Methoden, wie auch für loklae Variablen (vgl. Tablelle 5.5 und 5.6) fällt auf, dass hier eine größere Zahl von Refaktorisierungen abgeleht wurden (Unsolved). Dies liegt daran, dass es für diese Tests noch keine Konfigurationsmöglichkeit gibt, diejenigen Fälle isoliert zu testen, bei denen eine Refaktorisierung nicht möglich sein darf. Daher fließen sie hier in die Gesamtstatistik ein. Rename Only Method Anzahl Anzahl OK Exception Unsolved Compile Dauer ∅ jaxen 1.1.1 1320 65% 0 25% 10% 1315 junit3.8 462 92% 0 6% 2% 403 regexp 132 94% 0 6% 0 179 (Stand 13.06.2013) Tabelle 5.4: RTT-Test Rename Only Method Diskussion 52 Rename Only LocalVariable Anzahl Anzahl OK Exception Unsolved Compile Dauer ∅ jaxen 1.1.1 649 90% 5% 5% 0 161 junit 3.8 336 90% 8% 2% 0 105 regexp 171 81% 12% 7% 0 83 (Stand 21.06.2013) Tabelle 5.5: RTT-Test Rename Only LocalVariable Performance und Speicherverhalten Neben der reinen Funktionalität sind Performance und Speicherverhalten wichtige Aspekte für die Entwicklung eines Refaktorisierungswerkzeugs. Für diese Arbeit stand die Entwicklung der Constraintregeln vor allem unter funktionalen Gesichtspunkten im Vordergrund. Daher wurden keine expliziten Performancemessungen vorgenommen, die idealerweise auf einem vorgegebenen Standardsystem durchgeführt werden sollten. Tabelle 5.7 stellt die Daten der Testumgebung dar. Eclipse-Version Eclipse Juno 4.2 Betriebssystem Windows XP 32 Bit RAM 4 GB Tabelle 5.6: Parameter Testumgebung Diskussion 5.2 5.2.1 53 Verwandte Arbeiten Reflektive Aufrufe Java bietet mit dem Reflection-API (Applicaton Programming Interface) die Möglichkeit, Klassen und deren Bestandteile zur Laufzeit programmatisch zu analysieren. Damit ist es beispielsweise möglich, Klassen dynamisch zu laden. In reflektiven Ausdrücken wird häufig der Name einer benötigten Ressource als Text übergeben. Allerdings garantieren nicht alle Refaktorisierungswerkzeuge, dass bei einer Umstrukturierung das ursprüngliche Programmverhalten für diese Aufrufe bewahrt wird [TB12]. package a; package a; public class A { public int i = 0; } public class NewA { public int i = 0; } package a; package a; public class B { void m() throws Exception { Class c = Class.forName("A"); String a = "A"; } } public class B { void m() throws Exception { Class c = Class.forName("A"); String a = "A"; } } Listing 5.2: Klasse mit reflektivem Aufruf In Listing 5.3 besitzt die Klasse B einen solchen Aufruf (Class.forName("A")). Nach der Transformation ist die Klasse A umbenannt, jedoch der reflektive Ausdruck nicht mit angepasst. Dieser Fehler wird vom Compiler nicht entdeckt, sondern erst zur Laufzeit bemerkt. Das Problem dabei ist, dass sich diese Stellen nicht ohne weiteres von anderen Zeichenketten unterscheiden und damit eine herkömmliche statische Quelltextanalyse in der Regel nicht anwendbar ist. In [TB12] beschreiben Thies und Bodden einen Ansatz, wie auf Basis des constraintbasierten Refaktorisierens mit reflekiven Programmaufrufen umgegangen werden kann.44 Um Information über betroffene Programmstellen zu bekommen, registrieren sie diese Aufrufe während eines Testlaufs dynamisch. Auf dieser Basis können dann die Constraints generiert werden. Voraussetzung für die erfolgreiche Ermittlung ist hier, dass für die Programmstellen geeignete Tests vorliegen. 5.2.2 Sprachübergreifendes Refaktorisieren Eine Herausforderung bei der Refaktorisierung von Identifiern besteht darin, dass Informationen über Programmelemente nicht ausschließlich im Quelltext hinterlegt sein müssen. Viele Applikationen nutzen spezielle Dateien, wie beispielsweise XML-, Property- oder JSP-Dateien, für Konfigurationen oder Oberflächenbeschreibungen. Wenn sich in diesen nun Referenzen auf Programmelemente des 44 Die Umsetzung basiert ebenfalls auf dem REFACOLA-Framework Diskussion 54 ursprünglichen Javaprogramms befinden, wäre es für eine vollständige Refaktorisierung nötig, diese zu berücksichtigen und Änderungen auch an den betroffenen Dateien vorzunehmen. Das ist problematisch, weil die Informationen nicht in der Programmiersprache vorliegen und somit auch nicht den syntaktischen und semantischen Vorgaben der Java-Sprachspezifikation entsprechen [Kl10, Wa12]. Weiter verkompliziert wird dies, wenn die Refaktorisierung über mehrere Programmiersprachen hinweg stattfinden soll. Viele Projekte nutzen zunehmend mehrere Sprachen parallel. Unterstützt wird dies dadurch, dass die Java Virtual Machine (JVM) mittlerweile diverse andere Sprachen, wie Scala, Groovy oder JRuby, unterstützt. Hier stehen Refaktorisierungswerkzeuge noch am Anfang. Es gibt aber bereits erste Versionen, die zumindest für Sprachen, die innerhalb einer Laufzeitumgebung genutzt werden können, Unterstützung bieten [Kl10]. Auch im Rahmen der REFACOLA-Entwicklung wurden bereits Untersuchungen für einen sprachübergreifenden Ansatz durchgeführt. An dieser Stelle soll auf zwei Abschlussarbeiten verwiesen werden. Zum einen betrachtet Wagner die sprachübergreifende Refaktorisierung in Verbindung mit XML-Dateien für Topleveltypen [Wa12]. Osten stellt in [Os12] einen Lösungsansatz für das Refaktorisieren zwischen Java und JRuby vor. Hier ist zu beachten, dass die speziellen Bedingungen der Sprachen nicht isoliert voneinander betrachtet werden dürfen, sondern durchaus Auswirkungen aufeinander haben können (vgl. [Os12, S. 32]). 5.3 5.3.1 Ausblick Unterstützung durch Benutzerschnittstelle Bei der Entscheidung, ob ein Benutzer eine Refaktorisierung manuell oder mithilfe eines Werkzeugs ausführt, ist laut [MPB09] eine komfortable Benutzerschnittstelle ein wesentlicher Aspekt. Über die Benutzerschnittstelle bieten sich zusätzliche Möglichkeiten, ein Refaktorisierungswerkzeug für den Benutzer komfortabel zu gestalten und zusätzliche Hilfestellungen zu geben, wie zum Beispiel eine Vorschau. Eine Benutzerschnittstelle für das Umbenennen von Elementen für Eclipse Package-Explorer bzw. Outline ist bereits vorhanden. Es fehlt jedoch noch die Möglichkeit, Programmelemente direkt im Quelltext für die Refaktorisierung auswählen zu können. Dies wird für die Elemente benötigt, welche nicht in den Ansichten Outline oder Package-Explorer zur Verfügung stehen, zum Beispiel lokale Variablen. Ein Dialog eröffnet auch die Möglichkeit den Benutzer in gewissen Grenzen mit entscheiden zu lassen, wie eine Refaktorisierung durchzuführen ist. Überladene Methoden beispielsweise zeichnen sich dadurch aus, dass sie häufig einen gemeinsamen Einsatzzweck und daher alle den gleichen Namen haben. In REFACOLA werden diese beim Umbenennen einer Methode nicht mit umbenannt. Dies ist sinnvoll, da die Entscheidung dem Benutzer überlassen werden sollte, ob er dies möchte oder nicht. Perspektivisch wäre es aber denkbar, ihm über den Benutzerdialog hierfür eine Konfigurationsmöglichkeit zur Verfügung zu stellen. Da es in REFACOLA nicht möglich ist, einzelne Regeln aus einem Regelset aufzurufen, müssten hier die Regeln auf mehrere Regelsets aufgeteilt und eine eigene Refaktorisierungsdefinition geschrieben werden. Diskussion 5.3.2 55 Java-Versionen Die Entwicklung für REFACOLA basiert aktuell auf Java 1.6. Neben den noch offenen Themen (vgl. Kapitel 4.3.4) sind ebenfalls Erweiterungen für die aktuelle Version Java 1.7 vorzusehen45. Eine der wenigen Änderungen ist, dass nun Strings in switch-Anweisungen unterstützt werden. Außerdem bietet diese Version einen besseren Support für dynamische Sprachen, was den Einsatz von diesen zusätzlich fördern dürfte (vgl. Kapitel 5.2.2). Umfangreiche Neuerungen sind jedoch für die angekündigte Version 1.8 zu erwarten46. Mit Lambdas47 hält ein neues Konzept Einzug in die Sprache, welches ähnlich einschneidende Änderungen mit sich bringen könnte wie Generics oder Annotationen in Java 1.5. Mithilfe von ihnen sollen funktionale Sprachmittel in Java integriert werden. Lambdas sind als anonyme Methoden vorstellbar [Go11]. Einen ersten Eindruck auf die zu erwartende Syntax vermittelt Listing 5.3. //übernimmt die übergebenen integer-argumente, Rückgabe (x+y) (int x, int y) -> x + y // keine Argumente, Rückgabe 42 (integer) () -> 42 //druckt den übergebenen String (Console), keine Rückgabe (String s) -> { System.out.println(s);} Listing 5.3: Ausblick voraussichtliche Lambda-Syntax (vgl. [Go11]) 45 Eine Liste findet sich unter http://openjdk.java.net/projects/jdk7/features/#f618 Eine Übersicht der Java 1.8- Meilensteine unter: http://openjdk.java.net/projects/jdk8/milestones 47 http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html 46 Schlußbetrachtung 56 6 Schlußbetrachtung Refaktorisierung gewinnt zunehmend an Bedeutung. Dies mag unter anderem daran liegen, dass es in den immer beliebter werdenden agilen Vorgehensmodellen ein fester Bestandteil ist.48 Aber auch Initiativen wie die Clean Code DeveloperBewegung helfen, die Vorteile von regelmäßiger Quelltextpflege bewusst zu machen. Daher ist es wichtig, dass Entwickler bei dieser Arbeit von Werkzeugen zuverlässig unterstützt werden, gerade für eine so häufig genutzte Refaktorisierung wie das Umbenennen von Programmelementen (vgl. [MPB09, Sc10]). Bisher bestehende Werkzeuge sind zwar nicht fehlerfrei, allerdings schon sehr mächtig. Einen vergleichbaren Stand zu erreichen ist nicht einfach. Diese Arbeit zeigt, dass die Erreichung einer korrekten Refaktorisierung von Identifiern durchaus eine Herausforderung darstellt. Die Analyse aller Möglichkeiten, welche die Sprache Java bietet, erfordert zum Teil viel Recherchearbeit. Informationen aus der Java-Sprachspezifikation lassen sich zum Teil nur schwer herauslesen. Auch in der gängigen Literatur finden sich meist Beispiele, welche sich sehr an Konventionen orientieren, Spezialfälle werden dagegen häufig nicht berücksichtigt. Neben den spezifischen Eigenschaften der Sprache selbst unterstreichen vor allem die sprachübergreifenden Ansätze die Vielschichtigkeit des Themas. Fazit Dennoch konnte diese Arbeit einige Lücken für die Refaktorisierung von Identifiern in REFACOLA schließen. Das Regelset ist bereits fest in das Framework integriert und wird von allen bestehenden Refaktorisierungswerkzeugen eingebunden. Gerade aber im Zusammenhang mit fehlenden Qualifiern und qualifizierten Namen stößt allein der constraintbasierte Ansatz an Grenzen. Constraintregeln zu definieren, die das Abschatten einer Instanzvariablen korrekt behandeln, ist nur mit einer Vielzahl von Regeln denkbar. Außerdem wären Restriktionen nötig, wie es in Kapitel 4.3.3 erläutert wurde. Die Einbindung der Locked Bindings sollte daher eine wertvolle Ergänzung werden. Eine solche Kombination von Konzepten wurde auch bereits in [STST12] diskutiert. Erweiterungen am REFACOLA-Framework für Faktengenerierung und Rückschreibekomponente, welche während dieser Arbeit durchgeführt wurden, dienen allen Werkzeugen. Zudem wurde für die Ermittlung umschließender lokaler Variablen ein Prototyp vorgestellt. Die JUnit-Testsuite und die Erweiterung des RTT-Testframeworks unterstützen die Weiterentwicklung für das hier erstellte Refaktorisierungswerkzeug auch in Zukunft. Weil die Unittests den ganzen Refaktorisierungsablauf testen, werden darüber hinaus auch Faktengenerierung und Rückschreibekomponente geprüft. Schließlich wurde für die Einbindung der Locked Bindings eine Reihe von Testfällen zur Verfügung gestellt, die deren Einbindung ins Framework unterstützen können. Der Ansatz der constraintbasierten Refaktorisierung hat viele Stärken gezeigt. Mit vergleichsweise wenigen und einfachen Regeln konnten viele Probleme der Identifier-Refaktorisierungen in Java gelöst werden. Die Erstellung der Regeln wird durch den deklarativen Ansatz erleichtert. Zudem können die vorhandenen Constraintregeln von allen Werkzeugen genutzt werden. Die verbleibenden offenen Punkte erfordern jedoch noch einigen Aufwand. 48 Extreme Programming (XP) zum Beispiel definiert Refaktorisierung als eigene Praktik [WR11]. Literaturverzeichnis 57 Literaturverzeichnis CCD Clean Code Developer Unter: http://www.clean-code-developer.de/ Letzter Zugriff: 24.06.2013 DP06 Florian Deissenboeck , Markus Pizka: Concise and consistent naming; In: Software Quality Control, Volume 14 Issue 3, September 2006 , S. 261 – 282, Kluwer Academic Publishers Hingham, MA, USA Unter http://www.itestra.de/uploads/media/ 06_itestra_concise_and_consistent_naming.pdf Letzter Zugriff: 26.06.2013 EAPOGA11 Laleh M. Eshkevari, Venera Arnaoudova, Massimiliano Di Penta, Rocco Oliveto, Yann-Gaël Guéhéneuc, Giuliano Antoniol: An Exploratory Study of Identifier Renamings ; In: MSR '11 Proceedings of the 8th Working Conference on Mining Software Repositories, 2011, S. 33-42, ACM New York, NY, USA Unter: http://www.ptidej.net/publications/documents/MSR11.doc.pdf Letzter Zugriff: 24.06.2013 FU10 FernUniversität in Hagen: Prüfungsordnung für den Bachelorstudiengang Wirtschaftsinformatik an der FernUniversität in Hagen (Stand vom 14.07.2010); 2012 Unter: http://www.fernuni-hagen.de/ wirtschaftswissenschaft/studium/download/ordnungen/ba_winf_po.pdf Letzter Zugriff: 24.06.2013 Fo11 Martin Fowler (With contributions by Kent Beck, John Brant, William Opdyke, Don Roberts): Refactoring Improving the Design of Existing Code; Addison-Wesley Longman, Inc., 25.Auflage 2011 (Erstausgabe 1999) GJSB05 James Gosling, Bill Joy, Guy Steele, Gilad Bracha: The Java TM Language Specification – The (3rd Edition); Addison-Wesley, 3. Auflage 2005 Unter http://docs.oracle.com/javase/specs/jls/se5.0/jls3.pdf Letzter Zugriff: 10.05.2013 Go11 Brian Goetz: State of the Lambda; 4th edition, Dezember 2011 Unter: http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-4.html Letzter Zugriff: 24.06.2013 Ho10 Osama El Hosami: Implementierung eines Eclipse-Plugins zum automatisierten Testen von Refaktorisierungswerkzeugen; Masterarbeit, FernUniversität in Hagen, August 2010 Unter: http://www.fernuni-hagen.de/imperia/md/content/ps/masterarbeit-elhosami.pdf Letzter Zugriff: 24.06.2013 HW07 Hofstedt Petra, Wolf Armin: Einführung in die Constraint-Programmierung Grundlagen,Methoden, Sprachen, Anwendungen; Springer-Verlag Berlin Heidelberg 2007 Unter: http://link.springer.com/book/10.1007/978-3-540-68194-6/page/1 Letzter Aufruf: 25.05.2013 Literaturverzeichnis 58 Kl10 Michael Klenk: Sprachübergreifendes Refaktorisieren: Code über Sprachgrenzen hinweg verbessern; OBJEKTspektrum Ausgabe 04/2010 unter: http://www.sigs-datacom.de/fileadmin/user_upload/ zeitschriften/os/2010/04/klenk_OS_04_10.pdf Letzter Zugriff: 24.06.2013 Ma11 Robert C. Martin: Clean Code A Handbook of Agile Software Craftsmanship Prentice Hall Pearson Education, Inc., 9. Auflage 2011 MCo07 Steve Mc Connell: Code Complete (Deutsche Ausgabe der Second Edition) Übersetzung Detlef Johannis , Microsoft Press Deutschland 2007, dt. Ausg. der 2nd ed. [Nachdruck] MPB09 Emerson Murphy-Hill, Chris Parnin, Andrew P. Black: How we refactor, and how we know it; In: ICSE '09 Proceedings of the 31st International Conference on Software Engineering, 2009, S. 287-297 Unter: http://people.engr.ncsu.edu/ermurph3/papers/icse09.pdf Letzter Zugriff: 23.06.2013 MT04 Tom Mens, Tom Tourwé: A Survey of Software Refactoring; In: IEEE Transactions on software engineering, Volume 30 Issue 2, February 2004, S. 126–139, IEEE Press Piscataway, NJ, USA Unter: http://www.cs.bgu.ac.il/~se112/wiki.files/class-6-Refactoring-surveysoftware-refactorings-04.pdf Letzter Zugriff: 28.06.2013 Ni13 Sven Nicolai: Locked Bindings als Ergänzung des constraintbasierten Refaktorisierungsframeworks Refacola für die Programmiersprache Java Diplomarbeit, FernUniversität in Hagen Bisher unveröffentlicht, geplant für 2013 Os12 Antje Osten: Sprachübergreifendes Refaktorisieren zwischen Ruby und Java Bachelorarbeit, FernUniversität in Hagen, April 2012 Unter: http://www.fernuni-hagen.de/imperia/md/content/ps/bachelorarbeitosten.pdf Letzter Zugriff: 24.06.2013 Sc10 Max Schäfer, Specification, Implementation and Verification of Refactorings; PhD thesis, Oxford University Computing Laboratory, 2010 Unter: http://www.cs.ox.ac.uk/people/max.schaefer/ Letzter Zugriff: 01.06.2013 SEM08 Max Schäfer, Torbjörn Ekmann, Oege de Moor: Sound and Extensible Renaming for Java; In: Proceedings of the 23rd Annual ACM SIGPLAN Conference on ObjectOriented Programming, Systems, Languages, and Applications, OOPSLA 2008, October 2008, Nashville, TN, USA; S. 277–294, ACM; 2008 Unter: http://staff.cs.utu.fi/kurssit/doos/JavaRenaming.pdf Letzter Zugriff: 01.01.2013 SKP11 Friedrich Steimann, Christian Kollee, Jens von Pilgrim: A Refactoring Constraint Language and its Application to Eiffel; In Proceeding ECOOP'11 Proceedings of the 25th European conference on Object-oriented programming, S. 255-280, Springer-Verlag Berlin, Heidelberg, 2011 Unter: http://www.feu.de/ps/docs/Refacola.pdf Letzter Zugriff: 15.06.2013 Literaturverzeichnis ST12 59 Friedrich Steimann, Andreas Thies: From Public to Private to Absent: Refactoring Java Programms under Constrained Accessibility; In: Genoa Proceedings of the 23rd European Conference on ECOOP 2009 Object-Oriented Programming, S. 419 – 443, Springer-Verlag Berlin, Heidelberg, 2009 Unter: http://www.fernuni-hagen.de/ps/pubs/ECOOP2009.pdf Letzter Zugriff: 11.06.2013 St10 Friedrich Steimann: Korrekte Refaktorisierungen: Der Bau von Refaktorisierungswerkzeugen als eigenständige Disziplin; OBJEKTspektrum April 2010, S. 24–29 Unter: http://www.sigs-datacom.de/fileadmin/user_upload/zeitschriften/ os/2010/04/steimann_OS_04_10.pdf Letzter Zugriff: 24.06.2013 STST12 Max Schäfer, Andreas Thies, Friedrich Steimann, Frank Tip: A Comprehensive Approach to Naming and Accessibility in Refactoring Java Programs; In: IEEE Transactions on SOFTWARE ENGINEERING Volume 38, Issue 6, November 2012, S. 1233 – 1257 Unter : https://cs.uwaterloo.ca/~ftip/pubs/tse2012JL.pdf Letzter Zugriff: 13.06.2013 TB12 Andreas Thies, Eric Bodden: Safer Refactorings for Reflective Java Programs; In: Proceedings of the 21th International Symposium on Software Testing and Analysis (ISSTA) 2012, S. 1-11 ACM New York, NY, USA, 2012 (ACM SIGSOFT Distinguished Paper Award) Unter: http://www.feu.de/ps/prjs/rf/issta12.pdf Letzter Zugriff: 13.06.2013 TH12 Marc Teufel, Dr. Jonas Helming: Eclipse 4 Rich Clients mit dem Eclipse SDK 4.2 entwickler.press,Frankfurt am Main, 1. Auflage 2012 TR10 Andreas Thies, Christian Roth: Recommending Rename Refactorings; In: Proceedings of the 2nd International Workshop on Recommendation Systems for Software Engineering 2010, S. 1-5, ACM New York, NY, USA, 2010 Unter: http://www.fernuni-hagen.de/imperia/md/content/ps/rsse2010.pdf Letzter Zugriff: 22.06.2013 Wa12 Mirco Wagner: Sprachübergreifendes Java-XML-Refaktorisieren mit Refacola; Bachelorarbeit FernUniversität in Hagen, April 2012 Unter: http://www.fernuni-hagen.de/imperia/md/content/ps/bachelorarbeitwagner.pdf Letzter Letzter Zugriff 30.05.2013 We06 Frank Westphal: Testgetriebene Entwicklung - mit JUnit & FIT Wie Software änderbar bleibt; dpunkt.verlag Heidelberg, 1. Auflage 2006 WR11 Henning Wolf, Arne Roock: Agile Softwareentwicklung – Ein Überblick dpunkt.verlag GmbH Heidelberg, 3. Auflage 2011 SMG11 Gustavo Soares, Melina Mongiovi, and Rohit Gheyi: Identifying overly strong conditions in refactoring implementations; In: Proceedings of the 27th IEEE International Conference on Software Maintenance, September 2011, Seite 173 - 182 Unter: http://www.dsc.ufcg.edu.br/~spg/uploads/icsm2011.pdf Zugriff 24.06.2013 Anhang Anhang 60 A) Referenz Constraintregeln 61 A) Referenz Constraintregeln Names.ruleset.refacola UniqueMemberTypeNames1 for all t: Java.MemberType do if all(t) then t.identifier != t.enclosingNamedTypes.identifier , t.identifier != t.tlowner.identifier end UniqueTopLevelTypeIdentifier for all tlt1: Java.TopLevelType tlt2: Java.TopLevelType do if tlt1 != tlt2 then (tlt1.hostPackage = tlt2.hostPackage) -> (tlt1.identifier != tlt2.identifier) end PackageDeclarationNames for all packageDeclaration: Java.PackageDeclaration typeRoot: Java.TypeRoot do if Java.packageDeclarationInTypeRoot( packageDeclaration, typeRoot) then packageDeclaration.identifier = typeRoot.hostPackage.identifier end ImportDeclarationNames1 for all importDeclaration: Java.ImportDeclaration clazz: Java.NamedType do if Java.importDeclarationInTypeRoot( importDeclaration, clazz) then (importDeclaration.typeBinding = clazz) ->(importDeclaration.identifier=clazz.identifier) end ImportDeclarationNames2 for all importDeclaration: Java.ImportDeclaration tlt: Java.TopLevelType do if all(importDeclaration), all(tlt) then (importDeclaration.typeBinding != tlt) ->(importDeclaration.identifier != clazz.identifier) end UniqueMemberTypeNames2 for all memberType1: Java.MemberType memberType2: Java.MemberType do if memberType1 != memberType2 then (memberType1.owner = memberType2.owner) -> (memberType1.identifier != memberType2.identifier) end TopLevelTypeNames for all tlt: Java.TopLevelType tr: Java.TypeRoot do if all(tlt), all(tr) then (tlt.typeRoot = tr and tlt.accessibility = #public) -> (tlt.identifier = tr.identifier) end ReferenceIdentifier for all reference: Java.NamedTypedReference entity: Java.NamedEntity do if Java.binds(reference, entity) then reference.identifier = entity.identifier end TypeReferenceIdentifier for all type: Java.NamedType ref: Java.TypeReference do if all(type), all(ref) then (type = ref.typeBinding) -> (type.identifier = ref.identifier) end UniqueFieldIdentifier for all field1: Java.Field field2: Java.Field do if field1 != field2 then field1.owner = field2.owner ->(field1.identifier != field2.identifier) end A) Referenz Constraintregeln UniqueMethodIdentifier for all method1: Java.RegularTypedMethod method2: Java.RegularTypedMethod do if method1 != method2 then (method1.owner = method2.owner) and(method1.parameters.declaredParameterType = method2.parameters.declaredParameterType) -> (method1.identifier != method2.identifier) end AccidentalHidingFinalMethod for all method1: Java.Method method2: Java.Method do if Java.finalMethod(method2), all(method1) then (method1.identifier != method2.identifier) or (not Java.sub(method1.owner, method2.owner)) or (not(method1.parameters.declaredParameterType = method2.parameters.declaredParameterType)) or ((method1.tlowner != method2.tlowner) and (method2.accessibility = #private)) or ((method1.hostPackage != method2.hostPackage) and (method2.accessibility <= #package)) end UniqueLocalVariableNames1 for all parameter1: Java.FormalParameter parameter2: Java.FormalParameter method: Java.MethodOrConstructor do if Java.declaresLocalVariableOrParameter( method, parameter1), Java.declaresLocalVariableOrParameter( method, parameter2), (parameter1 != parameter2) then (parameter1.identifier != parameter2.identifier) end UniqueLocalVariableNames2 for all parameter: Java.FormalParameter localVariable: Java.LocalVariable methodOrConstructor: Java.MethodOrConstructor do if Java.declaresLocalVariableOrParameter( methodOrConstructor, parameter), Java.declaresLocalVariableOrParameter( methodOrConstructor, localVariable) then (parameter.identifier != localVariable.identifier) end 62 EagerInterfaceMethodNames for all type: Java.Class typeMethod: Java.InstanceMethod interfaceMethod: Java.InstanceMethod do if Java.eagerInterfaceImplementation( type, typeMethod, interfaceMethod) then typeMethod.identifier = interfaceMethod.identifier end OverridingMethodNames for all superMethod: Java.InstanceMethod subMethod: Java.InstanceMethod do if Java.overrides(subMethod, superMethod) then subMethod.identifier = superMethod.identifier end UniqueEnclosingMemberTypeNamesInAssignment1 for all field1: Java.InstanceField field2: Java.RegularTypedInstanceField fieldReference: Java.FieldReference expression: Java.Expression do if field1 != field2, Java.binds(fieldReference, field1), Java.isExpression(fieldReference, expression), Java.initialAssignment(field2, expression) then field1.tlowner = field2.tlowner and field1.owner != field2.owner and Java.sub!(field2.owner,field1.owner) ->fieldReference.identifier != field2.identifier end UniqueEnclosingMemberTypeNamesInAssignment2 for all field1: Java.InstanceField field2: Java.InstanceField fieldReference1: Java.FieldReference fieldReference2: Java.FieldReference expression1: Java.Expression expression2: Java.Expression do if field1 != field2, Java.binds(fieldReference1, field1), Java.binds(fieldReference2, field2), Java.isExpression(fieldReference1, expression1), Java.isExpression(fieldReference2, expression2), Java.assignment(expression2, expression1) then field1.tlowner = field2.tlowner and field1.owner != field2.owner and Java.sub!(field2.owner,field1.owner) ->fieldReference2.identifier != fieldReference1.identifier end A) Referenz Constraintregeln UniqueLocalVariableNames3 for all localVariable: Java.LocalVariable do if Java.hasEnclosingVariables(localVariable) then localVariable.identifier != localVariable.enclosingLocalVariables.identifier end 63 ConstructorNames for all type: Java.NamedType constructor: Java.Constructor do if Java.instantiates(constructor, type) then type.identifier = constructor.identifier end Regelset Accessibility (Autor A.Thies) AccidentalOverriding for all superMethod: Java.InstanceMethod subMethod: Java.InstanceMethod do if all (subMethod), all(superMethod) then Java.overrides(subMethod, superMethod) or (not Java.sub(subMethod.owner, superMethod.owner)) or (superMethod.identifier != subMethod.identifier) or (not (subMethod.parameters.declaredParameterType = superMethod.parameters.declaredParameterType)) or (superMethod.accessibility = #private) or ((superMethod.accessibility = #package) and (superMethod.hostPackage != subMethod.hostPackage)) end InstanceMethodOverridingStaticMethod for all staticMethod: Java.StaticMethod instanceMethod: Java.InstanceMethod do if all(staticMethod), all(instanceMethod) then (staticMethod.identifier != instanceMethod.identifier) or (not Java.sub(instanceMethod.owner, staticMethod.owner)) or (not( staticMethod.parameters.declaredParameterType = instanceMethod.parameters.declaredParameterType)) or ((staticMethod.tlowner != instanceMethod.tlowner) and (staticMethod.accessibility = #private)) or ((staticMethod.hostPackage != instanceMethod.hostPackage) and (staticMethod.accessibility <= #package)) end StaticMethodHidesInstanceMethod for all instanceMethod: Java.InstanceMethod staticMethod: Java.StaticMethod do if all(instanceMethod), all(staticMethod) then (instanceMethod.identifier != staticMethod.identifier) or (not Java.sub(staticMethod.owner, instanceMethod.owner)) or (not( instanceMethod.parameters.declaredParameterTyp = staticMethod.parameters.declaredParameterType)) or ((instanceMethod.tlowner != staticMethod.tlowner) and (instanceMethod.accessibility = #private)) or ((instanceMethod.hostPackage != staticMethod.hostPackage) and (instanceMethod.accessibility <= #package)) end B) Refaktorisierungsdefinition RenameMember 64 B) Refaktorisierungsdefinition RenameMember Im Rahmen dieser Arbeit wurde auch die Refaktorisierungsdefinition für das RenameMember erweitert. Die vollständige Datei ist hier abgebildet. /****************************************************************** * Copyright (c) 2012 FernUniversitaet in Hagen * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *****************************************************************/ import "Java.language.refacola" import "Accessibility.ruleset.refacola" import "Types.ruleset.refacola" import "Locations.ruleset.refacola" import "Names.ruleset.refacola" import "Reflection.ruleset.refacola" refactoring RenameMember languages Java uses Accessibility, Types, Names, Locations, Reflection forced changes identifier of Java.NamedEntity as NewName allowed changes /* rename NamedEntity -> update reference-Identifier */ identifier of Java.NamedReference {initial, NewName @ forced} /* rename type <-> update Constructor-Identifier */ identifier of Java.Constructor {initial, NewName @ forced} /* rename method */ identifier of Java.Method {initial, NewName @ forced} /* rename static method*/ identifier of Java.StaticMethod {initial, NewName @ forced} /* rename importdeclaration */ identifier of Java.ImportDeclaration /* rename Method-Parameter */ identifier of Java.FormalParameter /*rename memberType */ identifier of Java.MemberType {initial, NewName @ forced} {initial, NewName @ forced} {initial, NewName @ forced} // rename *.java file if top level type is renamed identifier of Java.TypeRoot {initial, NewName @ forced} // for the case that a type root is renamed identifier of Java.TopLevelType {initial, NewName @ forced} C) JUnit-Testsuite 65 C) JUnit-Testsuite Das UML-Diagramm stellt eine Reihe von Testklassen vor, die in der Testsuite AllReanameMemberTestsuite zusammengefasst sind. Die JUnit-Tests befinden sich im Paket de.feu.ps.refacola.lang.java.jdt.ui.tests.renamemember und wurden zur besseren Übersichtlichkeit nach einzelnen Schwerpunktthemen aufgeteilt. de.feu.ps.refacola.lang.java.jdt.ui.tests.renamemember AllRenameMemberTestsuite RenameMemberNestedMemberTests RenameMemberAbstractTests RenameMemberInterfaceTests RenameMemberControlFlowTests RenameMemberTopLevelTypeTests RenameMemberExceptionTests RenameMemberStaticsTests RenameMemberEnumTest RenameMemberLegalIdentifierTests RenameMemberTests RenameMemberEagerInterfaceTests RenameMemberOperatorTests RenameMemberUniqueIdentifierTests RenameParameterTests RenameLocalVariableTests RenameMemberVariousTests RenameMemberQualifiedNameTests RenameMemberHidingTests RenameMemberInheritanceTests RenameMemberOverrideTests RenameMemberShadowingTests RenameMemberOverloadTests RenameMemberErlandMuellerProblemTests RenameMemberWithThisOrSuperReference RenameLocalElementsTests RenameLocalVariableInInitializerTests RenameMemberEnclosingTypVariableNamesTests offene Themen RenameMemberAnonymousTests RenameMemberAnnotationTests RenameMemberImportTest RenamePackageTests RenameMemberGenericsTests dienen der Dokumentation Referenztests für Bugfixes RenameMemberKnownBugsTests RenameMemberReproduceRttBugs Tests für diverse Refaktorisierungswerkzeuge RenameRulesForOtherRefactoringsTests Abbildung C.1 Übersicht UML – Junit Tests RenameMemberBugtrackTests D) Inhalt der CD D) Inhalt der CD ./dokumente Enthält die Bachlorarbeit als PDF Installationsanleitung.pdf ./entwicklungsumgebung Enthält die Entwicklungsumgebung eclipse.juno inklusive aller Plugins jdk1.6.0_27 Generierte Plugin-Versionen ./projektverzeichnis Enthält Workspace aller Refacola-Projekte mit dem lokalen Entwicklungsstand vom 07.07.2013 Workspace RTT-Testprojekte ./teststatistiken Enthält Statistiken der RTT-Tests 66 Eidesstattliche Versicherung 67 Eidesstattliche Versicherung „Hiermit versichere ich an Eides statt, dass ich die Bachelorarbeit selbstständig und ohne Inanspruchnahme fremder Hilfe angefertigt habe. Ich habe dabei nur die angegebenen Quellen und Hilfsmittel verwendet und die aus diesen wörtlich oder inhaltlich entnommenen Stellen als solche kenntlich gemacht. Die Arbeit hat in gleicher oder ähnlicher Form noch keiner anderen Prüfungsbehörde vorgelegen. Ich erkläre mich damit einverstanden, dass die Arbeit mit Hilfe eines Plagiatserkennungsdienstes auf enthaltene Plagiate überprüft wird.“49 Ried, den 09.07.2013 49 Formulierung lt. Prüfungsordnung (Stand vom 14.07.2010) - §15 Bachlorarbeit (7) (vgl. [FU10])