C R D J Dissertation zur Erlangung des akademischen Grades des Doktors der Naturwissenschaften (Dr. rer. nat.) der Fakultät für Mathematik und Informatik der FernUniversität in Hagen Vorgelegt von Andreas Thies geboren in der Freien und Hansestadt Hamburg Constraintbasierte Refaktorisierung von Deklarationen in Java Dissertation, Andreas Thies Inhaltsangabe Refaktorisierungen sind Änderungen an einem Programm, ohne sein nach außen hin beobachtbares Programmverhalten zu ändern. Sie unterstü en Softwareentwickler dabei, bestehenden Programmcode zu warten und zu erweitern. Refaktorisierungswerkzeuge können bei dieser Aufgabe helfen, indem sie die für eine Refaktorisierung benötigten Quelltex ransformationen berechnen, anwenden und dabei sicherstellen, dass das Programmverhalten tatsächlich unverändert bleibt. Allerdings weist nach heutigem Stand ein Großteil der im Industrieeinsa befindlichen Refaktorisierungswerkzeuge hohe Fehlerraten auf, was im Wesentlichen der Komplexität moderner Programmiersprachen geschuldet ist. Die Folge ist, dass teilweise die refaktorisierten Programme entweder ungültig sind oder in ihrem Programmverhalten vom jeweiligen Eingabeprogramm abweichen. Vorliegende Arbeit verfolgt den Ansa der constraintbasierten Refaktorisierung. Die Grundidee besteht dabei in der Überführung von Refaktorisierungsproblemen in Bedingungserfüllungsprobleme. In diesen werden mögliche Codeänderungen durch Variablen dargestellt. Die durch diese anzunehmenden Werte werden durch Bedingungen derart eingeschränkt, dass die Menge der möglichen Variablenwerte und somit Programmtransformationen ausschließlich gültigen Refaktorisierungen entspricht. In Verbindung mit einem herkömmlichen Constraintlöser kann dann zu einem derart umgeformten Refaktorisierungsproblem bestimmt werden, ob eine bedeutungserhaltende Programmtransformation möglich ist und welche Änderungen am Quelltext dazu durchgeführt werden müssen. Anhand der Implementierung einer Suite von fünf Refaktorisierungswerkzeugen zur Refaktorisierung von Deklarationen in Java werden die Stärken des constraintbasierten Ansatzes demonstriert. Insbesondere wird gezeigt, dass sich Autoren von Refaktorisierungswerkzeugen beim verwendeten Ansa auf die Formulierung nur gering komplexer und vor allem wiederverwendbarer Regeln beschränken können, während die eigentliche Komplexität beim Zusammenwirken aller Bedingungen an bestehende Constraintlöser delegiert wird. Evaluiert werden die implementierten Werkzeuge anhand einer Testsuite bestehend aus mehr als . handgeschriebenen Tests sowie automatisierten Testläufen, in denen auf quelloffenen, frei verfügbaren Java-Projekten mehr als . Refaktorisierungen durchgeführt wurden. Die Testergebnisse zeigen, dass die entwickelten Werkzeuge viele Fälle korrekt behandeln, in denen herkömmliche Werkzeuge aufgrund der Komplexität der Sprache Java bisher scheitern. Danksagungen Auch wenn bei der Erstellung einer Dissertation die Arbeitsphasen, die man allein im stillen Studierzimmer verbringt, immer noch beträchtlich sind, so ist Wissenschaft heute doch wesentlich durch den Gedankenaustausch, die Zusammenarbeit und das voneinander Lernen geprägt. An dieser Stelle möchte ich einigen Personen danken, die mir in dieser Hinsicht zur Seite gestanden haben. Ein großer Dank gebührt meinem Doktorvater Friedrich Steimann, der mich vom ersten Tag an mit viel Energie an meine Aufgabe herangeführt und mich mit einer guten Mischung aus Stößen ins kalte Wasser und dem Zureichen von Schwimmhilfen begleitet hat. Insbesondere danke ich ihm für seine große Bereitschaft, mir alle Vorausse ungen für eine gute Vereinbarkeit von Familie, Beruf und Promotion zu bieten. Christoph Beierle, Jörg Desel sowie Daniela Keller danke ich dafür, dass sie die nicht immer dankbare Aufgabe als Mitglieder der Prüfungskommission übernommen haben. Weiterhin danke ich Jan Jürjens, Max Schäfer sowie Ma hias Riebisch, die mir allesamt große Gastfreundschaft entgegengebracht haben. Sie haben mir nicht nur an ihren jeweiligen Wirkungsstä en für teils mehrere Monate Tisch und Stuhl, sondern auch einen herzlichen Empfang und großartige Arbeitsbedingungen geboten. Für eine besonders intensive Zusammenarbeit danke ich weiterhin Eric Bodden. Er und Max Schäfer haben mir besonders tiefen Einblick in ihre Arbeitsweisen und Methodik gegeben, was meine Arbeit wesentlich geprägt hat. Mein Dank gilt ebenso meinen Kollegen Christian Kollee, Marcus Frenkel und Bastian Ulke. Sie ha en stets eine offene Tür und eine freie Ecke an ihrer Wandtafel, um mit mir die großen oder kleinen Probleme zu besprechen (und zu lösen!), die einem Doktoranden täglich so begegnen. Ebenso danke ich Hauke Col au, der als DoRF-Ältester und einer der Gründerväter der Doktorandenrunde der FernUni den Zusammenhalt und Austausch der Doktoranden quer über den Campus hinweg gefördert hat. Einen besonders herzlichen Dank möchte ich Daniela Keller aussprechen, die mich in den vergangenen Jahren bei enger und vertrauensvoller Zusammenarbeit oft in der Lehre entlastet hat, um mir mehr Zeit für die Forschung einzuräumen, und die stets ein offenes Ohr und eine helfende Hand ha e, wenn es um stilistische Fragen oder ums Korrekturlesen ging. Meinen Eltern danke ich, dass sie mir auf meinem Ausbildungsweg größtmögliche Freiheit gelassen und mich bis zum Abschluss dieser Arbeit in vielen Belangen unterstü t haben. Meiner Ehefrau Sandra danke ich für das Durchstehen der vielen Entbehrungen, die ihr ein promovierender Mann beschert hat. Sie hat mir mehr Unterstü ung gegeben, als sie glauben möchte. Eingereicht am: . . Tag der mündlichen Prüfung: Berichtersta er: Prof. Dr. Friedrich Steimann Prof. Dr. Jörg Desel . . Inhaltsverzeichnis 1. Einführung . . Refaktorisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge . . Herausforderungen . . . . . . . . . . . . . . . . . . . . . . . . . Der constraintbasierte Lösungsansa . . . . . . . . . . . . . . Beitrag der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . . . Au au der Arbeit . . . . . . . . . . . . . . . . . . . . . . . . 13 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2. Literaturbetrachtungen . . Die Anfänge . . . . . . . . . . . . . . . . . . . . . . . . . Vor- und Nachbedingungen für Refaktorisierungen . . Refaktorisierungen für dynamische Sprachen . . . . . . Referenzkonstruktion . . . . . . . . . . . . . . . . . . . . Constraintbasierte Refaktorisierungen . . . . . . . . . . Weitere constraintbasierte Werkzeuge . . . . . . . . . . Formale Darstellungen von Programmiersprachen . . . Refaktorisierungen als Graphtransformationen . . . . . Testen von Refaktorisierungswerkzeugen . . . . . . . . Einordnung der vorliegenden Arbeit . . . . . . . . . . . . Vorarbeiten . . . . . . . . . . . . . . . . . . . . . . Folgearbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3. Constraintbasierte Refaktorisierungen . . Eine formale Darstellung für Programmtransformationen . . . . . . . Transformationen von Syntaxbäumen . . . . . . . . . . . . . . Transformationen als Variablenzuweisungen . . . . . . . . . Programmtransformationen als Bedingungserfüllungsproblem . . Constraintregeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Constraintgenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . Constraintlösung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Refacola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sprachdefinitionen . . . . . . . . . . . . . . . . . . . . . . . . . Constraintregeln . . . . . . . . . . . . . . . . . . . . . . . . . . Refaktorisierungsdefinition . . . . . . . . . . . . . . . . . . . . Das Refacola-Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 4. Eine Sprachdefinition für Java in Refacola . . Angebotene Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 Inhaltsverzeichnis . . Taxonomie der Programmelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fakten und Abfragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Unveränderliche Programmeigenschaften und funktionale Einschränkungen . 5. Constraintregeln für Java . . Orts-Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugrei arkeits-Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugrei arkeit als Constraintvariable . . . . . . . . . . . . . . . . . . Zugrei arkeiten zur Zugriffskontolle . . . . . . . . . . . . . . . . . . Zugrei arkeit und Vererbung . . . . . . . . . . . . . . . . . . . . . . Zugrei arkeit bei Methodenüberschreibung und -verdeckung . . . Weitere Zugrei arkeitsregeln . . . . . . . . . . . . . . . . . . . . . Typ-Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typen als Constraintvariable . . . . . . . . . . . . . . . . . . . . . . . Constraintregeln zur Typinferenz . . . . . . . . . . . . . . . . . . . . Member-Zugriff und Zuweisungskompatibilität . . . . . . . . . . . . Überschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf statische Member . . . . . . . . . . . . . . . . . . . . . . . Weitere Regeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Typ-Constraints für generische Typen . . . . . . . . . . . . . . . . . . . . . . Typinferenz generisch typisierter Ausdrücke . . . . . . . . . . . . . . Beschränkt parametrische Typen . . . . . . . . . . . . . . . . . . . . . Zuweisungen an mit generischen Typen typisierte Ausdrücke . . . . Wildcards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Raw-Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Namens-Constraints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bindung anhand von Namen . . . . . . . . . . . . . . . . . . . . . . . Eindeutigkeit von Namen . . . . . . . . . . . . . . . . . . . . . . . . . Namen überschreibender Methoden . . . . . . . . . . . . . . . . . . . Weitere Namensregeln . . . . . . . . . . . . . . . . . . . . . . . . . . . Ungültige Namen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verdunkeln und Verscha en . . . . . . . . . . . . . . . . . . . . . . Unerwünschtes Überschreiben, Verdecken und Überladen . . . . . . . . . Unveränderliche Bibliotheksfunktionen . . . . . . . . . . . . . . . . . . . . Offene Probleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6. Berücksichtigung reflektiver Zugriffe . . Die Reflection-API . . . . . . . . . . . . . . . Refaktorisierung reflektiven Codes . . . . . . Programmelemente und Fakten . . . . . . . . Constraintregeln . . . . . . . . . . . . . . . . Zurückschreiben in reflektive Ausdrücke . . Einschränkungen des vorgestellten Ansa 85 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 . . . . . . . . . . es . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inhaltsverzeichnis 7. Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse . . Deklaration der Refaktorisierungswerkzeuge . . . . . . . . . . . . . . . . . . . . . . Move Compilation Unit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pull Up Member . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rename . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generalize / Specialize Declared Type . . . . . . . . . . . . . . . . . . . . . . Change Accessibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Validierung der Benu ereingaben . . . . . . . . . . . . . . . . . . . . . . . . Auswahl einer Constraintlösung . . . . . . . . . . . . . . . . . . . . . . . . . Benu erschni stelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Faktengenerierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Namensqualifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rückschreibekomponente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 8. Evaluation . . Forschungsfragen . . . . . . . . . . . . . . . . . . . . . . . . . . Korrektheitsbeweise . . . . . . . . . . . . . . . . . . . . . . . . Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Refactoring Tool Testing der implementierten Werkzeuge . . . Übermäßig starke Einschränkungen . . . . . . . . . . . . . . . Auswirkungen erlaubter Änderungen . . . . . . . . . . . . . . Laufzeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Evaluation der Constraintregeln für reflektive Zugriffe . . . . . . . . . . 173 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9. Weitere constraintbasierte Werkzeuge . . Quick Fixes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein prototypischer Quick Fix zum Anpassen von Zugrei arkeiten . . . Probleme bei der Implementierung beliebiger Quick Fixes . . . . . . . Constraintbasiertes Mutationstesten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 . . . . . . . . . . . . 10. Zusammenfassung 207 A. Java-Sprachdefinition 209 B. Constraintregeln für Java 223 C. Refaktorisierungsdefinitionen Move Compilation Unit . . . . . . . . Pull Up Member . . . . . . . . . . . . . Rename . . . . . . . . . . . . . . . . . . Generalize / Specialize Declared Type Change Accessibility . . . . . . . . . . 267 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Literaturverzeichnis 271 Index 283 1. Einführung Der umfangreiche Einsa von Programmierwerkzeugen ist aus dem Alltag der heutigen Softwareentwicklung nicht mehr wegzudenken. Während in den Pioniertagen der computergestü ten Programmierung lediglich ein einfacher Texteditor und Compiler zum Werkzeugkasten der Softwareentwicklung gehörten, haben sich nach und nach immer mehr Werkzeuge entwickelt, die heute wie selbstverständlich innerhalb von integrierten Entwicklungsumgebungen ihre Rolle bei der Programmierung spielen. Oft ist die Rolle dieser Werkzeuge eine passive, indem sie Softwareentwicklerinnen und Softwareentwicklern zusä liche Informationen bereitstellen, angefangen bei einfachen Syntaxhervorhebungen bis hin zu komplexen Navigationshilfen durch ganze Programmbibliotheken. Häufig sind Programmierwerkzeuge aber auch aktiv an der Programmerstellung und -bearbeitung beteiligt. So beschreibt Teitelman [Tei ] zum Beispiel bereits im Jahre ein Programmiersystem, welches bei Vorliegen eines Kompilierfehlers von sich aus Programmtransformationen anbietet, mi els derer das Ausgangsprogramm (wieder) in eine den syntaktischen und semantischen Regeln der Programmiersprache entsprechende Form gebracht werden kann. Heute ist es eine Selbstverständlichkeit, dass gute Entwicklungsumgebungen derartige Funktionen – meist unter dem Begriff quick fix bekannt – anbieten. Auch in der Folgezeit wurden für neu au ommende Vorgehensweisen bei der Softwareentwicklung zügig entsprechende Werkzeuge entwickelt. So hat Griswold [Gri ] Anfang der neunziger Jahre in der ersten Arbeit zu Refaktorisierungen bereits direkt ein entsprechendes Werkzeug mitgeliefert, welches imstande ist, bedeutungserhaltende Programmtransformationen durchzuführen. Programmierwerkzeugen, die Einfluss auf bestehenden Code nehmen und somit bestehendes Programmverhalten ändern können, kommt besondere Verantwortung zu! In keinem Falle sollte ein Programmierwerkzeug das Verhalten bestehender Programmteile ändern, ohne dass dies ausdrücklich der Intention der Programmiererin oder des Programmierers entspricht. Doch unglücklicherweise machen moderne objektorientierte Programmiersprachen mit ihren vielen Sprachkonzepten wie statischer und dynamischer Bindung, Typprüfungen oder ausgefeilten Sichtbarkeitskonzepten es Werkzeugentwicklerinnen und Werkzeugentwicklern nicht nur schwer, den Benu erwünschen entsprechende Codeänderungen zu berechnen, sondern fördern auch noch die Möglichkeit, unabsichtliche Änderungen im Programmverhalten zu provozieren. Einen guten – wenn auch mit einem Augenzwinkern vermi elten – Eindruck, zu welch unintuitiven und fehleranfälligen Situationen Java als moderne Programmiersprach mit ihren vielen Features führen kann, liefern zum Beispiel Joshua Bloch und Neal Gafter [BG ], die mit geschickt gewählten Beispielprogrammen bei der Frage nach deren Verhalten selbst gestandene Java-Programmierer¹ ins Schlingern bringen. ¹ Es steht außer Frage, dass nicht nur Programmierer, sondern ebenso auch Programmiererinnen von der Sprache Java in die Irre geleitet werden können. Zugunsten einer kompakteren Darstellung wird allerdings in der folgenden Arbeit auf die explizite Angabe beiderlei Geschlechts verzichtet. Da in dieser Arbeit oftmals die Schwächen der Programmiererinnen und Programmierer im Fokus stehen – sie machen Fehler – empfand es . Einführung Diese Arbeit widmet sich der Entwicklung korrekter, aktiv an der Codeerstellung beteiligter Programmierwerkzeuge und unter ihnen insbesondere den Refaktorisierungswerkzeugen. Die Wahl der dabei durchgängig betrachteten Programmiersprache fiel auf Java, alleine aufgrund ihres Verbreitungsgrades und ihrer Relevanz für die heutige Softwareindustrie. 1.1. Refaktorisierung Eine Refaktorisierung (engl. refactoring) ist eine Änderung (oder Umstrukturierung) eines Programms, ohne sein beobachtbares Programmverhalten zu ändern [Fow ]. Das dabei verfolgte Ziel ist meist, die Lesbarkeit und Verständlichkeit des Quellcodes zu verbessern. Ebenso ist es aber auch möglich, mi els Refaktorisierung Redundanzen zu entfernen oder bessere Erweiterungsmöglichkeiten zu schaffen [Fow ]. Der Begriff der Refaktorisierung – obwohl stets Ähnliches meinend – ist mehrdeutig. Einerseits dient er zur Beschreibung der allgemeinen Tätigkeit, andererseits aber auch zur Beschreibung eines Musters, nach dem bedeutungserhaltend Programmänderungen durchgeführt werden können. Bekanntheit erlangte hier insbesondere der durch Fowler zusammengetragene Katalog von Refaktorisierungen [Fow ]. Weiterhin können als Refaktorisierung auch entweder konkrete Transformationen eines bestimmten Programms oder Programmierwerkzeuge gemeint sein, die bei einer solchen Refaktorisierung unterstü en [DKWB ]. In le terem Fall wird zur Abgrenzung in dieser Arbeit der Begriff des Refaktorisierungswerkzeugs (engl. refactoring tool) genu t. Problematisch bei obiger Definition für Refaktorisierung ist, dass unklar bleibt, wann zwei Programme sich in ihrem beobachtbaren Programmverhalten gleich verhalten. Die Idee, dass gleiches Programmverhalten vorliegt, wenn bei gleicher Eingabe auch gleiche Ausgabe erfolgt, trägt nicht. Sie berücksichtigt weder nicht-funktionale Anforderungen, wie zum Beispiel solche an die Laufzeit, noch, dass je nach Aufgabenstellung auch bei identischen Programmen Ausgaben nach identischen Eingaben Unterschiede aufweisen können. Auf Ideen von Schäfer [Sch ] au auend werden innerhalb dieser Arbeit verschiedene sogenannte Abhängigkeiten (engl. dependencies) spezifiziert, die während einer Refaktorisierung zu erhalten sind und eine sinnvolle und formale Grundlage für den Begriff des unveränderten Programmverhaltens liefern. Refaktorisierungen, insbesondere im Kleinen – wie zum Beispiel beim Umbenennen einer Variable – sind heute gängige Praxis und selbstverständlicher Teil des Programmieralltags [MHPB ]. Daneben hat das Refaktorisieren durch die Verbreitung agiler Entwicklungsmethoden – zum Beispiel dem Extreme Programming [Bec ] – noch einmal an Bedeutung gewonnen. Dort wird das Refaktorisieren gar als ein so mächtiges Instrument betrachtet, dass eine vollständige Entwurfsphase (wie zum Beispiel im Wasserfallmodell vorgesehen) vor Implementierungsbeginn entfallen kann, da dank Refaktorisierungen Änderungen im Design auch noch nachträglich möglich seien. Dieser Vorteil kann allerdings nur zum Tragen kommen, soweit beim Refaktorisieren nicht versehentlich Fehler in das zu refaktorisierende Programm eingeschleust werden, die wiederum zeit- und kostenintensiv gefunden und behoben werden müssen. Eine Lösung hierfür können Refaktorisierungswerkzeuge bieten, die Programmierer nicht nur dahingehend entlasten, die nötigen Quelltex ransformationen durchzuführen, sondern dabei auch prüfen, ob das Programmverhalten unverändert bleibt. Wie der Autor als höflich, die männliche Form zu wählen. . . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge der folgende Abschni zeigt, sind derzeit verfügbare Werkzeuge allerdings noch weit davon entfernt, vor allem le tere Aufgabe zuverlässig zu erfüllen. 1.2. Probleme derzeit verfügbarer Refaktorisierungswerkzeuge Um die Schwierigkeiten der werkzeuggestü ten Programmänderung konkret erläutern zu können, gibt dieser Abschni Beispiele für einzelne Refaktorisierungswerkzeuge, wie sie in drei sehr verbreiteten Entwicklungsumgebungen für Java – NetBeans [NBE ], IntelliJ [IJI ] und Eclipse [ECL ] – implementiert sind. Es liegt in der Natur der Sache, dass diese Aufzählung nur exemplarisch Einzelfälle aufführt, hier größtenteils am Beispiel von Zugrei arkeitsproblemen in Java. Ab Kapitel wird deutlich werden, dass der in dieser Arbeit vorgestellte Lösungsansa nicht nur die gezeigten Einzelfälle löst, sondern generisch genug ist, auch darüber hinausgehende Probleme zu adressieren. Doch bevor es an die ersten Beispiele geht, ein weiterer Hinweis: Wenn in dieser Arbeit von der Sprache Java die Rede ist, dann ist durchgängig die Java-Sprachversion SE . gemeint. Wenn von der Java-Sprachspezifikation die Rede ist, dann ist stets die zugehörige The Java Language Specification, Third Edition [GJSB ] gemeint. Es ergibt sich aus dem Thema dieser Arbeit, dass bereits in den einführenden Beispielen, spätestens aber im technischen Teil eine Vielzahl von Referenzen auf die Java-Sprachspezifikation gemacht werden. Diese Arbeit nu t eine Kurzschreibweise für solche Referenzen, in denen in Klammern stehend die jeweilige Paragraphenangabe dem Java-Logo – einer Kaffeetasse – folgt. Wird also beispielsweise Bezug auf § . der Java-Sprachspezifikation genommen, liest sich die Kurzschreibweise ( § . ).² Berechnung von Zugreifbarkeiten in Java Java bietet für Typen, Methoden und Felder mi els der Zugrei arkeitsmodifizierer (engl. access modifier)³ public, protected und private Mechanismen zum Verbergen von Implementierungsdetails. Ihr disziplinierter Einsa hilft, Programme zu modularisieren, Abhängigkeiten zwischen Programmteilen deutlich zu machen und so le tendlich das Design des Programms zu verbessern [Mül ]. Ihre Allgegenwärtigkeit (denn auch ein fehlender Zugrei arkeitsmodifizierer impliziert einen bestimmten Zugrei arkeitsbereich, die default-Zugrei arkeit) macht es zwingend notwendig, dass Refaktorisierungswerkzeuge mit den unterschiedlichen Zugrei arkeitsstufen umgehen können. Da neben dem entsprechenden Schlüsselwort an einer Deklaration weiterhin über ihre Zugreifbarkeit entscheidet, von welchem Ort aus sie referenziert werden soll, sind hierbei insbesondere Refaktorisierungswerkzeuge betroffen, die Codeteile verschieben. Verschieberefaktorisierungen (M ) zählen zu den in der Praxis fünf häufigst ausgeführten Refaktorisierungen [MHPB ]. Sowohl Eclipse, als auch NetBeans und IntelliJ bringen entsprechende Werkzeuge mit, so auch eines, mit welchem ein deklarierter Typ (also eine Klasse oder ein Interface) zwischen Paketen verschoben werden kann. Wendet man diese Werkzeu² Leser einer digitalen Ausgabe dieser Arbeit dürfen sich über in den Referenzen verborgene Hyperlinks auf die online-Ausgabe der Java-Sprachspezifikation unter http://docs.oracle.com freuen. ³ Häufig findet man in der Literatur den Begriff der accessibility ebenso als Sichtbarkeit beziehungsweise den des access modifier als Sichtbarkeitsmodifizierer überse t [KH , Ull ]. Diese Überse ung ist aber fehlleitend, da in Java ebenso das Konzept der visibility ( § . . ) existiert, welches sta dessen treffender als Sichtbarkeit zu überse en ist. . Einführung 1 2 3 4 package a; public class A { void m(){}; } 5 6 package a ; 7 8 9 10 11 12 13 public class B extends A { void n(){ A myVar = new B(); myVar.m(); } } (a) package a; public class A { void m(){}; } ⇒ package b ; import a.A; public class B extends A { void n(){ A myVar = new B(); myVar.::: m(); } } (b) Abbildung . .: Werden die Refaktorisierungswerkzeuge von Eclipse, NetBeans und IntelliJ angewiesen, im Programm (a) die Klasse B in ein neues Paket b zu verschieben, erhält man als Ausgabe das rechtsseitig gezeigte Programm (b). In diesem gelingt der Zugriff auf die nur paketweit zugrei are Methode m in A in Zeile nicht mehr und es kommt zu einem Kompilierfehler. ge auf das in Abbildung . (a) gezeigte Programm⁴ an, indem man den Typen B in ein neues Paket b verschieben möchte, haben die Werkzeuge – neben der eigentlichen Verschiebung – zwei weitere Änderungen am Quellcode zu tätigen. Einerseits kann vom Paket b aus nicht mehr unqualifiziert auf den Typen A zugegriffen werden, sodass dieser entweder bei jeder Verwendung qualifiziert oder einmalig für B importiert werden muss. Andererseits ist zu beachten, dass die in Zeile aufgerufene Methode m keinen Zugrei arkeitsmodifizierer hat und deswegen nur innerhalb desselben Pakets aufgerufen werden kann. Während alle drei Werkzeuge einen passenden Import des Typen A einfügen, scheitern sie gleichzeitig an einem Anpassen der Zugrei arkeit, indem sie das in Abbildung . (b) gezeigte Programm erzeugen. Das Resultat ist in allen drei Fällen ein Kompilierfehler in Zeile . Dabei offenbart sich, dass Zugrei arkeiten durch die Eclipse-Implementierung des M scheinbar gar keine Berücksichtigung finden. IntelliJ und NetBeans hingegen warnen vor der Quelltex ransformation wenigstens mit einem entsprechenden Hinweisfenster und geben an, dass von B aus noch Zugriffe auf die Methode a bestehen. Gleichzeitig überlassen sie es aber beide dennoch dem Programmierer, die dadurch entstandenen Kompilierfehler von Hand zu beheben. Dem Programmierer allerdings macht es die Sprache Java keineswegs leicht, zu entscheiden, welcher Zugrei arkeitsmodifizierer für die Methode m zu wählen ist: private-Deklarationen sind nur innerhalb derselben Klasse zugrei ar, Deklarationen ohne Zugrei arkeitsmodifi⁴ Ein Hinweis zur Darstellung von Programmcode in der vorliegenden Arbeit: Java und teils auch Implementierungen entsprechender virtueller Maschinen verlangen, dass als public deklarierte Typen in eigenen Dateien und Typen unterschiedlicher Pakete in getrennten Verzeichnissen liegen. Zugunsten einer vereinfachten Darstellung wird in den Programmbeispielen dieser Arbeit auf diesen Umstand keine Rücksicht genommen. Sta dessen werden Typdeklarationen und ihnen zugehörige Importe stets nur durch eine Leerzeile getrennt untereinander gelistet. Der Leser sei gebeten, stets eine geeignete Aufteilung in Dateien und Verzeichnisse hinzuzudenken. . . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge 14 15 16 17 package a; public class A { public void m(){} } 18 19 20 21 22 23 24 25 26 package b; import a.A; public class B extends A { void n(){ A myVar = new B(); myVar.m(); } } (a) package a; public class A { protected void m(){} } ⇒ package b; import a.A; public class B extends A { void n(){ A myVar = new B(); myVar.::: m(); } } (b) Abbildung . .: Sowohl Eclipse als auch NetBeans lassen bei einem C S eine Änderung der Zugrei arkeit von m hin zu protected für das linke Programm (a) zu, obwohl dies zu einem Kompilierfehler in Zeile führt (b). zierer nur innerhalb desselben Pakets, weswegen beide in obigem Beispiel für m einen Zugriff aus dem Paket b verbieten würden. Die protected-Zugrei arkeit wird in gängigen Hand- und Lehrbüchern so beschrieben, dass ein Zugriff auf eine solch annotierte Deklaration nur aus demselben Paket oder Subklassen heraus zulässig ist. Demnach würde für obiges Beispiel die Wahl eines protected-Modifizierers für m genügen. Was dieselben Lehr- und Handbücher hingegen häufig verschweigen (exemplarisch seien [Eck , KH , Ull ] genannt), ist, dass eine Subklassenbeziehung noch nicht hinreichend ist. So reicht ein protected-Modifizierer für m nämlich nicht aus, denn zusä lich fordert die Java-Sprachspezifikation, dass der Aufruf auf eine protected-Methode auch im Kontext der abgeleiteten Klasse sta finden muss ( § . . ). In obigem Beispiel müsste demnach der Empfänger des Aufrufs also vom Typ B oder einem seiner Subtypen sein. Die einzig korrekte Lösung wäre also, die Methode m als public zu deklarieren. Aus Sicht eines Programmierers ist es ärgerlich, dass die ausgeführten M -Werkzeuge die nötige Änderung der Zugrei arkeiten nicht gleich mit ausgeführt haben, insbesondere, da es in allen drei Entwicklungsumgebungen weitere Refaktorisierungswerkzeuge gibt, die sehr wohl den Anspruch erheben, mit Änderungen von Zugrei arkeitsmodifizierern umgehen zu können. So erlaubt zum Beispiel ein in allen drei Umgebungen enthaltenes C S (beziehungsweise in NetBeans C M P und in Eclipse C M S ) genanntes Werkzeug unter anderem das Erhöhen und Verringern von Zugrei arkeitsmodifizierern von Methoden. Allerdings arbeiten auch diese Werkzeuge mitunter fehlerhaft, was sich zeigt, wenn man obiges Beispiel fortführt. Angenommen, der Entwickler hä e in diesem den entstandenen Kompilierfehler (gegebenenfalls nach erfolglosem Ausprobieren eines protected-Modifizierers) durch Einfügen eines public-Modifizierers behoben. Es ergibt sich somit das Programm in Abbildung . (a). Wendet man auf dieses eine C S -Refaktorisierung an, bei der die Zugreifbarkeit der Methode m hin zu protected verringert werden soll, scheitert sowohl Eclipse als . Einführung 27 28 29 30 31 32 33 34 package a; public class A { void m(){}; void n(){ A myVar = new B(); myVar.m() ; } } 35 36 37 38 package b; import a.A; public class B extends A { 39 40 41 42 43 } (a) package a; public class A { protected void m(){}; ⇒ } package b; import a.A; public class B extends A { void n(){ A myVar = new B(); myVar.m(); ::: } } (b) Abbildung . .: Weist man das Werkzeug P D in Eclipse an, in dem links gezeigten Programm (a) die Methode n in die Subklasse B zu verschieben, wird die Zugrei arkeit der von dieser referenzierten Methode m zwar angepasst, allerdings nur unzureichend auf protected-Stufe. auch NetBeans. Beide führen ohne weitere Warnungen die Codeänderungen aus und erzeugen das nicht kompilierbare Programm aus Abbildung . (b), obwohl – wie oben erörtert – ein protected-Modifizierer nicht genügt. Alleine IntelliJ vermag eine passende Warnung auszugeben, die den Programmierer von der fehlerhaften Refaktorisierung abhält. Das Problem der Redundanz Die beobachteten Fehler in den Refaktorisierungswerkzeugen scheinen um so ärgerlicher, wenn man sich vergegenwärtigt, dass fehlerhafte Routinen einzelner Refaktorisierungswerkzeuge in anderen Werkzeugen derselben Entwicklungsumgebung durchaus korrekte Behandlung erfahren. So scheint M T in Eclipse generell keine Zugrei arkeiten zu berücksichtigen. Stellt man das Beispiel aus Abbildung . hingegen ein wenig um, sodass der Codeblock mit der Referenz auf die Methode m nicht mi els eines M T , sondern eines P D in das Paket b verschoben wird, finden in Eclipse sehr wohl Prüfungen zur Zugrei arkeit sta . Zwar scheitert das Werkzeug le tendlich immer noch, denn es erhöht die Zugrei arkeit von m nur auf ein ungenügendes protected, wie in Abbildung . gezeigt, immerhin fanden aber anscheinend überhaupt Prüfungen zur Zugrei arkeit sta . Ein umgekehrter Fall ist bei IntelliJ zu beobachten. Während das dortige C S mit der protected-Zugrei arkeit in Java umzugehen wusste und die Refaktorisierung aus Abbildung . korrekterweise abgelehnt hat, wurde dieses Wissen offensichtlich nicht in das ebenso verfügbare U I W P -Werkzeug eingebracht. Für dieses zeigt Abbildung . (a) ein Beispiel, in welchem genau das nicht kompilierbare Ausgabeprogramm er- . . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge 44 45 46 47 package a; public class A { protected void m(){}; } 48 49 50 51 52 53 54 55 56 package b; import a.A; public class B extends A { void n(){ B myVar = new B(); myVar.m(); } } (a) package a; public class A { protected void m(){}; } ⇒ package b; import a.A; public class B extends A { void n(){ A myVar = new B(); myVar.::: m(); } } (b) Abbildung . .: Wendet man im Programm (a) das in IntelliJ eingebaute U I W P -Werkzeug an, um den deklarierten Typen der Variable myVar in Zeile von B in A zu ändern, kommt es zu einem Kompilierfehler in der Folgezeile, ohne dass das Werkzeug hiervor warnt. Das C S Werkzeug derselben Entwicklungsumgebung hat die Ausgabe des nicht kompilierenden Programms (b) in ähnlicher Situation hingegen korrekt verhindert (siehe Abbildung . ). zeugt wurde, welches das C S -Werkzeug zu verhindern wusste (Abbildung . (b)). Die Vermutung liegt nahe (und für das quelloffene Eclipse bestätigt auch ein Blick in den Programmtext), dass jeweils gänzlich unterschiedliche Implementierungen für sehr ähnliche Aufgaben existieren, was gleich zweifach negativ zu Buche schlägt: in mehrfachem Entwicklungsaufwand einerseits und unnötigen Fehlern andererseits. Folgeänderungen und Folgefehler Selbst wenn ein Refaktorisierungswerkzeug die für eine angestrebte Refaktorisierung nötigen Folgeänderungen wie ein Anpassen von Zugrei arkeiten oder Importieren von Typen korrekt bestimmt, ist damit noch nicht zwingend eine gültige Programmtransformation gefunden. Denn auch diese Folgeänderungen können wiederum weitere Änderungen im Programmtext erzwingen. Abbildung . (a) zeigt ein Programm, S -Werkzeugs in welchem die Methode m(String) der Klasse A mi els eines C auf public-Zugrei arkeit angehoben werden soll. Was alle drei betrachteten Entwicklungsumgebungen dabei korrekt erkennen, ist, dass Java für überschreibende Methoden fordert, dass diese die Zugrei arkeit im Vergleich zur überschriebenen Methode nicht verringern dürfen ( § . . . ). NetBeans erkennt dies zwar und teilt es über eine entsprechende Warnung mit, verweigert aber, automatisch die Zugrei arkeit der überschreibenden Methode m(String) in B mit anzupassen. Dem Benu er bleibt nur die Option, den angezeigten Refaktorisierungsdialog zu schließen und den Programmtext von Hand zu ändern. IntelliJ und Eclipse hingegen passen ebenso auch die überschreibende Methode in ihrer Zugrei arkeit mit an und geben das in Abbildung . (b) gezeigte Programm aus. Was beide dabei aber verkennen, ist, dass es nun zu einer Änderung im Programmverhalten kommt: der Methodenaufruf in Zeile . Einführung 57 58 59 60 package a; public class A { void m(String s) { /*...*/ } } package a; public class A { public void m(String s) { /*...*/ } } package a; public class B extends A { void m(String s) { throw new Error(); } public void m(Object o) { /*...*/ } } package a; public class B extends A { public void m(String s) { throw new Error(); } public void m(Object o) { /*...*/ } } 61 62 63 64 65 66 67 68 ⇒ 69 70 71 72 73 74 75 package x; public class X { void n() { new a.B().m("abc"); } } (a) package x; public class X { void n() { new a.B().m("abc"); } } (b) Abbildung . .: Soll im Programm (a) die Methode m(String) in A einen public-Modifizierer erhalten, muss in der Folge auch die überschreibende Methode m(String) in B als public deklariert werden. Dies führt aber zu einer Änderung des Programmverhaltens, da der Aufruf von m in der Klasse X nun an die Methode m(String) sta wie zuvor m(Object) bindet. . . Probleme derzeit verfügbarer Refaktorisierungswerkzeuge bindet nun nämlich nicht mehr an die aus der Klasse X einzig zugrei are Methode m(Object), sondern an die neuerdings ebenso zugrei are und von den Methodenparametern her spezifischere Methode m(String). Je nach konkretem Verhalten der m-Methoden kann dies zu beliebigen Änderungen im Programmverhalten – also einer Nicht-Refaktorisierung – führen. Dabei stehen die Chancen gut, dass dadurch ins Programm eingebrachte Fehler zunächst nicht auffallen, findet doch nun keine augenblickliche Fehlermeldung durch den Compiler sta , wie es in den vorhergehenden Beispielen der Fall war. Ebenso steht der Zugriff aus Klasse X in keinem räumlichen Zusammenhang mit der Refaktorisierung in den Klassen A und B, sondern kann von überall im Programm herrühren, also auch Programmteilen, die der Entwickler nicht unmi elbar im Blick hat. Es kann also zu tückischen Fehlern kommen, die erst zur Laufzeit auffallen. Dann allerdings ist die Ursache des Problems – eine Refaktorisierung mit zwei kleinen Änderungen – lange abgeschlossen und womöglich in Vergessenheit geraten, was eine Fehlerfindung erschwert. Eclipse und IntelliJ haben mit ihren Refaktorisierungswerkzeugen den Benu er somit in trügerischer Sicherheit gelassen, was die Beibehaltung des Programmverhaltens angeht. Ob der Benu er von NetBeans bei seinen von Hand ausgeführten Quelltextänderungen die Weitsicht gehabt hä e, nicht denselben Fehler wie diese Werkzeuge zu begehen, sei dahingestellt. Reflektives Programmverhalten Ein Fall, in dem die Refaktorisierungswerkzeuge aller betrachteten Entwicklungsumgebungen gänzlich die Segel streichen, sind introspektive Programme [Rok ], also Programme, die zur Laufzeit dynamisch Eigenschaften von sich selbst abfragen und in Abhängigkeit davon ihren Programmfluss verändern können [Rok ]. Java erlaubt die introspektive Programmierung mi els der Reflection-API [McC , REF ]. Sie ermöglicht (unter anderem), zur Laufzeit Objekte nach ihren deklarierten Methoden und Feldern zu fragen, Werte von Feldern zu lesen und zu schreiben sowie Methoden aufzurufen [FF ]. Abbildung . zeigt eine Verwendung der Reflection API, in welcher innerhalb der mainMethode von Klasse B auf die Klasse A reflektiv zugegriffen wird. Zunächst wird in Zeile eine Referenz auf das Class-Objekt der Klasse A geholt, welches den Einstiegspunkt für reflekeine Referenz auf die Methode m erzeugt, tive Zugriffe bildet. Anschließend wird in Zeile welche in Zeile genu t wird, um diese Methode auf einer neuen (ebenso dynamisch erzeugten) Instanz von A aufzurufen. Für Aufrufe über die Reflection API gelten ähnliche Einschränkungen wie für reguläre Zugriffe, so sind insbesondere auch Zugrei arkeiten zu berücksichtigen [FF ]. Reduziert ein Refaktorisierungswerkzeug die Zugrei arkeit von m, wäre eine IllegalAccessException und somit eine Änderung des Programmverhaltens in Zeile die direkte Folge. Dennoch ist keines der in den drei betrachteten Entwicklungsumgebungen vorhandenen Refaktorisierungswerkzeuge in der Lage, reflektive Aufrufe zu berücksichtigen. Die Ursache dafür ist, dass für eine statische Analyse – auf welcher die Refaktorisierungswerkzeuge au auen – reflektive Aufrufe häufig unentscheidbar bleiben (man überlege sich, dass in Zeile von Abbildung . auch ein Aufruf Class.forName(args[0]) enthalten sein könnte). Um reflektive Aufrufe vollständig zu erfassen, ist also eine Auswertung von Laufzeitinformationen unumgänglich. Bisher bietet aber kein bekanntes Refaktorisierungswerkzeug für Java eine solche an. . Einführung 76 77 78 79 80 81 82 83 84 85 86 87 package a; public class A { public void m() { /*...*/ } } package b; public class B { public static void main(String[] args) throws Exception { Class<?> a = Class.forName("a.A"); java.lang.reflect.Method m = a.getDeclaredMethod("m"); m.invoke(a.newInstance()); } } Abbildung . .: Reflektiver Zugriff auf eine Klasse A. In Zeile wird eine Referenz auf ein Class-Objekt der Klasse A erzeugt, Ausgangspunkt, um in Zeile eine Referenz auf die Methode m zu erhalten und diese in Zeile dynamisch aufzurufen. 1.3. Herausforderungen Wie der vorhergehende Abschni gezeigt hat, können selbst kleine und scheinbar einfache Änderungen von gegebenen Programmtexten komplexe Auswirkungen haben, die bei gegenwärtigen Refaktorisierungswerkzeugen keine ausreichende Berücksichtigung finden. Dieser Abschni fasst die wesentlichen Herausforderungen noch einmal zusammen. Nötige Programmtransformationen Nicht immer lässt sich aus den Eingabeparametern eines Refaktorisierungswerkzeugs direkt die durchzuführende Programmtransformation ableiten. Wie bei den einführenden Beispielen gesehen, müssen neben der eigentlich angestrebten Änderung (zum Beispiel dem Verschieben einer Deklaration) noch weitere nötige Quellcodeänderungen ermi elt werden, wie zum Beispiel das Hinzufügen von Import-Anweisungen oder Qualifizierern und Anpassen von Zugrei arkeitsmodifizierern. Je nach Refaktorisierung können aber auch weitere Arten von Folgeänderungen nötig werden. Wird zum Beispiel ein Feld in eine Superklasse verschoben, kann es nötig werden, weitere Felder mit zu verschieben, mi els derer es bei seiner Deklaration initialisiert wird [SP a]. Bei Typ-Refaktorisierungen, bei denen die Typen von Deklarationen oder Ausdrücken geändert werden (was auch geschehen kann, indem eine this-Referenz innerhalb einer Methode in eine andere Klasse verschoben wird), sind häufig Anpassungen weiterer deklarierter Typen nötig [TFK+ ]. Erschwerend kommt hinzu, dass nötige Folgeänderungen ihrerseits Folgeänderungen bewirken können. Erzwingt das Verschieben einer Deklaration das Verschieben einer weiteren Deklaration, kann es hierfür nötig werden, Zugrei arkeiten anzupassen. Dies kann aber – wie im Beispiel aus Abbildung . gesehen – erneut weitere Änderungen von Zugrei arkeiten bedingen. Insgesamt sind beliebig lange Ke en von Folgeeffekten denkbar [TKB , ST , SKP ]. . . Der constraintbasierte Lösungsansa Erkennung unerwünschter Änderungen im Programmverhalten Die komplexen Binderegeln von Java eröffnen vielfach Möglichkeiten, unbeabsichtigt Methoden- und Variablenbindungen zu beeinflussen oder das Programm in anderer Weise entgegen der Intention des Programmierers zu modifizieren [ST , SVEM , Sch ]. Im harmlosesten Fall wird das Programm dadurch ungültig, erzeugt also einen Kompilierfehler. Weitaus schlimmer wird es, wenn sich das Programmverhalten unbemerkt ändert, ein Beispiel hierfür lieferte Abbildung . . Soweit kein geeigneter Testfall diese Änderung im Programmverhalten aufdeckt, führt ein so entstandener Programmfehler zu erhöhtem Zeitaufwand (und somit Kosten) und stellt den Einsa von Refaktorisierungswerkzeugen generell in Frage [DDGM , SGM ]. Vermeidung von Redundanzen Refaktorisierungswerkzeuge überschneiden sich in ihrem konkreten Funktionsumfang. So hat zum Beispiel beinahe jedes Refaktorisierungswerkzeug in irgendeiner Art mit Zugrei arkeiten zu tun. Dennoch sind momentan innerhalb derselben Entwicklungsumgebungen ähnliche Funktionalitäten in unterschiedlichen Werkzeugen häufig mehrfach, teils unterschiedlich, und damit fehlerhaft implementiert. Das Beispiel aus Abbildung . zeigte, dass bei der Implementierung des U I W P in IntelliJ eine Zugrei arkeitsregel der Sprache Java offensichtlich unberücksichtigt blieb, die im Werkzeug C S hingegen korrekt implementiert ist. Wünschenswert wäre ein gemeinsames Framework, auf welches alle Refaktorisierungswerkzeuge zurückgreifen könnten und welches die Berechnung nötiger Änderungen sowie Folgeänderungen und die Überprüfung auf Beibehaltung des Programmverhaltens für alle Werkzeuge einheitlich durchführen würde. Für einen umfassenden Einsa müsste ein solches Framework allerdings Schni stellen bieten, über die unterschiedlichste Quellcodeänderungen angefordert werden könnten, was die Komplexität im Inneren dieses Frameworks (im Vergleich zu den bisherigen Einzelwerkzeugen) drastisch erhöhen würde. Schwächen der statischen Analyse Alle untersuchten Werkzeuge gründen ihre Berechnungen allein auf eine statische Analyse des Quellcodes [TB ]. Während die Vorteile dessen auf der Hand liegen – auch ohne eine Laufzeitanalyse steht die Funktionalität der Refaktorisierungswerkzeuge zur Verfügung – bleibt die Frage nach reflektivem Programmverhalten unentscheidbar [TB ] und damit die Frage nach Aufrufen an die Reflection-API in Java faktisch unberücksichtigt. Das zu entwickelnde Framework müsste also so modular aufgebaut sein, dass sich – wo notwendig – eine Laufzeitanalyse zuschalten ließe. 1.4. Der constraintbasierte Lösungsansatz Diese Arbeit stellt eine Lösung vor, welche die im vorhergehenden Abschni . genannten Probleme adressiert. Aufgebaut wird dabei auf den Ideen der constraintbasierten Typrefaktorisierungen, die von einer Arbeitsgruppe um Tip [TKB , DKTE , FTK+ , KETF , Tip ] entwickelt und schon für verschiedene Werkzeuge erfolgreich eingese t wurden. Die Grundidee dabei ist, das Berechnen einer bedeutungserhaltenden Programmtransformation auf ein Bedingungserfüllungsproblem (engl. constraint satisfaction problem) [Tsa ] zu reduzieren. Mögliche, durch das jeweilige Refaktorisierungswerkzeug durchführbare Programmänderungen werden in Form von Constraintvariablen modelliert, die jeweils einzelnen . Einführung Programmelementen zugeordnet sind. So werden beispielsweise einer Felddeklaration Constraintvariablen zugeordnet, die ihren Namen [SKP ], ihre Zugrei arkeitsstufe [ST ], ihren deklarierten Typen [TKB ] sowie den Ort ihrer Deklaration beschreiben. Möchte ein Refaktorisierungswerkzeug eine bestimmte Änderung durchführen, so beispielsweise eine Änderung der Zugrei arkeitsstufe eines Feldes, formuliert es dies in Form einer Bedingung (engl. constraint). Diese Bedingung bezieht sich auf nur eine Variable, nämlich die, die die Zugreifbarkeitsstufe des gewählten Feldes repräsentiert. Ihr Wert wird gemäß der vom Benu er gewünschten Zugrei arkeit beschränkt. Ausgehend von einem wohlgeformten Programm ist ein solches Bedingungserfüllungsproblem zunächst immer erfüllbar. Sämtliche Variablen werden dazu mit den Werten belegt, die dem Zustand vor der Refaktorisierung im Quellcode entsprechen. Einzig die entsprechende Constraintvariable für die gemäß dem Benu erwunsch zu ändernde Deklaration erhält einen geänderten Wert zugewiesen. Damit ist aber noch nicht sichergestellt, dass die dadurch repräsentierte Programmtransformation tatsächlich ein gültiges und bedeutungsgleiches Programm erzeugt. Daher werden in einem zweiten Schri über einen Sa Constraintregeln (engl. constraint rule) auf Grundlage von Abfragen an das Programm (den sogenannten Faktenabfragen) weitere Bedingungen generiert. Diese schränken die Menge der Constraintvariablen darüber hinaus in ihren möglichen Werten derart ein, dass als Lösungen des Bedingungserfüllungsproblems nur solche verbleiben, die gültige und bedeutungserhaltende Programmtransformationen repräsentieren. Die Constraintregeln bilden also den Teil der Java-Sprachspezifikation ab, der bei Änderungen der zur Verfügung stehenden Constraintvariablen berücksichtigt werden muss. Der Mechanismus der Constraintgenerierung und der dafür benötigte Regelsa sind zunächst unabhängig von der konkreten Aufgabenstellung eines Refaktorisierungswerkzeugs formulierbar. Ob ein bestimmtes Refaktorisierungswerkzeug auf einem Constraintsystem, also einer Menge von Constraints und zugehöriger Variablen, au auen kann, hängt davon ab, ob alle angestrebten Programmtransformationen des Werkzeugs (und alle zu erlaubenden Folgeänderungen) durch entsprechende Variablenbelegungen ausgedrückt werden können. Ein möglichst breites Spektrum an Variablen erhöht somit die mögliche Anwendungsdomäne eines Constraintsystems. Für die Autoren des Regelsa es besteht die Herausforderung darin, die Regeln in Abhängigkeit von den sich aus den Variablen ergebenden Transformationen derart zu formulieren, dass sie erstens vollständig sind, also keine ungültigen oder bedeutungsverändernden Programmtransformationen zulassen, und zweitens die Menge der erlaubten Programmtransformationen nicht unnötig einschränken. Anders formuliert sollte jede Constraintregel für sich genommen notwendig und die Regelmenge in ihrer Gesamtheit hinreichend sein. Die Stärke des constraintbasierten Ansa es ergibt sich aus der einfachen Formulierbarkeit der einzelnen, einzuhaltenden Bedingungen in Form von Constraintregeln, von denen jede einzelne nur einen Teilaspekt des Problems löst und sich in einem durch den Programmierer überschaubaren Rahmen formulieren lässt. Das komplexe Zusammenwirken der aus allen Regeln abgeleiteten Bedingungen ist hingegen nicht mehr Sorge des Werkzeugentwicklers, denn dies kann an einen Constraintlöser delegiert werden. Durch die konjunktive Natur der Constraints (alle Constraints müssen erfüllt sein) lassen sich auch nachträglich ohne weiteren Aufwand zusä liche Bedingungen formulieren, wie es in dieser Arbeit am Beispiel der optionalen Berücksichtigung von reflektiven Aufrufen geschieht. . . Beitrag der Arbeit 1.5. Beitrag der Arbeit Im Folgenden sind die wesentlichen Beiträge dieser Arbeit in Kürze zusammengefasst: • Den Haup eil dieser Arbeit bildet die Herleitung und Entwicklung eines Sa es von Constraintregeln für Java, wobei sich deren mögliches Einsa gebiet aus den durch die Regeln referenzierten Constraintvariablen ergibt. Durch die Vielseitigkeit der in dieser Arbeit berücksichtigten Variablen (Namen, Zugrei arkeiten, deklarierte Typen und Deklarationsorte) ist auch eine große Vielfalt implementierbarer Refaktorisierungswerkzeuge gegeben, von einem einfachen R über ein P U bis hin zu einem M T , welches optional auch Zugrei arkeitsmodifizierer mit anpasst. • Sämtliche Constraintregeln und Variablen sind in Refacola [SKP ] formuliert, einer formalen Sprache, welche feste Syntax- und Semantikregeln für die Formulierung constraintbasierter Refaktorisierungswerkzeuge und deren Constraintregeln vorgibt. Damit liegen die Constraintregeln gleichzeitig in einer für menschliche Leser kompaktübersichtlichen und für Maschinen interpretierbaren Form vor, was die in vielen bisherigen Arbeiten vorhandene Gefahr einer Diskrepanz zwischen formaler Notation und tatsächlicher Implementierung [SM ] behebt. • Ein erweiterter, auf einer Laufzeitanalyse au auender Regelsa erlaubt (optional) die Constrainterzeugung in Abwesenheit statisch ermi elbarer Abhängigkeiten von Programmelementen. • Au auend auf diesen Regelsä en wird ein Framework vorgestellt, um so die zur Constraintgenerierung benötigten Faktenabfragen an Java-Projekte zu ermöglichen. Intern baut das Framework auf den Java Development Tools [JDT ] auf und fügt sich damit nahtlos in die Eclipse-IDE [ECL ] ein. • Au auend auf diesem Framework wurden die folgenden Refaktorisierungswerkzeuge implementiert: – M C U – P {M ,F } – R {M ,F ,T – C – C T {F ,M {F ,C ,C U ,M } } ,T }A • Ein formaler Beweis für die Korrektheit der hergeleiteten Constraintregeln ist nicht möglich. Ein solcher Beweis müsste nämlich als Eingabe eine formale Beschreibung der Sprache Java erhalten, eine solche wird mit der vorliegenden Arbeit aber erst geliefert. Daher wurden die erstellten Refaktorisierungswerkzeuge anhand von massiven Anwendungen evaluiert. Es wurden mehr als . Refaktorisierungen auf acht verschiedenen Open-Source-Projekten durchgeführt. • Im Rahmen eines Ausblicks auf zukünftige Weiterentwicklungen wurde weiterhin eine Implementierung eines Quick-Fix-Werkzeugs zum Anpassen von Zugrei arkeitsmodifizierern implementiert, welches ebenso auf den hergeleiteten Constraintregeln au aut. . Einführung 1.6. Aufbau der Arbeit Das folgende, zweite Kapitel dieser Arbeit bildet eine Übersicht verwandter Literaturbeiträge zu Refaktorisierungen und Refaktorisierungswerkzeugen sowie eine Auflistung derjenigen Publikationen des Autors, die dieser Arbeit zugrunde liegen. Das dri e Kapitel dieser Arbeit führt noch einmal detailliert in die in Abschni . nur grob umrissene Idee der constraintbasierten Refaktorisierung ein. Abschni . geht insbesondere auf die Refacola ein, eine domänenspezifische Sprache zur Formulierung constraintbasierter Refaktorisierungswerkzeuge. Ab Kapitel widmet sich diese Arbeit der konkreten Umse ung constraintbasierter Refaktorisierungswerkzeuge. Zunächst wird die Wahl geeigneter Variablen beschrieben, bevor im nachfolgenden Kapitel ein Sa Constraintregeln für Java entwickelt wird. Die zur Berücksichtigung reflektiven Programmverhaltens nötigen Regeln sind dabei in ein eigenes Kapitel ausgelagert. Die im Rahmen dieser Arbeit erfolgte Einbindung des Constraintregelwerkes in die Entwicklungsumgebung Eclipse, sodass dieses dort in Refaktorisierungswerkzeugen unmi elbar nu bar wird, beschreibt Kapitel . Kapitel widmet sich einer Evaluation des Regelsa es anhand von Tests dieser Refaktorisierungswerkzeuge. Das le te Kapitel offeriert eine Zusammenfassung. Konkrete Constraintregeln werden innerhalb dieser Arbeit nur dort aufgelistet, wo sie unmi elbar zur Beschreibung eines bestimmten Sachverhaltes dienen. Eine komple e Auflistung der Deklarationen aller Constraintvariablen findet sich in Anhang A, eine vollständige Auflistung aller Constraintregeln in Anhang B sowie eine vollständige Auflistung der verwendeten Refaktorisierungsdefinitionen in Anhang C. 2. Literaturbetrachtungen Dieser Abschni listet zugrundeliegende und verwandte Arbeiten zu Refaktorisierungen und Refaktorisierungswerkzeugen auf. Da die Literaturlandschaft zu diesem Thema mi lerweile als üppig gelten kann (zum Jahre bereits mehr als Publikationen [MT ]), wurde sich einerseits auf eine Darstellung der wichtigsten Meilensteine bei der Entwicklung von Refaktorisierungswerkzeugen, sowie andererseits auf Vorarbeiten zu constraintbasierten Refaktorisierungswerkzeugen und den zu ihnen alternativen Ansä en konzentriert. Ebenso findet sich eine Auflistung von Arbeiten des Autors, die Grundlage dieser Arbeit waren oder Teilergebnisse dieser Arbeit bereits verwendet haben. 2.1. Die Anfänge Die erste Arbeit zu Refaktorisierungen und auch Refaktorisierungswerkzeugen ist Griswold [Gri ] zuzuschreiben. Er entwickelte in seiner Dissertation die Idee, Änderungen an der Struktur eines Programms von anderen Entwicklungstätigkeiten zu trennen. Lässt man während Strukturänderungen eines Programms das Programmverhalten unverändert, erleichtert dies die Erkennung von dabei eingebrachten Programmierfehlern, die sich dann in geändertem Programmverhalten widerspiegeln müssen [Gri ]. Bereits mit dieser ersten Arbeit zum Refaktorisieren machte Griswold die Bedeutung der Werkzeugunterstü ung deutlich und lieferte einen Sa von Werkzeugen für die Refaktorisierung von Scheme-Programmen. Die nötige Programmanalyse zur Berechnung der notwendigen Quelltex ransformationen sowie zur Prüfung auf unverändertes Programmverhalten geschah anhand von Programmabhängigkeitsgraphen (engl. program dependence graph) [KKP+ ], also Graphen, in denen die Knoten die Anweisungen des Programms und die Kanten ihre Abhängigkeiten, hauptsächlich Kontroll- oder Datenfluss, repräsentieren. Griswold baute dabei auf eine gegebene Implementierung von Programmabhängigkeitsgraphen auf, die in zusä lichen Kanten weitere indirekte Abhängigkeiten repräsentiert ha e und die für Refaktorisierungen relevant sind. So betrachtete er zum Beispiel Kanten, die beizubehaltende Reihenfolgen für Anweisungen angeben. Mit diesen kann zum Beispiel verhindert werden, dass eine Anweisung vor eine andere Anweisung verschoben wird, die ihren Effekt au eben würde. Dies könnte der Fall sein, wenn einer Variablen ein neu berechneter Wert zugewiesen werden soll, wobei nicht die Zuweisung vor der Neuberechnung sta finden darf. 2.2. Vor- und Nachbedingungen für Refaktorisierungen Der eigentliche Begriff der Refaktorisierung wurde dann ein Jahr später durch die Arbeit von Opdyke [Opd ] geprägt. Während Griswold den Fokus (bedingt durch die Wahl der Programmiersprache Scheme) auf Anweisungen innerhalb von Blöcken se te [Gri ], betrachtete . Literaturbetrachtungen Opdyke mit C++ eine objektorientierte Sprache und für diese insbesondere Refaktorisierungen, die Typhierarchien restrukturieren. Au auend auf vergleichsweise einfach durchzuführenden Teilrefaktorisierungen, wie zum Beispiel dem Löschen oder Einfügen einer Deklaration, beschreibt er drei komplexe Refaktorisierungen, so zum Beispiel das Einfügen einer abstrakten Superklasse, die gemeinsames Verhalten zweier (dann) Subklassen zusammenfasst. Richtungsweisend ist dabei die Betrachtung der bei den Refaktorisierungen einzuhaltenden Vorbedingungen (engl. precondition), die sich insbesondere durch das Betrachten der kleineren Teilrefaktorisierungen einfacher überblicken und somit prüfen lassen. Auf der Arbeit von Opdyke au auend nu t Roberts [Rob ] (ebenso später Koch und Kniesel [KK ]) in seiner Dissertation zusä lich Nachbedingungen (engl. postcondition), von deren Erfüllung nach einer Refaktorisierung ausgegangen werden kann. Mit diesen lässt sich die Komplexität von Refaktorisierungen (und damit Refaktorisierungswerkzeugen) reduzieren. Findet nämlich eine Aufteilung einer Refaktorisierung in einzelne Teilrefaktorisierungen sta und entsprechen dabei die Nachbedingungen einer vorhergehenden Refaktorisierung den Vorbedingungen der nachfolgenden Refaktorisierung, kann eine weitere Prüfung dieser Bedingungen entfallen. 2.3. Refaktorisierungen für dynamische Sprachen Hervorzuheben an der Arbeit von Roberts [Rob ] ist insbesondere, dass das entstandene Tool – der Refactoring Browser [RBJ ] – Smalltalk-Programme refaktorisiert. Smalltalk ist dynamisch typisiert [GR ]. Da somit schon bei einfachen Methodenaufrufen unklar ist, an welche Methode zur Laufzeit gebunden wird (oder in Smalltalk-Sprechweise: bei Nachrichtenversand erst zur Laufzeit klar ist, wer der Nachrichtenempfänger sein wird), lassen sich nur bedingt Aussagen über das Programmverhalten aus dem reinen Quelltext entnehmen, wie es zum Beispiel bei Griswold durch Extraktion eines Programmabhängigkeitsgraphen aus dem Quellcode geschehen konnte [Gri ]. Zwar konnten Feldthaus et al. [FMM+ ] zeigen, dass eine statische Quelltextanalyse durchaus sinnvoll Refaktorisierungen dynamischer Sprachen unterstü en kann, dennoch bleibt die Frage nach dem Programmverhalten im allgemeinen Fall unentscheidbar. Sta dessen geschieht beim Refactoring Browser die Programmanalyse zur Laufzeit, was auch die Ausführung der nötigen Änderungen (zumindest in Teilen) erst zur Laufzeit erlaubt. Diese Idee eines sich zur Laufzeit selbst ändernden Programms mag aus heutiger Sicht seltsam erscheinen, fügte sich aber in das – durch und durch reflektive – Konzept Smalltalks nahtlos ein, in welchem dank Just-in-time-Kompilierung sogar die jeweilig zum Programmieren verwendete Smalltalk-Umgebung selbst verändert und erweitert werden konnte. 2.4. Referenzkonstruktion Sollen Refaktorisierungswerkzeuge für Java entwickelt werden, lässt sich wegen der Ähnlichkeit von C++ und Java am ehesten auf Arbeiten von Opdyke au auen. Allerdings hat dieser nur Teilaspekte der Sprache C++ berücksichtigt. Entstehen bei seinem R zum Beispiel Namenskonflikte, wird die Refaktorisierung abgelehnt [Opd ], ohne gegebenenfalls mit Hilfe von den in C++ verfügbaren namespaces gegenzusteuern. Für die Sprache Java, welche mit . . Referenzkonstruktion dem Verdecken (engl. hiding), Überladen (engl. overloading) und Verscha en (engl. shadowing) gleich mehrere Konzepte anbietet, verschiedene deklarierte Elemente gleich zu benennen, ist ein pauschales Ablehnen von Refaktorisierungen bei Namenskonflikten eine sehr starke Einschränkung. Sta dessen sollten die vorhandenen Mi el der Programmiersprache – im Falle von Namenskonflikten die Qualifizierung von Referenzen – jeweils voll und geeignet zum Einsa kommen. Schäfer et al. [SEM ] haben sich dieses Problems angenommen und eine Lösung präsentiert, die durch Ausnu en der Java-Sprachmi el zur Qualifikation von Referenzen unnötig rigorose Refaktorisierungen verhindert. Dabei bauen sie ebenso wie Opdyke [Opd ] auf Teilrefaktorisierungen, also kleinere Refaktorisierungen, aus denen sich die Gesam ransformation zusammense t. Diese Teilrefaktorisierungen können dann jeweils für sich implementiert, verifiziert, getestet und ausgeführt werden, was sowohl die Entwicklungsarbeit, als auch Laufzeit verringern kann. Dem Problem, dass die Teiltransformationen bei einer Refaktorisierung für sich genommen nicht zwangsläufig ein gültiges Programm ergeben – man denke an ein R , bei dem die Referenzen auf eine umbenannte Deklaration gleichzeitig mit dieser angepasst und gegebenenfalls qualifiziert werden müssen – begegnen Schäfer et. al [SEM ] mit Spracherweiterungen. In der hierfür genu ten Zwischensprache NameJava [Sch ] (beziehungsweise ihrer Weiterentwicklung JL [STST ]) können Referenzen nicht nur, wie in Java üblich, über (gegebenenfalls qualifizierte) Namen erfolgen, sondern ebenso auch fest verzeigert werden (sogenanntes locking [Sch ]). Dies ermöglicht es zum Beispiel, nach vorherigem locking aller Referenzen erst eine Deklaration umzubenennen und anschließend in separaten Schri en die jeweils betroffenen Referenzen zu behandeln. Währenddessen bleibt das Programm jeweils ein gültiges NameJava-Programm, bis alle Referenzen wieder zu regulären Java-Referenzen zurückgewandelt wurden (das sogenannte unlocking). Der schwierige Teil ist dabei der Le tgenannte, das unlocking. Es müssen Abbildungsvorschriften gefunden werden, mit denen sich für einen gegebenen Programmpunkt und eine gegebene Deklaration eine gültige Referenz auf diese erzeugen lässt. Das Verfahren dazu nennt sich Referenzkonstruktion (engl. reference construction) [STST ]. Betrachtet man die Binderegeln der Sprache Java als eine partielle Funktion lookup : ProgramPoint × Reference ⇀ Declaration [STST ], welche zu einer gegebenen Referenz r am Programmpunkt p die referenzierte Deklaration d = lookup(p, r) gemäß der Binderegeln in Java findet, so muss für das unlocking also genau eine Rechtsinverse dieser Funktion zum Einsa kommen access : ProgramPoint × Declaration ⇀ Reference [STST ], für die dann der Ausdruck ∀p, d : lookup(p, access(p, d)) = d [STST ] erfüllt sein muss. Der Beitrag von Schäfer liegt vor allem darin, für die komplexen Bindealgorithmen in Java solche inversen Funktionen gefunden, implementiert und verifiziert zu haben [SEM , Sch ]. Steht eine solche Funktion zur Verfügung, können nicht nur Umbenennungen, sondern auch viele weitere – insbesondere auch Verschieberefaktorisierungen – mit korrekter Beibehaltung aller Namensbindungen implementiert werden [SM ]. . Literaturbetrachtungen 88 89 class A { int m(Object o) { return 1; } class A { int m m1 (Object o) { return 1; } private int mm2 (String s) { return 2; } } 90 91 } 92 93 94 95 96 class B extends A { int i = new B().m ("abc"); private int m(String s) { return 2; } } ⇒ class B extends A { int i = new B().↑ m2 ("abc"); } 97 98 99 100 class C extends A { int j = new C().m("def"); } (a) class C extends A { int j = new C().↑ m1 ("def"); } (b) Abbildung . .: Soll im linken Programm (a) die Methode m(String) von der Klasse B in die Klasse A verschoben werden, ergibt sich nach Schäfer [Sch ] während des unlocking das rechts gezeigte Programm (b) mit (noch teilweise) locked bindings. Entsprechend der Notation aus [STST ] bilden die Superskripte m1 und m2 dabei eindeutige Namen, die mi els der durch Pfeile markierten Referenzen ↑ m1 und ↑ m2 referenziert werden. Der Vorteil des Ansa es nach Schäfer et. al – nämlich bei Refaktorisierungen alle Referenzen für sich genommen betrachten und behandeln zu können – birgt allerdings auch einen Nachteil. Alle während des unlocking durchgeführten Änderungen dürfen nur lokal Auswirkungen haben, aber in keinem Fall Einfluss auf das unlocking weiterer Referenzen haben. Anderenfalls wären wechselseitige Beeinflussungen möglich, die eine unabhängige Bearbeitung verbieten. Aus diesem Grunde sind bei Schäfer et. al beim unlocking auch nur solche Änderungen gesta et, die sich jeweils lokal auswirken, was mögliche Folgeänderungen einer Refaktorisierung auf das Einfügen von Qualifizierern für Referenzen beschränkt. Dies kann aber nun wiederum zu unnötigen Einschränkungen der Werkzeuge führen. Abbildung . (a) zeigt ein Beispiel, in welchem die Methode m(String) mi els eines P U M aus der Klasse B in die Klasse A verschoben werden soll. Während der Refaktorisierung ergibt sich das in Abbildung . (b) gezeigte JL -Programm, in welchem noch das unlocking der Referenzen ↑ m1 respektive ↑ m2 aussteht. Für ↑ m1 würde die access-Funktion wieder die ursprüngliche JavaReferenz m("def") einse en, ohne dass sich eine Änderung der Methodenbindung ergäbe. Für ↑ m2 ist ein unlocking nicht möglich, da die private Methode m2 nicht mehr zugrei ar ist, worauf auch eine qualifizierte Referenz keinen Einfluss nehmen kann. Ein unlocking, welches die Zugrei arkeit von m2 anpasst (indem zum Beispiel der private-Modifizierer entfernt wird) wäre wünschenswert, untergräbt aber das vorher durchgeführte unlocking von ↑ m1 . Dieses ha e nur Erfolg, solange m2 nicht zugrei ar war. Im Falle einer zugrei aren Methode m2 wäre ein zusä licher Type Cast notwendig gewesen, sodass die access-Funktion den Ausdruck m((Object)"def") hä e erzeugen müssen, um die Bindung an m1 beizubehalten. Die Idee des unlocking funktioniert also gut, um Änderungen zu berechnen, die nur lokal Auswirkungen im Programm haben, wie es insbesonderen für Qualifizierungen von Referen- . . Constraintbasierte Refaktorisierungen zen der Fall ist. Ebenso konnten Schäfer et al. zeigen, dass ihre locking/unlocking-Mechanismen auch geeignet sind, um Bedingungen zu parallelem Programmverhalten zu erhalten [SDS+ ], wobei hier im Wesentlichen ohnehin nur Bedingungen zu prüfen sind, deren Nichterfüllung dann zu einem Abbruch der Refaktorisierung führen muss. Für die Behandlung von Folgeänderungen, die sich global im Programm auswirken können, ist der Ansa von Schäfer allein nicht tauglich. 2.5. Constraintbasierte Refaktorisierungen Möchte man die bei Refaktorisierungen einzuhaltenden Bedingungen nicht mehr isoliert betrachten, sondern ihre Wechselwirkungen zusä lich berücksichtigen (können), ergibt sich der Übergang von der Erfüllung einzelner Bedingungen zur Erfüllung eines komplexen Bedingungserfüllungsproblems. Den Grundstein für die ersten constraintbasierten Refaktorisierungswerkzeuge legten Palsberg und Schwarzbach [PS ], welche für eine Modellsprache (die sogenannte BOPL – Basic Object Programming Language) ein System von Typ-Constraints einführten, mit dem sich prüfen lässt, ob ein gegebenes Programm die Typregeln der Sprache erfüllt. Darauf au auend entwickelten Tip et. al [TKB ] die Idee, den Schri von einer reinen Bedingungsprüfung hin zu einer Bedingungserfüllung zu gehen, wie sie beim Refaktorisieren von Bedeutung ist und wie sie bereits in Abschni . einführend beschrieben ist. Tip et al. konzentrierten sich dabei ebenso wie Palsberg und Schwarzbach auf Bedingungen, die sich aus dem Typsystem der Sprache Java ergeben. Ähnlich wie bei Änderungen der Zugrei arkeiten von Deklarationen gilt ebenso für das Ändern deklarierter Typen von Deklarationen, dass hierfür auch an weiteren Programmstellen Auswirkungen auf das Programmverhalten zu berücksichtigen sind, weswegen eine isolierte Betrachtung solcher Quellcodeänderungen nicht funktionieren kann. Die Übertragung der Typ-Constraints von Palsberg und Schwarzbach auf die Typregeln von Java führte zunächst zur praxistauglichen Implementierung der constraintbasierten Refaktorisierungswerkzeuge U S W P und G T [TKB ], später folgten noch weitere Werkzeugentwicklungen [STD , BTF , Tip , TFK+ ]. Ganz ähnlich zu den Arbeiten von Tip et al., ohne die verwendeten Prüfungen formal als Constraintsystem zu beschreiben, entwickelten Steimann [Ste ] et al. mit I T ein Werkzeug, das nicht nur Refaktorisierungen unter Zuhilfenahme der vorhandenen, deklarierten Typen erlaubt, sondern die Einführung neuer, an die jeweilige Verwendung eines Typen angepasster, Interfaces erlaubte. Auf denselben Typprüfungen basierend folgte auch durch die Gruppe um Steimann die Entwicklung weiterer Werkzeuge [SM , KS ]. Mit der Einführung generischer Typen in Java war es hingegen wieder die Arbeitsgruppe um Tip, die entsprechende Erweiterungen der Typ-Constraints vornahm, um auch generische Typen zu berücksichtigen, und die insbesondere auch Refaktorisierungswerkzeuge entwickelte, die helfen, in bestehendem Code Typen geeignet zu parametrisieren [KETF , FTK+ ]. Ein Problem der bis dahin entwickelten constraintbasierten Refaktorisierungswerkzeuge ergibt sich aus der Diskrepanz zwischen formaler Notation der Constraintregeln und fehlender Formalisierung der eigentlichen implementierten Refaktorisierungen [SM ]. Die Entwicklung der domänenspezifischen Sprache Refacola [SKP ] durch Steimann et al. schließt diese Lücke, indem sie nicht nur eine formale Darstellung von Constraintregeln erlaubt, sondern . Literaturbetrachtungen auch die formale Beschreibung der erwarteten Eingabeprogramme und zu entwickelnden Werkzeuge gesta et und aus diesen automatisch Code zu generieren vermag. In Zusammenarbeit mit dem Autor der vorliegenden Arbeit sind verschiedene Abschlussarbeiten entstanden, die Teilaspekte der Sprache Java in Refacola abbilden [Wag , Mai , Spe ]. Vorübergehend offen geblieben ist bei den Arbeiten der Arbeitsgruppen um Tip und Steimann das Problem der Vorausschau [ST ]. Bedingungen, aufgrund derer ein Constraint aus einer Constraintregel generiert werden muss, können sich mitunter erst während des Constraintlösens erfüllen. Während sich dieses Problem bei isoliert betrachteten Einzelaspekten – wie es bei Tip mit den deklarierten Typen der Fall war – noch umgehen lässt, indem entweder keine weiteren Änderungen zugelassen werden oder alle weiteren Änderungen (vorausschauend) als Teil der vom Benu er gewünschten Änderungen bekannt sein müssen und somit unveränderlich sind, gewinnt das Problem zunehmend Oberhand, sobald unterschiedliche Arten von Variablen gleichzeitig zum Einsa kommen. Steimann und von Pilgrim [SP a] haben dieses Problem mi els automatischer Regeltransformationen gelöst, auf welche auch Abschni . dieser Arbeit noch einmal eingehen wird. 2.6. Weitere constraintbasierte Werkzeuge Die Technologie der constraintbasierten Refaktorisierungen lässt sich auch auf weitere Arten von Programmierwerkzeugen übertragen. Noch im engen Verwandtschaftsverhältnis zu den in dieser Arbeit vorgestellten Refaktorisierungswerkzeugen steht die Arbeit von Steimann und v. Pilgrim zu einem Werkzeug für unbenannte Refaktorisierungen [SP b, SP ]. Für diese ist eine Refaktorisierung nicht mehr an das Korse eines spezifischen Werkzeugs gebunden, viel mehr erhält das Werkzeug als Eingabe zwei Programmversionen, zwischen denen beliebige Änderungen sta gefunden haben können. Als Ausgabe liefert ein solches Werkzeug dann entweder die Information, dass sich das Programmverhalten zwischen beiden Versionen unweigerlich unterscheidet, oder eine Menge von Folgeänderungen, die nötig sind, um dies zu verhindern. Allerdings müssen Steimann und v. Pilgrim einräumen, dass der Vergleich beliebiger Programme hinsichtlich ihres Verhaltens schwierig ist [SP b, SP ] (im allgemeinen Fall sogar unentscheidbar [BA ]). Daher beschränken sie sich bei ihren Betrachtungen auf solche Programmtransformationen, die sich durch eine vorgegebene Menge von Constraintvariablen beschreiben lassen und für welche dann der constraintbasierte Ansa direkt angewendet werden kann. Dass sich constraintbasiert nicht nur Programmtext, sondern auch Modelle refaktorisieren lassen, zeigte unlängst Steimann [Ste , Ste ]. Modelle als Abstraktion konkreten Programmtextes büßen naturgemäß an Informationsgehalt ein, zumeist, was konkretes Programmverhalten angeht. Somit liegt bei den anzuwendenden Constraintregeln der Schwerpunkt eher bei der Wohlgeformtheit der Modelle, als der Bedeutung der durch sie repräsentierten Programme. Dies ändert sich, wenn man nicht nur isoliert die Modelle, sondern auch aus ihnen generierten Code mit refaktorisiert. Wird nämlich derart generierter Code nachträglich von Hand ergänzt und angereichert, verbietet sich nach einer Refaktorisierung eine Neugenerierung, da diese die erfolgten Änderungen im Java-Code überschreiben würde. Sta dessen müssen simultan das Modell und der Code transformiert werden, was – wie in [PUTS ] gezeigt – constraintbasiert gut gelingt. . . Formale Darstellungen von Programmiersprachen Das enge Verwandtschaftsverhältnis von Refaktorisierungswerkzeugen zu solchen Assistenzprogrammen, die für gegebene Programme verle te Eigenschaften der Wohlgeformtheit (wieder-) herstellen, konnten Steimann und Ulke [SU ] demonstrieren. Au auend auf den Arbeiten zu constraintbasierten Refaktorisierungen für Modelle entwickelten sie Werkzeuge, die mi els eines Constraintlösers nach einer solchen Menge von Änderungen im Modell suchen, welche die verle ten Wohlgeformtheitseigenschaften wiederherstellt. Damit haben sie Quick-Fix-Werkzeuge beschrieben, die analog zu denen arbeiten, wie sie im Ausblick dieser Arbeit in Abschni . beschrieben sind. 2.7. Formale Darstellungen von Programmiersprachen Constraintbasierte Refaktorisierungswerkzeuge schaffen mit den Constraintregeln – zumindest in Ausschni en – eine formale Darstellung einer Programmiersprache und den in ihr geschriebenen Programmen. Auch in dieser Hinsicht kann auf verwandte Literatur verwiesen werden. So existieren verschiedene Arbeiten, in denen einzelne Eigenschaften der Sprache Java formal nachgewiesen wurden. Als Grundlage für solche Beweise wird naturgemäß ebenso eine formale Darstellung der Sprache benötigt, aus welcher dann erst die nachzuweisenden Eigenschaften abgeleitet werden können. Erste Arbeiten für Java konzentrierten sich auf den Nachweis der Korrektheit des Typsystems. Sie sollten ausschließen, dass tro der statischen Typprüfung zur Laufzeit nicht dennoch Typfehler auftreten können [DE , NO ], ohne dass dies durch geeignete LaufzeitAusnahmebehandlungen behandelt wird. Damit haben diese Arbeiten Ähnlichkeit mit den oben erwähnten Arbeiten von Tip [Tip ] und deren Typ-Constraints, die ebenso das Typsystem von Java formalisieren. Im direkten Vergleich lesen sich – abgesehen von den unterschiedlichen Weisen der Notation – viele Regeln beinahe identisch, zum Beispiel eine solche, dass der Typ eines Konstruktoraufrufs der instanziierten Klasse entsprechen muss ([TKB ] und [NO ]). Ein direkter Abgleich aller Regeln eins-zu-eins fällt dennoch schwer, da bei Korrektheitsbeweisen für Java bisher stets nur Teilaspekte der Sprache berücksichtigt wurden [DE , NO , ON , Ohe , Sch , KN ]. So wurde zum Beispiel in [NO ] die Anzahl der Methodenparameter einer Methode auf einen einzigen beschränkt, die Typregeln von Tip et al. [Tip ] hingegen berücksichtigen auch Parameterlisten. Dennoch lassen sich bei der Formalisierung einige Gemeinsamkeiten erkennen, so wird in beiden Arbeiten der in Java nur implizit vorhandene Typ eines Ausdrucks explizit gemacht – auf der einen Seite mi els einer Constraintvariablen [TKB ], auf der anderen Seite durch zusä liche sogenannte type annotations [NO ]. 2.8. Refaktorisierungen als Graphtransformationen Refaktorisierungen sind Transformationen von Programmen. Gleichzeitig lassen sich Programme entsprechend der für sie geltenden Grammatiken gut als Graphen darstellen. Somit können Refaktorisierungen als Graphtransformationen betrachtet werden, eine Disziplin, in welcher bereits auf umfassende Forschungsergebnisse zurückgegriffen werden kann [Roz ]. Dieser Idee folgend haben Mens et al. Refaktorisierungen mi els Graphtransformationen beschrieben [MvEDJ , MDJ ]. Am Beispiel zweier Werkzeuge für E V . Literaturbetrachtungen respektive P U M machen sie ganz ähnlich zum constraintbasierten Ansa Bedingungen aus, die für die jeweilige Transformation erhalten werden müssen und geben Transformationsregeln für die jeweiligen Refaktorisierungen an. Interessant an diesem Ansa ist, dass mit dem Handwerkszeug der Graphtransformationen bereits auf viele Erkenntnisse zurückgegriffen werden kann, beispielsweise zur Analyse von Relationen zwischen verschiedenen Transformationen. So lassen sich beim Einsa von Vorschlagsgeneratoren für Refaktorisierungen (zum Beispiel [TR ]) die einzelnen Vorschläge zueinander in Bezug bringen und Aussagen ableiten, welche Vorschläge sich gegenseitig ausschließen oder voneinander unabhängig sind [MTR ]. Problematisch an der Arbeit von Mens et al. [MvEDJ , MDJ ] gestaltet sich indessen, dass sie auf einem recht abstrakten Niveau bleibt, insbesondere betrachtet sie keine Programmiersprache im Besonderen, sondern objektorientierte Sprachen im Allgemeinen, sodass viele Sprachkonstrukte – wie zum Beispiel innere Klassen in Java – keine Berücksichtigung finden [MvEDJ ]. Zwar lassen sich aus der von ihnen selbst als Machbarkeitsstudie [MvEDJ , MDJ ] bezeichneten Arbeit auch konkrete Werkzeuge für Java ableiten [FYBT ], doch es bleibt unklar, ob der Ansa auch erfolgreich ist, wenn es alle Eigenheiten einer konkreten Programmiersprache zu berücksichtigen gilt. 2.9. Testen von Refaktorisierungswerkzeugen Die formale Verifikation von Refaktorisierungswerkzeugen ist schwierig, wie in Abschni . thematisiert werden wird. Daher bietet es sich an, das korrekte Verhalten der Werkzeuge mittels Tests zu überprüfen. Neben handgeschriebenen Regressionstests, die speziell hierfür erdachte Programme refaktorisieren und die Ausgabe mit Erwartungswerten vergleichen, bieten sich ebenso automatisierte Tests an, die sich die spezifischen Eigenschaften von Refaktorisierungswerkzeugen zunu e machen. Eine erste Arbeit hierzu lieferten Dig et al. [DDGM ], die mit ASTGen ein Werkzeug zum automatischen Generieren von zu refaktorisierenden Probanden lieferten. Die Probanden ergeben sich dabei aus Kombinationen einzelner Teilprobanden, für die nach der Refaktorisierung einzuhaltende Bedingungen bekannt sind. So lässt sich mit begrenztem manuellen Einsa (der Spezifikation der Teilprobanden und der zu erhaltenden Bedingungen) durch viele Kombinationsmöglichkeiten dieser eine große Anzahl Testfälle generieren. Obwohl mit diesem Ansa eine beachtliche Zahl von Fehlern in verbreiteten Refaktorisierungswerkzeugen gefunden werden konnte [DDGM ], verbleibt unklar, ob diese auch bei tatsächlicher Verwendung der Werkzeuge auf realen Softwareprojekten Relevanz haben. An diesem Punkt se t die Idee des Refactoring Tool Testing (RTT) [TS , EH ] an. Ein solches Werkzeug nu t als Probanden reale, quelloffene Projekte und wendet auf diese systematisch Refaktorisierungen an. Es besteht im Wesentlichen aus drei Komponenten [TS , EH ]. Die erste ist eine zentrale Ablaufsteuerung, die die Probanden verwaltet und in einer Schleife über alle möglichen Anwendungsstellen des jeweils zu testenden Werkzeugs auf den Probanden iteriert und die dabei gemessenen Ergebnisse protokolliert. Wie diese Anwendungsstellen zu finden sind, ist in Abhängigkeit vom zu testenden Werkzeug zu entscheiden. Die hierfür erforderliche zweite Komponente – der Refaktorisierungs-Adapter [TS , EH ] – ist für jedes Werkzeug individuell zu entwickeln. Für einen gegebenen Probanden sucht dieser Adapter . . Einordnung der vorliegenden Arbeit alle möglichen Anwendungsstellen für das gegebene Werkzeug und berichtet entsprechende Belegungen der Eingabeparameter. Die dri e Komponente besteht aus einem Sa von Orakeln [TS , EH ], der zu beurteilen versucht, ob die durchgeführten Werkzeuganwendungen korrekt waren. Von wesentlicher Bedeutung ist dabei einerseits der Compiler, welcher nach der Refaktorisierung (für ein zuvor fehlerfrei kompilierendes Programm) keine Fehler ausgeben darf, andererseits können aber auch den Probanden beiliegende Regressionstests genu t werden, um auf geändertes Programmverhalten schließen zu können. Insgesamt wurde die Idee des Refactoring Tool Testing bereits bei der Entwicklung einer Vielzahl von Refaktorisierungswerkzeugen [Ste , KS , ST , Ikk , SKP , STST ] und auch einem weiteren Programmierwerkzeug [BFS ] genu t, wobei Gligoric et al. [GBO+ ] später die Technik mi els weiterer Orakel nochmals verfeinert haben. Ein Problem beider genannter Verfahren zum automatisierten Test von Refaktorisierungswerkzeugen ist, dass sie jeweils nur einseitig den Blick auf die ausführbaren Refaktorisierungen richten und nicht die wegen nicht erfüllter Vorbedingungen abgelehnten Refaktorisierungen hinterfragen. Soares et al. [SMG , SGM ] liefern mit ihrem Testwerkzeug eine Funktion, die Ergebnisse unterschiedlicher Implementierungen von Werkzeugen (beispielsweise aus verschiedenen Entwicklungsumgebungen) miteinander vergleicht und aus dabei ermi elten Diskrepanzen Rückschlüsse auf zu strenge Vorbedingungen zieht. 2.10. Einordnung der vorliegenden Arbeit Teile der in dieser Arbeit vorgestellten Forschungsergebnisse wurden bereits vorab publiziert. Umgekehrt bauten bereits Arbeiten sowohl mit, als auch ohne Beteiligung des Autors vorliegender Arbeit auf den in dieser Arbeit präsentierten Ergebnissen auf. Die folgenden beiden Unterabschni e listen diese Vor- und Folgearbeiten auf. 2.10.1. Vorarbeiten From Public to Private to Absent: Refactoring Java Programs under Constrained Accessibility [ST ]: Diese Veröffentlichung überträgt erstmals die Idee der constraintbasierten Refaktorisierungen auf eine Domäne abseits deklarierter Typen, nämlich auf Zugrei arkeiten. Für diese, als Variablen formuliert, wurde ein Sa von Constraintregeln abgeleitet, mit welchem für drei verschiedene Refaktorisierungswerkzeuge (M T ,P U M sowie C A ) erstmals die korrekte Anpassung von Zugrei arkeiten berechnet werden konnte. Die verwendeten Constraintregeln gehen dabei teilweise auch auf frühere Arbeiten zu nicht constraintbasierten Werkzeugen zurück [BGS ]. Die in der vorliegenden Arbeit genannten Constraintregeln zu Zugrei arkeiten in Kapitel . beinhalten zum Großteil die Constraintregeln aus [ST ]. RefaFlex: Safer Refactorings for Reflective Java Programs [TB ]: Diese Arbeit löst erstmalig das Problem, wie reflektives Programmverhalten in Java-Programmen bei Refaktorisierungswerkzeugen korrekt berücksichtigt werden kann. Da das Programmverhalten reflektiver Java-Anweisungen im schlimmsten Falle zum Zeitpunkt des Kompilierens unentscheidbar ist, kommt im Rahmen dieser Lösung eine Laufzeitanalyse zum Einsa , wie es schon bei . Literaturbetrachtungen Roberts [Rob ] der Fall war. Im Gegensa zu seiner Lösung für Smalltalk werden die zur Laufzeit gesammelten Informationen allerdings nicht zu dieser in Programmtransformationen umgese t. Sta dessen wird die Laufzeitanalyse vor der Transformation des Quellcodes vollständig abgeschlossen. Die dabei gesammelten Daten bilden die Grundlage für eine Constraintgenerierung gemäß einem entsprechenden Regelsa , die insgesamt fünf verschiedene Werkzeuge der Eclipse JDT constraintbasiert erweitert, dass auch reflektive Programme korrekt refaktorisieren werden können. Kapitel der vorliegenden Arbeit gibt große Teile dessen noch einmal wieder. A Comprehensive Approach to Naming and Accessibility in Refactoring Java Programs [STST ]: Während der Ansa des locking und unlocking von Schäfer bei der Referenzkonstruktion eine elegante Lösung darstellt, ist er ungeeignet, bei Refaktorisierungen solche Folgeänderungen zu unterstü en, die sich nicht nur lokal für die jeweils geänderte Anweisung auswirken (siehe Abschni . ). Umgekehrt ist eine Referenzkonstruktion mit dem constraintbasierten Ansa beinahe aussichtslos. In dieser Arbeit werden beide Ansä e miteinander verbunden, indem am Beispiel zweier Werkzeuge (P U M und E I ) einerseits mi els Zugrei arkeitsconstraints die hierfür notwendigen Anpassungen von Zugrei arkeitsmodifizierern berechnet sowie Zugrei arkeitsbedingungen geprüft wurden und andererseits mit dem Ansa nach Schäfer die Korrektheit der Namensbindungen sichergestellt wurde. Neben teilweiser Übernahme von Ergänzungen und Weiterentwicklungen verschiedener Constraintregeln schlagen sich die Ergebnisse dieser Vorarbeit vor allem in der Implementierung und Wahl der Constraintvariablen nieder, wobei nach diesen Erkenntnissen auf eine Referenzkonstruktion verzichtet werden konnte. Die Abschni e . . und . gehen hierauf jeweils ein. From behaviour preservation to behaviour modification: Constraint-based mutant generation [ST ]: Der constraintbasierte Ansa lässt sich nicht nur nu en, um Programmverhalten wie im Fall der Refaktorisierungswerkzeuge beizubehalten, sondern im Gegenteil auch gezielt zu verändern, wie es die Herausforderung beim Mutationstesten ist. Die Ideen zum in Abschni . beschriebenen constraintbasierten Mutationstesten wurden erstmalig in dieser Arbeit publiziert. 2.10.2. Folgearbeiten Constraint-Based Refactoring with Foresight [SP a]: Diese Arbeit, in welcher Steimann und v. Pilgrim das Problem der Vorausschau gelöst haben (siehe auch oben in Abschni . ), wurde anhand eines P U F -Werkzeugs evaluiert, bei dem große Teile der Implementierung – insbesondere der Refacola-Sprachdefinition für Java sowie Faktenextraktion – vom Autor der vorliegenden Arbeit beigesteuert wurden und direkte Vorläufer der in Kapitel , und vorgestellten Implementierung sind. Model/Code Co-Refactoring: An MDE Approach [PUTS ]: Vorstehende Arbeit betrachtet Co-Refaktorisierungen von Modellen und aus ihnen generiertem Java-Code. Die dabei zum Einsa gekommenen Constraintregeln für die drei betrachteten Co-Refaktorisierungswerkzeuge G D T ,P U und R sowie die Sprachdefinition in Re- . . Einordnung der vorliegenden Arbeit facola und die zugehörige Faktengenerierung entsprechen beinahe unverändert der in vorliegender Arbeit entwickelten. Es konnte somit direkt auf den Ergebnissen der vorliegenden Arbeit aufgebaut werden. 3. Constraintbasierte Refaktorisierungen Dieses Kapitel stellt das Konzept der constraintbasierten Refaktorisierung vor. Es werden die nötigen Grundlagen erläutert sowie die Eignung für die gegebene Aufgabenstellung begründet und alternative Konzepte genannt. 3.1. Eine formale Darstellung für Programmtransformationen Bevor sich der Frage gewidmet werden kann, wie Programmtransformationen unter bestimmten Randbedingungen berechnet werden können, ist zunächst eine geeignete Definition dessen zu liefern, was eine Programmtransformation eigentlich ist. Sei Σ das Alphabet der möglichen Eingabezeichen, aus denen ein Programm bestehen kann und Σ∗ die kleenesche Hülle über diesem Alphabet, also die Menge aller endlichen Zeichenke en, die sich mi els dieser Eingabezeichen bilden lässt. Auf unterster Ebene lässt sich eine Programmtransformation dann beschreiben als eine Funktion TΣ∗ , welche eine Zeichenkette s ∈ Σ∗ eines Eingabeprogramms auf eine Zeichenke e eines Ausgabeprogramms s′ ∈ Σ∗ abbildet:⁵ TΣ∗ : Σ∗ → Σ∗ 3.1.1. Transformationen von Syntaxbäumen Auch wenn ein Refaktorisierungswerkzeug le tendlich immer eine solche Abbildung zwischen Zeichenke en implementieren muss, bietet sich zur internen Repräsentation eine komfortablere Darstellung an, welche ausnu t, dass eine Programmtransformation nicht beliebige Zeichenke en aus Σ∗ transformieren soll, sondern nur Programme der Programmiersprache, welche einer entsprechenden Grammatik gehorchen. So lässt sich ein Programmtext mi els eines Parsers entsprechend der zugrundeliegenden Grammatik der Programmiersprache zunächst in einen konkreten und anschließend in einen abstrakten Syntaxbaum (engl. abstract syntax tree) [Wil ], auch abgekürzt als AST, transformieren. parse : Σ∗ → AST Abbildung . zeigt einen kurzen Programmausschni und einen zugehörigen abstrakten Syntaxbaum. Umgekehrt zum Parsen des Eingabeprogramms liefert ein Tiefendurchlauf über einen abstrakten Syntaxbaum und ein Berichten der Knotenwerte wieder das originale Programm, indem zu jedem Knoten der von ihm repräsentierte Quelltext ausgegeben wird:⁶ parse−1 : AST → Σ∗ ⁵ Dass bestimmte Änderungen an Programmen auch Änderungen an Dateinamen und Verzeichnispfaden erfordern können (zum Beispiel ein Verschieben von Typen in Java), bleibe hier der Einfachheit halber unberücksichtigt. ⁶ Eine solche Umkehrfunktion kann nur funktionieren, soweit im abstrakten Syntaxbaum alle Informationen des Ursprungsprogramms enthalten sind, was aber in der hier gezeigten Formalisierung nicht für überschüssige . Constraintbasierte Refaktorisierungen . … v1 :VarDeclStatement v2 :ModifierList 'public' public String s,t; (i) v3 :Type v5 :VarDeclFragment v7 :VarDeclFragment v4 :SimpleName 'String' v6 :SimpleName 's' v8 :SimpleName 't' (ii) Abbildung . .: Eine Variablendeklaration (i) und ein zugehöriger abstrakter Syntaxbaum (ii). Zusä liche Knoten für das Komma und Semikolon wurden beim Übergang vom konkreten zum abstrakten Syntaxbaum bereits eliminiert. Somit kann eine Programmtransformation auch als Graphtransformation [Hec ] TAST des abstrakten Syntaxbaumes ausgedrückt werden, TAST : AST → AST wobei eine Hintereinanderausführung von Generierung des abstrakten Syntaxbaumes, Transformation des Graphen und Zurückschreiben des Quellcodes erneut einer Programmtransformation entspricht: parse T parse−1 AST −→ AST −−−−−→ Σ∗ Σ∗ −−−→ AST −− Ein wesentlicher Vorteil dieser Transformationske e ist, dass schon allein durch die zwischenzeitliche Darstellung des Ausgabeprogramms als abstrakter Syntaxbaum die Menge der möglichen Programmtransformationen auf genau solche eingeschränkt wird, die den syntaktischen Regeln der Programmiersprache genügen, solange auch der Baum eine gültige Form wahrt und nicht etwa dort Kindknoten eingehangen werden, wo es die Grammatik der Programmiersprache verbietet. Weiterhin vereinfacht eine Baumdarstellung die Beschreibung typischerweise von Refaktorisierungswerkzeugen durchgeführten Transformationen. So entspricht das Verschieben einer Deklaration dem Umhängen eines Teilbaums, das Einfügen von Deklarationen dem Einfügen neuer Knoten sowie das Löschen von Ausdrücken dem Entfernen von Knoten aus dem Baum. Tro dieser Vorteile gehen mit der Darstellung von Programmtransformationen als Graphtransformationen wesentliche Nachteile einher, die allesamt daher rühren, dass Grammatiken für Programmiersprachen zum Zweck des Compilerbaus entworfen werden und andere Anforderungen erfüllen, als sie von Refaktorisierungswerkzeugen gestellt werden. Mit der syntaktischen Analyse steht die Erstellung eines abstrakten Syntaxbaumes ganz am Anfang Leerräume (engl. white spaces) oder Kommentare gilt. Während beides in dieser Arbeit auch nicht das Thema sein soll, kann sich bei den Leerräumen dennoch mi els allgemeiner Formatierungsregeln beholfen werden, wohingegen der sinnvolle Umgang mit Programmkommentaren Sonderbehandlungen erfordert, die für diese Arbeit in eingeschränktem Maße schon durch den Mechanismus des ASTRewrite der JDT [JDT ] bereitgestellt werden. . . Eine formale Darstellung für Programmtransformationen des Kompiliervorganges. Viele Programmeigenschaften, die für Refaktorisierungen von Belang sind, ergeben sich aber erst in späteren Kompilierschri en. So werden durch den JavaCompiler zum Beispiel default-Konstruktoren für Klassen angelegt, für die im abstrakten Syntaxbaum keine anderen Konstruktoren definiert sind, Zugrei arkeitsmodifizierer werden für Interface-Member und Enum-Konstruktoren im Nachhinein eingefügt, wo sie nicht explizit angegeben sind und Aufrufe von Superkonstruktoren werden nachgetragen, wo diese nicht vorgegeben sind. In den genannten drei Fällen ließe sich ein Eingabeprogramm noch entsprechend zu einem angereicherten abstrakten Syntaxbaum transformieren, um die impliziten Elemente explizit zu machen. Schwieriger wird es bei Programmeigenschaften, die üblicherweise von einem Compiler berechnet werden, die für Refaktorisierungswerkzeuge Relevanz haben und für welche die Grammatik der Programmiersprache keine Knotentypen im abstrakten Syntaxbaum vorsieht. Zu nennen wären hier zum Beispiel Bindungen von Referenzen an Deklarationen oder inferierte Typen von Ausdrücken. Da sich während einer Refaktorisierung diese Eigenschaften ebenso wie das Eingabeprogramm ändern können, wäre es wünschenswert, beide in einer einheitlichen Datenstruktur vorzuhalten, auf der die Refaktorisierungswerkzeuge arbeiten können. Alternative Ansä e wären hierfür eine weitere Anreicherung des abstrakten Syntaxbaumes teilweise hin zu Syntaxgraphen [MDJ , MvEDJ , Sch ]. Bei constraintbasierten Refaktorisierungen wird hingegen von der Darstellung als Baum beziehungsweise Graph abgerückt und sta dessen das zu refaktorisierende Programm im Wesentlichen in Form von Variablen und ihnen zugewiesenen Werten dargestellt. 3.1.2. Transformationen als Variablenzuweisungen Für ein zu refaktorisierendes Programm beschreibe V = (v1 , v2 , . . . , vn ) die Variablen vi , der durch das jeweilige Refaktorisierungswerkzeug änderbaren Programmeigenschaften. Jede Variable vi habe dabei einen initialen Wert (engl. initial value) viinit , der dem initialen Zustand der Variable vi vor der Transformation des Programms entspricht, sodass sich zusä lich die Folge ( ) V init = v1init , v2init , . . . , vninit der initialen Werte ergibt. Die konkreten Berechnungsvorschriften, nach denen aus einem Programm P (beziehungsweise dessen abstraktem Syntaxgraph) die Elemente von V und V init erzeugt werden können, richten sich jeweils danach, welche Programmeigenschaften durch das spätere Refaktorisierungswerkzeug änderbar sein sollen, also was für ein Refaktorisierungswerkzeug konkret zu implementieren ist. Formal ähneln die Variablen stark den A ributen a ributierter Grammatiken [Knu ]. Mi els dieser werden Symbolen einer Grammatik (also im Endeffekt den Knotentypen eines abstrakten Syntaxbaumes) Eigenschaften zugeordnet, die konkrete Werte annehmen können und die sich wiederum aus einer Menge von Produktionsregeln ergeben [Knu ]. Somit lassen sich für eine formale Darstellung der Variablengenerierung und ihrer initialen Werte a ributierte Grammatiken heranziehen. Die Unterschiede ergeben sich erst bei der späteren Verwendung. Während A ribute im Rahmen des Compilerbaus Verwendung finden, um das Programm mit (unveränderlichen) Teilergebnissen des Kompiliervorganges anzureichern, bilden die Variablen während einer Refaktorisierung veränderliche Programmeigenschaften ab. Ändert sich der Wert einer Variable, entspricht dies . Constraintbasierte Refaktorisierungen einer lokalen Änderung des Programmtextes (oder analog einer Transformation des abstrakten Syntaxbaumes). Der Ort der Quellcodeänderung ergibt sich dann aus dem Knoten des abstrakten Syntaxbaumes, dem die Variable zugeordnet ist. Für eine n-elementige Folge von Variablen V sei eine Programmtransformation T nun definiert als eine Menge von Zuweisungen T = {vi := valT,i | vi ∈ V } bei der jeder Programmeigenschaft vi des Eingabeprogramms genau ein Wert valT,i zugewiesen wird. In der Praxis nimmt ein Refaktorisierungswerkzeug zumeist nur Einfluss auf geringe Teile des Eingabeprogramms [MHPB ]. Es liegt also nahe, dass für die sich ergebenden Programmtransformationen die Gleichung valT,i = viinit für viele i gilt. Im Folgenden werden solche Zuweisungen bei Angabe von Programmtransformationen der Einfachheit halber ausgelassen: { } T = vi := valT,i | vi ∈ V ∧ valT,i ̸= viinit Die Darstellung von Programmtransformationen als Mengen von Zuweisungen ermöglicht, Mengenoperationen direkt auf Programmtransformationen übertragen zu können. Ist eine Transformation T die leere Menge, handelt es sich um eine Nicht-Transformation des Programmtextes, denn alle Zuweisungen an Variablenwerten entsprechen dem initialen Zustand des Programms. Weiterhin lässt sich sagen, dass eine Transformation T in einer anderen Transformation T ′ enthalten ist, wenn T eine Teilmenge von T ′ ist. Ebenso können sich Transformationen überdecken sowie disjunkt oder widersprüchlich sein. Le teres wäre der Fall für zwei Transformationen T und T ′ mit vx := valT,x ∈ T , vx := valT ′ ,x ∈ T ′ und valT,x ̸= valT ′ ,x . Zuordnungen von Variablen an Programmelemente Um – gerade bei einer großen Anzahl von Variablen – eine sinnvolle Strukturierung zu schaffen, werden Variablen in zwei Dimensionen geordnet. Einerseits wird jede Variable einem konkreten Programmelement (engl. program element) p ∈ P zugeordnet. Die Menge P der Programmelemente umfasst dabei alle im Programm enthaltenen Deklarationen und Referenzen, Ausdrücke und Anweisungen. Damit geht die Menge P der Programmelemente noch über die reine Menge der Knoten im abstrakten Syntaxbaum hinaus, denn nicht jedem Programmelement lässt sich eine konkrete Quellcodeposition zuordnen, so können auch Programmelemente für implizite Deklarationen wie default-Konstruktoren ( § . . ) existieren, die nur in einem angereicherten abstrakten Syntaxbaum zu finden wären. In zweiter Dimension werden Variablen verschiedenen Variablenarten v ∗ ∈ V ∗ zugeordnet. So gibt es zum Beispiel Variablen, die einen Zugrei arkeitsmodifizierer, einen Namen oder den deklarierten Typen einer Deklaration repräsentieren. Die Menge der Variablen v ∈ V wird somit Partioniert durch das Kreuzprodukt der Mengen der Programmelemente und Variablenarten P × V ∗ . Da jede Variable einem Programmelement zugeordnet ist, lässt sich ebenso eine umgekehrte Zuordnung finden, welche zu einem Programmelement die jeweiligen Variablen benennt. Darüber hinaus lässt sich sogar eine Abbildung finden, die für ein konkretes Programmelement die Arten der für sie zu generierenden Variablen benennt: applyingKinds : P → 2V ∗ . . Eine formale Darstellung für Programmtransformationen ∗ Diese bildet die Menge der Programmelemente P auf die Potenzmenge 2V der Variablenarten ab. In der Praxis ergeben sich die hierfür nötigen Abbildungsvorschriften aus der Art des jeweils betrachteten Programmelements. So lässt sich bereits aus der Art eines Programmelements ableiten, welche Variablenarten für dieses infrage kommen. Aus der Tatsache, dass ein Programmelement eine lokale Variable repräsentiert, ergibt sich beispielsweise, dass dieses Variablen für Namen, umschließende Deklaration oder deklarierten Typen – nicht aber für Zugrei arkeiten – hat. Benennung von Variablen Wie gezeigt lässt sich also jede Variable v ∈ V mi els eines Tupels (p, v ∗ ) ∈ (P × V ∗ ) und v ∗ ∈ applyingKinds(p) beschreiben. Frühe Arbeiten zu constraintbasierter Programmanalyse haben als Schreibweise für die Variablenarten v ∗ ∈ V ∗ verschiedenartige Klammerungen genu t, so zum Beispiel [[f ]] oder [f ] um den deklarierten Typen [PS , TKB ] und ⟨f ⟩ um die Zugrei arkeit des Feldes f zu referenzieren [ST ]. Abgesehen von der unintuitiven Darstellung verbietet sich eine solche Notation spätestens mit Einführung einer größeren Menge von Variablenarten, welche die Menge verfügbarer Klammersymbole bald erschöpft. Andere Publikationen wählten eine Schreibweise mit einer funktionsähnlichen Notation declaredT ype(f ) beziehungsweise accessibility(f ) [TB ] oder einer an die Objektorientierung angelehnten Punktnotation, mit teilweise durch griechische Buchstaben kodierten Variablenarten f.τ beziehungsweise f.α [SKP , SP a]. In dieser Arbeit findet le tgenannte Schreibweise Verwendung, wobei die Variablenarten ausgeschrieben werden, zum Beispiel f.accessibility und f.declaredType. Dies geschieht, um Konsistenz zur Implementierung zu schaffen, deren zugrundeliegende Sprache – die Refacola [SKP ] – ebenso eine ausgeschriebene Punktnotation benu t. Domänen Der Wert, den eine Variable v ∈ V annehmen darf, ist nicht beliebig. Vielmehr existiert zu jeder Variablenart v ∗ ∈ V ∗ eine zugehörige Domäne (engl. domain) Dv∗ , welche die möglichen Werte für eine Variable v dieser Variablenart beschreibt. Es lassen sich dabei insgesamt drei Arten von Domänen unterscheiden: Aufzählungsdomänen, programmabhängige Domänen und offene Domänen. Aufzählungsdomänen (engl. enumeration domain) [SP a] haben einen endlichen Wertebereich unabhängig vom konkreten Transformationsproblem. Ihre Wertebereiche ergeben sich zumeist aus der Sprachspezifikation der zugrundeliegenden Programmiersprache. In Java ist zum Beispiel die Menge der Zugrei arkeiten eine Aufzählungsdomäne, nämlich genau die Menge {public, protected, default, private}. Aufzählungsdomänen können geordnet sein, was für die Zugrei arkeiten in Java zutrifft, denn in der gegebenen Auflistung der Zugrei arkeiten schließt der von jedem genannten Zugrei arkeitsmodifizierer erzeugte Zugrei arkeitsbereich den vom danach genannten Modifizierer erzeugten Zugrei arkeitsbereich ein. Ein Beispiel für eine partiell geordnete Aufzählungsdomäne liefern die Zugrei arkeiten in C#, bei denen zwar public-Zugrei arkeit alle weiteren Zugrei arkeitsbereiche einschließt, aber weder die internal-Zugrei arkeit die protected-Zugrei arkeit umfasst, noch der umgekehrte Fall gilt [HWG , § . . ]. Beispiele für vollständig ungeordnete Domänen finden sich zumeist in den zweielementigen Mengen, zum Beispiel der Domäne einer Variablen, welche die An- oder Abwesenheit eines static-Modifizierers in Java repräsentiert. Zwar ist es technisch möglich, einen solchen Booleschen Wertebereich zu ordnen, es ergibt aber keinen Sinn, . Constraintbasierte Refaktorisierungen denn Ordnungen finden Anwendung, um später mi els Operatoren wie < oder ≤ vereinfacht Bedingungen auszudrücken, bei denen für Boolesche Domänen auch der Gleichheitsund Ungleichheitsoperator genügen. Domänen, deren Wertebereich zwar endlich ist, sich aber erst aus dem konkreten Transformationsproblem ergibt, sind sogenannte programmabhängige Domänen (engl. program-dependent domain) [SKP ] (beziehungsweise bei [Pal ] abstract domain). Als Beispiel sei eine Variablenart genannt, die den deklarierten Typen eines Feldes beschreibt. Die Werte, die eine solche Variable annehmen kann, ergeben sich erst aus dem zu transformierenden Programm, denn mit jedem weiteren deklarierten Typen des Programms steigt potentiell auch die Anzahl möglicher Typannotationen des Feldes. Programmabhängige Domänen ermöglichen Variablenwerte, die ihrerseits eigene Variablen definieren. Angenommen, für einen deklarierten Typen d sei eine Variable d.identifier definiert, die seinen Namen beschreibt. Weiterhin sei f ein Feld mit einer Variablen f.declaredType. Dann bezeichnet f.declaredType.identifier die Variable, die den Namen des deklarierten Typen beschreibt. Abhängig davon, welchen Wert f.declaredType zugewiesen bekommt, ändert sich entsprechend die mi els dieser Indirektion (engl. indirection) [SKP ] beschriebene identifierVariable. In die dri e Kategorie möglicher Domänen fallen die offenen Domänen. Ein typischer Vertreter ist die Menge gültiger Zeichenke en, welche als Namen von Deklarationen dienen dürfen. Auch wenn in der Theorie unendlich viele mögliche Werte existieren, lassen sich diese Mengen in der Praxis häufig auf endliche Wertebereiche reduzieren. Im Falle der Namen würde man für ein konkretes Transformationsproblem die Domäne einschränken auf alle derzeit im Programm verwendeten Namen nebst einiger weniger Elemente, welche im Programm bisher unbenu te, frische Namen repräsentieren. Notwendige Bedingung hierbei ist dann natürlich, dass die Menge vom Refaktorisierungswerkzeug neu eingebrachter Namen gering ist, was in der Praxis zutrifft [MHPB ]. Generierung von Programmelementen und Variablen Es lässt sich leicht einsehen, dass zu einem gegebenen Eingabeprogramm und einer Menge von Variablenarten V ∗ Vorschriften (zum Beispiel in Form a ributierter Grammatiken [Knu ]) zur Berechnung gefunden werden können, die aus beidem eine Menge von Programmelementen P ∈ P und Folgen der Variablen V ∈ V und ihrer initialen Werte V init ∈ Vinit erzeugen: vargenV ∗ : AST → P × V × Vinit In der Praxis wird bei dieser Variablengenerierung zunächst der abstrakte Syntaxbaum nach allen Programmelementen abgesucht, woraus eine Menge P resultiert. Anschließend wird für jedes Programmelement p ∈ P anhand oben beschriebener Abbildung applyingKinds geprüft, welche v ∗ ∈ V ∗ existieren, um aus diesen zu jedem derart ermi elten Tupel (p, v ∗ ) eine Variable v zu V hinzuzufügen. Die initialen Werte der Variablen ergeben sich dann direkt aus dem Quellcode. 3.2. Programmtransformationen als Bedingungserfüllungsproblem Der vorhergehende Abschni hat erläutert, wie Programmtransformationen mi els Variablen formal dargestellt werden können. Dieser Abschni beschreibt den eigentlichen Kern con- . . Programmtransformationen als Bedingungserfüllungsproblem straintbasierter Refaktorisierungen, nämlich, wie Programmtransformationen berechnet werden können, die die Intention einer bestimmten Refaktorisierungsanwendung umse en und dabei Invarianten zur Beibehaltung des Programmverhaltens erhalten. Formal lässt sich (nach [SP ]) eine solche Programmtransformation T wie folgt als HoareTripel [Hoa ] {Pre} T {Post} mit Vorbedingung Pre und Nachbedingung Post beschreiben. Bezeichne Int die Intention des Refaktorisierungswerkzeugs und Inv die zu erhaltenden Invarianten. Während die Intention des Refaktorisierungswerkzeugs (im allgemeinen Fall) nur nach der Transformation erfüllt ist, sollen die Invarianten sowohl vor als auch nach der Transformation gelten [SP ]: {Inv} T {Inv ∧ Int} Intention eines Refaktorisierungswerkzeugs Wie jedes Werkzeug erfüllen insbesondere Refaktorisierungswerkzeuge einen konkreten Zweck. Dieser ergibt sich einerseits aus der Art des Werkzeugs, also dem Muster, nach dem refaktorisiert werden soll, und andererseits aus mitunter möglichen Konfigurationen, typischerweise als Benu ereingaben. Mit einer gegebenen Menge von Variablen V kann im einfachsten Fall die Intention einer Refaktorisierung direkt als eine Menge von Variablenzuweisungen Int = {vi := valT,i | vi ∈ V } ausgedrückt werden. Für ein Refaktorisierungswerkzeug zum Umbenennen von Methoden ergibt sich beispielsweise aus der Art des Werkzeugs, dass einer identifier-Variable ein neuer Wert zugewiesen werden soll. Die konkrete Variable sowie der zuzuweisende Wert ergeben sich aus der Konfiguration, für die der Benu er eine konkrete Methode sowie einen neuen Namen für diese auswählt. Die le tendlich durchzuführende Programmtransformation – also gültige Refaktorisierung – muss mindestens der Intention genügen, also eine Obermenge der vorgegebenen Variablenzuweisungen sein. Mitunter ist die Intention eines Refaktorisierungswerkzeugs aber auch auf einer semantisch höheren Abstraktionsebene gegeben, was an einem (in diesem Abschni durchgängigen) Beispiel erläutert werden soll. Es sei folgendes Programm gegeben: 101 102 103 104 package a; class A { void m() { /* Methode m1 */ } } In diesem soll im Rahmen einer A S -Refaktorisierung [Fow ] in einem neuen Paket b eine Subklasse erzeugt werden, welche die Methode m überschreibt. Ein naiver Ansa würde den Programmtext der Klasse A kopieren, den Paketnamen und den Klassennamen anpassen und gegebenenfalls aus der Superklasse überschriebene Methoden mit einer @OverrideAnnotation kennzeichnen. Für das obige Beispiel käme es nach diesem Vorgehen aber zu nicht kompilierendem Code: 105 106 107 package b; class B extends a.A { @Override . Constraintbasierte Refaktorisierungen 108 109 } void ::: m() { /* Methode m2 */ } In Zeile kommt es zu einem Kompilierfehler, weil die Methode m2 entgegen der angegebenen @Override-Annotation keine Methode der Superklassen überschreibt. Um innerhalb eines fremden Pakets überschrieben werden zu können, müsste sowohl die Methode m in A, als auch in der Folge dann die Methode m in B protected-zugrei ar sein. Die Intention bei der gezeigten Refaktorisierung geht also über einzelne, lokale Änderungen hinaus. Sie beschreibt einen komplexeren semantischen Zusammenhang, nämlich, dass die Methode m2 die Methode m1 überschreiben muss. Schäfer [Sch ] fasst solche semantischen Informationen unter dem Begriff Abhängigkeiten zusammen: “The dependency framework allows us to abstract away from many of the gory details of name binding or control and data flow, which are handled transparently.” Er unterscheidet verschiedene Arten der Abhängigkeit wie Bindungen (name binding dependencies), Überschreibungen (overriding dependencies) und Kontroll- und Datenflussbeziehungen (flow dependencies). Abhängigkeiten lassen sich als (meist zweistelliges) Tupel von Programmelementen ausdrücken. Sei Method ⊂ P die Teilmenge der Programmelemente, die in einem Java-Programm Methoden repräsentieren. Damit lassen sich Überschreibungsbeziehungen zwischen Methoden beschreiben als ein zweistelliges Tupel von Programmelementen: overrides ⊆ Method × Method Analog lassen sich auch Bindungen zwischen deklarierten Entitäten Entity ⊂ P und Referenzen Reference ⊂ P als eine zweistelliges Tupel von Programmelementen beschreiben: binds ⊆ Reference × Entity In dieser Darstellung bei [SKP ] erstmalig verwendet heißen die Mengen dieser Tupel Programmfakten oder – in Anlehnung an das gleichnamige Konzept der logischen Programmierung [BG ] – einfach Fakten (engl. facts). Die einzelnen Elemente einer Faktenmenge werden – in gleicher Anlehnung – durch ein Tupel mit vorangestellter Faktenmenge beschrieben, so zum Beispiel binds(r, d) ∈ binds. Um zum obigen Beispiel der A S -Refaktorisierung zurückzukehren, ließe sich die Intention der Werkzeuganwendung als Int = overrides(m2 , m1 ) formulieren. Somit ergibt sich folgendes Hoare-Tripel: {Inv} T {Inv ∧ overrides(m2 , m1 )} Erfüllung der Invarianten Es bleibt zu klären, was die Invarianten Inv eines Refaktorisierungswerkzeugs konkret sind. In erster Linie sollte ein Refaktorisierungswerkzeug ein gültiges . . Programmtransformationen als Bedingungserfüllungsproblem (engl. well-formed) Programm erzeugen, also eines, welches den syntaktischen und semantischen Regeln der Programmiersprache genügt. Für ein zuvor fehlerfrei kompilierbares Programm muss gelten, dass es auch nach angewendeter Programmtransformation keine Kompilierfehler aufweist. Anderenfalls sollte es zumindest die Abhängigkeiten beibehalten, die vor der Programmtransformation fehlerfrei aufgelöst werden konnten. Ebenso wie die Intention eines Refaktorisierungswerkzeugs können auch die Invarianten mi els Abhängigkeiten ausgedrückt werden. Das Beibehalten von Beziehungen wie Bindung, Überschreibung, Kontroll- und Datenfluss ist wesentlicher Bestandteil bedeutungserhaltender Programmtransformation. Für das laufende Beispiel sei angenommen, dass Überschreibungsbeziehungen die einzigen relevanten Abhängigkeiten seien (tatsächlich greifen noch eine Menge weiterer Abhängigkeiten, Kapitel widmet sich diesen ausführlich). Es ergibt sich dann folgendes Hoare-Tripel {overrides = O} T {overrides = O ∪ {overrides (m2 , m1 )}} in welchem overrides-Abhängigkeiten als Vor- und Nachbedingung auftreten. Aus deklarativer Sicht bedeuten dabei die overrides-Abhängigkeiten O in Vor- und Nachbedingung dasselbe, was sich auch in der identischen Notation niederschlägt. Operational ergibt sich hingegen ein gewichtiger Unterschied: Während O in der Vorbedingung als eine Abfrage an das Programm beziehungsweise die daraus resultierende Faktenmenge zu verstehen ist, so ist die overrides-Abhängigkeit in der Nachbedingung als eine zu erfüllende Zusicherung zu verstehen [SP ]. Programmabfragen Eine konkrete Umse ung der sich aus der Vorbedingung ergebenden Programmabfragen ist unkompliziert. Unter welchen Bedingungen ein Fakt jeweils vorliegt, ergibt sich aus der Sprachspezifikation des Eingabeprogramms. Für Java und die oben genannten Fakten binds und overrides sind dies konkret die statischen Binde- ( § ) und Überschreibungsregeln ( § . . . ) der Sprache. Programmabfragen erfordern also lediglich eine Anpassung der Funktion vargen aus Abschni . . . Neben dem Eingabeprogramm und den Variablenarten sei nun ebenso die Menge F ∗ von Faktenarten gegeben, die innerhalb der Vorbedingung eines Hoare-Tripels auftreten können. Zusä lich erhält man dann als Ausgabe eine Menge F ∈ F von Programmfakten. factgenV ∗ , F ∗ : AST → P × V × Vinit × F Ein Quadrupel (P, V, V init , F ) nennen wir Faktenbasis (engl. fact base), der Prozess des Berechnens der Faktenbasis heißt Faktengenerierung (engl. fact generation). Eine konkrete Anfrage an eine Faktenbasis, ob eine Menge von Programmelementen existiert, die eine bestimmte Menge von Fakten erfüllt, heißt Programmabfrage (engl. query). Diesen Abfragen wird dabei stets die Annahme zur Weltabgeschlossenheit (engl. closed world assumption) zugrunde gelegt. Ist ein Fakt in der Faktenbasis nicht enthalten, besteht auch eine entsprechende Abhängigkeit im Programm nicht. Zurückkehrend zum laufenden Beispiel würde eine Faktenbasis des Eingabeprogramms für F ∗ = {binds} mangels jeglicher Überschreibungen keine binds-Fakten beinhalten. Die Menge O wäre also die leere Menge. {overrides = ∅} T {overrides = {overrides (m2 , m1 )}} . Constraintbasierte Refaktorisierungen Würde man ein erweitertes Beispiel mit zusä lichen Methodenüberschreibungen betrachten, würde innerhalb der Nachbedingung die Menge der zu erfüllenden Fakten ansteigen. Für das laufende Beispiel soll ein einzelnes Fakt aber genügen. Faktenerfüllung Die Zusicherung einer bestimmten Faktenmenge innerhalb der Nachbedingung des Hoare-Tripels ist sozusagen das Gegenstück zur Faktengenerierung. Zu einem gegebenen Fakt müssen die Bedingungen zugesichert werden, unter welchen das Fakt erfüllt ist. Da diese Bedingungen le ten Endes in einer Programmtransformation Manifestation finden sollen, sollte dies in Form von Variablenzuweisungen geschehen. Eine Überschreibung der Methoden m1 und m2 lässt sich dabei wie folgt ausdrücken:⁷ overrides(m2 , m1 ) ⇔ m1 .identifier = m2 .identifier ∧ m1 .parametercount = m2 .parametercount ∧ ∀i, 1 ≤ i ≤ m1 .parametercount : m1 .parameteri .type = m2 .parameteri .type ∧ m2 .hos ype ∈ m1 .hos ype.supertypes ∧ (m1 .accessiblity ≥ protected ∨ (m1 .accessiblity = default ∧ m1 .hostpackage = m2 .hostpackage) Die Überschreibung der Methoden m1 und m2 zuzusichern ist notwendige Bedingung, um die Nachbedingung des obigen Hoare-Tripels zu erfüllen. Allerdings ist dies noch nicht hinreichend, denn es wird nicht nur verlangt, dass overrides (m2 , m1 ) ∈ overrides sondern als Resultat der Invariante auch, dass keine weiteren Überschreibungen gelten (da im Ausgangsprogramm keine weiteren Überschreibungen vorhanden waren): overrides = {overrides (m2 , m1 )} Entsprechend muss (wegen der Annahme zur Weltabgeschlossenheit) für alle weiteren Paare von Methoden mi , mj , i ̸= j gelten, dass diese nicht überschreiben, also die entsprechend negierte Version obiger Variablenzuweisungen gegeben sind. Für jede weitere Methode im Programm – das kann zum Beispiel die von Object geerbte Methode finalize() sein – muss innerhalb der Nachbedingung des Hoare-Tripels ebenso die negierte Version obigen Ausdrucks gelten. Geht man – neben m1 und m2 – von nur einer weiteren solchen Methode m3 aus, ergibt dies vier weitere Konjunktoren – jeweils um zu verhindern, dass m1 oder m2 die Methode m3 überschreiben oder umgekehrt m1 oder m2 von m3 überschrieben wird. Insgesamt ergibt sich das folgende Hoare-Tripel, wobei zwecks kürzerer Darstellung bei den le tgenannten Prüfungen auf unerwünschte Methodenüberschreibung nur der erste der vier Fälle aufgeführt ist. ⁷ Darüber, dass sich seit Java die Situation aufgrund möglicher Typparameter in Methodensignaturen sowie Autoboxing tatsächlich noch ein wenig verkompliziert hat, sei an dieser Stelle (und bis Kapitel ) noch hinweggesehen. Dass hingegen in Java Methodenüberschreibung unabhängig von den Rückgabetypen der Methoden definiert ist, entspricht der Java-Sprachspezifikation ( § . . . ). . . Programmtransformationen als Bedingungserfüllungsproblem { } T { m1 .identifier = m2 .identifier ∧ m1 .parametercount = m2 .parametercount ∧ ∀i, 1 ≤ i ≤ m1 .parametercount : m1 .parameteri .type = m2 .parameteri .type ∧ m2 .hos ype ∈ m1 .hos ype.supertypes ∧ (m1 .accessiblity ≥ protected ∨ (m1 .accessiblity = default ∧ m1 .hostpackage = m2 .hostpackage) ∧ ( m1 .identifier ̸= m3 .identifier ∨ m1 .parametercount ̸= m3 .parametercount ∨ ∃i, 1 ≤ i ≤ m1 .parametercount : m1 .parameteri .type ̸= m3 .parameteri .type ∨ m3 .hos ype ∈ / m1 .hos ype.supertypes ∨ ¬(m1 .accessiblity ≥ protected ∨ (m1 .accessiblity = default ∧ m1 .hostpackage = m3 .hostpackage)) ... } Obwohl augenscheinlich das Ziel – die Anweisungen für eine konkrete Programmtransformation – mit der Darstellung der Nachbedingung als Variablenzuweisungen zum Greifen nahe ist, verbleibt dennoch ein weiter Weg. Die Nachbedingungen beinhalten nämlich noch keineswegs konkrete Handlungsanweisungen, sondern nur – wie der Name schon aussagt – dabei zu berücksichtigende Bedingungen. Bedingungserfüllung Glücklicherweise sind Systeme von Bedingungen, wie sie sich aus den Nachbedingungen der Programmtransformation dargestellt durch Variablenzuweisungen ergeben, gut untersucht. So ist ein Bedingungserfüllungsproblem (engl. constraint satisfaction problem) [Tsa ] oder kurz Constraintsystem oder CSP definiert als ein Tripel ⟨Z, D, C⟩ Darin ist Z eine Menge von Constraintvariablen (engl. constraint variable), D eine Funktion, welche die Elemente aus Z auf ihre jeweilige Domäne abbildet und C eine Menge von Bedingungen (engl. Constraint). Eine Lösung (oder auch Constraintlösung) eines Bedingungserfüllungsproblems ist definiert als eine Menge von Variablenzuweisungen, die alle Constraints gleichzeitig erfüllt [Tsa ]. Im Allgemeinen können Bedingungserfüllungsprobleme abhängig von den gegebenen Domänen und Constraints beliebig viele Lösungen haben. Existiert keine Lösung, nennt man ein Bedingungserfüllungsproblem inkonsistent oder auch unlösbar. Die Nachbedingungen, wie sie sich aus den Hoare-Tripeln ergeben haben, lassen sich damit formulieren als ein Bedingungserfüllungsproblem CSP = ⟨V, DV ∗ , R⟩ . Constraintbasierte Refaktorisierungen wobei V der oben definierten Menge der Variablen entspricht,⁸ DV ∗ einer Funktion, die jeder Variablenart v ∗ ∈ V ∗ jeweils ihre Domäne Dv∗ zuordnet (siehe Abschni . . ) und R die oben beschriebenen und zu erfüllenden Nachbedingungen der Programmtransformation beschreibt. Bedingungserfüllungsprobleme, deren Variablen ausschließlich endliche Domänen haben, werden endliche Bedingungserfüllungsprobleme (engl. finite constraint satisfaction problem) [Tsa ] genannt. Wie in Abschni . . auf Seite ausgeführt, sind die Aufzählungsdomänen ebenso wie die programmabhängigen Domänen endlich und die offenen Domänen lassen sich ohne funktionale Einschränkungen durch endliche Domänen erse en. V ∗ enthält also ausschließlich endliche Domänen. Insbesondere ist das erhaltene Bedingungserfüllungsproblem CSP damit endlich. In der Literatur wird eine Vielzahl von Verfahren beschrieben, um zu einem endlichen Bedingungserfüllungsproblem Aussagen zur Inkonsistenz oder Menge der Lösungen zu machen [Kum ]. Wendet man einen Constraintlöser (engl. constraint solver) auf CSP an, beinhaltet seine Ausgabe auch gleichzeitig eine Lösung, wie eine der Intention und den Invarianten des Refaktorisierungswerkzeugs genügende Programmtransformation durchzuführen ist. Im Falle eines inkonsistenten Constraintsystems kann die Transformation wegen sich widersprechender Invarianten und Intentionen nicht durchgeführt werden. Im Falle einer Lösung des Constraintsystems besteht die Ausgabe des Constraintlösers in der Zuweisung von Werten an Variablen, die alle Bedingungen erfüllen. Mit anderen Worten erhält man eine Menge von von Variablenzuweisungen, die sowohl Obermenge der Invarianten als auch Obermenge der Intentionen des Refaktorisierungswerkzeugs ist, also beiden genügt. Für das laufende Beispiel der A S -Refaktorisierung der zu überschreibenden Methode m1 ist das Constraintsystem, das sich aus der Nachbedingung des Hoare-Tripels ergibt, lösbar. Mit einem Constraintlöser (oder auch scharfem Hinsehen) erkennt man, dass es mehrere Möglichkeiten gibt, für eine Erfüllung aller Bedingungen zu sorgen. Während die ersten drei Bedingungen – Namensgleichheit beider Methoden, übereinstimmende Parametertypen und Subtyprelation – schon bei der initialen Belegung der Variablen erfüllt sind, lässt sich die dri e Bedingung erfüllen, indem wahlweise die Zugrei arkeit von m1 auf protected oder public angehoben wird oder die beiden Klassen A und B in dasselbe Paket verschoben werden. 3.3. Constraintregeln Der vorhergehende Abschni hat beschrieben, wie aus der Intention und den zu erhaltenden Invarianten eines Refaktorisierungswerkzeugs eine Menge von Bedingungen aufgestellt werden kann, um anhand dieser mit einem Constraintlöser eine geeignete Programmtransformation zu berechnen. Neben einigen ausgewählten Bedingungen, die aus der Intention einer Werkzeuganwendung resultieren und direkt in Form von Variablenzuweisungen gegeben werden können, muss der Großteil der zu erzeugenden Constraints die Beibehaltung von Abhängigkeiten, jede davon ausgedrückt als Fakt f ∈ F , sicherstellen. Um diese Constraints zu erstellen, werden Abbildungsvorschriften benötigt, die gemäß der jeweiligen Sprachspezifikation besagen, wie ein konkretes Fakt f zu erfüllen ist oder wie die Erfüllung eines in der Faktenmenge nicht enthaltenen Fakts zu verhindern ist. Ein Beispiel für das Resultat einer sol⁸ daher werden im Folgenden die Begriffe Constraintvariable und Variable synonym genu t . . Constraintregeln chen Abbildungsvorschrift ist schon im vorhergehenden Kapitel . gegeben, wo auf Seite gezeigt ist, wie die Variablen zweier Methoden m1 und m2 derart einzuschränken sind, um eine Überschreibung zwischen diesen zu erreichen. Die Abbildungsvorschriften, die aus einer gegebenen Faktenbasis Bedingungen ableiten, heißen Constraintgenerierungsregeln (engl. constraint generation rule), oder auch kurz Constraintregeln. Wie jede Regel besteht auch eine Constraintregel aus zwei Teilen, der Prämisse sowie der Konklusion. Die Prämisse nennt die Bedingungen, unter denen ein Constraint zu generieren ist und entspricht einer Programmabfrage an die Faktenbasis. Die Konklusion zeigt ein Muster, nach dessen Vorbild das Constraint zu generieren ist. Wie für Constraintvariablen lassen sich in der Literatur ebenso für Constraintregeln verschiedene Notationen finden (vergleiche zum Beispiel [TKB ], [ST ] und [SP a]). Diese Arbeit verwendet eine Schluss- beziehungsweise Inferenzregelnotation, bei welcher die Prämisse getrennt durch einen Balken über der Konklusion steht: Prämisse Konklusion Eine einfache Regel für Java besagt, dass bei überschreibenden Methoden diese gleiche Namen haben müssen: overrides(m2 , m1 ) ( ) m1 .identifier = m2 .identifier Innerhalb dieser Regel sind m2 und m1 Regelvariablen (das sind keine Constraintvariablen!), welche genau die geordneten Paare von Programmelementen repräsentieren, die in overrides enthalten sind. Es wird deutlich, dass die gegebene Notation für Constraintregeln eine wichtige Eigenschaft unterschlägt, die nur deswegen entfallen kann, weil sie allen Constraintregeln gemein ist. Die verwendeten Regelvariablen einer jeden Constraintregel sind allquantifiziert, was bedeutet, dass eine Regel für alle Regelvariablenbelegungen Anwendung finden soll, die die Prämisse erfüllen. Aus der oben gegebenen Regel ( ) ergeben sich also für n Methodenüberschreibungen insgesamt n Constraints. Es liegt auf der Hand, dass mit steigender Anzahl der Bedingungen in der Prämisse die Anzahl der daraus zu generierenden Constraints maximal gleich bleibt, in den meisten Fällen aber sinken wird. Eine zweite implizite Eigenschaft der Constraintgenerierungsregeln ergibt sich aus den Definitionen von Constraintsystemen und -lösungen. Demnach wirken alle erstellten Constraints konjunktiv zusammen, es müssen für eine Constraintlösung also alle Constraints gemeinsam erfüllt sein. Ein Nebeneffekt der Konjunktivität von Constraints ist, dass Constraintregeln, deren Konklusionen konjunktiv sind, in einzelne Regeln getrennt werden können. Umgekehrt können Regeln mit identischer Prämisse jeweils zusammengefasst werden. So treffen neben obiger Regel ( ) noch weitere Constraintregeln für überschreibende Methoden zu. Eine davon besagt, dass bei überschreibenden Methoden die überschreibende Methode mindestens dieselbe Zugrei arkeitsstufe haben muss wie die überschriebene ( § . . . ):⁹ overrides(m2 , m1 ) ( ) m2 .accessibility ≥ m1 .accessibility ⁹ Dies ist ein erstes Beispiel für eine Regel (beziehungsweise Constraintmenge), die nicht ein bestimmtes Programmverhalten zusichert, sondern die Gültigkeit des Programms sicherstellt. . Constraintbasierte Refaktorisierungen Wegen der zu Regel ( ) identischen Prämisse lassen sich beide Regeln äquivalent zu einer gemeinsamen Regel zusammenfassen: overrides(m2 , m1 ) ( )+( ) (m1 .identifier = m2 .identifier) ∧ (m2 .accessibility ≥ m1 .accessibility) Umgekehrt können die aus dieser Regel ( ) + ( ) generierten Constraints, die der Konklusion entsprechend Konjunktionen sind, wahlweise in ihrer Gänze als Constraints des Constraintsystems verstanden werden, oder in ihre einzelnen Konjunkte zerfallen, die dann als einzelne Constraints implizit durch ein und verbunden werden. Bedingte Constraints Es verbietet sich, in der Prämisse einer Constraintregel und damit einhergehend in einer verwendeten Faktenabfrage Annahmen über den Wert von Constraintvariablen zu machen [SP a]. Täte man dies und würde sich der Wert einer solchen Variablen bei der Suche nach einer gültigen Constraintlösung ändern, könnte sich dadurch für eine konkrete Variablenbelegung eine zunächst unerfüllte Prämisse einer Constraintregel erst noch erfüllen. Die Folge wäre, dass entweder nötige Constraints fehlen oder nachträglich generiert werden müssten. Im umgekehrten Fall könnte sich durch einen geänderten Variablenwert die Prämisse einer Constraintregel als nichterfüllt entpuppen, sodass die Constraintmenge womöglich zu groß und damit unnötig restriktiv wird. Dies soll an folgender Constraintregel erläutert werden, die besagt, dass in Java ein Topleveltyp ausschließlich public oder default-Zugrei arkeit haben darf ( § . ): isType(t) isToplevel(t) ( ) t.accessibility = public ∨ t.accessibility = default Diese Regel ist nur korrekt, soweit es keine Variable gibt, über deren Wertänderung beispielsweise ein Membertyp zu einem Topleveltypen transformiert werden kann. Denn gäbe es einen solchen Membertypen tm , würde für ihn kein entsprechendes Constraint angelegt werden. Würde tm aber nun im Rahmen der Programmtransformation zu einem Topleveltypen werden, fehlte im Constraintsystem ein entsprechendes Constraint, welches ihm einen privateoder protected-Modifizierer verbietet, der für ihn als Membertypen zulässig gewesen wäre. Die Programmtransformation könnte somit ungültig werden. Es ist genau so aber auch der umgekehrte Fall vorstellbar, in dem obige Regel zu restriktiv wäre, soweit ein Topleveltyp tt tatsächlich in einen Membertypen gewandelt wird. In diesem Fall wäre die Einschränkung von tt auf public- und default-Zugrei arkeit unnötig. Diesem Problem kann begegnet werden, wenn vor der Programmtransformation schon klar ist, ob tm nach der Transformation ein Toplevel- oder Membertyp sein wird. Neben dem Fall, dass ein Typ niemals seine Toplevel-Eigenschaft ändern darf, trifft dies ebenso zu, wenn die Intention eines Refaktorisierungswerkzeugs gerade in der Transformation eines Member- zu einem Topleveltypen liegt. In diesem Fall müsste bei der Abfrage isToplevel in der Prämisse der Regel nicht nur der aktuelle Programmzustand berücksichtigt werden, sondern ebenso im Rahmen einer vorausschauenden (engl. foresight) [ST ] Anwendung berücksichtigt werden, welche Änderungen sich aus der Intention ergeben. Entspricht die Intention der Umwandlung eines Membertypen tm in einen Topleveltypen, muss für diesen die Abfrage isToplevel unabhängig vom aktuellen Programmzustand zu Wahr auswerten. . . Constraintregeln Eine vorausschauende Auswertung der Regelprämisse verbietet jedoch, den Constraintlöser bei der Constraintlösung eigenständig die Toplevel-Eigenschaft vergeben zu lassen. Denn gäbe es wiederum diese Möglichkeit, könnte sich die Auswertung der Prämisse durch die Constraintlösung ändern – und erneut wäre die Constraintmenge entweder unvollständig oder zu restriktiv. Eine in [SP a] entwickelte generische Lösung für dieses Dilemma besteht darin, die veränderlichen Regelbestandteile von der Prämisse in die Konklusion zu ziehen, wie im Folgenden mit einer abgeänderten Regel ( *) gezeigt: isType(t) ( *) t.isToplevel → (t.accessibility = public ∨ t.accessibility = default) Die Konklusion hat nun eine zusä liche Wächterbedingung erhalten, die der entfallenen Abfrage innerhalb der Prämisse entspricht. Das Constraint ist damit zu einem bedingten Constraint (engl. conditional constraint) [SKP ] geworden. In ihm wird über eine zusä liche Constraintvariable ausgedrückt, ob t ein Topleveltyp ist. Entsprechend ihrer Belegung findet die Konklusion der zugrundeliegenden Regel ( ) Anwendung. Tro der offensichtlichen Vorteile bedingter Constraints muss die gesteigerte Mächtigkeit von Refaktorisierungswerkzeugen, die man durch Herabziehen und Veränderlichmachen von Regelbestandteilen erhält, gleich doppelt teuer erkauft werden. Einerseits sorgt das Entfallen von Bedingungen innerhalb der Prämisse dafür, dass die Regel häufiger angewendet werden kann und somit insgesamt mehr Constraints entstehen. Vergleicht man die Regeln ( ) und ( *), so steigt die Anzahl der hinzukommenden Constraints um die Anzahl der Typen im Programm, welche keine Topleveltypen sind. Unter der Annahme, dass in üblichen JavaProgrammen jeder dri e Typ ein Membertyp ist, stiege die Anzahl der Constraints um %. Andererseits steigt auch der Aufwand bei der Auswertung eines jeden – nun bedingten – Constraints. Nicht nur, dass nun eine zusä liche Variable referenziert wird, die Abhängigkeiten zu weiteren Constraints schafft, die Implikation sorgt für weitere, durch den Constraintlöser zu berücksichtigende Konfigurierbarkeit, das Constraintsystem wird zu einem dynamischen Bedingungserfüllungsproblem (engl. dynamic constraint satisfaction problem) [MF ]. Problematisch ist dabei der zusä liche oder-Operator, welcher bei der Suche nach einer gültigen Variablenbelegung zusä lichen Berechnungsaufwand bedeutet [CC , GGM ]. Während konjunktive Teilbedingungen mi els effizient durchführbarer Domänenreduktionen gemeinsam in die Suche nach einer Constraintlösung einfließen können, erzeugen disjunktive Teilbedingungen Constraintsysteme, bei denen typischerweise zeitaufwändiges Backtracking zum Einsa kommen muss [CC , GGM ]. Wegen der hohen Kosten bedingter Constraints scheint es also geboten, diese nach Möglichkeit zu vermeiden. Wo abzusehen ist, dass ein Transformationswerkzeug bestimmte Transformationen unter keinen Umständen ausführt, sollten davon abhängige Bedingungen als Faktenabfragen innerhalb der Prämisse stehen, sta sie über Variablen als Wächterbedingungen in Konklusion zu formulieren. Für die Regelschreiber bringt es naturgemäß Nachteile mit sich, wenn die Formulierung der Constraintregeln von den möglichen Transformationen des Refaktorisierungswerkzeugs abhängig gemacht wird. So geht insbesondere der wesentliche Vorteil des constraintbasierten Ansa es verloren, einmalig einen universellen Sa Constraintregeln für eine Programmiersprache entwickeln zu können, der für beliebige Transformationswerkzeuge Einsa finden . Constraintbasierte Refaktorisierungen kann. Steimann und von Pilgrim [SP a] liefern als Lösung hierzu die Technik des Regelumschreibens (engl. rule rewriting). Nach ihren Ideen ist der Ausgangspunkt der Constraintgenerierung nicht nur das zu transformierende Programm und der Regelsa , sondern auch eine Filterfunktion, welche die Menge der Variablen auf genau die Variablen einschränkt, deren Werte durch das Werkzeug tatsächlich Änderungen erfahren können sollen [SP a]. Sind die unveränderlichen Variablen bekannt, können von ihnen abhängige Bedingungen automatisch in die Prämisse übertragen werden, schließlich sind diese Bedingungen unabhängig von der konkreten Variablenbelegung [SP a]. Insgesamt genügt es damit, einheitlich für alle Werkzeuge nur noch Regeln für bedingte Constraints anzugeben. Das Hochziehen konstant auswertbarer Bedingungen kann automatisiert für jedes Werkzeug individuell geschehen. Während dieser Prozess formal gesehen einem fallweisen Umschreiben der Regel entspricht (und daher seinen Namen zurecht trägt), findet die von Steimann und von Pilgrim gelieferte technische Umse ung [SP a] in Form einer konstanten Auswertung (engl. constant evaluation) bedingter Boolescher Ausdrücke sta . Um dies erneut an einem Beispiel deutlich zu machen: Wie oben argumentiert genügt es, die allgemeinere Regel ( *) zu formulieren, aus welcher für einen Typen A zunächst noch das bedingte Constraint generiert wird: A.isT oplevel → (A.accessibility = public ∨ A.accessibility = default) Ist nun bekannt, dass die Variable A.isToplevel unveränderlich ist, kann ihr Wert entsprechend erse t werden. Im Falle, dass A ein Topleveltyp ist, ergibt sich true → (A.accessibility = public ∨ A.accessibility = default) wobei nach den Gese en der Booleschen Logik die Wächterbedingung entfallen kann: A.accessibility = public ∨ A.accessibility = default Es entsteht also genau das Constraint, welches Resultat der Anwendung von Regel ( ) gewesen wäre. Falls der Typ A hingegen ein Membertyp ist (und bleibt), so ergibt sich aus Regel ( *) das Constraint false ⇒ (A.accessibility = public ∨ A.accessibility = default) welches nach den Gese en der Booleschen Logik zu true ausgewertet werden kann. Insgesamt kann das Constraint also entfallen (da es stets gelöst ist), was ebenso genau dem Zustand entspricht, als wenn Regel ( ) Anwendung gefunden hä e. Nach dieser wäre schon die Prämisse fehlgeschlagen und eine Erzeugung des Constraints wäre somit ebenso verhindert worden. Vergleicht man die Idee der vorausschauenden Constraintregeln [ST ] (die le tenendes ebenso die teuren, bedingten Constraints erspart) mit dem Regelumschreiben, fällt auf, dass beide Ansä e ihr Optimierungspotential aus derselben Quelle schöpfen, nämlich dem vor Constraintgenerierung bekannten Wissen über unveränderliche Variablenwerte. Der Unterschied liegt hingegen in der höheren Flexibilität, die mit dem Verfahren des Regelumschreibens gegen höhere Laufzeiten erkauft werden kann. Während für vorausschauende Constraintregeln ganze Arten von Constraintvariablen (zum Beispiel t.isToplevel für alle t) als . . Constraintregeln während der Constraintlösung unveränderlich angenommen werden, können bei der dem Regelumschreiben folgenden konstanten Auswertung auch nur Teilmengen hiervon zwecks Optimierung als unveränderlich gekennzeichnet werden. Ersteres ist dabei weit weniger nü lich. So mag man einem Werkzeug, das Deklarationen verschiebt, ungern pauschal verbieten, Zugrei arkeiten anzupassen. Hingegen kann man die Menge der anzupassenden Zugreifbarkeiten durchaus sinnvoll auf solche der verschobenen Deklarationen reduzieren, was in der Praxis nur einem Bruchteil der Gesamtmenge der Zugrei arkeitsvariablen entsprechen sollte. Umgekehrt ist zu beachten, dass auch für das Regelumschreiben die vollständige Constraintmenge erzeugt werden muss und die Vereinfachung der Constraints erst beim Constraintlösen der Laufzeit zugute kommt. Annahmen über den Wert konkreter Constraintvariablen innerhalb der Regelprämisse führen zu fehlerhaften Constraintsystemen. Der umgekehrte Fall einer unveränderlichen Abfrage innerhalb der Konklusion ist hingegen problemlos. Constraints können nicht nur Beziehungen zwischen Variablen beinhalten, sondern auch Variablen mit Konstanten in Relation se ten, wie in Regel ( ) anhand der Konstanten public und default geschehen. Auch können die Abfragen, wie sie in der Prämisse der Regel erscheinen, Teil der Konklusion sein. Als Beispiel sei die folgende Regel gegeben: isMethod(m) isReference(r) binds(r, m) ( ) m.accessibility = public ∨ subType(r.hostType, m.hostType) ∨ r.hostPackage = m.hostPackage Sie besagt, dass bei einer Methodenreferenz r, die an eine Methode m bindet, entweder m als public-zugrei ar deklariert sein muss, die Referenz r in einem Subtypen des deklarierenden Typen von m liegen muss, oder Referenz und Methode beide im gleichen Paket liegen müssen. Die Abfrage der Subtyp-Eingenschaft erfolgt hierbei in Form einer Faktenabfrage, die entweder zu wahr oder falsch auswertet abhängig davon, ob ein entsprechendes Fakt in F enthalten ist. Regeln ohne Prämisse: Axiome Mit den Mi eln des Regelumschreibens scheint eine möglichst bedingungslose Prämisse Idealzustand zu sein, bis hin zu Constraintregeln der Form programElement(p1 ) programElement(p2 ) ... ... ( ) die ihre Prämissen so abstrakt formulieren, dass für die verwendeten Regelvariablen beliebige Programmelemente eingese t werden können. Es gibt mehrere Gründe, warum solche Regeln in der Praxis nur selten auftreten. Soweit ohnehin klar ist, dass absehbar kein Bedarf an bestimmter Variabilität entsteht, verschlechtert die dafür notwendige Einführung zusä licher Constraintvariablen die Lesbarkeit der Regeln unnötig. So könnte man in Regel ( *) die Prämisse tatsächlich hin zu programElement(t) reduzieren, soweit man eine Constraintvariable p.isType einführt: . Constraintbasierte Refaktorisierungen programElement(t) ( **) t.isType → (t.isToplevel → (t.accessibility = public ∨ t.accessibility = default)) Da es allerdings abwegig ist, (bedeutungserhaltend) in Java zum Beispiel eine Methode in einen Typen umwandeln zu wollen, sollte diese Verallgemeinerung entfallen. Weiterhin kann es vorkommen, dass sich Bedingungen der Prämisse nicht sinnvoll durch Variablen ausdrücken lassen, beispielsweise, wenn Bezug zum Ausgangszustand des Programms vor der Programmtransformation genommen werden soll. Abschni . nannte mit binds und overrides bereits zwei Abhängigkeiten, die bei bedeutungserhaltenden Programmtransformationen erhalten werden müssen. Um mit Constraints sicherzustellen, dass für eine konkrete Variablenbelegung diese Abhängigkeiten weiterhin gegeben sind, muss dem Regelschreiber die Möglichkeit gegeben sein, sich auf den Zustand vor der Programmtransformation zu beziehen. Im einfachsten Fall könnte dies geschehen, indem man sich in den Regeln auf die initiale Wertebelegung einer Variablen bezieht. [STST ] nennt ein Beispiel, in welchem eine Deklaration, die als Teil der API des Programms vor der Programmtransformation public deklariert war, auch nach der Transformation noch in der Programm-API enthalten sein soll. Dies ließe sich unter Bezugnahme auf den initialen Wert wie folgt in eine bedingte Constraintregel (ohne Vorbedingungen) gießen, soweit eine entsprechende Funktion init zur Verfügung steht: programElement(t) ( ) init(t.accessibility) = public → t.accessibility = public Umgekehrt sind initiale Werte aber unabhängig von der Belegung der Constraintvariablen. Sie hängen nur von der Belegung der Regelvariablen ab, wie sie zum Zeitpunkt der Constraintgenerierung bekannt ist. So lässt sich in obiger Regel ( ) der Ausdruck init(t.accessibility) schon in dem Moment zu wahr oder falsch auswerten, in dem klar ist, mit welchem Programmelement t im jeweilig aus der Regel generierten Constraint belegt ist. Da in der konkreten Regel die Abfrage des initialen Zustandes der Variable eine Wächterbedingung ist, deren Nichterfüllung eine Generierung des gesamten Constraints unnötig macht, wäre eine Auswertung zum Zeitpunkt der Constraintgenerierung sogar wünschenswert. Le tendlich bietet sich also eine Implementierung mi els einer eigenen Programmabfrage public eher an: public(t) ( ) t.accessibility = public Für den Regelschreiber ergeben sich also Situationen, in denen es tro Mechanismen zur Regelumschreibung oder konstanten Auswertung durchaus Sinn ergibt, Bedingungen in der Prämisse (sta der Konklusion) zu belassen. 3.4. Constraintgenerierung Wie sich aus einer Menge von Constraintregeln und einer Faktenbasis die resultierende Constraintmenge berechnen lässt, hat der vorhergehende Abschni . bereits erläutert. Für jede Regel wird die Prämisse als Abfrage gegen die Faktenmenge F ausgeführt [SKP ]. Dann wird für jede so erhaltene Erse ung der Regelvariablen durch konkrete Programmelemente, die die Prämisse erfüllt, gemäß der Konklusion ein Constraint erstellt [SKP ]. Abgesehen . . Constraintgenerierung von der Tatsache, dass die Programmabfragen effizient ausgeführt werden müssen¹⁰, ist dieses Verfahren weitgehend einfach umse bar. Leider führt es so aber nicht zum Erfolg, was an der großen Anzahl von Constraints liegt, die sich schon für sehr kleine Programme ergeben können und schnell zu untragbaren Laufzeiten führen [ST ]. Die Technik des Regelumschreibens liefert zwar schon einen Ansa , die Constraintmenge zu reduzieren, allerdings ist ihr Erfolg stark abhängig von der Menge an Variablen, deren Werte als unveränderlich gekennzeichnet werden können. Daher bedarf es geeigneter Mi el, die Menge generierter Constraints weiter zu reduzieren. Wie dies geschehen kann, soll an folgendem Beispielprogramm erläutert werden: 110 111 112 113 package a; public class A { public void m() { /*...*/ } } 114 115 116 117 118 package b; public class B extends a.A { public void m() { /*...*/ } } 119 120 121 122 123 package c; public class C { { new a.A().m();} } Wendet man die in Abschni . gezeigten Constraintregeln ( ), ( ) und ( ) auf dieses Beispiel an, so ergeben sich die vier in Tablelle . gezeigten Constraints. Was sich nun zunu e gemacht werden kann, ist, dass nicht für jedes Transformationsproblem tatsächlich die vollständige Constraintmenge benötigt wird [SKP ]. Das wird deutlich, sobald man das Constraintsystem als einen bipartiten Constraintgraphen betrachtet, in dem die Knotenmenge aus den Constraintvariablen und Constraints besteht und eine Kante genau dann eine Constraintvariable mit einem Constraint verbindet, wenn ein Constraint eine Constraintvariable beschränkt. Für die Constraints aus Tabelle . ist in Abbildung . eine graphische Repräsentation gegeben. Wie man sieht, zerfällt der Graph in drei nicht verbundene Zusammenhangskomponenten. Zur Lösung eines Transformationsproblems genügt es nun, nur die Zusammenhangskomponenten zu betrachten (und zu generieren), die genau die Variablen beinhalten, die zur Codierung der Intention des Transformationsproblems nötig sind [SKP ]. Anderenfalls – also wenn die Anpassung einer Variablen einer anderen Zusammenhangskomponenten nötig wäre – müsste es ein Constraint geben, welches diese benötigte Anpassung ausdrückt, was im Widerspruch dazu steht, dass es keinen Pfad zwischen den einzelnen Zusammenhangskomponenten gibt. Man kann sich bei der Constraintgenerierung also zunu e machen, dass für die Lösung eines konkreten Transformationsproblems in Abhängigkeit von diesem jeweils nur ein Teil an Constraints tatsächlich benötigt wird. ¹⁰ Gegebenenfalls können die aus dem abstrakten Syntaxbaum extrahierten Fakten in eine Datenbank übertragen werden, um die Abfragezeiten zu minimieren [SKP ]. . Constraintbasierte Refaktorisierungen Tabelle . .: Die vollständige Constraintmenge für das Programmbeispiel von Seite riert gemäß den Constraintregeln ( ), ( ) und ( ) Nr. Regel ( ) ( ) ( ) ( ) , gene- Constraint b.B.m().accessibility ≥ a.A.m().accessibility a.A.accessibility = public ∨ a.A.accessibility = default b.B.accessibility = public ∨ b.B.accessibility = default a.A.m().accessibility = public ∨ subType(new A().m().hostType, a.A.m().hostType) ∨ new A().m().hostPackage = a.A.m().hostPackage Faktisch wird der Fall, dass ein Constraintgraph in mehrere Zusammenhangskomponenten zerfällt, nur selten eintreten. Auch wenn die Idee nahe liegt, dass es insbesondere in größeren Softwareprojekten einzelne Programmteile gibt, die weitgehend voneinander unabhängig sind und daher der Constraintgraph in einzelne, nicht verbundene Komponenten zerfällt, gibt es dennoch Regeln, die Bedingungen für den Fall abprüfen, dass Programmteile beispielsweise zwischen diesen Komponenten verschoben werden. Erst wenn man bestimmte solcher Transformationen ausschließt, lassen sich kleinere Zusammenhangskomponenten erreichen. In der Praxis erfolgt so ein Ausschluss von Programmtransformationen, indem eine Variable als unveränderlich¹¹ gekennzeichnet wird, also ihre Domäne auf ihren initialen Wert eingeschränkt wird. Nimmt man an, dass die Methode a.A.m() in obigem Beispiel aus einer unveränderlichen Programmbibliothek stammt, kann der Knoten a.A.m().accessibility entfallen, da diese Variable im Constraint nun durch eine Konstante – nämlich den initialen Wert von a.A.m().accessibility – erse t werden kann (die sogenannte frühzeitige Auswertung (engl. early evaluation) [SP a]). Das Constraint aus Regel ( ) braucht somit nicht mehr generiert zu werden. Ebenso entfällt die Generierung weiterer Constraints, die sich aus durch Constraint referenzierte Variablen ergeben könnten. Würden also weitere Regeln existieren, die die Variable new A().m().hostPackage bedingen würden, könnte deren Behandlung ebenso entfallen. Steimann et al. [SKP , Figure ] und parallel dazu Müller [Mül ] haben einen auf dieser Idee au auenden Algorithmus zur Erzeugung solch minimierter Constraintsysteme in Abhängigkeit bestimmter Intentionen entwickelt. Das Constraintne wird dabei beginnend mit den die Intention codierenden Variablen aufgebaut. In Form einer Breitensuche wird dann der Constraintgraph vervollständigt, indem für jede erzeugte Variable die Constraints angelegt werden, die diese Variable bedingen, und für jedes generierte Constraint die Variablen angelegt werden, die dieses Constraint referenziert [SKP , Mül ]. Ist eine Variable als unveränderlich gekennzeichnet, werden von dieser ausgehend keine weiteren Constraints angelegt. Im Allgemeinen sind zwei Möglichkeiten denkbar, warum eine Variable als unveränderlich zu kennzeichnen ist. Einerseits kann dies schon bei der Faktengenerierung unabhängig ¹¹ Sta des etwas widersprüchlich anmutenden Begriffs einer unveränderlichen Variable kann man ebenso auch von einer Erse ung der Variable durch eine Konstante sprechen [SKP ]. Da – wie später klar wird – eine solche Erse ung aber häufig nur in Abhängigkeit von einem konkreten Transformationsproblem geschieht, wird mitunter auch bei diesen Konstanten weiterhin der Begriff Variable genu t. . . Constraintlösung . a.A.m().accessibility b.B.accessibility b.B.m().accessibility a.A.m().accessibility new A().m().hostType a.A.m().hostType new A().m().hostPackage a.A.m().hostPackage Abbildung . .: Darstellung der Constraints aus Tabelle . als (bipartiter) Constraintgraph. Die Variablen sind durch abgerundete Rechtecke und die Constraints durch ihre eingekreisten Nummern gemäß Tabelle . illustriert. vom jeweiligen Transformationsproblem geschehen, was typischerweise bei unveränderlichem Code wie Bibliotheksfunktionen der Fall ist. Umgekehrt ist es denkbar, für einzelne Werkzeuge die Menge der erlaubten Änderungen durch explizite Angabe der nur erlaubten Änderungen (engl. allowed changes) [SKP ] zu beschränken. So kann zum Beispiel für eine R -Refaktorisierung festgelegt werden, dass diese keine Zugrei arkeiten ändern darf, was in der Folge alle accessibility-Variablen als unveränderlich kennzeichnet. 3.5. Constraintlösung Wie in Abschni . gezeigt, kann bei der constraintbasierten Programmtransformation das Berechnen der durchzuführenden Änderungen auf ein generisches Bedingungserfüllungsproblem reduziert werden. Auch wenn damit die Frage der Constraintlösung formal gelöst erscheint, gilt es dennoch, gewisse technische Aspekte zu berücksichtigen. Übersetzung in Integer-Constraints Im Mi elpunkt der meisten Forschungsbestrebungen zu Constraintsystemen stehen solche Systeme und Lösungen, deren Variablen als Domänen ausschließlich endliche Mengen von Integer-Werten haben [Tsa ], was sich auch in den Implementierungen frei verfügbarer Constraintlöser (wie zum Beispiel Cream [Nao ] oder Choco [JRL ]) widerspiegelt. Entsprechend müssen Abbildungen für die in Abschni . . genannten Domänenarten gefunden werden, die jedes Element einer Domäne einem IntegerWert zuordnen. Da es sich – wie in Abschni . . beschrieben – ausschließlich um endliche Domänen handelt, genügt eine einfache Durchnummerierung der Domänenelemente. Neben den Variablenwerten selbst sind aber ebenso noch Überse ungen der in den Constraints verwendeten Operatoren notwendig, die ein äquivalentes Verhalten der Integer-Constraints erreichen. Einfache Prüfungen, wie die auf Gleichheit und Ungleichheit von Variablenwerten, können in Integer-Constraints ganz analog angewendet werden. Relationen wie ≥ und ≤ zwischen Werten aus vollständig geordneten Aufzählungsdomänen gelten ebenso . Constraintbasierte Refaktorisierungen analog, soweit die Nummerierung der Elemente anhand ihrer Ordnung erfolgt. Schwieriger wird es bei komplexeren Booleschen Ausdrücken, wie sie sich zum Beispiel aus den oben gezeigten Regeln ( **) und ( ) ergeben können. Bietet der verwendete Constraintlöser keine eigene Domäne für Boolesche Ausdrücke, müssen diese ebenso in Integer-Constraints transformiert werden. Soweit der Constraintlöser eine gewisse Grundmenge an Integer-Operationen bietet (zum Beispiel min- und max-Funktionen und einen Absolutbetrag), können Boolesche Operationen mit diesen simuliert werden [Nao ]. Indirektion (siehe dazu Abschni . . ) lässt sich auf die in vielen Constraintlösern verfügbaren Element-Constraints [vHSD ] abbilden [SKP ]. Auswahl einer Constraintlösung Ist ein Bedingungserfüllungsproblem lösbar, besteht für dieses die Ausgabe eines Constraintlösers aus der Menge der Lösungen [Tsa ]. Zwar stellt – soweit die Menge der Constraintregeln vollständig ist – jede Lösung des Constraintsystems eine gültige Programmtransformation dar, die gleichzeitig der Intention der Werkzeuganwendung gerecht wird, dennoch ist einem Benu er zumeist nur an einer einzigen Lösung gelegen. Welche Lösung dies genau ist, lässt sich nicht mit Sicherheit beantworten und hängt mitunter von den Vorlieben des Benu ers ab. Soll zum Beispiel eine Methode m in eine Subklasse verschoben werden, wobei m die einzige Verwenderin einer weiteren privaten Methode n innerhalb derselben Klasse ist, so gibt es zwei Alternativen. Entweder die Methode n wird mit m mit verschoben oder die Methode n wird durch ausreichende Anhebung ihrer Zugrei arkeit dem Klasseninterface der umschließenden Klasse hinzugefügt und somit auch in der Subklasse verfügbar. Forschungen, wie hier eine sinnvolle Heuristik zur Entscheidungsfindung aussehen kann, stehen aus. In jedem Fall aber zu vermeiden sind unnötige Änderungen. Insbesondere wenn zwei Programmtransformationen zur Auswahl stehen, von denen eine die andere enthält, so ist aller Voraussicht nach die Lösung mit der geringeren Anzahl von Änderungen zu bevorzugen. Stehen zwei Transformationen zur Auswahl, die der gleichen Menge von Variablen neue Werte zuweisen, ist in der Regel diejenige zu wählen, deren Änderungen weniger gewichtig ausfallen. So ist eine Änderung der Zugrei arkeit von private zu default vermutlich eher zu bevorzugen als eine Änderung von private zu public, auch wenn beide Änderungen eine gültige Lösung darstellen. Die Reihenfolge, in welcher Constraintlöser Lösungen berichten, hängt üblicherweise von internen Details ab und ist kein verlässlicher Indikator. Auch Constraintlöser, die ausgehend von einem bestehenden, beinahe gelösten Constraintsystem – wie es bei constraintbasierten Programmtransformationen durch die initialen Werte vorliegt – in einer Nachbarschaftssuche (engl. neighborhood search) nach einer Lösung suchen (zum Beispiel [Nao ]), berichten nicht notwendigerweise die Constraintlösungen mit minimaler Änderungsanzahl zuerst. Eine Lösung hierfür besteht darin, neben den eigentlichen Constraints noch eine weitere Bedingung zuzulassen, über die konkrete Präferenzen ausgedrückt werden können [VKDL ]. Dieser Idee folgend schlagen Steimann et al. [SKP ] für constraintbasierte Refaktorisierungen sogenannte penalties vor, Strafwerte, die sich durch die (gegebenenfalls gewichtete) Summe der Änderungen ergeben und welche während der Constraintlösung minimiert werden sollen. Problematisch sind die dabei entstehenden Laufzeiten, da zur Ermi lung einer minimalen Lösung im schlimmsten Fall der gesamte Suchraum abgelaufen werden muss [SKP ]. . . Refacola Glücklicherweise hat sich das Problem, eine minimale Lösung finden zu müssen, in der Praxis als wenig relevant herausgestellt. Durch die in Abschi . beschriebenen Minimierungen des Constraintsystems entfallen schon ein Großteil für die Constraintlösung nicht benötigter Variablen, somit auch die Möglichkeit, deren Belegung unnötigerweise zu ändern. Die Verwendung einer Nachbarschaftssuche reduziert (neben den Laufzeiten) weiter das Risiko unnötiger Änderungen. Weitere Fälle unnötiger Änderungen können durch ein mehrstufiges Constraintlösen verhindert werden. So können bei einer P U -Refaktorisierung beispielsweise zunächst anhand eingeschränkter erlaubter Änderungen [SKP ] weitere Änderungen neben der Intention ausgeschlossen werden. Erst wenn für dieses Constraintsystem keine Lösung gefunden wird, werden zunächst Änderungen beispielsweise von Zugrei arkeiten und dann das Verschieben weiterer Deklarationen gesta et. Für die in dieser Arbeit entwickelten Werkzeuge liefert dieses Vorgehen eine gute Balance zwischen Laufzeit und Menge unnötiger Änderungen, denn wie sich in der Evaluation in Abschni . zeigen wird, sind insbesondere für Refaktorisierungen mit wenigen erlaubten Änderungen die Laufzeiten sehr kurz. Offen hingegen ist das Problem, wie mit einem nicht lösbaren Constraintsystem umgegangen wird. Es wäre wünschenswert, in diesem Fall dem Benu er eine Fehlermeldung zu präsentieren, aus welchen Gründen die angestrebte Programmtransformation abgelehnt werden muss. Umgekehrt lässt sich im allgemeinen Fall für ein Constraintsystem nicht ein einzelnes Constraint ausmachen, welches die Inkonsistenz verursacht. Häufig sind mehrere Constraints gemeinschaftlich beteiligt, deren Erfüllung sich gegenseitig ausschließt. Eine Erklärungskomponente (zum Beispiel [Jun ]) könnte hier Abhilfe schaffen. Wie und ob diese einen Benu er aber sinnvoll durch einen Constraintlöseprozess führt und ob Softwareentwickler sich in der Praxis auf einen mehrschri igen Dialog einlassen würden, ist ein offenes Problem. 3.6. Refacola Die Refacola (Refactoring Constriant Language) [SKP , SP a] ist eine domänenspezifische Sprache sowie ein Framework, welches ursprünglich zur Entwicklung constraintbasierter Refaktorisierungswerkzeuge gedacht war. Beginnend wurde die Refacola hauptsächlich durch Jens von Pilgrim implementiert, gewisse Anteile wurden im Rahmen der vorliegenden Arbeit als Erweiterungen beigesteuert. Da für die Refacola bisher keine Dokumentation existiert, die sämtliche Funktionen beschreibt, auf welchen die vorliegende Arbeit au aut, schließt dieses Kapitel diese Lücke. 3.6.1. Sprachdefinitionen Die Entwicklung eines Refaktorisierungswerkzeugs mit der Refacola erfordert die Erstellung mindestens dreier Refacola-Dateien: Eine Sprachdefinition, eine Auflistung von Constraintregeln sowie eine Spezifikation der Refaktorisierung mit Angabe der Art der Intention und erlaubten Änderungen. Eine Refacola-Sprachdefinition beschreibt die Mengen P der Programmelemente, V ∗ der Variablenarten mit ihren Domänen Dv∗ und F ∗ der Faktenarten. Aufgebaut ist eine Sprachdefinition nach dem Schema 124 language Java . Constraintbasierte Refaktorisierungen 125 126 127 128 129 130 131 132 kinds ... properties ... domains ... queries ... In Zeile ist nach dem Schlüsselwort language der Name der beschriebenen Sprache zu nennen (hier Java), um auf diese aus den Constraintregeln heraus zugreifen zu können. Kinds Nach dem Schlüsselwort kinds folgt eine Auflistung der Arten von Programmelementen (sogenannten kinds) sowie der ihnen zugehörigen Variablenarten (ihren sogenannten properties). Folgender Ausschni definiert drei Arten von Programmelementen, nämlich Typdeklarationen (TypeDeclaration), Methodendeklarationen (MethodDeclaration) und Methodenreferenzen (MethodReference): 133 134 135 136 kinds TypeDeclaration <: ENTITY { identifier, accessibility } MethodDeclaration <: ENTITY { identifier, accessibility, parameterTypes, hostType } MethodReference <: ENTITY { identifier, argumentTypes } Variablenarten für eine Art von Programmelement werden deklariert, indem der jeweiligen Deklarationen eine Liste der entsprechenden Variablenarten v ∗ ∈ V ∗ in geschweiften Klammern angefügt wird. Die in Zeile deklarierte Programmelementart MethodDeclaration hat beispielsweise insgesamt vier Constraintvariablen, je eine für den Namen der Methode (identifier), ihre Zugrei arkeit (accessibility), die Typen ihrer Methodenparameter (parameterTypes) sowie ihren umschließenden Typen (hostType). Die Arten von Programmelementen sind durch eine Unterart-Relation (engl. subkind) geordnet, wobei der Operator <: die Unterartbeziehung spezifiziert.¹² An der Spi e der Hierarchie stehen die beiden vordefinierten Arten ENTITY und REFERENCE.¹³ Der Haup weck von Unterarten in der Refacola ergibt sich aus den vielfach zur Verfügung gestellten Mechanismen, anhand von Programmelementarten aus der Menge von Programmelementen Teilmengen auszuwählen. So gibt es in Constraintregeln die Möglichkeit, Regelvariablen auf bestimmte Arten zu begrenzen. Wenn also zum Beispiel Constraintregeln häufiger sowohl für Methoden als auch Konstruktoren gelten sollen, kann es Sinn ergeben, beim Hinzufügen von Konstruktoren für diese eine gemeinsame Überart MethodOrConstructor zu erstellen, von welchem dann ebenso auch Methodendeklarationen ableiten (die Refacola erlaubt Mehrfachvererbung). Weiterhin findet parallel zur Unterart-Hierarchie auch eine Vererbung der für diese deklarierten Variablenarten sta . So sind für jede Programmelementart stets auch die von seiner Überart definierten Variablenarten verfügbar. Eng im Zusammenhang damit steht die Möglichkeit, über das Schlüsselwort abstract solche Programmelemente zu kennzeichnen, die im ¹² Die eigentlich viel passenderen Begriffe Typ für Art und Subtyp für Unterart werden bewusst nicht verwendet, um Doppeldeutigkeiten in Verbindung mit den durch Programmelemente repräsentierte (Java-)Typen und deren Subtypen zu vermeiden. ¹³ Ob eine Programmelementart von REFERENCE oder ENTITY ableitet, ist in der Refacola semantisch unerheblich. Die Unterscheidung kann lediglich der Dokumentation des Codes dienen. . . Refacola Eingabeprogramm keine direkte Entsprechung haben, sondern bei denen lediglich die Unterarten instanziiert werden. Entsprechend lassen sich die Zeilen bis äquivalent ausdrücken als: 137 138 139 140 abstract NamedEntity <: ENTITY { identifier } TypeDeclaration <: NamedEntity { accessibility } MethodDeclaration <: NamedEntity { accessibility, parameterTypes, hostType } MethodReference <: NamedEntity { argumentTypes } Nun definiert keines der ursprünglichen Programmelemente mehr eine Variable identifier, dafür erbt jedes einzelne die Variable identifier der neu hinzugekommenen Programmelementart NamedEntity. Properties Nach dem Schlüsselwort properties muss jeder in den kinds-Definitionen verwendeten Variablenart eine Domäne zugeordnet werden: 141 142 143 144 145 146 properties identifier: Identifier parameterTypes: Sequence(Types) argumentTypes: Sequence(Types) hostType: Types accessibility: AccessModifiers Die Refacola hat vordefinierte Domänen Identifier für beliebige Zeichenke en und Boolean für Boolesche Werte, die anhand gleichnamiger Schlüsselworte verwendet werden können. (Geordnete) Listen von Variablen gleichen Typs (im Beispiel parameterTypes und argumentTypes) werden als Sequenzen über ein entsprechendes Schlüsselwort unter Angabe der Domäne der Listenelemente definiert. Alle weiteren Domänen müssen jeweils durch den Programmierer vorgegeben werden, wobei unterschiedliche Variablenarten dieselben Domänen nu en dürfen. Zum Beispiel verwenden sowohl hostType als auch parameterTypes die Domäne Types, wobei sie im le teren Fall als Elemen yp einer Sequenz zum Einsa kommt. Domains Jede nicht-vordefinierte und im properties-Abschni verwendete Domäne muss innerhalb des Abschni es domains definiert werden. Neben den vordefinierten Domänen erlaubt die Refacola zwei weitere Arten von Domänen: Aufzählungsdomänen und programmabhängige Domänen, wie aus dem laufenden Beispiel hervorgeht: 147 148 149 domains AccessModifiers = {private, package, protected, public} Types = [TypeDeclaration] Ein Beispiel für eine Aufzählungsdomäne liefert AccessModifiers. Aufzählungsdomänen werden durch eine in geschweifte Klammern gefasste Auflistung aller in der Domäne enthaltenen Elemente definiert. Die Refacola interpretiert Aufzählungsdomänen stets als vollständig geordnet und erlaubt dementsprechend innerhalb von Constraintregeln Größer- und KleinerVergleiche zwischen ihren Elementen. Programmabhängige Domänen werden durch die Angabe des Elemen yps in eckigen Klammern definiert. Die Domäne Types entspricht somit der Menge aller Programmelemente vom Typ TypeDeclaration sowie allen Programmelementen, deren Typ von TypeDeclaration ableitet. . Constraintbasierte Refaktorisierungen Facts Der le te Abschni einer Refacola-Sprachdefinition beschreibt die Menge F ∗ der Faktenarten. Jede Faktenart wird über den Namen des Fakts, gefolgt von einer in runde Klammern gefassten Liste der Arten, definiert, dessen Kreuzprodukt ein solches Fakt entspringt. In folgendem Beispiel sind mehrere Faktenarten definiert, so ist ein binds-Fact zum Beispiel ein geordnetes Paar bestehend aus einer MethodDeclaration und einer MethodReference: 150 151 152 153 queries binds(r: MethodReference, d: MethodDeclaration) overrides(m1: MethodDeclaration, m2: MethodDeclaration) subType(t1: TypeDeclaration, t2: TypeDeclaration) Neben den Arten innerhalb des Tupels ist jeder Tupelposition noch ein Name zugeordnet (im Beispiel r, d, m1, …). Innerhalb der Refacola-Definitionen ist dieser Name irrelevant und kann lediglich der zusä lichen Dokumentation dienen. 3.6.2. Constraintregeln Als weiteres Element können mit der Refacola Constraintregeln ausgedrückt werden, welche ausgehend von den in der Sprachdefinition festgelegten Fakten die dort ebenso definierten Variablen in ihren Wertebereichen einschränken. Eine Datei mit Regeldefinitionen hat in der Refacola folgenden Au au: 154 155 156 157 158 import "Java.language.refacola" ruleset JavaRules language Java rules ... Unter import sind (ggf. mit mehreren import-Anweisungen) die Dateien zu nennen, auf denen die Regeldefinition au aut. Hier ist zumindest eine Datei mit einer Refacola-Sprachdefinition zu nennen. Da die Refacola auch sprachübergreifende Refaktorisierungen unterstü t, können auch mehrere Sprachen eingebunden werden. Unter language sind noch einmal die verwendeten Sprachen anhand ihrer Namen zu nennen (welche sich nicht notwendigerweise mit den in den Imports verwendeten Dateinamen decken müssen). Hinter dem Schlüsselwort ruleset ist dem folgenden Regelsa ein Name zu geben. Constraintregeln können in mehrere Regelsä e (und damit Refacola-Dateien) aufgeteilt werden, was sinnvoll ist, um Constraintregeln für unterschiedliche Belange (Typen, Zugrei arkeiten, Namen, …) voneinander zu trennen. Dem Schlüsselwort rules folgt eine Auflistung der eigentlichen Constraintregeln. Eine Constraintregel ist aus vier Elementen aufgebaut: Einem Regelnamen, der Deklaration der Regelvariablen, der Regelprämisse und der Regelkonklusion, wobei folgende Syntax Anwendung findet. 159 160 161 162 163 164 165 166 <Regelname> for all <Deklaration der Regelvariablen> do if <Prämisse> then <Konklusion> . . Refacola 167 end Der Regelname einer Regel kann frei gewählt werden, wobei (auch über die Grenzen eines Regelsa es hinaus) jeder Name nur einmal vergeben werden darf. Als Beispiel soll im Folgenden die in Abschni . gezeigte Regel ( ) mi els Refacola beschrieben werden, welche zusichert, dass bei Methodenüberschreibung die Methodennamen identisch sind. Deklaration von Regelvariablen Jede innerhalb einer Constraintregel verwendete Regelvariable muss im for all-Teil der Regel durch Angabe eines Namens definiert werden. Weiterhin ist die Angabe einer Programmelementart erforderlich, anhand derer die Refacola die Menge der Programmelemente, die von der Regelvariablen angenommen werden kann, einschränkt. Regel ( ) verwendet zwei Regelvariablen, m1 und m2 , welche jeweils als MethodDeclaration deklariert sind: 168 169 170 for all m1: Java.MethodDeclaration m2: Java.MethodDeclaration Die Prämisse Die Prämisse einer Regel gibt an, unter welchen Bedingungen ein Constraint zu generieren ist. Wie in Abschni . beschrieben besteht eine Regelprämisse aus Faktenabfragen. Innerhalb der Refacola sind diese mit vorangestelltem Namen der Sprache zu qualifizieren (denn mehrere zusammen verwendete Sprachen könnten gleichnamige Fakten definieren). Im Falle von Regel ( ) verlangt die Prämisse eine Methodenüberschreibung zwischen m1 und m2 : 171 Java.overrides(m2, m1) Neben Faktenabfragen sind noch weitere Bedingungen innerhalb der Prämisse zulässig. So können Variablen auf Gleichheit (=) und Ungleichheit (!=) getestet werden. Die einzelnen Bedingungen sind dabei jeweils durch Kommata zu trennen und werden durch die Refacola als Konjunktion (verundet) ausgewertet. Andere logische Operatoren werden nicht unterstü t, eine Disjunktion (Veroderung) zum Beispiel kann aber mi els mehrere Regeln mit identischer Konklusion emuliert werden. Regelvariablen, die innerhalb einer Constraintregel definiert werden, aber durch die Prämisse in ihren Wertebereichen nicht eingeschränkt werden, heißen ungebunden. Durch die Allquantifizierung der Regelvariablen (siehe Abschni . ) vervielfacht jede ungebundene Variable die Menge der zu generierenden Constraints um einen Faktor entsprechend der Mächtigkeit ihrer Domäne. Da beim Regelschreiben ein häufiger Fehler darin besteht, versehentlich nicht mehr benötigte Variablen innerhalb der Regelvariablendeklaration zu belassen, werden ungebundene Regelvariablen seitens der Refacola stets mit einer Fehlermeldung versehen. Ist doch einmal eine ungebundene Variable erwünscht, so kann die Fehlermeldung mi els einer einstelligen pseudo-Query all unterdrückt werden. Wie in Abschni . auf Seite erklärt, führt die Verwendung von Constraintvariablen innerhalb einer Regelprämisse zu fehlerhaften Ergebnissen. Daher verbietet die Refacola eine solche und gibt hierfür eine entsprechende Fehlermeldung aus. . Constraintbasierte Refaktorisierungen Die Konklusion Die Konklusion einer Regel enthält das Muster der zu generierenden Constraints, in welchem die Regelvariablen bei Anwendung der Regel durch konkrete Programmelemente erse t werden. Im einfachsten Falle werden zwei Constraintvariablen zueinander in Bezug gese t, im Falle von Regel ( ) die Namen der beiden Methoden m1 und m2 : 172 m1.identifier = m2.identifier Weitere zulässige Operatoren sind der Ungleichheitsoperator != sowie für Vergleiche zwischen Elementen von Aufzählungsdomänen die Vergleichsoperatoren >=, >, <= und <. Um bedingte Constraints zu generieren, können über den Implikationsoperator −> entsprechende Wächterbedingungen vorangestellt werden. Boolesche Ausdrücke lassen sich dabei mi els der Operatoren and und or kombinieren und mi els not negieren. Ebenso ist es möglich, in der Konklusion Faktenabfragen zu verwenden, die dann ebenso zu Booleschen Werten evaluieren in Abhängigkeit davon, ob das entsprechende Fakt in der Faktenbasis enthalten ist. Wie Abschni . ausgeführt hat, wirken alle Constraints eines Constraintsystems implizit konjunktiv zusammen, insbesondere lassen sich daher Constraintregeln mit identischer Prämisse (und im Falle der Refacola auch identischer Regelvariablendeklaration) zusammenfassen oder aufteilen. Sollen Regeln derart zusammengefasst werden, so werden die Generierungsmuster innerhalb der Konklusion durch Kommata getrennt. Folgender Codeausschni zeigt die Constraintregel ( )+( ) von Seite formuliert in Refacola: 173 174 175 176 177 178 179 180 181 182 183 overriding for all m1: Java.MethodDeclaration m2: Java.MethodDeclaration do if Java.overrides(m1, m2) then m1.identifier = m2.identifier, m2.accessibility >= m1.accessibility end Queries Die Refacola erlaubt Faktenabfragen sowohl innerhalb der Prämisse als auch der Konklusion. In beiden Fällen können Faktenabfragen über zusä liche Suffixe modifiziert werden. Ein dem Faktennamen hinten angestelltes Sternchen macht die Abfrage reflexiv, lässt also eine Faktenabfrage, bei welcher alle übergebenen Regelvariablen dasselbe Programmelement referenzieren, immer zu true auswerten. Für das Fakt subType, welches ausdrückt, dass ein Typ ein Subtyp eines anderen Typen ist, würde eine Abfrage subType*(t1,t2) nicht nur zu wahr auswerten, wenn t1 Subtyp von t2 ist, sondern auch, wenn t1 und t2 identisch sind. Weiterhin kann einem Faktennamen auch ein Ausrufezeichen hinten angestellt werden, um die Faktenabfrage zu negieren. Während sich das damit erreichte Verhalten in der Regel mit dem des ebenso verfügbaren not-Operators deckt, spielt der !-Suffix nur bei Verwendung von Sequenzen eine eigene Rolle. Sequenzen Als Datenstruktur für Mengen von Constraintvariablen bietet die Refacola lediglich Sequenzen, also geordnete Mengen von Constraintvariablen gleichen Typs. Die Refacola bietet weder die Möglichkeit, gezielt auf einzelne Listenelemente zuzugreifen, noch die . . Refacola Liste nach konkreten Eigenschaften wie zum Beispiel der Listenlänge zu befragen. Sta dessen werden die Sequenzen innerhalb von Regelprämissen verwendet wie Variablen, wobei für jedes Element der Liste ein Teilconstraint generiert wird, in dem die vorkommende Sequenz durch jeweils dieses Listenelement erse t wird. Die Teilconstraints werden dann konjunktiv verbunden. Angenommen, es wäre eine Programmelementart NullType definiert, die allein den null-Typen in Java umfasst, dann würde folgende Constraintregel ausdrücken, dass kein Methodenparameter mit dem Typen null deklariert werden darf: 184 185 186 187 188 189 190 191 192 193 parameterTypesNotNull for all m: Java.MethodDeclaration null: Java.NullType do if all(m), all(null) then m.parameterTypes != null end Wird innerhalb der Konklusion eine Sequenz nicht mit einer einzelnen Constraintvariablen, sondern mit einer weiteren Sequenz in Relation gese t, so werden die Elemente der Sequenzen jeweils paarweise miteinander in Relation gese t und Konjunktionen in Anzahl entsprechend der Listenlängen generiert. Haben beide Sequenzen unterschiedliche Länge, evaluiert der die beiden Sequenzen umfassende Ausdruck pauschal zu false. Folgende Regel besagt, dass bei Methodenaufrufen die Argumen ypen identisch oder Subtypen der Parametertypen sein müssen: 194 195 196 197 198 199 200 201 202 203 methodBinding for all r: Java.MethodReference d: Java.MethodDeclaration do if Java.binds(r, d) then Java.subType*(r.argumentTypes,d.parameterTypes) end Soweit die Listen der Argumente und Parameter gleiche Länge haben, werden entsprechend der Listenlänge Constraints generiert, in denen paarweise gefordert wird, dass jeder Argumen yp jeweils Subtyp des zugehörigen Parametertypen ist. Haben die Listen unterschiedliche Längen, erzeugt die Regel für die jeweilige Regelvariablenbelegung nur den Ausdruck false. Das ist auch sinnvoll, schließlich würde bei unterschiedlich langer Parameter- und Argumentliste der Methodenaufruf nicht mehr an die Methode binden. Für manche Regeln kann es gewünscht sein, Abfragen für Sequenzen zu negieren, wobei der not-Operator nicht immer zum gewünschten Ergebnis führt. So liefert eine Konklusion 204 not Java.subType(m1.parameterTypes,m2.parameterTypes) . Constraintbasierte Refaktorisierungen bei gleichlangen Parameterlisten für m1 und m2 ein Constraint ( ) ¬( subType m1 .parameterType1 , m2 .parameterType1 ( ) ∧ subType m1 .parameterType2 , m2 .parameterType2 ∧ . . .) Ist hingegen eine Negation jeder einzelnen Konjunktion vorgesehen ( ) ¬subType m1 .parameterType1 , m2 .parameterType1 ( ) ∧ ¬subType m1 .parameterType2 , m2 .parameterType2 ∧ ¬... kann innerhalb der Refacola einer Query ein Ausrufezeichen hinten angestellt werden: 205 Java.subType!(m1.parameterTypes,m2.parameterTypes) Dabei ließe sich ein !-Suffix auch mit dem *-Suffix kombinieren. Kommentare Die Refacola erlaubt Kommentare, wie sie auch in Java definiert sind. Möglich sind entweder Blockkommentare (/*... */) oder durch zwei Schrägstriche // eingeleitete einzeilige Kommentare jeweils bis zum Zeilenende. 3.6.3. Refaktorisierungsdefinition Den dri en benötigten Teil, um mit der Refacola ein Refaktorisierungswerkzeug zu implementieren, bildet die Definition des Werkzeugs selbst. Nachfolgend ist ein Beispiel gegeben: 206 207 import "Java.language.refacola" import "Java.rules.refacola" 208 209 210 211 212 213 214 215 216 refactoring RenameMethod languages Java uses JavaRules forced changes identifier of Java.MethodDeclaration as NewName allowed changes identifier of Java.MethodDeclaration {initial, NewName @forced} identifier of Java.MethodReference {initial, NewName @forced} Wie schon bei den Definitionen des Regelsa es gesehen, sind zunächst die zu importierenden Refacola-Dateien zu nennen, darunter mindestens eine Sprachdefinition und ein Regelsa . Nach dem Schlüsselwort refactoring ist dem zu definierenden Werkzeug ein Name zu geben, den Schlüsselwörtern languages und uses folgend jeweils die Menge von Sprachdefinitionen und Regelsä en, die für das Refaktorisierungswerkzeug Anwendung finden sollen. Nach dem Schlüsselwort forced changes ist zu benennen, durch welche Variablenarten die Intention der Refaktorisierung ausgedrückt werden kann. Im gegebenen Beispiel – einem R M – soll die Intention aus dem Ändern des identifier einer MethodDeclaration bestehen. Soweit die Intention einer Werkzeuganwendung Zuweisungen an mehrere Variablen nötig macht, können in weiteren Zeilen weitere forced changes angegeben werden. . . Refacola Abbildung . .: Schematische Abbildung eines mit dem Refacola-Framework entwickelten Refaktorisierungswerkzeugs Dem Schlüsselwort allowed changes folgend sind die Arten der erlaubten Änderungen (siehe Abschni . ) der Werkzeuganwendungen zu nennen. Außer dem forced change wird das aus dieser Spezifikation generierte Werkzeug nur solche Variablen ändern, deren Variablenarten in den allowed changes gelistet sind. Für das Beispiel des R M sollen sowohl Methodenreferenzen als auch weitere Methodendeklarationen variabel sein. Le teres ist nötig, damit auch überschriebene und überschreibende Methoden mit umbenannt werden können. Neben der Möglichkeit, bei den erlaubten Änderungen Einschränkungen anhand von Variablenarten zu machen, erlaubt die Refacola ebenso auch konkret die von diesen Variablen angenommenen Wertebereiche einzuschränken. Dazu kann der Deklaration eines allowed change in geschweiften Klammern eine Liste von bestimmten, erlaubten Wertebereichen nachgestellt werden. Das Schlüsselwort initial gibt an, dass eine Variable v ihren initialen Wert init(v) annehmen darf. Über das Schlüsselwort @forced kann der Menge erlaubter Werte ein in den forced changes verwendeter Wert hinzugefügt werden, der dort über das Schlüsselwort as mit einem Namen versehen wurde. In obigem Beispiel dürfen also Methodenreferenzen und Methoden ihren Namen nur entweder beibehalten oder genau zu dem Wert ändern, der dem neuen Namen der in der Intention genannten Methode entspricht. 3.6.4. Das Refacola-Framework Die Entwicklung einer Sprachdefinition, eines Regelsa es und einer Refaktorisierungsdefinition, wie in den vorhergehenden drei Abschni en beschrieben, bilden für sich genommen noch kein ausführbares Werkzeug. Erst durch Verarbeitung mi els des Refacola-Compilers entsteht gemeinsam mit weiteren, von der Refacola zur Verfügung gestellten Bibliotheken ein ausführbares Werkzeug. Abbildung . gibt eine Übersicht über die gesamte Architektur. Sprachdefinitionen, Regelsä e und Refaktorisierungsdefinitionen werden durch den Refa- . Constraintbasierte Refaktorisierungen cola-Compiler eingelesen. Ausgabe sind dann aus Java-Code bestehende Komponenten: einerseits die sogenannte Sprach-API und andererseits die dann tatsächlich ausführbaren, constraintbasierten Werkzeuge. Die Sprach-API wird aus der Sprachdefinition und den Regelsätzen erzeugt und bildet die einzelnen Programmelemente, Variablenarten, Constraintregeln und Faktenarten auf konkrete Java-Klassen ab. Die konkreten Werkzeuge werden dann aus den Vorgaben der Refaktorisierungsdefinitionen generiert. Über entsprechende Methoden, deren Signaturen von der Sprach-API Gebrauch machen, kann einem Werkzeug die Intention der Werkzeuganwendung mitgeteilt werden. Ausgabe eines Werkzeugs ist eine ebenso anhand der Sprach-API beschriebene Menge von geänderten Variablenbelegungen (soweit das Constraintsystem lösbar ist). Intern bedient sich das generierte Werkzeug verschiedener, mit der Refacola ausgelieferter Bibliotheken (in Abbildung . als interne API bezeichnet), die insbesondere den Algorithmus zur Constraintgenerierung implementieren, ebenso aber auch abstraktes Verhalten von Elementen der Sprach-API vorgeben, welches somit nicht jedes Mal aufs Neue zu generieren ist. Weiterhin bedienen sich die generierten Werkzeuge eines generischen Constraintlösers, in der aktuellen Version der Refacola ist dies der Constraintlöser Choco . . [JRL ]. Da die Refacola sprachunabhängig ist, bietet sie von sich aus keine Komponente, die eine Überse ung der konkreten Syntax einer Programmiersprache in Programmelemente und Fakten anbietet. Daher sind durch den Werkzeugentwickler sowohl eine Abfrage-Engine, die die Auswertung von Faktenabfragen gegen den Code erlaubt, als auch eine Rückschreibekomponente, die entsprechend einer Menge von Variablenzuweisungen die entsprechenden Änderungen im Code durchführt, selbst zu entwickeln. Beide bauen dabei ganz wesentlich auf der Sprach-API auf. Diese bietet ihrerseits zum Beispiel Fabrikklassen an, um neue Programmelemente zu erzeugen und ihre Variablen mit initialen Werten zu belegen. Über entsprechende Methoden können Variablen eines Programmelementes ebenso als unveränderlich gekennzeichnet werden (zum Beispiel, falls es sich um eine importierte Bibliotheksdeklaration handelt). Weiterhin bietet die Sprach-API Funktionen, um beim Zurückschreiben für eine gegebene Constraintlösung im Vergleich zu initialen Variablenbelegungen geänderte Variablenwerte zu erkennen. Die folgenden vier Kapitel beschreiben diejenigen Komponenten näher, die über das Refacola-Framework hinausgehend zu implementieren waren, um eine Suite von constraintbasierten Refaktorisierungswerkzeugen für Java zu entwickeln. Kapitel geht auf die entwickelte Sprachdefinition für Java ein, Kapitel und erläutern die erstellten Regelsä e. Kapitel geht dann auf die einzelnen Refaktorisierungsdefinitionen sowie die Implementierung der Abfrage-Engine und Rückschreibekomponente ein. 4. Eine Sprachdefinition für Java in Refacola Dieses Kapitel beschreibt die Sprachdefinition für Java in Refacola. Gemeinsam mit den folgenden Kapiteln und liefert es eine formale Beschreibung dessen, wie sich Programmverhalten in Abhängigkeit von einer bestimmten Menge möglicher Codeänderungen – den Domänen der Variablen – beibehalten lässt. Diese Sprachdefinition bildet im Zusammenwirken mit den Constraintregeln die gemeinsame Grundlage für die im Rahmen dieser Arbeit entwickelten Refaktorisierungswerkzeuge und ist auch darüber hinaus für weitere Programmierwerkzeuge einse bar, deren mögliche Codeänderungen sich mit Hilfe der gegebenen Variablen ausdrücken lassen (Abschni . wird dies im Rahmen eines Ausblicks für QuickFix-Werkzeuge behandeln). Wie in Abschni . . beschrieben, definiert eine Refacola-Sprachdefinition die Mengen der Programmelemente P , die ihnen zugeordneten Arten von Constraintvariablen V ∗ , ihre Domänen Dv∗ sowie die Faktenarten F ∗ . 4.1. Angebotene Variablen Das Ziel dieser Arbeit ist, eine Sprachdefinition und einen zugehörigen Sa an Constraintregeln zu liefern, der Refaktorisierungswerkzeugen das korrekte Refaktorisieren von Deklarationen gesta et. Betrachtet man die in Katalogen wie dem von Fowler [Fow ] beschriebenen oder in integrierten Entwicklungsumgebungen wie Eclipse [ECL ] und IntelliJ [IJI ] implementierten Refaktorisierungen, lässt sich aus diesen ableiten, für welche Arten von Codeänderungen – also Variablen mit zugehörigen Domänen – primär Bedarf besteht. Generell lassen sich die durch die beschriebenen oder implementierten Werkzeuge ausgeführten Änderungen in zwei Kategorien aufteilen. Einerseits gibt es Änderungen, die sich auf Deklarationsebene abspielen, andererseits gibt es Änderungen, die auf Anweisungs- oder Ausdrucksebene sta finden. In erste Kategorie fallen Änderungen an Deklarationen wie Typen, Methoden und deren Parametern sowie Feldern und Variablen¹⁴. Änderungen, die diese betreffen, umfassen typischerweise das Einfügen, Verschieben und Löschen dieser Deklarationen ebenso wie das Ändern bestimmter Eigenschaften, beispielsweise Namen. Änderungen auf Anweisungs- oder Ausdrucksebene umfassen hingegen Änderungen innerhalb von Blöcken oder zwischen Blöcken, indem einzelne Ausdrücke oder Anweisungen hinzugefügt, entfernt, geändert, zusammengefasst oder verschoben werden. Viele Werkzeuge führen sowohl Änderungen auf Deklarations- als auch auf Anweisungs- und Ausdrucksebene durch. Eine M T -Refaktorisierung beispielsweise führt auf Deklarationsebene zunächst eine Verschiebung durch, bevor auf Ausdrucksebene gegebenenfalls Zugriffe auf den verschobenen Typen (neu) qualifiziert werden müssen. ¹⁴ Auch wenn die Deklaration einer Variable in Java streng genommen ein Ausdruck ist, zähle sie hier zu den Deklarationen. . Eine Sprachdefinition für Java in Refacola Nur ein verschwindend geringer Anteil an Werkzeugen spielt sich ausschließlich auf Ausdrucks- und Anweisungsebene ab. Eine dieser seltenen Ausnahmen ist C D C F [Fow ], bei welchem eine Anweisung, welche in beiden Fällen einer Fallunterscheidung enthalten ist, aus dieser herausgelöst wird. Ausgangspunkt für die Wahl der Variablen in der in dieser Arbeit entwickelten Sprachdefinition sind daher zunächst Deklarationen und ihre typischerweise durch Refaktorisierungswerkzeuge geänderten Eigenschaften. Welche Variablen dann in Folge für weitere erlaubte Änderungen auf Ausdrucks- und Anweisungsebene nötig werden, ergibt sich aus diesen. Die folgenden Abschni e beschreiben die für Deklarationen typischerweise Änderungen unterliegenden Eigenschaften: Namen, Zugrei arkeiten, Orte und Typen. Namen Variable Namen ermöglichen – wo nötig – die Bezeichner von Deklarationen wie Klassen, Feldern und Methoden, aber auch den benannten Referenzen auf diese, anzupassen. Häufigstes Anwendungsbeispiel ist zweifelsohne eine R -Refaktorisierung, bei der eine Deklaration umbenannt wird und im Rahmen der Constraintlösung entschieden wird, ob und welche Referenzen im Programm mit umbenannt werden müssen und dürfen. Aber auch im Kontext anderer Refaktorisierungen können variable Namen nü lich sein. In Java existiert die Regel, dass es pro Überse ungseinheit nur genau einen, als public-zugrei ar deklarierten Topleveltypen geben darf ( § . ). Für diesen Typen muss die umschließende Überse ungseinheit gleich diesem primären Typen¹⁵ benannt sein. Ändert nun ein Werkzeug die Zugrei arkeit eines nicht-primären Typen und macht diesen damit zu einem primären Typen (zum Beispiel, damit dieser für ein C D T verfügbar wird), kann dies zu einem Kompilierfehler führen, wenn der Name der umschließenden Überse ungseinheit dann nicht dem des primären Typen entspricht. Weitere Fälle, in denen variable Namen von Bedeutung sind, ergeben sich aus Verschieberefaktorisierungen. Wird im Kontext einer P U F -Refaktorisierung beispielsweise ein Feld in eine Superklasse gezogen, in welcher bereits ein gleichnamiges, privates Feld vorhanden ist, kann bei variablen Namen eines der beiden Felder durch den Constraintlöser gleich mit umbenannt werden.¹⁶ Die Alternative wäre (wie es auch derzeit in Eclipse, NetBeans und IntelliJ der Fall ist), dass das Refaktorisierungswerkzeug die Refaktorisierung mit einer entsprechenden Fehlermeldung abbricht. Dem Benu er bleibt dann nichts anderes übrig, als selbst zunächst ein entsprechendes R anzustoßen. Abgesehen davon, dass der Benu er bei diesem Vorgehen unnötigerweise die Verschieberefaktorisierung zweimal anstoßen und konfigurieren muss, kann es auch passieren, dass er nach der Umbenennung beim erneuten Ausführen des P U feststellt, dass nun die Refaktorisierung aus anderen Gründen scheitert (man nehme an, das zu verschiebende Feld hat noch einen Initializer, der ein Feld aus der ¹⁵ Die Java-Sprachspezifikation gibt diesem einzigen public-zugrei aren Topleveltypen einer Überse ungseinheit keinen speziellen Namen, zumal auch das Zielsystem gewisse Freiheiten hat, wie die Namen der Übersetzungseinheiten zu wählen sind ( § . ). Innerhalb dieser Arbeit wird daher die vom JDT [JDT ] verwendete, durchaus treffende Bezeichnung primärer Typ (engl. primary type) übernommen. ¹⁶ Es ist ein offenes Problem, wie dieser Name zu wählen ist. Wie Abschni . . beschreibt, kann die auf endlich viele Werte beschränkte Domäne der Namen durch eine Anzahl weiterer, bisher nicht benu ter, frischer Namen erweitert werden, was derzeit Stand der Implementierung ist. Diese künstlichen, frischen Namen werden aber die umzubenennende Variable kaum sinnvoll beschreiben. Es ist denkbar, in späteren Versionen in einem einfachen Nachbearbeitungsschri den frischen Namen gegebenenfalls durch den Benu er anpassen zu lassen oder auch während der Constraintlösung den Benu er interaktiv neue Namen wählen zu lassen. . . Angebotene Variablen 220 217 public class A { 218 219 221 222 } 223 224 class B extends A { void m() { this.n(); } void n() { this.l(); } void l() { /* ... */ } } Abbildung . .: Soll die Methode m() in Zeile aus B in die Superklasse A verschoben werden, müssen die Methoden n und l ebenso verschoben werden, da sie ansonsten auf den Empfängern this nicht mehr verfügbar sind. Ursprungsklasse referenziert). In diesem Fall geschah das vorher durchgeführte R umsonst und muss gegebenenfalls wieder rückgängig gemacht werden. Weder NetBeans noch IntelliJ prüfen derzeit bei einem Pull up Field noch weitere Bedingungen, nachdem klar ist, dass die Refaktorisierung wegen eines Namenskonflikts in der Zielklasse fehlschlägt. Lediglich Eclipse bietet eine Liste aller abzusehenden Konflikte – wobei sich diese aber dennoch nur auf die Situation vor einem R beziehen können. Zugreifbarkeiten Beinahe jedes Refaktorisierungswerkzeug hat Zugrei arkeiten von Deklarationen zu berücksichtigen. Insbesondere Verschieberefaktorisierungen, die Deklarationen und in ihnen enthaltene Referenzen zu anderen Deklarationen über Klassen- oder Paketgrenzen hinweg bewegen, machen häufig eine Anpassung von Zugrei arkeiten nötig. Seltener können variable Zugrei arkeiten auch Rename-Refaktorisierungen ermöglichen. So kann bei Umbenennung eines Feldes ein Zugriff auf dieses mehrdeutig (engl. ambigous) werden, wenn an der Aufrufestelle gleichnamige Felder aus einer Superklasse und einem Superinterface geerbt werden. Indem das Feld der Superklasse zum Beispiel auf private gese t wird, kann der Aufruf eindeutig gemacht werden. Orte Wird durch ein Refaktorisierungswerkzeug der Ort, an dem eine Deklaration steht, verändert, geschieht dies meist im Rahmen der Intention. Bei Verschieberefaktorisierungen wie Pull Up oder Push Down wählt der Benu er bei der Konfiguration der Refaktorisierung genau aus, welche Deklaration wohin verschoben werden soll. Ebenso sind Refaktorisierungswerkzeuge, die neue Deklarationen (zum Beispiel get- und set-Methoden) generieren, solche, bei denen die Änderung eines Ortes Teil der Intention ist. Praktisch können solche Werkzeuge mit Constraints derart implementiert werden, dass man die zu generierenden Methoden im Rahmen einer Vorausschau als bereits an einem Ort außerhalb des Programms (sozusagen im Nirvana) als existent annimmt und sie dann durch die Intention erst an ihren Bestimmungsort verschiebt. Dennoch kann es Sinn ergeben, Refaktorisierungswerkzeugen auch im Rahmen der Constraintlösung zuzubilligen, weitere Deklarationen eigenständig zu verschieben. Soll eine Methode in eine Superklasse verschoben werden, welche noch andere Instanzmethoden der Ursprungsklasse referenziert, müssen diese mit verschoben werden, wie das Beispiel in Abbildung . zeigt. Besondere Variabilität bei den möglichen Folgeänderungen ergibt sich, wenn sowohl Orte als auch Zugrei arkeiten variabel sind, hängt doch die Zugrei arkeit einer Deklaration nicht . Eine Sprachdefinition für Java in Refacola 229 230 226 public class A { 227 228 231 class B extends A { void m() { helper1(); helper2(); } void n() { helper1(); helper2(); } 232 } private static void helper1() {} private static void helper2() {} 233 234 235 } Abbildung . .: Sollen die Methoden m() und n() gemeinsam in die Superklasse A verschoben werden, scheint es sinnvoll, ebenso die helper-Methoden mit zu verschieben. nur vom Zugrei arkeitsmodifizierer, sondern insbesondere von den Orten der Deklaration und ihren Referenzen ab. Somit lässt sich die Zugrei arkeit einer Deklaration nicht nur erreichen, indem die Zugrei arkeitsstufe erhöht wird, sondern gegebenenfalls auch, indem eine Deklaration näher an ihre Referenzierungen gebracht wird. Abbildung . gibt hierfür ein Beispiel: Sollen beide Methoden n() und m() gemeinsam in ihre Superklasse A verschoben werden, scheint es sinnvoll, ebenso die beiden Hilfsmethoden helper1 und helper2 mit zu verschieben, schließlich erspart dies sowohl die Anpassung der Zugrei arkeiten der Hilfsmethoden, als auch die Qualifizierung ihrer Aufrufe. Zwar ermöglichen alle betrachteten Entwicklungsumgebungen – NetBeans, IntelliJ und Eclipse – dem Benu er mehr als eine Methode zum Hochziehen auszuwählen, allerdings wird keine weitere Unterstü ung geboten, um zu beurteilen, ob ein Hochziehen weiterer, nicht zwangsläufig im Supertyp benötigter Methoden sinnvoll ist. Diese Frage ist ohne genaue Codeanalyse nicht zu beantworten. Soll nämlich nur eine der beiden Methoden n() oder m() verschoben werden, scheint ein Verschieben der beiden Hilfsmethoden weniger sinnvoll. Diese müssen dann nämlich ohnehin in der Zugrei arkeit angepasst werden und auch in mindestens einer der beiden Methoden m() oder n() müssen die Aufrufe der Hilfsmethoden qualifiziert werden. Soweit ein Constraintlöser verwendet wird, der die Anzahl der Werteänderungen von Variablen im Vergleich zu ihren initialen Wertebelegungen minimieren kann (siehe hierzu Abschni . ), wird dieser automatisch abwägen, ob es sich lohnt, Zugrei arkeiten anzupassen oder Deklarationen zu verschieben. Ist ein Constraintlöser angebunden, welcher das Berechnen aller Constraintlösungen unterstü t, können dem Benu er ebenso die verschiedenen Alternativen zur Auswahl gegeben werden. Typen Die vierte unterstü te Kategorie von Variablen sind Typen, wie sie in Java genu t werden, um Felder, Methoden, Parameter und Variablen zwecks Typprüfung¹⁷ zu annotieren. Moderne IDEs bieten üblicherweise gleich mehrere Werkzeuge, die deklarierte Typen im Rahmen der Intention verändern, so zum Beispiel die Refaktorisierungswerkzeuge C M S und U S W P [TKB ], mit denen die Typen von Methodenparametern, Rückgabewerten und Variablen verändert werden können. Auch können sich Typänderungen nur indirekt aus der Intention ergeben, so bei M Refaktorisierungen. Wird eine Instanzmethode in eine andere Klasse verschoben, bedeutet dies auch, dass sich gleichzeitig alle in ihr enthaltenen Anweisungen verschieben. Ist dabei ¹⁷ und intern noch vieler weiterer Optimierungen . . Angebotene Variablen 236 237 238 239 240 241 242 243 244 class A { private String m() { String s = "abc"; return s; } String n() { return m().toString(); } } (a) ⇒ class A { Object Object m() { Object s = "abc"; return s; } String n() { return m().toString(); } } (b) Abbildung . .: Soll in Zeile der deklarierte Typ von s zu java.lang.Object geändert werden, erfordert dies auch die Änderung des Rückgabetypen der umschließenden Methode m(). eine this-Referenz enthalten, ändert diese damit im Rahmen der Intention ihren (durch den Compiler inferierten) Typen durch die Verschiebung ebenso. Weiterhin ist es bei vielen Refaktorisierungen nötig, als Folgeänderungen vorhandene deklarierte Typen zu erse en. In erster Linie betrifft dies die schon angesprochenen Typ-Refaktorisierungen C M S und U S W P , bei denen mitunter weitere Typannotationen anzupassen sind. Abbildung . gibt ein Beispiel, für das eine Anpassung des deklarierten Typen von s zu java.lang.Object ebenso eine Änderung des deklarierten Typen von m() hin zu java.lang.Object erfordert. Doch nicht nur Typrefaktorisierungen können von variablen Typen profitieren. Denkbar sind ebenso Anpassungen von Zugrei arkeiten, die erst dadurch ermöglicht werden, dass Typreferenzen erse t werden können. Abbildung . liefert ein Beispiel, in dem ein Membertyp überhaupt nur in seiner Zugrei arkeit auf private gese t werden kann, indem Typreferenzen auf diesen durch Referenzen auf seinen Supertypen erse t werden – eine Funktionalität, wie sie in den untersuchten Entwicklungsumgebungen Eclipse, IntelliJ und NetBeans innerhalb eines einzelnen Werkzeugs bisher gar nicht angeboten wird. Das Zusammenspiel von variablen Namen, Zugreifbarkeiten, Orten und Typen Abbildung . (a) zeigt ein umfangreiches Beispiel für eine Refaktorisierung, in welcher die Intention nur dann fehlerfrei erfüllt werden kann, wenn als Folgeänderungen sowohl Typ-, Namens-, Zugrei arkeits- und Ortsänderungen möglich sind. Ausgangspunkt ist die Klasse CImpl, aus welcher die Methode n() in die Superklasse A verschoben werden soll. Als erste Folgeänderung ergibt sich die Notwendigkeit, ebenso die Methode nInternal() nach A zu verschieben, da sie ansonsten von n() aus nicht mehr aufgerufen werden kann. Als Folge kommt es allerdings zu einem Problem bei der Zugrei arkeit der Klasse C, welche innerhalb der Methode nInternal(), die nun im Paket a liegt, nicht mehr referenziert werden kann. Variable Zugrei arkeiten ermöglichen, die Klasse C als public zu deklarieren, allerdings erfordert dies ebenso, auch die umschließende Überse ungseinheit CImpl.java in C.java umzubenennen, was nur mit variablen Namen möglich ist. Nachdem in Java aber nur ein ToplevelTyp pro Überse ungseinheit als public deklariert sein darf, muss nun der Typ CImpl auf Pake ugrei arkeit zurückgestuft wer- . Eine Sprachdefinition für Java in Refacola 245 246 247 248 abstract class A { static class AImpl extends A { public void m() { /* ... */ } } abstract class A { private static class AImpl extends A { public void m() { /* ... */ } } 249 static AImpl create() { return new AImpl(); } 250 251 252 static AImpl create() { return new AImpl(); } ⇒ 253 abstract void m(); 254 255 abstract void m(); } } class B { A.AImpl a = A.create(); } class B { A a = A.create(); } 256 257 258 259 (a) (b) Abbildung . .: Ist es einer C A -Refaktorisierung gesta et, auch deklarierte Typen zu ändern, muss eine Änderung der Zugrei arkeit von AImpl zu private nicht abgelehnt werden. Vielmehr können außerhalb von A Referenzen auf AImpl durch Referenzen auf A erse t werden, wie hier in Zeile geschehen. den, was aber für einen Kompilierfehler beim Aufruf der Methode m() in Zeile sorgt. In Java ist es nicht nur nötig, dass eine aufgerufene Methode zugrei ar ist, ebenso muss auch ihr jeweiliger Empfängertyp zugrei ar sein, was in diesem Fall CImpl ist. Abhilfe kann hier – variable Typen vorausgese t – eine Änderung des Rückgabetyps der Methode getC in Zeile hin zu C schaffen. Die insgesamt nötige Programmtransformation zeigt Abbildung . (b). Es ist davon auszugehen, dass ein wie in Abbildung . gezeigter Fall, der sowohl Folgeänderungen von Typen, Zugrei arkeiten, Orten und Namen nötig macht, in der Praxis nur selten auftri . Wohl ist aber davon auszugehen, dass mindestens die paarweisen Kombinationen von Folgeänderungen unterschiedlicher Art Praxisrelevanz haben, wie sie auch schon in den Abbildungen . , . und . gezeigt sind. 4.2. Taxonomie der Programmelemente Abschni . . zeigte auf, wie Programmelemente mit der Refacola definiert werden können und wie ihnen Constraintvariablen anhand von Variablenarten (den properties) zugeordnet oder an sie vererbt werden können. Die in dieser Arbeit präsentierte und in Anhang A vollständig wiedergegebene Sprachdefinition für Java in Refacola unterscheidet in ihrer Ableitungshierarchie auf oberster Ebene drei Arten von Programmelementen: Entity, Reference und Expression. Der relevante Teil der Sprachdefinition findet sich als Auszug ebenso noch einmal in Abbildung . . Entity umfasst die Menge aller Deklarationen im Programm, Reference die Menge aller Referenzen auf diese und Expression die Menge der Ausdrücke. Die Unterscheidung in Deklarationen und Referenzen folgt dem vom Compilerbau bekannten Ansa , . . Taxonomie der Programmelemente 260 261 // File "A.java" package a; // File "A.java" package a; public class A { } public class A { public void n() { nInternal(); } private void nInternal() { c.C c; /* ... */ } } // File "B.java" package c; // File "B.java" package c; public class B extends a.A { public CImpl getC() { return C.createC(); } } public class B extends a.A { public C getC() { return C.createC(); } } // File: "CImpl.java" package c; // File: " C.java " package c; 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 abstract class C extends B { public static CImpl createC() { return new CImpl(); } public void m() { /* ... */ } } ⇒ public abstract class C extends B { public static CImpl createC() { return new CImpl(); } public void m() { /* ... */ } } 286 287 288 289 290 public class CImpl extends C { public void n() { nInternal(); } private void nInternal() { c.C c; /* ... */ } } class CImpl extends C { // File: "Main.java" package main; // File: "Main.java" package main; public class Main { public void main() { new c.B().getC().m(); } } public class Main { public void main() { new c.B().getC().m(); } } } 291 292 293 294 295 296 297 298 299 (a) (b) Abbildung . .: Soll die Methode n() in Zeile in die Superklasse A verschoben werden, umfassen die nötigen Folgeänderungen sowohl das Verschieben weiterer Deklarationen, das Anpassen von Zugrei arkeiten, das Ändern deklarierter Typen und das Umbenennen einer Überse ungseinheit. . Eine Sprachdefinition für Java in Refacola 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 language Java kinds abstract Entity <: ENTITY abstract Reference <: REFERENCE abstract Expression <: ENTITY { expressionType } ... properties ... domains ... queries binds(reference: NonTypeReference, entity: Entity) receiver(reference: TypedReference, receiver: Expression) isExpression(reference: TypedReference, expression: Expression) ... Abbildung . .: Die Refacola-Sprachdefinition für Java baut auf drei abstrakten Arten von Programmelementen auf: Deklarationen (Entity), Referenzen (Reference) und Ausdrücken (Expression), die über die Bindungen (binds), Nachrichtenempfänger (receiver) und Identität von Referenzen und Ausdrücken (isExpression) miteinander verbunden sind. Definition-Use-Paare [ASU , S. - ] zu bilden, welche sich in Kombination mit dem Fakt binds ergeben, welches jeder Referenz die jeweils gebundene Deklaration zuweist.¹⁸ Die dri e Komponente – die Ausdrücke (engl. expression) ( § ) – sind solche Codebestandteile in Java, die auswertbar sind und einen (gegebenenfalls durch den Compiler zu inferierenden) Typ haben. Ausdrücke nehmen gegenüber den Deklarationen und Referenzen eine Sonderrolle ein, denn im Gegensa zu den Deklarationen bilden sie eine nicht-leere Schni menge mit den Referenzen. So ist beispielsweise eine Feldreferenz gleichzeitig eine Referenz und ein Ausdruck. Umgekehrt bilden aber weder Referenzen eine Unterart der Ausdrücke, noch Ausdrücke eine Unterart der Referenzen. So bilden zum Beispiel Literale ( § . ) in Java Ausdrücke, die keine Referenzen sind. Umgekehrt bilden Referenzen auf Pakete – beispielsweise innerhalb von Paketdeklarationen ( § . ) – und Referenzen auf Typen – zum Beispiel innerhalb eines Typ-Casts ( § . ) keine Ausdrücke. Auch unter den Referenzen auf Konstruktoren findet sich mit den this(...)- und super(...)-Aufrufen Referenzen, die keine Ausdrücke sind ( § . . . ) und im Gegensa zu void-Methoden schon allein von den syntaktischen Regeln der Sprache her niemals anstelle eines Ausdrucks eingese t werden dürfen. Man kann also weder Reference von Expression ableiten lassen, noch umgekehrt, weswegen beide parallel existieren. Fällt ein Ausdruck mit einer Referenz zusammen, wird das jeweilige Codeelement (konkret: der Knoten im abstrakten Syntaxbaum) auf zwei Refacola-Programmelemente abgebildet und diese Dualität durch ein Fakt isExpression angegeben. Über dieses können Constraintregeln dann zwischen Referenzen und den durch sie unmi elbar gebildeten Ausdrü¹⁸ Tatsächlich gilt das nicht für Typ-Referenzen. Deren Bindungen sind nicht als binds-Fakten, sondern als Constraintvariablen typeBinding der Unterart TypeReference modelliert, weswegen die Deklaration der Faktenart binds die Art der Referenzen auf NonTypeReference einschränkt. . . Fakten und Abfragen cken hin und her navigieren, was insbesondere für die Typinferenz benötigt wird. So hängt zum Beispiel der inferierte Typ einer Method Invocation Expression ( § . ) unmi elbar von der Bindung der in ihr enthaltenen Methodenreferenz ab. Innerhalb der drei Ableitungsbäume von Deklarationen, Referenzen und Ausdrücken ergibt sich die Strukturierung zumeist direkt aus den von der Java-Sprachspezifikation vorgegebenen Unterscheidungen und Nomenklaturen. So unterscheidet die Java-Sprachspezifikation bei Typen zum Beispiel zwischen Topleveltypen und geschachtelten Typen (engl. nested type). Le tere können wiederum Membertypen (engl. member type) oder innere Klassen (engl. inner class) sein. Ganz analog hierzu sieht die entwickelte Sprachdefinition eigene Programmelementarten für Topleveltypen, geschachtelte Typen, Membertypen und innere Klassen vor, wobei Membertypen und innere Klassen von geschachtelten Typen abgeleitet sind und Membertypen und Topleveltypen (über weitere Arten) von einer abstrakten Programmelementart Type ableiten. Diese Analogie ist insbesondere hilfreich bei der Formulierung der Constraintregeln. Dort, wo die Java-Sprachspezifikation Bedingungen für beliebige Arten von Typen vorgibt, kann bei Formulierung entsprechender Constraintregeln jeweils die abstrakte Programmelementart Type für die entsprechende Regelvariable verwendet werden. Gilt eine Bedingung nur für Membertypen, wird die entsprechende Unterart verwendet. Beim Erse en der Regelvariablen durch konkrete Programmelemente zum Zeitpunkt der Constraintgenerierung wählt die Refacola dann entsprechend alle Typen oder nur Membertypen aus, je nach Programmelementart der Regelvariablen. Abbildung . zeigt die Deklarationen für Java-Typen innerhalb der Refacola-Sprachdefinition als Diagramm. Jedes der horizontal dreigeteilten Rechtecke repräsentiert darin eine Programmelementart. Im oberen Dri el steht jeweils ihr Name, im mi leren Dri el sind die für sie definierten Constraintvariablen gelistet. Das untere Dri el listet die jeweils geerbten Constraintvariablen. Die Pfeile zwischen den Programmelementarten geben Ableitungsbeziehungen an. Programmelementarten, für die noch Ableitungsbeziehungen zu nicht von Type abgeleiteten Programmelementarten bestehen, sind farbig hinterlegt. Eine graphische Darstellung der gesamten Sprachdefinition ist technisch möglich [Kof ], aber wegen des resultierenden sehr großen und vor allem nicht-planaren Graphen mit vielen Linienkreuzungen nicht mehr zielführend. So zeigt auch Abbildung . nur einen Ausschni , in welchem beispielsweise die Programmelementart Class fehlt, von welcher noch einmal TopLevelClass, InnerClass und StaticMemberClass ableiten. Auch wurden aus Pla gründen einige Variablen (zum Beispiel enclosingTypes) ausgespart. Die vollständigen Definitionen ergeben sich aus dem in Anhang A gegebenen Quellcode. 4.3. Fakten und Abfragen Die für die Java-Sprachdefinition deklarierten Faktenarten ergeben sich direkt aus den zur Formulierung der Constraintregeln nötigen Abfragen. Viele der implementierten Faktenarten beschreiben einfache, sich unmi elbar aus einer Analyse der reinen Syntax eines Programms ergebende Zusammenhänge. So beschreibt zum Beispiel das zweistellige Fakt ReferenceInEntity, dass eine bestimmte Referenz unmi elbar innerhalb einer bestimmten Deklaration – meist einer Methode – enthalten ist. Andere Faktenarten beschreiben komplexere Zusammenhänge, die eine semantische Analyse nötig machen und bei denen für die Faktengenerie- . Eine Sprachdefinition für Java in Refacola Type AccessibleEntity · accessibility NullType AccessModifiableType TlOwnedEntity · tlOwner ReferenceType · accessibility · hostPackage ClassOrInterfaceType NamedEntity · identifier · accessibility TlOwnedType · accessibility · hostPackage · tlOwner ArrayableType ArrayType NamedType · componentType · elementType · accessibility · hostPackage · tlOwner · identifier TypeVariable CompilationUnitMember NamedClassOrInterfaceType · typeRoot TopLevelEnum · · · · · TopLevelClass · · · · · accessibility hostPackage identifier tlOwner typeRoot · · · · · · · · · · · · · · accessibility hostPackage identifier tlOwner typeRoot java.lang.Object ... PrimitiveType · identifier TopLevelInterface TopLevelType accessibility hostPackage identifier tlOwner typeRoot accessibility hostPackage identifier tlOwner accessibility hostPackage identifier tlOwner typeRoot PrimitiveVoid · identifier java.lang.Iterable ... PrimitiveBoolean · identifier NestedType HidableMember · accessibility · hostPackage · tlOwner LocalClass · · · · · · · · InnerClass AnonymousClass · accessibility · hostPackage · tlOwner · · · · · · identifier accessibility hostPackage identifier owner tlOwner NonStaticMember Class · · · · · PrimitiveShort MemberType · accessibility · hostPackage · tlOwner accessibility hostPackage identifier tlOwner accessibility identifier owner tlOwner accessibility hostPackage identifier owner tlOwner · · · · · InstanceMember StaticMemberType accessibility hostPackage identifier owner tlOwner · · · · · accessibility hostPackage identifier owner tlOwner StaticMember · · · · accessibility identifier owner tlOwner Abbildung . .: Graphische, ausschni sweise Darstellung der Programmelementarten für Typen, wie sie sich aus der entwickelten Refacola-Sprachdefinition für Java ergeben. . . Unveränderliche Programmeigenschaften und funktionale Einschränkungen rung zweckmäßigerweise ein Compiler befragt wird. Beispiele hierfür sind die zweistelligen binds-Fakten, die Bindungen zwischen Referenzen und Deklarationen wiedergeben, sowie die zweistelligen override-Fakten, die Paare überschreibender und überschriebener Methoden angeben. Leider lassen sich in der derzeitigen Implementierung gewisse Redundanzen nicht vermeiden, so unterstü t die Syntax der Refacola keine Formulierung transitiver Abfragen. Diese wären aber nü lich, zum Beispiel für overrides-Faktenabfragen. So unterscheidet die JavaSprachspezifikation bei den Vorausse ungen für Methodenüberschreibungen einen transitiven Fall (eine Methode m1 überschreibt eine Methode m2 , indem m1 eine Methode m3 überschreibt, die wiederum die Methode m2 überschreibt) und einen nicht-transitiven Fall, für welche unterschiedliche Anforderungen bezüglich der Zugrei arkeiten gelten ( § . . . ). Dies schlägt sich in der Implementierung zweier Faktenarten overrides und overridesDirectly nieder, wobei sich allerdings die erstere direkt aus der transitiven Hülle der le teren ergibt. 4.4. Unveränderliche Programmeigenschaften und funktionale Einschränkungen Mit steigender Anzahl der Freiheiten, die ein constraintbasiertes Werkzeug im Bezug auf mögliche Folgeänderungen neben der Intention hat, steigt die Komplexität gleich in doppelter Hinsicht. Einerseits verlangen mehr Freiheitsgrade in Form von zusä lichen Variablenarten zusä liche Constraintregeln, um die gesteigerte Anzahl möglicher Variablenbelegungen auf genau solche einzuschränken, die das Programmverhalten nicht unerwünscht verändern. Andererseits erhöht sich durch eine gesteigerte Anzahl von Variablen die Laufzeit des Programmierwerkzeugs, da die Zeiten zum Constraintlösen naturgemäß mit der Anzahl zu belegender Variablen steigen. Um einen sinnvollen Kompromiss zwischen Funktionalität und sich ergebendem Aufwand für die Regelerstellung sowie den sich ergebenden Laufzeiten zu finden, wurden in dieser Arbeit Annahmen über unveränderliche Programmeigenschaften gemacht, die im Folgenden genannt sind. Subtypbeziehungen Ableitungshierarchien von Typen sind in der vorliegenden Sprachdefinition nicht als Variablen repräsentiert. Damit sind keine Werkzeuge umse bar, die während der Constraintlösung dynamisch Änderungen der Typhierarchie vornehmen. Die dadurch zu erwartenden Einschränkungen sind aber gering. Die Prüfung auf Subtypbeziehung innerhalb diverser Constraintregeln finden derzeit über Faktenabfragen anhand der zweistelligen Faktenart sub sta . Über eine vorausschauende Anwendung [ST ] (siehe auch den Abschni über bedingte Constraints ab Seite ) sind also nach wie vor Werkzeuge umse bar, bei denen Änderungen der Typhierarchie schon vor Werkzeuganwendung vollständig bekannt sind, wie es zum Beispiel für die Replace Inheritance With Delegation-Refaktorisierung beschrie[Fow , KS ] der Fall ist. Im Katalog von Fowler [Fow ] findet sich unter den benen Werkzeugen keines, welches eine nicht im Vorhinein benennbare Änderung der Typhierarchie mit sich bringt. Ausführungsreihenfolgen von Anweisungen Ausführungsreihenfolgen von Anweisungen innerhalb von Blöcken sind derzeit in der Sprachdefinition nicht modelliert, auch exis- . Eine Sprachdefinition für Java in Refacola tieren keine Faktenabfragen für diese oder Constraintregeln, die bei (selbst vorausschauender Anwendung einer) Vertauschung von Anweisungen über eine Bedeutungserhaltung wachen könnten. Die einzige lokale Zuordnung von Anweisungen geschieht über die Faktenart ReferenceInEntity, dessen Fakten angeben, welche möglicherweise in Anweisungen enthaltenen Referenzen in welchen Deklarationen enthalten sind. Damit sind alle Arten von Programmtransformationen ausgeschlossen, die Anweisungen zwischen Blöcken verschieben oder innerhalb von Blöcken in ihrer Reihenfolge beeinflussen. Von den bei Fowler [Fow ] beschriebenen Werkzeugen betreffen nur zwei ausschließlich das Ändern von Anweisungen, nämlich I A und das in Abschni . schon erwähnte C D C F . Weitere zwei Refaktorisierungen kommen mit R E C E und R E T hinzu, wenn man davon ausgeht, dass es sich bei den hinzugefügten oder entfernten Ausnahmen um nicht in Methodensignaturen genannte Laufzeitfehler handelt. Jede der weiteren genannten Refaktorisierungen nimmt hingegen Einfluss auf Deklarationen. Ihre korrekte Behandlung ist also Vorausse ung beinahe aller Refaktorisierungen und bildet daher – wie auch ihr Titel vorgibt – den Schwerpunkt der vorliegenden Arbeit. Umgekehrt gibt es einen weiteren Anteil an Refaktorisierungen, die sowohl Deklarationen in ihren Eigenschaften ändern, hinzufügen oder entfernen, als auch Anweisungen zwischen Blöcken verschieben oder vertauschen. Ein prominentes Beispiel ist die E M Refaktorisierung [Fow ], für welche die vorliegende Arbeit zwar eine Lösung für das sichere Einfügen der neuen Methode bietet, nicht aber prüfen kann, ob durch das Einbringen des zusä lichen Methodenaufrufs anderweitige Änderungen im Programmverhalten ermöglicht werden. Die Vermutung liegt nahe, dass sich solche Bedingungen auf Anweisungsebene ebenso constraintbasiert lösen lassen wie solche für Refaktorisierungen von Deklarationen. So beschreiben Nielson et al. [NHN ] bereits constraintbasierte Analysen von Kontroll- und Datenfluss, wie sie beim Verschieben von Anweisungen relevant werden. Schäfer et al. beschreiben sogenannte synchronization dependencies [SDS+ ], die bei Refaktorisierung parallel laufendem Java-Codes einzuhalten sind und die insbesondere bei Programmtransformationen auf Ausdrücken Berücksichtigung finden müssen [SDS+ ]. Es darf vermutet werden, dass diese Abhängigkeiten analog zu den in dieser Arbeit behandelten Abhängigkeiten wie Bindungen und Überschreibungen ebenso constraintbasiert umgese t werden können. Qualifizierungen und explizite Typumwandlungen Die vorliegende Sprachdefinition betrachtet keine Qualifizierer von Namen und Ausdrücken ( §§ . , . , . . ) und auch – außer im Fall von Umbenennungen – keine Import-Deklarationen ( § . ). Bei der Modellierung des Eingabeprogramms als Constraintvariablen wird jede Referenz als unqualifiziert modelliert, auch existieren keine Constraintregeln, die über ein nötiges Qualifizieren von Referenzen wachen. Sta dessen wird in einem von der Constraintlösung separierten Postprocessing eine Prüfung aller betroffenen Referenzen durchgeführt, bei der gegebenenfalls Qualifizierer hinzugefügt oder angepasst werden. Damit findet ein Verfahren Anwendung, welches schon unter Beteiligung des Autors der vorliegenden Arbeit in [STST ] beschrieben wurde. Für dieses Vorgehen gibt es zweierlei Gründe. Einerseits wurde in der Sprache Java das Konzept der Qualifizierung derart entwickelt, dass unabhängig von Name und Ort und sons- . . Unveränderliche Programmeigenschaften und funktionale Einschränkungen tigen Eigenschaften einer Deklaration stets eine geeignete Möglichkeit zur Qualifizierung einer Referenz gefunden werden kann, womit das Finden von Qualifizierern unabhängig von Umbenennungen, Verschiebungen und Änderungen der sonstigen Eigenschaften einer Deklaration erfolgen kann.¹⁹ Andererseits liegt es in der Natur des constraintbasierten Ansa es, primär Eigenschaften von vorhandenen Programmelementen zu bedingen, aber keine Erzeugung neuer Variablen während der Constraintlösung zuzulassen. Somit wird das Einfügen von Qualifizierern zum Problem, welches sich höchstens dadurch lösen ließe, schon implizit jede Referenz als qualifiziert anzunehmen und die tatsächliche Existenz des Qualifizierers über eine Boolesche Variable zu steuern. Da Java aber gleich mehrere – teils von den Syntaxregeln her sogar unterschiedliche – Arten der Qualifikation für gleiche Arten von Referenzen anbietet, entstünden aus diesem Ansa viele bedingte – und damit teure – Constraints. Abschni . . wird im Rahmen der Beschreibung der Constraintregeln zu Verscha ungen (welche häufig ein Qualifizieren von Referenzen nötig machen) ein Beispiel zeigen und die Problematik erneut aufgreifen. Ganz ähnlich gelagert ist die Situation bei expliziten Typumwandlungen (engl. type cast) ( § . ). Der constraintbasierte Ansa ist nicht dafür ausgelegt, während der Constraintlösung neue Constraintvariablen einzufügen, sondern nur den bestehenden Constraintvariablen geeignete Werte zuzuweisen. Somit bietet sich von Haus aus keine Lösung an, nach Bedarf neue, explizite Typumwandlungen hinzuzufügen. Analog zu den Qualifizierern wäre es aber auch hier möglich, für jeden Ausdruck eines Programms eine explizite Typumwandlung anzunehmen. Ihr initialer Wert entspricht genau dem bisher vom Compiler inferierten Typen des jeweiligen Ausdruckes. Beim Zurückschreiben der durch den Constraintlöser ermi elten Variablenwerte in den Code muss dann für jeden Variablenwert geprüft werden, ob dieser vom initialen Wert abweicht. In einem solchen Fall ist eine explizite Typumwandlung in den Code einzufügen, in allen übrigen Fällen kann sie (weiterhin) entfallen. Neben der Frage, ob die damit gewonnenen Freiheitsgrade beim Refaktorisieren das dadurch entstehende Mehr an Constraintvariablen, Constraints und le tendlich Rechenzeit rechtfertigen, bleibt allerdings ebenso unklar, wie mit mehreren, direkt ineinander verschachtelten expliziten Typumwandlungen vorgegangen werden soll. Daher werden neu einzuführende, explizite Typumwandlungen in dieser Arbeit nicht weiter betrachtet. Im Programm bereits bestehende Typumwandlungen hingegen werden voll unterstü t, Abschni . . benennt die für diese implementierten Constraintregeln. Umwandlungen von Deklarationen Bisher ebenso nicht in der Sprachdefinition modelliert sind Transformationen von Deklarationen hin zu anderen Deklarationsarten. Die derzeitige Spezifikation bietet keine Möglichkeit, Arten von Programmelementen während der Constraintgenerierung anzupassen, somit kann zum Beispiel keine lokale Variable zu einem ¹⁹ Eine einzige Ausnahme bildet eine Qualifizierung mithilfe eines Typnamens, für welche der verwendete Typ vom Punkt der Referenzierung aus zugrei ar sein muss. In diesem – einigermaßen exotischen – Fall kann es tatsächlich passieren, dass eine Refaktorisierung wegen fehlender Möglichkeit der Qualifizierung abgelehnt werden muss, obwohl sie durchführbar gewesen wäre, wenn ein geeignetes Constraint zuvor die Zugrei arkeit des zur Qualifizierung notwendigen Typen gefordert hä e. [STST ] löst dieses Problem mi els einer gegebenenfalls erneuten Generierung von Zugrei arkeitsconstraints, wenn bei der Qualifizierung von Referenzen wiederum neue Referenzen mit ihrerseits neuen Zugrei arkeitsbedingungen entstehen. Dasselbe Verfahren kann auch für die in dieser Arbeit entwickelten Werkzeuge angewendet werden. . Eine Sprachdefinition für Java in Refacola Feld umgewandelt werden. Ein weiteres Beispiel liefern primitiv und nicht-primitiv typisierte Deklarationen, die ebenso durch unterschiedliche Arten von Programmelementen repräsentiert werden. So ist zum Beispiel ein C D T von einem nicht-primitiven Typen hin zu einem primitiven Typen derzeit nicht möglich.²⁰ Technisch wäre ein solches Verhalten umse bar, indem man unterschiedliche Arten von Programmelementen zu einer Art zusammenfasst und anhand einer Constraintvariablen den Constraintlöser entscheiden lässt, von welcher Art die entsprechende Deklaration ist. Dies würde allerdings bedeuten, dass bestimmte Constraintvariablen, die nur für einzelne der zusammengefassten Arten Sinn ergeben, in den übrigen Fällen eine Art null-Wert erhalten müssten. Um null-Zugriffe zu verhindern, ist dann aber mit einer deutlich erhöhten Anzahl nur teuer auszuwertender bedingter Constraints zu rechnen. Ob hierbei Kosten und Nu en in einem sinnvollen Verhältnis stehen, bleibt zu erforschen. Statische Aufrufe auf Instanzvariablen und Methodenparameter mit variabler Stelligkeit Die Sprache Java bietet zwei Komfortfunktionen, die nicht zwangsläufig Berücksichtigung in der Refacola-Sprachdefinition benötigen, da sie beide nur alternative Schreibweisen sind, die ohnehin sofort durch den Compiler erse t werden. Das sind einerseits statische Aufrufe auf Instanzvariablen und andererseits Methodenparameter mit variabler Stelligkeit (engl. variable arity parameter). Beim Aufruf statischer Methoden oder Felder besteht an und für sich keine Notwendigkeit, diese mit einer Instanz des deklarierenden Typen zu qualifizieren. Dennoch verbietet die Sprache Java einen solchen Aufruf nicht ( § . . . ), sondern erse t die jeweilige Referenz auf die Instanz implizit durch eine Referenz auf ihren deklarierten Typen. Gängige Compiler geben zusä lich eine Warnung aus. Ganz ähnlich verhält es sich mit Methodenparametern mit variabler Stelligkeit ( § . . ), welche durch den Java-Compiler intern direkt durch entsprechende, eindimensionale Array-Typen erse t werden ( § . . ). Somit erübrigt sich eine Berücksichtigung dieser beiden Kurzschreibweisen, da sie problemlos erse t und in einer Rückschreibekomponente gegebenenfalls wieder eingefügt werden können. ²⁰ Seit Java aber mit Version das Autoboxing ( § . . ) unterstü t, lässt sich hier ein Workaround recht einfach implementieren, indem zunächst constraintbasiert auf den passenden Wrapper-Typen hin refaktorisiert wird und dieser anschließend durch den entsprechenden Primitivtypen erse t wird. 5. Constraintregeln für Java Dieses und das folgende Kapitel widmen sich solchen Constraintregeln und Constraints, welche die im vorhergehenden Kapitel eingeführten Variablen derart beschränken, dass sie nur noch die für Refaktorisierungswerkzeuge so wichtigen bedeutungserhaltenden Programmtransformationen gesta en. Es liegt in der Natur der Constraints, nicht aufeinander aufzubauen, sondern gemeinsam und gleichberechtigt konjunktiv zusammenzuwirken. Aus diesem Grund fällt es schwer zu entscheiden, wie für eine didaktische Aufarbeitung die Constraintregeln zu reihen sind. Ein Hauptunterscheidungskriterium ist, ob ein Constraint anhand einer statischen Codeanalyse oder einer Laufzeitanalyse generiert wird. Dieses Kapitel widmet sich zunächst der rein statischen Analyse, Kapitel geht auf Constraintregeln ein, die sich eine Laufzeitanalyse zunu e machen. Weiterhin hat sich der Autor dieser Arbeit entschlossen, die Constraintregeln dieses Kapitels in fünf Schwerpunktkategorien zu gruppieren, von denen sich vier aus den im vorherigen Kapitel eingeführten Variablenarten ergeben: Orte (Abschi . ), Zugrei arkeiten (Abschi . ), Typen (Abschi . und . ) und Namen (Abschi . ). Ganz eindeutig ist diese Gruppierung nicht, so hängt zum Beispiel die Berechnung der Zugrei arkeit einer Deklaration von dem Ort ihrer Deklaration ab und der Typ einer Deklaration geht le tendlich auf den verwendeten Namen der ihr vorangestellten Typreferenz zurück. Regeln, bei denen sich partout keine Tendenz zu einer der vier Kategorien ausmachen lässt, sind vor allem solche, die verhindern, dass durch gemeinsames Zusammenwirken von Ort, Name, Zugrei arkeit und Typ einer Deklaration unerwünschte Bindungen oder Überschreibungsbeziehungen neu entstehen. Diese sind in einem eigenen Abschni . ausgegliedert. Für den Leser dieser Arbeit ergibt sich wegen der Unabhängigkeit der Constraintregeln voneinander die glückliche Situation, sich die Constraintregeln nach eigenen Wünschen erschließen zu können. Die Abschni e . bis . bauen nur lose aufeinander auf und können entsprechend in beliebiger Reihung gelesen werden. Bevor es an die erste, in Refacola formulierte Constraintregel geht, ein weiterer Hinweis. Der Autor dieser Arbeit hat es weder als hilfreich empfunden, innerhalb dieses und des kommenden Kapitels jede der implementierten Constraintregeln vollständig aufzulisten, noch bei den Ausführungen auf den Abdruck von Constraintregeln vollkommen zu verzichten und lediglich auf Seiten- oder Zeilennummern innerhalb von Anhang B zu verweisen, welcher ohnehin noch einmal den gesamten Sa der Constraintregeln auflistet. Vielmehr kommt eine Mischform zum Einsa , welche die besprochenen Constraintregeln – wo sinnvoll – entweder der Verständlichkeit halber innerhalb dieses Kapitels abdruckt, oder – insbesondere bei starker Ähnlichkeit zu vorhergehend erläuterten Regeln – auf Anhang B verweist. Referenzen auf im Anhang zu findende Constraintregeln geschehen jeweils anhand des (eindeutigen) Regelnamens und der jeweiligen Seitennummer mit einem vorangestellten Listenseiten-Piktogramm, )“.²¹ Darüber hinaus sind auch alle in diesem Kazum Beispiel „LocalClassAccessibility ( S. ²¹ Leser der digitalen Version dieser Arbeit dürfen sich über in diesen Referenzen verborgene Links freuen, mit . Constraintregeln für Java pitel abgedruckten Constraintregeln in Anhang B zu finden und dort in jedem Falle anhand der dort geltenden alphabetischen Sortierung der Regeln nach Regelnamen einfach auffindbar. Gewinnung von Constraintregeln Für die Gewinnung von Constraintregeln für Refaktorisierungswerkzeuge existiert kein gradliniges Verfahren, mit welchem sich systematisch ein vollständiger Sa von Constraintregeln erschließen ließe. Grundlage der Gewinnung von Constraintregeln für eine Programmiersprache ist die jeweilig zugrundeliegende Sprachspezifikation. Für die vorliegende Arbeit geht somit die Suche nach Constraintregeln von der Java-Sprachpezifikation [GJSB ] aus. Ein systematisches Durcharbeiten führt direkt zu einer Vielzahl benötigter Constraintregeln, so besagt zum Beispiel die Java-Sprachspezifikation “It is a compile-time error for a private method to be declared abstract.” ( § . . . ) Entsprechend wurde die Constraintregel AbstractMethodAccessibility ( S. ) implementiert, welche dies eins zu eins umse t, indem abstrakten Methoden untersagt wird, einen privateModifizierer anzunehmen. Dennoch führt ein systematisches, händisches Durcharbeiten der Sprachspezifikation (wie es unter anderem im Rahmen dieser Arbeit erfolgte) nicht zwangsläufig zu einem vollständigen Regelsa . So geschieht es leicht, dass unter der Menge der in der Java-Sprachspezifikation genannten Eigenschaften der Sprache bei Einzelnen verkannt wird, welche Auswirkungen sie in Bezug auf Refaktorisierungen haben können. So weist zum Beispiel in Ergänzung zu den gut ausgeführten Erklärungen zur Methodenüberschreibung ( § . . . ) ein paar Abschnitte später ( § . . . ) nur ein einzelner Sa innerhalb einer Fallunterscheidung mit unterschiedlichen Sonderfällen darauf hin, dass von einer Methodenüberschreibung auch in einem solchen Sonderfall gesprochen wird, in dem eine Klasse Methoden mit identischer Signatur sowohl aus einer Superklasse als auch einem Interface erbt. Die Folge ist, dass eine Reihe von Constraintregeln, die für Methodenüberschreibung gelten, entsprechend für diesen Sonderfall ergänzt werden müssen.²² Weiterhin gibt es auch indirekte Bedingungen, die der Sprachspezifikation nicht unmi elbar zu entnehmen sind, sondern die sich vielmehr aus den möglichen Programmtransformationen ergeben. Insbesondere betrifft dies einige technische Regeln, wie zum Beispiel Update). Diese besagt, dass beim Verschieben einer Deklaration auch alle in ReferenceOwner ( S. dieser vorkommenden Referenzen derselben Ortsveränderung unterworfen sind. Aus diesen Gründen sind Tests wesentlicher Bestandteil bei der Entwicklung korrekter Regelsä e für constraintbasierte Refaktorisierungen, während derer die Constraintregelmenge laufend ergänzt und korrigiert wird. Dies können einerseits klassische Regressionstests sein, bei denen jeweils für eine einzelne Refaktorisierung die constraintbasiert durchgeführte Programmtransformation mit einem vorgegebenen Erwartungswert verglichen wird. Andererseits können es aber auch automatisierte Tests sein, bei denen beispielsweise für ein Eingabeprogramm zunächst alle Constraints generiert werden und anschließend jede durch den Constraintlöser gefundene Programmtransformation gemäß ihrer Variablenbelegungen denen sich direkt zur jeweiligen Regel navigieren lässt. ²² Konkret entstanden die Constraintregeln EagerInterfaceImplementationAccessibility (siehe S. S. ), EagerInterfaceMethodNames ( S. ), EagerInterfaceImplementationParameterType ( S. EagerInterfaceImplementationReturnType ( S. ). , ebenso ) sowie . . Orts-Constraints in den Code zurückgeschrieben wird [Kre ]. Anhand automatisierter (zum Beispiel durch den Compiler) oder händischer Inspektion kann die Gültigkeit der Refaktorisierungen und damit der Constraintregeln für den Einzelfall untersucht werden. Kapitel liefert einen umfangreichen Überblick über die im Rahmen dieser Arbeit erfolgten abschließenden Tests, welche im Wesentlichen auf durchgeführten Refaktorisierungen auf real verwendeten, quelloffenen Programmen au auen und die Industrietauglichkeit der implementierten Refaktorisierungswerkzeuge nachweisen sollen. Während der laufenden Entwicklung der Constraintregeln bieten sich nach der Erfahrung des Autors zum Testen hingegen eher kleine Programmausschni e an, welche nach Möglichkeit die unterschiedlichen Funktionen und Eigenschaften der Sprache Java in hoher Dichte nu en. Den wertvollsten Hinweis, wie so ein Proband unkompliziert zu gewinnen ist, verdankt der Autor dieser Arbeit den von ihm betreuten Studierenden. Im Rahmen eines Fachpraktikums galt es, ein Werkzeug für das automatisierte Testen von Refaktorisierungswerkzeugen (im Stile von [TS ] und [EH ]) zu entwickeln und in einem We bewerb unter den Studierenden für ein gegebenes Refaktorisierungswerkzeug die meisten Fehler für dieses zu finden. Große Erfolge konnte dabei eine Gruppe verzeichnen, indem sie ihr Werkzeug mit Code aus gelösten Programmieraufgaben zu der im Bachelor-Studiengang obligatorischen Einführungsvorlesung zu objektorientierter Programmierung und Java fü erten. Dieser Code schöpfte naturgemäß die Mi el der Programmiersprache Java weitestgehend und in hoher Dichte aus. 5.1. Orts-Constraints In Java lässt sich der Ort einer Deklaration anhand seiner umschließenden Deklarationen beschreiben. Teilweise ist dies in Java schon durch die voll qualifizierten Namen (engl. fully qualified name) und kanonischen Namen (engl. canonical name) ( § . ) vorgegeben, mit denen eine Deklaration neben ihrem Namen auch anhand der Namen der umschließenden Deklarationen identifiziert werden kann. Auf diesem Prinzip au auend werden variable Orte umgese t, indem jede potentiell verschiebbare Deklaration eine Variable für ihre jeweils nächst äußere Deklaration erhält. Insgesamt werden drei verschiedene Arten von Orten unterstü t: • Container für Klassen TypeRoot (entweder Überse ungseinheiten in Form von *.javaoder *.class-Dateien) haben eine Variable hostPackage für das jeweils umschließende Paket. • Toplevel-Typen TopLevelType, (also nicht in andere Typen geschachtelte Typen) haben eine Variable typeRoot, die die jeweils umschließende Überse ungseinheit angibt. • Member, also Membertypen, Felder und Methoden haben eine Variable owner, die den jeweils umschließenden Typen angibt. Dies kann gegebenenfalls auch ein Toplevel-Typ sein. Bei Referenzen wird jeweils danach unterschieden, ob sie innerhalb oder außerhalb eines Typen auftreten. Typreferenzen können dabei außerhalb eines Typen zum Beispiel in einem Import oder in einer extends-Klausel auftreten. In ersterem Fall erhalten sie eine Variable für das umschließende Paket, im zweiten Falle eine Variable für die umschließende Klasse. . Constraintregeln für Java Das Law of Demeter in Refacola Obwohl jede Deklaration nach obiger Festlegung jeweils nur eine Variable für die nächst umschließende Deklaration hat, lässt sich davon ausgehend ebenso die Menge aller umschließenden Deklarationen ermi eln, indem die umschließenden Deklarationen wiederum auf ihre umschließenden Deklarationen hin befragt werden. Hierbei kommt es aber zu einem technischen Problem: die deklarative Natur der Refacola erlaubt nicht, in einer Schleife über alle umschließenden Deklarationen zu iterieren, bis beispielsweise das umschließende Paket gefunden ist. Auch bietet sie keine Möglichkeit, ähnlich wie in Java per instanceof, nach der Programmelementart einer Regelvariablen zu fragen und gegebenenfalls einen Typ-Cast durchzuführen. Dies wäre beim Iterieren aber nötig, um zu prüfen, ob ein owner ein Topleveltyp ist oder ein weiterer Membertyp, der zunächst noch weiterhin nach seinem umschließenden Typen befragt werden muss. Doch selbst wenn le teres möglich wäre, mutet es auch nicht als sonderlich elegant an, dort, wo die Iterationstiefe bekannt ist, lange Verke ungen von Indirektionen zu bilden. Soll in einer Constraintregel das Paket einer Methode in einem Topleveltypen referenziert werden, wäre dies mit einem Aufruf der Art 315 method.tlowner.typeroot.hostPackage verbunden, was in objektorientierten Sprachen als ein klarer Verstoß gegen das Gese Demeters [LHR ] zu werten wäre. In Kurzform besagt dieses Gese , dass innerhalb einer Methode nur solche Methoden aufgerufen werden sollen, die auf den dort unmi elbar verfügbaren Objekten definiert sind. Verke ete Aufrufe sind damit Tabu. Die Vorteile liegen auf der Hand, so wirken sich beispielsweise Designänderungen wie geänderte Schni stellen jeweils nur innerhalb eines Typen und seinen unmi elbaren Verwendern, nicht darüber hinaus aus. Auf die Refacola trifft dieses Gese ganz ähnlich zu. Würde man sich entschließen, eine TypeRoot nicht mehr als variabel zu betrachten (sodass dann Topleveltypen direkt eine Variable fürs umschließende Paket hä en), müsste jede Constraintregel angefasst werden, die das mögliche Paket einer Methode beschränken soll – obwohl hierfür die jeweiligen TypeRoots gar keine Rolle spielen. Die Lösung aus diesem Dilemma ist einfach: man gibt jeder Deklaration zusä liche Variablen nicht nur für die jeweils umschließende Deklaration, sondern auch für weitere relevante umschließende Deklarationen. Der Preis dafür ist derselbe wie bei einer Einhaltung des Gese Demeters in der Objektorientierung, nämlich die Einführung der zusä lichen Variablen. So erhält ein Member zusä lich zur ohnehin benötigten Variable owner auch Variablen tlowner und hostPackage, die jeweils den umschließenden Topleveltypen und das umschließende Paket benennen. Somit kann obiger Aufruf des Pakets einer Methode in Zeile durch 316 method.hostPackage erse t werden. Konsequenterweise muss dann aber auch dafür gesorgt werden, dass diese zusä lich eingefügten, sozusagen redundanten Constraintvariablen im Einklang schwingen. So dürfen die in Zeile und angegebenen Variablen niemals unterschiedliche Werte annehmen. Am einfachsten wird dies mi els zusä licher Constraints erreicht. So schreibt die folgende Constraintregel MemberOrConstructorTlOwnerAndHostPackage vor, dass sich der tlOwner und hostPackage jeweils aus den jeweiligen Variablenwerten des umschließenden owner ergeben. 317 MemberOrConstructorTlOwnerAndHostPackage . . Orts-Constraints 318 319 320 321 322 323 324 325 326 for all d: Java.MemberOrConstructor do if all(d) then d.tlowner = d.owner.tlowner, d.hostPackage = d.owner.hostPackage end Ganz analog sorgen die Regeln TopLevelTypeHostPackage ( S. ), UpdateReferenceHostPackage ( S. ), UpdateReferenceTLOwner ( S. ) und UpdateReferenceOwner ( S. ) dafür, dass auch bei weiteren redundanten Orts-Variablen die jeweiligen Variablenwerte mit den Orten der umschließenden Deklarationen übereinstimmen. Über diese Regeln hinaus ergibt sich kein weiterer Bedarf für Constraintregeln, die (ausschließlich) Orte beschränken. Für sich genommen können Deklarationen in einem Programm frei verschoben werden, Einschränkungen ergeben sich erst in Kombination mit weiteren Variablen. So müssen beim Verschieben von benannten Deklarationen sowohl Namenskonflikte als auch Zugrei arkeiten berücksichtigt werden. Beides wird jeweils im Detail in den jeweilig noch folgenden Unterabschni en behandelt. Weiterhin sind auch keine Constraints nötig, die verhindern, dass eine Deklaration an einen Ort verschoben wird, an dem dies von der Syntax der Sprache her nicht gesta et ist. So erlaubt Java zum Beispiel nicht, eine Methode außerhalb eines Typen zu deklarieren. Derartige Fehler werden aber schon durch die Typisierung der Refacola und die sich daraus ergebenden entsprechend eingeschränkten programmabhängigen Domänen vermieden. So ist die Domäne der Variable owner zum Beispiel schon auf [ClassOrInterfaceType] eingeschränkt. Es ist also sichergestellt, dass eine Methode weder in einen Primitivtypen, noch direkt in eine TypeRoot verschoben werden kann. Orts-Variablen für umschließende Blöcke Im Rahmen dieser Arbeit nicht umgese t werden Variablen für umschließende Methoden, Initializer oder andere Blöcke. Diese variabel zu halten würde bedeuten, dass ein Refaktorisierungswerkzeug eigenständig während der Constraintlösung entscheiden dürfte, einzelne Anweisungen von einer Methode in eine andere zu bewegen. Für sich genommen klingt diese Idee verlockend, könnte damit ein Werkzeug doch von sich aus entscheiden, zum Beispiel bei einer E M -Refaktorisierung weitere Anweisungen zusä lich in die neue Methode zu verschieben, um gegebenenfalls die Anzahl nötiger Methodenparameter für diese zu reduzieren. Umgekehrt sind beim Verschieben einer Anweisung über Blöcke hinweg zahlreiche Bedingungen zu berücksichtigen, um einen im Programmverhalten unveränderten Kontroll- und Datenfluss zu gewährleisten. Imperativ wurden Kontroll- und Datenflussanalysen für Refaktorisierungen bereits durch Schäfer et al. [SVEM ] umgese t. Hinweise darauf, wie die Behandlung von Kontroll- und Datenfluss möglicherweise auch constraintbasiert umgese t werden kann, gab bereits Abschni . . Ebenso von einer Analyse des Kontrollflusses abhängig ist die korrekte Behandlung finaler Felder in Java. Für diese gilt, dass diesen definitiv ein Wert zugewiesen werden muss ( § . . . ). Diese Bedingung kann verle t werden, wenn ein Feld verschoben wird und es nicht innerhalb seiner Deklaration, sondern beispielsweise in einem Konstruktor oder In- . Constraintregeln für Java itializer ( § . ) mit seinem Wert belegt wird. Dann sind nämlich Kontrollflüsse möglich, die das Feld referenzieren und gleichzeitig keine Initialisierung des Feldes beinhalten, weshalb bereits der Compiler einen Fehler meldet. Um derartige Situationen auszuschließen zu können, werden in der derzeitigen Implementierung nicht initialisierte, finale Felder bereits bei der Faktengenerierung als in ihrem Ort (owner) unveränderlich gekennzeichnet. 5.2. Zugreifbarkeits-Constraints Die Sprache Java erlaubt Programmierern mi els Zugrei arkeiten den Zugriff auf Klassen oder deren Member von anderen Klassen oder Paketen künstlich einzuschränken. Zugrei arkeit ist eine statische Programmeigenschaft, die zum Zeitpunkt des Kompiliervorganges ausgewertet wird und die mi els der Zugrei arkeitsmodifizierer public, protected und private individuell für Klassen, Methoden, Konstruktoren und Felder festgelegt wird ( § . ). Eine vierte Zugrei arkeitsstufe, die default-Zugrei arkeit, ergibt sich dadurch, dass keiner der übrigen Zugrei arkeitsmodifizierer verwendet wird ( § . . ). Die vier Zugrei arkeitsstufen haben in Java folgende Bedeutung: • Eine private-Deklaration ist nur innerhalb des Topleveltypen zugrei ar, in dem sie deklariert wurde. • Eine default-zugrei are Deklaration ist innerhalb desselben Paketes zugrei ar.²³ • Eine protected-Deklaration ist – innerhalb desselben Paketes zugrei ar und – innerhalb der Subtypen der deklarierenden Klasse zugrei ar, wenn die Aufrufstelle für die Implementierung von Subklassen-Objekten verantwortlich ist. • Eine public-Deklaration ist im gesamten Programm zugrei ar. Während die Definitionen für private-, public- und default-Zugrei arkeiten noch recht eingängig sind, gestalten sich die Zugrei arkeitsregeln für paketübergreifende Zugriffe auf Member mit protected-Modifizierer einigermaßen unintuitiv, insbesondere bei der Frage, wie es um die Verantwortlichkeit für die Implementierung eines Subtyp-Objekts steht. Folgendes Beispiel erläutert die zu unterscheidenden Fälle anschaulich: 327 package a; 328 329 330 331 332 public class A { protected A() {} protected void m() {} protected class X {} 333 protected static void n(){} 334 335 } 336 337 338 package b; public class B extends a.A { ²³ Es existieren einzelne Ausnahmen, die im folgenden Abschni thematisiert werden. . . Zugrei arkeits-Constraints B(a.A a) { super(); // zugreifbar new a.A(); // nicht zugreifbar ::::::::: new a.A() {}; // zugreifbar } 339 340 341 342 343 344 void x(a.A a, B b) { this.m(); // zugreifbar super.m(); // zugreifbar a.m(); // nicht zugreifbar ::::: b.m(); // zugreifbar 345 346 347 348 349 350 X x; // zugreifbar a.A.X y; // zugreifbar B.X z; // zugreifbar 351 352 353 354 n(); // zugreifbar 355 } 356 357 } Der Zugriff auf den protected-Konstruktor der Superklasse A in Zeile wird über einen super()-Aufruf gesta et, da dieser unmi elbar der Instanziierung eines Subtyp-Objekts dient. Die Instanziierung über new A(); in der folgenden Zeile schlägt hingegen fehl, da das hieraus erzeugte Objekt nicht in Kontext des Subtyps B steht (es ist für den Aufruf unerheblich, ob B Subtyp von A ist). Befremdlich wirkt hingegen, dass der im Endeffekt nahezu ergebnisgleibei Angabe einer (leeren) anonymen Klasse wiederum erfolgreich ist. che Aufruf in Zeile Dieser steht zwar ebenso nicht in Kontext der Klasse B, dafür aber in Kontext der damit deklarierten anonymen Klasse, die dann ihrerseits den Konstruktor von A in Form eines erlaubten ) aufruft. (impliziten) super()-Aufrufs (ähnlich wie in Zeile Ähnlich ist das Verhalten für Methoden. Die Aufrufe in den Zeilen und sind zulässig, da diese die Subtypbeziehung von A und B vorausse en. Der Aufruf von m auf einem Objekt hingegen wird durch den Compiler nicht gesta et, da dieser nicht vom Typ A in Zeile die Subtypbeziehung von A und B vorausse t. Erst beim Aufruf auf einem Objekt vom Typ B wie in Zeile ist der Zugriff erlaubt. protected-Zugrei arkeit für Felder gehorcht denselben Regeln wie die für Methoden, im obigen Beispiel könnte man die Methode m durch ein Feld i erse en und würde dieselben Einschränkungen bezüglich der Zugrei arkeit erhalten. Ohne weitere Einschränkungen hingegen kommen Zugriffe auf als protected deklarierte Membertypen und static-Member aus. Es ist völlig unerheblich, wie auf den Typen X der Superklasse A in den Zeilen bis zugegriffen wird. Dieser ist zugrei ar und auch der Zugriff auf die statische Methode in Zeile gelingt. Betrachtet man die Zugrei arkeiten in Java in ihrer Gänze, fällt auf, dass sich sowohl die Zugrei arkeitsmodifizierer als auch die Zugrei arkeitsregeln in Java nur auf Klassen und deren Member beziehen, nicht auf einzelne Objekte. So gilt zum Beispiel, dass bei Deklaration einer Methode die Zugrei arkeit dieser Methode für alle Objekte dieses Typs identisch ist. Andererseits gilt auch, dass bei Zugriff auf eine solche Methode nicht unterschieden wird, welches konkrete Objekt der Empfänger des Aufrufs ist – entscheidend ist allein der deklarierte Typ des Empfängers. Andere Programmiersprachen erlauben auch Zugrei arkeiten auf Ob- . Constraintregeln für Java jektebene, zum Beispiel Ruby, wo private Zugrei arkeit den Zugriff auf das jeweilige Objekt einschränkt, also jegliche (nicht implizit mit this) qualifizierte Aufrufe unterbunden werden. Ebenso ist es in Ruby möglich, eine Methode für ein einzelnes Objekt zu (re-)definieren, womit Zugrei arkeiten auch auf Objektebene unterschieden werden können. 5.2.1. Zugreifbarkeit als Constraintvariable Innerhalb der Refacola-Sprachdefinition für Java wird die Zugrei arkeit über eine einheitliche Variablenart accessibility der abstrakten Programmelementart AccessibleEntity modelliert. Die Domäne der Zugrei arkeiten umfasst entsprechend der obigen Auflistung die vier Zugrei arkeitsstufen, wobei die default-Zugrei arkeit eine Spezialbehandlung erfährt, um zwei Ausnahmen zu behandeln: Member von Interfaces ohne Zugrei arkeitsmodifizierer erhalten während des Kompiliervorganges einen impliziten public-Modifizierer ( § . ) und Konstruktoren von Enums sind auch ohne einen Zugrei arkeitsmodifizierer stets private ( § . ). Um Komplexität in den Constraintregeln zu reduzieren, werden diese impliziten Modifizierer bei der Faktengenerierung ganz analog zum Verhalten des Java-Compilers hinzugefügt und beim Zurückschreiben gegebenenfalls wieder entfernt. Um diesen Umstand deutlich zu machen, wird die um diese beiden Sonderfälle bereinigte default-Zugrei arkeit in der Refacola als #package-Zugrei arkeit bezeichnet. Die übrigen Zugrei arkeitsstufen sind analog zu den entsprechenden Schlüsselwörtern in Java #public, #protected und #private benannt. Da sich der Zugrei arkeitsbereich einer Deklaration von private bis hin zu public jeweils nur erweitert, kann die Domäne der Zugrei arkeiten vollständig geordnet werden: #public > #protected > #package > #private Von dem abstrakten AccessibleEntity mit seiner Eigenschaft accessibility leiten Programmelementunterarten für alle Member (also Felder, Methoden und Membertypen) sowie Konstruktoren und alle Referenztypen (also keine Primitivtypen) ab. Die Entscheidung, in der Sprachdefinition alle Referenztypen mit einem Zugrei arkeitsmodifizierer zu versehen, ist nicht ganz unbedenklich, zählen doch zu den Referenztypen auch anonyme und lokale Klassen, welche in Java keine Zugrei arkeiten aufweisen ( §§ . , . . ). In diesen beiden Fällen dennoch einen Zugrei arkeitsmodifizierer einzuführen, erlaubt aber an anderen Stellen Vereinfachungen. So haben zum Beispiel Array-Typen eine Zugrei arkeit gleich ihrer Elemen ypen ( § . . ), was durch die Regel ArrayTypeAccessibility ( S. ) umgese t wird. Hä en lokale Klassen keine accessibility-Eigenschaft, müsste bei Array-Typen ebenso zwischen solchen mit Zugrei arkeit und solchen ohne unterschieden werden. Diese Dualität würde sich über weitere Regeln, die Array-Typen bedingen (gegebenenfalls auch nur mittels Überarten), auf weitere Programmelementarten übertragen, sodass sich die Menge der Programmelementarten und Regeln unnötig erhöhen würde. Weiterhin sind in der Refacola Unterarten für den Constraintlöser unveränderlich, sodass in der Folge auch unnötigerweise Programmtransformationen ausgeschlossen werden würden, die zum Beispiel im Rahmen eines U S W P die Bindung einer Typreferenz auf eine lokale Klasse (ohne Zugrei arkeitsmodifizierer) durch eine Typbindung auf eine Toplevel-Klasse (mit Zugrei arkeitsmodifizierer) erse en. Führt man hingegen die zusä lichen accessibility-Modifizierer für lokale und anonyme Klassen ein, sind die negativen Konsequenzen gering. Da in Java auf lokale und anonyme Klassen . . Zugrei arkeits-Constraints ohnehin nicht außerhalb des umschließenden Blocks zugegriffen werden kann, wird der Modifizierer durch keine Constraints bedingt und somit während der Constraintlösung unberührt bleiben. Lediglich bei der Faktengenerierung ist eine entsprechende Berücksichtigung nötig, um einen zusä lichen Modifizierer einzufügen. Da der Java-Compiler intern für anonyme und lokale Klassen default-Zugrei arkeit vergibt (was sich leicht über Java-Reflection feststellen lässt), wird auch bei der Faktengenerierung der #package-Wert verwendet. Um zu verhindern, dass bei Auflistung aller möglichen Constraintlösungen eines Constraintsystems sich dieser Wert ändert, sichern die beiden Regeln LocalClassAccessibility ( S. ) und AnonymousClassAccessibility ( S. ) zusä lich zu, dass dieser Wert unverändert bleibt. 5.2.2. Zugreifbarkeiten zur Zugriffskontolle Der Haup weck der Zugrei arkeiten in Java ist eine Einschränkung der Orte, von denen aus bestimmte Deklarationen referenziert werden können. Umgekehrt hat die Zugrei arkeit damit Einfluss auf die Bindung von Referenzen, denn ob und an welche Deklaration eine Referenz bindet, hängt davon ab, welche Deklarationen zugrei ar sind. Wie ein Zugrei arkeitsmodifizierer einer Deklaration zu wählen ist, um einer bestimmten Referenz Zugriff auf die Deklaration zu gesta en, hängt dabei nicht nur von dem Ort der Referenz und Deklaration ab, sondern auch von der Art der referenzierten Deklaration. So wird, wie im Beispiel auf Seite gezeigt, die protected-Zugrei arkeit für Member-Typen anders definiert als für Methoden. Weiterhin schlägt sich in den zu definierenden Constraintregeln unmi elbar nieder, dass wie in Abschni . beschrieben Typbindungen als variabel angesehen werden sollen, während Methoden- und Feldbindungen durch den Constraintlöser unveränderlich bleiben. Sowohl bei der Definition der Constraintregeln, als auch bei ihrer Erklärung in diesem Abschni werden die unterschiedlichen Arten von Deklarationen daher separat behandelt. Typen Bei der Referenzierung eines Typen muss unterschieden werden, ob die Referenz innerhalb eines Typen oder außerhalb eines Typen erfolgt. Typreferenzen außerhalb von Typen können in Java einerseits in import-Anweisungen ( § . ) und andererseits bei der Deklaration eines Topleveltypen bei der Angabe von Supertypen und Superinterfaces vorkommen. Für Typreferenzen außerhalb von Typen gilt, dass sie nur Typen referenzieren dürfen, die mindestens default-zugrei ar sind ( §§ . . , . ). Insbesondere lassen sich private Membertypen damit weder importieren noch als Supertypen eines Topleveltypen verwenden, selbst wenn beide Typen in derselben Überse ungseinheit stehen. Bei Referenzierung von Typen in anderen Paketen ist darüber hinausgehend public-Zugrei arkeit nötig ( § . ). Folgende Regel TypeAccessOutsideType deckt diese Bedingungen für extends- und implements-Klauseln ab: 358 359 360 361 362 363 364 365 366 367 TypeAccessOutsideType for all tr: Java.TypeReferenceInCU type: Java.ClassOrInterfaceType do if all(type), all(tr) then (tr.typeBinding = type) −> (type.accessibility >= #package), . Constraintregeln für Java ((tr.typeBinding = type) and (tr.hostPackage != type.hostPackage)) −> (type.accessibility = #public) 368 369 370 end Die Zugrei arkeit von Typen, die innerhalb von Import-Deklarationen referenziert werden, wird über die analog formulierte Regel ImportAccessibility ( S. ) bedingt. Steht eine Typreferenz innerhalb eines Typen und wird somit zum Beispiel bei der Deklaration einer Variablen oder eines Methodenparameters verwendet, ist zusä lich die protectedZugrei arkeit zu berücksichtigen. Die beinahe identische Regel TypeAccess ( S. ), die allerdings gemäß der Regelvariablen nur für Typreferenzen innerhalb von Typen Anwendung findet, fordert daher von Typen aus fremden Paketen zunächst nur #protected-Zugrei arkeit. Ob diese dann tatsächlich genügt, hängt davon ab, ob der referenzierte Typ in einem Supertypen deklariert ist. Dabei kann nicht nur der die Typreferenz unmi elbar umschließende Typ der erbende sein, sondern auch alle weiteren die Typreferenz umschließenden Typen können diese Typreferenz erben und somit in der inneren Klasse zugrei ar sein. So gelingt in folgendem Codebeispiel das Referenzieren der protected-Klasse InA in Zeile wegen des nicht geerbten Typen InA nicht. Die beiden Referenzen in den Zeilen und hingegen sind zulässig, weil der Typ InA von A geerbt wird, wenn auch im le teren Fall nur durch die umschließende Klasse C: 371 package a; 372 373 374 375 public class A { protected class InA {} } 376 377 378 379 380 381 382 383 384 385 386 package b; class B { a.A.InA inA; :::::: class C extends a.A { a.A.InA inA; class D { a.A.InA inA; } } } Die folgende Regel MemberTypeAccess se t diese Bedingungen um. Dabei fragt die le te andBedingung die gesamte Sequenz enclosingTypes der umschließenden Typen ab und verlangt – falls zu keinem von ihnen eine Subtyp-Beziehung besteht – in Ergänzung zur Regel TypeAccess #public-Zugrei arkeit. 387 388 389 390 391 392 393 394 MemberTypeAccess for all tr: Java.TypeReferenceInType referencingType: Java.TlOwnedType type: Java.MemberType do if all(type), all(tr), all(referencingType) . . Zugrei arkeits-Constraints 395 then (tr.typeBinding = type) and (referencingType = tr.owner) and (tr.hostPackage != type.hostPackage) and Java.sub*!(referencingType.enclosingTypes, type.owner) −> type.accessibility = #public 396 397 398 399 400 401 end Konstruktoren Java kennt vier Möglichkeiten, einen Konstruktor aufzurufen. Das Beispiel ab Seite zeigte bereits drei der Möglichkeiten in den Zeilen bis , nämlich die Constructor Invocation ( § . . . ) mi els Schlüsselwort super, sowie die Class Instance Creation Expression ( § . ) mit Schlüsselwort new, einmal als reguläre Instanziierung und einmal als anonyme Klasse. Darüber hinausgehend kann eine Constructor Invocation auch über das Schlüsselwort this einen Konstruktor derselben Klasse aufrufen ( § . . . ), woraus aber bezüglich der Zugrei arkeit keine Einschränkungen folgen können, da Konstruktoren derselben Klasse immer zugrei ar sind ( § . . ). Die Menge der Konstruktoraufrufe aller vier Arten sind unter einer gemeinsamen Überart von Programmelementen ConstructorReference zusammengefasst. Für diese sichert die folgende Regel zu, dass referenzierte Konstruktoren auch zugrei ar sind: 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 ConstructorAccess for all r: Java.ConstructorReference c: Java.Constructor do if Java.binds(r, c) then (r.tlowner != c.tlowner) −> (c.accessibility >= #package), (r.hostPackage != c.hostPackage) −> (c.accessibility >= #protected), (r.hostPackage != c.hostPackage and not Java.sub*(r.owner, c.owner)) −> c.accessibility = #public end Wie aus dem Beispiel ab Seite hervorgegangen, gelten bezüglich der protected-Zugreifbarkeit bei Konstruktoraufrufen mi els new von nicht-anonymen Klassen strengere Regeln. Die nachfolgende Regel ClassInstanceCreationAccess fordert daher ergänzend zu ConstructorAccess, dass für nicht-anonyme Class Instance Creation Expressions über Paketgrenzen hinweg der aufgerufene Konstruktor als public deklariert sein muss: 417 418 419 420 421 422 423 ClassInstanceCreationAccess for all r: Java.ClassInstanceCreation c: Java.Constructor do if Java.binds(r, c) . Constraintregeln für Java 424 425 426 427 then (r.hostPackage != c.hostPackage) −> (c.accessibility >= #public) end Wie schon im Rahmen des obigen Beispiels dargelegt, gilt diese Einschränkung nicht für anonyme Klassen, da hier mit der anonymen Class Instance Creation Expression gleichzeitig ein Subtyp erzeugt wird, von dem aus der Zugriff wieder gesta et ist. Dafür gibt es für diese eine neue Einschränkung, nämlich dass bei einem anonymen Konstruktoraufruf der jeweils passendste Superkonstruktor mit gleicher Parameteranzahl zugrei ar sein muss ( § . . ), welcher aus dem impliziten Konstruktor der anonymen Klasse ( § . . . ) implizit mit aufgerufen wird. Die folgende Regel sichert diese Bedingung zu: 428 429 430 431 432 433 434 435 436 437 438 439 440 AnonymousClassConstructorAccessibility for all constructor: Java.Constructor anonymousConstructor: Java.AnonymousClassConstructor do if Java.isAnonymousSuperConstructor(constructor, anonymousConstructor) then (constructor.tlowner != anonymousConstructor.tlowner) −> (constructor.accessibility >= #package), (constructor.hostPackage != anonymousConstructor.hostPackage) −> (constructor.accessibility >= #protected) end Es genügt, nur nach #private-, #package- und #protected-Zugrei arkeit zu unterscheiden. Da der Superkonstruktor zwingend in einer Superklasse liegt, reicht #protected-Zugrei arkeit in jedem Fall. Eine le te aus Konstruktoraufrufen resultierende Eigenschaft zur Zugrei arkeit bedingt nicht die referenzierten Konstruktoren, sondern ihre umschließenden Typen. So erfordert ein Konstruktoraufruf in Java stets auch, dass die instanziierte Klasse zugrei ar ist ( § . . ). In folgendem Codebeispiel ist von der Klasse B aus der Konstruktor A() zwar zugrei ar, nicht jedoch der Typ A, weswegen der Aufruf in Zeile fehlschlägt. 441 package a; 442 443 444 445 class A { public A() {} } 446 447 package b; 448 449 450 451 public class B { Object o = :::: new :::: a.A(); } Die folgende Regel fordert bei Konstruktoraufrufen zusä lich auch die Zugrei arkeit des den Konstruktor umschließenden Typen. 452 ConstructorAccessRequiresTypeAccessibility . . Zugrei arkeits-Constraints 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 for all r: Java.ConstructorReference c: Java.Constructor type: Java.AccessModifiableType do if Java.binds(r, c), Java.instantiates(c,type) then (r.tlowner != c.tlowner) −> (type.accessibility >= #package), (r.hostPackage != c.hostPackage) −> (type.accessibility >= #protected), (r.hostPackage != c.hostPackage and not Java.sub*(r.owner, c.owner)) −> type.accessibility = #public end Für die ganz ähnlich gelagerten Konstruktoraufrufe im Rahmen der Instanziierung anonymer Klassen greift die Regel AnonymousClassRequiresSuperTypeAccessibility ( S. ). Methoden und Felder Während bei Typen und Konstruktoren verschiedene Arten des Zugriffs unterschieden wurden (Typreferenzen innerhalb eines Typen oder außerhalb beziehungsweise Referenzen auf Konstruktoren als Superkonstruktoraufruf oder zur Objektinstanziierung) entfällt eine solche Unterscheidung beim Aufruf von Methoden und dem Zugriff auf Felder, da diese prinzipiell nur innerhalb von Klassen und nur durch Angabe des Methodenoder Feldnamens referenziert werden. Wie schon bei den Zugriffsregeln für Membertypen gesehen, kann auch bei Methodenaufrufen und Feldzugriffen die Prüfung auf ausreichende Zugrei arkeit in zwei Regeln aufgeteilt werden. Die Regel MethodOrFieldAccess1 ( S. ) prüft zunächst analog zu TypeAccess ( S. ), ob mindestens #package- oder #protected-Zugrei arkeit vonnöten ist. Ergänzend ) – analog zu MemberTypefordern Constraints aus der Regel MethodOrFieldAccess2 ( S. Access (siehe S. , ebenso S. ) – dass #public-Zugrei arkeit gegeben sein muss, soweit das referenzierte Member nicht aus einer Superklasse oder der Superklasse einer umschließenden Klasse entspringt. Ähnlich den Konstruktoren und durch die Regel ConstructorAccessRequiresTypeAccessibility (siehe S. , ebenso S. ) sichergestellt, ist es auch bei Methodenaufrufen und Feldzugriffen so, dass der jeweilig deklarierende Typ des referenzierten Members zugrei ar sein muss. Während bei Konstruktoren dies noch halbwegs nachvollziehbar ist – schließlich deckt sich der Name des Typen mit dem Namen des Konstruktors, womit eine Konstruktorreferenz beinahe gleichzeitig als eine Art Typreferenz gesehen werden kann –, wirkt diese Einschränkung bei Methoden und Feldreferenzen wenig eingängig. In folgendem Beispiel verden Zugriff auf die Methode m. Zwar ist diese als weigert der Java-Compiler in Zeile public-Deklaration in B zugrei ar, nicht aber ihr nur default-zugrei arer umschließender Typ A. 468 469 package a; class A {public void m() {}} 470 471 public class AFactory { . Constraintregeln für Java public A createA() { return new A(); } 472 473 474 475 } 476 477 478 479 480 package b; class B { {new a.AFactory().createA().m();} :::::::::::::::::::::: } Die Constraintregeln MethodOrFieldAccessRequiresTypeAccessibility1 ( S. ) und MethodOrFieldAccessRequiresTypeAccessibility2 ( S. ) sichern zu, dass bei Zugriff auf Methoden oder Felder auch der jeweilig umschließende Typ zugrei ar ist. Analog zu den getrennten Zugrei arkeitsregeln bei Typreferenzen zu Topleveltypen und Membertypen behandelt hierbei die erste Regel Topleveltypen, die zweite Regel Membertypen. Andere Programmiersprachen kennen übrigens ähnliche Restriktionen, was die Zugrei arkeit von Klassen bei Zugriff auf deren Member angeht, diese manifestieren sich dann aber mitunter an anderer Stelle. So verbietet C# generell, bei geschachtelten Deklarationen die Zugrei arkeit im Inneren höher zu deklarieren als im Äußeren [HWG , § . . ], womit im obigen Beispiel schon die Deklaration der public-Methode m innerhalb der default-Klasse C zu einem Kompilierfehler geführt hä e. gesehen, gelten auch für Methodenaufrufe (und FeldzuWie schon im Beispiel ab Seite griffe analog) besondere Regeln, was die protected-Zugrei arkeit angeht. Bei Aufruf einer solchen Methode aus einem anderen Paket muss das Empfängerobjekt des Methodenaufrufs oder Feldzugriffs mindestens vom Typ der den Methodenaufruf umschließenden Klasse sein. Die folgende Regel nimmt diese Überprüfung vor und zwingt das referenzierte Member gegebenenfalls, public-Zugrei arkeit anzunehmen. 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 ProtectedMethodOrFieldAccess for all reference: Java.MethodOrFieldReference declaration: Java.InstanceMember receiver: Java.ClassOrInterfaceTypedExpression do if Java.binds(reference,declaration), Java.receiver(reference,receiver) then ((reference.hostPackage != declaration.hostPackage) and (not Java.sub*(receiver.expressionType,reference.owner)) and (not Java.isThisOrSuperExpression(receiver))) −> declaration.accessibility = #public end 5.2.3. Zugreifbarkeit und Vererbung Die Zugrei arkeitsstufe einer Member-Deklaration hat nicht nur Einfluss darauf, ob diese von einer bestimmten Referenz aus zugrei ar ist, sondern bestimmt auch, ob sie an ihre Subtypen vererbt wird. So besteht die Menge der Member einer Klasse neben den selbst deklarierten Membern aus einer Teilmenge der Member ihrer unmi elbaren Superklasse und ih- . . Zugrei arkeits-Constraints ren unmi elbaren Superinterfaces ( § . . ). Dabei werden zur Vererbung nur jene Member ausgewählt, die mindestens protected-Zugrei arkeit haben oder paketweit zugrei ar und im gleichen Paket deklariert sind ( § . ). private-Member werden nie vererbt. Insgesamt kann sich damit die Situation ergeben, dass ein Member zwar von einer Aufrufestelle aus zugrei ar ist, aber nicht an das Objekt vererbt wurde, über das auf dieses zugegriffen werden soll, wie das folgende Beispiel demonstriert: 496 497 498 499 package a; public class A { void m() {} private void n() {} 500 class D extends A { { new A().n(); new D().:: n(); } } 501 502 503 504 505 506 507 } 508 509 510 511 512 513 514 class C extends b.B { { new A().m(); new C().::: m(); } } 515 516 517 package b; public class B extends a.A {} Innerhalb der Klasse D ist die private Methode n aus A zugrei ar, da sie innerhalb desselben wird daher gestatTopleveltypen deklariert ist. Der Zugriff auf diese Methode in Zeile tet. Da private Methoden aber nicht vererbt werden, ist n kein Member von D, womit der Zugriff auf diese Methode auf einer Instanz von D in der folgenden Zeile fehlschlägt. Tückisch hierbei ist, dass die Zugrei arkeitsregeln für private Member bei Vererbung und Aufruf unterschiedliche sind. Während Vererbung bei privaten Membern ausgeschlossen ist, ist ein Aufruf immerhin noch innerhalb desselben Topleveltypen gesta et. Ein weiteres Beispiel liefert der Aufruf der Methode m aus A innerhalb von C. Diese paketweit zugrei are Methode ist in C zugrei ar, da sie ebenso wie B im Paket a deklariert ist. Allerdings wird sie nicht an den Typen B außerhalb von a vererbt. Daher darf m in C zwar in Zeile auf einer Instanz von A aufgerufen werden, nicht aber in Zeile auf einer Instanz von C. Die folgende Regel InheritedMemberAccessibility se t diese Bedingungen um. 518 519 520 521 522 523 InheritedMemberAccessibility for all reference: Java.MethodOrFieldReference declaration: Java.Member receiver: Java.ClassOrInterfaceTypedExpression inheritingType: Java.ClassOrInterfaceType . Constraintregeln für Java 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 do if Java.binds(reference,declaration), Java.receiver(reference,receiver), all(inheritingType) then ((Java.sub(inheritingType, declaration.owner)) and (Java.sub*(receiver.inferredClassOrInterfaceType, inheritingType)) and (inheritingType.tlowner != reference.tlowner)) −> (declaration.accessibility >= #package), ((Java.sub(inheritingType, declaration.owner)) and (Java.sub*(receiver.inferredClassOrInterfaceType, inheritingType)) and (inheritingType.hostPackage != reference.hostPackage)) −> (declaration.accessibility >= #protected) end Während InheritedMemberAccessibility dem Problem begegnet, dass ein Member aufgerufen wird, welches an seinen Empfänger nicht vererbt wurde, gibt es auch noch die gegenteilige Situation, dass ein Empfänger gleich mehrere Member gleichen Namens erbt – der Aufruf wird mehrdeutig. In folgendem Beispiel erbt die Klasse B gleichnamige Member i und X sowohl aus ihrer Superklasse A als auch ihrem Superinterface I. 539 540 541 542 class A { int i = 0; class X {} } 543 544 545 546 547 interface I { int i = 1; class X{} } 548 549 550 551 552 553 554 555 556 class B extends A implements I { X: x; A.X y; I.X z; int j = :i ; int k = super.i; int l = I.i; } Das Erben gleichnamiger Member führt für sich genommen noch zu keinem Fehler, erst beim und kommt es zum Fehler (unqualifizierten) Aufruf dieser Member in den Zeilen ( § . . ). Wird ein Membertyp hingegen anhand seines umschließenden Typen qualifiziert oder ein geerbtes statisches Feld über seinen deklarierenden Typ oder ein nicht-statisches Feld über super qualifiziert, gelingt der Zugriff. Zwar sind Constraintregeln denkbar, die in diesen Fällen unqualifizierte und damit mehrdeutige Referenzen verhindern und damit gegebenenfalls die Refaktorisierung ablehnen, eher wünschenswert ist hingegen, durch Einfügen geeigneter Qualifizierungen derartige Mehrdeutigkeiten aufzulösen. Abschni . hat ab Seite bereits erläutert, dass dies innerhalb dieser Arbeit in einem nicht-constraintbasierten Nach- . . Zugrei arkeits-Constraints behandlungsschri erfolgt, entsprechende Constraintregeln sind also nicht zu implementieren. 5.2.4. Zugreifbarkeit bei Methodenüberschreibung und -verdeckung Ganz ähnlich zu der Vererbung hat die Zugrei arkeit von Methoden ebenso Einfluss auf Methodenüberschreibung. So kann in Java eine Methode nur dann überschrieben werden, wenn sie mindestens protected-zugrei ar ist oder paketweite Zugrei arkeit hat und innerhalb desselben Pakets definiert ist ( § . . . ). Obwohl dies auf den ersten Blick den Zugrei arkeitsregeln für Vererbung zu entsprechen scheint, trügt dieser Umstand in Wirklichkeit. Während Vererbung nämlich in jeder Klasse entlang der Vererbungshierarchie die Zugrei arkeit der jeweils geerbten Methode vorausse t, kann eine Methode auch dann überschrieben werden, wenn sie innerhalb der Vererbungshierarchie zwischen überschriebener und überschreibender Methode nicht zugrei ar ist. Das führt zu der etwas absonderlichen Situation, dass eine nicht geerbte Methode überschrieben werden kann: 557 558 559 560 package a; public class A { void m() {} } 561 562 563 564 565 class C extends b.B { @Override void m() { super.m(); } :: } 566 567 568 package b; public class B extends a.A {} In Zeile kann innerhalb von C die Methode m aus A problemlos überschrieben werden. Der Aufruf der super-Methode hingegen scheitert, weil diese durch den Umweg über B und damit das fremde Paket b nicht geerbt wird. Unabhängig davon, was von dieser Eigentümlichkeit in Java zu halten ist, sollte ein Refaktorisierungswerkzeug nicht ohne die bewusste Handhabe des Benu ers Überschreibungsbeziehungen au eben, indem es die Zugrei arkeit von überschriebenen Methoden reduziert. Im folgenden Programm könnte die Überschreibungsbeziehung zwischen den beiden Methoden m in A und B aufgehoben werden, indem die überschriebene Methode m in A mit einem private-Modifizierer versehen wird. 569 570 571 572 573 574 575 576 577 578 579 class A { void m() { System.out.println("A"); } public static void main(String[] args) { A a = new A(); if( ... ) a = new B(); a.m(); } } class B extends A { void m() { System.out.println("B"); } . Constraintregeln für Java 580 } Die folgende Regel sichert zu, dass durch Verringerung von Zugrei arkeit keine Überschreibungsbeziehungen während der Refaktorisierung gelöst werden. 581 582 583 584 585 586 587 588 589 590 591 592 OverridingAccessibility1 for all overridingMethod: Java.InstanceMethod overriddenMethod: Java.InstanceMethod do if Java.overridesDirectly(overridingMethod, overriddenMethod) then (overriddenMethod.accessibility >= #protected) or ((overriddenMethod.accessibility = #package) and (overriddenMethod.hostPackage = overridingMethod.hostPackage)) end Eine weitere Regel zur Zugrei arkeit überschreibender Methoden in Java besagt, dass eine überschreibende Methode im Vergleich zur überschriebenen Methode mit mindestens gleicher Zugrei arkeit, nicht aber geringerer Zugrei arkeit deklariert werden darf ( § . . . ). Die folgende Regel se t dies um: 593 594 595 596 597 598 599 600 601 602 OverridingAccessibility2 for all overridingMethod: Java.InstanceMethod overriddenMethod: Java.InstanceMethod do if Java.overridesDirectly(overridingMethod, overriddenMethod) then overridingMethod.accessibility >= overriddenMethod.accessibility end Neben einer direkten Methodenüberschreibung gibt es ebenso den Fall, dass eine lediglich geerbte Methode eine Interfacemethode überschreibt ( § . . . ) beziehungsweise implementiert. Folgender Code zeigt einen entsprechenden Fall. 603 604 605 606 607 608 609 interface I { void m() } class A { public void m() { } } class B extends A implements I { } Die Methode m in Zeile wird von der Klasse B geerbt und überschreibt dort die Methode m aus Zeile , ohne dass die deklarierenden Typen der beiden m-Methoden in einer Subtypbeziehung stehen. Für den Fall einer solchen – anschaulich gesprochen vorausschauenden – Interfaceimplementierung, die in der Faktenbasis durch ein Fakt eagerInterfaceImplementation modelliert wird, sorgt die folgende Regel EagerInterfaceImplementationAccessibility dafür, dass die aus der Superklasse geerbte Methode im Vergleich zur Interfacemethode keine geringere . . Zugrei arkeits-Constraints Zugrei arkeit aufweist. Da Interfacemethoden stets als public deklariert sind, genügt es, nur diesen Fall zu berücksichtigen. 610 611 612 613 614 615 616 617 618 619 620 EagerInterfaceImplementationAccessibility for all method: Java.InstanceMethod interfaceMethod: Java.InstanceMethod class: Java.Class do if Java.eagerInterfaceImplementation(class, method, interfaceMethod) then method.accessibility = #public end Oben genannte Regel OverridingAccessibility2 zu überschreibenden Methoden hat ein Pendant bei sich verdeckenden Methoden ( § . . . ), also solchen statischen Methoden, die von ihrer Zugrei arkeit her in eine Subklasse vererbt werden, aber dort durch eine weitere statische Methode mit gleicher Signatur erse t werden. Für diese gilt ebenso, dass die Zugrei arkeit einer verdeckten Methode die Zugrei arkeit der verdeckenden Methode nicht übersteigen darf, was über die Regel HidingMethodAccessibility sichergestellt wird: 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 HidingMethodAccessibility for all hidingMethod: Java.StaticMethod hiddenMethod: Java.StaticMethod do if all(hidingMethod), all(hiddenMethod) then ((hidingMethod.identifier = hiddenMethod.identifier) and Java.sub(hidingMethod.owner, hiddenMethod.owner) and (hidingMethod.parameters.declaredParameterType = hiddenMethod.parameters.declaredParameterType) and ( ((hidingMethod.hostPackage = hiddenMethod.hostPackage) and (hiddenMethod.accessibility = #package)) or (hiddenMethod.accessibility >= #protected)) −> (hidingMethod.accessibility >= hiddenMethod.accessibility)) end 5.2.5. Weitere Zugreifbarkeitsregeln Neben den bereits genannten Constraintregeln zu Zugrei arkeiten, die sich aus Zugriffen, Vererbung, Überschreibung und Verdeckung ergeben, hält die Java-Sprachpezifikation auch noch eine Reihe weiterer, einfach zu prüfender Bedingungen bereit, die sich direkt aus den jeweiligen, mit einem Zugrei arkeitsmodifizierer versehenden Deklarationen ergeben. So ist bei der Zugrei arkeit von Typen zwischen Topleveltypen und geschachtelten Typen, die innerhalb anderer Typen deklariert sind, zu unterscheiden. Im Gegensa zu geschachtelten Typen, die mit allen vier möglichen Zugrei arkeitsstufen versehen werden können, kom- . Constraintregeln für Java men für Topleveltypen nur zwei Zugrei arkeitsstufen in Frage: public und default ( was über die folgende Regel TopLevelTypeAccessibility zugesichert wird: 638 639 640 641 642 643 644 645 646 § . ), TopLevelTypeAccessibility for all tlt: Java.TopLevelType do if all(tlt) then tlt.accessibility = #package or tlt.accessibility = #public end Array-Typen haben keine eigene Deklaration, also keinen Zugrei arkeitsmodifizierer. Dennoch haben sie eine Zugrei arkeit, welche der Zugrei arkeit ihres Basisdatentyps entspricht ( § . . ). Die Regel ArrayTypeAccessibility ( S. ) se t dies um. Für abstrakte Methoden gilt, dass sie nicht gleichzeitig als private deklariert sein dürfen ( § . . . ). Die Regel AbstractMethodAccessibility ( S. ) sichert dies zu. Weiterhin gilt, dass Interface-Member stets public deklariert sind ( § . . ). Die Regel InterfaceMember ( S. ) stellt dies sicher. Ganz analog definiert die Java-Sprachspezifikation, dass enumKonstruktoren stets private sein müssen ( § . . ) (gegebenenfalls auch implizit, falls kein ) siModifizierer angegeben ist), was durch die Regel EnumConstructorAccessibility ( S. chergestellt wird. Dies gilt auch für default-Konstruktoren von Enums ( § . . ), wo hingegen für default-Konstruktoren für Klassen gilt, dass ihre Zugrei arkeit der der umschließenden Klasse entspricht, was durch die Regel DefaultConstructorAccessibility ( S. ) sichergestellt wird. 5.3. Typ-Constraints Java ist eine streng typisierte (engl. strongly typed) Programmiersprache ( § ). Das bedeutet, dass jeder auswertbare Ausdruck einen Typen hat, der schon beim Kompilieren bekannt ist und die Menge der Operationen einschränkt, die auf ihm durchgeführt werden können. Insbesondere ist für jede Variable und Methode bei Deklaration ein entsprechender (Rückgabe-)Typ anzugeben. Auf solchen Typannotationen au auend kann dann für beliebige Ausdrücke mittels Inferenzregeln durch den Java-Compiler ein passender Typ abgeleitet werden ( § . ). In erster Linie dient das Typsystem von Java damit der Fehlervermeidung, indem schon beim Kompilieren zum Beispiel sichergestellt wird, dass für jeden Methodenaufruf das referenzierte Empfängerobjekt eine entsprechende Methode implementiert. Eine weitere entscheidende Rolle spielt das Typsystem in Java dadurch, dass es Einfluss auf das Bindeverhalten von Referenzen hat. So ist es in Java erlaubt, innerhalb einer Klasse mehrere Methoden gleichen Namens und mit gleicher Parameteranzahl zu deklarieren – die Methodenüberladung ( § . . ). Über die Methode, an welche dann eine Methodenreferenz bindet, wird anhand der Typen der Methodenparameter entschieden, die sich dann für die Methoden einer Klasse jeweils mindestens in einem Parameter unterscheiden müssen. Die Constraintregeln für Typen behandeln entsprechend diese drei Schwerpunkte. Einerseits gibt es Regeln, die entsprechend der Typinferenzregeln in Java zu jedem Ausdruck den inferierten Typen bestimmen (dazu Abschni . . ). Weiterhin gibt es Regeln, die die vom . . Typ-Constraints Java-Compiler geprüften Bedingungen zur Fehlervermeidung nachbilden, insbesondere beim Member-Zugriff (dazu Abschni . . ). Zule t gibt es noch Regeln, die Bindungsänderungen verhindern (dazu Abschni . ). Wie Kapitel beschrieben hat, haben Tip et al. bereits umfangreiche Arbeiten zu Typ-Constraints geleistet [TKB , STD , BTF , FTK+ , KETF , Tip , TFK+ ]. Die in den verschiedenen Arbeiten genannten Constraintregeln gleichen sich im Kern stark, auch wenn sie teils unterschiedlich benannt sind. Tabelle . nu t daher für eine Gegenüberstellung der Constraintregeln von Tip mit denen vorliegender Arbeit den Überblicksartikel [TFK+ ]²⁴, der die von Tip et al. gesammelten Typ-Constraintregeln weitgehend zusammenfasst. Viele der dort angegebenen Constraintregeln gelten für die vorliegende Arbeit genau so und haben lediglich eine Umformulierung in Refacola erfahren. Durch den unterschiedlichen Formalismus von Tip et al. im Kontrast zur Refacola-Sprachdefinition in dieser Arbeit ist es nicht immer möglich, eine direkte Zuordnung der Constraintregeln von Tip et al. zu denen in dieser Arbeit zu geben. So vereint zum Beispiel die Constraintregel ArgumentPassing der vorliegenden Arbeit die Constraintregeln ( ) und ( ) aus [TFK+ ]. Umgekehrt erforderte die Regel ( ) von dort eine Umse ung in zwei Regeln Assignment1 und Assignment2. Ebenso nennen Tip et al. Constraintregeln, zu denen in vorliegender Arbeit keine Entsprechung gegeben wird. Die Regel ( ) aus [TFK+ ] verbietet beispielsweise, dass ein verdeckendes Feld (hiding field) in einen Supertypen des verdeckten Feldes verschoben wird. Die vorliegende Arbeit ist da weniger restriktiv, denn wenn eine solche Verschieberefaktorisierung zum Beispiel mit einer Umbenennung eines der beiden Felder einhergeht, ist eine Änderung der Bindung – und damit des Programmverhaltens – leicht zu verhindern. Doch auch wenn eine solche nicht erfolgt, kann noch eine Qualifizierung des Feldes Abhilfe schaffen (dazu Abschni . ). Da Tip hingegen nur Typen als Variablenwerte betrachtete, ist die Regel ( ) mit den daraus resultierenden Einschränkungen in seinem Kontext korrekt. Umgekehrt sind mit vorliegender Arbeit auch eine Reihe Typ-Constraintregeln hinzugekommen, die Tip et al. nicht berücksichtigen, weil sie zum Beispiel Methodenüberladung bei ihren Betrachtungen gänzlich außen vor lassen [TFK+ ]. 5.3.1. Typen als Constraintvariable Constraintvariablen für Typen finden in der Sprachdefinition für Java an drei Stellen Verwendung. • Jede Referenz auf einen Typen (TypeReference) hat eine Variablenart typeBinding, die Werte dieser Variablen geben jeweils den gebundenen Typen an. Welcher dies ist, ergibt sich vor allem aus dem Namen einer solchen Typreferenz, kann und muss also mi els Constraintregeln abgeleitet werden. Obwohl die Werte der entsprechenden Constraintvariablen damit formal gesehen redundant sind, rechtfertigt sich ihr Vorhandensein mit wesentlichen Vereinfachungen bei Formulierung der Constraintregeln. In diesen können dann nämlich die Bindevorschriften für Typreferenzen und sich aus den Typen ergebende weitere Bedingungen getrennt voneinander behandelt werden. So konnte zum Beispiel in der bereits erwähnten Constraintregel TypeAccessOutsideType (siehe S. , ²⁴ siehe dort Abbildung . Constraintregeln für Java Tabelle . .: Gegenüberstellung der Constraintregeln aus [TFK+ den Arbeit Bedingt Refacola-Regel Zuweisungskompatibilität Typinferenz Dereferenzierung Parameterübergabe Objektinstanziierung Parametertypen überschriebener Methoden Rückgabetypen überschriebener Methoden Methodenüberschreibung Hiding von Feldern return-Typen Casts this Implizite Empfänger statischer Aufrufe Object als Wurzeltyp null-Typ ] mit denen der vorliegen- Assignment1 (siehe S. , ebenso S. ) Assignment2 ( S. ) RegularTypedReferenceExpressionTypes ( S. ) MemberAccess (siehe S. , ebenso S. ) ArgumentPassing ( S. ) ConstructorIncovationExpressionTypes ( S. ) OverridingParameterType ( S. ) [TFK+ ( ) ( ), ( ) ( ), ( ) ( ), ( ) ( ) ( ) OverridingReturnType (siehe S. , ebenso S. ) OverridingMustComeFromSubtype ( S. ) ( ) ( ) — MethodReturn ( S. TypeCasts ( S. ) This ( S. ) ThisType ( S. ) ( ( ( ( — ) ) ), ( ) ( ( ) ) — — ) ] ), ( ) . . Typ-Constraints ebenso S. ) die Zugrei arkeit eines referenzierten Typen geprüft werden, ohne dass auch schon weitere Bedingungen zu Namen zu prüfen wären (dazu dann Abschni . ). • Alle Deklarationen, die in Java mit einem Typen annotiert werden (also Felder, Methoden, formale Parameter und lokale Variablen, auch solche, die zum Beispiel durch forSchleifen und catch−Blöcke deklariert werden) leiten von TypedEntity ab. Soweit der deklarierte Typ nicht einer Typvariablen entspricht, findet die Programmelementunterart RegularTypedEntity Anwendung, welches eine Variable declaredType einführt. Mit einer Typvariablen typisierte Deklarationen (TypeVariableTypedEntity) haben hingegen keine Variable für ihren deklarierten Typen. Abschni . . beschreibt, wie dieser sta dessen in Abhängigkeit vom jeweiligen Verwendungskontext inferiert werden muss. • Als dri e Programmelementart der Refacola-Sprachdefinition für Java haben Ausdrücke eine Variable expressionType für ihren jeweiligen Typen. Für TypeReference und RegularTypedEntity umfasst die Domäne der Typvariablen jeweils die Menge aller Typen im Programm. Das umfasst die durch Klassen und Interfaces deklarierten Typen, Primitivtypen und Array-Typen. Da die Sprache Java keine Beschränkung zur maximalen Dimension eines Array gibt, ist le tere Menge nicht endlich.²⁵ Daher werden nur solche Array-Typen der Domäne hinzugefügt, die tatsächlich im zu transformierenden Programm auftreten. Soll eine Refaktorisierung einen bisher nicht genu ten Array-Typen einführen, muss dieser zuvor der Domäne der Typen hinzugefügt werden. Die Domänen der Typvariablen von Ausdrücken ergeben sich aus der Art des jeweiligen Ausdrucks vor der Refaktorisierung. Bei mit Klassen oder Interfaces typisierten Ausdrücken umfasst die Domäne der Typvariablen auch nur genau solche Typen, erlaubt also weder primitive, noch Array-Typen. Ebenso können Array- und primitiv typisierte Ausdrücke jeweils nur weitere Array- und Primitivtypen annehmen. Das verbietet Refaktorisierungen, in denen zum Beispiel für ein Array ohne Elemen ugriffe der entsprechende Array-Typ durch seinen Supertypen java.lang.Object erse t wird. Umgekehrt erspart es Regeln, die dann sicherstellen müssten, dass array access expressions ( § . ) oder .length-Abfragen auf nicht Array-Typen durchgeführt werden. Diese ließen sich mit der Refacola zwar umse en, indem jeder Ausdruck über Boolesche Constraintvariablen anzeigt, ob es sich aktuell um einen primitiven, Array- oder Klassen- beziehungsweise Interfacetyp handelt, deren korrekte Belegung über weitere Regeln gesteuert wird,²⁶ allerdings scheinen Aufwand und Nu en in keinem sinnvollen Verhältnis zu stehen. Man kann davon ausgehen, dass bei bedeutungserhaltenden (!) Programmtransformationen in der Hauptsache Sub- durch Supertypen (oder umgekehrt) zu erse en sind, wie es auch typischerweise bei den Refaktorisierungen G D T und U S W P der Fall ist. Da (außer zwischen Array-Typen und java.lang.Object) zwischen den drei genannten Typ-Arten keine Subtypbeziehungen bestehen, sind die negativen Konsequenzen wohl zu verschmerzen. ²⁵ Lediglich die Java Virtual Machine Specification [LY ] gibt in § . . vor, dass ein Array maximal dimensional sein darf, was aber dennoch zu einer unpraktikabel großen Domäne der Typen führen würde. ²⁶ Eine Formulierung innerhalb einer einzelnen Regel ist bisher mit der Refacola (noch) nicht möglich, erfordert dies doch instanceof-Abfragen und Casts von Programmelementarten (Kind Casts), die bisher nicht unterstü t sind. . Constraintregeln für Java 5.3.2. Constraintregeln zur Typinferenz Die Werte, die Typ-Constraintvariablen von Typreferenzen, typisierten Deklarationen und Ausdrücken annehmen dürfen, sind nicht voneinander unabhängig. Vielmehr ergeben sie sich innerhalb des Compilers aus den Binderegeln für Typreferenzen und Typinferenzregeln der Sprache Java. So ergibt sich der Typ eines Feldes aus dem Typen, an dem die Typreferenz innerhalb seiner Deklaration bindet. Der Typ eines Ausdrucks, der ein solches Feld referenziert, ergibt sich dann wiederum aus dem Typen des Feldes. Genau dieses Verhalten muss über Typ-Constraintregeln nachgebildet werden. Der erste Schri ist dabei einfach umzuse en, so sichert die folgende Regel TypedEntityTypes zu, dass der Typ eines Feldes, einer Methode oder Variablen genau der entsprechenden Typreferenz in ihrer Deklaration entspricht: 647 648 649 650 651 652 653 654 655 656 TypedEntityTypes for all typeReference: Java.TypeReference typedEntity: Java.RegularTypedEntity do if Java.definesTypeOf(typeReference, typedEntity) then typeReference.typeBinding = typedEntity.declaredType end Von diesen Typen abhängig auch für jeden Ausdruck im Programm den korrekten Typ zu inferieren ist komplizierter, muss hier doch nach der Art des jeweiligen Ausdrucks unterschieden werden. Referenzen auf typisierte Deklarationen Im einfachsten Fall entspricht ein Ausdruck einer Referenz auf eine typisierte Deklaration. In diesem Fall sichert die Constraintregel RegularTypedReferenceExpressionTypes ( S. ) zu, dass der Typ des Ausdrucks gleich dem Typ der referenzierten Deklaration ist. Der Sonderfall einer class instance creation expression, also eines Konstruktoraufrufs mit Schlüsselwort new zur Instanzerzeugung, dessen Typ sich aus der instanziierten Klasse ergibt, wird über die Regel ConstructorIncovationExpressionTypes ( S. ) abgehandelt. Typpropagation Bei einer Reihe weiterer Arten von Ausdrücken ergibt sich der Typ des Ausdrucks jeweils aus einem weiteren, in diesem enthaltenen Ausdruck. Ganz offensichtlich ist im Fall eines geklammerten Ausdrucks, welcher wiederum einen Ausdruck bildet und dessen Typ dem Typen des geklammerten Ausdrucks entspricht ( § . . ). Ähnlich sieht es zum Beispiel beim Inkrement- und Dekrement-Operator aus ( §§ . . , . . ), bei dem der Typ des Ausdrucks jeweils dem des in- oder dekrementierten Ausdrucks entspricht. Auf diese Fälle wird schon bei der Faktengenerierung geprüft und für Ausdrücke, die in einem derartigen Verhältnis stehen, ein Fakt typePropagation angelegt, anhand dessen über die Con) die Gleichheit der Typen beider Ausdrücke zugesichert straintregel TypePropagation ( S. wird. . . Typ-Constraints Der Typ von this Die Verwendung einer this-Expression innerhalb eines nicht-statischen Kontextes ( § . . ) referenziert das Objekt, welches – bei Konstruktoraufrufen – in diesem Kontext gerade erzeugt oder dem – bei Methodenaufrufen – gerade eine Nachricht gesendet wird ( § . . ). Der vom Compiler inferierte Typ eines this-Ausdrucks entspricht daher der unmi elbar umschließenden Klasse, was die Regel this sicherstellt: 657 658 659 660 661 662 663 664 665 666 This for all thisReference: Java.ThisReference thisExpression: Java.Expression do if Java.isExpression(thisReference,thisExpression) then thisReference.owner = thisExpression.expressionType end Für den Sonderfall geschachtelter Klassen, bei denen sich das this auf eine der umschließenden Klassen bezieht und gegebenenfalls auch qualifiziert werden kann ( § . . ), kommt als Programmelement sta einer ThisReference eine QualifiedThisReference zur Anwendung. Die Constraintregel QualifiedThis ( S. ) behandelt diesen Fall analog. Casts Bei einer class cast expression ( § . ) leitet sich der Typ des Ausdrucks nicht aus Typen von Deklarationen ab, sondern aus einer explizit angegebenen Typreferenz. Java erlaubt es dem Programmierer also, mit Type Casts dem Compiler zuzusichern, dass ein von einem Ausdruck referenziertes Objekt einen bestimmten Typen hat, ohne dass dieser dies von sich aus bestätigen könnte. Damit wird die Typprüfung durch den Compiler umgangen und ein eventuell auftretender Kompilierfehler auf die Laufzeitebene (dann mit einer ClassCastException) verlagert. Aus Sicht des damit von seiner Pflicht der Typprüfung entbundenen Compilers hat ein Cast-Ausdruck genau den Typ, der vom Programmierer vorgegeben ist. Innerhalb der Refacola-Sprachdefinition für Java besteht ein Cast-Ausdruck stets aus drei Programmelementen, die über ein dreistelliges Fakt castExpression miteinander als solche gekennzeichnet sind. Im Einzelnen handelt es sich um den CastType, die Typreferenz, die den Typen referenziert, dessen Typ dem Compiler zugesichert wird, sowie die innerExpression, der Ausdruck, dessen Typ zugesichert wird, und die CastExpression, die dann den zugesicherten Typen aufweist. Neben weiteren Bedingungen, die für Casts in Java gelten müssen ( § . ), weist die Regel TypeCasts ( S. ) mit der ersten Bedingung ihrer Konklusion einer class cast expressions jeweils genau den Typen zu, welchen der Cast vorgibt. Array-Elementzugriff Weiterhin sind noch array access expressions zu berücksichtigen, die Zugriffen auf einzelne Elemente eines Arrays entsprechen ( § . ). Hier entspricht der Typ eines Elemen ugriffs dem Elemen ypen des Arrays. Die Regel ArrayAccessTypes ( S. ) se t dies um, wobei Zugriffe über mehrere Array-Dimensionen hinweg, zum Beispiel 667 i = field[x][y] . Constraintregeln für Java bei der Faktengenerierung als zwei ineinander geschachtelte Ausdrücke behandelt werden (also im Beispiel (field[x])[y]). Der ?-Operator Die Sprache Java bietet die sogenannte conditional expression ( § . ), eine Kurzschreibweise des if statement mi els eines Fragezeichenoperators. Wertet ein vor dem ? stehender Ausdruck zu true aus, wird der dem Fragezeichen folgende Ausdruck ausgeführt. Wertet er zu false aus, wird ein optionaler dri er und hinter einem Doppelpunkt anzugebender Ausdruck ausgeführt. In folgendem Beispiel wird für einen Wert von temperature größer null die Zeichenke e hot ausgegeben. Anderenfalls lautet die Ausgabe cold; 668 temperature > 0 ? System.out.print("hot") : System.out.print("cold"); Offensichtlich muss für conditional expressions ebenso wie bei if-Anweisungen gelten, dass der vorderste der drei Ausdrücke ein Boolescher Ausdruck ist (dazu Abschni . . ). Darüber hinaus haben conditional expressions allerdings selbst auch noch einen Typen, welcher sich aus den Typen der beiden hinteren Ausdrücke ergibt. Im einfachsten Fall haben beide identische Typen, dann ist auch der Typ des gesamten Ausdrucks derselbe. Formuliert man obiges Beispiel wie folgt um 669 System.out.print(temperature > 0 ? "hot" : "cold"); dann wird der Typ der conditional expression als String inferiert und kann somit als Parameter der print-Methode übergeben werden. Unterscheiden sich die inferierten Typen der beiden hinteren Ausdrücke, kommen erweiterte Inferenzregeln zum Einsa , die im Wesentlichen den speziellsten gemeinsamen Supertypen beider Ausdrücke ermi eln ( § . ). Diese lassen sich allerdings mit den Ausdrucksmöglichkeiten der Refacola derzeit (noch) nicht formulieren. Die Java-Sprachspezifikation sieht insbesondere bei den primitiven Typen verschiedene Fallunterscheidungen vor ( § . ), die es erforderlich machen, für den expressionType einer Expression nach der Art des Programmelementes zu fragen – was im Moment seitens der Refacola nicht unterstü t wird. Als Folge werden bei der Faktengenerierung die Typen von Ausdrücken innerhalb von conditional expressions als unveränderlich gekennzeichnet. Damit bleibt der mit dieser Arbeit gelieferte Regelsa korrekt, allerdings für diesen Spezialfall restriktiv. 5.3.3. Member-Zugriff und Zuweisungskompatibilität Einer der Haup wecke eines Typsystems, wie Java es bietet, ergibt sich aus der damit möglichen Typprüfung beim Nachrichtenversand, also der Überprüfung, ob eine auf einem Empfänger aufgerufene Methode (beziehungsweise ein zugegriffenes Feld) auf diesem tatsächlich definiert ist. Die folgende Regel MemberAccess sichert dies zu: 670 671 672 673 674 675 676 677 MemberAccess for all receiver: Java.Expression reference: Java.TypedReference declaration: Java.OwnedEntity do if Java.receiver(reference, receiver), . . Typ-Constraints 678 679 680 681 Java.binds(reference, declaration) then Java.sub*(receiver.expressionType,declaration.owner) end Eine Beurteilung, ob ein Empfänger eine bestimmte Nachricht versteht, kann natürlich nur gelingen, wenn für den Empfänger verlässliche Typinformationen bekannt sind. Erlaubt eine Programmiersprache Zuweisungen, muss daher auch bei jeder Zuweisung im gesamten Eingabeprogramm geprüft werden, ob der Typ der Variablen, der zugewiesen wird, mindestens dem entspricht, der zugewiesen wird. Für Java bedeutet das im Wesentlichen, dass bei einer Zuweisung der Ausdruck links des Zuweisungsoperators den gleichen Typ oder einen Supertyp des Ausdrucks rechts des Zuweisungsoperators haben muss ( § . ), was die folgende Regel Assignment1 umse t: 682 683 684 685 686 687 688 689 690 691 Assignment1 for all lhs: Java.Expression rhs: Java.Expression do if Java.assignment(lhs, rhs) then Java.sub*(rhs.expressionType,lhs.expressionType) end Darüber hinaus gibt es noch einige wenige Ausnahmen, in denen eine Zuweisung auch dann gesta et ist, wenn die beteiligten Ausdrücke in keiner Vererbungsbeziehung zueinander stehen ( § . ). Diese ergeben sich aus automatischen Typkonvertierungen zwischen Primitivtypen und ihren Wrapperklassen, den sogenannten boxing conversions ( §§ . . , . . ). So sind zum Beispiel folgende Zuweisungen in Java erlaubt, bei denen ein primitiver Typ zu seinem Wrapperklassentyp und umgekehrt gewandelt wird: 692 693 694 695 int i = 0; Integer j; j = i; i = j; Automatische Typkonvertierungen sind in dem entwickelten Regelsa für Refacola nicht berücksichtigt. Technisch wäre dies machbar, würde aber zu einer großen Zahl bedingter Constraints führen, da bei jeder Zuweisung entsprechend unterschieden werden müsste, ob automatische Typkonvertierung Anwendung finden könnte. Sta dessen wird eine einfache technische Lösung umgese t, bei welcher intern Primitivtypen und ihre Wrapperklassen als gegenseitige Subtypen zueinander in der Faktenbasis definiert werden (es wird also zum Beispiel sowohl ein Fakt sub(int, Integer) als auch sub(Integer, int) angelegt. Problematisch kann eine solch künstliche Subtypbeziehung für Constraintregeln werden, die aus anderen Gründen als der Prüfung von Zuweisungskompatibilität Subtypbeziehungen prüfen. Bei Zugrei arkeitsregeln wird dies nicht zu Problemen führen, da Primitivtypen und ihre Wrapperklassen (sowie deren Member) ohnehin stets unveränderlich zugrei ar sind. Einzig , ebenso S. ) werden, problematisch kann oben stehende Regel MemberAccess (siehe S. würde sie doch in der Annahme, int sei ein Subtyp von Integer, Aufrufe der Art . Constraintregeln für Java 696 697 int i = 0; i.compareTo(1); :::::::::::: gesta en, obwohl eine Methode compareTo für Primitivtypen nicht definiert ist. Da dieser Fehler aber stets und völlig unabhängig durch Einfügen eines type casts auf die jeweilige Wrapperklasse verhindert werden kann, lässt er sich gegebenenfalls beim Zurückschreiben in den Code durch ein automatisches Postprocessing leicht korrigieren. Java erlaubt Zuweisungen nicht nur zwischen zwei Ausdrücken, wie in obigem Beispiel in den Zeilen und , sondern ebenso auch direkt bei der Deklaration einer Variablen wie in Zeile . Dies macht die Behandlung durch eine eigene Regel Assignment2 ( S. ) nötig, die analog zur oben bereits beschriebenen Constraintregel Assignment1 (siehe S. , ebenso S. ) formuliert werden kann. Ebenso finden indirekt Zuweisungen bei Methodenaufrufen sta , wobei die Argumente des Methodenaufrufs den Methodenparametern zugewiesen werden, sodass hier ebenso Zuweisungskompatibilität gelten muss. Umgekehrt müssen auch die von einer Methode per return-Anweisung zurückgegebenen Ausdrücke zuweisungskompatibel zu dem return-Typ der Methode sein. Beides wird mi els der Regeln ArgumentPassing ( S. ) und MethodReturn ( S. ) umgese t. 5.3.4. Überschreibung Aus Methodenüberschreibungen ( § . . . ) ergeben sich insgesamt mehrere, einfach umzuse ende Typ-Constraintregeln. Zuvorderst kann eine Methode eine andere Methode nur dann überschreiben, wenn zwischen den deklarierenden Typen beider Methoden eine Subtypbeziehung besteht, was die Regel OverridingMustComeFromSubtype ( S. ) sicherstellt. Weiterhin müssen bei überschreibenden Methoden die Parametertypen übereinstimmen, was OverridingParameterType ( S. ) sicherstellt. Die Rückgabetypen von überschreibenden Methoden müssen je nach Java-Version entweder identisch sein (bis Java . ) oder in kovarianter Subtypbeziehung stehen, sodass eine überschreibende Methode also höchstens einen Subtyp des Rückgabetypen der überschriebenen Methode zurückgeben darf (ab Java ). Folgende Regel zeigt die Java . -Version: 698 699 700 701 702 703 704 705 706 707 708 OverridingReturnType for all overridingMethod: Java.RegularTypedInstanceMethod overriddenMethod: Java.RegularTypedInstanceMethod do if Java.overridesDirectly(overridingMethod, overriddenMethod) then overridingMethod.declaredType = overriddenMethod.declaredType end Für Java -Projekte muss die Konklusion durch 709 Java.sub*(overridingMethodType.typeBinding, overriddenMethodType.typeBinding) erse t werden. Die beiden le tgenannten Regeln zu Parameter- und Rückgabetypen müssen nicht nur für direkte Methodenüberschreibung, sondern auch die vorausschauende Interfaceimplementie- . . Typ-Constraints rung ( § . . . ) gelten (siehe dazu Abschni . . ). Dies wird durch die Regeln EagerInterfaceImplementationParameterType ( S. ) und EagerInterfaceImplementationReturnType ( S. ) umgese t. 5.3.5. Zugriff auf statische Member Obige Regel MemberAccess greift ausnahmslos für Zugriffe auf nicht-statische Deklarationen. Das Query receiver ordnet darin jeweils einer Referenz einen Ausdruck – den Nachrichtenempfänger – zu. Zwar erlaubt Java auch, statische Member auf Ausdrücken aufzurufen, allerdings wird diese – ohnehin nicht empfohlene – Praxis, wie in Abschni . bereits erläutert, in vorliegender Arbeit nicht berücksichtigt. Sta dessen sind Aufrufe statischer Member mit einem Typen zu qualifizieren. Bei unqualifizierten Aufrufen entspricht der Empfängertyp dem die Referenz umschließenden Typen, wobei bei Membertypen auch deren umschließende Typen infrage kommen. Ebenso sind statische Imports zu berücksichtigen. Unabhängig davon, ob ein statischer Zugriff einen qualifizierenden Typen vorangestellt hat, wird bei der Faktengenerierung der Zugriff als qualifiziert angenommen, also gegebenenfalls implizit hinzugefügt. Dass dann auf diesem Typen das aufgerufene Member tatsächlich deklariert ist, sichert die Constraintregel StaticMemberAccess ( S. ) analog zum oben genannten nicht-statischen Pendant MemberAccess (siehe S. , ebenso S. ) zu. 5.3.6. Weitere Regeln Die Java-Grammatik sieht eine Reihe von Anweisungen vor, bei denen Bedingungen an den Typ der enthaltenen Ausdrücke gestellt werden. So ergibt ein if statement nur Sinn, soweit der dem if folgende Ausdruck einen Booleschen Typen hat ( § . ). Ähnliches gilt für array access expressions, bei denen der in eckigen Klammern angegebene Ausdruck für die Position ein int sein muss ( § . ). In beiden, wie auch vielen ähnlich gelagerten Fällen muss über Constraints sichergestellt sein, dass der Typ des Ausdrucks unverändert bleibt. Eine mögliche Umse ung wäre ein eigenes Fakt für jede Art von Anweisung sowie je eine Constraintregel für jedes Fakt, welches dann den Typ des Ausdrucks innerhalb der Anweisung unveränderlich macht. Einfacher ist aber, schon zum Zeitpunkt der Faktengenerierung eine vollständige Behandlung durchzuführen: die Refacola erlaubt, generierte Programmelemente als unveränderlich zu kennzeichnen – eine Funktion, die primär für Programmelemente aus unveränderlichem Code aus Programmbibliotheken gedacht ist (siehe Abschni . . ) – worüber auch die sich direkt aus der Syntax ergebenden Typeinschränkungen behandelt werden. Technisch bedingt sind die drei Regeln ArrayTypedExpressions ( S. ), ClassOrInterface) und PrimitiveTypedExpressions ( S. ). Manche ConstraintreTypedExpressions ( S. geln finden nur Anwendung für bestimmte Arten von Ausdrücken. So bezieht sich zum Bei) nur auf ClassOrspiel die Regel InheritedMemberAccessibility (siehe S. , ebenso S. InterfaceTypedExpression. Umgekehrt se t sie beim Abfragen des inferierten Typs eines solchen Ausdrucks auch voraus, dass hier nur ein ClassOrInterfaceType zurückgegeben werden kann. Da die Refacola keine Kind Casts unterstü t, kann die von Expression geerbte Variablenart expressionType keine Anwendung finden, sta dessen bringt die Unterart ClassOrInterfaceTypedExpression ihre eigene Variablenart inferredClassOrInterfaceType mit. Die Regel ClassOr- . Constraintregeln für Java InterfaceTypedExpressions ( S. ) sichert entsprechend zu, dass für eine ClassOrInterfaceTypedExpression die Variablen expressionType und inferredClassOrInterfaceType stets gleich belegt sind. Analog behandeln die beiden übrigen Regeln Array- und primitive Typen. Weiterhin Berücksichtigung in Bezug auf Typen müssen try und catch-Anweisungen finden ( § . ) . Wird innerhalb eines try-Blocks der Programmfluss durch eine Ausnahme (engl. exception) abrupt beendet, so werden anschließend die zugehörigen catch-Anweisungen dahingehend untersucht, ob eine von ihnen einen zum Ausnahmetypen passenden Parameter deklariert ( § . . ). Eine Änderung deklarierter Typen von catch-Parametern kann also darüber entscheiden, welcher catch-Block ausgeführt wird und somit das Programmverhalten im Ausnahmefall beliebig beeinflussen. Allerdings ist im Rahmen einer statischen Programmanalyse nicht entscheidbar, welche Ausnahmetypen tatsächlich auftreten können. Java liefert mit den runtime exceptions ( § . . ) ein Mi el, auch nicht deklarierte Ausnahmen auszulösen, die dennoch durch catch-Anweisungen abgefangen werden können. Aus diesem Grund kommt bei der Auswahl eines geeigneten catch-Blocks auch jeweils der Laufzei yp der Ausnahme zur Anwendung ( § . . ), welcher sich einer statischen Codeanalyse en ieht. Mit den Mi eln einer Laufzeitanalyse, wie sie ab Kapitel für Java-Reflection beschrieben wird, wäre dieses Problem lösbar, wird aber wegen seiner geringen Relevanz (im Vergleich zum Aufwand einer Laufzeitanalyse) in dieser Arbeit nicht weiter behandelt. Sta dessen werden Variablen, die deklarierte Typen von catch-Parametern beschreiben, als unveränderlich gekennzeichnet. 5.4. Typ-Constraints für generische Typen Seit Version unterstü t die Sprache Java generische Typen ( §§ . . , . . ) und Typvariablen ( § . ) beziehungsweise Typparameter²⁷, um Typen zu parametrisieren ( § . ). Generische Typen sorgen bei Refaktorisierungen für zusä liche Bedingungen, die berücksichtigt werden müssen, insbesondere, wenn deklarierte Typen selbst Gegenstand der Refaktorisierung sind. D T -Refaktorisierung, bei welAbbildung . zeigt ein Beispiel für eine C chem sich die Änderung eines deklarierten Typen ebenso auf die Belegungen von Typvariablen eines generischen Typen auswirken muss. Soll der Typ der Variablen in in Zeile zu Number geändert werden, muss ebenso die Belegung der Typvariablen T in Zeile von Double zu Number geändert werden. Als direkte Folge dessen sind ebenso die Belegung der Typvariablen beim Konstruktoraufruf in Zeile und die Typisierung von out in Zeile von Double hin zu Number anzupassen. Die vorliegende Implementierung bietet für die Transformation von Programmen mit generischen Typen Unterstü ung in eingeschränktem Umfang. Unterstü t werden in erster Linie Änderungen bei Belegungen von Typvariablen ( § . ), wie im vorstehenden Beispiel gezeigt wird. Dabei werden auch obere und untere Schranken von Wildcards (engl. bounded wildcards) ( § . . ) unterstü t, also Änderungen von beispielsweise NumberBox<? extends Double> zu NumberBox<? extends Number>. ²⁷ Die Java-Sprachspezifikation unterscheidet bei den Begriffen Typparameter und Typvariable nur sehr subtil: „It [the type parameter section of a class] defines one or more type variables that act as parameters“ ( § . . ). Innerhalb dieser Arbeit werden beide Begriffe synonym gebraucht. . . Typ-Constraints für generische Typen 710 711 712 class NumberBox<T extends Number> { T contents; } class NumberBox<T extends Number> { T contents; } class Main { public static void main(String[] args) { Double in = new Double(23); NumberBox< Double> box = new NumberBox< Double>(); booleanBox.contents = in; Double out = booleanBox.contents; } } class Main { public static void main(String[] args) { Number in = new Double(23); NumberBox<Number > box = new NumberBox<Number >(); booleanBox.contents = in; Number out = booleanBox.contents; } } 713 714 715 716 717 718 719 720 721 722 (a) ⇒ (b) Abbildung . .: Soll im Rahmen eines C D T der Typ der Variablen in in Zeile zu Number geändert werden, muss sich dies ebenso auf die Belegung der Typvariablen von box und auf den Typen von out auswirken. Nicht unterstü t werden Anpassungen von Schranken von Typvariablen (engl. bounded type variables) ( § . ). In obigem Beispiel würde eine Änderung des deklarierten Typen von in zu Object fehlschlagen, weil dann ebenso box als NumberBox<Object> zu deklarieren wäre. Dies widerspricht aber der in Zeile angegebenen oberen Schranke Number für T. Diese könnte entfallen, da von ihr innerhalb von NumberBox keine Verwendung gemacht wird (es wird auf contents keine Methode aus Number aufgerufen). Allerdings sieht die derzeitige Implementierung noch keine Variablen für Beschränkungen von Typvariablen vor, weswegen diese unveränderlich sind und die Transformation abgelehnt werden muss. Als weitere Einschränkung werden keine Transformationen unterstü t, die neue Typvariablen hinzufügen, vorhandene entfernen oder bei Verwendung von Wildcards neue obere oder untere Schranken hinzufügen oder entfernen (bestehende Schranken können aber angepasst werden). Auch sind keine Transformationen möglich, die an Verwendungsstellen generischer Typen das Hinzufügen oder Entfernen von Typisierungsinformationen nach sich ziehen müssen. Angenommen, es gäbe für obiges Beispiel aus Abbildung . zusä lich die folgenden Typdeklarationen, 723 724 class SubNumberBox<T extends Number> extends NumberBox<T> { /* ... */ } class DoubleBox extends NumberBox<Double> { /* ... */ } dann wäre eine Änderung einer Deklaration NumberBox<Double> box zum spezielleren Typen SubNumberBox<Double> box unterstü t, nicht aber zum spezielleren Typen DoubleBox, bei dessen Verwendung dann die Angabe eines Typen für die Typvariable T entfällt. Ebenso wird keine Unterstü ung geboten für die Refaktorisierung von Methoden, die generische Methoden überschreiben und dabei Typvariablen mit konkreten Typen belegen, wie es zum Beispiel bei der Klasse DoubleBox möglich wäre. Würde die Klasse NumberBox eine Methode T m() deklarieren, könnte diese mit Signatur Double m() in DoubleBox überschrieben werden. Beim Verschieben dieser Methode müsste dann gegebenenfalls wiederum eine Erse ung des . Constraintregeln für Java konkreten Typen durch die Typvariable sta finden. Der Grund für diese Einschränkungen ergibt sich aus der Modellierung der Typvariablenbelegungen als eigenes Programmelement. Programmelemente sind in der Refacola selbst nicht Teil der Constraints, sondern nur die ihnen zugeordneten Variablen. Insofern kann die Existenz oder Nichtexistenz eines Programmelements nicht bei der Constraintlösung beeinflusst werden. Zwar ließe sich so ein Verhalten mi els Boolescher Variablen modellieren, die den jeweiligen Programmelementen zugeordnet werden, dies würde aber zu einer deutlich vergrößerten Menge von Programmelementen führen (denn auch wo keine Typparameter gegeben sind, müssen als nicht-existent gekennzeichnete Programmelemente für diese eingefügt werden). Das Mehr an Programmelementen führt dann wiederum zu einem deutlichen Anstieg bedingter, und damit teurer (siehe dazu Abschni . ) Constraints. Untersuchungen, ob dieses Unterfangen lohnenswert ist, stehen aus. Da für generisch typisierte Deklarationen andere Programmelemente Verwendung finden, als für nicht generisch typisierte Programmelemente, sind ebenso Refaktorisierungen ausgeschlossen, die Typvariablen durch konkrete Typen erse en (oder umgekehrt). Mitunter verbietet dies auch Programmtransformationen, die in erster Linie gar nicht auf Änderungen deklarierter Typen abzielen, sondern diese nur als Folgeänderungen mit sich bringen. Ein Beispiel hierfür wäre das Verschieben einer überschriebenen Methode. Überschriebe zum Beispiel eine Methode aus DoubleBox eine Methode aus NumberBox, welche einen ihrer Parameter mit T typisiert, so ist in der überschriebenen Methode dieser Parameter mit der konkreten Belegung Double zu typisieren. Soll die überschreibende Methode in eine andere Klasse verschoben werden, könnte es notwendig werden, den konkreten Typen wieder durch eine Typvariable zu erse en. Insgesamt verhält sich in diesen Fällen die vorliegende Implementierung konservativ. Inkorrekte Transformationen, also solche, die außerhalb der Intention das Programmverhalten ändern oder zu nicht kompilierendem Code führen, werden unterbunden. Darüber hinaus gibt es aber Situationen, in welchen eine Transformation mit zusä lichen Änderungen wie den oben beschriebenen Anpassungen von Typschranken oder der Erse ung von konkreten Typen durch Typvariablen möglich wäre, aber dennoch unterbunden wird (sogenannte overly strong conditions [SMG ]). Tro der genannten Einschränkungen trägt die korrekte Behandlung generischer Typen weiterhin ein hohes Maß Komplexität in sich, welches in den folgenden Unterkapiteln au ereitet wird. 5.4.1. Typinferenz generisch typisierter Ausdrücke Werden Deklarationen innerhalb eines generischen Typen mit Typvariablen typisiert, folgt daraus direkt, dass der durch den Compiler inferierte Typ für diese Deklarationen nicht mehr an allen Verwendungsstellen identisch ist. Innerhalb der Deklaration eines generischen Typen kann für eine Typvariable dieses Typen zunächst gar kein Typ – beziehungsweise lediglich Object – angenommen werden,²⁸ soweit Typschranken keine anderen Annahmen erlauben (dazu weiteres in Abschni . . ). Bei der Verwendung generischer Typen kann der inferierte Typ einer mit Typvariable typisierten Deklaration je nach Verwendungsstelle völlig unter²⁸ Der Fall, dass bei der Deklaration eines generischen Typen derselbe Typ wiederum verwendet wird, sei hier ausgenommen. . . Typ-Constraints für generische Typen schiedlich lauten, je nach dem, wie die jeweilige Typvariable belegt wurde. Folgendes Beispiel illustriert diesen Sachverhalt: 725 726 727 728 729 730 731 732 733 734 735 public class Box<T> { T contents; } class Main { public static void main(String[] args) { Box<String> stringBox = ... Box<Object> objectBox = ... String s = stringBox.contents Object o = objectBox.contents } } Das Feld contents der Klasse Box ist durch die Typvariable T typisiert. Je nach Verwendungskontext dieser Variablen unterscheidet sich dann der konkrete Typ, der für die Typvariable eingese t wird. In obigem Beispiel ist der durch den Compiler inferierte Typ des Feldes contents in Zeile einmal String, während er in Zeile Object ist. Beides folgt aus den unterschiedlichen Belegungen der Typvariablen T bei den Variablendeklarationen von stringBox und objectBox in den Zeilen und . Die wichtigste Folge bei einer Darstellung von mit Typvariablen typisierten Deklarationen in Refacola ist, dass für solche Deklarationen kein einheitlicher, inferierter Typ angegeben werden kann, eine entsprechende Variablenart für den deklarierten Typen also keinen Sinn ergibt. Entsprechend deklariert das verantwortliche Programmelement TypeVariableTypedEntity im Gegensa zu seinem nicht-generischen Pendant RegularTypedEntity auch keine Variablenart declaredType. Dennoch muss aber ebenso wie bei den nicht generisch typisierten Deklarationen eine Typinferenz durchgeführt werden. So gilt beispielsweise auch für Zuweisungen an generisch typisierte Felder weiterhin, dass Zuweisungskompatibilität zwischen den Typen bestehen muss. Im obigen Beispiel darf stringBox also nur ein String (oder ein Subtyp von String, gäbe es diesen) als Inhalt gegeben werden, eine Zuweisung 736 stringBox.contents = new Object(); :::::::::::::::::::::::::::::: führt zu einem Kompilierfehler. Während für nicht-generisch typisierte Deklarationen sich der Typ des Ausdrucks stringBox.contents anhand der Regel RegularTypedReferenceExpressionTypes ( S. ) direkt aus der Deklaration des Feldes contents abgeleitet hä e (und die Zuweisung über die Regel Assignment1 (siehe S. , ebenso S. ) verhindert worden wäre), muss bei generisch typisierten Deklarationen der jeweilige Kontext – also die jeweils geltende Belegung der Typvariablen – berücksichtigt werden, wie sie im vorstehenden Beispiel in Zeile vorgenommen wurde, indem bei der Deklaration der Variablen Box die Typvariable T den Wert String zugewiesen bekommt. In vorliegender Implementierung wird eine jede solche Zuweisung über ein Programmelement TypeParameterBinding repräsentiert. Im einfachen Fall, in dem keine Wildcards Verwendung finden (näheres hierzu in Abschni . . ), wird die Unterart SimpleTypeParameterBinding verwendet, welche eine Variablenart boundType hat, deren Werte genau dem zugewiesenen Typen entsprechen. Folgende Regel SimpleTypeParameterBinding sorgt für eine derartige Zusicherung, wobei das Fakt typeReferenceFor die Information liefert, dass die genannte Typre- . Constraintregeln für Java ferenz genau diejenige ist, welche innerhalb der spi en Klammern bei Belegung der Typvariablen auftri . 737 738 739 740 741 742 743 744 745 746 SimpleTypeParameterBinding for all typeReference: Java.TypeReference typeParameterBinding: Java.SimpleTypeParameterBinding do if Java.typeReferenceFor(typeReference, typeParameterBinding) then typeReference.typeBinding = typeParameterBinding.boundType end Über eine weitere Constraintregel muss dann sichergestellt werden, dass Ausdrücke, deren Typ einer derart belegten Typvariablen entspricht, für ihren inferierten Typen ihrerseits denselben Wert annehmen. Während das dabei entstehende Constraint – Gleichheit zweier Typvariablen – relativ einfach formuliert ist, gestaltet sich die Formulierung der Prämisse einer zugehörigen Constraintregel ungleich aufwändiger, da sich aus dem Code der Zusammenhang zwischen der Belegung einer Typvariablen und dem Typ eines Ausdrucks nur über eine Ke e mehrerer dazwischenliegender Abhängigkeiten wie Bindungen und Empfängerbeziehungen auflösen lässt, wie aus der zugehörigen Regel InferredParameterizedTypeNoWildcard ersichtlich ist: 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 InferredParameterizedTypeNoWildcard for all expression: Java.TypeVariableTypedExpression reference: Java.TypedReference declaration: Java.TypeVariableTypedEntity receiverExpression: Java.Expression receiverReference: Java.TypedReference receiverDeclaration: Java.TypedEntity receiverDeclarationType: Java.TypeReference typeVariable: Java.TypeVariable genericType: Java.Type typeParameterBinding: Java.SimpleTypeParameterBinding do if Java.isExpression(reference, expression), Java.receiver(reference, receiverExpression), Java.isExpression(receiverReference, receiverExpression), Java.binds(receiverReference, receiverDeclaration), Java.definesTypeOf(receiverDeclarationType, receiverDeclaration), Java.binds(reference, declaration), 767 768 769 Java.typeOf(declaration, typeVariable), Java.typeVariableOf(typeVariable, genericType), 770 771 772 773 Java.typeParameterBinding(typeVariable, typeParameterBinding), Java.typeContextOf(receiverDeclaration, typeParameterBinding) then . . Typ-Constraints für generische Typen Tabelle . .: Die generische Typen betreffenden Faktendeklarationen Fakt Parameter Bedeutung typeOf TypeVariableTypedEntity Weist jeder mit Typvariable typiTypeVariable sierten Deklaration die Typvariable zu, mit welcher sie typisiert wurde typeVariableOf TypeVariable Weist jeder Typvariable den Typen Type zu, in dem die deklariert wurde Gibt zu jeder Belegung einer TypvatypeReferenceFor TypeReference TypeParameterBinding riablen die entsprechende Typreferenz an, mit welcher die Belegung durchgeführt wurde typeContextOf TypedEntity Gibt zu jeder Belegung einer TypvaTypeParameterBinding riablen an, für welche Deklaration die Belegung sta gefunden hat typeParameterTypeVariable Gibt zu jeder Belegung einer TypvaBinding TypeParameterBinding riablen an, welcher Typparameter belegt wurde typeVariableBound TypeVariable Gibt für jede Schranke einer TypvaTypeOrTypeVariableReference riablen die entsprechende Typreferenz dieser Schranke an wildcardBound WildcardTypeParameterBinding Ordnet Wildcards ihre jeweiligen TypeOrTypeVariableReference oberen oder unteren Schranken zu typeParameterTypeParameterBinding Gibt Paarungen zusammengehöriAssignment TypeParameterBinding ger Typparameterbelegungen bei Zuweisung von Ausdrücken generischen Typs an expression.expressionType = typeParameterBinding.boundType 774 775 end Tabelle . listet die für generische Typen hinzukommenden und teilweise in der Regel InferredParameterizedTypeNoWildcard verwendeten Fakten mit entsprechenden Erläuterungen auf. Abbildung . zeigt einen an obiges Box<T>-Beispiel angelehnten Quellcodeausschni und die für diesen generierten Fakten und Programmelemente, wie sie für die Regel InferredParameterizedTypeNoWildcard relevant sind. Das bei dieser Faktenlage generierte Constraint besagt dann nur noch, dass der Ausdruck stringBox.contents denselben Typ wie die Typparameterbelegung der Variablendeklaration Box<String> contents haben muss. Während das Beispiel aus Abbildung . nur eine einzige Typparameterbelegung, nämlich die von T auf String zeigt, können in komplexeren Programmen noch weitere Belegungen hinzukommen. Einerseits kann ein Typ mehr als einen Parameter haben, andererseits können auch die Typparameter eines Typen mehr als nur einmal belegt werden, wie in obigem Beispiel in Zeile und gezeigt. Die Lösung hierfür ist, für jede Variable und jede Belegung ein weiteres Programmelement TypeParameterBinding mitsamt typeContextOf- und typeParameterBinding-Fakten zu erzeugen. Für jedes dieser Programmelemente findet die Regel . Constraintregeln für Java genericType : Type typeVariableOf class Box<T> { typeVariable : TypeVariable typeParameterBinding typeParameterBinding: SimpleTypeParameterBinding typeOf T contents; typeContextOf declaration : TypeVariableTypedEntity receiverDeclaration : TypedEntity Box<String> stringBox; definesTypeOf receiverDeclarationType : TypeReference binds binds expression: TypeVariableTypedExpression receiver String s = stringBox.contents; receiverExpression : Expression isExpression isExpression reference : TypedReference } receiverReference : TypedReference Abbildung . .: Programmbeispiel einer generischen Klasse Box<T> und ihrer Verwendung. In grau sind (ausschni sweise) die sich aus einzelnen Programmteilen ergebenden Programmelemente gekennzeichnet. Grüne Linien und Beschriftungen zeigen, wie diese anhand von Fakten in Beziehung stehen. Für das gegebene Beispiel würde die Constraintregel InferredParameterizedTypeNoWildcard ein Constraint generieren, welches den Wert der Constraintvariable boundType von typeParameterBinding zur Übereinstimmung mit dem Wert des expressionType von expression zwingt, also für den Ausdruck stringBox.contents denselben Typen inferiert, wie er bei der Deklaration der stringBox als Typparameter angegeben wurde. . . Typ-Constraints für generische Typen InferredParameterizedTypeNoWildcard dann ein weiteres Mal Anwendung. 5.4.2. Beschränkt parametrische Typen In Java können für Typparameter (obere) Schranken angeben werden ( § . ), was einem beschränkt parametrischen Polymorphismus (engl. bounded polymorphism) [CW ] entspricht. Die Belegung der Typvariablen eines generischen Typen ist dann nur noch möglich, soweit die für die Variablen eingese ten Typen mindestens den angegebenen Schranken genügen. In folgendem Beispiel darf der Typparameter T der Klasse RunnableBox nur noch mit Typen belegt werden, die java.lang.Runnable implementieren. 776 777 778 779 780 class RunnableBox<T extends Runnable>{ T contents; RunnableBox<::::: String> stringBox; RunnableBox<Thread> threadBox; } Der Versuch, in Zeile , den Typparameter T mit String zu belegen, schlägt fehl, während die Belegung mit Thread als Subtyp von runnable in Zeile erfolgreich ist. Die Angabe der Existenz einer oberen Schranke wird in der Refacola-Umse ung mi els eines Fakts typeVariableBound umgese t, welches direkt von der Typvariablen auf die Typreferenz der oberen Schranke verweist. Mi els folgender, gleichnamiger Regel TypeVariableBound wird sichergestellt, dass Typvariablen nur mit Subtypen der jeweiligen oberen Schranken belegt werden. 781 782 783 784 785 786 787 788 789 790 791 792 TypeVariableBound for all typeVariable: Java.TypeVariable typeVariableBound: Java.TypeReference typeParameterBinding: Java.SimpleTypeParameterBinding do if Java.typeVariableBound(typeVariable,typeVariableBound), Java.typeParameterBinding(typeVariable, typeParameterBinding) then Java.sub*(typeParameterBinding.boundType, typeVariableBound.typeBinding) end Ein wenig bekanntes, aber dennoch in der Sprache Java enthaltenes Feature ist die Angabe mehrerer oberer Schranken für eine Typvariable. So lassen sich mi els des &-Trennzeichens weitere obere Schranken angeben, wie folgendes Beispiel zeigt: 793 794 795 class RunnableReadableBox<T extends Runnable & Readable> { T contents; } Der Typparameter T darf für den Typen RunnableReadableBox nur noch mit solchen Typen belegt werden, die sowohl Runnable als auch Readable implementieren, es entsteht ein sogenannter intersection type ( § . ). In diesem Fall wird für jede weitere obere Schranke ein weiteres typeVariableBound-Fakt erzeugt, somit auch ein weiteres Constraint nach der TypeVariableBound-Regel, welches mögliche Belegungen der Typvariablen T dahingehend einschränkt, auch alle weiteren Schranken berücksichtigen zu müssen. . Constraintregeln für Java Die obige Constraintregel TypeVariableBound hat gezeigt, dass durch den Einsa beschränkt parametrischer Typen im Programm zusä liche Bedingungen einzuhalten sind, die der Nü lichkeit eines solch beschränkten Typen im Wege stehen können. Daher ist es logisch, dass um den Einsa von Typschranken zu rechtfertigen, sich umgekehrt auch ein Nu en aus diesen ergeben muss. Während Typvariablen ohne Typschranken Vorteile in erster Linie außerhalb des generischen Typen, also bei den Verwendern, erzeugen, die für die Belegungen der Typvariablen verantwortlich sind, tri der Vorteil der beschränkten Typvariablen innerhalb des generischen Typen auf. Hier können dann nämlich konkrete Annahmen über die Typen hinter den Typvariablen gemacht werden. Für das Beispiel der RunnableReadableBox aus Zeile kann dann nämlich angenommen werden, dass durch das mit der Typvariablen T typisierte Feld contents sowohl die Methoden aus Runnable als auch Readable implementiert sind, was folgende Aufrufe möglich macht: 796 797 contents.run(); // declared in java.lang.Runnable contents.read(null); // declared in java.lang.Readable Auch wenn Typparameterschranken in der Refacola als Typreferenzen (TypeReference) mit Variablen für ihre jeweilige Bindung modelliert sind, so wird eine Änderung dieser Bindung und damit der Typschranken nicht unterstü t. Der Grund hierfür besteht in der immens großen Menge potentieller Typen, die sich für mit Typvariablen typisierte Ausdrücke (im obigen Beispiel contents) ergeben kann. Durch das mögliche Vorkommen beliebig vieler, mit & konkatenierter oberer Schranken wächst die Menge sich möglicherweise ergebender intersection types exponentiell (nämlich als Potenzmenge) mit den im Programm definierten Typen. Abgesehen davon, dass die Refacola derzeit außer den in Abschni . . beschriebenen programmabhängigen Domänen keine weiteren Möglichkeiten bietet, Domänen in Abhängigkeit von der Faktenbasis zu definieren, lässt sich vermuten, dass ein Verfahren wie bei der Refacola, welches die Domänen vorab vollständig berechnet, bei exponentiellem Wachstum der Domänengröße zu keinen befriedigenden Laufzeiten führen kann. Da die Schranken für Typvariablen unveränderlich sind, entfällt auch die Notwendigkeit, über Constraints Typen für Ausdrücke zu inferieren, die sich aus Typschranken ergeben würden. Auch diese können dann nur unverändert bleiben. Soweit eine Typvariable keiner Beschränkung unterliegt, kann ihr Typ ohnehin nur als Object angenommen werden. In folgendem Beispiel eines Typen mit unbeschränkter Typvariable T ergibt sich in Zeile für den Ausdruck contents als einzig inferierbarer Typ Object. 798 799 800 801 802 803 public class Box<T> { T contents; Object m() { return contents; } } Ist eine obere Typschranke wie im Beispiel der Klasse RunnableBox auf Seite gegeben, kann der inferierte Typ genau der oberen Schranke gleichgese t werden. Gibt es mehrere , so ist Schranken zur Auswahl, wie im obigen Beispiel der RunnableReadableBox auf Seite eigentlich der sich aus allen Typschranken ergebende intersection type zu inferieren ( § . ). Um aber der erwähnten Problematik zu entgehen, diese künstlichen Typen zu berechnen und . . Typ-Constraints für generische Typen in die Domäne der inferierten Typen einzupflegen, genügt es, auch nur jeweils den Typen als inferierten Typen anzunehmen, der im jeweiligen Kontext benötigt wird. Für den Ausdruck contents in Zeile wäre dies Runnable, für den gleichlautenden Ausdruck in Zeile wäre dies Readable. Weiterhin entfällt durch nicht-variable Typschranken die Notwendigkeit, bei der Angabe dieser Schranken auf Typkonformität zu achten, die sich aus der Schachtelung von generischen Typen ergeben kann. In folgendem Beispiel muss der Typparameter S von StorageRack mit einer oberen Schranke von (mindestens) Runnable versehen sein, da S wiederum Einsa in der oberen Schranke von R findet, welches der Typvariablen T von RunnableBox zugewiesen wird, für welches ebenso die Typschranke von Runnable gilt. 804 805 806 class RunnableBox<T extends Runnable> { T contents; } 807 808 809 810 public class StorageRack<S extends Runnable, R extends RunnableBox<S>> { /* ... */ } 5.4.3. Zuweisungen an mit generischen Typen typisierte Ausdrücke Für Ausdrücke, deren Typ einer generischen Klasse entspricht, gelten bei Zuweisungen dieselben Typregeln wie bei Ausdrücken nicht generischen Typs.²⁹ Auch weiterhin muss bei einer Zuweisung der inferierte Typ des Ausdrucks linker Hand derselbe oder ein Supertyp dessen sein, was sich nach Typinferenz für den Ausdruck rechter Hand ergibt. Sind die inferierten Typen darüber hinaus auch noch mit Typvariablen versehen, so genügt für deren Belegung keine Subtypbeziehung. Subtypbeziehungen zwischen generischen Typen gelten bei belegten Typvariablen nur, wenn diese tatsächlich übereinstimmend belegt sind die Zuweisung von einer ( § . . ). In folgendem Beispiel gelingt also weder in Zeile Box<Object> an eine Box<String>, noch der umgekehrte Fall in Zeile . 811 812 public class Box<T> { T contents; 813 void m() { Box<String> stringBox = ...; Box<Object> objectBox = ...; stringBox = objectBox; :::::::: objectBox = :::::::: stringBox; } 814 815 816 817 818 819 820 } An dieser Stelle zeigt sich ein deutlicher Unterschied zwischen generischen Typen und ArrayTypen. Bei le teren ist eine Zuweisung zwischen Arrays mit unterschiedlichen Elemen ypen gesta et, solange diese in einer Subtypbeziehung stehen ( § . . ). Allerdings kann dies ²⁹ Mit Zuweisungen sind in diesem Abschni nicht nur die klassischen Zuweisungen durch den =-Operator gemeint, sondern ebenso auch die sich aus Parameterübergaben und Methoden-return ergebenden indirekten Zuweisungen. . Constraintregeln für Java zu Laufzeitfehlern führen, wenn auf einem mit einem Super-Elemen yp deklarierten Alias Elemente eingefügt werden, die nicht mehr dem Typen des Ursprungs-Arrays entsprechen. Solche Fehler werden dann nicht mehr durch den Compiler erkannt, sondern zur Laufzeit durch eine ArrayStoreException deutlich. Folgender Codeausschni zeigt in Zeile eine solche Zuweisung, die in der folgenden Zeile einen Laufzeitfehler provoziert, da ein Long nicht in ein Integer-Array eingefügt werden kann. 821 822 Number[] numbers = new Integer[] { new Integer(1) }; numbers[0] = new Long(23); Beim Entwurf der generischen Typen in Java war es ein Ziel, genau solche Laufzeitfehler zu verhindern, weswegen Java Zuweisungskompatibilität nur noch bei übereinstimmend belegten Typvariablen vorsieht. In Ergänzung zu den Regeln Assignment1 (siehe S. , ebenso ) und Assignment2 ( S. ), die auch für Array-Typen greifen, werden die sich zusä S. lich aus den Belegungen der Typvariablen ergebenden Einschränkungen über die folgende Regel TypeParameterAssignment zugesichert. Das Fakt typeParameterAssignment kennzeichnet dabei die entsprechenden Paare von Typvariablenbelegungen, deren Gleichheit über die gleichnamige Regel sichergestellt wird. 823 824 825 826 827 828 829 830 831 832 TypeParameterAssignment for all lhs: Java.SimpleTypeParameterBinding rhs: Java.SimpleTypeParameterBinding do if Java.typeParameterAssignment(lhs,rhs) then lhs.boundType = rhs.boundType end Auch wenn die vorstehende Regel relativ einfach wirkt, wohnt ihr ein wenig mehr Komplexität inne, als sie vermuten lässt. Diese Komplexität ergibt sich aus dem schon angesprochenen Fakt typeParameterAssignment. Dieses verweist nämlich nicht direkt auf die Ausdrücke linker und rechter Hand der Zuweisung, sondern schon direkt an die Stellen, an denen die jeweiligen Typparameter belegt werden. Einerseits ist zu beachten, dass dabei auch schon ineinander geschachtelte generische Typen berücksichtigt werden, also solche generischen Typen, deren Typvariablen wiederum mit generischen Typen belegt sind. Ebenso werden aber auch Fälle abgedeckt, bei denen Typvariablen eines Supertypen im Laufe der Vererbungshierarchie innerhalb der Deklarationen der Subtypen belegt wurden. In folgendem Beispiel würde für die Zuweisung der neu erzeugten StringBox an die Variable myBox in Zeile ein Fakt typeParameterAssignment erzeugt werden, welches linksseitig auf die Belegung der Typvariablen myBox mit String in Zeile referenziert, rechtsseitig auf für alle Instanzen von die Belegung der von Box geerbten Typvariablen T, die in Zeile StringBox mit String belegt wird. 833 834 835 public class Box<T> { T contents; } 836 837 class StringBox extends Box<String> { . . Typ-Constraints für generische Typen void m() { Box<String> myBox = ...; myBox = new StringBox(); } 838 839 840 841 842 } 5.4.4. Wildcards Die im vorangegangenen Abschni behandelte Beschränkung, in Java nur solche generisch typisierten Ausdrücke einander zuweisen zu können, die identische Belegungen ihrer Typvariablen aufweisen, erhöht zwar die Typsicherheit, bringt für den Programmierer aber empfindliche Einschränkungen, was den Umgang mit generischen Typen anbelangt. Insbesondere für Programmteile, die mit generischen Typen umgehen müssen, aber innerhalb derer die generisch typisierten Deklarationen des generischen Typen keine Verwendung finden, sind die Einschränkungen unnötig. Um diesem Anwendungsfall gerecht zu werden, unterstü t Java mi els des ?-Operators sogenannte Wildcards ( § . . ). Eine Wildcard entspricht der Belegung einer Typvariablen mit einem beliebigen Typen. Entsprechend ist ein mit Wildcards belegter generischer Typ auch immer Supertyp jeder weiteren Parametrisierung dieses Typen. Folgendes Beispiel zeigt eine Wildcard-Belegung eines Typparameters T in Zeile sowie eine gültige Zuweisung eines Ausdrucks vom Typ Box<String> an einen Ausdruck vom Typ Box<Object>. 843 844 public class Box<T> { T contents; 845 public static void main(String[] args) { Box<String> stringBox = null; Box<?> wildCardBox = null; wildCardBox = stringBox; } 846 847 848 849 850 851 } Der Preis für damit erhaltene Zuweisungskompatibilität besteht in nur beschränkten Möglichkeiten, auf mit Wildcards typisierte Member zugreifen zu können. Zuweisungen³⁰ an eine solche Variable sind – außer einer Zuweisung an null – generell untersagt, da der Typ einer Wildcard über die sogenannten capture conversions in Java ( § . . ) nur zum null-Type gewandelt werden kann. Wird lesend auf eine solche Variable zugegriffen, kann für sie lediglich der Typ Object inferiert werden. In folgender Ergänzung obigen Beispiels führen beide Zuweisungen zu einem Compilerfehler – einmal in Zeile wegen des Verbots der Zuweisung , da für den rechtsseitigen Ausdruck sich der Typ an Wildcard-Typen und einmal in Zeile Object ergibt, der einem String nicht zugewiesen werden darf. 852 853 wildCardBox.contents = :::: new::::::: String(); String theContents = wildCardBox.contents; :::::::::::::::::: ³⁰ Auch hier sind weiterhin nicht nur die Zuweisungen durch den =-Operator gemeint, sondern auch die impliziten bei Parameterübergaben und return-Anweisungen auftretenden. . Constraintregeln für Java Obere und untere Schranken für Wildcards Wie das Beispiel der ArrayStoreException auf Seite gezeigt hat, war das bei generischen Typen zu vermeidende Problem die Möglichkeit, über ein Alias eines Arrays Supertypen des Elemen ypen des Arrays in dieses einzufügen. Erweitert man das Beispiel wie folgt, sieht man, dass eine Entnahme von Elementen wie in Zeile wiederum typsicher ist, während lediglich das Einfügen in Zeile zu einem Laufzeitfehler führt. 854 855 856 Number[] numbers = new Integer[] { new Integer(1) }; Number myNumber = numbers[0]; numbers[0] = new Long(23); // ArrayStoreException Der Wunsch, weiterhin Elemente bei mit Wildcards belegten generischen Typen typsicher entnehmen³¹ zu können, ohne gleichzeitig auf die Typsicherheit beim Einfügen von Elementen zu verzichten, führte zur Einführung von oberen Schranken für Wildcards in Java ( § . . ). Eine solche Schranke gibt an, dass die mit der beschränkten Wildcard typisierten Elemente mindestens diesem Typ entsprechen. Somit lässt sich obiges Array-Beispiel analog mit beschränkten Wildcards umse en, wobei nun aber der Laufzeitfehler aus Zeile schon durch den Compiler in Zeile erkannt wird. 857 858 859 Box<? extends Number> numbers = new Box<Integer>(); Number myNumber = numbers.contents; numbers.contents = :::: new:::::::: Long(23); Die eigentlich entscheidende Typprüfung findet dabei schon in Zeile sta , in welcher geprüft wird, ob die Typvariable rechter Hand der Zuweisung (Integer) tatsächlich einem Supertyp der oberen Schranke der Wildcard linker Hand (Number) entspricht. Den umgekehrten Fall zu oberen Schranken, nämlich untere Schranken, unterstü t Java ebenso ( § . . ). Bei diesen gilt für die Zuweisung an Wildcard-Typen aber die genau umgekehrte Bedingung, nämlich dass bei der Zuweisung an einen Wildcard-Typen die Typvariable rechter Hand der Zuweisung einem Subtyp der oberen Schranke der Wildcard linker Hand entspricht. Dann ist umgekehrt auch das typsichere Einfügen von Elementen möglich, die mindestens dieser Schranke entsprechen, während entnommene Elemente nur noch vom Typ Object sind, wie folgendes Beispiel zeigt: 860 861 862 863 Box<? super Number> numbers = new Box<Object>(); numbers.contents = new Long(23); Number myNumber = numbers.contents; ::::::::::::::: Object anObject = numbers.contents; Bei den oberen Schranken von Wildcards ergibt sich ein interessanter Spezialfall, wenn diese als Object angegeben wird. Auch dann ist weiterhin ein Einfügen von Elementen nicht möglich, bei der Entnahme kann nur noch der Typ Object angenommen werden. Das Verhalten entspricht damit exakt dem einer Wildcard ohne jede Schranke. Aus diesem Grund werden in ³¹ Es ist nicht ganz korrekt, bei generischen Typen von der Entnahme oder dem Einfügen von Elementen zu sprechen, implementiert doch keineswegs jeder generische Typ zwingend einen Container. Dennoch sind Container ein häufiger Anwendungsfall für generische Typen und auch das in diesem Kapitel allgegenwärtige Beispiel einer verschiedentlich Typ-parametrisierten Klasse Box folgt diesem Muster. Korrekterweise ist sta vom Einfügen eines Elements besser von einer Zuweisung an eine mit der jeweiligen Typvariablen typisierten Variable zu sprechen, beim Entnehmen eines Elements von der Referenzierung dieser Variablen. Bei Methoden gilt entsprechendes für die Argumentübergabe an mit Typvariablen typisierte Methodenparameter. . . Typ-Constraints für generische Typen der vorliegenden Implementierung Wildcards ohne Schranken gar nicht erst explizit berücksichtigt. Vielmehr wird beim Einlesen des Programmcodes für diese schon implizit eine obere Schranke Object hinzugefügt. Constraintregeln für Wildcards Belegungen von Typvariablen mit Wildcards werden in vorliegender Implementierung mi els einer Variablenart WildcardTypeParameterBinding, einer abstrakten, von TypeParameterBinding abgeleiteten Unterart, beschrieben. Abhängig davon, ob eine Wildcard mit einer oberen oder unteren Schranke versehen ist, kommen die Unterarten UpperBoundTypeParameterBinding oder LowerBoundTypeParameterBinding zur Anwendung. Mit der jeweils angegebenen Typschranke (einer TypeOrTypeVariableReference) sind diese dann über ein Fakt wildcardBound verbunden. Die bisher in den vorangegangenen Abschni en genannten Constraintregeln zur Typinferenz generisch typisierter Ausdrücke und Zuweisung zwischen generischen Typen finden bei Wildcards keinerlei Anwendung, da sich diese nur auf mit konkreten Typen belegte Typvariablen (SimpleTypeParameterBinding) beziehen, nicht aber auf WildcardTypeParameterBinding. Dieses Verhalten ist erwünscht, da für Wildcards ohnehin abweichende Regeln gelten müssen. Die Typinferenz von Wildcard-typisierten Ausdrücken gestaltet sich noch weitgehend analog zur Regel InferredParameterizedTypeNoWildcard (siehe S. , ebenso S. ). Die ähnlich ) wählt dabei lediglich sta digestaltete Regel InferredParameterizedTypeForWildcard ( S. rekt den die Typvariable belegenden Typen die jeweils obere beziehungsweise untere Schranke der Wildcard aus. Schreibende Zugriffe auf mit beschränkten Wildcards typisierte Member benötigen keine Behandlung. Zwar darf diesen – wie oben ausgeführt – nur null zugewiesen werden, allerdings sind derzeit weder Constraintvariablen vorgesehen, die obere oder untere Schranken entfernen oder Wildcards durch konkrete Belegungen ändern, noch solche, die ein null-Ausdruck in irgendeiner Weise beeinflussen. Daher kann sich ein Fehler hieraus während der Constraintlösung gar nicht erst ergeben. Für lesende Zugriffe bei nach unten beschränkten Wildcards ) den Typ des Ausdrucks rechter schränkt die Regel UpperBoundWildcardAssignments ( S. Hand auf Object ein, was Zuweisungen an nicht Object-typisierte Variablen unterbindet. Zule t müssen noch analog zur Regel TypeParameterAssignment die Zuweisungen zwischen Ausdrücken beschränkt werden, die generischen Typs sind. Tabelle . listet die möglichen Fälle von Zuweisungen generischer Typen mit und ohne Wildcards – le tere mit und ohne Schranken – auf und gibt dabei nötige Bedingungen für die Belegungen der Typvariablen an, wie sie sich aus den Regeln der type conversion ( § . ) und insbesondere der capture conversion ( § . . ) für Java ergeben. So ergibt sich aus der Tabelle zum Beispiel für die Zuweisung in Zeile aus obigem Beispiel, dass für die linksseitige Belegung der Typvariable mit der nach oben beschränkten Wildcard <? extends Number> und rechtsseitigen Belegung <Integer>, dass der rechts eingese te Typ (Integer) ein Subtyp von der linksseitigen Schranke (Number) sein muss. Für die Zuweisung aus Zeile ergibt sich bei linksseitiger und nach unten beschränkter Wildcard <? super Number> und rechtsseitiger Belegung mit <Object> die Bedingung, dass die Schranke links (Number) Subtyp der Belegung rechts (Object) sein muss. Nicht alle Bedingungen der Tabelle . bedürfen (noch) der Umse ung mit Constraintregeln. Der Fall ohne Wildcards auf beiden Seiten ist schon mit der Regel TypeParameterAssignment aus Abschni . . abgehandelt. Die drei Fälle, in denen Zuweisungen unter kei- . Constraintregeln für Java Tabelle . .: Einzuhaltende Bedingungen an die Belegungen von Typvariablen bei einer Zuweisung x=y; zwischen Ausdrücken generischen Typs. Belegung der i-ten Typvariable rechtsseitig <B> <? extends B> <? super B> Belegung der i-ten Typvariable linksseitig <A> <? extends A> <? super A> A=B sub*(B,A) sub*(A,B) false sub*(B,A) false false A = Object sub*(A,B) nen Umständen möglich sind (false), bedürfen ebenso keiner Berücksichtigung. Wandlungen von mit Wildcards belegten Typvariablen zu mit regulären Typen belegten Typvariablen (und umgekehrt) werden derzeit nicht unterstü t. Da es keine Constrainvariablen hierfür gibt, kann während der Constraintlösung auch keine Situation provoziert werden, die zu einem solchen Fehler führen kann. Besteht der Fehler schon vor der Transformation, ist er mit der derzeitigen Implementierung nicht behebbar. Ein fehlschlagendes Constraint würde aber auch nicht helfen, da dieses weitere, mitunter an anderer Stelle sinnvolle Transformationen blockieren würde. Die übrigen fünf Fälle aus Tabelle . werden durch die Constraintregeln TypeParameterAssignmentWildcards1 ( S. ) bis TypeParameterAssignmentWildcards5 ( S. ) abgehandelt. 5.4.5. Raw-Types Um vorhandenen Code, welcher vor Java entwickelt wurde, gemeinsam mit neueren Programmteilen betreiben zu können, die generische Typen verwenden, erlaubt Java die Nu ung sogeannter raw types ( § . ). Ein raw type ergibt sich durch die Benu ung eines generischen Typen ohne Angabe von Typvariablen. Vom Punkt der Referenzierung aus scheint es also, als ob der verwendete Typ keine Typvariablen deklarieren würde. Der Unterschied zu den ähnlich anmutenden – weil auch ohne konkret belegte Typvariablen daherkommenden – Wildcard-Typen ergibt sich dabei aus signifikant gelockerten Typregeln. So sind bei der Zuweisungskompatibilität von generischen Typen die Belegungen der Typvariablen irrelevant, sobald ein raw type beteiligt ist. Auch ist lesender und schreibender Zugriff auf die mit Typvariablen typisierten Member eines raw types stets möglich, wobei die Typvariablen stets als Object angenommen werden. Als Folge kann bei Verwendung von raw types die Typsicherheit durch den Compiler nicht mehr zugesichert werden, weswegen die Verwendung von raw types auch Warnungen durch den Compiler auslöst und nur als Notlösung betrachtet werden sollte, die lediglich die Verwendung älteren Codes ermöglichen soll. In der vorliegenden Implementierung erfahren raw types keine gesonderte Behandlung. Vielmehr werden sie schon dadurch korrekt behandelt, dass bei ihrer Verwendung aufgrund der fehlenden Typisierungsinformationen gar nicht erst TypeParameterBinding-Programmelemente angelegt werden, somit auch die vorstehenden Constraintregeln zu generischen Typen keine Anwendung finden und keine zusä lichen Einschränkungen bringen, was den Eigenschaften der raw types entspricht. Da auch bei raw types weiterhin gilt, dass derzeit keine Programmtransformationen vorgesehen sind, die zusä liche Typinformationen einfü- . . Namens-Constraints gen, werden keine Refaktorisierungswerkzeuge unterstü t, die die Erse ung von raw types zugunsten parametrisierter Typen zur Folge haben. 5.5. Namens-Constraints Namen werden in Java genu t, um auf deklarierte Elemente im Programm wie Pakete, Typen, Member, Parameter und lokale Variablen Bezug nehmen zu können ( § ). Entsprechend tauchen Namen an zwei Stellen im Programm auf, einerseits bei Deklarationen, andererseits bei Referenzen. Folgerichtig deklariert die Refacola-Sprachdefinition für Java Subtypen von Entity und Reference namens NamedEntity und NamedReference, die jeweils eine Variablenart identifier deklarieren. Es leiten dann genau die Deklarationen und Referenzen von NamedEntity und NamedReference ab, die einen Namen in sich tragen, und erben somit die passenden identifier-Variablen. 5.5.1. Bindung anhand von Namen Die offensichtlichste, sich aus Namen ergebende Bedingung ist, dass bei Bindung einer benannten Referenz an eine benannte Deklaration die verwendeten Namen beider übereinstimmen müssen. Die folgende Regel ReferenceIdentifier se t dies für Bindungen an lokale Variablen, formale Parameter, Felder und Methoden um: 864 865 866 867 868 869 870 871 872 873 ReferenceIdentifier for all reference: Java.NamedTypedReference entity: Java.NamedEntity do if Java.binds(reference, entity) then reference.identifier = entity.identifier end Dasselbe muss ebenso für Referenzen zu Typen gelten. Die Regel TypeReferenceIdentifier se t dies analog unter Berücksichtigung variabler Typbindungen um: 874 875 876 877 878 879 880 881 882 883 884 885 TypeReferenceIdentifier for all type: Java.NamedType ref: Java.TypeReference do if all(type), all(ref) then (type = ref.typeBinding) −> (type.identifier = ref.identifier) end . Constraintregeln für Java 5.5.2. Eindeutigkeit von Namen Bindungen anhand von Namen sind nur möglich, soweit die verwendeten Namen in ihrem jeweiligen Kontext eindeutig sind. Die bereits erwähnte Regel AmbigousFieldAccess ( S. ) ist eine solche, die verhindert, dass eine Referenz mehrdeutig wird. Darüber hinaus gibt die Java-Sprachspezifikation Regeln vor, die uneindeutige Namen schon im Vorhinein verhindern sollen. So dürfen zum Beispiel niemals zwei gleich benannte Typen ineinander geschachtelt sein ( §§ . , . ), was über die folgende Regel UniqueMemberTypeNames1 umgese t wird: 886 887 888 889 890 891 892 893 894 895 UniqueMemberTypeNames1 for all t: Java.MemberType do if all(t) then t.identifier != t.enclosingNamedTypes.identifier, t.identifier != t.tlowner.identifier end Zu beachten bei dieser Regel ist, dass das für MemberType definierte enclosingNamedTypes eine Sequenz ist (siehe Abschni . . ), welche jeweils alle umschließenden Typen eines Membertypen beinhaltet. Diese Regel generiert also für jeden umschließenden Typen ein einzelnes Constraint, was nur unter der Bedingung korrekte Ergebnisse liefert, dass die Menge der umschließenden Typen eines Membertypen nicht variabel ist (eine Annahme, die in Abschni . begründet wurde). Sollen hingegen Membertypen ihre umschließenden Typen ändern können, würde dies Mengen-Constraints [Sto ] erfordern, deren Formulierung und Lösung das Refacola-Framework derzeit nicht unterstü t. Weiterhin gibt es mehrere Bedingungen, die gleich benannte Deklarationen unmi elbar innerhalb einer sie gemeinsam umschließenden Deklaration verbieten. Innerhalb eines Typen dürfen nicht zwei Typen oder Felder gleichen Namens deklariert sein ( § . ), ebenso wie nicht mehrere lokale Variablen innerhalb einer Methode gleichen Namen tragen dürfen oder mit den Namen von etwaigen Methodenparametern in Konflikt stehen dürfen. Die Regeln UniqueMemberTypeNames2 ( S. ), UniqueFieldIdentifier ( S. ) sowie UniqueLocalVariableNames1 ( S. ), UniqueLocalVariableNames2 ( S. ) und UniqueLocalVariableNames3 ( S. ) decken diese Fälle ab. Da Methoden in Java nicht nur anhand ihres Namens, sondern auch anhand ihrer Signatur unterschieden werden können, sind mehrere Methoden gleichen Namens innerhalb einer Klasse gesta et und auch ein häufig anzutreffendes Muster – das sogenannte Überladen von Methoden ( § . . ). Allerdings müssen ihre Signaturen dann unterschiedlich sein ( § . ), was die folgende Regel UniqueMethodIdentifier sicherstellt. 896 897 898 899 900 901 UniqueMethodIdentifier for all method1: Java.RegularTypedMethod method2: Java.RegularTypedMethod do if . . Namens-Constraints 902 903 904 905 906 907 908 method1 != method2 then (method1.owner = method2.owner) and (method1.parameters.declaredParameterType = method2.parameters.declaredParameterType) −> (method1.identifier != method2.identifier) end Zu guter Le t gilt, dass innerhalb eines Pakets nicht mehrere Toplevel-Typen gleichen Namens deklariert sein dürfen ( § . ), was die Regel UniqueTopLevelTypeIdentifier ( S. ) sicherstellt. 5.5.3. Namen überschreibender Methoden Methodenüberschreibung ist in Java nur zwischen Methoden mit identischen Namen möglich ( § . . . ). Die Regel OverridingMethodNames ( S. ) kann recht einfach sicherstellen, dass beim Umbenennen einer Methode ebenso überschreibende oder überschriebene Methoden mit umbenannt werden. Darüber hinaus kann die Situation bestehen, dass auch ohne direkte Methodenüberschreibung eine Klassenmethode eine Interfacemethode implementiert, wie bereits im Zusammenhang mit Zugrei arkeiten in einem Beispiel auf Seite gezeigt wurde. Die Regel EagerInterfaceMethodNames ( S. ) sorgt entsprechend dafür, dass auch bei vorausschauenden Überschreibungen die Methodennamen weiterhin übereinstimmen. 5.5.4. Weitere Namensregeln Eine Besonderheit bei Java ist – und das scheint auf den ersten Blick den gerade zuvor erläuterten Regelungen, Ambiguitäten vermeiden zu wollen, zuwiderzulaufen – dass Konstruktoren den gleichen Namen wie die jeweils umschließende Klasse tragen müssen ( § . ). Faktisch ist dies natürlich kein Problem, da Konstruktoraufrufe schon rein syntaktisch anhand der stets übergebenen (wenn auch gegebenenfalls leeren) Parameterliste von Typreferenzen zu unterscheiden sind. Mi els des Fakts instantiates, welches jedem Konstruktor den durch diesen instanziierten Typen zuordnet, lässt sich diese Bedingung in Refacola einfach formulieren: 909 910 911 912 913 914 915 916 917 918 ConstructorNames for all type: Java.NamedType constructor: Java.Constructor do if Java.instantiates(constructor, type) then type.identifier = constructor.identifier end Weitere Bedingungen aus Namen ergeben sich bei TopLevelTypen, welche – wenn sie public deklariert sind – den Namen ihrer umschließenden Überse ungseinheit (TypeRoot) tragen müssen ( § . ). Da hierbei etwaige Dateinamenserweiterungen ohnehin auszusparen sind und diese auch je nach Zielsystem unterschiedlich sein können (bei Java-Quellcode .java oder . Constraintregeln für Java auch .jav), wird der Name einer TypeRoot intern stets ohne Dateinamenserweiterung dargestellt. Dieser muss bei der Faktengenerierung abhängig vom Zielsystem korrekt entfernt beziehungsweise beim Zurückschreiben korrekt wieder hinzugefügt werden. Daher kann in der folgenden Regel TopLevelTypeNames, die bereits ganz ähnlich bei [Wag ] zu finden ist, tatsächlich Gleichheit der beiden identifier gefordert werden: 919 920 921 922 923 924 925 926 927 928 929 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 Weiterhin gilt für Überse ungseinheiten, die eine Paketdeklaration enthalten (also nicht im sogenannten default-package liegen), dass sich der dort angegebene Name mit dem des umschließenden Pakets decken muss. Die Regel PackageDeclarationNames ( S. ) sichert dies zu. 5.5.5. Ungültige Namen Die Java-Sprachspezifikation sieht nicht nur Einschränkungen bei der Verwendung von Namen, sondern auch bezüglich der Namen selbst vor. So darf ein Name weder Leerräume noch Klammern enthalten. Auch Schlüsselwörter der Sprache sind tabu, weiterhin muss das erste Zeichen eines Bezeichners stets aus einer bestimmten Teilmenge der Unicode-Zeichen stammen ( § . ). Es wäre möglich, diese Bedingungen mi els Constraints zu formulieren, allerdings unterstü t die Refacola keine String-Operationen, wie sie hierfür nötig wären. In Anbetracht der Tatsache, dass die Gültigkeit von Namen bei Refaktorisierungen aber auch vollkommen isoliert durch eine Vorabprüfung aller Namen der Identifier-Domäne sta finden kann,³² ist dies keine Einschränkung. So können bei einer R -Refaktorisierung die neu zu vergebenen Namen bei der Eingabe mi els regulärer Ausdrücke zunächst auf Gültigkeit geprüft (und gegebenenfalls zurückgewiesen) werden, bevor sie in die Domäne der Identifier eingefügt werden. Die vorgestellte Implementierung der R -Refaktorisierung (siehe Abschni . . ) macht von einer solchen Lösung Gebrauch. 5.5.6. Verdunkeln und Verschatten Verdunkeln (Obscuring) Java kennt Situationen, in denen von der Syntax her ein Name im Programm sowohl eine Referenz zu einem Paket als auch zu einem Typen oder einer Variablen (also zum Beispiel Feld, Methodenparameter oder lokale Variable) darstellen kann. Finden sich mehrere passende Deklarationen unterschiedlicher Art, gibt der Bindealgorithmus in Java Variablen den Vorzug vor Typen und Typen den Vorzug vor Paketen – etwaig ³² Es sei daran erinnert, dass es sich bei den Namen nur um eine endliche Domäne handelt, siehe Abschni . . . . . Namens-Constraints 930 931 932 933 934 935 936 package a; public class A { static void m(Object a) { a.A X; A.m(new Object()); } } (a) ̸⇒ package a; public class a { static void m(Object a) { a.a X; ::: a .:::::: m(new:::::::: Object()); } } (b) Abbildung . .: Wird im linken Programm (a) die Klasse A mitsamt der Referenzen auf diese in a umbenannt, kommt es aufgrund von Verdunkelung zu Kompilierfehlern in den Zeilen und (b). gleich genannte Typ- und Paketdeklarationen werden verdunkelt (engl. Obscuring) ( § . . ). Abbildung . gibt ein Beispiel. Wird in dem Programm auf der linken Seite (a) die Klasse A mitsamt beider Referenzen auf diese in a umbenannt, kommt es bei beiden Referenzen zu Fehlern wegen verdunkelnder Deklarationen. In Zeile wird die Paketreferenz a dann als Referenz auf die Klasse a uminterpretiert, die wiederum keine Memberklasse namens a besi t, in Zeile wird die Typreferenz a zu einer Referenz auf den Methodenparameter a umgedeutet, auf welchem als Object typisiert keine Methode m aufgerufen werden kann. In dieser Arbeit wird keine Lösung für Verdunkelung von Paket- oder Typnamen gegeben. Zwar ist es denkbar, mi els Constraintregeln versehentliches Verdunkeln von Deklarationen zu verhindern, allerdings steht der Aufwand in keinem Verhältnis zum Nu en, da Verdunkelung in der Praxis belanglos ist. Die Java-Sprachspezifikation gibt eine Reihe von Namenskonventionen vor ( § . ), bei deren Einhaltung Verdunkelung auszuschließen ist. So haben zum Beispiel Paket- und Variablennamen mit Kleinbuchstaben und Typnamen mit Großbuchstaben zu beginnen, was Verdunkelung von oder durch Typen ausschließt. Berücksichtigt man weiterhin die Regelungen zur Benennung von Paketen – mit Punkten separiert und zuvorderst mit einer dem Domain Name System entlehnten Top-Level-Domain beginnend – und Variablen, die nicht einer solchen Top-Level-Domain entsprechen sollen, sind auch hier keine Konflikte zu befürchten. Verschattung (Shadowing) Vom Verdunkeln zu unterscheiden ist in Java das Verscha en (engl. shadowing) ( § . . ). Hierbei wird innerhalb des Geltungsbereichs (engl. scope) einer Deklaration eine weitere Deklaration mit gleichem Namen eingeführt, sodass sich beide Geltungsbereiche überlagern. Innerhalb dieses Schni es von Geltungsbereichen kann unqualifiziert nur noch auf das jeweils zule t deklarierte Element zugegriffen werden. Verscha ung findet (im Gegensa zur Verdunkelung) in Java nur zwischen Deklarationen gleicher Art sta , im Einzelnen sind dies Typen, Methoden und Variablen, die sich jeweils gegenseitig untereinander verscha en können ( § . . ). Abbildung . (a) zeigt ein Beispiel, in welchem das Umbenennen eines formalen Parameters (hier von j zu i) die Verscha ung eines Feldes durch diesen bewirkt. Es lassen sich Constraintregeln angeben, die ein versehentliches Verscha en verhindern, wenn es dadurch zu einer Bindungsänderung bei unqualifizierten Referenzen kommt. Die . Constraintregeln für Java 937 938 939 940 941 942 class A { int i; public A(int j ){ i = j; } } ⇒ class A { int i; public A(int i ){ this.i = i; } } (a) (b) Abbildung . .: Wird im linken Programm (a) der formale Parameter j in i umbenannt, führt dies zu einer Verscha ung des Feldes i in Zeile . Erst durch Einfügen des Qualifizierers this kann j bedeutungserhaltend in i umbenannt werden (b). folgende Regel, in welcher das Fakt isImplicit wiedergibt, ob eine thisReference nur implizit durch den Compiler eingefügt wurde oder explizit im Programm vorgegeben ist, zeigt exemplarisch, wie für unqualifizierte Feldreferenzen ein Verscha en des referenzierten Feldes durch formale Parameter wie in Abbildung . (a) verhindert werden könnte. 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 ShadowingByFormalParameter for all fieldReference: Java.FieldReference receiver: Java.ClassOrInterfaceTypedExpression thisReference: Java.ThisReference formalParameter: Java.FormalParameter method: Java.Method do if Java.receiver(fieldReference, receiver), Java.isExpression(thisReference, receiver), Java.referenceInEntity(fieldReference, method), Java.isImplicit(thisReference), Java.formalParameterOfMethod(formalParameter, method) then fieldReference.identifier != formalParameter.identifier end Analog lassen sich auch Verscha ungen durch Typen und Methoden verhindern, wobei – um auch geschachtelte Typen und Blöcke zu berücksichtigen – zusä liche Faktenabfragen und Bedingungen nötig werden. Auch wenn derartige Regeln Änderungen des Programmverhaltens im Fall von Verscha ungen lösen können, sind sie dennoch unbefriedigend, da sie übermäßig starke Einschränkungen [SMG ] mit sich bringen. Ansta im Beispiel aus Abbildung . (a) ein Umbenennen von j in i zu verhindern, wäre eine zu bevorzugende Lösung, mit this zu qualifizieren, wie in Abbildung . (b) sta dessen den Zugriff auf i in Zeile gezeigt. Im Rahmen der Constraintlösung über das Einfügen oder Auslassen von Qualifizierern zu entscheiden ist problembehaftet. Das Einfügen eines Qualifizierers macht nämlich das Einfügen neuer Programmelemente – in obigem Beispiel einer this-Referenz beziehungsweise eines this-Ausdrucks – nötig. Die Menge der Programmelemente ist aber nach Abschluss der Fak- . . Namens-Constraints tengenerierung unveränderlich. Der Umfang der möglichen Refaktorisierungen ergibt sich lediglich aus den Constraintvariablen, die den initial generierten Programmelementen über die Funktion applyingKinds (siehe Abschni . . ) zugeordnet sind. Eine allgemeine Lösung für dieses Problem könnte darin bestehen, intern jeden Memberzugriff mit einem Qualifizierer zu versehen und über eine Boolesche Constraintvariable zu steuern, ob dieser Qualifizierer tatsächlich im Quellcode ausgeschrieben ist oder ausgelassen werden kann. Alle Constraints, die den Qualifizierer bedingen, müssten dann als bedingte Constraints formuliert sein, in Abhängigkeit davon, ob der Qualifizierer tatsächlich Anwendung findet. Doch auch diese Lösung führt zu weiteren Problemen. So kann es zu derselben Referenz unterschiedliche Arten von Qualifizierern geben, unter denen eine Auswahl zu bis insgesamt neun Mögtreffen ist. Folgendes Programmstück zeigt in den Zeilen lichkeiten, einen Zugriff auf das Feld i so zu qualifizieren, dass nicht an den gleichnamigen, verscha enden Methodenparameter des Konstruktors von B gebunden wird. 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 package a; class A { int i; class B extends A { public B(int i){ this.i = i; super.i = i; A.this.i = i; a.A.this.i = i; B.this.i = i; a.A.B.this.i = i; B.super.i = i; A.B.super.i = i; a.A.B.super.i = i; } } } Wie zu ersehen ist, können einerseits this- und super-Qualifizierer verwendet werden, gleichzeitig diese aber wiederum noch selbst qualifiziert werden ( § . . ) wobei auch der qualifizierende Typ wie zum Beispiel in Zeile qualifiziert sein kann ( § . . . ). Um das korrekte Einfügen von Qualifizierern während der Constraintlösung zu steuern, wären neben einer Booleschen Variablen, ob überhaupt ein Qualifizierer gegeben ist, ebenso noch Constraintvariablen nötig, die zwischen this- und super-Qualifizierer unterscheiden, und angeben, ob und wie diese qualifiziert sind und ob und inwieweit deren Qualifizierer erneut qualifiziert ist. Die sich daraus ergebende Vielzahl bedingter Constraints und die zunehmende Komplexität der Constraintregeln stellen eine Behandlung von Qualifizierern durch den Constraintlöser generell in Frage. Schäfer et al. [SVEM , Sch ] stellen mit dem Konzept der locked bindings ein Konzept vor, bei welchem nach einer Refaktorisierung für jede Referenz einzeln geprüft wird, ob die sich aus der Intention oder übrigen Abhängigkeiten (siehe Abschni . ) ergebenden Bindungen erhalten wurden oder gegebenenfalls zu qualifizieren sind. Dass dieses Verfahren im Rahmen eines Postprocessing für eine constraintbasierte Refaktorisierung erfolgreich eingese t werden kann, wurde in [STST ] gezeigt. Aus diesen Gründen werden innerhalb dieser Ar- . Constraintregeln für Java beit keine Constraintregeln angegeben, die versehentliches Verscha en verhindern, sondern sta dessen die Namensqualifizierung im Nachgang durchgeführt (siehe Abschni . ). 5.6. Unerwünschtes Überschreiben, Verdecken und Überladen Ein Großteil der Regeln, die sich aus den vorhergehenden Abschni en zu Zugrei arkeits-, Namens-, aber auch Typ-Constraints ergeben haben, betrafen das Beibehalten der statischen und dynamischen Bindungen (wobei sich le teres in Java in den Überschreibungsbeziehungen widerspiegelt). Neben dem Umstand, dass durch Änderung von Zugrei arkeiten, Namen oder deklarierten Typen eine Referenz nicht mehr an ihre ursprüngliche Deklaration gebunden werden kann, gibt es auch das gegenteilige Problem, dass neue, unerwünschte Bindungen hinzukommen. Unerwünschtes Überschreiben und Verdecken Wenn durch eine Refaktorisierung eine neue Methodenüberschreibung in das Programm eingebracht wird, kann es zu Änderungen dynamischer Bindungen kommen. In Abschni . . ist ein Beispiel gegeben, in welchem durch Änderung einer Methodenzugrei arkeit eine Überschreibungsbeziehung aufgehoben wurde und sich das Programmverhalten in Folge dessen änderte. Abhilfe schuf die , ebenso S. ). Analog wurden weitere Regeln Regel OverridingAccessibility1 (siehe S. gezeigt, die für Beibehaltung von bestehenden Methodenüberschreibungen sorgen, nämlich OverridingMethodNames ( S. ) für übereinstimmende Methodennamen und OverridingParameterType ( S. ) und OverridingMustComeFromSubtype ( S. ) für passende Parametertypen und Deklarationsorte bei überschreibenden Methoden. Soll nun verhindert werden, dass neue Methodenüberschreibungen in das zu transformierende Programm eingebracht werden, genügt es, jeweils eine der Bedingungen aus diesen Regeln fehlschlagen zu lassen, was folgende Regel AccidentalOverriding umse t: 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 AccidentalOverriding for all superMethod: Java.InstanceMethod subMethod: Java.InstanceMethod do if subMethod != 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 Diese Regel besagt, dass für jedes Paar von Instanzmethoden • zuvor eine Überschreibungsbeziehung bestanden haben muss . . Unerwünschtes Überschreiben, Verdecken und Überladen • oder die deklarierenden Methoden nicht in einer Subtypbeziehung stehen dürfen • oder beide Methoden unterschiedliche Namen tragen müssen • oder sich an mindestens einer Stelle in den Parameterlisten beider Methoden sich der Parametertyp unterscheiden muss • oder die Methode des Supertyps als private deklariert sein muss, was Methodenüberschreibung ausschließt, • oder die Methode aus dem Supertyp im Subtypen nicht zugrei ar ist, indem sie defaultzugrei ar und in einem anderen Paket deklariert ist. Es ist zu beachten, dass man weitere Bedingungen, die für überschreibende Methoden zutreffen müssen, an dieser Stelle aussparen kann. So muss für überschreibende Methoden zum Beispiel gelten, dass ihre Rückgabetypen (je nach Java-Version) gleich sind oder in kovarianter Subtypbeziehung stehen, was durch die bereits oben beschriebene Regel OverridingReturnType (siehe S. , ebenso S. ) sichergestellt wird. Für die Regel AccidentalOverriding ist dies aber unerheblich. Entweder, die Rückgabetypen beider Methoden stehen tatsächlich in geeigneter Beziehung, dann kann es wegen der hinzukommenden Methodenüberschreibung zur Laufzeit zu Verhaltensänderungen kommen (was zu verhindern ist), oder die Rückgabetypen sind für eine Methodenüberschreibung geeignet, was zu einem Kompilierfehler führt (der ebenso zu unterbinden ist). Neben einer unbeabsichtigten Methodenüberschreibung zwischen Instanzmethoden sind ebenso Paarungen zwischen Instanzmethoden und statischen Methoden zu berücksichtigen, bei denen entweder eine Instanzmethode versucht, eine statische Methode zu überschreiben, oder umgekehrt. Beides ist in Java nicht erlaubt ( § . . . ), wobei es sich im zweiten Fall streng genommen auch nicht um eine unerlaubte Überschreibung, sondern unerlaubtes Verdecken ( § . . . ) handelt. Analog zur Regel AccidentalOverriding wird dies durch die Re) und StaticMethodHidesInstanceMethod geln InstanceMethodOverridingStaticMethod ( S. ( S. ) unterbunden. Diese insgesamt drei Regeln für unbeabsichtigtes Überschreiben zwischen Paarungen von Instanz- und statischen Klassenmethoden waren nötig, um den Fall zweier Klassenmethoden aussparen zu können, bei dem es nicht zu Überschreibung, sondern Verdeckung kommt. Sich erst nach der Refaktorisierung neuerdings verdeckende Methoden sind unproblematischer als sich neuerdings überschreibende Methoden, da Bindungen an statische Methoden in Java durch den Compiler ausgewertet werden und somit sich erst aus den Methodenreferenzen Bedingungen ergeben können, die gegen ein Verdecken zweier Methoden sprechen (siehe dazu weiter unten). Was aber Berücksichtigung finden muss, ist, dass in Java eine als final deklarierte Methode nicht verdeckt werden darf ( § . . . ), was die folgende Regel AccidentalHidingFinalMethod sicherstellt: 994 995 996 997 998 999 AccidentalHidingFinalMethod for all method1: Java.Method method2: Java.Method do if . Constraintregeln für Java 1011 1012 1013 1014 public class A { void m(String s) { /* ... */ } void n (Object o) { /* ... */ } } 1015 1016 1017 1018 ̸⇒ public class A { void m(String s) { /* ... */ } void m (Object o) { /* ... */ } } class B { {new A().n (new String());} } class B { {new A().m(new String());} } (a) (b) Abbildung . .: Wird im linken Programm (a) die Methode n mitsamt ihrer Referenz in Klasse B in m umbenannt, kommt es bei dieser zu einer Bindungsänderung an die dann überladene Methode m(String) in A (b). 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 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 Unerwünschtes Überladen Eng im Zusammenhang mit dem Problem des unerwünschten Überschreibens und Verdeckens finaler Methoden steht das unerwünschte Überladen. Während beim unerwünschten Überschreiben und Verdecken aber die Constraintregeln unabhängig von einem konkreten Methodenaufruf zu formulieren waren, ist ein Überladen von Methoden nur dann als kritisch einzustufen, wenn tatsächlich eine Methodenreferenz vorliegt, für die sich eine Bindungsänderung an eine überladene Methode ergibt. Abbildung . zeigt ein Beispiel für eine solche Bindungsänderung. Soll die Methode n mit Parametertypen Object in m umbenannt werden, führt dies innerhalb der Klasse A zunächst nicht zu Problemen. Erst bei Betrachtung der Klasse B mit ihrer Referenz auf die Methode n (welche mi els ReferenceIdentifier (siehe S. , ebenso S. ) ebenso in m umbenannt wird) wird klar, dass dieser Methodenaufruf in Folge der Transformation an die Methode m(String) sta die umbenannte Methode m(Object) binden wird. Es gibt mehrere Möglichkeiten, Umbindungen bei Methodenüberladung zu verhindern. So könnte in dem Beispiel aus Abbildung . entweder • die Methode m(String) auf private gese t werden, damit sie für Aufrufe aus B nicht mehr zugrei ar ist • oder die Methode m(String) umbenannt werden . . Unerwünschtes Überschreiben, Verdecken und Überladen • oder der Typ des Methodenparameters s derart geändert werden, dass er nicht mehr identisch mit (oder Subtyp des) Argumen ypen des Aufrufs von n in B ist • oder mi els eines sicheren Upcast auf Object der Typ des Arguments des Aufrufs von n in B derart geändert werden, dass er nicht mehr mit dem von m(String) identisch ist.³³ Die folgende Regel AccidentalInstanceMethodOverloadingInstanceMethod se t dies für den allgemeinen Fall einer Instanzmethode und einer mit ihr im We bewerb stehenden und sie überladenden weiteren Instanzmethode um. 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 AccidentalInstanceMethodOverloadingInstanceMethod for all method: Java.InstanceMethod reference: Java.MethodReference receiver: Java.ClassOrInterfaceTypedExpression competingMethod: Java.InstanceMethod do if Java.binds(reference, method), Java.receiver(reference, receiver), competingMethod != method then (method.identifier != competingMethod.identifier) or (not Java.sub*(receiver.expressionType,competingMethod.owner)) or (Java.sub*(method.parameters.declaredParameterType, competingMethod.parameters.declaredParameterType)) or (not Java.sub*(reference.arguments.expressionType, competingMethod.parameters.declaredParameterType)) or ((reference.tlowner != competingMethod.tlowner) and (competingMethod.accessibility = #private)) or ((reference.hostPackage != competingMethod.hostPackage) and (competingMethod.accessibility <= #package)) or ((reference.hostPackage != competingMethod.hostPackage) and (not Java.sub*(receiver.expressionType, reference.owner)) and (competingMethod.accessibility <= #protected)) end Daneben gibt es noch drei weitere Regeln • AccidentalInstanceMethodOverloadingStaticMethod, • AccidentalStaticMethodOverloadingInstanceMethod und • AccidentalStaticMethodOverloadingStaticMethod die analog eine Umbindung von Methodenaufrufen bei überladenen statischen Methoden sowie Paaren von statischen und Instanzmethoden verhindern. Der Grund für die vier separaten Regeln für die vier möglichen Kombinationen von Klassen- und Instanzmethoden re³³ eine Bedingung, die sich zwar derzeit in Constraints formulieren lässt, aber – da die Existenz von Type Casts im Rahmen dieser Arbeit nicht in Form von Variablen modelliert wurde (siehe Abschni . ) – nur geprüft und nicht durch die Constraintlösung ermi elt werden kann . Constraintregeln für Java sultiert aus den unterschiedlichen Arten von Methodenempfängern bei statischen und nichtstatischen Methoden (einmal Klassen, einmal Objekte) sowie den unterschiedlichen Zugreifbarkeitsregeln für statische und nicht-statische Methoden. Bei le teren greift zusä lich eine weitere Einschränkung bei protected-Zugrei arkeit, wie in Abschni . . anhand der Regel ProtectedMethodOrFieldAccess bereits ausgeführt. 5.7. Unveränderliche Bibliotheksfunktionen Neben den in den vorhergehenden Abschni en genannten Einschränkungen möglicher Refaktorisierungen durch Constraintregeln gibt es darüber hinaus eine weitere Art der Einschränkung, die durch die Refacola implizit umgese t wird. Nicht sämtlicher bei der Constraintgenerierung zu berücksichtigender Code kann tatsächlich durch ein Refaktorisierungswerkzeug bearbeitet werden. Das betrifft insbesondere Code, der aus Bibliotheken stammt und nur als Bytecode vorhanden ist, aber auch manche Programmelemente, die nur implizit durch den Compiler oder die Laufzeitumgebung vorgegeben sind, wie das size-A ribut von Arrays ( § . ) oder die values()- und valueOf()-Methoden, die in Java jede Enum-Klasse ( § . ) implizit deklariert. Wie Abschni . . erläutert hat, kann bei der Faktengenerierung ein Statusindikator gese t werden, der angibt, ob die dem Programmelement anhängenden Variablen in ihren Werten unveränderlich sein sollen. Dies geschieht insbesondere für Bibliothekscode, aber auch solche impliziten Programmelemente, die unveränderlich sind. Dabei wird nicht jede implizite Deklaration automatisch unveränderlich gese t. Default-Konstruktoren sind beispielsweise veränderlich, damit ihr Name und ihre Zugrei arkeit entsprechend des Namens und der Zugrei arkeit des umschließenden Typen mit verändert werden können. 5.8. Offene Probleme Eine Schwäche der Refacola ist, dass sie innerhalb der Constraintregeln keine Verwendung eines Existenzquantors gesta et. Auch wurde bisher nicht erforscht, wie sich die Formulierung solcher Constraints auf die Laufzeiten zur Constraintlösung auswirken würde. Ein einfaches Beispiel für eine Situation, in der ein Existenzquantor erforderlich ist, ergibt sich für eine Constraintregel, die besagen soll, dass zu jedem privaten Member mindestens eine an dessen Deklaration bindende Referenz im Programm vorhanden sein muss.³⁴ Erdenkt man Schlüsselwörter exists und with zur Refacola geeignet hinzu, könnte eine entsprechende Constraintregel wie folgt lauten: 1045 1046 1047 1048 1049 1050 1051 1052 PrivateMemberMustBeReferenced for all member: Java.Member do if all(member) then member.accessibility != #private ³⁴ Tatsächlich lässt sich zum Beispiel der Eclipse-Compiler so einstellen, dass er in diesem Fall einen Kompilierfehler meldet. . . Offene Probleme 1056 1057 1058 interface I { int i = 1; } class A implements I { private int j ;} class B extends A { int x = i; } ⇒ (a) interface I { int i = 1; } class A implements I { private int i ;} class B extends A { int x = :i ; } (b) Abbildung . .: Wird das Feld j in i umbenannt, verdeckt es für den Zugriff aus Klasse B anschließend das gleichnamige Feld aus I, obwohl ersteres in B nicht zugrei ar ist. Die Folge ist ein Kompilierfehler. or exists reference: Java.Reference with binds(reference, member) 1053 1054 end Die Konklusion besagt in dieser Regel, dass für jedes Member gelten muss, dass dieses nicht als private deklariert sein darf, oder eine Java.Reference (namens reference) existieren muss, für die gilt, dass sie an das Member bindet. Die Formulierung einer äquivalenten Regel nur mit den bisherigen Mi eln der Refacola ist nicht möglich. Schließlich müsste dann die Regelvariable reference im for all-Teil der Regel formuliert werden, um überhaupt ein binds-Fakt in der Regel angeben zu können. Das würde aber – wegen der impliziten Allquantifizierung über alle Regelvariablen – ausschließen, dass diese Regel generiert werden könnte, wenn im Programm zum Beispiel keine einzige Referenz existieren würde. Existiert dann noch zusä lich ein privates Member, entsteht aber genau die Situation, die es ursprünglich zu verhindern galt. Es existieren weitere Einzelfälle, in denen es hilfreich wäre, Constraintregeln mit Existenzquantor zu formulieren. Ein Anwendungsfall, den auch schon Müller [Mül ] beschreibt, ist das Verdecken von Feldern. Java erlaubt Subklassen, Felder zu deklarieren, deren Name bereits bei Feldern der Superklasse Verwendung findet ( §§ . . . , . . . ). Für die Subklasse und alle ihre Subtypen gilt dann, dass unqualifizierte Zugriffe stets an das Feld der Subklasse binden, das gleichnamige Feld der Superklasse wird verdeckt. Bemerkenswert ist, dass dabei auch ein nicht-zugrei ares Feld ein zugrei ares Feld verdecken kann. Abbildung . zeigt ein Beispiel, in dem nach einer Rename-Refaktorisierung ein solcher Fall eintri , wodurch das Programm anschließend nicht mehr kompiliert. Das konkrete Problem ist hierbei, dass nach der Refaktorisierung auf dem Vererbungspfad von I nach B nun das umbenannte, gleichnamige und nicht zugrei are Feld i liegt. Freilich verhindert hä e dieses Problem werden können, wenn noch ein weiterer Vererbungspfad von I nach B existiert hä e, auf dem kein nicht-zugrei ares, verdeckendes Feld gelegen hä e. Das wäre insbesondere der Fall gewesen, wenn die Klasse zuvor B (mit völlig identischem Verhalten) als 1055 class B extends A implements I { int x = i; } deklariert worden wäre. Es genügt nämlich im Falle nicht zugrei arer, verdeckender Felder, dass innerhalb der Vererbungshierarchie von dem deklarierenden Typ bis zur referenzierenden Klasse mindestens ein Pfad existiert, auf dem kein solches Feld liegt. Für die vorliegende Arbeit ist dieses Problem unerheblich. Durch Qualifizierung kann auch auf verdeckte Felder der Superklasse zugegriffen werden. Auf Felder der unmi elbaren Superklasse kann dies anhand des Schlüsselwortes super geschehen, weiter oben in der Vererbungshierarchie hilft eine mit einem cast versehene Qualifikation auf this. Für das Beispiel aus . Constraintregeln für Java Abbildung . wäre eine passende Qualifizierung ((I)this).i. Ein Verdecken von Feldern kann und sollte daher nur durch Qualifizierungen und den in Abschni . beschriebenen Nachbearbeitungsschri erfolgen, sta durch Constraints verhindert zu werden. Eine Formulierung von Constraintregeln zu verdeckenden Feldern ist daher nicht nötig. Ein Fall, der sich hingegen tatsächlich auf die implementierten Werkzeuge auswirkt, betrifft das Verschieben abstrakter Methoden. Für diese gilt, dass erbende, nicht-abstrakte Klassen eine Implementierung bereitstellen oder erben müssen ( § . . . ). Wird nun eine abstrakte Methode in eine Superklasse verschoben, muss gelten, dass alle damit neu hinzukommenden, die Methode erbenden nicht-abstrakten Klassen diese Methode ebenso implementieren oder eine Implementierung erben müssen. Für jede solche Klasse muss also gelten, dass mindestens eine nicht-abstrakte Methode deklariert oder geerbt wird, welche die neu geerbte abstrakte Methode implementiert. Auch für diese Existenz-Bedingung stellt die Refacola derzeit keine Ausdrucksmöglichkeiten bereit. Faktisch kann damit ein P U M für abstrakte Methoden (derzeit) nicht unterstü t werden. 6. Berücksichtigung reflektiver Zugriffe Java bietet mit der Reflection-API [REF ] einen Mechanismus, zur Laufzeit die Struktur des ausgeführten Programms zu analysieren sowie den Ablauf eines Programms in Abhängigkeit von einer solchen Analyse zu gestalten [McC ]. So ist es möglich, eine Referenz auf eine Klasse zu erzeugen, welche dann – als Objekt repräsentiert – nach ihren Eigenschaften befragt oder über Methodenaufrufe instanziiert werden kann. Derart instanziierte Objekte können mi els der Reflection-API wiederum nach ihren Feldern und Methoden befragt werden. Es ergibt sich somit innerhalb der Reflection-API eine Parallelwelt zu den regulären Mi eln der Programmiersprache Java, in welcher ebenso Objekte instanziiert, referenziert und in ihrem Zustand verändert werden können. Dieses Kapitel stellt einen Sa von Constraintregeln für Refaktorisierungswerkzeuge vor, der solche Zugriffe über die Reflection-API berücksichtigt. Ein großes Problem ergibt sich daraus, dass sich Zugriffe über die Reflection-API einer statischen Programmanalyse en iehen. Zwar ist im Rahmen einer statischen Programmanalyse ersichtlich, dass über Reflection zum Beispiel ein Methodenaufruf erfolgt³⁵, allerdings bleibt der verborgen, welche Methode auf einem Objekt welcher Klasse aufgerufen wurde. Aus diesem Grund se en die in diesem Kapitel beschriebenen Constraintregeln eine Laufzeitanalyse des zu refaktorisierenden Programms voraus, welche beispielsweise während der Ausführung von Unit-Tests durchgeführt werden kann. Während dieser Programmausführung werden Aufrufe der Reflection-API in Form von geeigneten Fakten protokolliert. Bei anschließenden Refaktorisierungen können dann auf Basis dieser Faktenmenge mi els der in diesem Kapitel beschriebenen ReflectionConstraintregeln auch solche Bedingungen geprüft (und eingehalten) werden, die sich aus reflektivem Programmverhalten ergeben. Die Inhalte dieses Kapitel bauen im Wesentlichen auf einer vorangegangenen Publikation – [TB ] – auf. Der Autor vorliegender Arbeit war in dieser verantwortlich für die Entwicklung der Constraintregeln, wie sie in diesem Kapitel beschrieben werden, während der zweite Autor im Wesentlichen die Herausforderungen der Laufzeitanalyse gestemmt hat. Entsprechend sind die dort und hier genannten Constraintregeln Werk des Autors. Das im Folgenden nur dem besseren Verständnis halber auszugsweise erläuterte Werkzeug zum Sammeln der Laufzeitinformation – T F – hingegen ist nicht als Teil der vorliegenden Arbeit zu werten. 6.1. Die Reflection-API Die von der Programmiersprache Java angebotene Reflection-API findet in der Praxis rege Verwendung. Bodden et al. [EAJM , BSS+ ] berichten über von ihnen analysierte JavaProjekte (unter anderem die vollständige DaCapo-Benchmark-Suite [BGH+ ]), von denen alle Gebrauch von der Reflection-API machten. Dabei schwankte die Zahl reflektiver Aufru³⁵ der Fall, dass ein Aufruf an die Reflection-API mi els Reflection erfolgt, sei hier einmal ausgenommen . Berücksichtigung reflektiver Zugriffe fe von (beim avrora-Projekt) bis zu (beim tradebeans-Projekt). Anwendung findet die Reflection-API häufig dort, wo Programmteile voneinander zu entkoppeln sind. Ein typischer Anwendungsfall ist ein Framework, welches Code von Klienten aufrufen, aber gleichzeitig unabhängig von diesen implementiert sein soll. Anhand von Konfigurationsdateien kann dem Framework mitgeteilt werden, welche Java-Klassen konkret zu verwenden sind. Ihre Instanziierung erfolgt über die Reflection-API. Ein Beispiel hierfür ist das JUnit-Framework, bei welchem allerdings Angaben über auszuführende Testklassen in der Regel per Kommandozeilenparameter sta Konfigurationsdateien erfolgen. Weiterhin findet Reflection dort Anwendung, wo Objekte unabhängig von der durch sie instanziierten Klasse inspiziert werden sollen. Anwendungsbeispiele hierfür sind Prozessmonitore, die Auskunft über von bestimmten Programmteilen verwendete Objektgeflechte geben sollen oder auch Persistierungsframeworks, die ihnen übergebene Objektgeflechte in eine geeignete Textdarstellung oder Datenbank übertragen. Schon anhand dieser Beispiele ist zu erkennen, dass Aufrufe an die Reflection-API nicht nur im Anwendercode³⁶ zu finden sind, sondern auch aus (als jar-Dateien eingebundenen) Bibliotheken stammen können. Diese referenzieren dann über Reflection wiederum den Anwendercode. Ein großes Problem beim Umgang mit der Reflection-API resultiert aus der teilweise mangelhaften Dokumentation. Während sich eine Beschreibung der wesentlichen Grundfunktionen in jedem besseren Java-Handbuch findet und auch vom Entwickler Sun (beziehungsweise Oracle) verschiedene Anleitungen und auch JavaDoc-Kommentare innerhalb der eigentlichen API bereitgestellt wurden, mangelt es an einer offiziellen Dokumentation. So lassen sich viele Detailfragen erst anhand händischer Tests klären, was im Rahmen dieser Arbeit mehrfach nötig wurde. Class-Objekte Den Einstieg in die Reflection-API bildet die Klasse java.lang.Class. Instanzen dieser Klasse repräsentieren Java-Klassen. Zum Erzeugen einer solchen Instanz sieht Java zwei unterschiedliche Wege vor. Einerseits kann eine Instanz von Class entweder über ein sogenanntes class literal ( § . . ) gewonnen werden, indem einer Typreferenz ein .class nachgestellt wird. Andererseits erlaubt die statische Fabrikmethode Class.forName(...) die Erzeugung von Class-Objekten, indem ihr der voll qualifizierte Name der gewünschten Klasse übergeben wird. Ergänzend zum kurzen Einführungsbeispiel aus Abbildung . in Kapitel zeigt Abbildung . ein erweitertes Beispiel eines reflektiven Programms. In diesem wird in Zeile über den Aufruf Class.forName("a.C") eine neue Instanz eines Class-Objekts erzeugt, welches die Klasse C repräsentiert. Denselben Effekt hä e ein Aufruf a.C.class gehabt, beide liefern eine Referenz auf dasselbe Objekte zurück. Analyse von Programmstukturen Ist erst einmal ein Class-Objekt gewonnen, kann dieses über verschiedene get-Methoden nach seinen Eigenschaften befragt werden. Neben Abfragen zum Beispiel zum Klassennamen, der Zugrei arkeit oder dem umschließenden Paket ist es insbesondere möglich, eine Klasse nach den durch sie deklarierten Feldern und implementierten Methoden zu befragen, welche ebenso wie Klassen durch eigene Objekte repräsentiert ein Field-Objekt angefordert, welches werden. So wird im gegebenen Beispiel in Zeile das von C implementierte Feld f repräsentiert. Ebenso wäre es möglich, über die Methode ³⁶ also dem als Quellcode vorliegenden und potentiell zu refaktorisierenden Projekt . . Die Reflection-API 1059 package a; 1060 1061 1062 1063 1064 1065 1066 public class C { private String s; public String toString() { return s; } } 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 public class Reflection { public static void main(String[] args) throws Exception { Class c = Class.forName("a.C"); java.lang.reflect.Field f = c.getDeclaredField("s"); Object myInstance = c.newInstance(); f.setAccessible(true); f.set(myInstance, "abc"); System.out.println(myInstance); } } Abbildung . .: Beispiel für ein reflektives Programm. Zunächst wird eine Instanz der Klasse C erzeugt, dann folgt ein schreibender Zugriff auf das private Feld f. getDeclaredFields die Menge aller von einer Klasse deklarierten Felder zu erfragen. Sind auch die von den Superklassen geerbten Felder gefragt, so liefert die Methode getFields diese zusä lich zurück. Ganz analog existieren auch Methoden zur Abfrage von Methoden und Konstruktoren. Instanziierung, Methoden- und Feldzugriffe Ist bekannt, welcher Klasse ein Objekt angehört und welche Programmelemente es deklariert oder erbt, können diese auch aktiv genu t werden. Im Fall von Konstruktoren können analog zur Class Instance Creation Expression ( § . ) – also der Objektinstanziierung mi els new-Operator – über die parameterlose Methode newInstance von Class neue Instanzen einer Klasse angelegt werden, wie es im Beispiel in Zeile geschieht. Soll ein nicht-parameterloser Konstruktor aufgerufen werden, ist die Methode newInstance der Constructor-Klasse des jeweils gewünschten Konstruktors zu verwenden. Da sich einer statischen Analyse offensichtlich en ieht, ob die Typen der übergebenen Argumente denen des deklarierten Konstruktors entsprechen oder im Falle von Class.newInstance überhaupt ein parameterloser Konstruktor zur jeweiligen Klasse existiert, müssen sich etwaig ergebende Fehler zur Laufzeit abgefangen werden, was mi els einer java.lang.InstantiationException geschieht. Aufrufe von Methoden gestalten sich ganz ähnlich zu Konstruktoraufrufen, nur dass sta dessen die Methode invoke der Klasse Method verwendet wird, der neben den Argumenten noch das jeweilige Empfängerobjekt übergeben wird. Für den lesenden und schreibenden Zugriff auf Felder existieren get- und set-Methoden innerhalb der Klasse Field.³⁷ ³⁷ Da die Reflection-API auf Zeiten vor dem mit Version eingeführten Autoboxing ( § . . ) zurückgeht, existieren sowohl separate get- und set-Methoden für alle primitiven Typen, als auch mit Object typisierte . Berücksichtigung reflektiver Zugriffe Class.forName; Class.getDeclaredField; Class.newInstance; Field.set*; a.C; <a.C: java.lang.String s>; a.C; <a.C: java.lang.String s>; a.Reflection.main; a.Reflection.main; a.Reflection.main; a.Reflection.main; 5; 6; 7; 9; ; ; ; isAccessible=true; Abbildung . .: Ein durch das TamiFlex-Werkzeug erstellte Reflection-Log für eine Ausführung des Programms aus Abbildung . . Bemerkenswert ist, dass für reflektive Zugriffe – ebenso wie bei regulären Referenzen auf Felder, Methoden, Konstruktoren und Klassen – die Zugrei arkeiten der jeweiligen Deklarationen zu berücksichtigen sind. Allerdings gibt es drei gewichtige Unterschiede: Erstens unterscheiden sich die Zugrei arkeitsregeln merklich (Details führt Abschni . in diesem Kapitel aus). Zweitens lässt sich der Zugrei arkeitsschu mi els der Methode setAccessible umgehen, die von Feldern, Methoden und Konstruktoren implementiert wird und deren Verwendung im obigen Beispiel in Zeile den Zugriff auf das Feld f in Zeile überhaupt erst möglich macht. Dri ens ist es auch bei der Zugrei arkeit – gleich der der oben schon erwähnten Typprüfung – nicht möglich, mit einer statischen Codeanalyse zu entscheiden, ob eine Deklaration zugrei ar ist. Aus diesem Grunde findet auch die Prüfung auf Zugrei arkeit zur Laufzeit sta , wobei im Fehlerfall eine java.lang.IllegalAccessException geworfen wird. Analyse reflektiver Programmzugriffe Eine statische Analyse von reflektiven Zugriffen, welche Aufschluss darüber gibt, mit welchen konkreten Parameterwerten welche Methode der Reflection-API innerhalb welchen Ausdrucks im Programm aufgerufen wird, ist schwierig. Im gegebenen Beispiel aus Abbildung . lässt sich noch problemlos nachverfolgen, welche Zugriffe im Einzelnen erfolgen. Im Wesentlichen liegt dies an den verwendeten StringLiteralen, die eine eindeutige Zuordnung erlauben. In der Praxis werden übergebene Namen aber zumeist erst zur Laufzeit berechnet, häufig durch Extraktion aus Konfigurationsdateien [BSS+ , SR ]. Selbst wenn ein Analysewerkzeug auf diese Zugriff hä e, müsste es immer noch die Programmlogik durchschauen, nach welcher die Datei eingelesen und die entsprechenden Teilstrings extrahiert werden. Eine solche Analyse von Programmlogik zur Ermi lung von String-Werten ist zwar in Einzelfällen möglich [CMS ] und konnte auch schon zur statischen Auswertung reflektiver Ausdrücke genu t werden [LWL ], im allgemeinen Fall bleibt die Frage aber unentscheidbar. Man denke nur an den Fall, dass im obigen Programm der String a.C durch args[0] erse t wird und die Aufrufe des Programms von einer entfernten Stelle erfolgen, die dem Analysewerkzeug unbekannt bleibt. Für den Umgang mit reflektiven Zugriffen wird in dieser Arbeit daher eine dynamische Analyse verwendet, für welche auf das TamiFlex-Werkzeug [BSS+ ] zurückgegriffen wurde. Dieses protokolliert zur Laufzeit – zum Beispiel bei der Ausführung von Tests – sämtliche vom Programm durchgeführten Aufrufe an die Reflection-API mitsamt der jeweils übergebenen Argumente. Intern bedient es sich dabei der durch die java.lang.instrument-API ermöglichten sogenannten Java-Agenten. Ein solcher Agent kann bei Programmausführung der virtuellen get- und set-Methoden. In der mit vorliegender Arbeit gelieferten Implementierung werden diese aber alle identisch behandelt, weswegen im weiteren Text auf die unterschiedlichen Methoden nicht näher eingegangen wird. . . Refaktorisierung reflektiven Codes Maschine übergeben werden und beim Eintreffen bestimmter Ereignisse – im Falle von TamiFlex dem Aufruf einer Methode der Reflection-API – eigenen Code ausführen. Abbildung . zeigt eine Logausgabe für eine Ausführung des Programmbeispieles aus Abbildung . . Die erste Spalte gibt jeweils die Art des Aufrufs an die Reflection-API an. Die zweite Spalte zeigt die übergebenen Parameter an, welche im Fall von Class.getDeclaredField; und Field.set*; auch die verwendeten Parametertypen beinhalten. Die folgenden beiden Spalten geben die Quelle des Aufrufs an, für das Beispiel jeweils die main-Methode in der Klasse Reflection mit Zeilennummer³⁸. Die le te Spalte gibt jeweils an, ob im Falle einer Referenz auf ein Feld, eine Methode oder einen Konstruktor die isAccessible-Eigenschaft des Programmelements auf true gese t wurde. Im gegebenen Beispiel ist dies ab Zeile der Fall. 6.2. Refaktorisierung reflektiven Codes Es lässt sich leicht einsehen, dass bei der Refaktorisierung von Programmen, die von der Reflection-API Gebrauch machen, ganz ähnliche Bedingungen einzuhalten sind, wie sie sich auch für nicht reflektive Zugriffe ergeben. So verhält sich im Beispiel aus Abbildung . die Übergabe des Strings a.C wie eine reguläre Typreferenz. Wird entweder C in ein anderes Paket verschoben oder das Paket a beziehungsweise die Klasse C umbenannt, wird die Referenz ungültig. Nur im Gegensa zu einer regulären Typreferenz gestaltet sich das Anpassen einer solchen reflektiven Referenz schwierig bis unmöglich. Zwar ergibt sich für das Beispiel aus Abbildung . erneut eine noch recht einfache Situation, denn hier kann bei einem entsprechenden R oder M T der String innerhalb des Aufrufs Class.forName("a.c") angepasst werden. Im allgemeinen Fall muss aber wiederum damit gerechnet werden, dass der jeweils übergebene String erst zur Laufzeit berechnet wird. Eine weitere Schwierigkeit ergibt sich aus der Tatsache, dass eine einzelne reflektive Anweisung durchaus mehrfach ausgeführt werden kann, zum Beispiel, wenn in einer Schleife über mehrere zu instanziierende Klassen iteriert wird. Somit können sich aus einem einzelnen Ausdruck gleich mehrere reflektive Referenzen ergeben. Gilt eine Umbenennung nur in einem dieser Fälle, wäre eine zusä liche Fallunterscheidung einzufügen, was aber nicht unbedingt der Intention des Benu ers entsprechen muss. Zu guter Le t muss berücksichtigt werden, dass – wie in den einführenden Beispielen zur Reflection-API erwähnt – reflektive Aufrufe auch aus eingebundenen Bibliotheken stammen können, die sich einer direkten Bearbeitung en iehen und keine Änderungen ihres Quellcodes zulassen. Im allgemeinen Fall wird eine Refaktorisierung wegen fehlschlagender Zugriffe aus reflektiven Ausdrücken also eher abgelehnt werden müssen, als dass für diese eine geeignete Quelltex ransformation zu finden wäre. Abschni . wird dennoch einen Ausblick geben, wie auch reflektive Anweisungen während einer Refaktorisierung entsprechende Quelltex ransformationen erfahren können. Bisherige Ansätze Als bisher einziges Refaktorisierungswerkzeug für reflektive Programme kann der Refactoring Browser [RBJ ] für Smalltalk gelten. Smalltalk ist eine nicht typisierte, objektorientierte Programmiersprache, die umfangreiche [FJ ] und viel genu te [CRTR ] Reflection-Möglichkeiten bietet. Durch das fehlende Typsystem ergeben sich in ³⁸ Wegen der fortlaufenden Zeilennummerierung aller Codebeispiele in dieser Arbeit ergeben sich naturgemäß Abweichungen. . Berücksichtigung reflektiver Zugriffe Smalltalk schon für reguläre Methodenaufrufe³⁹ dieselben Probleme beim Refaktorisieren wie in Java bei Nu ung der Reflection-API. So ist wegen der fehlenden Typisierung allein anhand des Quellcodes keineswegs ersichtlich, ob eine bestimmte Methode vom Empfänger implementiert wird. Daher können Fehler auch nur zur Laufzeit behandelt werden, was in Smalltalk anhand einer zurückgegebenen doesNotUnderstand:-Nachricht passiert. Entsprechend nu te der Refactoring Browser ebenso eine Laufzeitanalyse [RBJ ], welche allerdings einige Besonderheiten aufwies, die sich damit erklären lassen, dass die Sprache Smalltalk dem Programmierer nicht den typischen Zyklus des Programmieren, Kompilieren und Ausführen vorschreibt. Sta dessen können in Smalltalk Programme entwickelt werden, indem zur Laufzeit das aktuell ausgeführte und ebenso in Smalltalk geschriebene Laufzeitsystem inklusive seines Klassenbrowsers nach und nach angepasst und in seinem Quellcode verändert wird, bis es die gewünschten Funktionalitäten aufweist. Der Refactoring Browser greift diesen Mechanismus auf, indem für eine Refaktorisierung die offensichtlich nötigen Änderungen sofort im Code ausgeführt werden, die sich ergebenden Folgeänderungen aber erst bei erkanntem Bedarf erfolgen. Soll beispielsweise eine Methode umbenannt werden, schreibt der Refactoring Browser den Code der Methodendeklaration unmi elbar um. Ergibt sich später zur Laufzeit – etwa bei der Ausführung von Tests – ein Methodenaufruf unter Verwendung des alten Namens, wird dies innerhalb einer Laufzeitanalyse erkannt und erst dann der entsprechende Methodenaufruf umgeschrieben. Somit wird sichergestellt, dass für die ausgeführten Testfälle das Programmverhalten unverändert bleibt. Während ein solches Verfahren für Smalltalk gute Dienste leistete, scheint es nicht sinnvoll, eine Adaption dessen für Java vorzunehmen. Zwar mag es technisch möglich sein, zur Laufzeit ein Java-Programm gegebenenfalls anzuhalten und mi els eines inkrementellen Compilers den Bytecode und Quellcode simultan zu verändern, soweit eine Laufzeitanalyse dies für nötig befindet. Allerdings lässt sich dieses Vorgehen nicht mit dem üblichen Bild der JavaWelt vereinen, in dem ein Programmierer nicht davon ausgehen muss, dass sich sein Code zur Laufzeit selbstständig verändert. Wünschenswert ist viel mehr ein Refaktorisierungswerkzeug, welches den üblichen Konventionen entspricht, dem Programmierer alle nötigen Quellcodeänderungen auf einmal in einem Dialog zu präsentieren und nach Bestätigung geschlossen in den Code zu übertragen. Aus diesem Grunde findet bei den in dieser Arbeit vorgestellten Refaktorisierungswerkzeugen die Laufzeitanalyse vor der Refaktorisierung sta . Der Benu ter muss hierzu zunächst unmi elbar vor einer Refaktorisierung bestehende Testsuiten anstoßen, deren Ausführung vom TamiFlex-Werkzeug überwacht wird. Die so gesammelten Informationen über reflektive Zugriffe werden in eine Faktenmenge umgewandelt, anhand derer aus Constraintregeln die Constraints generiert werden können, die zur Einhaltung aller zur Beibehaltung des Programmverhaltens erforderlichen Bedingungen nötig sind. 6.3. Programmelemente und Fakten Um auch reflektive Referenzen mi els Refacola-Constraintregeln zu bedingen, müssen diese analog zu den regulären Java-Referenzen mi els Programmelementen repräsentiert werden. Die verschiedenen Arten von Programmelementen ergeben sich für die in dieser Ar³⁹ Häufig wird im Smalltalk-Umfeld auch von Nachrichtenversand sta Methodenaufruf gesprochen. . . Programmelemente und Fakten beit vorgestellte Implementierung dabei direkt aus der Reflection-API. So existiert für jeden zu berücksichtigenden Methodenaufruf der Reflection-API eine entsprechende Programmelementart, wobei jede von diesen von der abstrakten Art ReflectiveReference ableitet. So werden zum Beispiel Aufrufe eines Class.forName(...) durch eine Programmelementart ClassForName repräsentiert. Insgesamt existieren solcher nicht-abstrakten Unterarten, die Methoden der Reflection-API repräsentieren, wobei mitunter verschiedene Methoden der Reflection-API zu einer Programmelementart zusammengefasst sind. So bietet zum Beispiel die Reflection-API gleich zwei Wege, eine Klasse zu instanziieren. Für ein Class-Objekt c sind die beiden Aufrufe c.newInstance() und c.getConstructor().newInstance() äquivalent.⁴⁰ Beide werden daher in einer Programmelementart ConstructorNewInstance zusammengefasst. Weiterhin werden beispielsweise die vielen get- und set-Methoden, mit denen für Field-Objekte deren Feldwert gelesen und geschrieben werden kann, in einer Programmelementart FieldGetSet zusammengefasst. Eine vollständige Auflistung aller Programmelementarten ist den entsprechenden Deklarationen in Anhang A zu entnehmen. Neben den nicht-abstrakten Programmelementarten für Zugriffe auf die Reflection-API existieren noch mehrere Programmelementarten, die genu t werden, um entweder Aufrufe an die Reflection-API mi els einer gemeinsamen Überart zu gruppieren, oder auch gezielt an eine solche Gruppe eine Constraintvariable zu vererben. So erbt zunächst jede der Arten reflektiver Referenzen von ReflectiveReference die beiden Constraintvariablen owner und hostPackage, welche die umschließende Klasse respektive das umschließende Paket angeben. Im Gegensa zu regulären Referenzen ist eine Constraintvariable für den umschließenden Topleveltypen (tlowner) entbehrlich. Für reguläre Java-Referenzen diente diese zur Bestimmung von Zugrei arkeiten. Für reflektive Zugriffe werden Zugrei arkeiten allerdings zur Laufzeit bestimmt, zu dieser sind alle Klassen allerdings schon in jeweils eigene class-Dateien kompiliert, für die keine Schachtelungsbeziehung mehr gegeben ist, welche also auch nicht für die Bestimmung von Zugrei arkeiten zurate gezogen werden kann. Ebenso über Vererbung werden verschiedene Namens-Variablen identifier an Programmelementarten vererbt, wo diese bei Aufrufen der Reflection-API übergeben oder zurückgegeben werden. So erbt beispielsweise die Programmelementart ClassForName sowohl von ReflectiveTypeNameReference als auch ReflectivePackageNameReference jeweils eine Namensvariable für den übergebenen Klassenund Paketnamen. Die Programmelementart MethodGetName, die einen Aufruf der Methode getName() von Method repräsentiert, erbt hingegen eine Namensvariable von ReflectiveMethodNameReference, die den nach Abarbeitung der Methode zurückgegebenen Methodennamen anzeigt. An Faktenarten ergeben sich bei der Behandlung von Reflection nur die beiden zweistelligen Prädikate reflectiveBinding und reflectiveReceiver. Beide nehmen eine ähnliche Rolle wie die bereits bekannten Faktenarten binds und receiver ein. Ein reflectiveBinding-Fakt repräsentiert die Bindung einer reflektiven Referenz an eine Deklaration. So wird für das Beispiel aus Abbildung . in Zeile das den Aufruf repräsentierende ClassGetDeclaredField-Programmelement per reflectiveBinding-Fakt an die Deklaration des Feldes s gebunden. Die EmpfängerBeziehung kommt zusä lich bei solchen Aufrufen an die Reflection-API ins Spiel, bei denen ⁴⁰ Technisch ist das nicht ganz korrekt. Im Verhalten unterscheiden sich beide newInstance-Methoden insofern, als dass bei einer auftretenden Ausnahme während des Konstruktoraufrufs die Klasse Constructor diese in eine InvocationTargetException kapselt, während die Klasse Class die Ausnahme direkt wirft. Für die für Refaktorisierungen zu prüfenden Bedingungen ist dies aber unerheblich. . Berücksichtigung reflektiver Zugriffe Zugriffe auf eine konkrete Objektinstanz sta finden, wie es im Beispiel in Zeile der Fall ist. Hier bindet der Aufruf an das Feld s, zusä lich ist der Empfänger hier ein Class-Objekt, welches eine C-Klasse repräsentiert, was über ein entsprechendes reflectiveReceiver-Fakt ausgedrückt wird und später bei der Berechnung von Zugrei arkeiten Relevanz haben wird. Generierung der Programmelemente und Fakten Die Generierung der Programmelemente und Fakten für reflektive Zugriffe gestaltet sich im Wesentlichen gradlinig. Aufgebaut wird sowohl auf der bereits erzeugten regulären Faktenbasis, welche insbesondere bereits die Programmelemente für die mi els der Reflection-API referenzierbaren Deklarationen enthält, als auch auf der entsprechenden Log-Ausgabe des TamiFlex-Werkzeugs. Le teres wird linear abgearbeitet, wobei für jeden Eintrag die entsprechenden Programmelemente und Fakten generiert werden. Zu beachten ist dabei, dass keine Zuordnung eins zu eins von Quellcodeanweisungen zu Programmelementen entsteht, sondern von Ausführungen der Anweisungen zu Programmelementen. Findet ein Aufruf an die Reflection-API innerhalb einer Schleife oder mehrfach aufgerufenen Methode sta und wird dieser entsprechend mehrfach (gegebenenfalls mit unterschiedlichen Parametern) ausgeführt, wird für jeden solchen Aufruf eine entsprechende Kombination aus Programmelement, reflectiveBinding- und gegebenenfalls reflectiveReceiver-Fakt erzeugt. Trickreich vom technischen Standpunkt her gestaltet sich bei der Faktengenerierung die Abbildung von zur Laufzeit erkannten Deklarationen und Referenzen auf die entsprechenden Anweisungen im Quellcode. So erhalten anonyme und lokale Klassen beim Kompilieren einen Dateinamen mit Hilfe fortlaufender Nummerierung, anhand dessen wieder auf die ursprüngliche Deklaration geschlossen werden muss. Komplizierter und auch kaum mehr eindeutig möglich ist hingegen eine Zuordnung von Referenzen. Zwar bieten Java-Compiler die Option, den Anweisungen im kompilierten Bytecode die jeweils entsprechenden Zeilennummern des Java-Quellcodes hinzuzufügen (insbesondere Debugger profitieren hiervon), allerdings verliert diese Information an Wert, wenn mehrere Anweisungen in einer Zeile stehen. Da in aller Regel in Java jeweils nur eine Anweisung bestehend aus wenigen Ausdrücken pro Zeile verwendet wird (was sich auch automatisiert vorm Kompilieren erzwingen ließe), macht die vorliegende Implementierung die Annahme, dass dies stets der Fall ist. 6.4. Constraintregeln Die Constraintregeln für Reflection lassen sich ähnlich den in Kapitel vorgestellten gliedern. Erneut kann eine Trennung nach den Bereichen Namen, Zugrei arkeiten, Orte, Typen und Unerwünschte Bindungsänderungen vorgenommen werden, wobei auch hier die Constraintregeln nicht aufeinander au auen, sondern in ihrer Gesamtheit zusammenwirken. Erneut ist es also dem Leser überlassen, in welcher Reihenfolge er sich die folgenden fünf Teilabschni e zu den oben genannten Bereichen erschließt. Namen Ebenso wie bei regulären Referenzen im Quellcode gilt gleichermaßen für Aufrufe über die Reflection-API, dass bei Bindungen anhand von Namen diese an Aufrufstelle und Deklaration übereinstimmen müssen. Für reguläre Referenzen wurde dies über die Constraintregel ReferenceIdentifier (siehe S. , ebenso S. ) zugesichert. Bei reflektiven Referenzen . . Constraintregeln geschieht dies analog über vier verschiedene Constraintregeln, die jeweils die Übereinstimmung von Paket-, Klassen-, Methoden- und Feldnamen fordern. Für Typnamen lautet die Regel wie folgt. 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 ReflectiveTypeNamesEquality for all r: Java.ReflectiveTypeNameReference d: Java.NamedType do if Java.reflectiveBinding(r, d) then r.identifier = d.identifier end Die weiteren drei Regel ReflectiveFieldNamesEquality ( S. ), ReflectiveMethodNamesEquality ( S. ) und ReflectivePackageNamesEquality ( S. ) sind analog formuliert. Die Aufteilung in mehrere Regeln ist nötig, da einzelne reflektive Referenzen durchaus mehrere Deklarationen referenzieren können. So referenziert ein Class.forName() wegen des übergebenen qualifizierten Namens sowohl ein Paket als auch einen Typen. Entsprechend erbt (wie oben bereits beschrieben) ClassForName sowohl vom Programmelement ReflectiveTypeNameReference als auch vom Programmelement ReflectivePackageNameReference je eine identifier-Variable und zieht ebenso die Erzeugung zweier reflectiveBinding-Fakten nach sich. Die beiden Constraintregeln ReflectivePackageNamesEquality und ReflectiveTypeNamesEquality stellen dann die Übereinstimmung von Typ- und Paketnamen sicher. Als eine weitere Bedingung aus einem Aufruf von Class.forName() ergibt sich, dass wegen des qualifizierten übergebenen Typnamens sich der Typ nicht mehr aus seinem ursprünglichen Paket wegbewegen darf, wie es beispielsweise bei einem Move Type geschehen könnte. Die folgende Regel ReflectiveClassForNameStayInPackage verhindert dies. 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 ReflectiveClassForNameStayInPackage for all r: Java.ClassForName d: Java.NamedClassOrInterfaceType p: Java.Package do if Java.reflectiveBinding(r, d), Java.reflectiveBinding(r, p) then d.hostPackage = p end Zugreifbarkeiten Ebenso wie für reguläre Zugriffe auf die Member eines Typen in Java Regeln zur Zugrei arkeit greifen, etabliert auch die Reflection-API eine Menge von Zugrei arkeitsregeln. Für Refaktorisierungen ist die korrekte Einhaltung von Zugrei arkeitsbedingungen insofern relevant, als dass bei dem reflektiven Zugriffsversuch auf eine nicht zugrei are Deklaration eine java.lang.IllegalAccessException ausgelöst wird, welche das Programmverhalten in unvorhergesehener Art beeinflussen kann. Im Einzelnen betroffen sind hiervon die Me- . Berücksichtigung reflektiver Zugriffe thoden newInstance von Class und Constructor, die Methode invoke der Klasse Method sowie die diversen get- und set-Methoden der Klasse Field. Es werden also Konstruktor- und Methodenaufrufe sowie Feldzugriffe beschränkt. Typreferenzen – wie beispielsweise innerhalb eines Class.forName(...) erfahren keine Einschränkungen durch Zugrei arkeiten, obwohl Typen mit Zugrei arkeitsmodifizierern ausgesta et sind. Dennoch werden diese indirekt bei Zugriffen auf Member einer Klasse relevant (dazu weiter unten). Neben der Tatsache, dass Typreferenzen keiner Zugrei arkeitsbeschränkung unterliegen, unterscheiden sich die Zugrei arkeitsregeln für reflektive Zugriffe auch in weiteren Punkten von den regulären Zugrei arkeitsregeln. Einige davon ergeben sich aus technischen Restriktionen, andere hingegen scheinen von den Autoren der Reflection-API beliebig festgelegt worden zu sein. Da es keine offizielle Dokumentation der Zugrei arkeitsregeln für JavaReflection gibt (die vorliegende Arbeit stü t sich im Wesentlichen auf spärliche JavadocKommentare sowie exzessives Testen), sind die Motivationen für diese Festlegungen unklar. Technisch bedingt ist, dass die Zugrei arkeitsregeln für reflektive Zugriffe nicht zwischen Topleveltypen und Membertypen unterscheiden. Während Java regulär den Zugriff auf private Deklarationen innerhalb desselben Typen auch von Membertypen aus gesta et, verweigert die Reflection-API solche Zugriffe. So wird im folgenden Beispiel in Zeile der Zugriff auf das private Feld der umschließenden Klasse gesta et (es kommt zu keinem Kompilierfehler), der Zugriff in der folgenden Zeile hingegen führt zu einer IllegalAccessException. 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 public class C { private String s = "abc"; static class MemberClass { public static void main(String[] args) throws Exception { C c = new C(); System.out.println(c.s); System.out.println(C.class.getDeclaredField("s").get(c)); } } } Ursächlich ist hierfür, dass zur Laufzeit durch den Compiler alle Typen (auch lokale und anonyme) in jeweils eigene Class-Dateien kompiliert werden. Eine etwaige Schachtelung ist für die Laufzeitumgebung nicht mehr unmi elbar ersichtlich und wird offensichtlich deswegen nicht mehr als Kriterium herangezogen. Während also auf private Deklarationen reflektiv nur noch unmi elbar innerhalb desselben Typen zugegriffen werden kann, ist innerhalb desselben Pakets default-Zugrei arkeit hingegen stets ausreichend, was exakt den regulären (nicht reflektiven) Zugrei arkeitsbedingungen entspricht. Unterschiede ergeben sich wieder bei der protected-Zugrei arkeit, welche durch die Reflection-API wiederum gar keine Berücksichtigung erfährt und stets als default-Zugrei arkeit interpretiert wird. Für reflektive Zugriffe über Paketgrenzen hinweg ist also zwingend public-Zugrei arkeit erforderlich. Ähnlichkeiten zu den regulären Zugrei arkeitsregeln lässt wiederum eine Einschränkung zur Zugrei arkeit deklarierender Typen beim Zugriff auf seine Member erkennen. So fordern alle oben genannten Methoden zum Zugriff auf Konstruktoren und deren Member, dass auch der jeweilige Empfängertyp zugrei ar ist. Ein Typ gilt dabei als reflektiv zugrei ar, wenn er die reflektive Anweisung unmi elbar umschließt oder als default- oder protected-zugrei ar und innerhalb desselben Pakets deklariert ist oder als public deklariert ist. In folgendem Bei- . . Constraintregeln spiel kommt es also in Zeile zu einer IllegalAccessException, da zwar das Feld s zugrei ar ist, aber nicht der umschließende Typ B. 1110 1111 1112 1113 1114 1115 1116 1117 package a; public class A { public static void main(String[] args) throws Exception { Class bClass = Class.forName("b.B"); Object o = bClass.newInstance(); System.out.println(bClass.getDeclaredField("s").get(o)); } } 1118 1119 1120 1121 1122 package b; class B { public String s = "abc"; } Leider lassen sich auch diese Bedingungen zu Typ-Zugrei arkeiten keiner dem Autor bekannten, offiziellen Dokumentation entnehmen, sondern waren nur anhand von ausführlichen, manuellen Tests zu erschließen. Erschwerend kam dabei hinzu, dass nicht einmal die in den IllegalAccessException-Objekten übergebenen Fehlermeldungen sachdienlich waren. Für das vorstehende Beispiel liest sich diese 1123 Class a.A can not access a member of class b.B with modifiers "public" wobei der dort genannte public-Modifizierer derjenige des Feldes s ist, nicht der eigentlich relevante der Klasse B. Entsprechend wenig verwundert, dass häufig zu beobachten ist, wie ratlose Benu er mit dieser Fehlermeldung in den üblichen Java-Hilfeforen anbranden. Eine weitere Besonderheit im Umgang mit Zugrei arkeiten und Reflection ergibt sich daraus, dass es die Reflection-API erlaubt, Zugrei arkeiten zu umgehen. So können Constructor-, Method- und Field-Objekte über die Methode setAccessible jeweils angewiesen werden, den Zugrei arkeitsschu zu ignorieren.⁴¹ Eine solche Anweisung zur Umgehung von Zugreifbarkeiten gilt dabei allerdings weder für eine konkrete Deklaration einer Klasse, noch eine Deklaration eines Objekts, sondern jeweils für ein konkretes Constructor-, Method- oder FieldObjekt. Alle anhand dieses Objekts durchgeführten Zugriffe sind dann von den Zugrei arkeitsregeln befreit. Das schließt jeweils auch die Bedingungen an Zugrei arkeiten von deklarierenden Typen ein. Die folgende Constraintregel ReflectiveAccessibility se t diese Zugreifbarkeitsbedingungen um. 1124 1125 1126 1127 1128 1129 1130 1131 ReflectiveAccessibility for all r: Java.ReflectiveAccess d: Java.MemberOrConstructor rt: Java.ClassOrInterfaceType do if Java.reflectiveBinding(r, d), ⁴¹ Freilich kann dies verhindert werden, indem beim Start der virtuellen Maschine ein sogenannter und geeignet konfigurierter SecurityManager übergeben wird. Dessen Existenz ist aber von konkreten Refaktorisierungen unabhängig und wird hier entsprechend nicht weiter thematisiert. . Berücksichtigung reflektiver Zugriffe 1132 1133 1134 1135 Java.reflectiveReceiver(r, rt) then r.accessEnabled = #public or( 1136 1137 1138 (((((r.owner != d.owner) −> (d.accessibility > #private)) and ((r.hostPackage != d.hostPackage) −> (d.accessibility = #public))) 1139 1140 1141 1142 and ((rt != d.owner) −> (rt.accessibility > #private))) and ((rt.hostPackage != d.hostPackage) −> (rt.accessibility = #public)))) end Die Constraintvariable accessEnabled gibt dabei an, ob für das jeweils verwendete ConstructorMethod- oder Field-Objekt die Umgehung der Zugrei arkeitsprüfung aktiviert wurde. Ihr initialer Wert ergibt sich bei der Faktengenerierung direkt aus dem von TamiFlex ausgegebenen Log, wie im Beispiel in Abbildung . in der le ten Spalte zu sehen. Weitere Bedingungen zu Zugrei arkeiten ergeben sich aus den drei Methoden getField, getMethod und getConstructor der Klasse Class. Diese Methoden können genu t werden, eine von einer Klasse deklarierte oder geerbte Deklaration (Felder, Methoden oder Konstruktoren, wobei le tgenannte allerdings in Java nicht vererbt werden) zu erfragen. Im Gegensa zu ihren Pendants getDeclaredField, getDeclaredMethod und getDeclaredConstructor, die keine geerbten Deklarationen zurückgeben, geben die drei zuerst genannten Methoden nur solche Deklarationen zurück, die public deklariert sind. Ansonsten kommt es zu einer Ausnahme, zum Beispiel einer NoSuchFieldException im Falle eines getField. Es leuchtet ein, dass deshalb eine über eine solche Methode zurückgegebene Deklaration ihre Zugrei arkeit nicht verändern darf, was die folgende Constraintregel ReflectiveClassGetDeclarationStayPublic zusichert: 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 ReflectiveClassGetDeclarationStayPublic for all r: Java.ReflectiveMemberReference d: Java.MemberOrConstructor do if Java.reflectiveBinding(r, d) then d.accessibility = # public end In dieser Hinsicht identisch verhalten sich die drei Methoden getFields, getMethods und getConstructors, die jeweils alle deklarierten und geerbten public-Deklarationen einer Klasse zurückgeben. Für sie formulieren die Constraintregeln ReflectiveClassGetMethodsStayPublic ( S. ), ReflectiveClassGetConstructorsStayPublic ( S. ) und ReflectiveClassGetFieldsSet) diese Bedingungen analog, dann aber für jeweils alle public-Deklarationen StayPublic ( S. der jeweiligen Art innerhalb derselben Klasse. Eine le te Bedingung für Zugrei arkeiten ergibt sich aus verschiedenen Methoden, die jeweils direkt oder indirekt den Zugrei arkeitsmodifizierer einer Deklaration zurückgeben. Das gilt insbesondere für die Methode getModifiers(), die von den Klassen Field-, Method-, . . Constraintregeln Constructor- und Class implementiert wird, aber auch für weitere Methoden derselben Klassen, wie toGenericString. Dass sich bei Vorliegen einer solchen Abfrage die Zugrei arkeit der jeweiligen Deklaration nicht ändert, wird durch die folgende Constraintregel ReflectiveFieldGetModifiersKeepAccessibility sichergestellt. 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 ReflectiveFieldGetModifiersKeepAccessibility for all r: Java.ReflectiveAccessModifierReference d: Java.MemberOrConstructor do if Java.reflectiveBinding(r, d) then d.accessibility = r.returnedAccessModifier end Die dort verwendete Programmelementart ReflectiveAccessModifierReference ist Überart aller reflektiven Zugriffe, die Informationen über Zugrei arkeiten zurückgeben. Orte Bereits im vorangegangenen Abschni wurden mit den Methoden getDeclaredField, getDeclaredMethod und getDeclaredConstructor der Klasse Class Beispiele für reflektive Zugriffe genannt, die Annahmen über den Ort einer Deklaration machen. Ist bekannt, dass ein Feld vor einer Refaktorisierung über einen Aufruf von Class.getDeclaredField zurückgegeben wurde und wird dieses Feld im Rahmen einer Refaktorisierung verschoben, muss hinterher derselbe Aufruf zwangsläufig in einer NoSuchFieldException resultieren. Die beiden Constraintregeln ReflectiveDeclaredMemberReferenceStayThere ( S. ) und ReflectiveDeclaredMemberSetReferenceStayThere ( S. ) stellen jeweils sicher, dass es in diesen Fällen zu keiner Verschiebung der Deklarationen kommt. Le tere der beiden Regeln behandelt die Varianten, die Mengen von Deklarationen zurückgeben. Ein sich dazu ergebendes gegenteiliges Problem kann daraus resultieren, dass bei einer Abfrage von zum Beispiel getDeclaredFields nach einer Refaktorisierung sich die zurückgegebene Menge von Deklarationen ändert, nicht weil Deklarationen wegverschoben wurden, sondern weil neue Deklarationen hinzugekommen sind. Die beiden Constraintregeln ReflectiveClassGetDeclaredFieldsNoNewFields ( S. ) und ReflectiveClassGetDeclaredMethodsNoNewMethods ( S. ) nehmen sich jeweils dieser Fälle für die Methoden getDeclaredFields und getDeclaredMethods an. Eine Berücksichtigung von Konstruktoren ist hierbei unnötig, da ein Verschieben von Konstruktoren zwischen Klassen weder Bestandteil üblicher Refaktorisierungen ist, noch im Rahmen der vorliegenden Arbeit unterstü t wird. Dieselben Bedingungen müssen weiterhin bei Aufrufen der Methoden getFields, getMethods und getConstructors eingehalten werden. Dabei gibt es aber einerseits die Einschränkung, dass nur public-Deklarationen betroffen sind, und andererseits eine Ausweitung auf Deklarationen der Superklassen, die bei diesen Methoden zusä lich berücksichtigt werden. Die Constraintregeln ReflectiveClassGetFieldsNoNewFields ( S. ), ReflectiveClassGetMethodsNo) sowie ReflectiveClassGetConstructorsNoNewConstructors nehmen sich NewMethods ( S. dieser Fälle an. Hierbei sind wiederum Konstruktoren zu berücksichtigen, da sie durch Änderungen der Zugrei arkeit hin zu public zu der Menge der neu zurückgegebenen Deklarationen hinzukommen können. . Berücksichtigung reflektiver Zugriffe 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 public class A { public void m(String string) { System.out.println(string); } public static void main (String[] args) throws Exception { String s = "abc"; Class a = A.class; Class s = String.class; Method m = a.getMethod("m", s); m.invoke(new A(), s); } } (a) ⇒ public class A { public void m(String string) { System.out.println(string); } public static void main (String[] args) throws Exception { Object s = "abc"; Class a = A.class; Class s = String.class; Method m = a.getMethod("m", s); m.invoke(new A(), s); } } (b) Abbildung . .: Wird in Zeile der deklarierte Typ von s zu Object geändert, bleibt das Programmverhalten unverändert. Eine le te Bedingung ergibt sich aus der Methode getDeclaringClass, welche von Field-, Method- und Constructor-Objekten implementiert wird. Erfolgt der Aufruf einer solchen Methode für eine Deklaration, die während einer Refaktorisierung verschoben wird, ändert sich zwangsläufig der zurückgegebene Wert. Die Regel ReflectiveDeclaringClassReferenceStayThere ( S. ) verhindert dies. Typen Deklarierte Typen spielen im Zusammenhang mit der Reflection-API eine untergeordnete Rolle. Hauptsächlich relevant werden deklarierte Typen beim Zugriff auf Methoden und Konstruktoren einer Klasse. So erwarten für die Klasse class alle vier Methoden getMethod, getDeclaredMethod, getConstructor und getDeclaredConstructor jeweils eine Angabe der deklarierten Parametertypen, um bei überladenen Methoden und Konstruktoren eine eindeutige Zuordnung zu erhalten. Die Constraintregeln ReflectiveParameterListMatching ( S. ) verhindert jeweils eine Abweichung der deklarierten Parametertypen im Falle eines solchen Aufrufs. Eine Berücksichtigung von Typen für Methodenaufrufe via Method.invoke (und analog Instanziierungen mi els Constructor.newInstance) ist hingegen nicht notwendig. Für diese sind jeweils nur die Laufzei ypen der Objekte bei Ausführung des Programms relevant. Wie die jeweiligen Referenzen auf die Objekte typisiert sind, ist unerheblich. Abbildung . zeigt ein Beispiel einer G D T -Refaktorisierung, die zulässig ist. Ändert sich in der Typ der Variablen s von String zu Object, kann diese dennoch per Reflection Zeile der Methode m(String s) übergeben werden. Erst wenn sich der Laufzei yp des durch s referenzierten Objekts zu Object ändern würde (indem der Initialisierungsausdruck von s beispielsweise durch new Object() erse t wird), kommt es zu einer IllegalArgumentException. Da aber keine der in dieser Arbeit angesprochenen Refaktorisierungen Änderungen an Laufzeittypen vorsieht (Bindungen an Konstruktoren werden stets erhalten), ist dieser Fall nicht zu berücksichtigen. . . Constraintregeln Unerwünschtes Verdecken Ähnlich wie es für reguläre Java-Zugriffe das Konzept des Verdecken (hiding) gibt, kann eine dem Verdecken ähnliche Situation für reflektive Ausdrücke auftreten. In folgendem Beispiel wird innerhalb der main-Methode die parameterlose Methode m auf dem Typen C mi els getMethod abgefragt. Zurückgegeben wird die entsprechende, von Super geerbte Methode. 1176 1177 1178 class Super { public void m() { System.out.println("a"); }; } 1179 1180 1181 1182 class C extends Super { public void n() { System.out.println("b"); }; } 1183 1184 1185 1186 1187 1188 1189 1190 1191 public class Reflection { public static void main(String[] args) throws Exception { Class c = C.class; Method m = c.getMethod("m"); Object o = c.newInstance(); m.invoke(o); } } Wird nun beispielsweise die Methode n in m umbenannt (oder eine andere parameterlose Methode mit Namen m nach C verschoben), ändert sich die reflektive Bindung. Es wird beim Aufruf von getMethod nicht mehr länger m aus Super zurückgegeben, sondern nun m aus C. Die Constraintregel ReflectiveClassGetMethodNoHiding 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 ReflectiveClassGetMethodNoHiding for all r: Java.ClassGetMethod d: Java.Method rt: Java.NamedType c: Java.Method do if Java.reflectiveBinding(r, d), Java.reflectiveBinding(r, rt), d != c then (d.identifier != c.identifier) or(c.accessibility < # public) or(not Java.sub(c.owner, d.owner)) or(not Java.sub*(rt, c.owner) or(d.parameters.declaredParameterType != c.parameters.declaredParameterType)) end beugt entsprechenden Bindungsänderungen vor. Sie fordert nämlich, dass neben der referenzierten Deklaration jede weitere Methodendeklaration entweder einen abweichenden Namen, abweichende Parametertypen oder zu geringe Zugrei arkeit haben muss oder nicht innerhalb der Typhierarchie zwischen referenzierter Deklaration und Empfängertyp deklariert ist. . Berücksichtigung reflektiver Zugriffe Analog dazu beugt die Constraintregel ReflectiveClassGetFieldNoHiding ( S. ) Bindungsänderungen bei Feldreferenzierungen vor, allerdings kann hier auf die Prüfung der Parametertypen verzichtet werden. 6.5. Zurückschreiben in reflektive Ausdrücke Abschni . hat bereits angesprochen, dass nicht erfüllte Bedingungen aus den ReflectionConstraintregeln dieses Kapitels tendenziell eher ein Ablehnen der Refaktorisierung zur Folge haben, als dass eine Erfüllung mi els des Constraintlösers möglich wäre. Die Ursache hierfür ist, dass für viele der Variablen, die für reflektive Referenzen deklariert sind, keine entsprechende Änderung des Quellcodes gefunden werden kann. In der Folge müssen diese Variablen bei der Faktengenerierung als unveränderlich gekennzeichnet werden (dazu siehe Abschni . . ). Betrachtet man die Unterarten von ReflectiveReference, so deklarieren oder erben diese insgesamt sechs verschiedene Variablenarten. Die Werte der direkt von ReflectiveReference geerbten Variablen owner und hostPackage ergeben sich aus dem jeweiligen Ort, der eine reflektive Referenz beheimatet. Er ist zwar variabel, allerdings nur in Ausnahmefällen auch Teil der erlaubten Änderungen einer Refaktorisierung, sodass es unwahrscheinlich ist, dass der Constraintlöser durch zum Beispiel Verschieben einer Methode mit einem reflektiven Aufruf eine Constraintlösung herbeiführen kann.⁴² Die übrigen Constraintvariablen identifier, returnedAccessModifier und parameters spiegeln weitere, übergebene oder empfangene Eigenschaften von Deklarationen wider, die Constraintvariable accessEnabled repräsentiert eine Eigenschaft des Objekts, über die der Zugriff erfolgte (zum Beispiel das Field-Objekt zu einem Feld). Diese Variablen in ihren Werten verändern zu können, kann helfen, eine Constraintlösung herbeizuführen. Ob die dazu nötigen Programmtransformationen aber den Vorstellungen des Programmierers entsprechen, ist fraglich. Im Fall der Constraintvariable accessEnabled kann ein geänderter Wert in den Quellcode zurückgeschrieben werden, indem ein zusä licher Aufruf von setAccessible in das zu refaktorisierende Programm eingefügt wird. Soll zum Beispiel in folgendem Programm die Zugreifbarkeit von i in A hin zu private verringert werden, würde dies für den Aufruf in Zeile eine IllegalAccessException auslösen. 1210 package a; 1211 1212 1213 1214 class A { int i = 1; } 1215 1216 1217 1218 1219 1220 1221 public class Reflection { public static void main(String[] args) throws Exception { Field field = Class.forName("a.A").getDeclaredField("i"); Object object = new A(); System.out.println(getVal(field, object)); } ⁴² Dass die Orts-Variablen eines reflektiven Aufrufs denen der jeweils umschließenden Deklaration entsprechen, sichert die (bisher nicht erwähnte) Constraintregel ReflectiveReferenceSamePackage ( S. ) zu. . . Zurückschreiben in reflektive Ausdrücke 1226 1227 1228 ... <expression>.getInt(<parameter>); ⇒ Field <freshName> = <expression>; <freshName>.setAccessible(true); ... <freshName>.getInt(<parameter>); (a) (b) Abbildung . .: Allgemeines Muster, um beim Zurückschreiben geänderte Werte von accessEnabled-Variablen abzubilden. static int getVal(Field field, Object o) throws Exception { return field.getInt(o); } 1222 1223 1224 1225 } Entsprechend würde aufgrund der Constraintregel ReflectiveAccessibility (siehe S. , ebenso S. ) ein Constraint erstellt werden, welches für den Aufruf in Zeile entweder fordert, dass die Zugrei arkeit von i nicht private ist, oder, dass für field zuvor mi els der setAccessible-Methode die Zugrei arkeitsmechanismen umgangen werden. Die erste Bedingung ist dabei nicht erfüllbar, sie widerspricht der Intention. Die zweite Bedingung ist erder Ausdruck field.setAccessible(true); eingefügt wird. Ein allfüllbar, indem vor Zeile gemeines Muster hierfür ist in Abbildung . gegeben. Es ist aber zu beachten, dass als Nebenwirkung dessen dann für alle reflektiven Zugriffe, die über diesen Ausdruck erfolgen, die Zugriffsregeln umgangen werden. Der Grund hierfür ist, dass ein einzelner Ausdruck mehrfach ausgeführt werden kann und somit mehrere reflektive Referenzen erzeugen kann. Würde in obigem Beispiel also die Methode getVal auch noch von anderer Stelle aufgerufen werden, würden die erweiterten Zugriffsrechte dann auch dort gelten. Ähnlich kann man auch bei identifier-Variablen innerhalb von reflektiven Referenzen vorgehen. Abbildung . zeigt ein allgemeines Muster für eine Transformation, die angewendet werden kann, wenn eine Klasse umbenannt werden soll, die innerhalb einer Class.forName(...) referenziert wird. Hierbei tri aber das Problem, dass ein einzelner Ausdruck mehrfach ausgeführt werden kann, stärker zu Tage. Zwar wird bei der Transformation aus Abbildung . noch auf den Namen geprüft, existieren aber zwei Klassen mit gleichem voll qualifizierten Namen,⁴³ von denen nur eine umbenannt wird, gilt die Umformulierung des reflektiven Ausdrucks dennoch für beide. Doch auch wenn man davon ausgeht, dass eine solche Refaktorisierung bedeutungserhaltend ist, muss dennoch die Frage gestellt werden, ob sie tatsächlich dem entspricht, was sich der Programmierer erhofft. Es darf vermutet werden, dass in einem Großteil der Fälle dem Programmierer mehr mit einem Hinweis auf die jeweilig betroffene reflektive Referenz geholfen ist, als mit den etwas befremdlich anmutenden Codetransformationen. Abbildung . hat bereits einen Kompromiss aufgezeigt, indem dort zusä lich ein Kommentar eingefügt wurde, der den Programmierer direkt auf das Problem hinweist. ⁴³ Dieser Fall ist zwar exotisch, aber da Java das Hinzuladen von Klassen zur Laufzeit gesta et, kann er tatsächlich eintreten. . Berücksichtigung reflektiver Zugriffe ... Class.forName(<old name>) // TODO: consider renaming of <old name> // to <new name> in <expression> if(<freshName>.equals("<old␣name>") <freshName> = "<new␣name>"; ... Class.forName(<freshName>) (a) (b) 1229 ⇒ 1230 1231 1232 1233 Abbildung . .: Allgemeines Muster, um beim Zurückschreiben geänderte Werte von identifier-Variablen reflektiver Referenzen abzubilden. 6.6. Einschränkungen des vorgestellten Ansatzes Die in diesem Kapitel beschriebene Laufzeitanalyse hilft gemeinsam mit den genannten Constraintregeln, während Refaktorisierungen ungewollte Änderungen des Programmverhaltens zu erkennen und zu verhindern. Dennoch bringt der Ansa einige Einschränkungen mit sich, die im Folgenden genannt werden sollen. Als wesentliche Einschränkung ist zu nennen, dass die Laufzeitanalyse nur solche Aufrufe an die Reflection-API berücksichtigen kann, die während der Programmausführung vor der Refaktorisierung tatsächlich aufgetreten sind. Wird die Laufzeitanalyse also beispielsweise während der Ausführung einer Testsuite durchgeführt, kann lediglich zugesichert werden, dass sich für diese Tests das Programmverhalten nicht ändert. Werden Aufrufe an die Reflection-API durch die Testsuite nicht abgedeckt, werden hierfür keine Fakten angelegt und es werden keine entsprechenden Constraints generiert. Als ein weiteres Problem ist zu nennen, dass die aufgrund von Aufrufen an die ReflectionAPI erzeugten Constraints in Einzelfällen zu restriktiv sein können. Die Laufzeitananlyse erkennt nur, welche Aufrufe sta gefunden haben und welche Informationen über Programmelemente oder deren Zustand dabei über- oder zurückgegeben wurden. Es bleibt ihr verborgen, zu welchem Zweck die Daten jeweils verwendet werden. Ein prominentes Beispiel, für welches hieraus unnötig scharfe Einschränkungen resultieren, ist das JUnit-Framework. Bis zur dri en Version wurden als Tests auszuführende Methoden gekennzeichnet, indem ihre Namen mit dem Präfix test zu versehen waren. Wird in diesem Fall per Method.getName der Methodenname abgefragt, erkennt der vorgestellte Ansa zwar, dass der Methodenname Einfluss auf das Programmverhalten (in diesem Falle die Menge ausgeführter Tests) hat und lehnt eine Umbenennung ab. Gleichzeitig verkennt er aber, dass ein Umbenennen zum Beispiel von testeDies zu testeDas die Testausführung nicht weiter beeinflusst. Dieses Phänomen betrifft insbesondere auch Funktionen, die nur Programminformationen abfragen, um diese zum Beispiel in Logs zu schreiben. Nachdem eine Deklaration zum Beispiel umbenannt wird, ist es dann vermutlich nicht Intention des Programmierers, dass hinterher noch derselbe, dann veraltete Name, in den Log-Ausgaben auftaucht. Handelt es sich wiederum nicht um Logs, sondern um ein wieder einlesbares Datenformat, welches abwärtskompatibel zu halten ist, ist genau gewünscht, weiterhin den alten Namen zu verwenden. Le tendlich ist es im Falle eines nicht erfüllbaren Constraintsystems somit wohl am sinnvollsten, dem Programmierer lediglich die fehlschlagenden Reflection-Constraints zu präsentieren und ihn selbst entscheiden zu lassen, wie er mit diesen umgeht. . . Einschränkungen des vorgestellten Ansa es Eine eher exotische Einschränkung des vorgestellten Ansa es resultiert aus einer Schwäche des TamiFlex-Werkzeugs. Dieses erkennt nur erfolgreiche Aufrufe an die Reflection-API. Wird also beispielsweise über Method.invoke(...) eine Methode mit einer Menge von vom Typen her nicht passender Parametertypen aufgerufen, sodass es zu einer IllegalArgumentException kommt, findet dieser Aufruf in den TamiFlex-Ausgaben keine Berücksichtigung. Natürlich ist es aber denkbar, dass durch eine Refaktorisierung (beispielsweise durch Änderung an den Parametertypen einer Methode) der vorher fehlgeschlagene Aufruf nun zum Erfolg führt. Sta der Ausnahmebehandlung kommt es dann zum Methodenaufruf, was aller Vermutung nach das Programmverhalten verändert. Dennoch werden solche Fälle durch den vorliegenden Ansa nicht abgedeckt. 7. Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse Die vorhergehenden Kapitel und haben sich ausführlich den Bedingungen gewidmet, die einzuhalten sind, wenn Java-Programme bedeutungserhaltend transformiert – also refaktorisiert – werden sollen. Wirft man noch einmal einen Blick zurück auf das schematische Bild eines Refaktorisierungswerkzeugs mit Refacola aus Abbildung . auf Seite , sind (gemeinsam mit der Sprachdefinition aus Kapitel ) in der Abbildung linksseitig die beiden oberen Komponenten abgeschlossen. Damit ist der Hauptbeitrag der vorliegenden Arbeit abgeschlossen. Dennoch sind – wie sich aus oben genannter Abbildung ergibt – noch einige weitere Komponenten zu liefern, bis ein fertiges Refaktorisierungswerkzeug geboren ist. Zunächst sind die Constraintregeln in ihrer Formulierung unabhängig von einem konkreten Refaktorisierungswerkzeug. Erst mit Angabe von Refaktorisierungsdefinitionen (Abbildung . links unten) wird spezifiziert, was Intention und erlaubte Änderungen der fertigen Refaktorisierungswerkzeuge sein sollen. Weiterhin müssen Komponenten für die Faktenextraktion, das Zurückschreiben sowie die Benutzerschni stelle implementiert werden. Dieses Kapitel beschreibt diese noch verbliebenen Komponenten. 7.1. Deklaration der Refaktorisierungswerkzeuge Wie Kapitel beschrieben hat, sind die in der gegebenen Refacola-Sprachdefinition enthaltenen Variablen ausreichend, um einen Großteil solcher Refaktorisierungswerkzeuge zu implementieren, die Deklarationen und deren Eigenschaften in Java-Programmen verändern. Insgesamt wurden im Rahmen dieser Arbeit fünf verschiedene Refaktorisierungen zur Implementierung ausgewählt, wobei mit der Auswahl eine gewisse Diversität erreicht werden sollte. So entstanden zwei Verschieberefaktorisierungen – einmal auf Klassen- und einmal auf Methodenebene – und je ein Werkzeug zur Änderung von Namen, deklarierten Typen und Zugrei arkeiten. Die einzelnen Werkzeuge bieten jeweils noch verschiedene Optionen, um die neben der Intention weiterhin erlaubten Änderungen zu steuern, sodass sich aus Refacola-Sicht insgesamt verschiedene Refaktorisierungswerkzeuge ergeben. Der entsprechende Refacola-Code alangegeben. ler Werkzeuge ist in Anhang C ab Seite 7.1.1. Move Compilation Unit Die Absicht einer M C U -Refaktorisierung ist, eine Überse ungseinheit (also *.java-Datei) von einem Paket in ein anderes Paket zu verschieben, typischerweise um einen . Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse oder mehrere Topleveltypen mitsamt enthaltenen Membern zwischen Paketen zu bewegen. Die Intention (also der forced change) besteht im Ändern des hostPackage einer TypeRoot⁴⁴. Als erlaubte Änderung muss es dem Constraintlöser gesta et sein, das hostPackage weiterer, möglicherweise in der TypeRoot enthaltener Deklarationen zu ändern. Da beim Verschieben einer Kompilierungseinheit in ein anderes Paket auch die jeweilige package declaration ( § . ) (das Schlüsselwort package gefolgt vom Paketnamen als erste Anweisung innerhalb einer Überse ungseinheit) mit anzupassen ist, umfassen die erlaubten Änderungen ebenso einen Wechsel des identifier von PackageDeclaration-Elementen. Das Verschieben von Deklarationen über Paketgrenzen hinweg kann Einfluss auf Zugreifbarkeiten haben, wenn Deklarationen default- oder protected-Zugrei arkeit haben. Aus diesem Grund wurde für M C U ein zweiter Modus implementiert, in dem zusä lich Zugrei arkeitsmodifizierer angepasst werden dürfen. Im Folgenden wird – in Anlehnung an die Benennungen aus [SP a] – der Basismodus von M C U ohne erlaubte Zugrei arkeitsänderungen mit dem Zusa NOC (no other changes) kenntlich gemacht, der Modus mit erlaubten Änderungen von Zugrei arkeiten trägt den Zusa CA (change accessibility). 7.1.2. Pull Up Member P U M ist eine der klassischen, schon von Fowler [Fow ] beschriebenen Refaktorisierungen – dort allerdings noch einmal unterteilt nach P U M und P U F . Ziel ist es, eine Instanzmethode oder ein nicht-statisches Feld in einen Supertypen zu verschieben. Soweit mehrere Subklassen dasselbe Feld oder dieselbe Methode implementieren, sieht Fowler vor, dass durch das P U diese Redundanz behoben wird, indem alle Implementierungen in den Subklassen entfernt werden. Da die bisher entwickelten Constraintregeln keine Prüfung zulassen, ob verschiedene Implementierungen derselben Methode dasselbe Verhalten aufweisen, beschränkt sich das in dieser Arbeit entwickelte Werkzeug auf den Fall des P U aus nur einer Klasse heraus. U in der Änderung des Wertes einer owner-Variable Der forced change besteht beim P eines Feldes oder einer Methode. Die weiteren erlaubten Änderungen müssen tlowner- und hostPackage-Eigenschaften von Membern und Referenzen einschließen, die in den zu verschiebenden Membern enthalten sein können. Weiterhin muss im Rahmen der erlaubten Änderungen auch zugelassen werden, Ausdrücke in ihrem inferierten Typen zu ändern, denn wenn zum Beispiel innerhalb einer zu verschiebenden Methode eine this-Referenz liegt, erzwingen die Constraints eine Änderung ihres Typen auf die jeweilige Superklasse,⁴⁵ was für sich allein genommen noch keine Ablehnung der Refaktorisierung begründen darf. In der in Anhang C gezeigten Implementierung ist eine Besonderheit zu beobachten: Ansta nur den Wechsel des owner der zu verschiebenden Methode als Intention zu formulieren und ⁴⁴ Die Java-Sprachdefinition unterscheidet nicht anhand unterschiedlicher Programmelemente zwischen als quellcode vorliegender *.java und kompilierten und in das Programm eingebundenen *.class-Dateien. Da für beide gleiche Bedingungen gelten, werden diese sta dessen einheitlich als TypeRoot behandelt. Technisch ist es also bei dem beschriebenen Werkzeug auch möglich, als Intention die Verschiebung einer *.class-Datei anzugeben, faktisch führt dies aber nie zu erfüllbaren Constraints, da die hostPackage-Variable einer *.class-Datei wie alle anderen Variablen von Programmelementen aus Bibliothekscode als unveränderlich markiert sind. ⁴⁵ wie die Regel This ( S. ) sicherstellt . . Deklaration der Refaktorisierungswerkzeuge den Wechsel des tlowner und des hostPackage im Rahmen der Constraintlösung über die entsprechenden Constraints sicherzustellen, sind dort ebenso besagte Wechsel des tlowner und hostPackage Teil des forced change. Diese müssen also beim Initiieren der Refaktorisierung für die zu verschiebende Methode im Vorhinein ermi elt und ebenso passend gese t werden. Dieser Aufwand rechtfertigt sich dadurch, dass in der Folge bei Angabe der allowed changes diese forced changes genu t werden können, um per @forced die Domänen der Constraintvariablen einschränken zu können (dazu Abschni . . ). Sollen für eine Anwendung eines P U M nur die oben aufgezählten erlaubten Änderungen gelten, sei dieser noch recht restiktive Modus erneut mit dem Suffix NOC (no other changes) bezeichnet. Darüber hinaus werden in vorliegender Implementierung noch zwei weitere, auch miteinander kombinierbare Modi angeboten. Da auch beim P U M Verschiebungen über Paketgrenzen hinweg sta finden können, wurde ebenso eine mit Suffix CA (change accessibility) bezeichnete Variante implementiert, die auch Änderungen von Zugreifbarkeiten ermöglicht. Weiterhin können P U -Refaktorisierungen daran scheitern, dass eine weitere, in einer hochzuschiebenden Deklaration referenzierte Deklaration mit verschoben werden muss. Abbildung . auf Seite zeigte bereits ein Beispiel hierfür. Dieser Modus sei mit Suffix CL (change location) gekennzeichnet. Der Fall, dass sowohl weitere Deklarationen verschoben als auch Zugrei arkeiten geändert werden dürfen, ist durch den Modus CACL (change accessibility and change location) repräsentiert. 7.1.3. Rename R ist der Klassiker unter den Refaktorisierungen und entsprechende Werkzeuge sind die häufigst ausgeführten Refaktorisierungswerkzeuge überhaupt [MHPB ]. Die vorliegende Implementierung erlaubt ein Umbenennen von Feldern, Typen, Methoden und Konstruktoren. Implementierungen eines R für Parameter und lokale Variablen mitsamt dafür nötiger Regeln wurden auf den Ergebnissen dieser Arbeit au auend durch Spengler [Spe ] geliefert und werden in vorliegender Arbeit daher nicht weiter untersucht. Die Intention eines R ist die Umbenennung einer benannten Deklaration. In der Folge sind als erlaubte Änderungen nicht nur Umbenennungen von Referenzen zu gesta en, sondern auch Umbenennungen weiterer Deklarationen. So erzwingt das Umbenennen eines Typen auch das Umbenennen seiner Konstruktoren und – im Falle von primären Topleveltypen – der umschließenden Überse ungseinheit. Bei Methoden sind ebenso überschriebene und überschreibende Methoden mit umzubenennen. Nicht zu den erlaubten Änderungen zählt hingegen das Ändern der Bezeichner von Feldern. Der einzige Fall, in welchem das Umbenennen eines Feldes als erlaubte Änderung dienlich wäre, wäre eine Namenskollision, bei der der neue Name eines Feldes mit dem eines weiteren Feldes in Konflikt stände (zum Beispiel, wenn beide in derselben Klasse deklariert sind). In diesem Fall müsste der Benu er interaktiv einen neuen auf Seite ). Namen vergeben (siehe dazu auch Fußnote Weitere erlaubte Änderungen, die noch zusä liche Modi rechtfertigen würden, ergeben sich nicht. Deswegen wurde nur ein Modus – im Folgenden CI (Change Identifier) genannt – implementiert. . Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse 7.1.4. Generalize / Specialize Declared Type Beim G beziehungsweise S D T besteht die Intention der Refaktorisierung aus einer Änderung des Typen einer Deklaration entweder hin zu einem Sub- oder Supertypen des ursprünglich verwendeten Typen. In vorliegender Implementierung wurde sich dabei auf Typen von Feldern und Rückgabetypen von Methoden beschränkt. Das Ändern des Typen einer solchen Deklaration wird als forced change mi els Umbelegung des typeBinding der TypeReference innerhalb der jeweiligen Deklaration umgese t. Als erlaubte Änderungen sind dem Constraintlöser in jedem Fall die Änderung des identifier solcher Typreferenzen zu gesta en, da ansonsten das jeweilige Constraintsystem nicht lösbar wäre. Weiterhin ist es Ausdrücken zu gesta en, ihren jeweiligen inferierten Typen expressionType beziehungsweise inferredClassOrInterfaceType zu ändern. Damit wird bewirkt, dass zum Beispiel bei Typänderung eines Feldes die Refaktorisierung nicht allein dadurch zurückgewiesen wird, dass auch Referenzen auf dieses Feld ihren inferierten Typen ändern müssen und es nicht können. Es ist zu erwarten, dass viele Typ-Refaktorisierungen abgelehnt werden, wenn sich nur der Typ einer einzelnen Deklaration ändern darf. Mitunter ist auch weiteren Deklarationen eine Änderung ihres deklarierten Typen zu gesta en. Abbildung . auf Seite zeigte hierfür ein Beispiel. Aus diesem Grunde wurde ein zusä licher Modus implementiert, der au auend auf oben beschriebener Variante – sie sei im Nachfolgenden ebenso als NOC bezeichnet – noch die Änderung weiterer deklarierter Typen von Feldern, Methoden, Parametern und Variablen zulässt. Dieser Modus wird entsprechend mit dem Suffix CT (change type) gekennzeichnet. Zwar verschiebt ein G beziehungsweise S D T keine Deklaration über Paketgrenzen, ändert aber Bindungen von Typreferenzen, sodass Bedarf bestehen kann, zuvor an einer bestimmten Stelle nicht zugrei are Typen dort zugrei ar zu machen. Um Änderungen von Typzugrei arkeiten möglich zu machen, wurde analog zu den oben beschriebenen Werkzeugen ein entsprechender Modus CA (change accessibility) implementiert. Für sowohl weitere Typ-Änderungen als auch Änderungen von Zugrei arkeiten wurde ein CACT-Modus (change accessibility and change type) implementiert. 7.1.5. Change Accessibility Die Absicht einer C A -Refaktorisierung ist die Änderung der Zugrei arkeit einer Deklaration und damit die allgemeinere Formulierung des schon bei Fowler [Fow ] beschriebenen H M . Im Gegensa zu diesem macht C A weder Vorgaben zur Art der Deklaration, noch dazu, ob die Zugrei arkeit erweitert oder beschränkt werden soll. Mit einer Ausnahme ergibt sich beim C A kein Vorteil daraus, neben der Intention noch weitere Änderungen von Zugrei arkeiten zuzulassen. Lediglich beim Überschreiben oder Hiding von Methoden kann das Ändern von Zugrei arkeiten weiterer Methoden zusä liche Refaktorisierungen ermöglichen, wie bereits das Beispiel aus Abbildung . im ersten Kapitel dieser Arbeit zeigte. Daher wurde neben dem NOC-Modus für alle Arten von Deklarationen, die einen Zugrei arkeitsmodifizierer haben, ein CA-Modus speziell für C A auf Methoden angeboten. . . Deklaration der Refaktorisierungswerkzeuge 7.1.6. Validierung der Benutzereingaben Zu jedem der im vorhergehenden Abschni . beschriebenen Refaktorisierungswerkzeuge wurde ferner eine zusä liche Komponente implementiert, die auf herkömmliche, imperative Art und Weise einige wesentliche Vorbedingungen prüft. Dies umfasst in der Hauptsache ganz offensichtlich nötige Prüfungen der Benu ereingaben auf Plausibilität, so zum Beispiel, ob überhaupt alle nötigen Parameter entsprechend gese t sind. Weiterhin wird geprüft, ob die vom Benu er gegebenen Parameter zu den von den Refacola-Refaktorisierungen erwarteten Eingaben passen, so zum Beispiel, ob eine für ein P U ausgewählte Methode nicht als static deklariert ist oder ob für ein C D T der ausgewählte Typ kein primitiver ist (dazu Näheres in Abschni . ). Außerdem umfasst die zusä liche Prüfung von Vorbedingungen auch solche, die aus verschiedenen in dieser Arbeit jeweils zuvor genannten Gründen nicht constraintbasiert erfolgen. Beispiele dafür sind der Test auf Gültigkeit von durch den Benu er beim R vorgegebenen Namen (dazu näher in Abschni . . ) oder der Test, ob ein zu verschiebendes, finales Feld auch initialisiert wurde (dazu Abschni . ). 7.1.7. Auswahl einer Constraintlösung Abschni . hat beschrieben, dass eine jede Constraintlösung für ein gegebenes Refaktorisierungsproblem (einen korrekten Regelsa vorausgese t) zwar eine korrekte Lösung für dieses darstellt, allerdings nicht immer den Wünschen des Benu ers entsprechen muss. Insbesondere sind Änderungen von Variablenwerten zu vermeiden, die nicht aktiv zur Lösung des Constraintsystems beitragen. So kann es zum Beispiel Variablenbelegungen geben, bei denen die Erse ung des Variablenwerts durch den initialen Wert der Variablen weiterhin zu einem gelösten Constraintsystem führt. Wie in Abschni . beschrieben, ist die Berechnung einer minimalen Lösung häufig wegen der sich ergebenden Laufzeiten ausgeschlossen. Bei händischen Tests hat sich sehr häufig als störend gezeigt, dass insbesondere bei Modi mit erweiterten erlaubten Änderungen (CA, CL, CT, CACL, CACT) diese durch den Constraintlöser auch genu t wurden, obwohl jeweils ein restriktiverer Modus genügt hä e. Es wurden also zum Beispiel bei einem G D T im CT-Modus weitere Typen mit angepasst, obwohl dies gar nicht nötig gewesen wäre. Daher nu t die vorliegende Implementierung das in Abschni . bereits vorgeschlagene Verfahren, bei einer Werkzeuganwendung mit erweiterten erlaubten Änderungen zunächst die jeweils restriktiveren Modi beginnend bei NOC durchzuprobieren, um bei Vorliegen einer Lösung für diesen die Anzahlen unnötiger Änderungen zu minimieren. Im praktischen Umgang mit den Werkzeugen hat sich gezeigt (und in der Evaluation in Kapitel . bestätigt), dass insbesondere die restriktiven Modi mit nur sehr geringen Laufzeiten auskommen, weswegen es durch das gegebenenfalls mehrfache Constraintlösen hier nicht zu spürbaren Einbußen kommt. Als weiteren störenden Faktor im praktischen Umgang mit den Werkzeugen hat sich das unnötige Anpassen von Zugrei arkeiten erwiesen. So ist insbesondere bei Anwendungen, die tatsächlich eine Änderung von Zugrei arkeiten erfordern und somit tatsächlich von den erlaubten Änderungen profitieren, zu beobachten, dass es zu unnötig vielen Quellcodeänderungen kommt. So fiel bei händischen Tests zum Beispiel auf, dass das Verringern der Zugreifbarkeit einer Methode gleichzeitig bedingte, dass weitere Methoden derselben Klasse oder ih- . Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse rer Superklassen in ihrer Zugrei arkeit verringert wurden, obwohl dies für eine Constraintlösung nicht notwendig war. Insbesondere bei Deklarationen, die bewusst (zum Beispiel beim Au au eines Frameworks) höhere Zugrei arkeit haben als eigentlich nötig, ist dieses Verhalten unerwünscht. Als pragmatische Lösung wird in der vorliegenden Implementierung vor der Refaktorisierung in der Faktenbasis die Zugrei arkeit von public-Deklarationen als unveränderlich gekennzeichnet. Auch wenn dies in der Praxis zum gewünschten Verhalten der Werkzeuge führt, ist dieses Vorgehen keineswegs generisch. Umgekehrt liegt nahe, dass sich zu vielen Refaktorisierungswerkzeugen Heuristiken finden lassen, mit denen sich nicht erwünschte (aber dennoch bedeutungserhaltende) Teiländerungen ausschließen lassen und welche sich unterbinden lassen, indem Constraintvariablen gezielt als unveränderlich gekennzeichnet werden. Sollte beispielsweise der Fall auftreten, dass für die Implementierung eines Change Declared Type-Werkzeugs unnötig viele Deklarationen in ihrem Typen angepasst werden, kann über einen Zuweisungsgraphen [TR ] die Menge genau solcher Deklarationen gefunden werden, die tatsächlich über Zuweisungen von der Ursprungsdeklaration erreicht werden kann. Alle weiteren Deklarationen können dann als unveränderlich markiert werden. 7.1.8. Benutzerschnittstelle Für die in dieser Arbeit implementierten Werkzeuge wurde eine Benu erschni stelle implementiert, um einen testweisen Gebrauch innerhalb der Eclipse-Pla form möglich zu machen. Da der Hauptbeitrag dieser Arbeit – die Entwicklung der Constraintregeln für besagte Refaktorisierungswerkzeuge – unabhängig von einer speziellen Entwicklungsumgebung ist, wurde die Benu erschni stelle bewusst nur prototypisch implementiert. Die einzelnen Werkzeuge lassen sich über Kontextmenüeinträge der outline in Eclipse aufrufen, die sich dann jeweils öffnenden Dialoge fragen nötige Eingabeparameter sowie Modi ab. Die Eingabedialoge sind dabei auf ein Minimum reduziert. Sollen auch reflektive Zugriffe berücksichtigt werden, ist zuvor eine mi els des separat zu beziehenden TamiFlex [BSS+ ] generierte Log-Datei in einem bestimmten Verzeichnis zu hinterlegen. Auch gestaltet sich mitunter die Auswahl der Parameter in den Dialogen aufwändig. So werden bei einem G /S D T derzeit alle wählbaren Typen in einer einfachen drop-down-Liste angezeigt. Abgesehen von der damit einhergehenden mühsamen Auswahl benötigt allein die Berechnung der Typen und damit die Anzeige des Dialogs einige Sekunden. Eine geeignete Implementierung würde hier die bereits in anderen Dialogen von Eclipse vorhandenen Möglichkeiten nu en, bequem und effizient Deklarationen auszuwählen. Im Falle der Auswahl eines Typen wäre dies entweder eine inkrementell berechnete Baumansicht der Typhierarchie oder ein Textfeld mit automatischer Vervollständigung, in welches ein Typname einzugeben ist. Soweit der Benu er eine Vorschau der durchzuführenden Änderungen wünscht, wird ihm diese angezeigt. Da die für die Implementierung verwendete Rückschreibekomponente von Frenkel [Fre ] entsprechende Schni stellen bedient, konnten für die Vorschaudialoge die bereits vom Eclipse-Framework angebotenen Funktionen genu t werden. Weiterhin wird dank derselben Schni stellen die ebenso verfügbare Option, Refaktorisierungen rückgängig zu machen, auch vom Eclipse-Framework übernommen. Da die Entwicklung einer Erklärungskomponente noch aussteht (dazu in Abschni . ), brechen Werkzeuganwendungen bei einem . . Faktengenerierung nicht lösbaren Constraintsystem derzeit mit einem entsprechenden Hinweisfenster, aber ohne nähere Angabe zum nicht erfüllbaren Constraintsystem, ab. Abbildung . zeigt verschiedene Bildschirmfotos der implementierten Benu erschni stelle. 7.2. Faktengenerierung Wie bereits in Abschni . dargestellt, ist die Grundlage der Constraintgenerierung die Faktenbasis, gegen welche die Prämissen der Constraintregeln ausgewertet werden und somit durch Erse ung der Regelvariablen innerhalb der Konklusion die Constraints entstehen. Damit eine solche Faktenbasis gemäß der jeweiligen Sprachdefinition aufgebaut werden kann, generiert die Refacola eine Sprach-API (siehe auch Abbildung . ), in welcher jede Art von Programmelement und jede Faktenart durch eine Java-Klasse repräsentiert ist. Diese Klassen besi en wiederum Methoden, um die initialen Werte der ihnen zugehörigen Constraintvariablen respektive die von den Fakten referenzierten Programmelemente anzugeben. Im derzeitigen Stadium der Entwicklung sieht das Refacola-Framework lediglich eine Abfrage-Engine vor, die stets den Au au einer vollständigen Faktenbasis fordert, also die Generierung aller Programmelemente und Fakten eines Programms unabhängig von der Art des Eingabeprogramms und Refaktorisierungsproblems. Der Au au einer Faktenbasis, in welcher Fakten und Programmelemente nur dann erstellt werden, wenn sie tatsächlich durch eine entsprechende Abfrage angefordert werden, wird hingegen nicht unterstü t. Somit reduziert sich die Faktengenerierung auf das Erstellen der abstrakten Syntaxbäume, auf ein Traversieren des Eingabeprogramms und das Berichten stets aller Programmelemente mit den initialen Werten ihrer Variablen sowie der Fakten. Die vorliegende Implementierung baut auf den JDT [JDT ] auf, welche zu gegebenen JavaProjekten in Eclipse auf Anforderung bereits Syntaxbäume der Quellcodedateien bereitstellen und in denen viele Abhängigkeiten – insbesondere Bindungen – bereits aufgelöst und somit navigierbar sind. Der abstrakte Syntaxbaum ist somit angereichert zu einem abstrakten Semantikgraphen (engl. abstract semantic graph) [DRW ]. Ebenso wie die zu traversierenden Semantikgraphen Zyklen enthalten können, kann es auch in der zu erstellenden Faktenbasis zu Zyklen kommen. So ist ein Eingabeprogramm denkbar, in welchem zwei Klassen A und B jeweils Methoden B m() und A m() mit Rückgabetypen der jeweils anderen Klasse beinhalten. Um die initialen Werte für die Typen der Methoden zu se en, muss zunächst die jeweils andere Klasse als Programmelement verfügbar sein. Um derartige Konflikte zu lösen, wurde – ähnlich zum Compilerbau, wo moderne Sprachen mit ihren Vorwärtsreferenzen ebenso nicht mit einem Single-Pass-Compiler [ASU ] auskommen – auch bei der Faktengenerierung auf ein mehrstufiges Verfahren gese t. In diesem sind verschiedene Faktenlieferanten in einer Ke e hintereinander geschaltet. Jeder Faktenlieferant ergänzt jeweils die bis dahin erstellte Faktenbasis um neue Programmelemente, initiale Werte von Variablen oder Fakten. So werden in einem ersten Durchlauf lediglich Programmelemente für Pakete, Klassen und Interfaces, Methoden und Felder erstellt. Im folgenden Durchlauf werden dann die initialen Werte der ihnen zugehörigen Variablen gese t, wobei nun angenommen werden kann, dass für deren Constraintvariablen die nötigen Programmelemente bereits vorliegen. Unabhängig von den Vorteilen bei der Handhabung von Zyklen hat die Aufteilung der Faktengenerierung in einzelne Faktenlieferanten auch den Vorteil, dass die Faktengenerierung insgesamt einfacher . Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse Abbildung . .: Bildschirmfotos des implementierten User Interface. Links oben das Kontextmenü einer Methode in Eclipse, die neu angebotenen Werkzeuge sind durch Icons roter Refacola-Dosen gekennzeichnet. Rechts oben ist die Eingabemaske eines P U M mit Auswahl des CACL-Modus gezeigt, unten die Vorschau der berechneten Änderungen mit nebenstehendem Originalcode. Im Einzelnen ist zu sehen, dass das Hochziehen der Methode calculate() ebenso ein Hochziehen der Methode loadData() bewirkt. Ebenso wird bei le terer Methode der private-Modifizierer entfernt, um weiterhin die Zugrei arkeit innerhalb des Konstruktors von Sub zu gewährleisten. . . Namensqualifizierung zu warten und erweitern ist, da sich die Komplexität innerhalb der einzelnen Faktenlieferanten deutlich reduziert. So kann zum Beispiel die Faktengenerierung für sub-Fakten mit dem Au au der Subtyphierarchie gekapselt und unabhängig von anderen Komponenten implementiert werden. Ebenso können auch unkompliziert einzelne Faktenlieferanten deaktiviert werden, so zum Beispiel der Faktenlieferant für Programmelemente und Fakten, die sich aus reflektiven Aufrufen ergeben, wenn der Benu er bei Ausführung des Werkzeugs keine dynamische Codeanalyse wünscht. 7.3. Namensqualifizierung Abschni . hat beschrieben, dass in der derzeitigen Refacola-Sprachdefinition der Sprache Java ebenso wie in den Constraintregeln keine Qualifizierungen von Referenzen berücksichtigt werden und dass dies wegen der vielen sich daraus ergebenden bedingten und damit teuren Constraints auch nicht sinnvoll ist. Sta dessen kommt in vorliegender Arbeit zur Qualifizierung von Referenzen der Ansa von Schäfer et al. zur Anwendung, wie er bereits ausführlich in Abschni . beschrieben wurde und unter Mitwirkung des Autors vorliegender Arbeit mit dem constraintbasierten Ansa kombiniert wurde [STST ]. Da le tgenannte Arbeit zur Vereinigung von constraintbasiertem Ansa und locked bindings allerdings auf dem JastAddJCompiler [EH ] au aute und die Implementierung somit in aspektorientiertem JastAddCode gegeben war, war eine direkte Wiederverwendung des dortigen Codes im Zusammenspiel mit den JDT [JDT ] nicht möglich. Sta dessen wurden im Rahmen dieser Arbeit jene Teile des locking und unlocking von Bindungen erneut implementiert, die für die Berechnung von Qualifizierern zuständig waren.Dabei wurde auf eine Prüfung vieler bei Schäfer behandelter Einzelfälle verzichtet, was teilweise Einschränkungen für den Benu er der sich ergebenden Werkzeuge mit sich bringt. So wird zum Beispiel beim Verschieben einer Deklaration nicht im Einzelnen geprüft, ob in ihr enthaltene Typreferenzen am neuen Ort einen entsprechenden Typ-Import benötigen, dieser schon vorhanden ist oder gegebenenfalls gar ein Typ gleichen Namens schon importiert ist. Sta dessen wird jede verschobene Typreferenz pauschal voll qualifiziert, um ohne tiefere Programmanalyse Konflikte in jedem Fall ausschließen zu können. Ebenso bewirkt ein Verschieben von Typen, dass alle Referenzen auf diesen Typ voll qualifiziert werden, unabhängig davon, ob eine Qualifizierung tatsächlich nötig ist. Auch sind die durchgeführten Prüfungen auf Verscha ung von Feldern, welche ebenso Qualifizierungen nötig machen, nur genau auf die oben beschriebenen Refaktorisierungswerkzeuge hin beschränkt. All diese Einschränkungen sind für einen Produktiveinsa der Werkzeuge und des Frameworks hinderlich, stellen aber keine prinzipiellen Probleme des constraintbasierten Ansa es dar, da [STST ] bereits aufgezeigt hat, dass und wie ein effektives Postprocessing zur Namensqualifizierung implementierbar ist. Eine im Erscheinen befindliche Arbeit von Nicolai [Nic ] nimmt sich bereits einer solchen Komponente an, was die hier nur prototypische Implementierung rechtfertigt. 7.4. Rückschreibekomponente Wie in Abschni . beschrieben, wird beim constraintbasierten Ansa nicht direkt eine Transformation des Syntaxgraphen berechnet, sondern die Änderungen in abstrakterer Form als Ei- . Deklaration konkreter Refaktorisierungswerkzeuge und Integration in Eclipse genschaften von Programmelementen beschrieben. Eine Rücküberse ung in konkrete Änderungen im abstrakten Syntaxbaum (oder auch direkt im Quelltext) ist anschließend in einem von der Constraintlösung unabhängigen Schri mi els einer Rückschreibekomponente durchzuführen. Eine solche Rückschreibekomponente, implementiert von Frenkel [Fre ], findet in dieser Arbeit Verwendung. Ebenso wie der Faktengenerator baut auch die Rückschreibekomponente auf den JDT auf. Zur Eingabe steht ein bereitgestelltes Interface bereit [Fre ], über welches die Constraintlösung berichtet werden kann. Als Ausgabe erhält man sogenannte Rewrite-Objekte, welche Transformationen der Syntaxbäume repräsentieren und direkt an die Eclipse-IDE zwecks Darstellung in einer Vorschau oder Anwendung auf den Quellcode übergeben werden können. Dabei behandelt die Rückschreibekomponente von sich aus schon verschiedene technische Details, die innerhalb der Constraintregeln somit ausgespart werden konnten. So werden bei der Faktengenerierung explizit gemachte – aber im Quellcode nur implizit vorhandene – Modifizierer beim Zurückschreiben korrekt ausgelassen und auch Felddeklarationen, die mehrere Felder innerhalb einer Anweisung deklarieren, korrekt in einzelne Anweisungen aufgetrennt, soweit sich Änderungen nur auf eines der Felder beziehen [Fre ]. 8. Evaluation Dieses Kapitel soll Antworten auf die Fragen liefern, ob der in dieser Arbeit entwickelte Sa von Constraintregeln korrekt und mit annehmbarer Laufzeit arbeitet und ob die in Kapitel angeführten Vorteile constraintbasierter Refaktorisierungen – insbesondere das einfache Ermöglichen nötiger Folgeänderungen – in der Praxis tatsächlich Relevanz haben. Im Wesentlichen baut die Evaluation dabei auf einem systematischen und automatisierten Anwenden der in Kapitel beschriebenen Refaktorisierungswerkzeuge auf Open-SourceProgrammen auf, wobei für jede Anwendung gemessen wurde, ob eine Constraintlösung vorlag, nach welcher Ausführungszeit diese gefunden wurde, wie viele Änderungen diese im Code auslöst und ob das Programm nach der Transformation fehlerfrei kompilierte. In Stichproben wurden auch den Open-Source-Programmen beiliegende Testsuiten ausgeführt. Zum Test der in Kapitel beschriebenen Constraintregeln wurden insgesamt mehr als . Refaktorisierungen angestoßen, von denen in ca. % eine Constraintlösung gefunden und damit eine passende Quelltex ransformation angeboten werden konnte. In weiteren . Tests wurden Refaktorisierungen angestoßen, um Aussagen über die in Kapitel beschriebenen Constraintregeln zu reflektivem Programmverhalten zu gewinnen. Weitere Messungen, insbesondere eine umfassende Testsuite mit über . handgeschriebenen JUnit-Testfällen (davon knapp aus [Spe ]), von denen jeder eine einzelne Refaktorisierung von der Faktengenerierung bis zum Zurückschreiben ausführt, ergänzen die Evaluation. 8.1. Forschungsfragen Die Vorgehensweise der Evaluation orientiert sich am Goal-Question-Metric-Ansa von Basil et al. [BCR ], wobei bereits der oben stehende erste Absa dieses Kapitels die Ziele der Evaluation (goal) beschreibt. Aus diesem Ziel lassen sich die folgenden Forschungsfragen (question) ableiten: Frage 1: Arbeiten die implementierten Constraintregeln und die daraus entwickelten Werkzeuge korrekt? Für die entwickelten Refaktorisierungswerkzeuge muss gelten, dass sie gültigen Quellcode erzeugen, also solchen, der nach der Transformation kompiliert, und dass dessen Programmverhalten unverändert bleibt. Weiterhin muss gelten, dass die Werkzeuge die Benu erwünsche tatsächlich umse en, also beispielsweise nicht einfach gar keine Änderung ausgeführt wird. Weiterhin sind von den Werkzeugen Eigenschaften zu erfüllen, die für jedes Programm gelten sollten: sie sollten keine ungültigen Zustände annehmen (in Java also insbesondere keine nicht abgefangenen Ausnahmen auslösen) und regulär terminieren (engl. proper termination) [Dij ]. Frage 2: Unterliegen die constraintbasierten Werkzeuge übermäßig starken Einschränkungen? Übermäßig starke Einschränkungen [SMG ] liegen vor, wenn eine Codetransfor- . Evaluation mation (gegebenenfalls mit weiteren, erlaubten Codetransformationen) bedeutungserhaltend möglich gewesen wäre, aber dennoch durch das Werkzeug abgelehnt wurde. Ein Refaktorisierungswerkzeug, das nur in Einzelfällen von erfüllten Vorbedingungen ausgeht und Transformationen anbietet, kann zwar nicht direkt als inkorrekt bezeichnet werden, allerdings ist sein praktischer Nu en dennoch begrenzt. Frage 3: Bringen zusätzliche, erlaubte Änderungen in der Praxis einen Mehrwert? Ein Vorteil constraintbasierter Refaktorisierungswerkzeuge liegt darin, ohne nennenswerten Zusa aufwand bei der Implementierung weitere, erlaubte Änderungen zuzulassen. So kann das Verschieben von Deklarationen das Ändern von Zugrei arkeiten bedingen, was wiederum das Ändern weiterer Zugrei arkeiten bedingt. Es soll festgestellt werden, ob und wie häufig diese Fälle in der Praxis überhaupt auftreten. Frage 4: Sind die sich ergebenden Laufzeiten praxistauglich? Ein Refaktorisierungswerkzeug mit (zu) langen Berechnungszeiten wird faktisch nicht zum Einsa kommen, zumal es derzeit ohnehin schon mit durch den Programmierer händisch durchgeführten Refaktorisierungen konkurrieren muss [MHPB , VCN+ ]. Ab welchen Laufzeiten ein Werkzeug konkret an Tauglichkeit einbüßt, lässt sich nicht genau definieren und hängt sowohl vom Benu er als auch von der konkreten Art des Refaktorisierungsproblems ab. Generell lässt sich aber annehmen, dass die Berechnungszeiten den unteren einstelligen Sekundenbereich nicht übersteigen sollten. Frage 5: Bringt die Behandlung der sich aus der Reflection-API ergebenden Bedingungen einen Mehrwert? Kapitel hat eine Reihe von Bedingungen aufgezeigt, die sich bei der Refaktorisierung von reflektivem Java-Code ergeben und wie diese Bedingungen durch constraintbasierte Refaktorisierungswerkzeuge Berücksichtigung finden können. Es soll festgestellt werden, ob und in wie weit Notwendigkeit für eine Berücksichtigung dieser Bedingungen besteht. 8.2. Korrektheitsbeweise Insbesondere die erste Forschungsfrage – die nach der Korrektheit der implementierten Constraintregeln und daraus resultierender Werkzeuge – schürt den Wunsch nach einem formalen Korrektheitsbeweis, aber auch bei der Frage nach zu starken Restriktionen wäre ein solcher wünschenswert. Leider ist so ein Beweis nicht vollständig durchführbar – und damit im Fall von Refaktorisierungswerkzeugen nahezu wertlos. Ein Korrektheitsbeweis für Java-Refaktorisierungswerkzeuge müsste als Eingabe die JavaSprachspezifikation haben. Diese liegt aber nur in Form mehrerer hundert Seiten weitgehend textueller Beschreibung vor [GJSB ]. Der erste Schri eines Beweises wäre also, die Sprachspezifikation in eine geeignete formale Notation zu überführen, anhand derer ein systematischer Abgleich der implementierten Constraintregeln mit der Sprachspezifikation erfolgen könnte. Da jedoch die vorliegenden Constraintregeln genau denselben Zweck haben, Teile der Java-Sprachspezifikation zu formalisieren, ist unklar, wie dann die Korrektheit der im . . Korrektheitsbeweise Rahmen des Beweises nötigen Formalisierung der Sprachspezifikation nachgewiesen werden soll. Schaut man sich in der Literatur Korrektheitsbeweise für Formalisierungen (von Teilen) der Java-Sprachspezifikation an, haben sie stets mit demselben Problem zu kämpfen. Niemals ist vollständig gesichert, dass alle in der Sprachspezifikation genannten Bedingungen berücksichtigt wurden. Abschni . nannte bereits Arbeiten zur Formalisierung von Java, die im Wesentlichen von Nipkow und Oheimb [NO ] stammen und die starke Parallelen zu den von Tip et al. [TKB ] genannten Constraintregeln zu Typ-Refaktorisierungen zeigen. Dabei strengten beide formale Korrektheitsbeweise an, wenn auch das Beweisziel jeweils unterschiedlich war. Bei Nipkow et al. ist das Beweisziel, die Korrektheit des Typsystems von Java zu zeigen. Genauer haben sie bewiesen, dass für ein beliebiges, gültiges (also kompilierendes) Java-Programm mit einem typsicheren Programmzustand (ein Programmzustand, in dem jede Variable tatsächlich ein Objekt referenziert, das mindestens dem Typen entspricht, mit dem die jeweilige Variable annotiert ist) auch das Ausführen einer beliebigen, vom Compiler als typsicher erkannten Anweisung zu einem typsicheren Zustand führt. Dazu wurden sowohl die Typregeln der Sprache, als auch die Bedeutung der weiteren in Java zur Verfügung stehenden Anweisungen von der natürlichen Sprache der Sprachspezifikation in eine formale Darstellung übertragen, die die weiteren Beweisschri e ermöglicht hat. Tro der durch diesen Beweis gewonnenen Aussagen zur Korrektheit des Java-Typsystems bleibt eine mögliche Fehlerquelle, die der Beweis nicht vollständig schließen kann. Es bleibt unklar, ob eine der sich aus dem natürlichsprachlichen Teil der Java-Sprachspezifikation ergebenden Bedingung übersehen wurde. Ein solches Versehen würde nämlich im weiteren Beweis nicht zwangsläufig auffallen. Nimmt man an, dass zum Beispiel eine Typregel zu streng übernommen wurde, ergibt der Beweis erst recht, dass Java typsicher ist. Wurde hingegen eine Typregel zu schwach übernommen (oder übersehen), kann es sein, dass das Typsystem von Java bereits im Vorhinein rigoroser als nötig in seinen Typregeln war. Während unklar bleibt, ob dies auch für allgemeine Arbeiten zur Formalisierung der JavaSprachspezifikation gilt, ist nach Erfahrung des Autors vorliegender Arbeit das Übersehen oder falsche Übertragen von sich aus der Sprachspezifikation ergebenden Bedingungen die häufigste Fehlerquelle für Refaktorisierungswerkzeuge.⁴⁶ Dies gilt für seine eigenen Arbeiten, aber auch die anderer Entwickler. Ein Beispiel kann hier das durch Tip et al. [TKB ] constraintbasiert implementierte E I liefern, welches innerhalb der JDT von Eclipse [JDT ] verfügbar ist. Während die von Tip et al. angegebenen Constraintregeln reguläre Methodenüberschreibungen ( § . . . ) korrekt berücksichtigen, wurde offensichtlich die zu Beginn von Kapitel thematisierte und in der Sprachspezifikation nur versteckt erwähnte Möglichkeit der Überschreibung von Methoden, die nicht aus einer Superklasse stammen ( § . . . ), übersehen. Als Folge erlaubt das resultierende Werkzeug ungültige Programmtransformationen, Abbildung . zeigt ein Beispiel, welches sich in Eclipse einfach nachstellen lässt. Die ebenso von Tip et al. [TKB ] gegebenen Korrektheitsbeweise für dieses Werkzeug, die den Nachweis erbringen sollen, dass die refaktorisierten Programme stets typkorrekt sind, erweisen sich, wie anhand dieses Beispiels zu erkennen ist, als unzureichend. Sie ⁴⁶ Das bei Erstellung jeden Programms unvermeidliche Auftreten von Flüchtigkeitsfehlern, die sich zügig mi els einfacher Regressionstests aufdecken lassen, sei hier einmal außer Acht gelassen. . Evaluation 1234 1235 1236 1237 1238 class A { public void m(A a) { a.m(a); } } 1239 1240 1241 1242 interface I { void m(A a); } ⇒ class A implements J { public void m(A a) { a.m(a); } } interface I { void m( A a); } 1243 interface J { } 1244 1245 1246 class B extends A implements I {} class B: extends A implements I {} (a) (b) Abbildung . .: Das von Tip et al. [TKB ] implementierte E I verkennt Methodenüberschreibungen geerbter Interfacemethoden durch Superklassen. In der Folge erlaubt es für das Programm (a) das Einführen und Verwenden des Interfaces J innerhalb der Methodensignatur von m in I. Die dadurch verloren gegangene Methodenüberschreibung resultiert im Ausgabeprogramm (b) in einem Kompilierfehler für die Klasse B. bauen darauf auf, dass alle sich aus der Sprachspezifikation ergebenden Bedingungen vollständig erfasst wurden – eine Vorausse ung, von der wegen der Komplexität und dem geringen Formalisierungsgrad der Java-Sprachspezifikation nicht ausgegangen werden darf. 8.3. Tests Neben den oben genannten Problemen formaler Beweise ist gerade bei jüngeren Arbeiten zu Refaktorisierungswerkzeugen (und auch Programmierwerkzeugen allgemein) zu beobachten, dass sta formaler Beweise insbesondere Tests zur Evaluation herangezogen werden (dazu auch Abschni . ). So ist allein das Testen von Refaktorisierungswerkzeugen selbst Thema einiger jüngerer Arbeiten [DDGM , TS , EH , SMG , SGM , GBO+ ] und es kann auf eine Vielzahl von Publikationen geblickt werden, die zur Evaluation eines Programmierwerkzeugs primär auf die Möglichkeiten des Testens se en [Ste , BFS , KS , ST , Ikk , FMM+ , RH , SKP , SSA+ , STST , RHS ]. Auch diese Arbeit baut ihre Evaluation auf Tests auf, wobei unterschiedliche Ansä e verfolgt wurden. Einerseits wurden Regressionstests durchgeführt, die als Eingabe jeweils kleine, speziell hierfür handgeschriebene Eingabeprogramme ha en. Sie liefern insbesondere Antworten auf die ersten beiden Forschungsfragen. Andererseits erfolgten Tests, bei denen die Eingabe aus realen Open-Source-Programmen besteht, auf denen die implementierten Refaktorisierungswerkzeuge systematisch angewendet wurden. Sie liefern Antworten auf die erste, dri e, vierte und fünfte Forschungsfrage. . . Tests Regressionstests Parallel zur Implementierung der in Kapitel vorgestellten Refaktorisierungswerkzeuge erfolgte auch der Au au einer Suite von Testfällen. Jeder dieser Testfälle führt genau eine Refaktorisierung durch, indem zunächst ein neues Projekt mit im Testfall vorgegebenen Quellcodedateien erstellt, hierauf eine Refaktorisierung angestoßen und das Ergebnis der Refaktorisierung mit einem Erwartungswert vergleichen wird. Soweit ein gelöstes Constraintsystem erwartet wird, werden ebenso die durchgeführten Quelltex ransformationen mit im Testfall vorgegebenen Erwartungswerten verglichen. Die Eingabeprogramme bestehen jeweils aus typischerweise nur zwei bis vier Deklarationen, gegebenenfalls mit darin enthaltenen Referenzen auf diese. Die Tests wurden jeweils so formuliert, dass nach Möglichkeit Rückschlüsse auf die Korrektheit einzelner Constraintregeln zu erlangen waren, die dann im jeweiligen Testfall entscheidenden Einfluss auf das Ergebnis haben sollten. Da aber typischerweise mehrere Constraintregeln gemeinsam Einfluss auf die Constraintmenge und damit Constraintlösung haben, waren viele Regeln auch nur kombiniert mit anderen Regeln zu testen. Insgesamt wurden Testfälle zusammengetragen. Von diesen wurden mit leichten Anpassungen aufgrund unterschiedlicher Schni stellen Testfälle aus der ebenso vom Autor stammenden Implementierung zu [ST ] übernommen. Weitere Testfälle konnten aus der eigenen Implementierung zu [STST ] übernommen werden. Mit insgesamt Testfällen stammt ein Großteil der genu ten Testfälle aus der Arbeit von Spengler [Spe ] und testet insbesondere Namensregeln. Die übrigen Testfälle waren bisher unveröffentlicht. In der Hauptsache beschränken sich die vorliegenden Tests auf C A -, R und G /S D T -Refaktorisierungen, da sich mit diesen wegen der jeweils geringeren Anzahl erlaubter Änderungen einzelne Constraintregeln gezielter testen lassen. Nur neun beziehungsweise acht Tests wenden ein M C U beziehungsweise P U an. Die Probanden Insbesondere die Fragen , und zielen auf im Alltag eines Softwareentwicklers zu erwartende Ergebnisse ab, weswegen sich eine Evaluation au auend auf Transformationen realer Softwareprojekte unterschiedlicher Größe (den Probanden) anbietet. Tabelle . listet die zur Anwendung gekommenen und frei verfügbaren Projekte mit einigen ihrer Kenndaten auf. Die Spalte JDK gibt Aufschluss über die von den Projekten benötigte JavaVersion. Alle drei . -Projekte machen von den in dieser Version neu hinzugekommenen generischen Typen Gebrauch. Die Spalten Pakete, Typen und Member geben die Anzahl der Pakete, Topleveltypen und Member der einzelnen Projekte an, die nach der Faktengenerierung innerhalb der Menge P enthalten sind. Neben solchen Programmelementen, die im Quellcode vorliegen, sind hier auch Elemente aus den durch die jeweiligen Probanden referenzierten Bibliotheken (jar-Dateien) enthalten, soweit sie zur Erstellung der Faktenbasis notwendig sind. Insbesondere sind alle solche Typen (und deren nicht-private Member) aus Bibliotheken in der Faktenbasis enthalten, die Supertypen eines im Programmcode vorliegenden Typen sind. Da die Mengen der Typen und Member nur wenig Rückschlüsse auf die tatsächliche Menge und Komplexität der Anweisungen eines Programms erlaubt, ist zusä lich noch die Anzahl der Ausdrücke (expressions) im Code angegeben, wobei auch bei ineinander geschachtelten Ausdrücken jeder innere Ausdruck gezählt wurde. Weiterhin gibt Tabelle . Auskunft über die Gesam ahl der Programmelemente p ∈ P (P.-Elem) und Fakten f ∈ F , wie sie sich bei . Evaluation der Faktengenerierung ergibt. Eine Schwachstelle des RTT-Testens ist, dass es nur Aussagen über Refaktorisierungen innerhalb der jeweilig genu ten Probanden erlaubt. Gleichzeitig ist aber denkbar, dass ein bestimmtes Muster oder Programmkonstrukt einer Programmiersprache durch die Entwickler der Probanden nicht oder nur selten genu t wurde. Die einzige Möglichkeit, diesem Problem gegenzusteuern, besteht in der Wahl einer möglichst breiten Basis von Probanden. So wurde bei den in Tabelle . genannten Probanden Wert auf eine gewisse Diversität gelegt. Viele Probanden stammen aus unterschiedlichen Entwicklerkreisen. JUnit ist als Proband zwar doppelt vorhanden, allerdings ergibt sich hier eine deutliche Unterscheidung aus dem Versionssprung von Java . zu . , in welchem sich allein durch die Einführung von Annotationstypen, Enums und generischer Typen eine deutliche Änderung des Codes und der verwendeten Programmiermuster ergeben hat. Tabelle . .: Die für die Evaluation der Constraintregeln aus Kapitel zum Einsa gekommenen Testprojekte. Projekt Joda Convert . Jester . b org.apache.commons.codec . JUnit . . org.apache.commons.io . Junit . org.htmlparser . Jakarta Commons Collections . JDK . . . . . . . . Pakete Typen Member . . . Ausdrücke . . . . . . . P.-Elem. . . . . . . . . Fakten . . . . . . . . Bevor die Testprojekte Verwendung fanden, wurden sie in zweierlei Hinsicht modifiziert, um den in Abschni . beschriebenen Einschränkungen gerecht zu werden. Einerseits wurden statische Aufrufe auf Instanzvariablen erse t, indem für jeden solchen Aufruf der jeweilige Empfänger durch den vom Compiler inferierten Typen erse t wurde. Andererseits wurden variable Parameterlisten durch Array-Typen erse t. Beide Arten der Änderung sind bedeutungserhaltend und entsprechen genau den Schri en, die durch den Java-Compiler bei Erzeugung des Bytecodes ohnehin vorgenommen werden und wie sie in einer weiterentwickelten Version der Werkzeuge durch ein entsprechendes Pre- und Postprocessing bei Faktengenerierung beziehungsweise vor dem Zurückschreiben automatisch angewendet und rückgängig gemacht werden könnten (dazu ebenso Abschni . ). Eine Laufzeitanalyse zum Beispiel anhand der Ausführung von JUnit-Tests für die in Tabelle . genannten Probanden fand nicht sta . Entsprechend liefern alle für diese Probanden erhobenen Daten keine Aussage über die in Kapitel beschriebenen Constraintregeln für Zugriffe auf die Reflection-API. Sta dessen wird eine Evaluation dieser Regeln auf einer separaten Probandenmenge in Abschni . beschrieben. Die separate Evaluation beider Regelmengen bringt dabei durchaus Vorteile. So kann sich eine fehlende Reflection-Regel nicht durch einen Kompilierfehler nach der Refaktorisierung äußern, denn diese Regeln sollen allein das Programmverhalten bedingen. Allerdings ist es dennoch denkbar, dass eine (korrekte oder unkorrekte) Reflection-Regel genau eine solche Refaktorisierung zufälligerweise verhindert. Somit ist bei einem separaten Testen beider Regelsä e insgesamt von einer besseren Testab- . . Refactoring Tool Testing der implementierten Werkzeuge deckung auszugehen. 8.4. Refactoring Tool Testing der implementierten Werkzeuge Einen wesentlichen Teil der Evaluation macht das RTT-Testen (siehe Abschni . ) der implementierten Refaktorisierungswerkzeuge aus. In den durchgeführten Testläufen kamen dabei vier verschiedene Orakel zum Einsa . Ein erstes Orakel prüft auf ungültige Programmzustände, insbesondere aufgetretene und nicht abgefangene Ausnahmen. Soweit für eine Werkzeuganwendung eine Constraintlösung gefunden werden kann – also eine Programmtransformation angeboten wird – prüft ein zweites Orakel, ob diese Programmtransformation tatsächlich eine ist, sich also das Ausgabeprogramm vom Eingabeprogramm unterscheidet. Ist dem so, prüft ein dri es Orakel, ob dieses Programm kompiliert. Ein viertes Orakel versucht das erhaltene Ausgabeprogramm auf Bedeutungserhaltung zu testen, indem es die den OpenSource-Programmen beiliegenden Testsuiten ausführt. Wegen der sich durch das vierte Orakel ergebenden sehr hohen Laufzeiten wurde dieses nur stichprobenhaft für einzelne Projekte in Betrieb genommen. Refaktorisierungs-Adapter Das Testen der vorliegenden fünf Refaktorisierungswerkzeuge machte die Implementierung fünf verschiedener Adapter notwendig, die im Folgenden beschrieben sind. • M C U wurde auf jede Überse ungseinheit der Probanden angewendet, wobei einmal in ein (gegebenenfalls neu erzeugtes) leeres Paket und einmal in ein zufällig⁴⁷ ausgewähltes, vorhandenes und nicht-leeres Paket verschoben wurde, soweit der Proband mehr als ein nicht-leeres Paket aufwies. Da das implementierte M C U -Werkzeug die Option bietet, wahlweise Zugrei arkeiten von Typen und Membern mit anzupassen, ergaben sich somit pro Überse ungseinheit bis zu vier Anwendungen. • P U M wurde für jede Methode und jedes Feld ausgeführt, deren deklarierender Typ eine im Quellcode vorliegende Superklasse hat. Durch die zwei miteinander kombinierbaren Optionen, Zugrei arkeiten anzupassen und weitere Deklarationen mit zu verschieben, ergaben sich für jede solche Deklaration stets vier Anwendungsstellen. • R wurde auf jede Typ-, Methoden- und Felddeklaration einmal angewendet, wobei jeweils ein frischer, im Programm zuvor nicht gebrauchter Name gewählt wurde. • G /S D T wurde auf jedes Feld und jede Methode im Programm angewendet. Als neu zu wählender Typ der jeweiligen Deklaration wurde zufällig aus den Sub- und Supertypen des ursprünglichen Typen der Deklaration gewählt. Insgesamt wurden aus dieser Typhierarchie vier Typen pro Anwendungsstelle gewählt. Stand dabei der Typ java.lang.Object mit zur Auswahl (indem er nicht schon ursprünglicher Typ gewesen ist), wurde sta dessen dieser zuzüglich drei weiterer zufälliger Typen ⁴⁷ Um die Tests tro des Zufalls dennoch reproduzierbar zu gestalten, wurde bei allen Experimenten auf Pseudozufall mit festem Startwert (engl. seed) zurückgegriffen. . Evaluation ausgewählt. Da in Java jeder Typ von java.lang.Object erbt, schien ein Favorisieren dieses Typen sinnvoll, um an möglichst vielen Anwendungsstellen erwünschtes oder unterwünschtes Subtyping zu provozieren. Durch die zwei miteinander kombinierbaren Optionen, wahlweise Zugrei arkeiten von Typen und deklarierte Typen weiterer Programmelemente mit anzupassen, ergaben sich maximal sechzehn Anwendungsstellen pro Deklaration, je nach verfügbaren Sub- und Supertypen aber häufig weniger. • C A wurde auf jede Typdeklaration (mit Ausnahme anonymer und lokaler Klassen), Methodendeklaration sowie Felddeklaration angewendet. Dabei ergaben sich für jede Deklaration insgesamt drei Anwendungsstellen, indem der (gegebenenfalls auch nur implizit) vorhandene Zugrei arkeitsmodifizierer durch jeden der drei weiteren in Java verfügbaren Zugrei arkeitsmodifizierer erse t wird. Für Methodendeklarationen ergaben sich darüber hinaus noch drei weitere Anwendungsstellen, da hier zusä lich die Option geboten wird, weitere Methodendeklarationen in ihrer Zugrei arkeit mit anzupassen. Um die Messergebnisse nicht zu verfälschen – insbesondere was Laufzeiten angeht – nutzen sämtliche Refaktorisierungs-Adapter die in Abschni . . beschriebenen Routinen, um solche Refaktorisierungen auszufiltern, die wegen nicht durch Constraints geprüften Bedingungen scheitern, sondern schon zuvor abgebrochen wurden. Somit sind in den sich aus dem Refactoring Tool Testing ergebenden Messungen nur noch solche Anwendungen enthalten, bei denen tatsächlich Constraints generiert und zu lösen versucht wurden. Ergebnisse des Refactoring Tool Testing Tabelle . schlüsselt die Menge der sich ergebenden Anwendungsstellen und erfolgreich durchführbarer Refaktorisierungen nach Probanden auf. Die Spalte total zeigt die Menge der von den oben beschriebenen RefaktorisierungsAdaptern gefundenen Anwendungsstellen für das jeweilige Werkzeug. Die Spalte erfolgreich zeigt sowohl in absoluter Zahl als auch mi els Prozentsa an, wie häufig im jeweiligen Fall eine Constraintlösung gefunden werden konnte, also wie häufig die Refaktorisierung durchgeführt werden konnte und Änderungen in den Code geschrieben wurden. Insgesamt wurden . Refaktorisierungen angestoßen (Summe total), von denen . (Summe erfolgreich), also rund , % durchführbar waren und für welche eine Programmtransformation des Quellcode durchgeführt wurde. Die ersten beiden Orakel vermochten keine Fehler zu erkennen: Bei keiner der Anwendungen eines Refaktorisierungswerkzeugs kam es zu einer nicht-abgefangenen Ausnahme, die an das Eclipse-Framework weitergereicht werden musste; weiterhin wurde auch für jede Anwendung festgestellt, dass mindestens eine Änderung im Quellcode vorgenommen wurde. Das Compiler-Orakel hingegen vermochte Fehler zu erkennen, insgesamt traten Kompilierfehler auf. Obwohl dies insgesamt nicht einmal , % aller Anwendungsfälle ausmacht, wurden die entsprechenden Fehler dennoch von Hand analysiert. In der Hauptsache betroffen war das C T -Werkzeug, angewendet auf die Projekte Junit . ( Fehler) und Jakarta Commons Collections . ( Fehler). Es zeigte sich, dass nicht die Constraintregeln für die Fehler zu verantworten sind, sondern mangelnde oder fehlerhafte Vorabprüfungen, insbesondere, was den Umgang mit generischen Typen anbelangt. Abschni . hat bereits ausgeführt, dass innerhalb dieser Arbeit nur bedingt Unterstü ung für generische Typen Move C. Unit total erfolgreich , % , % , % , % , % , % , % , % . . , % Pull Up Member total erfolgreich , % , % , % , % , % . , % . . , % . , % . . , % . . . . total Rename erfolgreich , % , % , % , % , % . , % . , % . , % . , % Gen./Spec. Decl. Type total erfolgreich , % , % , % . , % . , % . , % . , % . . , % . . , % Projekt Erlaubte Änderungen Joda Convert . Jester . b org.apache.commons.codec . JUnit . . org.apache.commons.io . Junit . org.htmlparser . Jakarta Commons Collections . total Move C. Unit NOC CA NOC Pull Up Member CA CL CACL Gen./Spec. Decl. Type NOC CA CT CACT Change Acc. total erfolgreich , % . , % . . , % . . , % . . , % . . , % . . , % . . , % . . , % Change Meth. Acc. NOC CA Tabelle . .: Erfolgreiche Refaktorisierungen aufgeschlüsselt nach erlaubten Änderungen. Projekt Joda Convert . Jester . b org.apache.commons.codec . JUnit . . org.apache.commons.io . Junit . org.htmlparser . Jakarta Commons Collections . total Tabelle . .: Gesamtüberblick über alle durchgeführten Refaktorisierungen. . . Refactoring Tool Testing der implementierten Werkzeuge . Evaluation geboten wird. So werden beispielsweise keine Wandlungen deklarierter Typen von generischen Typen hin zu nicht-generischen Typen und umgekehrt unterstü t. Entsprechend müssen solche Refaktorisierungsgesuche im Vorhinein abgelehnt werden (siehe Abschni . . ), dennoch waren hier die Vorabprüfungen teils zu schwach implementiert. Beispielsweise wurde für Jakarta Commons Collections . die Änderung des Rückgabetypen Map.Entry<K, V> einer Methode hin zu Object nicht unterbunden. Ein Blick in die Implementierung zeigte, dass in der Vorabprüfung Membertypen nicht geeignet berücksichtigt wurden. Ebenso zu schwache Prüfungen erfolgten bei Methoden, die in ihrer Methodensignatur Typvariablen aus überschriebenen Methoden durch konkrete Typen erse ten. Auch dieser Fall wird nicht unterstü t (siehe Abschni . ), es fand aber dennoch keine ausreichende Vorabprüfung sta . Weitere Kompilierfehler gingen auf die R -Refaktorisierung zurück, wobei sich die Kompilierfehler erneut allein auf die beiden Projekte Jakarta Commons Collections . ( Fehler) und Junit . ( Fehler) verteilten. Sechs weitere Fehler zeigten sich beim P U auf dem Junit . -Projekt. Hier zeigte sich die im Nachgang sta findende und nur prototypisch implementierte Namensqualifizierung für die Fehler verantwortlich. Teils kam es zu semantischen Fehlern, wenn beispielsweise versucht wurde, eine Typvariable zu qualifizieren. Ebenso wurden aber auch Qualifizierungen teils nicht passend aktualisiert, was insbesondere beim Umgang mit statischen und on-demand-Importen ( § . ) zu beobachten war. Erste händische Tests mit den jüngst durch Nicolai [Nic ] entwickelten, umfangreicheren Komponenten zur Namensqualifizierung zeigten, dass dort bereits ein Großteil der in dieser Arbeit gefundenen Fehler behoben werden konnte. Das vierte Orakel, welches nach jeder Werkzeuganwendung die für den Probanden vorhandenen Testsuiten ausführt, wurde ausschließlich für den JUnit . . -Probanden aktiviert, was in etwa % der Anwendungsfälle entspricht. Es lag die Vermutung nahe, dass insbesondere beim JUnit-Projekt die zuverlässigsten Tests vorhanden waren. Hier konnten zwar in sehr geringem Umfang einzelne Fehler aufgrund von fehlschlagenden Testfällen gefunden werden, eine händische Inspektion dieser ergab aber, dass die Ursache sich aus der Verwendung der Reflection-API durch JUnit und seine Testfälle ergab. Es handelte sich um Fehler, die durch die in Kapitel vorgestellten Regeln hä en verhindert werden können, aber wegen der in Abschni . vorgebrachten Gründe nicht Teil dieses Testlaufs waren. Wegen der unterschiedlichen Struktur der einzelnen Probanden ergibt sich bei den Erfolgsquoten der Werkzeuge eine gewisse Varianz. Auffällig in Tabelle . ist zum Beispiel das R -Werkzeug, bei dem für sechs Projekte eine Erfolgsquote von über % zu erreichen war, hingegen beim org.apache.commons.io . -Projekt die Erfolgsquote unter % und bei Jakarta Commons Collections . gar unter % lag. Ursache hierfür ist, dass beide Projekte in großem Umfang von Klassen der Java-Standardbibliothek ableiten und deren Methoden überschreiben, was ein Umbenennen dieser Methoden unmöglich macht.⁴⁸ Beim org.apache.commons.io . -Projekt sind dies im Wesentlichen Stream-Klassen aus dem java.io-Paket, bei den Jakarta Commons Collections . die Collection-Interfaces aus dem java.util-Paket, die neu implementiert werden. ⁴⁸ Sichergestellt wird dies durch die Regel OverridingMethodNames ( S. Eigenschaft der Programmelemente aus Bibliotheken. ) in Verbindung mit der read-only- . . Refactoring Tool Testing der implementierten Werkzeuge Beantwortung von Frage 1 Die erhaltenen Daten legen den Schluss nahe, dass die Constraintregeln korrekt arbeiten. Es sind keine ungültigen Programmzustände aufgetreten und die wenigen, nicht kompilierenden Ausgaben der Werkzeuge lassen sich auf Fehler in Komponenten zurückführen, die nicht Teil der Constraintgenerierung oder -lösung sind. Auch konnten durch das Testsuite-Orakel keine Anwendungsfälle gefunden werden, die auf Fehler in den in Kapitel entwickelten Constraintregeln schließen lassen. Fraglich ist, ob die Probanden und die durch den Refactoring Tool Tester darauf gefundenen Anwendungsstellen für die Refaktorisierungen repräsentativ für die im Alltag der Softwareentwicklung anfallenden Refaktorisierungsarbeiten sind. So handelt es sich bei den Probanden um Projekte in gereiftem Stadium. In der Praxis mag es aber insbesondere vermehrt zu Refaktorisierungen kommen, während das zu refaktorisierende Programm noch in einem unfertigeren Zustand ist. So ist es denkbar, dass während der Softwareentwicklung ein G D T eine größere Rolle spielt, als es derzeit das Refactoring Tool Testing abbilden kann, wenn typischerweise zum Abschluss von Programmiertätigkeiten durch den Entwickler noch einmal viele Typen generalisiert werden würden. Diese Anwendungen wären dann nämlich schon getätigt und könnten nicht erneut durch den RTT reproduziert werden. Weiterhin kann nicht zugesichert werden, dass die von den Refaktorisierungs-Adaptern gelieferten, weiteren Parameter der Refaktorisierung dem entsprechen, was im Alltag der Softwareentwicklung typischerweise der Fall ist. Bei M C U kann noch davon ausgegangen werden, dass bei den geringen Mengen von Paketen in den Probanden und durch die zufällige Wahl eines solchen auch hinreichend viele praxisnahe Fälle abgedeckt werden. Kritisch hingegen ist die Wahl eines neuen Namens beim R . Für die Wahl dessen gibt es nahezu beliebig viele Möglichkeiten – möchte man nur gezielt auf Namenskonflikte hin testen, verbleiben immer noch sehr viele zu testende Fälle. Wegen der Wahl bisher ungenu ter Namen in oben beschriebenem Refaktorisierungs-Adapter kam es innerhalb der RTTTests höchstens beim Testen der übrigen Werkzeuge in Ausnahmefällen⁴⁹ zu Namenskonflikten. Eine mögliche Alternative wäre gewesen, den Adapter fürs R sta dessen derart zu implementieren, einen zufällig gewählten und bereits im Programm genu ten Namen zu verwenden. Tatsächlich wurde auch mit einem solche Adapter getestet, doch die Anzahl der zusä lich abgelehnten Refaktorisierungen war verschwindend. Mit derart wenigen Einzelfällen kann nicht behauptet werden, die Behandlung von Namenskonflikten ausreichend getestet zu haben. Eine mögliche Abhilfe wäre, innerhalb des Refaktorisierungs-Adapters eine tiefer gehende Programmanalyse durchzuführen, um Namenskonflikte gezielter herbeizuführen. Doch je ausgefeilter ein solcher Adapter wäre, nach um so spezielleren Mustern würde er in den Probanden suchen müssen, bis schließlich die Frage gestellt müsste, wie hoch die Trefferquote dieser Spezialfälle in den Probanden überhaupt noch wäre. Die Alternative hierzu sind klassische Regressionstests, die jeden derart überlegten Spezialfall testen, ohne dass dieser zunächst in einem Probanden gesucht und gefunden werden muss. Da sich insbesondere beim R im RTT-Testen wegen der Vielzahl verfügbarer Namen keine nennenswerte Abdeckung aller theoretisch möglichen Werkzeuganwendungen erreichen lässt, lag der Schwerpunkt der oben bereits beschriebenen Suite von Testfällen eben bei diesem Werkzeug. ⁴⁹ zum Beispiel dem P U eines Feldes in eine Superklasse, die ein gleichnamiges Feld deklariert . Evaluation 8.5. Übermäßig starke Einschränkungen Die Beantwortung der zweiten Forschungsfrage gestaltet sich schwierig, erfordert sie doch eigentlich händischen Einblick in jedes nicht erfüllte Refaktorisierungsproblem, ob nicht doch eine Möglichkeit bestanden hä e, die Refaktorisierung durchzuführen. Eine solche manuelle Prüfung verbietet sich aber alleine schon wegen der schieren Menge durchgeführter Anwendungen. Klassische Werkzeuge, wie sie Opdyke [Opd ] beschreibt, die in weitgehend unabhängigen Schri en zunächst Vorbedingungen prüfen und erst anschließend Änderungen berechnen, ließen sich hingehend Frage testen, indem auch bei nicht erfüllten Vorbedingungen tro dem die Änderungen berechnet und in den Code geschrieben werden. Schlägt das Kompilieren für das so berechnete Programm fehl, ist dies ein Indiz für korrekt berechnete Vorbedingungen. Ist das Ausgabeprogramm kompilierbar, muss dennoch manuell inspiziert werden, ob sich das Programmverhalten geändert hat. Es ist aber zu erwarten, dass hier nur noch ein Bruchteil aller Fälle geprüft werden muss, da man davon ausgehen kann, dass ein Großteil der Problemfälle bereits durch den Compiler erkannt wird. Für constraintbasierte Werkzeuge ist dieser Ansa hingegen nicht durchführbar, da die Prüfung von Vorbedingungen und das Berechnen der Änderungen in Form der Constraintlösung unmi elbar miteinander verwoben sind. Soweit die Constraints nicht lösbar sind, gibt es auch keine Angabe über einen Sa nötiger Änderungen. Soares et al. [SMG ] schlagen vor, zu restriktive Werkzeuganwendungen zu erkennen, indem unterschiedliche Werkzeuge mit identischen Parameterbelegungen auf demselben Programm angewendet und die Ergebnisse verglichen werden. Dazu ist allerdings ein weiteres Werkzeug mit ähnlichem Umfang nötig. Mit den Refaktorisierungswerkzeugen der JDT [JDT ] stehen zwar Werkzeuge für die gleiche Pla form zur Verfügung, allerdings unterscheiden sich die Ein- und Ausgabeparameter. So bietet M C U in Eclipse ebenso wie C D T kein Anpassen von Zugrei arkeiten und P U M keine Option, zusä lich nötige Verschiebungen in Folge direkt mi uberechnen.⁵⁰ Bei der Ausgabe hingegen unterscheidet Eclipse nicht nur zwischen gültiger und ungültiger Transformation, sondern nu t zusä lich auch noch die Option, Warnungen anzuzeigen. Diese Warnungen weisen auf nur möglicherweise geändertes Programmverhalten hin und erschweren so einen automatischen Vergleich der Werkzeuge. Um unnötig restriktiven Werkzeuganwendungen auf die Spur zu kommen, kann man sich bei constraintbasierten Werkzeugen aber eines anderen Testverfahrens bedienen. So resultieren bei constraintbasierten Werkzeugen übermäßig starke Einschränkungen aus zu restriktiven Constraintregeln. Kreis [Kre ] liefert hierzu ein Werkzeug, um solche Regeln aufzuspüren: Ansta wie in Abschni . beschrieben, den minimalen Sa von Constraints für ein gegebenes Transformationsproblem zu berechnen, wurde einmalig für jedes in Tabelle . genannte Projekt pauschal jede Regel auf jede mögliche Anwendungsstelle angewendet. Erse t man anschließend in jedem Constraint die Variablen durch ihre initialen Werte, müsste ein jedes solchermaßen erzeugtes Constraint gelöst sein. Anderenfalls wäre die zugrundeliegende Constraintregel zu restriktiv, denn das Ausgangsprogramm ist wohlgeformt (also kompilierbar) und trivialer Weise in seinem Programmverhalten unverändert. ⁵⁰ Sta dessen gibt es hier ein vorgelagertes Werkzeug, das eine eigene Analyse durchführt. . . Übermäßig starke Einschränkungen 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 generiere Faktenbasis (P, V, init, F ) setze alle Programmelemente p ∈ P auf read−only für alle Variablen v ∈ V für alle Constraintregeln r ∈ R erzeuge alle Constraints C aus r, die v bedingen können für alle c ∈ C probiere c konstant zu lösen wenn c = FALSE Fehlermeldung wenn c nicht konstant lösbar Fehlermeldung Abbildung . .: Algorithmus zum systematischen Generieren aller Constraints. Tabelle . .: Anzahlen erfüllter und nicht erfüllter Constraints bei vollständiger Generierung gemäß dem Algorithmus aus Abbildung . . Projekt Joda Convert . Jester . b org.apache.commons.codec . JUnit . . org.apache.commons.io . Junit . org.htmlparser . Jakarta Commons Collections . . . . . . . . . . . . total . . . . . . . . nicht erfüllt . . . nicht erfüllt in % , , , , , , , , Um nicht nur die Constraintregeln zu testen, sondern insbesondere auch die Komponenten des Refacola-Frameworks, welche aus diesen Regeln die Constraints generieren, wurde der in Abbildung . gezeigte Algorithmus verwendet. Der Vorteil dieses Verfahrens ist, dass die Schri e aus den Zeilen und beide dieselben sind, die auch während der Erzeugung eines minimierten Constraintsystems mi els Breitensuche auf dem Constraintgraphen und konstanter Auswertung durch die Refacola durchgeführt werden (siehe dazu Abschni . ). Als Nachteil kann empfunden werden, dass eine große Menge Constraints mehrfach erzeugt wird, nämlich für jede eingehende Kante im Constraintgraphen einmal. Umgekehrt wird damit erreicht, dass jede theoretisch denkbare Möglichkeit, das entsprechende Constraint zu generieren, mindestens einmal getestet wird. Tabelle . zeigt die Ergebnisse bei Ausführung des Algorithmus aus Abbildung . auf den Probanden. Die Spalte total nennt die Gesamtanzahl der generierten Constraints. Die Spalten nicht erfüllt und nicht erfüllt in % geben zu jedem Projekt die Anzahl der nicht erfüllten Constraints an. Obwohl der Fehleranteil mit ca. , % (org.htmlparser . ) bis ca. , % (Joda Convert . ) verschwindend gering ist, wurden auch hier stichprobenhaft und von Hand die Fehlerquellen untersucht. Als Ergebnis konnten mehrere Implementierungsfehler innerhalb der Faktengenerierung (siehe Abschni . ) gefunden werden. So wurde offenbar, dass für Member der Wrapper-Klassen von Primitivtypen (Integer, Boolean, ...) als initialer Werte für owner und tlowner gleiche, aber nicht identische Programmelemente verwendet wurden. In der Folge . Evaluation schlagen mehrfach für deren Member Constraints nach der Regel MemberOrConstructorTlOwnerAndHostPackage (siehe S. , ebenso S. ) fehl. Zum Beispiel ist dann nämlich das Constraint 1258 java.lang.Integer.intValue().owner.tlowner = java.lang.Integer.intValue().tlowner nicht mehr erfüllt. Auf tatsächlich ausgeführte Refaktorisierungen wird sich dieser Fehler allerdings nicht auswirken können. Da die Wrapperklassen der Primitivtypen Bibliothekscode sind und daher alle ihre Variablen schon bei der Faktengenerierung als unveränderlich markiert werden, wird das oben genannte Constraint so regulär nach den in Abschni . beschriebenen Optimierungen niemals generiert werden, schließlich sind sowohl seine linke als auch seine rechte Seite unveränderlich. Auch bei weiteren anhand der Tests gefundenen Fehlern, die auf Implementierungsfehler bei der Faktengenerierung hindeuten, konnte davon ausgegangen werden, dass diese maximal in sehr speziellen Einzelfällen Einfluss auf das Ergebnis der Refaktorisierung haben. Gemeinsam mit der im Vergleich zu den Zahlen der insgesamt generierten Constraints sehr geringen Anzahl der fehlschlagenden Constraints darf davon ausgegangen werden, dass diese die erstellten Refaktorisierungswerkzeuge nicht übermäßig stark einschränken. Neben zu restriktiven Constraintregeln gibt es eine weitere mögliche Quelle übermäßig starker Einschränkungen. Eine davon bildet das Fehlen geeigneter Variablen, um nötige Folgeänderungen zu erlauben. Auch ist denkbar, dass geeignete Variablen zwar vorhanden, aber nicht in den erlaubten Änderungen entsprechender Werkzeuge enthalten sind. Für ein R wird derzeit zum Beispiel die Umbenennung eines Feldes abgelehnt, wenn ein weiteres Feld mit dem gewünschten Namen bereits in derselben Klasse vorhanden ist. Die Alternative wäre, beide Felder umzubenennen. Da die Namen weiterer Felder aber nicht in den erlaubten Änderungen des implementierten R -Werkzeugs enthalten sind, kommt es nicht zu einer solchen Constraintlösung. Es ist schwer zu beurteilen und entspricht le tlich auch dem Geschmack des Anwenders, welche Änderungen erlaubt sein sollen, also ob die Menge der jeweils erlaubten Änderungen für die angebotenen Werkzeuge ausreicht. Der folgende Abschni . wird näher auf die Auswirkungen erlaubter Änderungen eingehen. Nach den ermi elten Testergebnissen für das vollständige Generieren aller Constraints lässt sich aber zumindest vermuten, dass innerhalb der Constraintregeln keine unnötig scharfen Bedingungen zu finden sind. 8.6. Auswirkungen erlaubter Änderungen Ein Vorteil constraintbasierter Programmtransformationen ist, dass durch die deklarative Formulierung der einzuhaltenden Bedingungen während der Programmtransformation Folgeänderungen sehr einfach Berücksichtigung finden können. Dies spiegelt sich in den verschiedenen von den implementierten Werkzeugen angebotenen Modi wider, die neben der eigentlichen Intention noch unterschiedliche Arten von Folgeänderungen zulassen. Tabelle . schlüsselt die in Tabelle . in der Spalte erfolgreich gezeigten Werkzeuganwendungen noch einmal nach den jeweils gewählten Modi der Werkzeuge auf. So entsprechen zum Beispiel die sich aus Tabelle . ergebenden vier beziehungsweise fünf Anwendungen von M C U im NOC- beziehungsweise CA-Modus auf Joda Convert . genau den neun in Tabelle . genannten erfolgreichen M C U -Anwendungen . . Auswirkungen erlaubter Änderungen für diesen Probanden. Da für das R -Werkzeug nur ein einziger Modus implementiert wurde, ist dieser nicht noch einmal in Tabelle . gelistet. Für die C A Refaktorisierung steht wie in Abschni . . begründet ein CA-Modus nur für Methoden zur Verfügung, weswegen sich die in Tabelle . genannten Zahlen für dieses Werkzeug auch nur auf Zugrei arkeitsänderungen von Methoden beziehen. Vergleicht man die Anzahlen erfolgreicher Refaktorisierungen, variieren die Erfolgsquoten je nach Werkzeug. Für M C U macht sich beim CA-Modus, in welchem zusä lich Zugrei arkeitsmodifizierer angepasst werden dürfen, eine deutliche Steigerung von . auf . Anwendungsfälle bemerkbar, was einer Steigerung um % gegenüber der Anzahl erfolgreicher Refaktorisierungen ohne weitere erlaubte Änderungen (NOC) entspricht. Noch extremere Steigerungsraten sind bei P U M zu verzeichnen. Durch die Möglichkeit, zusä lich Zugrei arkeiten anzupassen (CA), stieg die Erfolgsquote von Anwendungen auf , was einem Anstieg um % entspricht. Die Möglichkeit, weitere Deklarationen zu verschieben (CL), sorgte für eine Steigerung auf Anwendungen beziehungsweise einen Anstieg um circa %. Waren sowohl Änderungen von Zugrei arkeiten als auch das Verschieben weiterer Deklarationen gesta et (CACL), ergab sich gar eine Steigerung auf erfolgreiche Anwendungen, was einer Steigerung um circa % entspricht. Beim G /S D T gab es ähnlich signifikante Steigerungsraten, wenn man neben der Ursprungsdeklaration auch weiteren Deklarationen erlaubte, ihren deklarierten Typen zu ändern (CT). Hier konnte eine Steigerung von auf , also um circa % gemessen werden. Beinahe nu los gestaltete sich dagegen die Option, auch Typen in ihrer Zugrei arkeit mit anpassen zu können – die wenigen Einzelfälle, in denen zusä liche Lösungen gefunden werden konnten, liegen deutlich unter einem Prozentpunkt. Eine händische Untersuchung diverser Anwendungsstellen in den Probanden offenbarte, dass ein Großteil der enthaltenen Typen ohnehin öffentlich zugrei ar und die Möglichkeit einer zusä lichen Anpassung ihrer Zugrei arkeiten somit unnötig sind. Messbar hingegen sind Erfolge bei der Durchführung des C M A . Hier steigerte sich die Erfolgsquote durch Zulassen zusä licher Zugrei arkeitsänderungen (CA) von . auf . Anwendungen, was immerhin noch circa , % entspricht. Die aus Tabelle . entnehmbaren Daten haben gezeigt, dass zusä lich erlaubte Änderungen die Anzahl möglicher Refaktorisierungen je nach Werkzeug in mäßigem bis deutlichem Umfang steigern können. Aber auch hier muss die Frage gestellt werden, ob die durchgeführten Refaktorisierungen dem entsprechen, was in der Praxis an Werkzeuganwendungen zu erwarten ist. So lässt es sich annehmen, dass mit einer ausreichenden Pale e erlaubter Änderungen beinahe jede Refaktorisierung durchführbar ist,⁵¹ somit also mit steigender Anzahl erlaubter Änderungen stets die Erfolgsquote nach oben getrieben werden kann. Umgekehrt muss damit gerechnet werden, dass mit steigender Anzahl von Änderungen die Werkzeuganwendungen immer praxisferner werden. So gibt zum Beispiel Fowler in seinem Handbuch [Fow ] vor, wie sogenannte Big Refactorings [Fow , Chapter ] in einzelne Teilrefaktorisierungen zerlegt werden können, um das Verständnis für den Prozess zu bewahren. Es lässt sich vermuten, dass tro vorhandener Werkzeugunterstü ung Programmierer nur solche Änderungen automatisiert durchführen lassen, die sie selbst noch auf einmal überblicken ⁵¹ selbst eine programmweit viel genu te Interface-Methode lässt sich auf Pake ugrei arkeit herabse en, indem das Interface zu einer abstrakten Klasse gewandelt wird und alle implementierenden und referenzierenden Klassen in dasselbe Paket verschoben werden... . Evaluation und nachvollziehen können und anderenfalls selbst eine Zerlegung in kleinere Teilrefaktorisierungen vornehmen. Um fes ustellen, wie komplex die einzelnen Programmtransformationen ausgefallen sind, wurde während der RTT-Durchläufe auf den Probanden die Anzahl der jeweiligen Quellcodeänderungen protokolliert. Abbildung . fasst sie graphisch in Form von Häufigkeitsverteilungen zusammen. Die fünf gezeigten Balkendiagramme zeigen die erfolgreichen Refaktorisierungen der fünf implementierten Werkzeuge jeweils in ihrem Modus mit der maximalen Zahl erlaubter Änderungen. Für M C U wird somit beispielsweise im Balkendiagramm oben links die entsprechende CA-Spalte aus Tabelle . noch einmal genauer aufgeschlüsselt. Jede Säule eines Balkendiagramms repräsentiert eine Menge von Refaktorisierungen mit gleicher Anzahl von Änderungen einer bestimmten Variablenart. Oberhalb der Säule ist die Menge der jeweiligen Refaktorisierungen angegeben, unterhalb die Anzahl der Änderungen. Die Art der Änderungen ergibt sich aus der Balkenfarbe. Im Falle des M C U gab es somit . Anwendungsstellen, an denen keine Zugrei arkeiten angepasst werden mussten (eine Information, die sich auch schon aus Tabelle . entnehmen ließ). In Fällen wurde ein Zugrei arkeitsmodifizierer angepasst, in Fällen waren es derer zwei und so weiter. Die in den Diagrammen gezeigten Änderungen beinhalten sowohl die Änderungen, die sich direkt aus der Intention der Werkzeuganwendung ergeben, als auch Änderungen, die im Rahmen der Constraintlösung zusä lich ermi elt wurden. So ist es zu erklären, dass im Fall des P U M sich stets der Ort mindestens einer Deklaration geändert hat. Gleiches gilt für Namen bei R und Zugrei arkeiten bei C A . Um eine sinnvolle graphische Darstellung zu erlauben, skalieren die Balken nicht linear, sondern logarithmisch zur Basis mit der Anzahl durch sie repräsentierter Werkzeuganwendungen. Um dennoch leere von einelementigen Mengen unterscheiden zu können, ist bei le teren tro fehlendem Balken stets immer noch eine „ “ mit angegeben. Wie aus den Diagrammen in Abbildung . hervorgeht, bewegten sich für die meisten Werkzeuganwendungen die Anzahl jeweils durchgeführter Änderungen pro Variablenart im Bereich bis zu einem Du end, bei G D T auch leicht darüber hinausgehend mit einigen, wenigen Ausreißern. Insgesamt handelt es sich damit um plausibel wirkende Anwendungen von Refaktorisierungen, die sich auch noch bequem durch den Werkzeugbenutzer in einem entsprechenden Vorschaufenster nachvollziehen lassen. Lediglich beim R kommt es zu einer wesentlich größeren Anzahl von Folgeänderungen. Zu beachten ist, dass hier das entsprechende Diagramm nur einen Ausschni zeigt. Tatsächlich gibt es noch weitere einzelne Ausreißer, deren obere Grenzen bei einer Menge von Änderungen erreicht ist. Dies liegt in der Natur der Sache des R , bei dem sich die Anzahl nötiger Folgeänderungen direkt aus der Anzahl der Referenzierungen ergibt. Den Spi enreiter mit Anwendungen bildet hier eine Umbenennung der Klasse (oder ebenso des Konstruktors der Klasse) CharacterReference innerhalb des org.htmlparser . -Projekts, welche zur Implementierung von Singleton-Objekten für diverse Sonderzeichen hunderte Male referenziert wurde. Aber selbst in diesem Extremfall kann davon ausgegangen werden, dass durch die sich beim R relativ einleuchtend gestaltenden Änderungen in Form von Umbenennungen die Gesamtrefaktorisierung durch den Benu er nachvollziehbar bleibt und auch tatsächlich derart ausgeführt werden würde, somit also als praxisrelevant zu deuten ist. Die dri e gestellte Forschungsfrage lässt sich somit ebenso positiv beantworten. Zwar unterscheiden sich die Häufigkeiten, in denen weitere Folgeänderungen nötig waren. Für alle . . Auswirkungen erlaubter Änderungen MOVE COMPILATION UNIT (CA) PULL UP MEMBER (CACL) 1100 Zugreifbarkeiten Zugreifbarkeiten Orte 516 420 141 65 34 26 29 24 12 6 6 8 6 4 2 2 7 3 2 67 5 1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 0 1 SPECIALIZE / GENERALIZE DECLARED TYPE (CTCA) 1162 678 Zugreifbarkeiten 2 4 4 5 6 7 8 9 10 1 11 12 Zugreifbarkeiten 8339 Typen 6 5 2 8 3 55 47 13 8 3 2 3 1 158 115 3 0 4 CHANGE METHOD ACCESSIBILITY (CA) 143 161 78 26 13 5 22 10 8 4 8 4 8 2 2 2 10 12 14 16 18 20 22 24 26 28 30 32 0 1 2 3 4 5 6 7 8 9 10 11 12 RENAME (CI) 1055 Namen 243 116 44 35 8 2 126 1 120 114 111 108 105 99 1 129 2 1 102 1 84 1 81 66 63 60 57 54 51 48 45 42 39 36 33 30 27 24 21 18 15 9 12 6 3 0 1 117 2 2 1 3 96 2 1 7 5 4 93 4 3 123 7 4 90 8 3 78 8 75 12 87 26 13 72 57 46 69 86 Abbildung . .: Häufigkeitsverteilungen durchgeführter Änderungen bei Anwendung der fünf implementierten Refaktorisierungswerkzeuge. Jeder Balken repräsentiert eine Menge von Refaktorisierungen mit gleicher Anzahl Änderungen einer Variablenart. Unterhalb der Balken ist jeweils die Anzahl dieser Änderungen angegeben. Oberhalb der Balken ist die Kardinalität der Mengen angegeben. Für die R -Refaktorisierung ist nur ein Ausschni gezeigt, hier treten bis zu einer Menge von Änderungen noch weitere einzelne Ausreißer auf. . Evaluation Werkzeuge konnten aber Fälle gefunden werden, in denen zusä liche Änderungen die Menge durchführbarer Refaktorisierungen vermehrt haben. Weiterhin lässt die Anzahl zusä licher Folgeänderungen vermuten, dass die Refaktorisierungen dem entsprechen, was ein Benutzer tatsächlich mi els Werkzeugunterstü ung für ihn nachvollziehbar an Refaktorisierungen durchführen würde. 8.7. Laufzeiten Die vierte Forschungsfrage betrifft die zu erwartenden Laufzeiten einer Werkzeuganwendung, weswegen im Rahmen der RTT-Durchläufe ebenso auch die zugehörigen Laufzeiten erfasst wurden. Alle erhobenen Zeitmessungen wurden auf einem Intel i QM Quad-Core Mobilprozessor mit , GHz Taktfrequenz durchgeführt. Der JVM standen maximal GB Speicher zur Verfügung. Die gemessenen Zeiten sind insgesamt mit Vorsicht zu betrachten. Einerseits macht es einem die Sprache Java mit ihrer Laufzeitumgebung durch nichtdeterministische Mechanismen wie Garbage Collection [BCM ] oder weitere Optimierungen [GVG ] ohnehin schwer, zuverlässig reproduzierbare Laufzeitmessungen durchzuführen [GBE ], andererseits bringt auch Eclipse mit seinen JDT [JDT ] eigene Caches und Optimierungen mit, die je nach übrigen Umständen zu unterschiedlichen Laufzeiten führen. Insbesondere bei der Faktengenerierung lässt sich beobachten, dass sich bei mehrfach wiederholter Ausführung die Laufzeiten beinahe halbieren können. Es ist zu vermuten, dass insbesondere bei Anforderung abstrakter Syntaxbäume und den in ihnen gespeicherten Bindungen sowie bei Abfragen von Deklarationen in jar-Dateien vom JDT teilweise Werte aus Caches zurückgeliefert werden. Ebenso machten sich bei wiederholter Nu ung der Rückschreibekomponente – die ebenso auf die Berechnung von Syntaxbäumen angewiesen ist – Geschwindigkeitssteigerungen bemerkbar. Dem entgegen stehen Einbußen durch die sogenannte local history, in welcher Eclipse erfolgte Quellcodeänderungen nachverfolgt und welche wiederum dazu neigt, mit steigender Zahl erfolgter Refaktorisierungen das System zu bremsen. Unglücklicherweise bietet Eclipse auch keine Möglichkeit, die local history abzuschalten. Offensichtlich wurde hier der Anwendungsfall abertausender, reihenweise hintereinander ausgeführter Refaktorisierungen ohne zwischenzeitliche Beendigung der Pla form (bei welcher die local history gesäubert wird) nicht berücksichtigt. Tro obiger Vorbehalte listet Tabelle . die erhobenen Daten auf. Die einzelnen Spalten geben die durchschni lichen Laufzeiten der verschiedenen Refaktorisierungsanwendungen aufgeschlüsselt nach den verwendeten Modi in Sekunden an. Da in der vorliegenden Implementierung die Faktengenerierung unabhängig vom jeweiligen Transformationsproblem geschieht und somit für jede Anwendung auf demselben Probanden einheitlich ist, bot es sich bei den ohnehin lang laufenden Tests an, die Faktengenerierung nur einmalig auszuführen. Die Zeiten zur Faktengenerierung sind entsprechend in Tabelle . nicht enthalten, werden aber in einem folgenden Abschni noch einmal separat behandelt. Somit beschreiben die gemessenen Zeiten für jede Anwendung das Zeitintervall ab Bereitstehen der Faktenbasis bis zu dem Moment, in dem die Rückschreibekomponente die Quellcodeänderungen in Form konkreter String-Erse ungen in den Quellcode zurückgeschrieben oder der Constraintlöser vermeldet hat, dass das Constraintsystem unlösbar war. Tabelle . zeigt jeweils die Durchschni szeiten aller Werkzeuganwendungen pro Projekt und Werkzeug. Die le te Zeile zeigt die Durch- . . Laufzeiten schni szeiten über alle Refaktorisierungen eines Werkzeugs, wobei hierbei zu beachten ist, dass die größeren Probanden mit ihren entsprechend höheren Anzahlen angestoßener Refaktorisierungen (siehe Tabelle . ) auch entsprechend stärker in die Mi elwertberechnung eingehen. So konnte ein M C U beispielsweise auf Joda Convert . nur mal angestoßen werden, auf Jakarta Commons Collections . hingegen mal. Entsprechend stärker geht hier die lange durchschni liche Laufzeit von , Sekunden für le teres Projekt gegenüber der kurzen Laufzeit von nur , Sekunden für ersteres Projekt in den Mi elwert ein. Wie aus Tabelle . zu erkennen ist, fallen starke Schwankungen nicht nur zwischen den einzelnen Werkzeugen, sondern insbesondere auch zwischen ihren unterschiedlichen Modi auf. In ihren jeweils restriktivsten Modi – also bei R CI und bei den übrigen Werkzeugen NOC – machen die meisten Werkzeuge eine gute Figur. Die Zeiten liegen jeweils unterhalb einer Sekunde, lediglich M C U benötigt im Schni Sekunden, was noch annehmbar scheint, allerdings mit Blick auf die durchschni liche Laufzeit von knapp Sekunden für das Jakarta Commons Collections . -Projekt als kaum zumutbar einzustufen ist. Auch lassen die Zahlen nicht erwarten, dass für größere Projekte die Laufzeiten linear mit der Projektgröße skalieren, da insbesondere bei den umfangreichen Projekten die Laufzeiten sprunghaft ansteigen. Ähnlich sprunghafte Anstiege der Laufzeiten sind darüber hinaus noch einmal zu vermelden, wenn man die einfachen Modi ohne weitere Änderungen mit denen vergleicht, die zusä liche Änderungen zulassen. Betreibt man Ursachenforschung für die langen Laufzeiten, fällt auf, dass die angegebenen Durchschni szeiten ein verzerrtes Bild auf die tatsächliche Situation geben. Tatsächlich ist ein Großteil der Refaktorisierungsanwendungen binnen kürzester Zeit abgeschlossen. Dem gegenüber steht aber ein geringer Anteil von Refaktorisierungen mit aberwi ig langen Laufzeiten. Betrachtet man nur einmal das mit den längsten Laufzeiten aus Tabelle . hervorgehende P U M im CACL-Modus, so war dort im Höchstfall eine Laufzeit von über einer Stunde zu messen. Dem gegenüber stehen viele Anwendungen mit sehr kurzen Laufzeiten. Abbildung . zeigt ein Histogramm der ersten Sekunden aller Anwendungen dieses Werkzeugs. Nach bereits , Sekunden waren über % und nach , Sekunden über % aller Werkzeuganwendungen abgeschlossen. Bei den übrigen Modi sind ähnliche Effekte zu beobachten. Tabelle . zeigt analog zu Tabelle . noch einmal die durchschni lichen Laufzeiten aller Werkzeuge, wobei allerdings die in den Laufzeiten am längsten dauernden % aller Werkzeuganwendungen ignoriert wurden. Somit ergibt sich ein ganz anderes Bild, nämlich eines, in dem sich die Laufzeiten im mi leren, einstelligen Sekundenbereich einpendeln, was wiederum vertretbar erscheint. Um zu ergründen, wie in den wenigen, kritisch lang andauernden Fällen die Laufzeiten zustande kommen, wurden diese Fälle näher untersucht. Als entscheidender Flaschenhals hat sich dabei die Constraintgenerierung innerhalb des Refacola-Frameworks erwiesen. Bei stichprobenhaften Untersuchungen der mehrminütig laufenden Werkzeuganwendungen hat sich gezeigt, dass beinahe die vollständige Laufzeit für die Belegung der Regelvariablen verwendet wird. In diesem Schri wird für alle Programmelemente geprüft, wie sich mit diesen die im for all-Teil der Constraintregel definierten Regelvariablen belegen lassen, sodass die Prämisse im if-Teil der Regel erfüllt ist (Schri 06 im Algorithmus aus Figure in [SKP ]) und gleichzeitig eine bestimmte Constraintvariable eines Programmelements bedingt wird. Es leuchtet ein, dass sich je nach Menge und Art der Regelvariablen und Faktenabfragen in der Prämisse unterschiedliche Laufzeiten ergeben. Bemerkenswert ist, dass der nahezu ge- . Evaluation Move C. Unit NOC CA , , , , , , , , , , , , , , , , , , NOC , , , , , , , , , Pull Up Member CA CL CACL , , , , , , , , , , , , , , , , , , , , , , , , , , , Rename CI , , , , , , , , , Gen./Spec. Decl. Type NOC CA CT CACT , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , Change Acc. NOC CA , , , , , , , , , , , , , , , , , , Tabelle . .: Durchschni liche Laufzeiten der Refaktorisierungsanwendungen aus Tabelle . in Sekunden. Gemessen wurde für jede Refaktorisierungsanwendung jeweils die Zeit ab Bereitstehen der Faktenbasis bis zur Durchführung der Quellcodeänderungen beziehungsweise der Fehlermeldung, dass das Constraintsystem nicht lösbar ist. Projekt Joda Convert . Jester . b org.apache.commons.codec . JUnit . . org.apache.commons.io . Junit . org.htmlparser . Jakarta Commons Collections . Durchschni Move C. Unit NOC CA , , , , , , , , , , , , , , , , , , NOC , , , , , , , , , Pull Up Member CA CL CACL , , , , , , , , , , , , , , , , , , , , , , , , , , , Rename CI , , , , , , , , , Gen./Spec. Decl. Type NOC CA CT CACT , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , Change Acc. NOC CA , , , , , , , , , , , , , , , , , , Tabelle . .: Durchschni liche Laufzeiten der Refaktorisierungsanwendungen aus Tabelle . in Sekunden analog zu Tabelle . , wobei die pro Werkzeug und Modus jeweils langsamsten % aller Anwendungen ignoriert wurden. Projekt Joda Convert . Jester . b org.apache.commons.codec . JUnit . . org.apache.commons.io . Junit . org.htmlparser . Jakarta Commons Collections . total . . Laufzeiten PULL UP MEMBER (CACT) JAKARTA COMMONS COLLECTIONS Häufigkeit im Laufzeitintervall Häufigkeit kumuliert in % 100% 250 90% 80% 200 Häufigkeit 70% 150 60% 50% 100 40% 30% 50 20% 10% 0 0% 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Laufzeit in s Abbildung . .: Histogramm der Laufzeiten aller Anwendungen von P U M im CACL-Modus auf Jakarta Commons Collections . . Die grünen Balken repräsentieren Mengen von Refaktorisierungsanwendungen, deren Laufzeiten im gleichen Sekundenintervall liegen. Die viole e Kurve zeigt die kumulierten Häufigkeiten als Prozentwerte. Während das Histogramm als Balken nur die Anwendungen zeigt, die innerhalb von Sekunden terminierten, beziehen sich die Prozentwerte auf alle Anwendungen. samte Berechnungsaufwand auf nur sehr wenige Regeln zurückgeht, nämlich solche, die mehrere ungebundene – also in der Prämisse nicht mit Programmabfragen bedingten – Regelvariablen haben. Solche ungebundenen Variablen lassen sich häufig nicht vermeiden. So ist es ja gerade der Zweck vieler Regeln, eine große Menge von Variablen anderer Programmelemente zu bedingen. Soll zum Beispiel eine für Methodenüberschreibung relevante Eigenschaft einer , ebenso S. ) ja Methode geändert werden, soll die Regel AccidentalOverriding (siehe S. gerade für jede weitere Methode im Programm prüfen, ob diese nun nicht versehentlich eine neue Überschreibungsbeziehung au aut. Zwar kann man hierbei geschickt weitere Annahmen über die Menge der zu prüfenden Methoden machen, das Refacola-Framework macht hiervon aber (bisher) keinen Gebrauch. So könnte bei der Belegung der Regelvariablen schon Wissen über die erlaubten Änderungen oder anderweitig unveränderlich gemachten Constraintvariablen genu t werden. Im Falle oben genannter Regel AccidentalOverriding kann beispielsweise im Falle eines R M darauf verzichtet werden, alle weiteren Methoden des Programms zu betrachten. Es genügen jeweils die in den Sub- und Superklassen. Diese Information ergibt sich allerdings erst aus der Konklusion (dem then-Teil), welche aber bei Belegung der Regelvariablen nicht berücksichtigt wird, sodass zunächst unabhängig von dieser alle möglichen Belegungen der Regelvariablen gesucht und in den Speicher geschrieben werden. Weitere Optimierungen des Refacola-Frameworks dahingehend wären für die Zukunft also erstrebenswert. Weiterhin könnte man aber auch mit geeigneten Erweiterungen der Refacola-Syntax einen Teil der all-Queries vermeiden. Hilfreich wäre hierbei ein neuer Operator, der es in Constraint- . Evaluation 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 MemberTypeAccess for all tr: Java.TypeReferenceInType referencingType: Java.TlOwnedType do if all(tr), all(referencingType) then assume tr.typeBinding instanceof Java.MemberType and (referencingType = tr.owner) and (tr.hostPackage != type.hostPackage) and Java.sub*!(referencingType.enclosingTypes, type.owner) −> type.accessibility = #public end Abbildung . .: Alternative Implementierung der Regel MemberTypeAccess, bei welcher mittels einer hypothetischen Refacola-Erweiterung um ein assume-instanceofKonstrukt die Regelvariable type eingespart werden konnte. regeln gesta et, einen Variablenwert auf seinen Typ zu befragen. So hat zum Beispiel die Regel MemberTypeAccess (siehe S. , ebenso S. ) eine Regelvariable type, die theoretisch durch eine Indirektion tr.typeBinding erse t werden könnte, was einen Verzicht der zusä lichen all-Query sowie der Bedingung (tr.typeBinding = type) erlaubte. Der Grund, warum die zusä liche Variable type existiert, ist, dass die Indirektion tr.typeBinding zum Typ Type ausgewertet wird, auf welchem die in der Constraintregel benötigten Variablenarten wie owner nicht verfügbar war. Umgekehrt kann tr.typeBinding auch nicht so typisiert werden, dass hierauf ein owner zur Verfügung steht, denn im Falle einer Referenz zu einer lokalen Klasse gäbe es keinen unmi elbar umschließenden Typen (sondern nur einen Block) und die Regel wäre nicht anzuwenden. Die Lösung wäre eine Syntax, die in der Konklusion einer Regel eine Typprüfung erlaubt und das jeweilige Constraint erfüllt sein lässt, wenn ein durch Indirektion beschriebenes Programmelement nicht vom entsprechenden Typen ist. Abbildung . zeigt eine mögliche Umse ung mi els neuer Schlüsselworte assume und instanceof. Unabhängig davon lässt sich vermuten, dass für die Faktenabfragen und Berechnung von Regelvariablenbelegungen im Refacola-Framework noch weitere Optimierungen verfügbar sind, die über das Vorhalten der Fakten in einfachen, assoziativen Datenfeldern (zum Beispiel java.util.HashMap) hinausgehen. So wäre zu überlegen, ob intern für die Faktenabfragen leistungsfähigere Komponenten zum Einsa kommen, ein erster Schri wäre ein Einsa des JTransformer-Frameworks [SRK ], welches eine Darstellung abstrakter Syntaxbäume als Prolog-Fakten ermöglicht und Abfragen aus Java-Code zum Beispiel über das leistungsfähige SWI-Prolog [WSTL ] gesta et. Dies sind aber le tendlich alles Optimierungen des RefacolaFrameworks. Der in dieser Arbeit entwickelte Sa an Constraintregeln ist von diesen Fragen unabhängig. Laufzeiten der Faktengenerierung Die RTT-Durchläufe haben in den Laufzeiten nicht den Zeitanteil berücksichtigt, der für die Faktengenerierung benötigt wird. In diesem Prozess sind . . Evaluation der Constraintregeln für reflektive Zugriffe wie in Abschni . beschrieben aus den Quellcodedateien abstrakte Syntaxbäume zu generieren, Bindungen zu berechnen und in mehreren Durchläufen der Bäume die Programmelemente und Fakten zu erzeugen. Abbildung . listet tabellarisch die Zeiten auf, die zum Einlesen der verwendeten Testprojekte benötigt wurden. Rechnet man diese Zeiten direkt zu den Laufzeiten der einzelnen Refaktorisierungsanwendungen hinzu, erscheinen sie lang, insbesondere bei den beiden größten Projekten mit über Sekunden Laufzeit. Tabelle . .: Laufzeiten der Faktengenerierung Projekt Joda Convert Jester . b org.apache.commons.codec . JUnit . . org.apache.commons.io . Junit . org.htmlparser . Jakarta Commons Collections . P.-Elem . . . . . . . . Zeit (s) , , , , , , , , P.-Elem pro Sekunde , , , , , , , , Umgekehrt ergibt sich aber gerade bei der Faktengenerierung noch großes Optimierungspotential. Auch wenn das Refacola-Framework Derartiges noch nicht unterstü t, ist es denkbar, die Fakten nicht vollständig vorab zu generieren, sondern nur im Rahmen einer konkret anstehenden Query gezielt die entsprechenden Syntaxbäume zu durchlaufen, wie es Müller [Mül ] schon erfolgreich und mit erfreulichen Laufzeiten gezeigt hat. Entscheidend hierbei ist auch, was für Sichten auf den Code die jeweils genu te Bibliothek zum Au au der Syntaxbäume bereitstellt. So bietet zum Beispiel der JastAddJ-Compiler [EH ], auf welchem Schäfer [Sch ] seine Werkzeugsammlung au aute (im Gegensa zu den JDT [JDT ]) Rückreferenzen für Bindungen an, sodass hier in konstanter Zeit zu einer Deklaration die Menge der bei vorherigen Compilerdurchläufen gefundenen Referenzen auf diese abgefragt werden können, ohne jede potentiell referenzierende Klasse eigens durchsuchen zu müssen. Möchte man an einer Generierung aller Fakten für ein Projekt festhalten, bietet sich eine inkrementelle Faktengenerierung an. Fanden zwischen zwei Werkzeuganwendungen nur innerhalb einer Klasse Änderungen sta , müssen auch nur für diese Klasse neue Fakten generiert werden. Allerdings erkauft man sich hier die bessere Laufzeit mit höherem Speicherverbrauch. Die vollständige Faktenbasis für das größte der betrachteten Projekte Jakarta Commons Collections . belegt ca. MB, was ungefähr dem entspricht, was Eclipse in Standardeinstellung maximal an Speicherbelegung zulässt. Umgekehrt werden Arbeitspla rechner heutzutage mit mehreren Gigabyte Hauptspeicher bestückt, sodass hier kaum Probleme zu erwarten sind. Es ist somit davon auszugehen, dass die in der prototypischen Faktengenerierung auftretenden Laufzeiten in einem für den Praxiseinsa entwickelten Tool kaum negativ in Erscheinung treten. 8.8. Evaluation der Constraintregeln für reflektive Zugriffe Die Evaluation der Constraintregeln zur Behandlung reflektiver Zugriffe, die in Kapitel beschrieben wurden, erfolgte bereits im Rahmen einer vorangegangenen Publikation des Au- . Evaluation tors [TB ]. Der Vollständigkeit halber und insbesondere zur Beantwortung von Forschungsfrage werden die dort erhobenen Daten an dieser Stelle erneut vorgestellt. Die für die Evaluation der Reflection-Regeln genu ten Refaktorisierungswerkzeuge weisen Abweichungen zu den in Kapitel beschriebenen auf. So wurden zur Constraintgenerierung allein die in Kapitel beschriebenen Reflection-Regeln herangezogen. Für die Berechnung weiterer Vorbedingungen und Programmtransformationen wurden hingegen die Implementierungen von Refaktorisierungswerkzeugen der JDT [JDT ] genu t. Beim Ausführen einer Refaktorisierung wurde demnach zunächst das JDT-Werkzeug angestoßen. Erst wenn dieses die Refaktorisierung als durchführbar betrachtet und konkrete Codetransformationen berechnet hat, schaltet sich auf Basis dieser Codetransformationen zusä lich der constraintbasierte Teil hinzu, in welchem dann lediglich die Bedingungen geprüft werden, die sich aus Zugriffen auf die Reflection-API ergeben. Nach diesem Prinzip wurden insgesamt sechs Werkzeuge der JDT erweitert: vier Varianten der in den JDT vorhandenen R -Werkzeuge (für Pakete, Typen, Felder sowie Methoden und Konstruktoren), das M C U - sowie das C M /C A -Werkzeug. Insgesamt ist die Wahl bezüglich weiterer erlaubter Änderungen (den Modi) bei den Werkzeugen der JDT eingeschränkter als bei den in Kapitel beschriebenen Werkzeugen aus vorliegender Implementierung. Das M C U der JDT bietet nur die Wahl, zusä lich Referenzen anzupassen, was dem NOC-Modus des in dieser Arbeit entwickelten Werkzeugs entspricht. Das C M A passt von sich aus auch Zugrei arkeiten weiterer Methoden an, was dem CAModus des in dieser Arbeit vorgestellten Werkzeugs entspricht. Die R -Werkzeuge der JDT bieten ebenso wie die R -Werkzeuge aus dieser Arbeit nur einen CI-Modus. RTT-Tests Analog zu den in Abschni . vorgestellten Tests der übrigen Constraintregeln fußt auch die Evaluation der Reflection-Regeln im Wesentlichen auf RTT-Tests. Insgesamt kamen drei Testprojekte zur Anwendung. Neben dem Jakarta Commons Collections . -Projekt, welches auch schon bei den oben genannten RTT-Tests zum Einsa kam, kamen mit Joda Convert . und The Play Framework . . zwei weitere Projekte hinzu, für die im Vorhinein bekannt war, dass sie Gebrauch von der Reflection-API machen. Tabelle . listet ganz analog zu Tabelle . wesentliche Daten zu den drei Projekten auf, wobei sich für das Jakarta Commons Collections . -Projekt Abweichungen gegenüber Tabelle . daraus ergeben, dass nun zusä lich auch zur Verfügung stehende JUnit-Testfälle der Projekte mit betrachtet (und refaktorisiert) wurden. Als Ergänzung findet sich in Tabelle . eine zusä liche Spalte Refl. Aufrufe, welche die Anzahl der in der Laufzeitanalyse gefundenen Verwendungen der Reflection-API nennt. Es ist zu beachten, dass die Anzahl dieser Zugriffe nicht unbedingt mit der Anzahl der Ausdrücke korrelieren muss. Unabhängig davon, dass die Reflection-API in den einzelnen Projekten unterschiedlich starke Nu ung erfahren kann, können einerseits solche Zugriffe auch aus (bei der Anzahl der Anweisungen nicht berücksichtigtem) Bibliothekscode stammen, andererseits kann auch durch Schleifen ein einzelner Ausdruck zur Laufzeit zu mehreren reflektiven Aufrufen führen. Tabelle . zeigt die Ergebnisse der RTT-Tests. Die Spalten total geben dabei für jedes der drei Projekte an, wie häufig eine Refaktorisierung auf dem jeweiligen Projekt insgesamt angestoßen werden konnte. Zur Auswahl der Anwendungsstellen für die Refaktorisierungswerkzeuge kamen in den entsprechenden Refaktorisierungs-Adaptern ähnliche Algorithmen zur . . Evaluation der Constraintregeln für reflektive Zugriffe Anwendung, wie sie auch in Abschni . für die bereits oben beschriebenen RTT-Tests gewählt wurden. Im Falle der R -Refaktorisierungen wurde für jede Deklaration jeweils ein frischer Name gewählt. Beim C A wurde jede Methode und jeder Konstruktor jeweils mit private-Zugrei arkeit versehen, soweit er nicht schon vorher als private deklariert war. In diesem Fall wurde seine Zugrei arkeit auf public angehoben. Für M C U wurde jeweils ein beliebiges existentes Paket zum Ziel gewählt. Wie man an Tabelle . erkennt, ergeben sich zumeist weniger Anwendungsstellen für Refaktorisierungen, als es entsprechende Deklarationen gibt. Ursächlich hierfür ist, dass Deklarationen aus Bibliotheken von vornherein vom Refaktorisieren ausgeschlossen wurden. Beantwortung von Forschungsfrage 5 Forschungsfrage zielt darauf ab zu ergründen, ob die Laufzeitanalyse und die zusä lichen Reflection-Constraints einen Mehrwert ergeben. Um dem auf den Grund zu gehen, wurde ein vergleichender Versuchsau au gewählt, in welchem zunächst die Generierung der Reflection-Constraints unterdrückt wurde, also allein die Werkzeuge der JDT zum Einsa kamen. In Tabelle . listen die als ohne bezeichneten Spalten für diesen Durchgang zu jedem Refaktorisierungswerkzeug und Projekt auf, in wie vielen Fällen eine Refaktorisierung während der Überprüfung der Vorbedingungen durch die JDT abgelehnt wurde. So wurden beispielsweise in der Summe für alle Projekte von R F -Refaktorisierungen abgelehnt. Beim Umbenennen von Methoden hingegen lag die Ablehnungsquote mit aus deutlich höher, was im Wesentlichen darin begründet ist, dass bei Methoden häufig der Fall eintri , dass diese Methoden aus Bibliotheken überschreiben, was ein Umbenennen verbietet. Ein Umbenennen der Pakte konnte in jedem Fall durchgeführt werden. Hier sind in der Praxis auch keine Bedingungen zu erwarten, die dies verhindern sollten (vom sehr exotischen Verdunkeln ( § . . ) einmal abgesehen). In einem zweiten Durchlauf wurde jede der vorher ausgeführten Refaktorisierungen erneut angestoßen. Nun wurde allerdings die zusä liche Generierung der Reflection-Constraints anhand einer vorher sta gefundenen Laufzeitanalyse zugeschaltet. Die als mit überschriebenen Spalten in Tabelle . listen die Anzahl der in diesem zweiten Durchgang abgelehnten Refaktorisierungen auf. Da sich durch die zusä lich zur Anwendung kommenden Constraintregeln die geprüften Bedingungen nur verschärfen konnten, sank in keinem der Fälle die Anzahl abgelehnter Refaktorisierungen. Bemerkenswert hingegen ist, dass in der Summe für jedes der erweiterten Refaktorisierungswerkzeuge die Anzahl der abgelehnten Refaktorisierungen stieg. Von den insgesamt . Refaktorisierungen wurden nun . Refaktorisierungen zusä lich abgelehnt. Das bedeutet, dass in jedem dieser Anwendungsfälle für die den Projekten beiliegenden Testsuiten mindestens ein Zugriff auf die Reflection-API sta gefunden hat, welcher nun einen veränderten Wert zurückgibt. Ob sich so ein geänderter Wert und somit geändertes Programmverhalten tatsächlich bis zum endgültigen Testergebnis propagiert, bleibt dabei allerdings unbekannt. Eine Ausführung der JUnit-Tests nach jeder Refaktorisierung hat wegen der sich ergebenen langen Laufzeiten nicht sta gefunden. In Stichproben hingegen konnten Fälle gefunden werden, in denen neuerdings abgelehnte Refaktorisierungen ein Fehlschlagen einzelner Testfälle tatsächlich verhinderten. Forschungsfrage kann damit also positiv beantwortet werden. Für die betrachteten Probanden konnte tatsächlich ein signifikanter Anteil an Refaktorisierungen verhindert werden, der ansonsten zu abweichendem . Evaluation JDK . . . Pakete Typen . Member . . Ausdrücke . . . Tabelle . .: Die für die Evaluation der Reflection-Constraintregeln aus Kapitel Projekt The Play Framework . . Joda Convert . Jakarta Commons Collections . zum Einsa P.-Elem. . . . Refl. Aufrufe . . . gekommenen Testprojekte. Fakten . . . Rename Meth./Const. (CI) total ohne mit zurück Ch. Meth./Const. Acc. (CA) total ohne mit zurück total Rename Type (CI) ohne mit zurück total Move Compilation Unit (NOC) total ohne mit zurück Rename Field (CI) ohne mit zurück Rename Package (CI) total ohne mit zurück Tabelle . .: Ergebnisse der RTT-Tests der Reflection-Constraintregeln. Die Spalte total gibt für jedes Refaktorisierungswerkzeug die Menge aller angestoßener Refaktorisierungen an. Die folgenden drei Spalten zeigen die Anzahl erfolgreicher Refaktorisierungen, jeweils ohne Einsa der Reflection-Constraintregeln, mit Einsa der Reflection-Constraintregeln sowie mit Einsa der Reflection-Constraintregeln und zusä lichem Zurückschreiben in reflektive Referenzen. Projekt The Play Framework . . Joda Convert . Jakarta Commons Collections . total Projekt The Play Framework . . Joda Convert . Jakarta Commons Collections . total . . Evaluation der Constraintregeln für reflektive Zugriffe Programmverhalten hä e führen können. Gewinn durch zusätzliches Zurückschreiben in reflektive Ausdrücke Die zusä lichen Bedingungen, die sich aus Zugriffen auf die Reflection-API ergeben, schränken notwendigerweise die Anzahl der durchführbaren Refaktorisierungen ein. Abschni . hat einen Ausblick geliefert, wie in Einzelfällen dennoch mi els Umschreiben von Aufrufen an die ReflectionAPI Refaktorisierungen durchgeführt werden können. Mit einem dri en Durchlauf der RTTTests sollte geprüft werden, ob mi els dieser Technik in der Praxis Vorteile errungen werden können. Dazu wurden beide in Abschni . gezeigten Umschreibemuster implementiert. In Tabelle . ist in den mit zurück überschriebenen Spalten jeweils die Anzahl an Refaktorisierungen gelistet, die auch mit den erweiterten Funktionen der Refaktorisierungswerkzeuge abgelehnt wurden. Für R M /C und R F gab es insgesamt Fälle (Differenz aus mit und zurück), in denen die zusä lichen Constraintregeln zur Berücksichtigung von Reflection die Refaktorisierung unterbunden hä en, aber diese dennoch dank dem Zurückschreiben in reflektive Aufrufe bedeutungserhaltend durchgeführt werden konnte. Im Fall der übrigen Werkzeuge konnte keine Verbesserung erreicht werden. Eine Hauptursache, warum das Zurückschreiben in reflektive Aufrufe nur derart geringe Erfolgsquoten hatte, lag darin begründet, dass Aufrufe an die Reflection-API häufig aus Bibliotheken stammten, für die sich eine Modifikation verbot. Es ist anzunehmen, dass sich auch für den praktischen Einsa eine ähnliche Situation ergeben wird. Reflection bietet in Java die einzige Möglichkeit, durch ein bereits kompiliertes Framework Code der Verwender (erstmalig) aufzurufen. Es ist anzunehmen, dass ein Großteil der Anwendungen von Java-Reflection daher innerhalb von Programmbibliotheken sta findet, die sich einer möglichen Änderung en iehen. 9. Weitere constraintbasierte Werkzeuge Die in dieser Arbeit entwickelten Constraintregeln wurden aus der Motivation heraus zusammengetragen, Refaktorisierungswerkzeuge zu entwickeln. Ihr Anwendungsbereich ist aber keineswegs auf Refaktorisierungen beschränkt. Durch die deklarative Formulierung der Regeln bieten sie eine abstrakte Beschreibung, unter welchen Transformationen ein gegebenes Programm wohlgeformt (also kompilierbar) und bedeutungserhaltend bleibt, womit sich der Nu erkreis der Regeln auf solche Werkzeuge erweitern lässt, die Programmtransformationen durchführen und Eigenschaften zu Wohlgeformtheit und / oder Beibehaltung des Programmverhaltens zusichern sollen. Dieses Kapitel zeigt im Rahmen eines Ausblicks zwei weitere Nu ungsformen der entwickelten Constraintregeln. Einerseits werden Quick Fixes beleuchtet und andererseits eine Anwendungsmöglichkeit beim Mutationstesten aufgezeigt. 9.1. Quick Fixes Quick Fixes sind Werkzeuge, die für nicht kompilierende Programme Programmtransformationen vorschlagen, durch die Kompilierfehler behoben werden. Die Berechnung von Quick Fixes kann dabei als Bedingungserfüllungsproblem formuliert werden [SU ]. Generiert man für ein nicht kompilierbares Programm gemäß einem vollständigen und korrekten Regelsa Constraints, so können aus den Kompilierfehlern fehlschlagende Constraints resultieren. So würde zum Beispiel eine ungültigerweise als protected deklarierte Interface-Methode ein Constraint aus der Regel InterfaceMember ( S. ) verle en. Durch das Suchen nach Lösungen für das Constraintsystem und Rückschreiben der geänderten Variablenwerte in den Code erhält man Programmtransformationen, die die Wohlgeformtheit des Programms wiederherstellen. 9.1.1. Ein prototypischer Quick Fix zum Anpassen von Zugreifbarkeiten Im Rahmen dieser Arbeit wurde au auend auf den gegebenen Constraintregeln für Java ein Werkzeug implementiert, welches als erster exemplarischer Prototyp bei Zugriffen auf nicht zugrei are Deklarationen entsprechende Quick-Fixes anbietet. Intern arbeitet das Werkzeug ähnlich zu einer C A -Refaktorisierung. Zunächst wird das nicht kompilierbare Programm eingelesen und in eine Faktenbasis überführt, in welcher anhand der Fehlermeldung des Compilers die nicht zugrei are Deklaration ermi elt wird. Anschließend werden von dieser ausgehend gemäß den Constraintregeln Constraints generiert und eine Constraintlösung gesucht, welche einem dann insbesondere kompilierenden Programm entsprechen muss. Die Angabe einer Intention ist an und für sich nicht nötig. Im Fall einer Refaktorisierung gibt sie die eigentlich durchzuführende Änderung an. Ohne sie könnte der Constraintlöser die initiale Belegung der Constraintvariablen (also das unveränderte Programm) . Weitere constraintbasierte Werkzeuge als gültige Lösung ausgeben, schließlich müssen für ein kompilierendes Programm initial alle Constraints erfüllt sein, da es einerseits keine Kompilierfehler hat und andererseits per Definition im Programmverhalten unverändert ist. Für Quick-Fixes hingegen erübrigt sich die Angabe einer Intention, denn schon die den Kompilierfehler auslösende Deklaration muss mindestens ein initial nicht erfülltes Constraint bedingen, welches dann der Constraintlöser zu erfüllen versuchen wird. Allerdings verbietet die derzeitige Version des Refacola-Frameworks (noch), keine Intention anzugeben. Der hier präsentierte Prototyp wählt als Intentionen daher konkrete Änderungen der Zugrei arkeit der nicht zugrei aren Deklaration, wobei nacheinander alle für die Deklaration in Frage kommenden Zugrei arkeiten durchprobiert werden. Sobald eine Constraintlösung gefunden wird, erhält der Benu er den entsprechenden Vorschlag zur Programmtransformation. Ebenso wie die implementierten Refaktorisierungswerkzeuge wurde auch das Quick-FixWerkzeug in die Benu erschni stelle der JDT integriert, wobei derzeit sowohl die von Eclipse als auch vom constraintbasierten Werkzeug gegebenen Vorschläge parallel angezeigt werden. Abbildung . zeigt verschiedene Screenshots. Der jeweils obere Vorschlag kommt von den JDT, der untere Vorschlag vom constraintbasierten Prototypen. Neben der Tatsache, dass le terer einige Fehler der Eclipse-Werkzeuge behebt, ist insbesondere der Fall aus Abbildung . (iv) interessant. Für diesen kann keine Constraintlösung gefunden werden, ohne dass das übrige Programm sein Programmverhalten ändert. 9.1.2. Probleme bei der Implementierung beliebiger Quick Fixes Auch wenn das im vorhergehenden Abschni vorgestellte Werkzeug mit verblüffend wenig Aufwand auf Basis der in dieser Arbeit entwickelten und implementierten Constraintregeln erstaunlich ausgereift wirkt, lassen sich die Ergebnisse nicht ohne Weiteres auf andere Werkzeuge übertragen. Dieser Abschni nennt einige der sich ergebenden Probleme. Zunächst ist der Ansa auf solche Kompilierfehler beschränkt, die sich bei der semantischen Prüfung des Compilers ergeben. Meldet der Compiler bereits Fehler beim Parsen des Programms (indem es nicht der Grammatik der Sprache entspricht), wird schon die Abstraktion hin zu einer Faktenbasis scheitern. Für die Entwicklung der Refaktorisierungswerkzeuge war es von Vorteil, durch die Abstraktion konkreter Syntaxknoten hin zu Belegungen von Variablenwerten (siehe Abschni . . ) von vorneherein Syntaxprobleme ausblenden zu können. Umgekehrt schließt dies bei Quick Fixes die Behandlung von Syntaxfehlern aus. Für das oben vorgestellte Quick-Fix-Werkzeug wurde angenommen, dass bekannt ist, an welche (nicht zugrei are) Deklaration eine Referenz binden sollte. Das muss aber keineswegs immer der Fall sein. So ist es denkbar, dass der Aufruf einer Methode an möglicherweise zwei verschiedene überladene, nicht zugrei are Methoden mit gleicher Parameterzahl der Superklasse binden könnte. In diesem Fall muss sich bei der Faktengenerierung vorausschauend für eine der beiden Deklarationen entschieden werden, denn für eine von beiden muss ein binds-Fakt von der fehlschlagenden Referenz an die jeweilige Deklaration angelegt werden. Anderenfalls käme es gar nicht erst zur Erzeugung des Constraints, welches die Zugrei arkeit einer der Deklarationen von der Aufrufstelle aus fordern würde. Im Fall der zwei Methoden ist dies noch handhabbar, indem einfach beide Fälle nacheinander behandelt und im Falle einer Constraintlösung dem Benu er präsentiert werden. Es sind aber auch Fälle denkbar, in denen sich weit mehr Möglichkeiten ergeben. Kann eine Methodenreferenz nicht . . Quick Fixes (i) (ii) (iii) (iv) Abbildung . .: Vier Bildschirmfotos des implementierten Quick-Fix-Werkzeugs zum Anpassen von Zugrei arkeiten für Eclipse. Für jede gezeigte Codestelle werden jeweils zwei Quick-Fixes angeboten. Der jeweils obere, mit einem grünen Pfeil markierte Vorschlag wird durch die in Eclipse enthaltenen JDT [JDT ] berechnet, der jeweils untere, mit dem Icon einer roten Refacola-Dose markierte Vorschlag entstammt dem in dieser Arbeit beschriebenen Quick-FixWerkzeug. In den Fällen (i) bis (iii) kommt das JDT-Werkzeug zu fehlerhaften Vorschlägen, deren Anwendung den Code weiterhin nicht kompilieren lässt, während das constraintbasierte Werkzeug korrekte Vorschläge unterbreitet. Im Fall (iv) behebt das JDT-Werkzeug den vorhandenen Kompilierfehler, sorgt aber durch seine Änderung der Zugrei arkeit von m in B gleichzeitig für geändertes Programmverhalten in der Zeile oberhalb des Kompilierfehlers. . Weitere constraintbasierte Werkzeuge aufgelöst werden, weil keine Methode gleichen Namens auf dem Empfängerobjekt vorhanden ist, kommen potentiell alle weiteren Methoden auf diesem in Frage. Da dies aber schnell einige hundert sein können, scheidet ein Constraintlösen für jeden dieser hundert Fälle einerseits schon wegen der Laufzeit aus, andererseits aber auch, weil der Benu er typischerweise nur eine begrenzte Zahl an Vorschlägen sinnvoll erfassen kann. Wünschenswert hingegen wäre schon eine vorausschauende Vorabprüfung, ob es Methoden gibt, die sich im Namen nur geringfügig von dem bei der Methodenreferenz gewählten Bezeichner unterscheiden. Für diese könnte dann im Einzelnen anhand von Constraints geprüft werden, ob sich durch die Umbindung der Referenz an diese ein gültiges Programm ergibt, in dem auch Zugrei arkeit, Parametertypen und alle weiteren Bedingungen an einen Methodenaufruf erfüllt sind. Das Problem, auf eine vorausschauende Vorabprüfung angewiesen zu sein, gilt nicht für alle Arten von Quick Fixes. Bei den obigen Beispielen zu Zugrei arkeiten und ebenso zum unbekannten Namen innerhalb der Methodenreferenz war die vorausschauende Vorabprüfung nötig, damit überhaupt Fakten generiert werden konnten, die die fehlschlagenden Constraints erzeugen. In den Beispielen waren es jeweils binds-Fakten, die in die Faktenbasis einzutragen waren, obwohl technisch gesehen wegen der zu geringen Zugrei arkeit oder dem falschen Namen gar keine Bindung vorlag. Anders sieht dies hingegen bei Eigenschaften aus, die sich allein aus Belegungen von Constraintvariablen ergeben. Dies können zum Beispiel die Bindungen von Typreferenzen sein, die in der vorliegenden Arbeit anhand von Constraintvariablen typeBinding sta eines bindsFakts repräsentiert sind. Schlägt eine solche Typreferenz beispielsweise fehl, weil kein Typ passenden Namens gefunden wird, kann der initiale Wert der Variablen typeBinding der Typreferenz zunächst mit einem null-Wert belegt werden. Über eine zusä liche Constraintregel könnte dann gefordert werden, dass keine Variable mit einem solchen null-Wert belegt ist. Besonders elegant kommt dann zum Tragen, dass sich die in den oben gegebenen Beispielen erwähnte Notwendigkeit erübrigt, mehrere Constraintsysteme lösen zu müssen. Vielmehr stellt jede Lösung des Constraintsystems nun einen gültigen Vorschlag dar, der dem Benu er unterbreitet werden kann. Für das Beispiel einer Typreferenz, zu der kein Typ dieses Namens existiert, bestünden die Constraintlösungen vermutlich in Umbenennungen der Typreferenz oder Umbenennen von Typen hin zum in der Typreferenz verwendeten Namen. Allerdings bleibt dann als le tes Problem noch, dass zumindest das in der vorliegenden Arbeit genu te Refacola-Framework zwingend die Angabe einer Intention vorausse t, da auf ihr die Implementierung des Algorithmus zur Constraintgenerierung au aut. Dies ist allerdings nur ein technisches Problem. 9.2. Constraintbasiertes Mutationstesten Mutationstesten ist eine Methodik, um die Qualität von Testsuiten zu beurteilen. Zunächst erstmalig durch eine Studienarbeit Liptons Anfang der er Jahre publiziert [Mat ] hat sich aus der Anfangsidee ein ganzes Forschungsgebiet mit gegenwärtig über Publikationen entwickelt [JH ][Jia ]. Indem absichtlich kleine Änderungen (sogenannte Mutationen) in ein zu testendes Programm eingestreut werden, können Rückschlüsse auf die Qualität der Testsuiten gezogen werden. Schlagen die Tests nach den Änderungen fehl, ist dies ein Indikator für gute Testabdeckung. . . Constraintbasiertes Mutationstesten Das Hauptproblem beim Mutationstesten liegt in der Frage, wie geeignete Anwendungsstellen für Mutationen zu finden sind. Während es erstrebenswert ist, dass ein Verfahren zur Mutantengenerierung in möglichst vielen Fällen Codeänderungen durchführt, die das Programmverhalten tatsächlich ändern, wird ein naives Verfahren, das nur auf syntaktischer Ebene Änderungen, aber keine semantisch tiefer gehende Programmanalyse durchführt, entweder das Programmverhalten häufig unverändert lassen oder nicht kompilierbaren Code erzeugen. In ersterem Fall, einem sogenannten redundanten Mutanten, wird kein Testfall je die Chance haben, den Mutanten zu entdecken (zu töten im Jargon des Mutationstestens), in le terem Fall lassen sich die zu testenden Testsuiten nicht einmal ausführen. Eines der Hauptprobleme beim Mutationstesten besteht somit darin, wie Codetransformationen gefunden werden können, die einerseits wohlgeformten Code erzeugen, und andererseits das Programmverhalten mit hoher Wahrscheinlichkeit ändern. Diesem Problem kann ebenso constraintbasiert begegnet werden [ST , Bär ]. So lassen sich die für constraintbasierte Refaktorisierungswerkzeuge verwendeten Constraintregeln in zwei Kategorien aufteilen. Zum einen gibt es solche Regeln, die die Wohlgeformtheit des Ausgabeprogramms sicherstellen. Ein Beispiel hierfür ist die Regel InterfaceMember ( S. ), deren Verle ung stets zu einem Kompilierfehler führt. Zum anderen gibt es aber auch solche Regeln, deren Verle ung nicht durch einen Kompilierfehler offensichtlich wird. Ein Beispiel hierfür gibt die Regel AccidentalOverriding (siehe S. , ebenso S. ). Eine Verle ung dieser Regel bedeutet, dass eine zuvor nicht überschriebene Methode nach der Programmtransformation von einer anderen Methode überschrieben wird, was heimtückisch subtile Änderungen im Programmverhalten aufgrund sich ändernder, dynamischer Methodenbindungen nach sich ziehen kann. In dieselbe Kategorie fällt die Regel AccidentalInstanceMethod, ebenso S. ), deren fehlschlagende Constraints OverloadingInstanceMethod (siehe S. geänderte statische Methodenbindungen bedeuten. Mit einer solchen Unterteilung in Wohlgeformtheitsregeln und Programmverhaltensregeln lassen sich constraintbasiert Mutanten generieren, indem für das Eingabeprogramm gemäß aller Constraintregeln sämtliche Constraints generiert werden. In diesem, zunächst erfüllten, Constraintsystem wird genau eines der Constraints, welches aus einer ProgrammverhaltensRegel entspringt, negiert. Anschließend wird für das nun ungelöste Constraintsystem eine Lösung gesucht. Existiert eine solche Lösung, ist sie mit großer Wahrscheinlichkeit ein tauglicher Mutant. Einerseits erfüllt sie weiterhin alle Wohlgeformtheits-Constraints, ist somit kompilierbar. Andererseits existiert ein Programmverhaltens-Constraint, welches fehlgeschlagen ist, und somit existiert eine Änderung statischer oder dynamischer Bindung im Programm, welche – bei unterschiedlichem Verhalten der vorher und hinterher referenzierten Deklarationen – das Programmverhalten zumindest lokal (als sogenannter weak mutant [How ]) ändern wird. Die Arbeit [ST ] zeigt erfolgreich, dass constraintbasiert qualitativ hochwertige Mutanten erzeugt werden können. Sie baut dabei auf einer früheren Version [ST ] der in der vorliegenden Arbeit noch einmal überarbeiteten Zugrei arkeitsconstraints auf. Während in der damaligen Version die Constraintregeln noch als Java-Code und noch nicht in Refacola vorlagen, hat Grothoff [Gro ] mi lerweile gezeigt, wie über zusä liche Annotationen der Constraintregeln in Refacola Wohlgeformtheits-Regeln entsprechend annotiert werden können und über ein von ihm entwickeltes Werkzeug constraintbasierte Mutationstests durchgeführt werden können. 10. Zusammenfassung Korrekte Refaktorisierungswerkzeuge zu entwickeln ist schwer. Die vielen Funktionen und Ausdrucksmöglichkeiten moderner Programmiersprachen wie Java mögen es Programmierern heute einfacher machen, Code zu entwickeln. Gleichzeitig erschweren sie Werkzeugentwicklern die Implementierung von korrekten Refaktorisierungswerkzeugen. Die enge Verflechtung vieler Konzepte und Eigenschaften der Sprache Java zwingt sogar scheinbar einfache Refaktorisierungswerkzeuge dazu, komplexe Quellcodeanalysen durchzuführen. Schon das unverdächtig anmutende Einfügen eines public-Modifizierers kann Einfluss auf Methodenbindungen im Programm nehmen und es somit in seinem Verhalten verändern. Lässt ein Refaktorisierungswerkzeug so eine Codeänderung ohne entsprechende Prüfungen zu, sind zeitraubende und somit kostenintensive Fehlersuchen seitens des Programmierers die Folge. Erschwert wird die Situation dadurch, dass Refaktorisierungen typischerweise Folgeänderungen erfordern. So kann eine Verschieberefaktorisierung ebenso eine Änderung von Zugrei arkeiten verlangen, muss also zusä lich die entsprechenden Prüfungen hierfür durchführen. Gleichzeitig werden in heutigen Entwicklungsumgebungen Refaktorisierungswerkzeuge typischerweise weitgehend unabhängig voneinander implementiert. Die Folge ist, dass sie das in ihnen implementierte Wissen über zu prüfende Bedingungen kaum teilen. Nicht nur mehrfacher Entwicklungsaufwand ist die Folge, sondern auch, dass einzelne Werkzeuge Fehler aufweisen, die in anderen Werkzeugen derselben Entwicklungsumgebung vermieden wurden. Bisher gänzlich bei Refaktorisierungswerkzeugen für Java unberücksichtigt geblieben sind die Reflection-Funktionen der Sprache. Da bisherige Werkzeuge stets allein auf eine statische Codeanalyse se ten, konnten Bedingungen aus reflektiven Aufrufen nur in Spezialfällen Berücksichtigung finden. Die vorliegende Arbeit löst diese Probleme und stellt darüber hinaus noch einen Werkzeugkasten zur Verfügung, auf welchem auch andere Programmierwerkzeuge als nur solche für Refaktorisierungen au auen können. Im Kern dieses Ansa es stehen die constraintbasierten Refaktorisierungen, wie sie erstmalig von einer Arbeitsgruppe um Tip im Jahre beschrieben wurden [TKB ]. Bei diesen werden die durch das Refaktorisierungswerkzeug möglichen Quelltex ransformationen als Variablen beschrieben. Die für eine bedeutungserhaltende Codetransformation einzuhaltenden Bedingungen werden als Constraintregeln formuliert. Aus diesen generierte Bedingungen können gemeinsam mit den Variablen einem Constraintlöser übergeben werden. Kann dieser eine gültige Variablenbelegung finden, beinhaltet diese in Form von Variablenbelegungen direkt die Information, welche Änderungen im Code für eine alle Bedingungen erfüllende Refaktorisierung durchzuführen sind. Doch während diese ersten Werkzeuge von Tip et al. lediglich deklarierte Typen zu refaktorisieren vermochten und damit ebenso wie bisherige Refaktorisierungswerkzeuge isoliert in der Eclipse-IDE zu finden sind, ohne ihr Wissen mit anderen Werkzeugen teilen zu können, baut die vorliegende Arbeit die enthaltenen Grundideen zu einem universell nu baren Framework für Refaktorisierungswerkzeuge aus. . Zusammenfassung Den Schwerpunkt legt die vorliegende Arbeit auf Refaktorisierung von Deklarationen in Java. Um deren Grundeigenschaften – Namen, Deklarationsorte, deklarierte Typen und Zugrei arkeiten – bei Refaktorisierungen bedeutungserhaltend ändern zu können, wurde aus der Java-Sprachspezifikation ein umfangreicher Sa von Constraintregeln abgeleitet. Dieser Sa von Constraintregeln formuliert alle Bedingungen, die einzuhalten sind, um einzeln oder auch beliebig kombiniert die oben genannten Eigenschaften von Deklarationen bedeutungserhaltend ändern zu können. Um zusä lich auch Bedingungen zu berücksichtigen, die sich aus Verwendung der Reflection-API in Java ergeben, bietet das implementierte Framework ebenso die Möglichkeit, vor der Refaktorisierung eine Laufzeitanalyse – zum Beispiel bei Ausführung einer Testsuite – durchzuführen. Während dieser werden reflektive Aufrufe protokolliert, sodass aus diesem Protokoll weitere einzuhaltende Bedingungen abgeleitet werden können. Auf dieser Basis wurden fünf Refaktorisierungswerkzeuge implementiert: M C U ,P U ,R ,G /S D T und C A . Die Korrektheit von Refaktorisierungswerkzeugen zu beweisen ist schwierig. Bisherige Arbeiten anderer Autoren haben einzelne Eigenschaften ihrer Werkzeuge nachweisen können, lassen aber die Frage offen, ob in diesen Beweisen auch der natürlichsprachliche Teil der JavaSprachspezifikation vollständig und korrekt erfasst wurde. Daher kam in vorliegender Arbeit die etablierte Methode des Testens durch systematisches Anwenden der Refaktorisierungswerkzeuge auf quelloffenen Java-Projekten zum Einsa . Insgesamt wurden mehr als . Refaktorisierungen erfolgreich durchgeführt, was – wenn man von Refaktorisierungen am Tag ausgeht – dem entspricht, was ein Programmierer in vierzig Jahren Berufstätigkeit an Refaktorisierungen ausführt. Die dabei ermi elten Fehler – die Fehlerquote lag deutlich unterhalb eines Prozent – ließen sich auf Implementierungsfehler zurückführen, die unabhängig von den entwickelten Constraintregeln sind. Gleichzeitig lagen die Laufzeiten für den Großteil der Anwendungsfälle im Bereich weniger Sekunden. Darüber hinaus zeigt die vorliegende Arbeit noch Möglichkeiten zur weiteren Optimierung der Laufzeiten auf. Da die entwickelten Constraintregeln für Java unabhängig von konkreten Refaktorisierungen formuliert sind, bietet sich ihre Nu ung auch über den Anwendungsbereich von Refaktorisierungswerkzeugen hinaus an. Im Rahmen eines Ausblicks wurden zwei mögliche Anwendungen aufgezeigt, einerseits das constraintbasierte Mutationstesten und andererseits die Implementierung von Quick-Fix-Werkzeugen. In le terem Anwendungsfall konnte au auend auf den entwickelten Constraintregeln mit minimalem Aufwand ein Quick-Fix-Werkzeug zum Anpassen von Zugrei arkeiten entwickelt werden, welches gegenüber seinem der Funktion nach identischen Pendant innerhalb der Entwicklungsumgebung Eclipse gleich eine Reihe von Fehlern behebt. A. Java-Sprachdefinition Dieser Anhang zeigt die Java-Sprachdefinition, wie sie in Kapitel beschrieben wird. In den Kommentaren genannte Paragraphen beziehen sich auf solche der Java-Sprachspezifikation [GJSB ], ihnen nachfolgende Texte sind aus dieser wörtlich übernommen und nicht als Werk des Autors vorliegender Arbeit zu betrachten. language Java kinds abstract Entity <: ENTITY abstract NamedEntity <: Entity { identifier } abstract PackagedEntity <: Entity { hostPackage } abstract TlOwnedEntity <: PackagedEntity { tlowner } abstract CompilationUnitMember <: Entity { typeRoot } abstract AccessibleEntity <: Entity { accessibility } abstract OwnedEntity <: Entity { owner } abstract TypedEntity <: Entity abstract RegularTypedEntity <: TypedEntity { declaredType } abstract TypeVariableTypedEntity <: TypedEntity /* * Packages and Compilation Units */ Package <: NamedEntity TypeRoot <: NamedEntity, PackagedEntity PackageDeclaration <: NamedEntity ImportDeclaration <: NamedEntity, PackagedReference { typeBinding } /* * Types * * § 4.1 * There are two kinds of types in the Java programming language: primitive types (§4.2) and * reference types (§4.3). */ abstract Type <: Entity abstract Interface <: ClassOrInterfaceType abstract Class <: ClassOrInterfaceType abstract Enumeration <: Class abstract ArrayableType <: Type abstract NamedType <: ArrayableType, NamedEntity abstract AccessModifiableType <: AccessibleEntity, Type /* * The Null Type A. Java-Sprachdefinition * * § 4.1 * There is also a special null type, the type of the expression * null, which has no name. Because the null type has no name, it is * impossible to declare a variable of the null type or to cast to * the null type. The null reference is the only possible value of * an expression of null type. The null reference can always be * cast to any reference type. */ NullType <: Type /* * Reference Types * * § 4.3 * There are three kinds of reference types: class types (§8), interface types (§9), and array * types (§10) * * N.B. * ReferenceType inherits the property accessibility from AccessModifiableType. This is * questionable, since anonymous and local types are reference types, but must not have an * access modifier. Basically, this decision was made to allow cut downs on the reference side, * where otherwise e.g., field references must have been divided in AccessibleTypeTypedField− * References and NonAccessibleTypeTypedFieldReferences to correctly handle implicit type * accesses. Instead, an additional constraint rule now ensures that local and anonymous types * always have default accessibility. */ abstract ReferenceType <: AccessModifiableType /* * Primitive Types * * § 4.2 * A primitive type is predefined by the Java programming language and named by its reserved * keyword */ abstract PrimitiveType <: NamedType PrimitiveBoolean <: PrimitiveType PrimitiveByte <: PrimitiveType PrimitiveShort <: PrimitiveType PrimitiveInt <: PrimitiveType PrimitiveLong <: PrimitiveType PrimitiveChar <: PrimitiveType PrimitiveFloat <: PrimitiveType PrimitiveDouble <: PrimitiveType PrimitiveVoid <: PrimitiveType /* * Class or Interface Type * * § 4.3 * A class or interface type consists of a type declaration specifier */ abstract TlOwnedType <: ReferenceType, TlOwnedEntity {enclosingTypes, enclosingNamedTypes} abstract ClassOrInterfaceType <: TlOwnedType abstract NamedClassOrInterfaceType <: ClassOrInterfaceType, NamedType /* * Top Level Types * * § 7.6 * A top level type declaration declares a top level class type (§8) or a top level interface * type (§9) * *§8 * A top level class is a class that is not a nested class. */ abstract TopLevelType <: CompilationUnitMember, NamedClassOrInterfaceType TopLevelClass <: TopLevelType, Class TopLevelInterface <: TopLevelType, Interface TopLevelEnum <: TopLevelClass, Enumeration /* * Nested Types * *§8 * A nested class is any class whose declaration occurs within the body of another class or * interface. * *§9 * A nested interface is any interface whose declaration occurs within the body of another * class or interface. */ abstract NestedType <: ClassOrInterfaceType /* * Member Types * * § 8.5 * A member class is a class whose declaration is directly enclosed in another class or * interface declaration. Similarly, a member interface is an interface whose declaration is * directly enclosed in another class or interface declaration. */ abstract MemberType <: NestedType, HideableMember, NamedClassOrInterfaceType /* * Static Member Types * * § 8.5.2 * The static keyword may modify the declaration of a member type C within the body of a * non−inner class T. Its effect is to declare that C is not an inner class. */ abstract StaticMemberType <: MemberType, StaticMember StaticMemberClass <: StaticMemberType, Class StaticMemberInterface <: StaticMemberType, Interface StaticMemberEnum <: StaticMemberClass, Enumeration /* * Inner Class * A. Java-Sprachdefinition * § 8.1.3 * An inner class is a nested class that is not explicitly or implicitly declared static. Inner * classes include local (§14.3), anonymous (§15.9.5) and non−static member classes (§8.5). */ abstract InnerClass <: NestedType, Class /* * Inner Classes in Non Static Context * * § 8.1.3 * A statement or expression occurs in a static context if and only if the innermost method, * constructor, instance initializer, static initializer, field initializer, or explicit * constructor invocation statement enclosing the statement or expression is a static method, a * static initializer, the variable initializer of a static variable, or an explicit * constructor invocation statement (§8.8.7). */ abstract InnerClassInNonStaticContext <: InnerClass /* * Anonymous Classes * * § 15.9.5 * An anonymous class declaration is automatically derived from a class instance creation * expression by the compiler. An anonymous class is always an inner class (§8.1.3); it is * never static (§8.1.1, §8.5.2). */ abstract AnonymousClass <: InnerClass AnonymousClassInNonStaticContext <: AnonymousClass, InnerClassInNonStaticContext AnonymousClassInStaticContext <: AnonymousClass /* * Non Static Member Class * * § 8.5.2 * The static keyword may modify the declaration of a member type C within the body of a * non−inner class T. Its effect is to declare that C is not an inner class. */ NonStaticMemberClass <: MemberType, InstanceMember, InnerClassInNonStaticContext /** * Local Classes * * § 14.3 * A local class is a nested class (§8) that is not a member of any class and that has a name. * All local classes are inner classes (§8.1.3). Every local class declaration statement is * immediately contained by a block. */ abstract LocalClass <: InnerClass, NamedClassOrInterfaceType LocalClassInNonStaticContext <: LocalClass, InnerClassInNonStaticContext LocalClassInStaticContext <: LocalClass /* * Array Types * * § 10 * An array object contains a number of variables. (...) These variables are called the * components of the array. * * The component type of an array may itself be an array type. The components of such an array * may contain references to subarrays. If, starting from any array type, one considers its * component type, and then (if that is also an array type) the component type of that type, * and so on, eventually one must reach a component type that is not an array type; this is * called the element type of the original array, and the components at this level of the data * structure are called the elements of the original array. */ ArrayType <: ArrayableType, NamedType { componentType, elementType } TlOwnedArrayType <: TlOwnedType, ArrayType MemberArrayType <: TlOwnedArrayType, MemberType JavaLangObjectType <: TopLevelClass JavaLangStringType <: TopLevelClass JavaLangIterableType <: TopLevelInterface JavaLangThrowableType <: TopLevelClass /* * Type Variables * * § 4.4 * * A type variable is an unqualified identifier. Type variables are introduced by generic class * declarations (§8.1.2) generic interface declarations (§9.1.2) generic method declarations * (§8.4.4) and by generic constructor declarations (§8.8.4). */ TypeVariable <: NamedType abstract TypeParameterBinding <: Entity SimpleTypeParameterBinding <: TypeParameterBinding { boundType } abstract WildcardTypeParameterBinding <: TypeParameterBinding UpperBoundWildcardTypeParameterBinding <: WildcardTypeParameterBinding LowerBoundWildcardTypeParameterBinding <: WildcardTypeParameterBinding abstract MemberOrConstructor <: TlOwnedEntity, AccessibleEntity, OwnedEntity, NamedEntity abstract MethodOrConstructor <: MemberOrConstructor { parameters } /* * Members * * § 6.4.3 * * The members of a class type (§8.2) are classes (§8.5, §9.5), interfaces (§8.5, §9.5), fields * (§8.3, §9.3, §10.7), and methods (§8.4, §9.4). Members are either declared in the type, or * inherited because they are accessible members of a superclass or superinterface which are * neither private nor hidden nor overridden (§8.4.8). * * § 9.2 * * The members of an interface are: * − Those members declared in the interface. * − Those members inherited from direct superinterfaces. * − If an interface has no direct superinterfaces, then the interface implicitly * declares a public abstract member method m with signature s, return type r, and A. Java-Sprachdefinition * throws clause t corresponding to each public instance method m with signature s, * return type r, and throws clause t declared in Object, unless a method with the * same signature, same return type, and a compatible throws clause is explicitly * declared by the interface. It is a compile−time error if the interface explicitly * declares such a method m in the case where m is declared to be final in Object. */ abstract Member <: MemberOrConstructor abstract InstanceMember <: Member abstract HideableMember <: Member abstract StaticMember <: HideableMember /* * § 8.3 Field Declarations */ abstract Field <: HideableMember, TypedEntity abstract InstanceField <: Field, InstanceMember abstract RegularTypedField <: Field, RegularTypedEntity TypeVariableTypedInstanceField <: InstanceField, TypeVariableTypedEntity RegularTypedInstanceField <: InstanceField, RegularTypedField StaticField <: RegularTypedField, StaticMember /* * Enum Constants * * § 16.5 * * an enum constant is essentially a static final field (§8.3.1.1, §8.3.1.2) * that is initialized with a class instance creation expression * */ EnumConstant <: StaticField /* * § 8.4 Method Declarations */ abstract Method <: MethodOrConstructor, Member, TypedEntity abstract InstanceMethod <: Method, InstanceMember abstract RegularTypedMethod <: Method, RegularTypedEntity TypeVariableTypedInstanceMethod <: InstanceMethod, TypeVariableTypedEntity RegularTypedInstanceMethod <: InstanceMethod, RegularTypedMethod StaticMethod <: RegularTypedMethod, StaticMember /* * § 8.6 Instance Initializers * * An instance initializer declared in a class is executed when an instance * of the class is created (§15.9), as specified in §8.8.7.1. * * * § 8.7 Static Initializers * * Any static initializers declared in a class are executed when the class is * initialized and, together with any field initializers (§8.3.2) for class * variables, may be used to initialize the class variables of the class (§12.4). * */ abstract Initializer <: OwnedEntity InstanceInitializer <: Initializer StaticInitializer <: Initializer /* * Local Variables and Formal Parameters (also from catch clauses) */ abstract LocalVariableOrParameter <: NamedEntity abstract LocalVariable <: TypedEntity, LocalVariableOrParameter {enclosingLocalVariables} RegularTypedLocalVariable <: LocalVariable, RegularTypedEntity TypeVariableTypedLocalVariable <: LocalVariable, TypeVariableTypedEntity abstract FormalParameter <: LocalVariableOrParameter, TypedEntity { declaredParameterType } RegularTypedFormalParameter <: FormalParameter, RegularTypedEntity TypeVariableTypedFormalParameter <: FormalParameter, TypeVariableTypedEntity /* * Constructors * * § 8.8 * A constructor is used in the creation of an object that is an instance of a class. * Constructors are invoked by class instance creation expressions (§15.9), by the * conversions and concatenations caused by the string concatenation operator + (§15.18.1), * and by explicit constructor invocations from other constructors (§8.8.7). * * Constructor declarations are not members. They are never inherited and therefore are * not subject to hiding or overriding. * * § 8.8.9 * If a class contains no constructor declarations, then a default constructor that takes * no parameters is automatically provided. */ abstract Constructor <: MethodOrConstructor DeclaredConstructor <: Constructor DefaultConstructor <: Constructor AnonymousClassConstructor <: Constructor abstract Reference <: REFERENCE abstract NamedReference <: Reference { identifier } abstract PackagedReference <: Reference { hostPackage } abstract ReferenceInType <: PackagedReference { owner, tlowner } abstract TypeOrTypeVariableReference <: NamedReference TypeVariableReference <: TypeOrTypeVariableReference abstract TypeReference <: TypeOrTypeVariableReference, PackagedReference { typeBinding } TypeReferenceInCU <: TypeReference TypeReferenceInType <: TypeReference, ReferenceInType ImplicitThisTypeReference <: TypeReferenceInType abstract NonTypeReference <: ReferenceInType abstract TypedReference <: NonTypeReference A. Java-Sprachdefinition abstract ThisOrSuperReference <: TypedReference ThisReference <: ThisOrSuperReference SuperReference <: ThisOrSuperReference QualifiedThisReference <: ThisReference { thisQualifier } abstract NamedTypedReference <: NamedReference, TypedReference abstract ReferenceWithArguments <: NonTypeReference { arguments } abstract MethodOrFieldReference <: NamedTypedReference VariableOrParameterReference <: NamedTypedReference MethodReference <: MethodOrFieldReference, ReferenceWithArguments FieldReference <: MethodOrFieldReference // Constructor accesses abstract ConstructorReference <: ReferenceWithArguments ClassInstanceCreation <: NamedTypedReference, ConstructorReference ThisConstructorInvocation <: ConstructorReference SuperConstructorInvocation <: ConstructorReference ImplicitSuperConstructorInvocation <: SuperConstructorInvocation abstract Expression <: ENTITY { expressionType } NullExpression <: Expression ArrayTypedExpression <: Expression { inferredArrayType } ClassOrInterfaceTypedExpression <: Expression { inferredClassOrInterfaceType } PrimitiveTypedExpression <: Expression { inferredPrimitiveType } TypeVariableTypedExpression <: Expression ClassExpression <: ClassOrInterfaceTypedExpression /* Expressions consisting of accesses to the pseudo array field 'length' */ ArrayLengthAccessExpression <: PrimitiveTypedExpression abstract ClassCastExpression <: Expression ArrayTypedCastExpression <: ClassCastExpression, ArrayTypedExpression ClassOrInterfaceCastTypedExpression <: ClassCastExpression, ClassOrInterfaceTypedExpression PrimitiveTypedCastExpression <: ClassCastExpression, PrimitiveTypedExpression TypeVariableTypedCastExpression <: ClassCastExpression, TypeVariableTypedExpression /* R E F L E C T I O N */ abstract ReflectiveReference <: REFERENCE {owner, hostPackage } abstract ReflectiveFieldNameReference <: ReflectiveReference { identifier } abstract ReflectiveMethodNameReference <: ReflectiveReference { identifier } abstract ReflectiveTypeNameReference <: ReflectiveReference { identifier } abstract ReflectivePackageNameReference <: ReflectiveReference { identifier } abstract ReflectiveAccessModifierReference <: ReflectiveReference { returnedAccessModifier } abstract ReflectiveAccess <: ReflectiveReference { accessEnabled } abstract ReflectiveParameterTypeListReference <: ReflectiveReference { parameters } abstract ReflectiveMemberReference <: ReflectiveReference abstract ReflectiveMemberSetReference <: ReflectiveReference abstract ReflectiveDeclaredMemberReference <: ReflectiveReference abstract ReflectiveDeclaredMemberSetReference <: ReflectiveReference abstract ReflectiveDeclaringClassReference <: ReflectiveReference // Class#forName(String className) ClassForName <: ReflectiveTypeNameReference, ReflectivePackageNameReference {} // Class#getDeclaredField(String name) ClassGetDeclaredField <: ReflectiveFieldNameReference, ReflectiveDeclaredMemberReference {} // Class#getDeclaredFields() ClassGetDeclaredFields <: ReflectiveDeclaredMemberSetReference {} // Class#getDeclaredMethod(String, Class<?>...) ClassGetDeclaredMethod <: ReflectiveMethodNameReference, ReflectiveDeclaredMemberReference, ReflectiveParameterTypeListReference {} // Class#getDeclaredMethods() ClassGetDeclaredMethods <: ReflectiveDeclaredMemberSetReference {} // Class#getDeclaredConstructor(String, Class<?>...) ClassGetDeclaredConstructor <: ReflectiveMethodNameReference, ReflectiveDeclaredMemberReference, ReflectiveParameterTypeListReference {} // Class#getDeclaredConstructors() ClassGetDeclaredConstructors <: ReflectiveDeclaredMemberSetReference {} // Class#getField(String name) ClassGetField <: ReflectiveFieldNameReference, ReflectiveMemberReference {} // Class#getFields() ClassGetFields <: ReflectiveMemberSetReference {} // Class#getMethod(String, Class<?>...) ClassGetMethod <: ReflectiveMethodNameReference, ReflectiveMemberReference, ReflectiveParameterTypeListReference {} // Class#getMethods() ClassGetMethods <: ReflectiveMemberSetReference { } // Class#getConstructor(String, Class<?>...) ClassGetConstructor <: ReflectiveTypeNameReference, ReflectiveMemberReference, ReflectiveParameterTypeListReference {} // Class#getConstructors() ClassGetConstructors <: ReflectiveMemberSetReference { } // Field#get(Object obj) // Field#getBoolean(Object obj) // Field#getByte(Object obj) // Field#getChar(Object obj) // Field#getDouble(Object obj) A. Java-Sprachdefinition // Field#getFloat(Object obj) // Field#getInt(Object obj) // Field#getLong(Object obj) // Field#getShort(Object obj) // Field#set(Object obj, Object value) // Field#setBoolean(Object obj, boolean z) // Field#setByte(Object obj, byte b) // Field#setChar(Object obj, char c) // Field#setDouble(Object obj, double d) // Field#setFloat(Object obj, float f) // Field#setInt(Object obj, int i) // Field#setLong(Object obj, long l) // Field#setShort(Object obj, short s) FieldGetSet <: ReflectiveAccess { } // Field#getDeclaringClass() FieldGetDeclaringClass <: ReflectiveDeclaringClassReference { } // Field#getModifiers() FieldGetModifiers <: ReflectiveAccessModifierReference { } // Field#getName FieldGetName <: ReflectiveFieldNameReference { } // Field#toGenericString() FieldToGenericString <: ReflectiveFieldNameReference, ReflectiveTypeNameReference, ReflectivePackageNameReference, ReflectiveAccessModifierReference, ReflectiveDeclaringClassReference { } // Method#getDeclaringClass() MethodGetDeclaringClass <: ReflectiveReference {} // Method#getModifiers() MethodGetModifiers <: ReflectiveAccessModifierReference {} // Method#getName() MethodGetName <: ReflectiveMethodNameReference {} // Method#invoke(Object, Object...) MethodInvoke <: ReflectiveAccess {} // Method#toGenericString MethodToGenericString <: ReflectiveMethodNameReference, ReflectiveTypeNameReference, ReflectivePackageNameReference, ReflectiveAccessModifierReference, ReflectiveDeclaringClassReference { } // Constructor#newInstance ConstructorNewInstance <: ReflectiveAccess // Constructor#toGenericString ConstructorToGenericString <: ReflectiveTypeNameReference, ReflectivePackageNameReference, ReflectiveAccessModifierReference, ReflectiveDeclaringClassReference { } // Constructor#getName ConstructorGetName <: ReflectiveTypeNameReference // Constructor#getDeclaringClass ConstructorGetDeclaringClass <: ReflectiveDeclaringClassReference // Constructor#getModifiers() ConstructorGetModifiers <: ReflectiveAccessModifierReference {} properties identifier: Identifier accessibility: AccessModifierDomain owner: ClassOrInterfaceTypes tlowner: TopLevelTypes hostPackage: Packages typeRoot: TypeRoots declaredType: Types declaredParameterType: Types typeBinding: Types expressionType: Types boundType: Types thisQualifier: Types inferredTypeVariableBinding: Types inferredPrimitiveType: PrimitiveTypes inferredClassOrInterfaceType: ClassOrInterfaceTypes inferredArrayType: ArrayTypes componentType : ArrayableTypes elementType : NamedTypes enclosingTypes : Sequence(Types) // INCLUDING the type itself! enclosingNamedTypes : Sequence(NamedTypes) // NOT INCLUDING the type itself enclosingLocalVariables : Sequence(LocalVariables)//NOT INCLUDING the localVariable itself! isUpperBound : Boolean parameters: Sequence(FormalParameters) arguments: Sequence(Expressions) /* R E F L E C T I O N */ returnedAccessModifier: AccessModifierDomain accessEnabled : Boolean domains AccessModifierDomain = {private, package, protected, public} ClassOrInterfaceTypes = [ ClassOrInterfaceType ] TopLevelTypes = [ TopLevelType ] Packages = [ Package ] TypeRoots = [ TypeRoot ] Types = [ Type ] A. Java-Sprachdefinition PrimitiveTypes = [ PrimitiveType ] ArrayTypes = [ ArrayType ] ArrayableTypes = [ ArrayableType ] NamedTypes = [ NamedType ] FormalParameters = [ FormalParameter ] Expressions = [ Expression ] LocalVariables = [ LocalVariable ] queries binds(reference: NonTypeReference, entity: Entity) receiver(reference: TypedReference, receiver: Expression) staticReceiver(reference: TypedReference, receiver: TypeReferenceInType) sub(subtype : Type, supertype: Type) isExpression(reference: TypedReference, expression: Expression) castExpression(castType: TypeOrTypeVariableReference, innerExpression: Expression, castExpression: ClassCastExpression) instantiates(C: Constructor, T: Type) assignment(lhs: Expression, rhs : Expression) abstractMethod(abstractMethod: InstanceMethod) mainMethod(mainMethod: StaticMethod) finalMethod(finalMethod : Method) // fact expressing that a certain constructor is an implicit constructor inside an anonymous // class implicitly invoking it's super constructor isAnonymousSuperConstructor(superTypeConstructor: Constructor, anonymousClassConstructor: AnonymousClassConstructor) isAnonymousClass(anonymousClass: AnonymousClass, superClass: ClassOrInterfaceType) initialAssignment(lhs: RegularTypedEntity, rhs: Expression) returnStatement(expression: Expression, method: Method) argumentPassing(expression: Expression, formalParameter: FormalParameter) // e.g. a++ will cause typePropagation(a, a++) typePropagation(fromExpression: Expression, toExpression: Expression) arrayAccess(arrayAccessExpression: Expression, array: ArrayType) overrides(overridingMethod : InstanceMethod, overriddenMethod: InstanceMethod) // overridingMethod overrides overriddenMethod directly i.e., there is no other overriding // method between them within the type hierarchy overridesDirectly(overridingMethod : InstanceMethod, overriddenMethod: InstanceMethod) // The method helps to implement an interface by providing an override equivalent signature // to a method inherited by the class from one of its implementing interfaces. // If there are two eager interface implementing methods overriding each other, only the // overriding one is considered. eagerInterfaceImplementation(clazz: Class, typeMethod: InstanceMethod, interfaceMethod: InstanceMethod) referenceInEntity(r: Reference, p: Entity) initialHostType(t: ClassOrInterfaceType, m: Java.Member) // e.g. new String[]{ "abc", x } for which two facts are generated: // − arrayInitializer(new String[]{ "abc", x } , "abc") // − arrayInitializer(new String[]{ "abc", x } , x) arrayInitializer(arrayTypedExpression: ArrayTypedExpression, expression: Expression) memberOrConstructorOfType(memberOrConstructor: MemberOrConstructor, type: Type) publicMemberOrConstructorOfType(memberOrConstructor: MemberOrConstructor, type: Type) isThisOrSuperExpression(expression: Expression) packageDeclarationInTypeRoot(packageDeclaration: PackageDeclaration, typeRoot: TypeRoot) importDeclarationInTypeRoot(importDeclaration: ImportDeclaration, type: NamedType) // type references inside of method, field, variable and parameter declarations definesTypeOf(typeReference: TypeOrTypeVariableReference, typedEntity: TypedEntity) thisOrSuperQualifier(typeReference: TypeOrTypeVariableReference, qualifiedThisOrSuper: ThisOrSuperReference) declaresLocalVariableOrParameter(method: MethodOrConstructor, localVariableOrParameter: LocalVariableOrParameter) hasEnclosingVariables(localVariable : LocalVariable) booleanExpression(booleanExpression: Expression) iterableExpression(iterableExpression: Expression) throwStatement(throwExpression: Expression) /* G E N E R I C S */ typeOf(variableOrMember: TypeVariableTypedEntity, typeVariable: TypeVariable) typeVariableOf(typeVariable: TypeVariable, genericClass: Type) typeReferenceFor(typeReference: TypeReference, typeParameterBinding: TypeParameterBinding) typeContextOf(parameterizedTypeEntity: TypedEntity, typeParameterBinding: TypeParameterBinding) typeParameterBinding(typeVariable: TypeVariable, typeParameterBinding: TypeParameterBinding) typeVariableBound(typeVariable: TypeVariable, bound: TypeOrTypeVariableReference) wildcardBound(wildcardParameterBinding: WildcardTypeParameterBinding, bound: TypeOrTypeVariableReference) typeParameterAssignment(lhs: TypeParameterBinding, rhs: TypeParameterBinding) /* R E F L E C T I O N */ reflectiveBinding(reflectiveReference: ReflectiveReference, namedEntity: NamedEntity) reflectiveReceiver(reflectiveAccess: ReflectiveAccess, classOrInterfaceType: ClassOrInterfaceType) B. Constraintregeln für Java Dieser Anhang zeigt die Constraintregeln für Java, wie sie in den Kapiteln und beschrieben werden. In den Kommentaren genannte Paragraphen beziehen sich auf solche der JavaSprachspezifikation [GJSB ], ihnen nachfolgende Texte sind aus dieser wörtlich übernommen und nicht als Werk des Autors vorliegender Arbeit zu betrachten. B. Constraintregeln für Java /* * § 8.4.3.1 * * It is a compile−time error for a private method to be declared abstract. */ AbstractMethodAccessibility for all method: Java.InstanceMethod do if Java.abstractMethod(method) then method.accessibility != #private end /* * 8.4.3.3 Final Methods * * A method can be declared final to prevent subclasses from overriding or * hiding it. It is a compile−time error to attempt to override or hide a * final method. */ 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 /* * Former Ovr rule from [ST09] */ AccidentalInstanceMethodOverloadingInstanceMethod for all method: Java.InstanceMethod reference: Java.MethodReference receiver: Java.ClassOrInterfaceTypedExpression competingMethod: Java.InstanceMethod do if Java.binds(reference, method), Java.receiver(reference, receiver), competingMethod != method then (method.identifier != competingMethod.identifier) or (not Java.sub*(receiver.expressionType,competingMethod.owner)) or (Java.sub*(method.parameters.declaredParameterType, competingMethod.parameters.declaredParameterType)) or (not Java.sub*(reference.arguments.expressionType, competingMethod.parameters.declaredParameterType)) or ((reference.tlowner != competingMethod.tlowner) and (competingMethod.accessibility = #private)) or ((reference.hostPackage != competingMethod.hostPackage) and (competingMethod.accessibility <= #package)) or ((reference.hostPackage != competingMethod.hostPackage) and (not Java.sub*(receiver.expressionType, reference.owner)) and (competingMethod.accessibility <= #protected)) end /* * Former Ovr rule from [ST09] */ AccidentalInstanceMethodOverloadingStaticMethod for all method: Java.StaticMethod reference: Java.MethodReference receiver: Java.TypeReferenceInType competingMethod: Java.InstanceMethod do if Java.binds(reference, method), Java.staticReceiver(reference, receiver), all(competingMethod) then (method.identifier != competingMethod.identifier) or (not Java.sub*(receiver.typeBinding,competingMethod.owner)) or (Java.sub*(method.parameters.declaredParameterType, competingMethod.parameters.declaredParameterType)) or (not Java.sub*(reference.arguments.expressionType, competingMethod.parameters.declaredParameterType)) or ((reference.tlowner != competingMethod.tlowner) and (competingMethod.accessibility = #private)) or ((reference.hostPackage != competingMethod.hostPackage) and (competingMethod.accessibility <= #package)) or ((reference.hostPackage != competingMethod.hostPackage) and (not Java.sub*(reference.owner, competingMethod.owner)) and (competingMethod.accessibility <= #protected)) or ((reference.hostPackage != competingMethod.hostPackage) and (not Java.sub*(receiver.typeBinding, reference.owner)) and (competingMethod.accessibility <= #protected)) end /* * Former Dyn−2 rule from [ST09] */ AccidentalOverriding for all superMethod: Java.InstanceMethod subMethod: Java.InstanceMethod B. Constraintregeln für Java do if subMethod != 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 /* * Former Ovr rule from [ST09] */ AccidentalStaticMethodOverloadingInstanceMethod for all method: Java.InstanceMethod reference: Java.MethodReference receiver: Java.ClassOrInterfaceTypedExpression competingMethod: Java.StaticMethod do if Java.binds(reference, method), Java.receiver(reference, receiver), all(competingMethod) then (method.identifier != competingMethod.identifier) or (not Java.sub*(receiver.expressionType,competingMethod.owner)) or (Java.sub*(method.parameters.declaredParameterType, competingMethod.parameters.declaredParameterType)) or (not Java.sub*(reference.arguments.expressionType, competingMethod.parameters.declaredParameterType)) or ((reference.tlowner != competingMethod.tlowner) and (competingMethod.accessibility = #private)) or ((reference.hostPackage != competingMethod.hostPackage) and (competingMethod.accessibility <= #package)) or ((reference.hostPackage != competingMethod.hostPackage) and (not Java.sub*(reference.owner, competingMethod.owner)) and (competingMethod.accessibility <= #protected)) end /* * Former Ovr rule from [ST09] */ AccidentalStaticMethodOverloadingStaticMethod for all method: Java.StaticMethod reference: Java.MethodReference receiver: Java.TypeReferenceInType competingMethod: Java.StaticMethod do if Java.binds(reference, method), Java.staticReceiver(reference, receiver), all(competingMethod) then (method.identifier != competingMethod.identifier) or (not Java.sub*(receiver.typeBinding,competingMethod.owner)) or (Java.sub*(method.parameters.declaredParameterType, competingMethod.parameters.declaredParameterType)) or (not Java.sub*(reference.arguments.expressionType, competingMethod.parameters.declaredParameterType)) or ((reference.tlowner != competingMethod.tlowner) and (competingMethod.accessibility = #private)) or ((reference.hostPackage != competingMethod.hostPackage) and (competingMethod.accessibility <= #package)) or ((reference.hostPackage != competingMethod.hostPackage) and (not Java.sub*(reference.owner, competingMethod.owner)) and (competingMethod.accessibility <= #protected)) end /* * Accesses to static fields in interfaces might become ambiguous in case * of competing fields, which must then be either * * (1) not declared in a super type of the static field access receiver type * (2) declared using a different identifier * (3) hidden * (4) inaccessible * * (Former Inh−2 rule) */ AmbigousFieldAccess for all field: Java.StaticField fieldReference: Java.FieldReference receiver: Java.TypeReferenceInType competingField: Java.Field do if Java.binds(fieldReference, field), Java.staticReceiver(fieldReference, receiver), all(competingField) then ((field = competingField) or (not Java.sub(receiver.typeBinding, competingField.owner)) or (Java.sub(field.owner, competingField.owner)) or ((competingField.identifier != field.identifier) or (((competingField.tlowner != field.tlowner) and (competingField.accessibility = #private)) or ((competingField.hostPackage != field.hostPackage) and (competingField.accessibility <= #package))))) end /* * For convenience, all reference types inherit from AccessModifiableType. * Since local and anonymous classes have no access modifier, B. Constraintregeln für Java * their accessibility is fixed to default accessibility to avoid vain * constraint solutions. */ AnonymousClassAccessibility for all anonymousClass: Java.AnonymousClass do if all(anonymousClass) then anonymousClass.accessibility = #package end /* * § 15.9.3 * * [...] a compile−time method matching error results if there is no unique * most−specific constructor that is [...] accessible */ AnonymousClassConstructorAccessibility for all constructor: Java.Constructor anonymousConstructor: Java.AnonymousClassConstructor do if Java.isAnonymousSuperConstructor(constructor, anonymousConstructor) then (constructor.tlowner != anonymousConstructor.tlowner) −> (constructor.accessibility >= #package), (constructor.hostPackage != anonymousConstructor.hostPackage) −> (constructor.accessibility >= #protected) end /* * § 15.9.1 * * [...] let T be the ClassOrInterfaceType after the new token. It is a * compile−time error if the class or interface named by T is not accessible * (§6.6) */ AnonymousClassRequiresSuperTypeAccessibility for all anonymousClass: Java.AnonymousClass superClass: Java.ClassOrInterfaceType do if Java.isAnonymousClass(anonymousClass, superClass) then (anonymousClass.tlowner != superClass.tlowner) −> (superClass.accessibility >= #package), (anonymousClass.hostPackage != superClass.hostPackage) −> (superClass.accessibility >= #protected) end /* * Rule (5) and (8) from [TFK+11] */ ArgumentPassing for all expression: Java.Expression formalParameter: Java.RegularTypedFormalParameter do if Java.argumentPassing(expression, formalParameter) then Java.sub*(expression.expressionType, formalParameter.declaredType) end /* * Types of array access expression */ ArrayAccessTypes for all arrayAccessExpression: Java.Expression arrayType: Java.ArrayType do if Java.arrayAccess(arrayAccessExpression, arrayType) then arrayAccessExpression.expressionType = arrayType.componentType end /* * Types of array initializers */ ArrayInitializer for all arrayTypedExpression: Java.ArrayTypedExpression expression: Java.Expression do if Java.arrayInitializer(arrayTypedExpression, expression) then expression.expressionType = arrayTypedExpression.inferredArrayType.componentType end /* * § 6.6.1 * * An array type is accessible if and only if its element type is accessible. */ ArrayTypeAccessibility for all arrayType: Java.TlOwnedArrayType elementType: Java.AccessModifiableType do if all(arrayType), all(elementType) then B. Constraintregeln für Java (arrayType.elementType = elementType) −> (arrayType.accessibility = elementType.accessibility) end /* * just a technical rule: the expression type of an expression is equal to its inferred type */ ArrayTypedExpressions for all arrayTypedExpression: Java.ArrayTypedExpression do if all(arrayTypedExpression) then arrayTypedExpression.expressionType = arrayTypedExpression.inferredArrayType end /* * Rule (1) from [TFK+11] */ Assignment1 for all lhs: Java.Expression rhs: Java.Expression do if Java.assignment(lhs, rhs) then Java.sub*(rhs.expressionType,lhs.expressionType) end /* * Rule (1) from [TFK+11] */ Assignment2 for all lhs: Java.RegularTypedEntity rhs: Java.Expression do if Java.initialAssignment(lhs, rhs) then Java.sub*(rhs.expressionType,lhs.declaredType) end /* * Class instance creation expressions from other packages require the * accessed constructor to be public, even when the access comes from a * sub−type * * See § 6.6.2.2 */ ClassInstanceCreationAccess for all r: Java.ClassInstanceCreation c: Java.Constructor do if Java.binds(r, c) then (r.hostPackage != c.hostPackage) −> (c.accessibility >= #public) end /* * just a technical rule: the expression type of an expression is equal to its inferred type */ ClassOrInterfaceTypedExpressions for all classOrInterfaceTypedExpression: Java.ClassOrInterfaceTypedExpression do if all(classOrInterfaceTypedExpression) then classOrInterfaceTypedExpression.expressionType = classOrInterfaceTypedExpression.inferredClassOrInterfaceType end /* * When accessing a constructor (via this(), super() or a class instance * creation expression), it must be accessible from the point of reference * * See § 6.6.2.2 */ ConstructorAccess for all r: Java.ConstructorReference c: Java.Constructor do if Java.binds(r, c) then (r.tlowner != c.tlowner) −> (c.accessibility >= #package), (r.hostPackage != c.hostPackage) −> (c.accessibility >= #protected), (r.hostPackage != c.hostPackage and not Java.sub*(r.owner, c.owner)) −> c.accessibility = #public end /* * When accessing a constructor, the instantiated type must be accessible * * See § 15.9.1 */ ConstructorAccessRequiresTypeAccessibility for all r: Java.ConstructorReference c: Java.Constructor B. Constraintregeln für Java type: Java.AccessModifiableType do if Java.binds(r, c), Java.instantiates(c,type) then (r.tlowner != c.tlowner) −> (type.accessibility >= #package), (r.hostPackage != c.hostPackage) −> (type.accessibility >= #protected), (r.hostPackage != c.hostPackage and not Java.sub*(r.owner, c.owner)) −> type.accessibility = #public end /* * The type of a Class instance creation expression is the * type declaring the invoked constructor. * * Rule (7) from [TFK+11] */ ConstructorIncovationExpressionTypes for all reference: Java.TypedReference constructor: Java.Constructor expression: Java.Expression type: Java.Type do if Java.isExpression(reference, expression), Java.binds(reference, constructor), Java.instantiates(constructor,type) then expression.expressionType = type end /* * § 8.8 * * The SimpleTypeName in the ConstructorDeclarator must be the simple * name of the class that contains the constructor declaration; otherwise * a compile−time error occurs. */ ConstructorNames for all type: Java.NamedType constructor: Java.Constructor do if Java.instantiates(constructor, type) then type.identifier = constructor.identifier end /* * § 8.8.9 * * In an enum type (§8.9), the default constructor is implicitly private. * Otherwise, if the class is declared public, then the default constructor * is implicitly given the access modifier public (§6.6); if the class is * declared protected, then the default constructor is implicitly given the * access modifier protected (§6.6); if the class is declared private, then * the default constructor is implicitly given the access modifier private * (§6.6); otherwise, the default constructor has the default access implied * by no access modifier. */ DefaultConstructorAccessibility for all c: Java.DefaultConstructor t: Java.Class do if Java.instantiates(c,t) then t.accessibility = c.accessibility end /* * A method implementing an interface method in a subclass must be public accessible */ EagerInterfaceImplementationAccessibility for all method: Java.InstanceMethod interfaceMethod: Java.InstanceMethod class: Java.Class do if Java.eagerInterfaceImplementation(class, method, interfaceMethod) then method.accessibility = #public end /* * Similar to rule (9) from [TFK+11] but for eager interface implementation */ EagerInterfaceImplementationParameterType for all clazz: Java.Class overridingMethod: Java.InstanceMethod overriddenMethod: Java.InstanceMethod do if Java.eagerInterfaceImplementation(clazz, overridingMethod, overriddenMethod) then overridingMethod.parameters.declaredParameterType = overriddenMethod.parameters.declaredParameterType end /* * § 8.4.8.3 in case of eager interface implementation */ EagerInterfaceImplementationReturnType B. Constraintregeln für Java for all clazz: Java.Class overridingMethod: Java.RegularTypedInstanceMethod overriddenMethod: Java.RegularTypedInstanceMethod do if Java.eagerInterfaceImplementation(clazz, overridingMethod, overriddenMethod) then overridingMethod.declaredType = overriddenMethod.declaredType end /* * § 8.4.8.4 Inheriting Methods with Override−Equivalent Signatures * * It is possible for a class to inherit multiple methods with override−equivalent * (§8.4.2) signatures. [...] the method that is not abstract is considered to * override, and therefore to implement, all the other methods on behalf of the * class that inherits it. */ 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 /* * § 8.8.3 * * If no access modifier is specified for the constructor of an enum type, * the constructor is private. It is a compile−time error if the constructor * of an enum type (§8.9) is declared public or protected. */ EnumConstructorAccessibility for all enumeration: Java.Enumeration constructor: Java.Constructor do if Java.instantiates(constructor,enumeration) then constructor.accessibility = #private end /* * § 8.4.8.3 * * The access modifier (§6.6) of an overriding or hiding method must provide at * least as much access as the overridden or hidden method, or a compile−time * error occurs. */ HidingMethodAccessibility for all hidingMethod: Java.StaticMethod hiddenMethod: Java.StaticMethod do if all(hidingMethod), all(hiddenMethod) then ((hidingMethod.identifier = hiddenMethod.identifier) and Java.sub(hidingMethod.owner, hiddenMethod.owner) and (hidingMethod.parameters.declaredParameterType = hiddenMethod.parameters.declaredParameterType) and ( ((hidingMethod.hostPackage = hiddenMethod.hostPackage) and (hiddenMethod.accessibility = #package)) or (hiddenMethod.accessibility >= #protected)) −> (hidingMethod.accessibility >= hiddenMethod.accessibility)) end /* * § 8.4.8.3 * * If a method declaration d1 with return type R1 overrides * or hides the declaration of another method d2 with return * type R2, then d1 must be return−type substitutable for d2, * or a compile−time error occurs. */ HidingMethodReturnType for all hidingMethod: Java.StaticMethod hiddenMethod: Java.StaticMethod do if all(hidingMethod), all(hiddenMethod) then ((hidingMethod.identifier = hiddenMethod.identifier) and Java.sub(hidingMethod.owner, hiddenMethod.owner) and (hidingMethod.parameters.declaredParameterType = hiddenMethod.parameters.declaredParameterType) and ( ((hidingMethod.hostPackage = hiddenMethod.hostPackage) and (hiddenMethod.accessibility = #package)) or (hiddenMethod.accessibility >= #protected)) −> (hidingMethod.declaredType = hiddenMethod.declaredType)) end /* * § 7.5.1: * * A single−type−import declaration imports a single type by giving its canonical name [...] * The named type must be accessible (§6.6) or a compile−time error occurs. */ ImportAccessibility for all tr: Java.ImportDeclaration type: Java.TlOwnedType B. Constraintregeln für Java do if all(type), all(tr) then (tr.typeBinding = type) −> (type.accessibility >= #package), ((tr.typeBinding = type) and (tr.hostPackage != type.hostPackage)) −> (type.accessibility = #public) end InferredParameterizedTypeForWildcard for all expression: Java.TypeVariableTypedExpression reference: Java.TypedReference declaration: Java.TypeVariableTypedEntity receiverExpression: Java.Expression receiverReference: Java.TypedReference receiverDeclaration: Java.TypedEntity receiverDeclarationType: Java.TypeReference typeVariable: Java.TypeVariable genericType: Java.Type typeParameterBinding: Java.WildcardTypeParameterBinding wildcardBound: Java.TypeReference do if Java.isExpression(reference, expression), Java.receiver(reference, receiverExpression), Java.isExpression(receiverReference, receiverExpression), Java.binds(receiverReference, receiverDeclaration), Java.definesTypeOf(receiverDeclarationType, receiverDeclaration), Java.binds(reference, declaration), Java.typeOf(declaration, typeVariable), Java.typeVariableOf(typeVariable, genericType), Java.typeParameterBinding(typeVariable, typeParameterBinding), Java.typeContextOf(receiverDeclaration, typeParameterBinding), Java.wildcardBound(typeParameterBinding, wildcardBound) then expression.expressionType = wildcardBound.typeBinding end /* * 01: class Box<T> { T contents; } * 02: Box<String> stringBox; * 03: String s = stringBox.contents; * * The inferred type of stringBox.contents must be String */ InferredParameterizedTypeNoWildcard for all expression: Java.TypeVariableTypedExpression /* 03: stringBox.contents */ reference: Java.TypedReference /* 03: contents */ declaration: Java.TypeVariableTypedEntity /* 01: T contents */ receiverExpression: Java.Expression /* 03: stringBox */ receiverReference: Java.TypedReference /* 03: stringBox */ receiverDeclaration: Java.TypedEntity /* 02: Box<String> stringBox*/ receiverDeclarationType: Java.TypeReference /* 02: Box<String> */ typeVariable: Java.TypeVariable /* 01: T */ genericType: Java.Type /* 01: class Box<T> {T contents;} */ typeParameterBinding: Java.SimpleTypeParameterBinding /* 02: <String> */ do if Java.isExpression(reference, expression), Java.receiver(reference, receiverExpression), Java.isExpression(receiverReference, receiverExpression), Java.binds(receiverReference, receiverDeclaration), Java.definesTypeOf(receiverDeclarationType, receiverDeclaration), Java.binds(reference, declaration), Java.typeOf(declaration, typeVariable), Java.typeVariableOf(typeVariable, genericType), Java.typeParameterBinding(typeVariable, typeParameterBinding), Java.typeContextOf(receiverDeclaration, typeParameterBinding) then expression.expressionType = typeParameterBinding.boundType end /* * See § 15.12.4. * * Accessing a member not only requires it to be accessible from the point of * reference, but also to be accessible within the whole type hierarchy * through which it gets accessed. * * (Former Inh−1 rule, but now checks the whole type hierarchy between * declaration an receiver) */ InheritedMemberAccessibility for all reference: Java.MethodOrFieldReference declaration: Java.Member receiver: Java.ClassOrInterfaceTypedExpression inheritingType: Java.ClassOrInterfaceType do if Java.binds(reference,declaration), Java.receiver(reference,receiver), all(inheritingType) then ((Java.sub(inheritingType, declaration.owner)) and (Java.sub*(receiver.inferredClassOrInterfaceType, inheritingType)) and (inheritingType.tlowner != reference.tlowner)) −> (declaration.accessibility >= #package), ((Java.sub(inheritingType, declaration.owner)) and (Java.sub*(receiver.inferredClassOrInterfaceType, inheritingType)) and (inheritingType.hostPackage != reference.hostPackage)) −> (declaration.accessibility >= #protected) B. Constraintregeln für Java end /* * § 8.4.8.1 * * A compile−time error occurs if an instance method overrides a static method. */ 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 /* * * § 9.1.5 * * All interface members are implicitly public. They are accessible outside * the package where the interface is declared if the interface is also * declared public or protected, in accordance with the rules of §6.6. */ InterfaceMember for all interface: Java.Interface member: Java.Member do if all(interface), all(member) then (member.owner = interface) −> (member.accessibility = #public) end /* * Iterable Expressions */ IterableExpressions for all iterableExpression: Java.Expression iterable: Java.JavaLangIterableType do if Java.iterableExpression(iterableExpression), all(iterable) then iterableExpression.expressionType = iterable end /* * For convenience, all reference types inherit from AccessModifiableType. * Since local and anonymous classes have no access modifier, * their accessibility is fixed to default accessibility to avoid vain * constraint solutions. */ LocalClassAccessibility for all localClass: Java.LocalClass do if all(localClass) then localClass.accessibility = #package end /* * Rule (3) and (6) from [TFK+11] */ MemberAccess for all receiver: Java.Expression reference: Java.TypedReference declaration: Java.OwnedEntity do if Java.receiver(reference, receiver), Java.binds(reference, declaration) then Java.sub*(receiver.expressionType,declaration.owner) end /* * The top level type of a member or constructor is the top level type of the declaring class * (owner). * * The host package of a member or constructor is the host package of the declaring class. */ MemberOrConstructorTlOwnerAndHostPackage for all d: Java.MemberOrConstructor do if all(d) then d.tlowner = d.owner.tlowner, d.hostPackage = d.owner.hostPackage end /* * Accessibility when accessing member types B. Constraintregeln für Java * * See § 6.6.1 */ MemberTypeAccess for all tr: Java.TypeReferenceInType referencingType: Java.TlOwnedType type: Java.MemberType do if all(type), all(tr), all(referencingType) then (tr.typeBinding = type) and (referencingType = tr.owner) and (tr.hostPackage != type.hostPackage) and Java.sub*!(referencingType.enclosingTypes, type.owner) −> type.accessibility = #public end /* * Accessibility for method and field accesses * * See § 6.6.1 */ MethodOrFieldAccess1 for all r: Java.MethodOrFieldReference d: Java.Member do if Java.binds(r,d) then (r.tlowner != d.tlowner) −> (d.accessibility >= #package), (r.hostPackage != d.hostPackage) −> (d.accessibility >= #protected) end /* * Accessibility for method and field accesses * * See § 6.6.1 */ MethodOrFieldAccess2 for all r: Java.MethodOrFieldReference d: Java.Member referencingType: Java.TlOwnedType do if Java.binds(r,d), all(referencingType) then ( (referencingType = r.owner) and (r.hostPackage != d.hostPackage) and ( Java.sub*!(referencingType.enclosingTypes, d.owner)) ) −> d.accessibility = #public end /* * Private accessibility is not sufficient, if the referencing * type is not enclosed within (or equal to) the declaring type * * See § 6.6.1 */ MethodOrFieldAccess3 for all r: Java.MethodOrFieldReference d: Java.Member referencingType: Java.NestedType do if Java.binds(r,d), all(referencingType) then ( referencingType = r.owner and referencingType.enclosingTypes != d.owner and d.owner.enclosingTypes != referencingType ) −> d.accessibility != #private end /* * In case of a member access on an anonymous class declaration ( new A(){}.m() ) the owner of * the method invocation is the enclosing type, but not the anonymous class. For this reason, * in case of an anonymous class creation, the following rule is required */ MethodOrFieldAccess3AnonymousClass for all r: Java.MethodOrFieldReference d: Java.Member rec: Java.ClassOrInterfaceTypedExpression classInstanceCreation: Java.ClassInstanceCreation anonymousClassConstructor: Java.AnonymousClassConstructor anonymousClass: Java.AnonymousClass do if Java.binds(r,d), Java.receiver(r,rec), Java.isExpression(classInstanceCreation, rec), Java.binds(classInstanceCreation,anonymousClassConstructor), Java.instantiates(anonymousClassConstructor, anonymousClass) then ( anonymousClass.enclosingTypes != d.owner and d.owner.enclosingTypes != anonymousClass ) −> d.accessibility != #private end /* * See § 15.12.4.3 B. Constraintregeln für Java */ MethodOrFieldAccessRequiresTypeAccessibility1 for all reference: Java.MethodOrFieldReference member: Java.Member memberOwner: Java.TopLevelType do if Java.binds(reference,member), all(memberOwner) then ( (memberOwner = member.owner) and (reference.tlowner != memberOwner)) −> (memberOwner.accessibility >= #package), ((memberOwner = member.owner) and (reference.hostPackage != memberOwner.hostPackage)) −> (memberOwner.accessibility >= #public) end /* * See § 15.12.4.3 */ MethodOrFieldAccessRequiresTypeAccessibility2 for all reference: Java.MethodOrFieldReference member: Java.Member memberOwner: Java.MemberType referenceOwner: Java.ClassOrInterfaceType do if Java.binds(reference,member), all(memberOwner), all(referenceOwner) then ( (memberOwner = member.owner) and (reference.tlowner != memberOwner.tlowner)) −> (memberOwner.accessibility >= #package), ( (memberOwner = member.owner) and (reference.hostPackage != memberOwner.hostPackage)) −> (memberOwner.accessibility >= #protected), ( (memberOwner = member.owner) and (referenceOwner = reference.owner) and (reference.hostPackage != memberOwner.hostPackage) and (Java.sub!(referenceOwner.enclosingTypes, memberOwner.owner)) ) −> (memberOwner.accessibility = #public) end /* * Rule (13) from [TFK+11] */ MethodReturn for all expression: Java.Expression method: Java.Method methodType: Java.TypeReference do if Java.returnStatement(expression, method), Java.definesTypeOf(methodType, method) then Java.sub*(expression.expressionType,methodType.typeBinding) end /* * Currently we do not allow expressions to change from * primitive to class or interface or array types or vice versa */ NoExpressionKindChange1 for all primitiveTypedExpression: Java.PrimitiveTypedExpression classOrInterfaceType: Java.ClassOrInterfaceType do if all(primitiveTypedExpression), all(classOrInterfaceType) then primitiveTypedExpression.expressionType != classOrInterfaceType end NoExpressionKindChange2 for all primitiveTypedExpression: Java.PrimitiveTypedExpression arrayType: Java.ArrayType do if all(primitiveTypedExpression), all(arrayType) then primitiveTypedExpression.expressionType != arrayType end NoExpressionKindChange3 for all arrayTypedExpression: Java.ArrayTypedExpression classOrInterfaceType: Java.ClassOrInterfaceType do if all(arrayTypedExpression), all(classOrInterfaceType) then arrayTypedExpression.expressionType != classOrInterfaceType end NoExpressionKindChange4 for all arrayTypedExpression: Java.ArrayTypedExpression primitiveType: Java.PrimitiveType do if all(arrayTypedExpression), B. Constraintregeln für Java all(primitiveType) then arrayTypedExpression.expressionType != primitiveType end NoExpressionKindChange5 for all classOrInterfaceTypedExpression: Java.ClassOrInterfaceCastTypedExpression primitiveType: Java.PrimitiveType do if all(classOrInterfaceTypedExpression), all(primitiveType) then classOrInterfaceTypedExpression.expressionType != primitiveType end NoExpressionKindChange6 for all classOrInterfaceTypedExpression: Java.ClassOrInterfaceCastTypedExpression arrayType: Java.ArrayType do if all(classOrInterfaceTypedExpression), all(arrayType) then classOrInterfaceTypedExpression.expressionType != arrayType end /* * § 8.4.8.1 * * An instance method m1 declared in a class C overrides another instance method, * m2, declared in class A iff all of the following are true: * * − (...) * − Either * − m2 is public, protected or declared with default * access in the same package as C, or * − m1 overrides a method m3, m3 distinct from m1, * m3 distinct from m2, such that m3 overrides m2. */ OverridingAccessibility1 for all overridingMethod: Java.InstanceMethod overriddenMethod: Java.InstanceMethod do if Java.overridesDirectly(overridingMethod, overriddenMethod) then (overriddenMethod.accessibility >= #protected) or ((overriddenMethod.accessibility = #package) and (overriddenMethod.hostPackage = overridingMethod.hostPackage)) end /* * § 8.4.8.3 * * The access modifier (§6.6) of an overriding or hiding method must provide at * least as much access as the overridden or hidden method, or a compile−time * error occurs. */ OverridingAccessibility2 for all overridingMethod: Java.InstanceMethod overriddenMethod: Java.InstanceMethod do if Java.overridesDirectly(overridingMethod, overriddenMethod) then overridingMethod.accessibility >= overriddenMethod.accessibility end /* * 8.4.8.1 Overriding (by Instance Methods) * * An instance method m1 declared in a class C overrides another instance method, m2, * declared in class A iff all of the following are true: * − (...) * − The signature of m1 is a subsignature (§8.4.2) of the signature of m2. * * * 8.4.2 Method Signature * * Two methods have the same signature if they have the same name and (...) */ OverridingMethodNames for all superMethod: Java.InstanceMethod subMethod: Java.InstanceMethod do if Java.overrides(subMethod, superMethod) then subMethod.identifier = superMethod.identifier end /* * Rule (11) from [TFK+11] */ OverridingMustComeFromSubtype for all overridingMethod: Java.InstanceMethod overriddenMethod: Java.InstanceMethod do if Java.overridesDirectly(overridingMethod, overriddenMethod) then Java.sub(overridingMethod.owner, overriddenMethod.owner) end B. Constraintregeln für Java /* * Rule (9) from [TFK+11] */ OverridingParameterType for all overridingMethod: Java.InstanceMethod overriddenMethod: Java.InstanceMethod do if Java.overridesDirectly(overridingMethod, overriddenMethod) then overridingMethod.parameters.declaredParameterType = overriddenMethod.parameters.declaredParameterType end /* * Rule (10) from [TFK+11] * * For Java 1.5 the conclusion must read: * Java.sub*(overridingMethodType.typeBinding, overriddenMethodType.typeBinding) */ OverridingReturnType for all overridingMethod: Java.RegularTypedInstanceMethod overriddenMethod: Java.RegularTypedInstanceMethod do if Java.overridesDirectly(overridingMethod, overriddenMethod) then overridingMethod.declaredType = overriddenMethod.declaredType end /* * § 7.3 Compilation Units * * A compilation unit consists of three parts, each of which is optional: * * − A package declaration (§7.4), giving the fully qualified name (§6.7) * of the package to which the compilation unit belongs. A compilation * unit that has no package declaration is part of an unnamed package * (§7.4.2). * − ... */ PackageDeclarationNames for all packageDeclaration: Java.PackageDeclaration typeRoot: Java.TypeRoot do if Java.packageDeclarationInTypeRoot(packageDeclaration, typeRoot) then packageDeclaration.identifier = typeRoot.hostPackage.identifier end /* * In case of nested types do not move references to method parameters * outside the parameter declaring method: * * class A{} * * class B{ * void m(final int p) { * class X extends A { * void m() {int y = p;} // <− do not pull up m to A *} *} *} * */ ParameterReferencesFromInnerClassStayInPlace for all parameterReference: Java.VariableOrParameterReference parameterReferenceHostMethod: Java.Method parameter: Java.FormalParameter parameterHostMethod: Java.Method originalParameterReferenceHostType: Java.ClassOrInterfaceType do if Java.binds(parameterReference, parameter), Java.declaresLocalVariableOrParameter(parameterHostMethod, parameter), Java.referenceInEntity(parameterReference,parameterReferenceHostMethod), Java.initialHostType(originalParameterReferenceHostType, parameterReferenceHostMethod) then (parameterReferenceHostMethod != parameterHostMethod) −> parameterReferenceHostMethod.owner = originalParameterReferenceHostType end ParameterTypes for all regularTypedFormalParameter: Java.RegularTypedFormalParameter do if all(regularTypedFormalParameter) then regularTypedFormalParameter.declaredParameterType = regularTypedFormalParameter.declaredType end /* * just a technical rule: the expression type of an expression is equal to its inferred type */ PrimitiveTypedExpressions for all primitiveTypedExpression: Java.PrimitiveTypedExpression do if all(primitiveTypedExpression) then primitiveTypedExpression.expressionType B. Constraintregeln für Java = primitiveTypedExpression.inferredPrimitiveType end /* * § 6.6.2 * * A protected member or constructor of an object may be accessed from outside * the package in which it is declared only by code that is responsible for * the implementation of that object. * * (Former Acc−2 rule) */ ProtectedMethodOrFieldAccess for all reference: Java.MethodOrFieldReference declaration: Java.InstanceMember receiver: Java.ClassOrInterfaceTypedExpression do if Java.binds(reference,declaration), Java.receiver(reference,receiver) then ((reference.hostPackage != declaration.hostPackage) and (not Java.sub*(receiver.expressionType,reference.owner)) and (not Java.isThisOrSuperExpression(receiver))) −> declaration.accessibility = #public end QualifiedThis for all qualifiedThisReference: Java.QualifiedThisReference thisExpression: Java.Expression do if Java.isExpression(qualifiedThisReference,thisExpression) then qualifiedThisReference.thisQualifier = thisExpression.expressionType end /* * The identifier of a named reference must match the name of the name of the referenced entity. */ ReferenceIdentifier for all reference: Java.NamedTypedReference entity: Java.NamedEntity do if Java.binds(reference, entity) then reference.identifier = entity.identifier end /* * When accessing a member or constructor the accessed declaration must * be reflective accessible. * * Please note, that accessibility for reflective invocations notably * differs from the accessibility rules for regular member accesses as * defined in the JLS. A member is reflective accessible if one of the * following statements is true: * * − The accessed member has been set accessible with the setAccessible()− * method * − The accessed member is declared in the same type as the accessing * statement (the JLS requires same top level types instead) * − The accessed member is declared package accessible and in the same * package as the accessing statement * − The accessed member is declared as public * (the JLS instead also considers protected accessibility) * * When accessing a member for which isAccessible() is not set to true, * also its declaring type must be accessible. * * A type is reflective accessible if one of the following statements is * true: * − The type is the same as that in which the accessing statement resides * − The type is declared package accessible and resides in the same * package as the accessing statement * − The type is declared as a public type * */ ReflectiveAccessibility for all r: Java.ReflectiveAccess d: Java.MemberOrConstructor rt: Java.ClassOrInterfaceType do if Java.reflectiveBinding(r, d), Java.reflectiveReceiver(r, rt) then r.accessEnabled = #public or( // accessibility of the member (((((r.owner != d.owner) −> (d.accessibility > #private)) and ((r.hostPackage != d.hostPackage) −> (d.accessibility = #public))) // accessibility of the declaring type and ((rt != d.owner) −> (rt.accessibility > #private))) and ((rt.hostPackage != d.hostPackage) −> (rt.accessibility = #public)))) end /* * When accessing a type with a qualified name, it must remain in the * package used as qualification */ ReflectiveClassForNameStayInPackage for all r: Java.ClassForName d: Java.NamedClassOrInterfaceType B. Constraintregeln für Java p: Java.Package do if Java.reflectiveBinding(r, d), Java.reflectiveBinding(r, p) then d.hostPackage = p end ReflectiveClassGetConstructorsNoNewConstructors for all r: Java.ClassGetConstructors t: Java.NamedType competing: Java.Constructor competingOldOwner: Java.Type do if Java.reflectiveBinding(r, t), Java.memberOrConstructorOfType(competing,competingOldOwner) then ( competing.accessibility < # public) or (Java.sub*(t, competingOldOwner)) or(not Java.sub*(t, competing.owner)) end ReflectiveClassGetConstructorsStayPublic for all r: Java.ClassGetConstructors t: Java.NamedType d: Java.Constructor do if Java.reflectiveBinding(r, t), Java.publicMemberOrConstructorOfType(d,t) then d.accessibility = #public end /* * Class#get...() returns a member declared public only; thus all * members d referenced by a get...()−invocation r must be declared * public. */ ReflectiveClassGetDeclarationStayPublic for all r: Java.ReflectiveMemberReference d: Java.MemberOrConstructor do if Java.reflectiveBinding(r, d) then d.accessibility = # public end /* * If Class#getDeclared...s() gets invoked on a type t no other * member of the same type must change its declared type to t. */ ReflectiveClassGetDeclaredFieldsNoNewFields for all r: Java.ClassGetDeclaredFields t: Java.NamedType c: Java.Field // the competing field ct: Java.NamedType // the (original) declaring type of h do if Java.reflectiveBinding(r, t), Java.memberOrConstructorOfType(c, ct), ct != t then(c.accessibility < # private) or(c.owner != t) end ReflectiveClassGetDeclaredMethodsNoNewMethods for all r: Java.ClassGetDeclaredMethods t: Java.NamedType c: Java.Method //the competing method ct: Java.NamedType //the (original) declaring type of h do if Java.reflectiveBinding(r, t), Java.memberOrConstructorOfType(c, ct), ct != t then (c.accessibility < # private) or(c.owner != t) end /* * If Class#get...(...) gets invoked on a subtype rt of the type declaring the * referenced member declaration d, there must not be any other public member * c with the same name moving in between. */ ReflectiveClassGetFieldNoHiding for all r: Java.ClassGetField // the reference to a field d: Java.Field // the referenced field rt: Java.NamedType // the receiver type c: Java.Field // the competing field do if Java.reflectiveBinding(r, d), Java.reflectiveBinding(r, rt), d != c then (d.identifier != c.identifier) or(c.accessibility < # public) or(not Java.sub(c.owner, d.owner)) or(not Java.sub*(rt,c.owner)) end B. Constraintregeln für Java /* * If Class#get...s() is invoked on a type t there must not be any other * competing public declaration of the same kind moving to t or a super * type of t. */ ReflectiveClassGetFieldsNoNewFields for all r: Java.ClassGetFields t: Java.NamedType competing: Java.Field competingOldOwner: Java.Type do if Java.reflectiveBinding(r, t), Java.memberOrConstructorOfType(competing,competingOldOwner) then ( competing.accessibility < # public) or (Java.sub*(t, competingOldOwner)) or(not Java.sub*(t, competing.owner)) end ReflectiveClassGetFieldsSetStayPublic for all r: Java.ClassGetFields t: Java.NamedType d: Java.Field do if Java.reflectiveBinding(r, t), Java.publicMemberOrConstructorOfType(d,t) then d.accessibility = #public end ReflectiveClassGetMethodNoHiding for all r: Java.ClassGetMethod // the reference to a method d: Java.Method // the referenced method rt: Java.NamedType // the receiver type c: Java.Method // the competing method do if Java.reflectiveBinding(r, d), Java.reflectiveBinding(r, rt), d != c then (d.identifier != c.identifier) or(c.accessibility < # public) or(not Java.sub(c.owner, d.owner)) or(not Java.sub*(rt, c.owner) or(d.parameters.declaredParameterType != c.parameters.declaredParameterType)) end ReflectiveClassGetMethodsNoNewMethods for all r: Java.ClassGetMethods t: Java.NamedType competing: Java.Method competingOldOwner: Java.Type do if Java.reflectiveBinding(r, t), Java.memberOrConstructorOfType(competing,competingOldOwner) then ( competing.accessibility < # public) // not accessible or (Java.sub*(t, competingOldOwner)) // or returned before or(not Java.sub*(t, competing.owner)) // or not returned after end /* * Class#get...s() returns public declarations, only; thus * all d returned by a get...s()−invocation r must be * declared as public. * * Note that three different rules are required in order to keep e.g., in * case of a Class.getMethods only the methods public. */ ReflectiveClassGetMethodsStayPublic for all r: Java.ClassGetMethods t: Java.NamedType d: Java.Method do if Java.reflectiveBinding(r, t), Java.publicMemberOrConstructorOfType(d,t) then d.accessibility = #public end /* * In case of a Class#getDeclared...(...) the referenced declaration d * must not change it's declaring type t. */ ReflectiveDeclaredMemberReferenceStayThere for all r: Java.ReflectiveDeclaredMemberReference d: Java.MemberOrConstructor t: Java.Type do if Java.reflectiveBinding(r, d), Java.memberOrConstructorOfType(d, t) then d.owner = t end /* * In case of a Class#getDeclared...s() none of the declared members d * in this type t must change its location. B. Constraintregeln für Java */ ReflectiveDeclaredMemberSetReferenceStayThere for all r: Java.ReflectiveDeclaredMemberSetReference d: Java.MemberOrConstructor t: Java.NamedType do if Java.reflectiveBinding(r, t), Java.memberOrConstructorOfType(d, t) then d.owner = t end /* * If the method getDeclaringClass() gets invoked on a declaration d, * d must not change its declaring type t. */ ReflectiveDeclaringClassReferenceStayThere for all r: Java.ReflectiveDeclaringClassReference d: Java.MemberOrConstructor t: Java.NamedType do if Java.reflectiveBinding(r, d), Java.reflectiveBinding(r, t) then d.owner = t end /** * An invocation of d.getModifiers() requires the member * declaration d to keep its original access modifier. */ ReflectiveFieldGetModifiersKeepAccessibility for all r: Java.ReflectiveAccessModifierReference d: Java.MemberOrConstructor do if Java.reflectiveBinding(r, d) then d.accessibility = r.returnedAccessModifier end /* * If there exists a reflective reference r to the name of a field * declaration d the name used in r must remain equal to the name of d. */ ReflectiveFieldNamesEquality for all r: Java.ReflectiveFieldNameReference d: Java.Field do if Java.reflectiveBinding(r, d) then r.identifier = d.identifier end ReflectiveMethodNamesEquality for all r: Java.ReflectiveMethodNameReference d: Java.Method do if Java.reflectiveBinding(r, d) then r.identifier = d.identifier end ReflectivePackageNamesEquality for all r: Java.ReflectivePackageNameReference d: Java.Package do if Java.reflectiveBinding(r, d) then r.identifier = d.identifier end ReflectiveParameterListMatching for all r: Java.ReflectiveParameterTypeListReference d: Java.MethodOrConstructor do if Java.reflectiveBinding(r,d) then r.parameters.declaredParameterType = d.parameters.declaredParameterType end /* * A quite technical rule: the host package of a reference is the host * package of the enclosing type. */ ReflectiveReferenceSamePackage for all R: Java.ReflectiveReference do if all(R) then R.hostPackage = R.owner.hostPackage end ReflectiveTypeNamesEquality for all B. Constraintregeln für Java r: Java.ReflectiveTypeNameReference d: Java.NamedType do if Java.reflectiveBinding(r, d) then r.identifier = d.identifier end /* * For expressions consisting of a field−, method− or variable reference * the type of the expression is the declared type of the referenced * field, method or variable. * * In case of field references: Rule (2) from [TFK+11] * In case of method invocations: Rule (4) from [TFK+11] * */ RegularTypedReferenceExpressionTypes for all typedReference: Java.TypedReference expression: Java.Expression typedEntity: Java.RegularTypedEntity do if Java.isExpression(typedReference, expression), Java.binds(typedReference, typedEntity) then expression.expressionType = typedEntity.declaredType end SimpleTypeParameterBinding for all typeReference: Java.TypeReference typeParameterBinding: Java.SimpleTypeParameterBinding do if Java.typeReferenceFor(typeReference, typeParameterBinding) then typeReference.typeBinding = typeParameterBinding.boundType end /* * Similar to rule (3) and (6) from [TFK+11], but for static accesses */ StaticMemberAccess for all reference: Java.TypedReference receiver: Java.TypeReferenceInType declaration: Java.OwnedEntity do if Java.staticReceiver(reference, receiver), Java.binds(reference, declaration) then Java.sub*(receiver.typeBinding,declaration.owner) end /* * § 8.4.8.2 * * A compile−time error occurs if a static method hides an instance method. */ 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.declaredParameterType = staticMethod.parameters.declaredParameterType)) or ((instanceMethod.tlowner != staticMethod.tlowner) and (instanceMethod.accessibility = #private)) or ((instanceMethod.hostPackage != staticMethod.hostPackage) and (instanceMethod.accessibility <= #package)) end /* * Rule (19) from [TFK+11] */ This for all thisReference: Java.ThisReference thisExpression: Java.Expression do if Java.isExpression(thisReference,thisExpression) then thisReference.owner = thisExpression.expressionType end /* * When accessing a static member unqualified, the receiver is the current type. */ ThisType for all thisTypeReference: Java.ImplicitThisTypeReference do if all(thisTypeReference) then thisTypeReference.typeBinding = thisTypeReference.owner end /* * Thrown objects must be throwable B. Constraintregeln für Java */ ThrowStatements for all throwsExpression: Java.Expression throwable: Java.JavaLangThrowableType do if Java.throwStatement(throwsExpression), all(throwable) then Java.sub*(throwsExpression.expressionType, throwable) end /* * § 6.7 * * By default, the top level types declared in a package are accessible only within the * compilation units of that package, but a type may be declared to be public to grant access to * the type from code in other packages (§6.6, §8.1.1, §9.1.1). */ TopLevelTypeAccessibility for all tlt: Java.TopLevelType do if all(tlt) then tlt.accessibility = #package or tlt.accessibility = #public end /* * The host package of a top level type can be inferred from the host package of it's * enclosing type root. * * The tlOwner of a top level type is the top level type itself (in order to end the recursion). */ TopLevelTypeHostPackage for all t: Java.TopLevelType do if all(t) then t.typeRoot.hostPackage = t.hostPackage, t.tlowner = t end /* * § 7.6 * * When packages are stored in a file system (§7.2.1), the host system may * choose to enforce the restriction that it is a compile−time error if a * type is not found in a file under a name composed of the type name plus * an extension (such as .java or .jav) if either of the following is true: * * − The type is referred to by code in other compilation units of the * package in which the type is declared. * − The type is declared public (and therefore is potentially accessible * from code in other packages). * * This restriction implies that there must be at most one such type per * compilation unit. */ 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 /* * Accessibility for types accesses * * See § 6.6.1 */ TypeAccess for all tr: Java.TypeReferenceInType type: Java.TlOwnedType do if all(type), all(tr) then ((tr.typeBinding = type) and (tr.tlowner != type.tlowner)) −> (type.accessibility >= #package), ((tr.typeBinding = type) and (tr.hostPackage != type.hostPackage)) −> (type.accessibility >= #protected) end /* * Accessibility of super classes and super interfaces * * See §§ 8.1.4, 8.1.5, and 9.1.3 */ TypeAccessOutsideType for all tr: Java.TypeReferenceInCU type: Java.ClassOrInterfaceType do if all(type), all(tr) then (tr.typeBinding = type) −> (type.accessibility >= #package), ((tr.typeBinding = type) and (tr.hostPackage != type.hostPackage)) −> (type.accessibility = #public) B. Constraintregeln für Java end /* * Rule (14−16) from [TFK+11] */ TypeCasts for all castType: Java.TypeReference innerExpression: Java.Expression castExpression: Java.ClassCastExpression do if Java.castExpression(castType, innerExpression, castExpression) then castExpression.expressionType = castType.typeBinding, Java.sub*(castType.typeBinding, innerExpression.expressionType) or Java.sub*(innerExpression.expressionType, castType.typeBinding) end TypeParameterAssignment for all lhs: Java.SimpleTypeParameterBinding rhs: Java.SimpleTypeParameterBinding do if Java.typeParameterAssignment(lhs,rhs) then lhs.boundType = rhs.boundType end TypeParameterAssignmentWildcards1 for all lhs: Java.UpperBoundWildcardTypeParameterBinding rhs: Java.SimpleTypeParameterBinding lhsBound: Java.TypeReference do if Java.typeParameterAssignment(lhs,rhs), Java.wildcardBound(lhs,lhsBound) then Java.sub*(rhs.boundType,lhsBound.typeBinding) end TypeParameterAssignmentWildcards2 for all lhs: Java.LowerBoundWildcardTypeParameterBinding rhs: Java.SimpleTypeParameterBinding lhsBound: Java.TypeReference do if Java.typeParameterAssignment(lhs,rhs), Java.wildcardBound(lhs,lhsBound) then Java.sub*(lhsBound.typeBinding,rhs.boundType) end TypeParameterAssignmentWildcards3 for all lhs: Java.UpperBoundWildcardTypeParameterBinding rhs: Java.UpperBoundWildcardTypeParameterBinding lhsBound: Java.TypeReference rhsBound: Java.TypeReference do if Java.typeParameterAssignment(lhs,rhs), Java.wildcardBound(lhs,lhsBound), Java.wildcardBound(rhs,rhsBound) then Java.sub*(rhsBound.typeBinding,lhsBound.typeBinding) end TypeParameterAssignmentWildcards4 for all lhs: Java.LowerBoundWildcardTypeParameterBinding rhs: Java.LowerBoundWildcardTypeParameterBinding lhsBound: Java.TypeReference rhsBound: Java.TypeReference do if Java.typeParameterAssignment(lhs,rhs), Java.wildcardBound(lhs,lhsBound), Java.wildcardBound(rhs,rhsBound) then Java.sub*(lhsBound.typeBinding,rhsBound.typeBinding) end TypeParameterAssignmentWildcards5 for all lhs: Java.UpperBoundWildcardTypeParameterBinding rhs: Java.LowerBoundWildcardTypeParameterBinding lhsBound: Java.TypeReference object: Java.JavaLangObjectType do if Java.typeParameterAssignment(lhs,rhs), Java.wildcardBound(lhs,lhsBound), all(object) then lhsBound.typeBinding = object end /* * Propagation of types from nested to enclosing expressions */ TypePropagation for all expression1: Java.Expression expression2: Java.Expression do if Java.typePropagation(expression1,expression2) B. Constraintregeln für Java then expression1.expressionType = expression2.expressionType end /* * The identifier of a type reference must match the name of the referenced type. */ TypeReferenceIdentifier for all type: Java.NamedType ref: Java.TypeReference do if all(type), all(ref) then (type = ref.typeBinding) −> (type.identifier = ref.identifier) end TypeVariableBound for all typeVariable: Java.TypeVariable typeVariableBound: Java.TypeReference typeParameterBinding: Java.SimpleTypeParameterBinding do if Java.typeVariableBound(typeVariable,typeVariableBound), Java.typeParameterBinding(typeVariable, typeParameterBinding) then Java.sub*(typeParameterBinding.boundType, typeVariableBound.typeBinding) end /* * The type of a typed entity (method, field, variable, parameter,...) * depends on the type binding of the type reference inside the typed * entity's declaration. */ TypedEntityTypes for all typeReference: Java.TypeReference typedEntity: Java.RegularTypedEntity do if Java.definesTypeOf(typeReference, typedEntity) then typeReference.typeBinding = typedEntity.declaredType end /* * § 8.3 Field Declarations * * It is a compile−time error for the body of a class declaration to * declare two fields with the same name. Methods, types, and fields * may have the same name, since they are used in different contexts * and are disambiguated by different lookup procedures (§6.5). */ UniqueFieldIdentifier for all field1: Java.Field field2: Java.Field do if field1 != field2 then field1.owner = field2.owner −>(field1.identifier != field2.identifier) end /* * § 8.4.1: * * If two formal parameters of the same method or constructor are declared to have the same name * (that is, their declarations mention the same Identifier), then a compile−time error occurs. * */ UniqueLocalVariableNames1 for all parameter1: Java.FormalParameter parameter2: Java.FormalParameter methodOrConstructor: Java.MethodOrConstructor do if Java.declaresLocalVariableOrParameter(methodOrConstructor, parameter1), Java.declaresLocalVariableOrParameter(methodOrConstructor, 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 UniqueLocalVariableNames3 for all localVariable: Java.LocalVariable do if Java.hasEnclosingVariables(localVariable) then B. Constraintregeln für Java localVariable.identifier != localVariable.enclosingLocalVariables.identifier end /* * § 8.1 Class Declaration * * A compile−time error occurs if a class has the same simple name as any * of its enclosing classes or interfaces * * § 9.1 Interface Declarations * * A compile−time error occurs if an interface has the same simple name * as any of its enclosing classes or interfaces * */ UniqueMemberTypeNames1 for all t: Java.MemberType do if all(t) then t.identifier != t.enclosingNamedTypes.identifier, t.identifier != t.tlowner.identifier end /* * The property enclosingNamedTypes does not include the type itself so we need * this rule in addition to UniqueMemberTypeNames1. */ UniqueMemberTypeNames2 for all memberType1: Java.MemberType memberType2: Java.MemberType do if memberType1 != memberType2 then (memberType1.owner = memberType2.owner) −> (memberType1.identifier != memberType2.identifier) end /* * § 8.4 Method Declarations * * It is a compile−time error for the body of a class to declare as * members two methods with override−equivalent signatures (§8.4.2) * (name, number of parameters, and types of any parameters). Methods * and fields may have the same name, since they are used in different * contexts and are disambiguated by different lookup procedures (§6.5). */ 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 /* * § 7.6 * * A compile−time error occurs if the name of a top level type appears as * the name of any other top level class or interface type declared in * the same package (§7.6). */ UniqueTopLevelTypeIdentifier for all tlt1: Java.TopLevelType tlt2: Java.TopLevelType do if tlt1 != tlt2 then (tlt1.hostPackage = tlt2.hostPackage) −> (tlt1.identifier != tlt2.identifier) end /* * References in Methods, Constructors or field initializers have the same host package, * owner and tlowner as their enclosing declarations */ UpdateReferenceHostPackage for all r: Java.PackagedReference d: Java.PackagedEntity do if Java.referenceInEntity(r, d) then r.hostPackage = d.hostPackage end UpdateReferenceOwner for all r: Java.ReferenceInType d: Java.OwnedEntity do if Java.referenceInEntity(r, d) then r.owner = d.owner end B. Constraintregeln für Java UpdateReferenceOwner2 for all r: Java.TypeReferenceInCU d: Java.PackagedEntity do if Java.referenceInEntity(r, d) then r.hostPackage = d.hostPackage end UpdateReferenceTLOwner for all r: Java.ReferenceInType d: Java.TlOwnedEntity do if Java.referenceInEntity(r, d) then r.tlowner = d.tlowner end UpperBoundWildcardAssignments for all lhs: Java.TypeVariableTypedExpression rhs: Java.Expression reference: Java.TypedReference receiverExpression: Java.Expression receiverReference: Java.TypedReference receiverDeclaration: Java.TypedEntity typeParameterBinding: Java.WildcardTypeParameterBinding object: Java.JavaLangObjectType do if Java.assignment(lhs,rhs), Java.isExpression(reference,rhs), Java.receiver(reference,receiverExpression), Java.isExpression(receiverReference,receiverExpression), Java.binds(receiverReference,receiverDeclaration), Java.typeContextOf(receiverDeclaration,typeParameterBinding), all(object) then rhs.expressionType = object end C. Refaktorisierungsdefinitionen Move Compilation Unit refactoring MoveCompilationUnit languages Java uses Accessibility, Types, Names, Locations, Reflection forced changes hostPackage of Java.TypeRoot as HostPackage allowed changes identifier of Java.PackageDeclaration hostPackage of Java.TlOwnedType hostPackage of Java.MemberOrConstructor hostPackage of Java.ReferenceInType hostPackage of Java.TypeReferenceInCU refactoring MoveCompilationUnitCA languages Java uses Accessibility, Types, Names, Locations, Reflection forced changes hostPackage of Java.TypeRoot as HostPackage allowed changes identifier of Java.PackageDeclaration hostPackage of Java.TlOwnedType hostPackage of Java.MemberOrConstructor hostPackage of Java.ReferenceInType hostPackage of Java.TypeReferenceInCU accessibility of Java.AccessibleEntity Pull Up Member refactoring PullUpMember languages Java uses Accessibility, Types, Locations, Names, Reflection forced changes owner of Java.Member as HostType tlowner of Java.Member as ToplevelType hostPackage of Java.Member as HostPackage allowed changes inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, HostType @ forced} expressionType of Java.Expression {initial, HostType @ forced} hostPackage of Java.PackagedReference {initial, HostPackage @ forced} owner of Java.ReferenceInType {initial, HostType @ forced} tlowner of Java.ReferenceInType {initial, ToplevelType @ forced} C. Refaktorisierungsdefinitionen refactoring PullUpMemberCA languages Java uses Accessibility, Types, Locations, Names, Reflection forced changes owner of Java.Member as HostType tlowner of Java.Member as ToplevelType hostPackage of Java.Member as HostPackage allowed changes inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, HostType @ forced} expressionType of Java.Expression {initial, HostType @ forced} hostPackage of Java.PackagedReference {initial, HostPackage @ forced} owner of Java.ReferenceInType {initial, HostType @ forced} tlowner of Java.ReferenceInType {initial, ToplevelType @ forced} accessibility of Java.AccessibleEntity refactoring PullUpMemberCL languages Java uses Accessibility, Types, Locations, Names, Reflection forced changes owner of Java.Member as HostType tlowner of Java.Member as ToplevelType hostPackage of Java.Member as HostPackage allowed changes inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, HostType @ forced} expressionType of Java.Expression {initial, HostType @ forced} hostPackage of Java.PackagedReference {initial, HostPackage @ forced} owner of Java.ReferenceInType {initial, HostType @ forced} tlowner of Java.ReferenceInType {initial, ToplevelType @ forced} owner of Java.Member {initial, HostType @ forced} tlowner of Java.Member {initial, ToplevelType @ forced} hostPackage of Java.Member {initial, HostPackage @ forced} refactoring PullUpMemberCACL languages Java uses Accessibility, Types, Locations, Names, Reflection forced changes owner of Java.Member as HostType tlowner of Java.Member as ToplevelType hostPackage of Java.Member as HostPackage allowed changes inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, HostType @ forced} expressionType of Java.Expression {initial, HostType @ forced} hostPackage of Java.PackagedReference {initial, HostPackage @ forced} owner of Java.ReferenceInType {initial, HostType @ forced} tlowner of Java.ReferenceInType {initial, ToplevelType @ forced} accessibility of Java.AccessibleEntity owner of Java.Member {initial, HostType @ forced} tlowner of Java.Member {initial, ToplevelType @ forced} hostPackage of Java.Member {initial, HostPackage @ forced} Rename refactoring Rename Generalize / Specialize Declared Type languages Java uses Accessibility, Types, Names, Locations, Reflection forced changes identifier of Java.NamedEntity as NewName allowed changes identifier of Java.NamedReference {initial, NewName @ forced} identifier of Java.Constructor {initial, NewName @ forced} identifier of Java.Method {initial, NewName @ forced} identifier of Java.ImportDeclaration {initial, NewName @ forced} identifier of Java.FormalParameter {initial, NewName @ forced} identifier of Java.MemberType {initial, NewName @ forced} identifier of Java.TypeRoot {initial, NewName @ forced} identifier of Java.TopLevelType {initial, NewName @ forced} Generalize / Specialize Declared Type refactoring ChangeDeclaredType languages Java uses Accessibility, Types, Names, Locations, Reflection forced changes typeBinding of Java.TypeReference as NewType allowed changes identifier of Java.TypeReference declaredType of Java.RegularTypedEntity {initial, NewType @ forced} expressionType of Java.Expression {initial, NewType @ forced} inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, NewType @ forced} refactoring ChangeDeclaredTypeCA languages Java uses Accessibility, Types, Names, Locations, Reflection forced changes typeBinding of Java.TypeReference as NewType allowed changes identifier of Java.TypeReference declaredType of Java.RegularTypedEntity {initial, NewType @ forced} expressionType of Java.Expression {initial, NewType @ forced} inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, NewType @ forced} accessibility of Java.AccessModifiableType accessibility of Java.DefaultConstructor refactoring ChangeDeclaredTypeCT languages Java uses Accessibility, Types, Names, Locations, Reflection forced changes typeBinding of Java.TypeReference as NewType allowed changes identifier of Java.TypeReference declaredType of Java.RegularTypedEntity {initial, NewType @ forced} declaredParameterType of Java.FormalParameter {initial, NewType @ forced} expressionType of Java.Expression {initial, NewType @ forced} inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, NewType @ forced} typeBinding of Java.TypeReference {initial, NewType @ forced} C. Refaktorisierungsdefinitionen refactoring ChangeDeclaredTypeCACT languages Java uses Accessibility, Types, Names, Locations, Reflection forced changes typeBinding of Java.TypeReference as NewType allowed changes identifier of Java.TypeReference declaredType of Java.RegularTypedEntity {initial, NewType @ forced} declaredParameterType of Java.FormalParameter {initial, NewType @ forced} expressionType of Java.Expression {initial, NewType @ forced} inferredClassOrInterfaceType of Java.ClassOrInterfaceTypedExpression {initial, NewType @ forced} typeBinding of Java.TypeReference {initial, NewType @ forced} accessibility of Java.AccessModifiableType accessibility of Java.DefaultConstructor Change Accessibility refactoring ChangeAccessibility languages Java uses Accessibility, Types, Locations, Names, Reflection, Reflection forced changes accessibility of Java.AccessibleEntity as Accessibility allowed changes accessibility of Java.DefaultConstructor accessibility of Java.AnonymousClassConstructor accessibility of Java.TlOwnedArrayType refactoring ChangeMethodAccessibility languages Java uses Accessibility, Types, Locations, Names, Reflection forced changes accessibility of Java.Method as Accessibility allowed changes accessibility of Java.Method accessibility of Java.DefaultConstructor accessibility of Java.AnonymousClassConstructor accessibility of Java.TlOwnedArrayType Literaturverzeichnis [ASU ] Alfred V. Aho, Ravi Sethi, and Jeffrey D. Ullman. Compilers, principles, techniques, and tools. Addison-Wesley Pub. Co., Reading and Mass, . [BA ] Timothy A. Budd and Dana Angluin. Two notions of correctness and their relation to testing. Acta Informatica (Springer), ( ): – , . [Bär ] Robert Bär. Mutantengenerierung durch Typ-Constraints. Bachelorarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/md/content/ ps/bachelorarbeit-baer.pdf, . [BCM ] Stephen M. Blackburn, Perry Cheng, and Kathryn S. McKinley. Myths and realities: the performance impact of garbage collection. ACM SIGMETRICS Performance Evaluation Review, ( ): – , . [BCR ] Victor Basili, Gianluigi Caldiera, and Dieter H. Rombach. The goal question metric approach. In Encyclopedia of Software Engineering, pages – . Wiley, . [Bec ] Kent Beck. Extreme programming eXplained: Embrace change. XP series. AddisonWesley, Reading and MA, . [BFS ] Markus Bach, Florian Forster, and Friedrich Steimann. Declared Type Generalization Checker: An Eclipse plug-in for systematic programming with more general types. In Proceedings of the ᵗ International Conference on Fundamental Approaches to Software Engineering. FASE’ , pages – . Springer, . [BG ] Chi a Baral and Michael Gelfond. Logic Programming and Knowledge Representation. Journal of Logic Programming, : – , . [BG ] Joshua Bloch and Neal Gafter. Java puzzlers: Traps, pitfalls, and corner cases. Addison-Wesley, Upper Saddle River and NJ, . [BGH+ ] Stephen M. Blackburn, Robin Garner, Chris Hoffmann, Asjad M. Khang, Kathryn S. McKinley, Rotem Ben ur, Amer Diwan, Daniel Feinberg, Daniel Frampton, Samuel Z. Guyer, Martin Hirzel, Antony Hosking, Maria Jump, Han Lee, J. Eliot B. Moss, Aashish Phansalkar, Darko Stefanović, Thomas VanDrunen, Daniel von Dincklage, and Ben Wiedermann. The DaCapo Benchmarks: Java Benchmarking Development and Analysis. In Proceedings of the ᵗ Conference on Object-Oriented Programming, systems, languages, and applications. OOPSLA’ , pages – . ACM, . Literaturverzeichnis [BGS ] [BSS+ ] [BTF [CC Philipp Bouillon, Eric Großkinsky, and Friedrich Steimann. Controlling Accessibility in Agile Projects with the Access Modifier Modifier. In Proceedings of the ᵗ International Conference on Technology of Object-Oriented Languages and Systems (TOOLS EUROPE’ ), pages – . Springer, . Eric Bodden, Andreas Sewe, Jan Sinschek, Hela Oueslati, and Mira Mezini. Taming reflection: Aiding static analysis in the presence of reflection and custom class loaders. In Proceedings of the ᵈ International Conference on Software Engineering. ICSE ’ , ICSE ’ , pages – , New York and NY and USA, . ACM. ] I ai Balaban, Frank Tip, and Robert M. Fuhrer. Refactoring Support for Class Library Migration. In Proceedings of the ᵗ Conference on Object-Oriented Programming, systems, languages, and applications. OOPSLA’ , pages – . ACM, . ] [CMS Björn Carlson and Mats Carlsson. Compiling and Executing Disjunctions of Finite Domain Constraints. In Proceedings of the ᵗ International Conference on Logic Programming. ICLP’ , pages – . MIT Press, . ] [CRTR [CW Aske Simon Christensen, Anders Møller, and Michael I. Schwar bach. Precise Analysis of String Expressions. In Proceedings of the ᵗ International Conference on Static Analysis. SAS ’ , pages – . Springer-Verlag, . ] ] Oscar Callaú, Romain Robbes, Éric Tanter, and David Röthlisberger. How Developers Use the Dynamic Features of Programming Languages: The Case of Smalltalk. In Proceedings of the ᵗ Working Conference on Mining Software Repositories. MSR ’ , pages – . ACM, . Luca Cardelli and Peter Wegner. On understanding types, data abstraction, and polymorphism. ACM Computing Surveys, ( ): – , . [DDGM ] Bre Daniel, Danny Dig, Kely Garcia, and Darko Marinov. Automated testing of refactoring engines. In Proceedings of the ᵗ joint meeting of the European Software Engineering Conference and the ACM SIGSOFT Symposium on the Foundations of Software Engineering. ESEC/FSE ’ , pages – . ACM, . [DE ] Sophia Drossopoulou and Susan Eisenbach. Java is type safe — Probably. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages – , . [Dij ] Edsger Wybe Dijkstra. A discipline of programming. Prentice-Hall series in automatic computation. Prentice-Hall, Englewood Cliffs and N.J, . [DKTE ] Alan Donovan, Adam Kiežun, Ma hew S. Tschan , and Michael D. Ernst. Converting java programs to use generic libraries. In Proceedings of the ᵗ Conference on Object-oriented programing, systems, languages, and applications. OOPSLA’ , pages – , New York and NY and USA, . ACM. Literaturverzeichnis [DKWB ] Martin Drozdz, Derrick G. Kourie, Bruce W. Watson, and Andrew Boake. Refactoring Tools and Complementary Techniques. In Proceedings of the ᵗ ACS/IEEE International Conference on Computer Systems and Applications (AICCSA ’ ), pages – , . [DRW ] Premkumar T. Devanbu, David S. Rosenblum, and Alexander L. Wolf. Generating testing and analysis tools with Aria. ACM Transactions on Software Engineering and Methodology (TOSEM), ( ): – , . [EAJM Eric Bodden, Andreas Sewe, Jan Sinschek, and Mira Mezini. Taming Reflection: Static Analysis in the Presence of Reflection and Custom Class Loaders. Technical Report TUD-CS, CASED, . ] [Eck ] Bruce Eckel. Thinking in Java. Prentice Hall, edition, [ECL The Eclipse IDE, Juno Release . . : http://www.eclipse.org/, ] . . [EH ] Torbjörn Ekman and Görel Hedin. The JastAdd Extensible Java Compiler. In Proceedings of the ᵈ Conference on Object-Oriented Programming, systems, languages, and applications. OOPSLA’ , pages – , New York and NY and USA, . ACM. [EH ] Osama El Hosami. Implementierung eines Eclipse-Plugins zum automatisierten Testen von Refaktorisierungswerkzeugen. Masterarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/md/content/ps/ . masterarbeit-el-hosami.pdf, [FF ] Ira R. Forman and Nate Forman. Java Reflection In Action. Manning and Pearson Education, Greenwich and Conn and London, . [FJ ] B. Foote and Ralph E. Johnson. Reflective Facilities in Smalltalk- . In Proceedings of the ᵗ Conference on Object-Oriented Programming, systems, languages, and applications. OOPSLA’ , pages – . ACM, . [FMM+ [Fow [Fre ] Asger Feldthaus, Todd Millstein, Anders Møller, Max Schäfer, and Frank Tip. Tool-supported Refactoring for JavaScript. In Proceedings of the ᵗ Conference on Object-oriented programing, systems, languages, and applications. OOPSLA’ , pages – , . ] Martin Fowler. Refactoring: Improving the Design of Existing Code. Addison-Wesley, . ] [FTK+ Sarah Frenkel. Implementierung einer Zurückschreibekomponente für Constraintbasierte Refaktorisierungen. Bachelorarbeit, FernUniversität in Hagen, http://www. fernuni-hagen.de/imperia/md/content/ps/bachelorarbeit-frenkel.pdf, . ] Robert M. Fuhrer, Frank Tip, Adam Kiezun, Julian Dolby, and Markus Keller. Efficiently Refactoring Java Applications to Use Generic Libraries. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages – . Springer, . Literaturverzeichnis [FYBT ] [GBE Francisco Javier Pérez, Yania Crespo, Berthold Hoffmann, and Tom Mens. A case study to evaluate the suitability of graph transformation tools for program refactoring. International Journal on Software Tools for Technology Transfer, ( ): – , . ] Andy Georges, Dries Buytaert, and Lieven Eeckhout. Statistically Rigorous Java Performance Evaluation. In Proceedings of the ᵈ Conference on Object-Oriented Programming, systems, languages, and applications. OOPSLA’ , pages – , New York and NY and USA, . ACM. [GBO+ ] Milos Gligoric, Farnaz Behrang, Jeffrey Overbey, Munawar Hafiz, and Darko Marinov. Systematic Testing of Refactoring Engines on Real Software Projects. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages – . Springer, . [GGM ] Georg Go lob, Gianluigi Greco, and Toni Mancini. Conditional Constraint Satisfaction: Logical Foundations and Complexity. In Proceedings of the ᵗ International Joint Conference on Artifical Intelligence. IJCAI’ , pages – , San Francisco, . Morgan Kaufmann Publishers Inc. [GJSB ] James Gosling, Bill Joy, Guy Steele, and Gilad Bracha. The Java™ Language Specification. Addison-Wesley, ᵈ edition, . [GR ] Adele Goldberg and David Robson. Smalltalktation. Addison-Wesley, . [Gri ] William G. Griswold. Program Restructuring as an Aid to Software Maintenance. PhD thesis, University of Washington, . [Gro ] Markus Grothoff. Mutation Testing mit Refacola. Bachelorarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/md/content/ps/ bachelorarbeit-grothoff.pdf, . [GVG ] : The Language and its Implemen- Dayong Gu, Clark Verbrugge, and Etienne M. Gagnon. Relative factors in performance analysis of Java virtual machines. In Proceedings of the ᵈ International Conference on Virtual execution environments. VEE’ , pages – . ACM, . [Hec ] Reiko Heckel. Graph Transformation in a Nutshell. Electronic Notes in Theoretical Computer Science, ( ): – , . [Hoa ] Charles A. R. Hoare. An Axiomatic Basis for Computer Programming. Communications of the ACM, ( ): – , . [How [HWG ] William E. Howden. Weak Mutation Testing and Completeness of Test Sets. IEEE Transactions on Software Engineering, ( ): – , . ] Anders Hejlsberg, Sco Wiltamuth, and Peter Golde. C# Language Specification. Addison-Wesley, . Literaturverzeichnis [IJI [Ikk ] IntelliJ IDEA Community Edition . ] [JDT . . : http://www.jetbrains.com/idea/, Sergei Ikkert. Untersuchung der Eclipse-JDT-Refaktorisierungen mit Hilfe des Refactoring Tool Testers. Masterarbeit, FernUniversität in Hagen, http://www. fernuni-hagen.de/imperia/md/content/ps/masterarbeit-ikkert.pdf, . The Eclipse Java Development Tools, Juno Release: http://www.eclipse.org/ jdt/, . ] [JH ] Yue Jia and Mark Harman. An Analysis and Survey of the Development of Mutation Testing. IEEE Transactions on Software Engineering, ( ): – , . [Jia ] Yue Jia. Mutation Testing Repository – Publications on Mutation Testing: http: //www.dcs.kcl.ac.uk/pg/jiayue/repository/, . [JRL ] Narendra Jussien, Guillaume Rochart, and Xavier Lorca. The CHOCO constraint programming solver. In Workshop on Open-Source Software for Integer and Constraint Programming.OSSIP’ , . [Jun ] Ulrich Junker. QUICKXPLAIN: preferred explanations and relaxations for overconstrained problems. In Proceedings of the ᵗ national conference on Artifical intelligence. AAAI’ , pages – , . [KETF ] Adam Kiezun, Michael D. Ernst, Frank Tip, and Robert M. Fuhrer. Refactoring for Parameterizing Java Classes. In Proceedings of the ᵗ International Conference on Software Engineering. ICSE ’ , pages – . IEEE Computer Society, . [KH ] Guido Krüger and Heiko Hansen. Handbuch der Java-Programmierung. AddisonWesley, München, edition, . [KK Günter Kniesel and Helge Koch. Static Composition of Refactorings. Science of Computer Programming - Special issue on program transformation, ( – ): – , . ] [KKP+ ] [KN ] [Knu David J. Kuck, Robert H. Kuhn, David A. Padua, Bruce Leasure, and Michael J. Wolfe. Dependence Graphs and Compiler Optimizations. In Proceedings of the ᵗ Symposium on Principles of Programming Languages. POPL ’ , pages – , . Gerwin Klein and Tobias Nipkow. A machine-checked model for a Java-like language, virtual machine, and compiler. Transactions on Programming Languages and Systems (TOPLAS), ( ): – , . ] [Kof ] Donald E. Knuth. Semantics of context-free languages. Mathematical systems theory, ( ): – , . Jürgen Kofler. Verbinden eines textuellen und eines graphischen Editors bei modellgetriebener Entwicklung am Beispiel der Sprache Refacola. Diplomarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/ps/arbeiten/kofler.shtml, . Literaturverzeichnis [Kre ] [KS Marius Kreis. Systematisches Testen von Constraintregeln. Masterarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/md/content/ ps/masterarbeit-kreis.pdf, . ] Hannes Kegel and Friedrich Steimann. Systematically refactoring inheritance to delegation in java. In Proceedings of the ᵗ International Conference on Software Engineering. ICSE ’ , pages – . ACM, . [Kum ] Vipin Kumar. Algorithms for Constraint Satisfaction Problems: A Survey. AI Magazine, ( ): – , . [LHR ] Karl J. Lieberherr, Ian M. Holland, and Arthur J. Riel. Object-oriented programming: an objective sense of style. In Proceedings of the ᵈ Conference on Object-Oriented Programming, systems, languages, and applications. OOPSLA’ , pages – . ACM, . [LWL ] Benjamin Livshits, John Whaley, and Monica S. Lam. Reflection Analysis for Java. In Proceedings of the ᵈ Asian Conference on Programming Languages and Systems. APLAS ’ , pages – . Springer-Verlag, . [LY ] Tim Lindholm and Frank Yellin. The Java Virtual Machine Specification. The Java series. Addison-Wesley, Reading and MA, edition, . [Mai ] André Mainka. Typconstraints für generische Typen in Refacola. Diplomarbeit, FernUniversität in Hagen, . [Mat ] Aditya Mathur. Mutation Testing. In Encyclopedia of Software Engineering, volume , pages – . Wiley, . [McC ] Glen McCluskey. Using Java Reflection: http://www.oracle.com/technetwork/ . articles/java/javareflection-1536171.html, [MDJ ] Tom Mens, Serge Demeyer, and Dirk Janssens. Formalising Behaviour Preserving Program Transformations. In Andrea Corradini, Hartmut Ehrig, Hans-Jörg Kreowski, and Grzegorz Rozenberg, editors, Graph Transformation, volume of Lecture Notes in Computer Science, pages – . Springer Berlin Heidelberg, . [MF ] Sanjay Mi al and Brian Falkenhainer. Dynamic Constraint Satisfaction Problems. In Proceedings of the ᵗ National Conference on Artificial Intelligence. AAAI’ , pages – , . [MHPB ] Emerson Murphy-Hill, Chris Parnin, and Andrew P. Black. How we refactor, and how we know it. In Proceedings of the ᵗ International Conference on Software Engineering. ICSE ’ , pages – . IEEE, . [MT ] Tom Mens and Tom Tourwé. A Survey of Software Refactoring. IEEE Transactions on Software Engineering, ( ): – , . Literaturverzeichnis [MTR ] Tom Mens, Gabriele Taen er, and Olga Runge. Analysing refactoring dependencies using graph transformation. Software & Systems Modeling, ( ): – , . [Mül ] Andreas Müller. Bytecode Analysis for Checking Java Access Modifiers. In Proceedings of the Work-in-Progress Session at the ᵗ International Conference on the Principles and Practice of Programming in Java (PPPJ’ ), . [Mül ] Erland Müller. Ein Algorithmus zur systematischen Constrainterzeugung für die constraintbasierte Refaktorisierung. Diplomarbeit, FernUniversität in Hagen, http:// www.fernuni-hagen.de/imperia/md/content/ps/masterarbeit-mueller.pdf, . [MvEDJ ] Tom Mens, Niels van Eetvelde, Serge Demeyer, and Dirk Janssens. Formalizing Refactorings with Graph Transformations. Journal of Software Maintenance and Evolution: Research and Practice, ( ): – , . [Nao ] Naoyuki Tamura. Cream: Class Library for Constraint Programming in Java: http://bach.istc.kobe-u.ac.jp/cream/, . [NBE ] NetBeans IDE . : http://netbeans.org/, . [NHN ] Flemming Nielson, Chris Hankin, and Hanne Nielson. Principles of program analysis: With tables. Springer, Berlin [u.a.], . [Nic ] Sven Nicolai. Postprocessing zur Qualifizierung von Namen: als Ergänzung des constraintbasierten Refaktorisierungsframeworks REFACOLA für die Programmiersprache Java. Diplomarbeit, FernUniversität in Hagen, . [NO ] Tobias Nipkow and David von Oheimb. Javalight is type-safe—definitely. In Proceedings of the ᵗ Symposium on Principles of Programming Languages. POPL ’ , pages – . ACM, . [Ohe ] David von Oheimb. Analyzing Java in Isabelle/HOL: Formalization, Type Safety and Hoare Logic. Dissertation, Technische Universität München, . [ON ] David Oheimb and Tobias Nipkow. Machine-Checking the Java Specification: Proving Type-Safety. In Jim Alves-Foss, editor, Formal Syntax and Semantics of Java, volume of Lecture Notes in Computer Science, pages – . Springer Berlin Heidelberg, . [Opd ] William F. Opdyke. Refactoring object-oriented frameworks. Dissertation, University of Illinois at Urbana-Champaign, . [Pal ] Jens Palsberg. Closure Analysis in Constraint Form. Transactions on Programming Languages and Systems (TOPLAS), ( ): – , . [PS ] Jens Palsberg and Michael I. Schwar bach. Object-oriented type systems. Wiley, . Literaturverzeichnis [PUTS ] Jens von Pilgrim, Bastian Ulke, Andreas Thies, and Friedrich Steimann. Model/Code Co-Refactoring: An MDE Approach. In Proceedings of the ᵗ International Conference on Automated Software Engineering. ASE’ , pages – . ACM, . [RBJ ] Don Roberts, John Brant, and Ralph Johnson. A Refactoring Tool for Smalltalk. Theory and Practice of Object Systems, ( ): – , . [REF ] The Reflection API: http://docs.oracle.com/javase/tutorial/reflect/ index.html, . [RH ] Soroush Radpour and Laurie Hendren. Refactoring MATLAB. Technical Report SABLE-TR- , Sable Research Group, School of Computer Science, McGill University, Montréal and Québec and Canada, . [RHS ] Soroush Radpour, Laurie Hendren, and Max Schäfer. Refactoring MATLAB. In Compiler Construction, volume of Lecture Notes in Computer Science, pages – . Springer Berlin Heidelberg, . [Rob ] Donald B. Roberts. Practical analysis for refactoring. Dissertation, University of Illinois at Urbana-Champaign, . [Rok ] Sosic Rok. The Many Faces of Introspection. Dissertation, University of Utah, [Roz ] Grzegorz Rozenberg. Handbook of graph grammars and computing by graph transformation. World Scientific, Singapore and New Jersey, . [Sch ] Norbert Schirmer. Analysing the Java package-access concepts in Isabelle-HOL: Research Articles. Concurrency and Computation: Practice & Experience - Formal Techniques for Java-like Programs, ( ): – , . [Sch ] Max Schäfer. Specification, Implementation and Verification of Refactorings. Dissertation, Oxford University, . [SDS+ ] . Max Schäfer, Julian Dolby, Manu Sridharan, Emina Torlak, and Frank Tip. Correct refactoring of concurrent java code. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages – . Springer, . [SEM ] Max Schäfer, Torbjörn Ekman, and Oege de Moor. Sound and Extensible Renaming for Java. In Proceedings of the ᵈ Conference on Object-oriented Programming Systems Languages and Applications (OOPSLA’ ), pages – , . [SGM ] Gustavo Soares, Rohit Gheyi, and Tiago Massoni. Automated Behavioral Testing of Refactoring Engines. IEEE Transactions on Software Engineering, ( ): – , . [SKP ] Friedrich Steimann, Christian Kollee, and Jens von Pilgrim. A Refactoring Constraint Language and its Application to Eiffel. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages – . SpringerVerlag, . Literaturverzeichnis [SM ] Friedrich Steimann and Philip Mayer. Type Access Analysis: Towards Informed Interface Design. Journal of Object Technology, ( ): – , . [SM ] Max Schäfer and Oege de Moor. Specifying and implementing refactorings. In Proceedings of the ᵗ Conference on Object-oriented programing, systems, languages, and applications. OOPSLA’ , pages – . ACM, . [SMG ] Gustavo Soares, Melina Mongiovi, and Rohit Gheyi. Identifying overly strong conditions in refactoring implementations. In Proceedings of the ᵗ Conference on Software Maintenance. ICSM ’ , pages – . IEEE, . [SP ] Friedrich Steimann and Jens von Pilgrim. Refactorings without names. Technical Report (unpublished), FernUniversität in Hagen, . [SP a] Friedrich Steimann and Jens von Pilgrim. Constraint-Based Refactoring with Foresight. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages – . Springer, . [SP b] Friedrich Steimann and Jens von Pilgrim. Refactorings without names. In Proceedings of the ᵗ International Conference on Automated Software Engineering. ASE’ , pages – . ACM, . [Spe ] Hermine Spengler. Constraintbasiertes Refaktorisieren von Identifiern in Java. Bachelorarbeit, FernUniversität in Hagen, . [SR Jason Sawin and Atanas Rountev. Improving Static Resolution of Dynamic Class Loading in Java Using Dynamically Gathered Environment Information. Automated Software Engineering, ( ): – , . ] [SRK ] [SSA+ Daniel Speicher, Tobias Rho, and Günter Kniesel. JTransformer - Eine logikbasierte Infrastruktur zur Codeanalyse. Softwaretechnik-Trends, ( ), . ] Hesam Samimi, Max Schäfer, Shay Ar i, Todd Millstein, Frank Tip, and Laurie Hendren. Automated Repair of HTML Generation Errors in PHP Applications Using String Constraint Solving. In Proceedings of the ᵗ International Conference on Software Engineering. ICSE ’ , pages – , . [ST ] Friedrich Steimann and Andreas Thies. From Public to Private to Absent: Refactoring Java Programs under Constrained Accessibility. In Proceedings of the ᵈ European Conference on Object-Oriented Programming. ECOOP ’ , pages – . Springer, . [ST ] Friedrich Steimann and Andreas Thies. From behaviour preservation to behaviour modification: Constraint-based mutant generation. In Proceedings of the ᵈ International Conference on Software Engineering. ICSE ’ , pages – . ACM, . [STD ] Bjorn de Su er, Frank Tip, and Julian Dolby. Customization of Java Library Classes using Type Constraints and Profile Information. In Proceedings of the ᵗ European Conference on Object-Oriented Programming. ECOOP ’ , pages – , . Literaturverzeichnis [Ste ] Friedrich Steimann. The Infer Type Refactoring and its Use for Interface-Based Programming. Journal of Object Technology, ( ): – , . [Ste ] Friedrich Steimann. Constraint-Based Model Refactoring. In Proceedings of the ᵗ International Conference on Model Driven Engineering Languages and Systems. MODELS’ , pages – . Springer, . [Ste ] Friedrich Steimann. From well-formedness to meaning preservation: model refactoring for almost free. Software & Systems Modeling, to appear, . [Sto ] Frieder Stolzenburg. Membership-Constraints and Complexity in Logic Programming with Sets. In Frontiers of Combining Systems. FroCoS ’ , volume , pages – . Springer Netherlands, . [STST [SU ] Max Schäfer, Andreas Thies, Friedrich Steimann, and Frank Tip. A Comprehensive Approach to Naming and Accessibility in Refactoring Java Programs. IEEE Transactions on Software Engineering, ( ): – , . ] Friedrich Steimann and Bastian Ulke. Generic Model Assist. In Proceedings of the ᵗ International Conference on Model Driven Engineering Languages and Systems. MODELS’ , volume to appear, . [SVEM ] Max Schäfer, Mathieu Verbaere, Torbjörn Ekman, and Oege de Moor. Stepping Stones over the Refactoring Rubicon. In Proceedings of the ᵈ European Conference on Object-Oriented Programming. ECOOP ’ , pages – . Springer, . [TB ] Andreas Thies and Eric Bodden. RefaFlex: Safer Refactorings for Reflective Java Programs. In Proceedings of the International Symposium on Software Testing and Analysis, ISSTA , pages – , New York and NY and USA, . ACM. [Tei ] Warren Teitelman. Automated programmering: the programmer’s assistant. In Proceedings of the December - , , fall joint computer conference, part II (AFIPS ’ ), pages – , New York and NY and USA, . ACM. [TFK+ [Tip ] ] [TKB [TR Frank Tip. Refactoring Using Type Constraints. In Hanne Riis Nielson and Gilberto Filé, editors, Proceedings of the ᵗ International Symposium on Static Analysis. SAS’ , volume of Lecture Notes in Computer Science, pages – . Springer, . ] ] Frank Tip, Robert M. Fuhrer, Adam Kieżun, Michael D. Ernst, I ai Balaban, and Bjorn de Su er. Refactoring using type constraints. Transactions on Programming Languages and Systems (TOPLAS), ( ): : – : , . Frank Tip, Adam Kiezun, and Dirk Bäumer. Refactoring for generalization using type constraints. In Proceedings of the ᵗ Conference on Object-oriented programing, systems, languages, and applications. OOPSLA’ , pages – . ACM, . Andreas Thies and Christian Roth. Recommending Rename Refactorings. In Proceedings of the ᵈ International Workshop on Recommendation Systems for Software Engineering (RSSE ’ ), pages – , New York and NY and USA, . ACM. Literaturverzeichnis [TS [Tsa ] Andreas Thies and Friedrich Steimann. Systematic Testing of Refactoring Tools. In Poster exhibition at the ᵗ Workshop on Automation of Software Test. AST’ , . ] Edward Tsang. Foundations of Constraint Satisfaction. Academic Press, . [Ull ] Christian Ullenboom. Java ist auch eine Insel: Das umfassende Handbuch. Galileo Press, edition, . [VCN+ ] Mohsen Vakilian, Nicholas Chen, Stas Negara, Balaji A. Rajkumar, Brian P. Bailey, and Ralph E. Johnson. Use, Disuse, and Misuse of Automated Refactorings. In Proceedings of the ᵗ International Conference on Software Engineering. ICSE ’ , pages – , . [vHSD ] [VKDL ] Angi Voß, Werner Karbach, Uwe Drouven, and Darius Lorek. Competence Assessment in Configuration Tasks. In European Conference on Artificial Intelligence. (ECAI’ ), pages – , . Pascal van Hentenryck, Helmut Simonis, and Mehmet Dincbas. Constraint Satisfaction Using Constraint Logic Programming. Artificial Intelligence, ( ): – , . [Wag ] Mirko Wagner. Sprachübergreifendes Java-XML-Refaktorisieren mit Refacola. Bachelorarbeit, FernUniversität in Hagen, http://www.fernuni-hagen.de/imperia/ . md/content/ps/bachelorarbeit-wagner.pdf, [Wil ] David S. Wile. Abstract syntax from concrete syntax. In Proceedings of the ᵗ International Conference on Software Engineering. ICSE ’ , pages – . ACM, . [WSTL ] Jan Wielemaker, Tom Schrijvers, Markus Triska, and Torbjörn Lager. SWI-Prolog. Theory and Practice of Logic Programming, ( - ): – , . Index private, protected, public, Abfrage-Engine, Abhängigkeiten, , , , abstract semantic graph, siehe abstrakter Semantikgraph abstract syntax tree, siehe abstrakter Syntaxbaum abstrakter Semantikgraph, abstrakter Syntaxbaum, access modifier, siehe Zugrei arkeitsmodifizierer allowed changes, siehe erlaubte Änderungen ambigous, siehe Mehrdeutigkeit angereicherter abstrakter Syntaxbaum, , Annahme zur Weltabgeschlossenheit, Art, AST, siehe abstrakter Syntaxbaum Aufzählungsdomäne, , geordnete, partiell geordnete, ungeordnete, Ausdruck, Ausnahme, Bedingung, , Bedingungserfüllungsproblem, , dynamisches, endliches, beschränkt parametrischer Polymorphismus, bounded polymorphism, siehe beschränkt parametrischer Polymorphismus bounded type variables, siehe Schranken von Typvariablen bounded wildcards, siehe Schranken von Wildcards CA, – CACL, CACT, CI, CL, Class Instance Creation Expression, , class literal, closed world assumption, siehe Annahme zur Weltabgeschlossenheit constant evaluation, siehe konstante Auswertung Constraint bedingtes, Constraint, siehe Bedingung constraint, siehe Bedingung constraint generation rule, siehe Constraintregel constraint rule, siehe Constraintregel constraint satisfaction problem, siehe Bedingungserfüllungsproblem constraint solver, siehe Constraintlöser constraint variable, siehe Constraintvariable Constraintlöser, Constraintlösung, Constraintregel, , Constraintsystem, siehe Bedingungserfüllungsproblem Constraintvariable, , , Constructor Invocation, CSP, siehe Bedingungserfüllungsproblem CT, Index default-Zugrei arkeit, dependencies, siehe Abhängigkeiten domain, siehe Domäne Domäne, , early evaluation, siehe frühzeitige Auswertung Einschränkungen Übermäßig starke, , enumeration domain, siehe Aufzählungsdomäne Erklärungskomponente, erlaubte Änderungen, , , exception, siehe Ausnahme explizite Typumwandlung, expression, siehe Ausdruck Extreme Programming, fact base, siehe Faktenbasis fact generation, siehe Faktengenerierung facts, siehe Fakten Fakten, , , Faktenart, Faktenbasis, Faktengenerierung, Faktenlieferant, foresight, siehe Vorausschau frühzeitige Auswertung, Geltungsbereich, geschachtelter Typ, , hiding, siehe Verdecken indirection, siehe Indirektion Indirektion, Inferenzregeln, initial value, siehe initialer Wert initialer Wert, Inkonsistenz, inner class, siehe innere Klasse innere Klasse, interne API, intersection type, kind, siehe Art Konklusion, , konstante Auswertung, Leerraum, , locked bindings, , Lösung, siehe Constraintlösung Mehrdeutigkeit, , member type, siehe Membertyp Membertyp, Methodenparameter mit variabler Stelligkeit, Methodenüberladung, Mutation, Mutationstesten, , Nachbarschaftssuche, Nachbedingung, Nachrichtenversand, Name kanonischer, voll qualifizierter, neighborhood search, siehe Nachbarschaftssuche nested type, siehe geschachtelter Typ – NOC, Obscuring, siehe Verdunkelung offene Domäne, overloading, siehe Überladen postcondition, siehe Nachbedingung precondition, siehe Vorbedingung primary type, siehe primärer Typ primärer Typ, program dependence graph, siehe Programmabhängigkeitsgraph program element, siehe Programmelement program-dependent domain, siehe programmabhängige Domäne Programm gültiges, , Programmabfrage, , programmabhängige Domäne, , , Programmabhängigkeitsgraph, Programmanalyse dynamische, statische, , Programmelement, , Index Programmfakten, siehe Fakten Programmierung introspektive, Programmierwerkzeug, Programmtransformation, Programmtransformationen, property, siehe Variablenart Prämisse, , subkind, siehe Unterart query, siehe Programmabfrage quick fix, , Überladen, Unterart, raw types, Refacola, , , refactoring, siehe Refaktorisierung Refactoring Browser, refactoring tool, siehe Refaktorisierungswerkzeug Refactoring Tool Tester, , Refaktorisierung, , constraintbasierte, Refaktorisierungs-Adapter, Refaktorisierungswerkzeug, reference construction, siehe Referenzkonstruktion Referenzkonstruktion, Reflection-API, , reflektive Referenz, Regelsa , Regelumschreiben, Regelvariable, , ungebundene, rule rewriting, siehe Regelumschreiben runtime exception, Rückschreibekomponente, , Variable, , unveränderlich, variable arity parameter, siehe Methodenparameter mit variabler Stelligkeit Variablenart, , Variablengenerierung, Verdecken, , , Verdecken von Feldern, Verdunkelung, Verscha en, , visibility, siehe Sichtbarkeit Vorausschau, , , Vorbedingung, Schranken von Typvariablen, Schranken von Wildcards, scope, siehe Geltungsbereich Seiteneffekt, Sequenz, , shadowing, siehe Verscha en Sichtbarkeit, Sprach-API, streng typisiert, strongly typed, siehe streng typisiert TamiFlex, Typ primär, type cast, siehe explizite Typumwandlung Typparameter, Typvariable, white spaces, siehe Leerraum Zugrei arkeit, , Zugrei arkeitsmodifizierer, Zugrei arkeitsstufe, , Lebenslauf Berufliche Tätigkeiten seit / / Anwendungsentwickler Novomind AG - / Wintersemester und Sommersemester Wissenschaftlicher Mitarbeiter FernUniversität in Hagen Lehrgebiet Programmiersysteme Studentische Hilfskraft Universität Bonn Institut für Informatik I und II Ausbildung seit / Promotionsstudium zum Dr. rer. nat. / Abschluss als Diplom-Informatiker Universität Bonn Nebenfach: Jura (Strafrecht) / – / / Studium der Informatik Universität Bonn Erwerb der allgemeinen Hochschulreife – Besuch des Gymnasiums Blankenese Freie und Hansestadt Hamburg Wehrpflicht / – / Wehrersa dienst Caritasverband Leverkusen Häusliche Krankenpflege Persönliche Angaben Geburtsdatum Geburtsort Staatsangehörigkeit . März Freie und Hansestadt Hamburg Deutsch Familienstand Verheiratet
© Copyright 2024 ExpyDoc