Luka Steinbach Dynamische Analyse von JavaScript durch Tests
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Bachelorarbeit Dynamische Analyse von JavaScript durch Tests Luka Steinbach Gutachter: Prof. Dr. Peter Thiemann Betreuer: Fernando Cristiani Albert-Ludwigs-Universität Freiburg Technische Fakultät Institut für Informatik Lehrstuhl für Programmiersprachen 16. September 2021
Bearbeitungszeit 16. 06. 2021 – 16. 09. 2021 Gutachter Prof. Dr. Peter Thiemann Betreuer Fernando Cristiani
Erklärung Hiermit erkläre ich, dass ich diese Abschlussarbeit selbständig verfasst habe, keine anderen als die angegebenen Quellen/Hilfsmittel verwendet habe und alle Stellen, die wörtlich oder sinngemäß aus veröffentlichten Schriften ent- nommen wurden, als solche kenntlich gemacht habe. Darüber hinaus erkläre ich, dass diese Abschlussarbeit nicht, auch nicht auszugsweise, bereits für eine andere Prüfung angefertigt wurde. Ort, Datum Unterschrift i
ii
Zusammenfassung TypeScript ist eine Programmiersprache, die die JavaScript-Syntax erweitert und immer mehr an Beliebtheit gewinnt. Um JavaScript-Module in Type- Script sinnvoll zu nutzen, können so genannte Declaration Files verwendet werden, die Beschreibungen zu den Typen der Funktionen und Objekte eines JavaScript-Moduls enthalten. Diese Declaration Files müssen manuell erstellt und gewartet werden, was zeitaufwendig sein und zu Fehlern führen kann. Diese Arbeit befasst sich mit der automatischen Erstellung von Declaration Files. Dafür werden Tests benutzt, die von den Entwicklern des JavaScript- Moduls geschrieben wurden. Mit diesem Ansatz wurden 1243 Declaration Files aus 7505 Modulen erzeugt, von denen 55 keine Unterschiede im Vergleich zu manuell geschriebenen Declaration Files aus dem Repository DefinitelyTyped aufwiesen. iii
iv
Inhaltsverzeichnis 1 Einleitung 1 2 Hintergrund 3 2.1 JavaScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2.1.1 NPM . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2.1.2 ECMAScript . . . . . . . . . . . . . . . . . . . . . . . 4 2.1.3 Babel . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.4 Testen in JavaScript . . . . . . . . . . . . . . . . . . . 7 2.1.5 Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 2.2 TypeScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2.1 Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.2.2 Declaration Files . . . . . . . . . . . . . . . . . . . . . 12 2.2.3 DefinitelyTyped . . . . . . . . . . . . . . . . . . . . . . 13 2.3 Jalangi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3 Ansatz 17 3.1 Problemstellung . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.2 Bisheriger Ansatz . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.3 Neuer Ansatz . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.3.1 Schritt 1: Herunterladen des Moduls . . . . . . . . . . 21 3.3.2 Schritt 2: Instrumentierung des Moduls . . . . . . . . 21 3.3.3 Schritt 3: Installation der Abhängigkeiten . . . . . . . 22 v
3.3.4 Schritt 4: Generierung der Laufzeitinformationen . . . 23 3.3.5 Schritt 5: Generierung des Declaration Files . . . . . . 24 4 Experimente 25 4.1 Untersuchung der package.json der Module . . . . . . . . . . . 25 4.2 Herunterladen der Module . . . . . . . . . . . . . . . . . . . . 27 4.3 Instrumentierung der Module . . . . . . . . . . . . . . . . . . 28 4.4 Installation von Abhängigkeiten . . . . . . . . . . . . . . . . . 29 4.5 Generierung der Laufzeitinformationen . . . . . . . . . . . . . 30 4.6 Generierung der Declaration Files . . . . . . . . . . . . . . . . 37 4.7 Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 5 Evaluation der Ergebnisse 41 5.1 Vergleich mit gemeinsamen Modulen . . . . . . . . . . . . . . 44 5.2 Vergleich mit allen Modulen . . . . . . . . . . . . . . . . . . . 45 6 Verwandte Arbeiten 47 7 Fazit 51 7.1 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Bibliography 54 A Abbildungen 59 B Tabellen 71 vi
1 Einleitung JavaScript ist eine dynamisch typisierte, interpretierte und objektorientierte Programmiersprache. Sie ist laut der Stack Overflow Entwicklerumfrage seit mehreren Jahren die weltweit am häufigsten verwendete Programmierspra- che [8, 9, 10, 11]. Ursprünglich wurde JavaScript für die Entwicklung von kleinen Skripten zur Manipulation von Elementen auf Webseiten entwickelt. Inzwischen wird JavaScript aber auch für große Anwendungen innerhalb und außerhalb des Browsers eingesetzt. Die dynamische Typisierung der Sprache kann hier die Entwicklung, durch zum Beispiel versteckte Fehler und schlechte Lesbarkeit, erschweren. Aus diesem Grund hat Microsoft die Programmiersprache TypeScript entwickelt, die JavaScript um statische Ty- pisierung erweitert. Die TypeScript-Syntax ist eine Obermenge der JavaScript-Syntax, was be- deutet, dass bestehende JavaScript-Module in TypeScript verwendet werden können. Um die JavaScript-Module mit Typen zu erweitern, können Entwick- ler sogenannte Type Declaration Files schreiben. Diese ermöglichen es zum einen eine falsche Benutzung des Moduls zu vermeiden und zum anderen den Entwickler in Entwicklungsumgebungen, durch zum Beispiel intelligentere Codenavigation und Autovervollständigung, zu unterstützen. Declaration files für über 7500 JavaScript-Module werden im GitHub-Repository Defini- telyTyped1 zur Verfügung gestellt und werden über NPM2 wöchentlich über 1 https://github.com/DefinitelyTyped/DefinitelyTyped 2 https://www.npmjs.com/ 1
100 Millionen Mal heruntergeladen und verwendet [13]. Diese Declaration Files werden in den meisten Fällen manuell geschrieben, was vor allem bei großen Modulen einen erheblichen Zeitaufwand bedeuten kann. Dies führt außerdem dazu, dass bei neuen Versionen von manchen JavaScript-Modulen erst nach längerer Zeit ein Declaration File für die Benutzung der neuen Modulversion veröffentlicht wird. Durch das manuelle Schreiben kann es zu Fehlern kommen, die bei der Aufnahme in das DefinitelyTyped-Repository übersehen werden. Zusätzlich gibt es den Nachteil, dass Unstimmigkeiten zwischen dem DeclarationFile und dem benutzten JavaScript-Modul nicht vom TypeScript-Compiler erkannt werden, was zu fehlerhaften Verhalten der Entwicklungsumgebung führen kann. Eine automatische Generierung der Declaration Files könnte einige dieser Probleme vermeiden. Ein bisheriger Ansatz für eine automatische Generierung der Declaration Files basiert auf der dynamischen Analyse von Codebeispielen aus Readme-Dateien von Modulen [17]. Auf diese Weise wurden 249 Declaration Files für 6029 DefinitelyTyped-Module erzeugt, was einem Anteil von 4,1% entspricht. In dieser Arbeit wird ein neuer Ansatz zur Generierung von Declaration Files untersucht und mit manuell geschriebenen Declaration Files aus DefinitelyTy- ped und der bisherigen Methode verglichen. Der Ansatz dieser Arbeit basiert auf der dynamischen Analyse von Tests von Modulen, die oft einen hohen Informationsgehalt bezüglich der Verwendung des Moduls enthalten, ähnlich wie Codebeispiele in Readme-Dateien von Modulen. Außerdem sind die Tests in den meisten Fällen in einem Testskript in der package.json des Moduls definiert, was einen einheitlichen Ausgangspunkt für die Tests darstellt. 2
2 Hintergrund 2.1 JavaScript JavaScript ist eine dynamisch typisierte, interpretierte und objektorientierte Programmiersprache, die es dem Nutzer ermöglicht einfache Skripte zu schrei- ben. Mittlerweile ist JavaScript die meistverwendete Programmiersprache weltweit (siehe Abbildung 24) [11]. Ursprünglich wurde JavaScript für die Verwendung in Browsern entwickelt, um die Struktur von Webseiten über das DOM (Document Object Model) zu manipulieren. Moderne Frontend- Frameworks, wie React, Angular und Vue.js, die auf JavaScript basieren, werden immer mehr verwendet, um große und dynamische Webanwendungen zu entwickeln [3]. Zusammen mit HTML und CSS bildet JavaScript die Grundlage für moderne Webanwendungen. 2.1.1 NPM Mit der JavaScript-Laufzeitumgebung Node.js1 wird JavaScript jedoch auch zunehmend außerhalb des Browsers eingesetzt. Node.js ist plattformüber- greifend und ermöglicht Entwicklern über NPM (ehemals Node Package Manager) den Zugriff auf über 1,3 Millionen Module [12]. Module können mit der NPM Command-Line-Schnittstelle (NPM CLI) über den Befehl npm install heruntergeladen und installiert werden. Jedes NPM-Modul besitzt eine package.json, die unter anderem Name, Version, Beschreibung, Autor, 1 https://nodejs.org/ 3
Softwareabhängigkeiten und Skripte des Moduls definiert (siehe Abbildung 1). Scripte können vom Nutzer mit dem Befehl npm run ausge- führt werden und geben dem Entwickler die Möglichkeit komplexere Routinen für den Benutzer leichter ausführbar zu machen. Häufige Anwendungsbeispie- le sind Build-Scripte zur Kompilierung des Sourcecodes oder Testskripte zur Ausführung der vom Entwicklern definierten Tests. { "name": "beispiel-modul", "description": "", "version": "1.0.0", "scripts": { "test": "eslint && mocha test" }, "repository": { "type": "git", "url": "https://github.com/beispiel-person/beispiel-modul.git" }, "keywords": [], "author": "https://github.com/beispiel-person", "license": "MIT", "bugs": { "url": "https://github.com/beispiel-person/beispiel-modul/issues" }, "homepage": "https://github.com/beispiel-person/beispiel-modul" } Abbildung 1: Beispiel für eine package.json - Die package.json de- finiert verschiedene Informationen eines Moduls. Für diese Arbeit relevante Informationen sind der Name (name), die Version (version), das Testskript (scripts.test) und die Repository-URL (repository.url). 2.1.2 ECMAScript Den Sprachkern von JavaScript bildet das sognannte ECMAScript, das in der ECMAScript Language Specification [2] definiert wird und die Syntax und Semantik der Sprache standardisiert definiert. Einzelne Versionen des ECMA- Script werden mit dem jeweiligen Veröffentlichungsjahr bezeichnet, so gibt es zum Beispiel die ECMAScript-Versionen ECMAScript 2015 (ES2015 oder 4
ES6) und ECMAScript 2016 (ES2016). Jede neue Version des ECMAScripts erweitert den bestehenden Sprachkern um neue Funktionen wie zum Beispiel beim ECMAScript 2015 die unterstützte Nutzung der JavaScript Object Notation (JSON) oder im ECMAScript 2017 die Ergänzung von asynchronen Funktionen [6]. 2.1.3 Babel Aufgrund der vielen verschiedenen Versionen von ECMAScript kann es zu Problemen mit der Cross-Browser-Kompatibilität von JavaScript kommen. Eine häufige Ursache ist die Verwendung von modernen JavaScript-Funktionen in älteren Browsern oder älteren Browserversionen, die die neuen Versionen und damit die neuen JavaScript-Funktionen nicht unterstützen. Moderne JavaScript-Funktionen, die oft zu solchen Problemen führen, sind zum Beispiel Arrow-Funktionen und Template-Strings [15]. Um die Cross-Browser-Kompatibilität für den Benutzer zu gewährleisten, verwenden Entwickler oft das Tool Babel, das JavaScript-Code zu einer bestimmten EMCAScript-Version kompiliert und so moderne JavaScript- Funktionen durch alte ersetzt, ohne die Semantik des Programms zu verändern (siehe Abbildung 2 und Abbildung 3). 5
Vor Kompilierung: 1 var helloWorld = () => { 2 console.log("Hello World!") 3 } 4 5 helloWorld() Nach Kompilierung: 1 "use strict"; 2 3 var helloWorld = function helloWorld() { 4 console.log("Hello World!"); 5 }; 6 7 helloWorld(); Abbildung 2: Babel - Kompilieren von Code mit Arrow-Funktionen zu ES5-kompatiblem Code. Vor Kompilierung: 1 var personName = "Max" 2 var personAge = 10 3 console.log(`${personName} is ${personAge} years old.`) Nach Kompilierung: 1 "use strict"; 2 3 var personName = "Max"; 4 var personAge = 10; 5 console.log("".concat(personName, " is ").concat(personAge, " years ,→ old.")); Abbildung 3: Babel - Kompilierung von Code mit Template-Strings zu ES5-kompatiblem Code. 6
2.1.4 Testen in JavaScript Das Schreiben von Tests spielt bei der Softwareentwicklung eine sehr wichtige Rolle und ist oft ein wesentlicher Bestandteil weitverbreiteter Methoden wie Extreme Programming [14]. Tests können zum einen die Produktivität erhöhen, da man nicht nach der Implementierung oder Änderung einzelner Codebestandteile manuell testen muss, ob das gewünschte Ergebnis erreicht wird. Stattdessen kann man einfach die Tests ausführen und so übersichtlich fehlerhafte und funktionierende Bestandteile sehen. Dies erleichtert oft auch die Fehlerbehebung, da Fehlerursachen leichter zu finden sind. Darüber hinaus fungieren die Tests auch als eine Art Dokumentation, da sie oft zeigen, wie die Nutzung des Programms beabsichtigt ist [16]. Diese Eigenschaft bildet die Grundlage für den Ansatz diese Arbeit. Der Artikel von Zaidman (2020) [23] präsentiert den aktuellen Stand für das Schreiben von Tests in JavaScript für das Jahr 2021. Dieser wird im folgenden zusammengefasst. Grundsätzlich gibt es drei verschiedene Testarten: Unittests, Integrationstests und End-to-End-Tests. Bei Unittests werden funktionale Einheiten eines Programms getestet, indem man eine Eingabe macht und überprüft, ob die Ausgabe dem erwarteten Wert entspricht. Eine funktionale Einheit kann hierbei zum Beispiel ein ganzes Modul, eine Funktion oder eine Klasse sein. Ein Beispiel für Unittests ist in Abbildung 25 dargestellt. Integrationstests bauen häufig auf Unittests auf und werden verwendet, um die Interaktion von funktionalen Einheiten untereinander und die Interaktion von funktionalen Einheiten mit zum Beispiel Datenbanken und Program- mierschnittstellen (APIs) zu testen. Ein Beispiel hierfür ist in Abbildung 26 dargestellt. End-to-End-Tests werden in der Regel bei Webanwendungen eingesetzt und 7
testen das eigentliche Produkt, mit dem der Benutzer interagiert. End-to- End-Tests ignorieren die interne Struktur der Anwendung und behandeln sie wie eine Blackbox. Zu diesem Zweck werden häufig Browser ohne grafische Benutzeroberfläche verwendet, wie zum Beispiel der Browser PhantomJS. Je nach Testtool werden Tests auch in normalen Browsern oder in Node.js ausgeführt. Das Schreiben von Tests für Software ist eine bewährte Praxis und ist in den meisten großen Repositories üblich. Von den 1000 meistgenutzten NPM- Modulen haben 942 ein GitHub-Repository, wovon 92% ein nicht leeres, nicht standardmäßiges Testskript in der package.json definiert haben (siehe Abbil- dung 4a). Im Vergleich dazu haben von 7505 Modulen in DefinitelyTyped 6054 ein GitHub-Repository, wovon wiederum 80,3% ein nicht leeres, nicht standardmäßiges Testskript in der package.json definiert haben (siehe Abbil- dung 4b). B B 8% 19.7% 92% 80.3% A A (a) Top 1000 NPM-Module (b) DefinitelyTyped-Module A: Mit Testskript, B: Ohne Testskript Abbildung 4: Module mit Testskript - Von 942 Modulen (mit einem GitHub-Repository) der Top 1000 NPM-Module haben 867 ein nicht leeres, nicht standardmäßiges Testskript. Von 6054 DefinitelyTyped-Modulen haben 4864 ein nicht leeres, nicht standardmäßiges Testskript. 8
Die bekanntesten Test-Frameworks sind Mocha, Tap, Jest, Tape, AVA und tsd (siehe Abbildung 27). Mocha ist das bei weitem am häufigsten verwendete Framework. 2.1.5 Typen In den meisten Programmiersprachen haben Werte einen Typ, der angibt, wie die Daten des Wertes zu interpretieren sind. JavaScript besitzt sieben primitive Datentypen: string, number, bigint, boolean, undefined, symbol und null. Außerdem gibt es den Typ object, mit dem zum Beispiel Arrays umgesetzt sind. Wenn Methoden auf einem primitiven Datentyp aufgerufen werden, erstellt JavaScript automatisch sogenannte Wrapper-Objekte für alle primitiven Datentypen außer undefined und null. Diese Wrapper-Objekte heißen dann String, Number, BigInt, Boolean oder Symbol und besitzen Methoden, wie zum Beispiel valueOf(), die aufgerufen werden können [7]. Ein Typ kann in einen anderen umgewandelt werden, was als Typkonvertie- rung bezeichnet wird. Die Regeln, die dieser Konvertierung zugrunde liegen, werden von der jeweiligen Sprache bestimmt. Eine Typkonvertierung kann explizit sein, wenn sie vom Entwickler vorgenommen wird. Andererseits kann sie auch implizit sein, wenn sie vom Compiler beziehungsweise Interpreter durchgeführt wird. Da JavaScript eine dynamisch typisierte Sprache ist, wer- den implizite Typkonvertierungen häufig durchgeführt [21]. Implizite Typkonvertierungen sind ein kontroverses Thema, da sie sowohl Vor- als auch Nachteile mit sich bringen. Der Hauptvorteil impliziter Typkonver- tierungen besteht darin, dass sie Entwicklern die Programmierung erleichtern, was Anfängern oft den Einstieg in JavaScript erleichtert. Allerdings können implizite Typkonvertierungen auch dazu führen, dass der Code schwerer zu lesen ist, da der Typ eines Wertes manchmal nicht eindeutig aus dem Code hervorgeht. Darüber hinaus können implizite Typkonvertierungen zu einer erhöhten Fehleranfälligkeit führen, da möglicherweise unbeabsichtigte 9
Operationen auf einem Wert unbemerkt bleiben. Implizite Typkonvertierun- gen können in JavaScript zu kontraintuitiven Eigenschaften führen, die zu frustrierenden Problemen führen können (siehe Abbildung 5). Die genannten Nachteile zeigen sich häufig vor allem in großen Codebasen [19]. 1 0 == '0' // true 2 3 false == [] // true 4 false == ![] // true 5 6 !!null // false 7 null == false // false 8 9 null == undefined // true Abbildung 5: Beispiele für unintuitive Eigenschaften von JavaS- cript - Unintuitive Eigenschaften von JavaScript werden meistens durch implizite Typkonvertierungen verursacht. 2.2 TypeScript TypeScript ist eine von Microsoft entwickelte Programmiersprache, die die bestehende JavaScript-Syntax erweitert. Die Syntax von TypeScript ist somit eine Obermenge der JavaScript-Syntax. Wie in Abschnitt 2.1.5 beschrieben, kann die dynamische Typisierung von JavaScript einige Nachteile haben, die TypeScript durch die Erweiterung der JavaScript-Syntax mit optionaler statischer Typisierung zu beheben versucht. Dies soll die Entwicklung großer Codebasen erleichtern und die Produktivität erhöhen. TypeScript-Code wird vom TypeScript-Compiler zu gewöhnlichem JavaScript-Code kompiliert. Dies bedeutet, dass TypeScript mit allen JavaScript-Modulen kompatibel ist. Dar- über hinaus führt die statische Typisierung zu strukturierterem Code, der für den Entwickler besser lesbar ist und sich vorhersehbarer verhält als entspre- chender JavaScript-Code. Außerdem können Entwicklungsumgebungen den Entwickler im Code gezielter unterstützen. Dies ist zum Beispiel durch bessere 10
Codenavigation und Autovervollständigung möglich, was die Produktivität erhöhen kann. 2.2.1 Typen Genau wie in JavaScript gibt es in TypeScript die primitiven Datentypen string, number, bigint, boolean, undefined, symbol und null und den nicht-primitiven Datentyp object. Hierbei wird wie in JavaScript nicht un- terschieden, ob es sich um eine ganze Zahl oder eine Gleitkommazahl handelt. Die primitiven Datentypen können bei einer Variablendefinition vermerkt werden und führen bei einer Zuweisung der Variable mit einem falschen Datentyp zu einem Kompilierungsfehler (siehe Abbildung 6). 1 // Primitive Datentypen 'string', 'number' und 'boolean' 2 var personName: string = 'Max' 3 var personAge: number = 10 4 var personIsAdult: boolean = false 5 6 var personSiblingNames: string[] = ['Julian', 'Emily'] Abbildung 6: Primitive Datentypen string, number und boolean Zusätzlich gibt es den Datentyp any, der verwendet werden kann, um jegliche Kompilierungsfehler bei der Überprüfung des Typs der jeweiligen Variable zu vermeiden (siehe Abbildung 28). Für Funktionsparameter und -rückgabewerte können ebenfalls Typen fest- gelegt werden, die bei einer falschen Benutzung der Funktion zu einem Kompilierungsfehler führen (siehe Abbildung 29). Um Typen für ein Objekt festzulegen, kann ein neuer Typ definiert werden. Dieser kann die Struktur und Typen der Attribute des Objekts festlegen und kann wie primitive Datentypen verwendet werden (siehe Abbildung 30). 11
2.2.2 Declaration Files JavaScript-Entwicklern stehen über NPM mehr als 1,3 Millionen Module zur Verfügung, mit denen sie schnell und einfach kleine oder komplexere Module mit JavaScript implementieren können. Diese NPM-Module können auch in TypeScript verwendet werden. Hierbei fehlen allerdings die Vorteile in Ent- wicklungsumgebungen, die reguläre TypeScript-Module mit sich bringen. Die Erweiterung von JavaScript-Modulen mit Typdeklarationen ist mit sogenann- ten Declaration Files möglich. Sie haben in der Regel die Dateierweiterung .d.ts und enthalten declare-, interface- und export-Anweisungen, die die Typen von Funktionsparametern, Funktionsrückgabewerten, Objekten, Klassen und globalen Variablen deklarieren. Ein Beispiel für ein Declaration File ist in Abbildung 7 abgebildet. Um das Schreiben von Declaration Files zu erleichtern, hat Microsoft das Tool dts-gen2 entwickelt, das als Ausgangspunkt für das Schreiben qualitativ hochwertige Declaration Files dienen soll. Zur Generierung der Declaration Files untersucht dts-gen die Objekte eines Moduls zur Laufzeit, um eine Grundstruktur und teilweise auch Typen der Objekte des Moduls zu erken- nen. Dies geschieht ohne die Verwendung von Quellcode, der das Modul importiert und verwendet. Dies kann dazu führen, dass vielen Funktions- parametern und -rückgabewerten der Wert any zugewiesen wird und auch Teile des Moduls im generierten Declaration File deklariert werden, die vom Entwickler des Moduls nicht zur öffentlichen Verwendung vorgesehen waren. Selbst mit dts-gen kann das Schreiben von Declaration Files, insbesondere für große JavaScript-Module, sehr zeitaufwändig sein und außerdem leicht zu Fehlern führen, die vom TypeScript-Compiler bei der späteren Verwendung nicht erkannt werden. Dies kann zu Fehlern in der Entwicklungsumgebung und zu frustrierendem Debugging für den Benutzer führen. 2 https://github.com/microsoft/dts-gen 12
helper.js 1 var someVar = 7; 2 3 function getArraySum(array) { 4 let sum = 0 5 for (let i = 0; i < array.length; i++) { 6 sum += array[i] 7 } 8 return sum 9 } 10 11 module.exports = { 12 getArraySum 13 }; helper.d.ts 1 export function getArraySum(arr: number[]): number; 2 declare var someVar: number; Abbildung 7: Beispiel für ein Declaration File - Für die exportier- te Funktion getArraySum wird der Funktionsparameter als Zahlenarray (number[]) und der Rückgabewert als Zahl deklariert. Die Variable someVar wird mit einer declare- Anweisung als Zahl deklariert, da sie nicht exportiert wird. 2.2.3 DefinitelyTyped Da Entwickler von JavaScript-Modulen in den meisten Fällen die Declaration Files für ihre Module nicht selber schreiben, wird das GitHub-Repository Defi- nitelyTyped betrieben. Es enthält Declaration Files für über 7500 JavaScript- Module, die von der Open-Source-Community manuell geschrieben und ge- pflegt werden. Die Declaration Files werden über den bezeichner @types auf NPM veröffentlicht. So kann man zum Beispiel mit dem Befehl npm install –save-dev @types/jquery sehr leicht, die Declaration Files für die JavaScript-Bibibliothek jQuery herunterladen. Über NPM werden die Declaration Files von DefinitelyTyped wöchentlich über 100 Millionen Mal 13
heruntergeladen und verwendet [13]. Um ein Declaration File für ein Modul zu DefinitelyTyped hinzuzufügen, muss eine Pull-Request auf GitHub erstellt werden. Diese wird von Mitwirkenden oder DefinitelyTyped-Maintainern überprüft. Nach einer erfolgreichen Überprüfung werden die Änderungen der Pull-Request zu DefinitelyTyped hinzugefügt. 2.3 Jalangi Jalangi3 ist ein Framework zur Durchführung dynamischer Analysen von Front-End- und Back-End-JavaScript. Jalangi ermöglicht es dem Benut- zer, jede im Code ausgeführte Operation zu überwachen und sie in selbst geschriebenen Analysen zu verwenden. Die Überwachung der ausgeführten Operationen wird mit sogenannten Callback-Funktionen umgesetzt, die immer dann ausgeführt werden, wenn die jeweilige Operation ausgeführt wird. Eine Callback-Funktion kann nach Bedarf angepasst werden. Ein Beispiel für eine solche Callback-Funktion ist getField(), die immer aufgerufen wird, wenn auf eine Eigenschaft eines Objekts zugegriffen wurde. Ein weiteres Beispiel ist die Callback-Funktion functionEnter(), die immer vor der Ausführung eines Funktionskörpers aufgerufen wird. Insgesamt gibt es 28 Callback-Funktionen, die dem Benutzer zur Verfügung stehen. Um JavaScript-Code zu überwachen, muss er zunächst instrumentiert werden, um ihn dann mit Jalangi und den benutzerdefinierten Analysen auszuführen. Die Instrumentierung verändert im Idealfall nicht das Verhalten des Codes bei der Ausführung mit Jalangi. Allgemein gibt es drei Befehle die Jalangi zur Verfügung stellt: jalangi.js, instrument.js und direct.js. Mit jalangi.js wird der übergebene Code automatisch intern instrumentiert und direkt analysiert. Mit instrument.js wird der übergebene Code nur instrumentiert und in einem Ausgabeordner 3 https://github.com/Samsung/jalangi2 14
abgespeichert. Der Befehlt direct.js analysiert die angegebenen Dateien. Diese müssen bereits instrumentiert sein, um analysiert zu werden [4]. Jalangi unterstützt offiziell die ECMAScript-Version 5.1 [5]. 15
16
3 Ansatz 3.1 Problemstellung Für die Nutzung von JavaScript-Modulen in TypeScript können Declaration Files geschrieben werden, die Informationen bezüglich der Typen von Funk- tionen und Objekten eines Moduls enthalten. Das manuelle Schreiben von Declaration Files ist zeitaufwendig und kann zu Fehlern führen. Gesucht wird ein Verfahren, das für ein gegebenes Modul automatisch ein Declaration File erstellen kann, das inhaltlich möglichst nah an der vom Autor vorgesehenen Nutzung ist. Hierfür muss eine robuste Informationsquelle gefunden werden, die Informationen bezüglich der Nutzung enthält. Diese Informationen müssen analysiert und verarbeitet werden, um ein Declaration File zu erstellen. 3.2 Bisheriger Ansatz Der bisherige Ansatz von Cristiani und Thiemann [17] basiert im Wesentli- chen auf der dynamischen Analyse von Codebeispielen aus der Readme-Datei eines Moduls zur Generierung von Declaration Files. Readme-Dateien von JavaScript-Modulen sind in der Regel Markdown- Dateien, die wichtige Informationen wie Konfigurations-, Installations- und Betriebsanweisungen sowie Informationen über die Struktur des Moduls, die Lizenzierung und den Prozess der Zusammenarbeit enthalten. Zum Zweck der Betriebsanweisung werden häufig Codeblöcke mit einfachen Beispielen für 17
die Verwendung des Moduls eingefügt, die dem Benutzer des Moduls einen leichten Einstieg ermöglichen sollen. Diese Codeblöcke werden bei diesem Ansatz mit Jalangi (siehe Abschnitt 2.3) instrumentiert und dann ausgeführt, um Laufzeitinformationen zu sammeln. Diese Laufzeitinformationen werden verwendet, um Declaration Files zu generieren. Die Qualität der generierten Declaration Files ist hier von der Ausführlichkeit der Codebeispiele abhängig, die der Entwickler des Moduls zur Verfügung stellt. Dies kann man zum Beispiel beim NPM-Modul abs1 sehen (siehe Abbildung 8). Bei jeder Verwendung der Funktion abs() in 1 const abs = require("abs"); 2 3 console.log(abs("/foo")); 4 // => "/foo" 5 6 console.log(abs("foo")); 7 // => "/path/to/where/you/are/foo" 8 9 console.log(abs("~/foo")); 10 // => "/home/username/foo" Abbildung 8: Codebeispiele für Abs - Codebeispiele aus der Readme- Datei des NPM-Moduls abs den Beispielen der Readme-Datei des Moduls ist der Funktionsparameter nicht leer. Nach der Generierung der Laufzeitinformationen und des darauf basierenden Declaration Files führt dies dazu, dass der Funktionsparameter input nicht optional ist. Im manuell geschriebenen Declaration File von DefinitelyTyped ist dies jedoch der Fall (siehe Abbildung 9). Hier liegt also ein Fehler vor, weil ein Beispiel im Codeblock fehlt. Eine weitere Einschränkung des Ansatzes besteht darin, dass manchmal Codeblöcke in der Readme-Datei nicht zusammenhängend sind und zum Beispiel durch Text unterbrochen werden. Da es keine einheitliche Methode 1 https://www.npmjs.com/package/abs 18
Automatisch generiertes Declaration File 1 export = Abs ; 2 declare function Abs( input : string ) : string ; Declaration File von DefinitelyTyped 1 declare function abs( input ?: string ) : string ; 2 export = abs ; Abbildung 9: Declaration Files für Abs - Im automatisch generierten Declaration File ist der Funktionsparameter input nicht op- tional, anders als im Declaration File von DefinitelyTyped. gibt, um zu unterscheiden, ob zwei Codeblöcke zusammengehören oder als unabhängige Beispiele zu betrachten sind, kann dies zu unvollständigen Co- debeispielen führen, die analysiert werden und eventuell Fehler erzeugen oder zu unvollständigen Declaration Files führen. Darüber hinaus gibt es einige Module, bei denen die Dokumentation und damit auch die Code-Beispiele auf externe Websites ausgelagert sind, so dass die Erzeugung von Declaration Files für diese Module nicht möglich ist. Insgesamt wurden mit diesem Ansatz aus 6029 DefinitelyTyped-Modulen 249 Declaration Files generiert, was einem Anteil von 4,1% entspricht. 19
DefinitelyTyped- 6029 Module GitHub-Repository 4974 README-Datei 4292 Codebeispiele 2260 Funktionierende Beispiele 946 Laufzeitinformationen 436 Generierte Declaration Files 249 Abbildung 10: Anzahl der analysierten Module für jede Phase der Experiments mit der Readme-Methode - Für 249 von 6029 DefinitelyTyped-Modulen wurde ein Declaration File generiert [17]. 3.3 Neuer Ansatz Der Ansatz dieser Arbeit, wie er im Überblick in Abbildung 31 dargestellt ist, basiert auf der dynamischen Analyse von Tests der Module zur Gene- rierung von Declaration Files. Tests sind ein wesentlicher Bestandteil in der Softwareentwicklung und können viele Informationen zur Benutzung eines Programms beinhalten. Außerdem werden Tests für JavaScript-Module in den meisten Fällen einheitlich in einem NPM-Script mit dem Namen test definiert, die mit dem Befehl npm run test ausgeführt werden können. Es wurden die 1000 meistgenutzten Module und 7505 DefinitelyTyped-Module auf ihre Verwendung von Tests über das NPM-Script test untersucht. Dies ergab, dass 942 der 1000 meistgenutzten Module und 4864 der 6054 der DefinitelyTyped-Module mit einem GitHub-Repository ein nicht leeres und nicht voreingestelltes Testskript besitzen. “Nicht voreingestellt” heißt hier, dass das Testskript nicht dem Testskript entspricht, das automatisch bei der Initia- lisierung eines Moduls vom NPM-Kommandozeilentool generiert wird (echo 20
\“Error: no test specified\” && exit 1). Die häufige Verwendung von Testskripten macht die Verwendung von Tests zu einer vielversprechenden Alternative zum bisherigen Ansatz als Informationsquelle für die Verwendung von Modulen. Der Ansatz dieser Arbeit ist im Repository ts-declaration-file-generator- service2 implementiert und verwendet intern run-time-information-gathering- tests3 zur Generierung der Laufzeitinformationen mit Tests. 3.3.1 Schritt 1: Herunterladen des Moduls Bei der normalen Verwendung eines NPM-Moduls würde man den Befehl npm install beziehungsweise npm install @ ausführen, um das Modul herunterzuladen. Tests, Entwicklungsabhängigkei- ten und andere Komponenten, die man für die übliche Verwendung nicht benötigt, werden jedoch oft aus der .tar-Datei ausgeschlossen, die NPM zum Herunterladen bereitstellt. Der Hauptgrund dafür liegt in der Verkür- zung der Downloadzeit. Da Tests und Entwicklungsabhängigkeiten, wie zum Beispiel Test-Frameworks, jedoch für den Ansatz dieser Arbeit benötigt werden, muss ein alternativer Weg zum Herunterladen des Moduls gewählt werden. Die package.json von NPM-Modulen beinhaltet unter dem Schlüs- sel repository Informationen, wie eine Repository-URL. Diese ist in den meisten fällen eine URL zu einem GitHub-Repository, die man in dem Be- fehl git clone benutzen kann um das gesamte Modul herunterzuladen. 3.3.2 Schritt 2: Instrumentierung des Moduls Da Jalangi nur JavaScript-Code der Version ES5.1 unterstützt, wird das Tool Babel4 verwendet, um das jeweilige Modul zu ES5-kompatiblen JavaScript- 2 https://github.com/luka1199/ts-declaration-file-generator-service 3 https://github.com/luka1199/run-time-information-gathering-tests 4 https://babeljs.io/ 21
Code zu kompilieren. Dadurch können Fehler bei der Instrumentierung mit Jalangi vermieden werden (siehe Abschnitt 4.3). Falls hierbei ein Fehler auftritt, wird der unkompilierte Code des Moduls weiterverwendet. Der ES5-kompatible Code wird daraufhin mit Jalangi instrumentiert. Hiebei ist zu beachten, dass zu diesem Zeitpunkt noch keine externen Abhängigkeiten installiert sind. Das heißt, dass nur der Code und die Tests des Moduls instrumentiert sind und später analysiert werden. 3.3.3 Schritt 3: Installation der Abhängigkeiten In diesem Schritt werden die externen Abhängigkeiten, die in der package.json des Moduls definiert sind, mit dem Befehl npm install heruntergeladen. Dazu gehören sowohl die normalen Abhängigkeiten unter dependecies als auch die Entwicklungsabhängigkeiten, wie z. B. Test-Frameworks, die unter devDependecies in der package.json definiert sind. Für Abhängigkeiten, die nicht über NPM installiert werden können, gibt es keine einheitliche Methode diese zu erkennen. Das bedeutet, dass Module mit solchen Abhängigkeiten nicht weiterverwendet werden können. Unter normalen Umständen kann von Jalangi instrumentierter Code mit dem Jalangi-Befehl direct.js ausgeführt werden. Da im Anwendungsfall dieser Arbeit jedoch in den meisten Fällen ein Test-Framework die Tests ausführen muss, ist dies nicht möglich. Grundsätzlich wäre es möglich das Test-Framework mit dem direct.js-Befehl auszuführen. Dies kann jedoch bei manchen Test-Frameworks aus verschiedenen Gründen zu Problemen füh- ren. Ein Beispiel dafür sind Test-Frameworks, die Child-Prozesse starten, um die instrumentierten Tests isoliert auszuführen und somit Fehler verursachen, da die instrumentierten Tests nicht über Jalangi ausgeführt werden. Die Lösung, die hier angewendet wird, besteht darin, eine leere HTML-Datei inklusive Analyse-Dateien zu instrumentieren. Aus der resultierenden HTML- 22
Datei kann man dann den generierten JavaScript-Code extrahieren, der die gesamte Jalangi-Umgebung inklusive der beigefügten Analysen beinhaltet. Diese Datei wird in den node_modules-Ordner eingefügt und in allen in- strumentierten Dateien des Moduls importiert. Der node_modules-Ordner beinhaltet die Abhängigkeiten des Moduls. Dadurch ist in den instrumentier- ten Dateien die globale Variable von Jalangi J$ definiert. Die globale Variable J$ wird im nächsten Schritt von Jalangi verwendet, wenn die Analysen durchgeführt werden. 3.3.4 Schritt 4: Generierung der Laufzeitinformationen Generell werden Laufzeitinformationen von den Analysen bei der Ausfüh- rung der Tests mit npm run test automatisch generiert. Um hier Fehler zu vermeiden, werden zuvor Tools zur Codeanalyse wie zum Beispiel ESLint oder StandardJS aus dem Testskript entfernt. Das resultierende Testskript wird als __test__ gespeichert (siehe Abbildung 11). Dies hat den Grund, { "scripts": { "test": "eslint && mocha", "__test__": "mocha" } } Abbildung 11: Entfernung von Analysetools - Das Skript test ist das ursprüngliche Testskript. Das Skript __test__ ist das ur- sprüngliche Testskript ohne Tools zur Codeanalyse. dass Tools zur Codeanalyse bei der Ausführung auf instrumentierten Code in den meisten Fällen Fehler erzeugen, da der instrumentierte Code nicht den Stilnormen der Tools entspricht. Daraufhin werden die Tests mit npm run __test__ ausgeführt, wodurch von den Analysedateien Laufzeitinformationen in JSON-Format generiert werden. 23
3.3.5 Schritt 5: Generierung des Declaration Files Zur Generierung des Declaration Files mit den generierten Laufzeitinforma- tionen wird das Tool ts-declaration-file-generator5 verwendet, wie es von Cristiani und Thiemann [17] vorgestellt wurde. 5 https://github.com/proglang/ts-declaration-file-generator 24
4 Experimente Um die Effizienz und Limitationen des Ansatzes zu untersuchen, wurden die Schritte des neuen Ansatzes, wie sie in Abschnitt 3.3 vorgestellt wurden, für 7505 DefinitelyTyped-Module (Stand 13. Juni 2021) ausgeführt und an- schließend evaluiert. Von Schritt zu Schritt wird die Anzahl der verwendeten Module aufgrund von zum Beispiel fehlenden Informationen oder Fehlern bei der Durchführung der Experimente kleiner. Alle Ergebnisse der folgenden Experimente sind im GitHub-Repository bachelor- results1 zu finden. Jegliche Tools, die zur Erstellung dieser Ergebnisse ver- wendet wurden sind im GitHub-Repository bachelor-tools2 enthalten. 4.1 Untersuchung der package.json der Module 6054 der 7505 DefinitelyTyped-Modulen haben ein GitHub-Repository, was 80.7% entspricht. Die restlichen Module werden in den meisten Fällen auf anderen Platformen wie GitLab, Bitbucket oder auf privaten Servern zur Verfügung gestellt. Um zu evaluieren, wie viele der 6054 Module mit einem GitHub-Repository über Tests verfügen, die für die Generierung der Declara- tion Files verwendet werden können, wurden die Testskripte der Module aus der package.json extrahiert. Dabei wurden auch Unterskripte ausgewertet, die im Testskript ausgeführt werden (siehe Abbildung 12). 1 https://github.com/luka1199/bachelor-results/ 2 https://github.com/luka1199/bachelor-tools/ 25
{ "scripts": { "test": "npm run test:lint && npm run test:mocha", "test:lint": "eslint", "test:mocha": "mocha" } } Abbildung 12: Testskript mit Unterskripten - Die NPM-Skripte test:lint und test:mocha werden im Testskript ausge- führt. Dies resultiert in dem eingelesenen Testskript “eslint && mocha”. Die extrahierten Testskripte wurden in zwei Gruppen unterteilt: • Gruppe A: Testskripte die leer sind, nicht in der package.json ent- halten sind oder dem standardmäßigen Testskript entsprechen, das bei der Initialisierung eines NPM-Moduls vom NPM-Kommandozeilentool generiert wird (echo \“Error: no test specified\” && exit 1). • Gruppe B: Definierte Testskripte Ergebnisse Hierbei fielen 1190 Module in die Gruppe A und 4864 Module in die Gruppe B. Dies bedeutet, dass 80,3% der DefinitelyTyped-Module mit einem GitHub- Repository ein definiertes Testskript besitzen und damit Tests besitzen, die möglicherweise für die Generierung von Declaration Files verwendet werden können. Die Anzahl der Vorkommen verschiedener Tools in den Testskripten sind in den Tabellen 4-7 aufgelistet. Vor allem interessant für diese Arbeit sind die am häufigsten verwendeten Test-Frameworks. Diese sind Mocha, Tap, Jest, Tape und AVA. 26
4.2 Herunterladen der Module Um sicherzustellen, dass die verwendete Version eines Moduls mit der Versi- on übereinstimmt, mit der das Declaration File des Moduls erstellt wurde, wurden Informationen aus dem Declaration File verwendet. Die meisten Declaration Files enthalten mehrere Kommentarzeilen, die unter anderem die verwendete Modulversion, die Repository-URL, den Autor des Declaration Files und die verwendete TypeScript Version enthält (siehe Abbildung 13). 1 // Type definitions for amp 0.3 2 // Project: https://github.com/visionmedia/node-amp 3 // Definitions by: Vilim Stubičan 4 // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 // TypeScript Version: 2.4 Abbildung 13: Kommentarzeilen in Declaration File - Beispiel für Kommentarzeilen im Declaration File des Moduls amp3 NPM folgt zur Standardisierung der Versionsbezeichnungen von Modulen dem Versionierungsschema Semantic Versioning4 . Da die Versionsbezeich- nungen in den Kommentarzeilen der Declaration Files von DefinitelyTyped häufig nicht diesem Versionierungsschma folgen, werden diese zunächst nach der Spezifikation von Semantic Versioning umgewandelt. Diese umgewan- delte Versionsbezeichnung eines Moduls kann verwendet werden, um das Veröffentlichungsdatum der Modulversion zu bestimmen. Hierfür wird der Be- fehl npm view time ––json verwendet (siehe Abbildung 32). Mit dem entsprechenden Veröffentlichungsdatum kann der passende Commit des Moduls identifiziert werden. Das Verfahren hierfür ist in Abbildung 33 dargestellt. Das Modul kann daraufhin mit git clone heruntergeladen und mit git checkout auf dem Stand der Version des Declaration Files gesetzt werden. 3 https://www.npmjs.com/package/amp 4 https://semver.org/ 27
Ergebnisse Das Herunterladen wurde für 5448 Module erfolgreich abgeschlossen. Diese Reduzierung der Anzahl ist darauf zurückzuführen, dass es sich bei vielen der Repositories um sogenannte Monorepositories handelt, die viele kleine Untermodule in Unterordnern enthalten. Diese sind für die weitere Verwen- dung dieser Arbeit nicht brauchbar, da es keine einheitliche Struktur dieser Monorepositories gibt. Sie wurden teilweise manuell aussortiert oder konn- ten aufgrund der Struktur der URL der Unterordner des Repositories nicht geclont werden. 4.3 Instrumentierung der Module Bei der Instrumentierung wurde, wie in Abschnitt 3.3.2 beschrieben, das Tool Babel verwendet, um den Code der Module in ES5-kompatiblen Code zu konvertieren. Um die Effizienz der Verwendung von Babel bei der Instrumen- tierung zu evaluieren, wurde die Instrumentierung für die DefinitelyTyped- Module sowohl mit als auch ohne Babel durchgeführt. Bei der Instrumentie- rung mit Babel wurde bei einem Kompilierungsfehler automatisch die un- kompilierte Version des Moduls verwendet, um Verluste bei der Verwendung von Babel zu vermeiden. Wichtig ist außerdem, dass die Instrumentierung hierbei noch keine Analysedateien beinhaltet. Ergebnisse Instrumentierung ohne Babel Die Instrumentierung ohne Babel wurde 2977 Mal erfolgreich abgeschlossen und scheiterte 2469 Mal. Dies entspricht einer Erfolgsquote von 54,7%. Die Fehler bei der Instrumentierung sind in den meisten Fällen auf modernen JavaScript-Funktionen der JavaScript-Versionen ab ES6 zurückzuführen. 28
Instrumentierung mit Babel Die Instrumentierung mit Babel wurde 4530 Mal erfolgreich abgeschlossen und scheiterte 916 Mal. Dies entspricht einer Erfolgsquote von 83,2% und einer Verbesserung um 28,5%. Die Anzahl der fehlgeschlagenen Instrumentierungen ist oft auf eine fehl- geschlagene Kompilierung mit Babel zurückzuführen, die dazu führt, dass moderne JavaScript-Funktionen nicht konvertiert werden und zu Fehlern in Jalangi führen. Ein Beispiel, das häufig zu Fehlern bei der Kompilierung mit Babel führt, ist die Verwendung von JavaScript-Syntaxerweiterungen wie JSX5 , die in vielen Modulen für das React.js-Framework verwendet wird. Für solche Fälle gibt es häufig Erweiterungen für Babel, die diese Probleme lösen, aber die Erkennung der erforderlichen Erweiterungen für das jeweilige Modul ist schwierig auf automatisierte Weise zu implementieren. Generell kann in seltenen Fällen selbst eine erfolgreiche Kompilierung mit Babel zu Fehlern beim Instrumentieren mit Jalangi führen. 4.4 Installation von Abhängigkeiten Für das spätere Ausführen der Tests werden die Abhängigkeiten der Module benötigt. Diese sind in der package.json des Moduls aufgelistet und können mit dem Befehl npm install ausgeführt werden. Weitere benötigte Schritte zur Installation können hierbei nicht beachtet werden, da diese häufig in der Readme-Datei des Moduls angegeben sind, wodurch eine automatisierte Extrahierung der weiteren Schritte nicht einheitlich möglich ist. Außerdem wurde für die Installation ein Zeitlimit von 250 Sekunden festgelegt. Dieses Experiment wird mit 4358 Modulen durchgeführt, für die die Instru- mentierung erfolgreich war und für die die package.json gültig ist. 5 https://reactjs.org/docs/introducing-jsx.html 29
36.4% Zeitlimit überschritten Nicht erfolgreich Erfolgreich 59.1% 4.4% Abbildung 14: Installation der Abhängigkeiten - 2577 Mal erfolgreich, 1588 Mal Zeitlimit überschritten, 193 Mal nicht erfolgreich Ergebnisse Die Abhängigkeiten für 2577 der 4358 Module wurden erfolgreich installiert. Bei 1588 der 4358 Module wurde das Zeitlimit von 250 Sekunden überschritten und bei 193 der 4358 Module schlug die Installation fehl. 4.5 Generierung der Laufzeitinformationen Zur Generierung der Laufzeitinformationen müssen die Tests der instrumen- tierten Module ausgeführt werden. Um hierbei Auswirkungen der Instrumen- tierung auf den Erfolg der Tests zu evaluieren, wurden zunächst die Tests der Module im nicht instrumentierten Zustand ausgeführt. Die Ausführung der Tests wurde hierbei einheitlich mit npm run test durchgeführt. Daraufhin wurden die Tests der instrumentierten Module in vier verschiedenen Varianten ausgeführt: 1. Ausführung der Tests mit Codeanalyse-Tools und ohne Analysedateien 2. Ausführung der Tests ohne Codeanalyse-Tools und ohne Analysedateien 3. Ausführung der Tests mit Codeanalyse-Tools und mit Analysedateien 30
4. Ausführung der Tests ohne Codeanalyse-Tools und mit Analysedateien Der Erfolg der einzelnen Ausführungen kann hier an zwei Werten gemessen werden. Der erste Wert ist die Anzahl der Ausführungen bei denen Fehler aufgetreten sind. Der zweite Wert ist die Anzahl der nicht leeren Laufzeit- informationen, die bei der Ausführung mit Analysedateien erzeugt wurden. Für die Generierung der Declaration Files ist vor allem die Anzahl der nicht leeren Laufzeitinformationen wichtig. Die Entfernung der Codeanalyse-Tools wird wie in Abschnitt 3.3.4 beschrie- ben durchgeführt und bei allen Ausführungen der Tests wurde ein Zeitlimit von 100 Sekunden festgelegt. Dieses Experiment wird mit 1922 Modulen durchgeführt, bei denen die In- stallation der Abhängigkeiten erfolgreich war und die ein nicht leeres und nicht voreingestelltes Testskript besitzen. Ergebnisse Ohne Instrumentierung Die Ausführung der Tests auf die normalen, nicht instrumentierten Modu- le war bei 1227 Modulen erfolgreich, bei 559 nicht erfolgreich und bei 136 Modulen dauerte die Ausführung länger als das Zeitlimit von 100 Sekunden. Dies entspricht einer Erfolgsquote von 64,8%. Die relativ hohe Anzahl an nicht erfolgreichen Tests ist auf mehrere Gründe zurückzuführen. Ein Hauptgrund war, dass bei einigen Modulen die für die Tests benötig- ten Frameworks nicht in den devDependecies der package.json angegeben waren. Dies führte dazu, dass sie bei der Ausführung der Tests fehlten, was zu Fehlern führte. Dies wurde in späteren Versionen der Module teilweise korrigiert. Ein weiterer Grund war, dass bei manchen Modulen weitere Schritte vor 31
5.7% 29.6% Zeitlimit überschritten Nicht erfolgreich Erfolgreich 64.8% Abbildung 15: Ausführung der nicht instrumentierten Tests - 1227 Mal erfolgreich, 559 Mal nicht erfolgreich, 136 Mal Zeitlimit überschritten der Ausführung von npm run test benötigt werden, die nicht im Testskript angeben waren. In seltenen Fällen waren außerdem Syntax-Fehler im Testskript vorhanden, die zu Fehlern geführt haben. Mit Instrumentierung, ohne Analyse Die Ausführung der instrumentierten Tests ohne Analysedateien und mit Codeanalyse-Tools war bei 797 Modulen erfolgreich, bei 1030 Modulen nicht erfolgreich und bei 95 Modulen wurde das Zeitlimit von 100 Sekunden über- schritten. Dies entspricht einer Erfolgsquote von 41,5%. Die Ausführung der instrumentierten Tests ohne Analysedateien und ohne Codeanalyse-Tools hingegen war bei 900 Modulen erfolgreich, bei 924 Mo- dulen nicht erfolgreich und bei 98 Modulen wurde das Zeitlimit von 100 Sekunden überschritten. Dies entspricht einer Erfolgsquote von 46,8%. Somit konnte die Erfolgsquote durch die Entfernung von Codeanalyse-Tools um 5,3% verbessert werden. Dies zeigt jedoch auch, dass die Instrumentierung mit Jalangi bei ungefähr der Hälfte der Module zu Fehlern führt. 32
4.9% 41.5% Zeitlimit überschritten Nicht erfolgreich Erfolgreich 53.6% Abbildung 16: Ausführung der instrumentierten Tests mit Codeanalyse-Tools, ohne Analyse - 797 Mal er- folgreich, 1030 Mal nicht erfolgreich, 95 Mal Zeitlimit überschritten 5.1% 46.8% Zeitlimit überschritten Nicht erfolgreich Erfolgreich 48.1% Abbildung 17: Ausführung der instrumentierten Tests ohne Codeanalyse-Tools, ohne Analyse - 900 Mal erfolgreich, 924 Mal nicht erfolgreich, 98 Mal Zeitlimit überschritten 33
Mit Instrumentierung, mit Analyse Die Ausführung der instrumentierten Tests mit Analysedateien und mit Codeanalyse-Tools war bei 355 Modulen erfolgreich, bei 1384 Modulen nicht erfolgreich und bei 183 Modulen wurde das Zeitlimit von 100 Sekunden überschritten. Dies entspricht einer Erfolgsquote von 18,5%. Die Ausführung der instrumentierten Tests mit Analysedateien und ohne Codeanalyse-Tools hingegen war bei 396 Modulen erfolgreich, bei 1353 Mo- dulen nicht erfolgreich und bei 173 Modulen wurde das Zeitlimit von 100 Sekunden überschritten. Dies entspricht einer Erfolgsquote von 20,6%. Die Erfolgsquote konnte hier also durch die Entfernung von Codeanalyse- Tools um 2,1% verbessert werden. Die Hinzunahme der Analysedateien führte generell zu einer starken Re- duzierung der Erfolgsquote. Trotz dessen konnten mit Codeanalyse-Tools für 1914 Module und ohne Codeanalyse-Tools für 1915 Module nicht leere Laufzeitinformationen generiert werden. Ob die Entfernung der Codeanalyse-Tools auch einen qualitativen Unterschied macht, wird in Abschnitt 4.6 untersucht. 34
9.5% 18.5% Zeitlimit überschritten Nicht erfolgreich Erfolgreich 72% Abbildung 18: Ausführung der instrumentierten Tests mit Codeanalyse-Tools, mit Analyse - 355 Mal er- folgreich, 1384 Mal nicht erfolgreich, 183 Mal Zeitlimit überschritten 9% 20.6% Zeitlimit überschritten Nicht erfolgreich Erfolgreich 70.4% Abbildung 19: Ausführung der instrumentierten Tests ohne Codeanalyse-Tools, mit Analyse - 396 Mal erfolgreich, 1353 Mal nicht erfolgreich, 173 Mal Zeitlimit überschritten 35
Erfolgsquote nach Testtools Ausschlaggebend für den Erfolg der instrumentierten Tests sind häufig die verwendeten Tools. Diesbezüglich wurde bei der Ausführung der instrumen- tierten Tests mit Analysedateien und Codeanalyse-Tools die Erfolgsquote der verwendeten Tools untersucht (siehe Tabelle 1). Interessant ist hier vor allem die Erfolgsquote von Mocha, da Mocha das am häufigsten verwendete Tool der DefinitelyTyped-Module war. Mocha hatte eine Erfolgsquote von 18,37%. Die unter “Andere” kategorisierten Tools hatten die höchste Erfolgsquote von 34,21%. Dies ist darauf zurückzuführen, dass es sich um kleine Tools handelt, die selten mit Unterprozessen arbeiten und nicht sehr komplex sind, und somit seltener Fehler mit Jalangi verursacht werden. Tests, die direkt mit Node.js ausgeführt wurden, hatten aus ähnlichen Gründen eine relativ hohe Erfolgsquote von 27,41%. Make hatte die geringste Erfolgsquote von 6,38%. Tool Erfolgreich Nicht erfolgreich Timeout Erfolgsquote Andere 26 46 4 34,21% Jasmine 11 24 4 28,21% Node 159 370 51 27,41% Tap 50 182 27 19,31% Mocha 174 695 78 18,37% Tape 31 133 8 18,02% grunt 8 68 3 10,13% make 6 79 9 6,38% Tabelle 1: Erfolgsquote der am häufigsten verwendeten Tools - “An- dere” sind hier verschiedene Tools, die sehr selten verwendet werden. In der Regel sind diese nicht sehr große Tools. “Node” be- deutet hier, dass die Tests direkt von Node.js ausgeführt wurden und kein Framework die Tests ausführt. Häufig wurde hierbei die Assertion-Bibliothek von Node.js verwendet. 36
4.6 Generierung der Declaration Files Declaration Files wurden sowohl für Laufzeitinformationen, die mit Tests mit Codeanalysetools generiert wurden, als auch für Laufzeitinformationen, die mit Tests ohne Codeanalysetools generiert wurden, erstellt. In diesem Schritt wurde nur untersucht, ob die Declaration Files nicht leer sind. Ergebnisse Mit Codeanalyse-Tools Mit den Laufzeitinformationen für 1922 Module, die mit den Tests mit Codeanalyse-Tools generiert wurden, konnten 1233 nicht leere Declaration Files generiert werden. 662 Declaration Files waren leer und bei 27 gab es Fehler bei der Generierung. Das bedeutet, dass für 64,2% der 1922 Module mit benutzbaren Tests und erfolgreicher Installation Declaration Files generiert werden konnten. Ohne Codeanalyse-Tools Mit den Laufzeitinformationen für 1922 Module, die mit den Tests ohne Codeanalyse-Tools generiert wurden, konnten 1243 nicht leere Declaration Files generiert werden. 653 Declaration Files waren leer und bei 26 gab es Fehler bei der Generierung. Somit konnten für 64,7% der 1922 Module mit benutzbaren Tests und erfolgreicher Installation Declaration Files generiert werden. Die Entfernung der Codeanalyse-Tools hat somit auch zu einer leichten Ver- besserung der Qualität der Laufzeitanalyse-Dateien geführt. Die Fehler bei der Generierung der Declaration Files sind häufig auf unvoll- ständige Laufzeitinformationen zurückzuführen, zum Beispiel durch Über- schreitung des Zeitlimits bei der Ausführung der Tests. 37
Sie können auch lesen