Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/typescript adapter #246

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions adapter/typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ECCO TypeScript Adapter

## Overview
This ECCO adapter is able to parse a select subset of TypeScript language features.

It uses the JavaScript engine Javet to run the Microsoft TypeScript compiler in a Node
environment. This generates an AST in a JSON format which is exported to Java as a collection of Maps and ArrayLists.
An Ecco tree is then built from that AST information.

## Usage

Since parsing relies on the TypeScript compiler this needs to be installed.
For this a Node.js and npm installation is needed.

``
typescript/src/main/resources/script
``
includes a package.json that only defines a dependency for the npm typescript package
which is the Microsoft TypeScript compiler. Either run
``
npm install
`` in that directory or run the gradle
``
npmInstall
``
task.

## Features

The TypeScript reader can resolve the following language features:

- While loops
- Do While loops
- For loops
- For of loops
- For in loops
- Class Declarations
- Method Declarations
- Enums
- Switch Statements
- If Statements
- Functions
- Arrow functions assigned to variables
- Arrow functions as properties of objects

Any other statement is treated atomically and ends up as a leaf in the tree.

## Details

The TypeScriptParser uses a short JavaScript to invoke the TypeScript compiler. This script also maps the kinds of the
visited node from an integer to a string. That "kind" information string is then used in the Java part to distinguish
between the various language constructs. We need to do this because the AST data is structured differently for each kind.
The text containing relevant semantic information is used as a basis for the hash code of the nodes. White space and comments
are not included in the hash code but stored as separate information. The writer simply pastes the leading trivia, semantic
information and trailing trivia together to form the original source code for each node in the ECCO tree.

## Limitations
- The Reader is depended on the TypeScript compiler producing an AST. If the compiler fails to parse the input file the reader will fail as well.
- White space and comments follow the logic of the TypeScript compiler and are associated with the node directly after it.
If features are added or removed this may interfere with the formatting of the document for that reason.
- Features that are hidden in some lambda that that is passed to a function won't be resolved.
- Newer versions of the typescript compiler might need adjustments in the parsing of the kinds.
36 changes: 36 additions & 0 deletions adapter/typescript/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
plugins {
id 'java'
id("com.github.node-gradle.node") version "7.0.1"
}
ecco.adapter = true
group = 'at.jku.isse.ecco'
version = '0.1.9'
npmInstall

repositories {
mavenCentral()
}

dependencies {
implementation project(':ecco-service')
// https://mvnrepository.com/artifact/com.caoccao.javet/javet
implementation group: 'com.caoccao.javet', name: 'javet', version: '2.1.2'
// https://mvnrepository.com/artifact/com.caoccao.javet/javet-macos
implementation group: 'com.caoccao.javet', name: 'javet-macos', version: '2.1.2'
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'junit:junit:4.13.1'
}

ospackage {
requires('ecco-base', version, EQUAL)
requires('ecco-service', version, EQUAL)
}
node {
download = false
nodeProjectDir = file("${project.projectDir}/src/main/resources/script")
}
test {
useJUnitPlatform()
}
build.dependsOn npmInstall
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package at.jku.isse.ecco.adapter.typescript.test;

import at.jku.isse.ecco.adapter.typescript.TypeScriptReader;
import at.jku.isse.ecco.storage.mem.dao.MemEntityFactory;
import at.jku.isse.ecco.tree.Node;
import org.testng.annotations.Test;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;

public class AdapterTest {

private static final Path DATA_DIR;

static {
Path dataPath = null;
try {
dataPath = Paths.get(AdapterTest.class.getClassLoader().getResource("data").toURI());
} catch (URISyntaxException e) {
e.printStackTrace();
}
DATA_DIR = dataPath;
}

private static final Path BASE_DIR = DATA_DIR.resolve("input");
private static final Path[] FILES = new Path[]{Paths.get("parse.js")};

@Test(groups = {"integration"})
public void Java_Adapter_Test() {
TypeScriptReader reader = new TypeScriptReader(new MemEntityFactory());

System.out.println("READ");
Set<Node.Op> nodes = reader.read(BASE_DIR, FILES);

System.out.println(nodes);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const ts = require(nodePath);
const sf = ts.createSourceFile('sf',fileContent,ts.ScriptTarget.Latest);
const generateAst = (node, sourceFile) => {
const syntaxKind = ts.SyntaxKind[node.kind];
node.nodeText = node.getText(sourceFile);
node.start = node.getStart(sourceFile);
node.fullText = node.getFullText(sourceFile);
node.triviaWidth = node.getLeadingTriviaWidth(sourceFile);
node.forEachChild((child) => generateAst(child, sourceFile));
node.kind = syntaxKind;
};
(() => generateAst(sf, sf))();
14 changes: 14 additions & 0 deletions adapter/typescript/src/integrationTest/resources/testng.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="Adapter Integration Test" verbose="1">
<test name="TypeScript Adapter">
<groups>
<run>
<include name="integration"/>
</run>
</groups>
<packages>
<package name="at.jku.isse.ecco.adapter.typescript.test"/>
</packages>
</test>
</suite>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package at.jku.isse.ecco.adapter.typescript;

import at.jku.isse.ecco.adapter.ArtifactExporter;
import at.jku.isse.ecco.adapter.ArtifactReader;
import at.jku.isse.ecco.adapter.ArtifactViewer;
import at.jku.isse.ecco.adapter.ArtifactWriter;
import at.jku.isse.ecco.tree.Node;
import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.Multibinder;

import java.nio.file.Path;
import java.util.Set;

public class TypeScriptModule extends AbstractModule {

@Override
protected void configure() {


final Multibinder<ArtifactReader<Path, Set<Node.Op>>> readerMultibinder = Multibinder.newSetBinder(binder(),
new TypeLiteral<>() {
});
readerMultibinder.addBinding().to(TypeScriptReader.class);

final Multibinder<ArtifactWriter<Set<Node>, Path>> writerMultibinder = Multibinder.newSetBinder(binder(),
new TypeLiteral<>() {
});
writerMultibinder.addBinding().to(TypeScriptWriter.class);

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package at.jku.isse.ecco.adapter.typescript;

import com.caoccao.javet.exceptions.JavetException;
import com.caoccao.javet.interop.NodeRuntime;
import com.caoccao.javet.interop.V8Host;
import com.caoccao.javet.interop.V8Runtime;
import com.caoccao.javet.utils.JavetOSUtils;
import com.caoccao.javet.values.V8Value;

import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

public class TypeScriptParser {
static File codeFile;

static {
InputStream inputStream = TypeScriptParser.class.getClassLoader().getResourceAsStream("script/parse.js");
if (inputStream == null) {
System.out.println("Could not find resource");
}
//Resource stream to codeFile
Path outputPath = Path.of("parse.js");
try (OutputStream outputStream = new FileOutputStream(outputPath.toFile())) {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
codeFile = outputPath.toFile();
} catch (IOException e) {
System.out.println("Error writing to file");
e.printStackTrace();
}
}

private static final String SCRIPT_PATH = "../adapter/typescript/src/main/resources/script/parse.js";
private static final String NODE_MODULE_PATH = "../adapter/typescript/src/main/resources/script/node_modules/typescript";

public HashMap<String, Object> parse(Path path) throws FileNotFoundException {
HashMap<String, Object> res;
var read = new BufferedReader(new FileReader(path.toFile()));
Path cwd = Path.of(JavetOSUtils.WORKING_DIRECTORY);
File codeFile = cwd.resolve(SCRIPT_PATH).toFile();
var nodePath = cwd.resolve(NODE_MODULE_PATH).toString();
var fileContent = read.lines().collect(Collectors.joining("\n"));
try (NodeRuntime v8Runtime = V8Host.getNodeInstance().createV8Runtime()) {
v8Runtime.getConverter().getConfig().setMaxDepth(1000);
v8Runtime.getGlobalObject().set("fileContent", fileContent);
v8Runtime.getGlobalObject().set("nodePath", nodePath);
try (V8Value x = v8Runtime.getExecutor(codeFile).execute()) {
res = v8Runtime.getExecutor("sf").executeObject();
}
} catch (JavetException e) {
throw new RuntimeException(e);
}
return res;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package at.jku.isse.ecco.adapter.typescript;

import at.jku.isse.ecco.adapter.ArtifactPlugin;
import com.google.inject.Module;

public class TypeScriptPlugin extends ArtifactPlugin {

private static final String[] fileTypes = new String[]{"ts"};//, "c", "h", "cpp", "hpp"};

private TypeScriptModule module = new TypeScriptModule();

public String[] getFileTypes() {
return fileTypes;
}

@Override
public String getPluginId() {
return TypeScriptPlugin.class.getName();
}

@Override
public Module getModule() {
return this.module;
}

@Override
public String getName() {
return "TypeScriptArtifactPlugin";
}

@Override
public String getDescription() {
return "TypeScript Artifact Plugin";
}

}
Loading
Loading