Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
node_modules
dist
src/parser/JavaParser.ts
src/parser/JavaParserListener.ts
src/parser/JavaParserVisitor.ts
src/parser/JavaLexer.ts
src/parser/JavaContexts.ts
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# java-ast

Java Parser for JavaScript/TypeScript, based on [antlr4ts](https://www.npmjs.com/package/antlr4ts)
Java Parser for JavaScript/TypeScript, based on [antlr4ts](https://www.npmjs.com/package/antlr4ts), grammar taken from [antlr4's java grammar](https://github.com/antlr/grammars-v4/tree/master/java/java) too (so please report bugs and open pull requests related to grammars upstream)

[![npm version](https://img.shields.io/npm/v/java-ast.svg)](https://www.npmjs.com/package/java-ast)
[![Build Status](https://travis-ci.org/urish/java-ast.png?branch=master)](https://travis-ci.org/urish/java-ast)
Expand All @@ -9,8 +9,28 @@ Java Parser for JavaScript/TypeScript, based on [antlr4ts](https://www.npmjs.com
## Usage Example

```typescript
import { parse } from './index';
import { parse, createVisitor } from 'java-ast';

const ast = parse(`package test;\n\nclass TestClass {}\n`);
// do something with ast, e.g. console.log(ast.toStringTree());
const countMethods = (source: string) => {
let ast = parse(source);

return createVisitor({
visitMethodDeclaration: () => 1,
defaultResult: () => 0,
aggregateResult: (a, b) => a + b,
}).visit(ast);
};

console.log(
countMethods(`
class A {
int a;
void b() {}
void c() {}
}
class B {
void z() {}
}
`),
); // logs 3
```
35 changes: 35 additions & 0 deletions generate-contexts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// @ts-check

const fs = require('fs').promises;
const path = require('path');
const { EOL } = require('os');

const listenerFilePath = path.join(__dirname, '/src/parser/JavaParserListener.ts');
const contextsFilePath = path.join(__dirname, '/src/parser/JavaContexts.ts');

const main = () =>
fs
.stat(listenerFilePath)
.catch(
() => 'src/parser/JavaParserListener.ts not found use generate:parser script to generate it',
)
.then(() => fs.readFile(listenerFilePath, 'utf-8'))
.then((listenerSource) =>
listenerSource
.split(EOL)
.map((l) => {
let matches = l.match(/import\s*\{\s*(.*Context)\s*\}.*/);
if (matches === null) return null;
return matches[1];
})
.filter((c) => c !== null),
)
.then((contexts) => contexts.reduce((list, context) => list + ` ${context},${EOL}`, ''))
.then((exportList) => `export {${EOL}${exportList}} from './JavaParser';`)
.then((contextsSource) => fs.writeFile(contextsFilePath, contextsSource));

main().catch((error) => {
if (typeof error !== 'string') throw error;
console.log('Failure: ' + error);
process.exit(1);
});
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
"format": "prettier --write src/**.ts **/*.json",
"prepublish": "yarn build",
"generate:parser": "antlr4ts -visitor -o src/parser src/parser/JavaLexer.g4 src/parser/JavaParser.g4",
"generate:contexts": "node generate-contexts.js",
"precommit": "lint-staged",
"postcommit": "git update-index --again",
"test": "jest"
},
"devDependencies": {
"@types/jest": "^23.1.5",
"@types/node": "^14.0.22",
"antlr4ts-cli": "^0.4.0-alpha.4",
"husky": "^0.14.3",
"jest": "^23.3.0",
Expand All @@ -39,7 +41,7 @@
"rimraf": "^2.6.2",
"ts-jest": "^23.0.0",
"tslint": "^5.10.0",
"typescript": "~2.8.2"
"typescript": "^3.9.6"
},
"dependencies": {
"antlr4ts": "^0.4.1-alpha.0"
Expand Down
58 changes: 56 additions & 2 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,65 @@
import { parse } from './index';
import { createVisitor, parse, walk } from './index';

describe('Java AST parser', () => {
describe('parser', () => {
it('should parse the given Java code and return the AST', () => {
const tree = parse(`
class TestClass {
}
`);
expect(tree.children[0].getChild(0).getChild(1).text).toEqual('TestClass');
});

it('should handle super invocation with arguments', () => {
const tree = parse(`
class B extends A {
public B() {
super(1);
}
}
`);

const expressions = [];
walk({ enterExpression: (c) => expressions.push(c.text) }, tree);

expect(expressions).toContain('super(1)');
});

it('should allow super alone as expression', () => {
const tree = parse(`
class B extends A {
public B() {
super;
}
}
`);

const expressions = [];
walk({ enterExpression: (c) => expressions.push(c.text) }, tree);

expect(expressions).toContain('super');
});
});

describe('usage example', () => {
it('works', () => {
const countMethods = (source: string) =>
createVisitor({
visitMethodDeclaration: () => 1,
defaultResult: () => 0,
aggregateResult: (a, b) => a + b,
}).visit(parse(source));

expect(
countMethods(`
class A {
int a;
void b() {}
void c() {}
}
class B {
void z() {}
}
`),
).toEqual(3);
});
});
125 changes: 123 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,137 @@
// parse
import { ANTLRInputStream, CommonTokenStream } from 'antlr4ts';
import { JavaLexer } from './parser/JavaLexer';
import { CompilationUnitContext, JavaParser } from './parser/JavaParser';
export { CompilationUnitContext };

// walk
import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker';
import { JavaParserListener } from './parser/JavaParserListener';
import { JavaParserVisitor } from './parser/JavaParserVisitor';

// createVisitor
import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor';
import { ParseTreeVisitor } from 'antlr4ts/tree/ParseTreeVisitor';
import { RuleNode } from 'antlr4ts/tree/RuleNode';

/**
* Parses the given source code and returns the AST
* @param source Java source code to parse
*/
export function parse(source: string): CompilationUnitContext {
export function parse(source: string): ParseTree {
const chars = new ANTLRInputStream(source);
const lexer = new JavaLexer(chars);
const tokens = new CommonTokenStream(lexer);
const parser = new JavaParser(tokens);
return parser.compilationUnit();
}

// Just to create a more user-friendly name as all arguments that are name 'tree' take this
// (type alias doesn't create a new name)
// tslint:disable-next-line:no-empty-interface
export interface ParseTree extends CompilationUnitContext {}

/**
* Walks a parse tree
* @see https://github.com/antlr/antlr4/blob/master/doc/listeners.md
*/
export function walk(listener: JavaParserListener, tree: ParseTree) {
ParseTreeWalker.DEFAULT.walk(listener, tree);
}
export { JavaParserListener } from './parser/JavaParserListener';

/**
* Create a parse tree visitor
*/
export function createVisitor<T>(visitor: Visitor<T>): ConcreteVisitor<T>;
export function createVisitor(visitor: VoidVisitor): ConcreteVisitor<void>;
export function createVisitor<T>(visitor: Visitor<T>): ConcreteVisitor<T> {
// we don't want users to write classes because it's not JavaScript-y
// so we'll set implementation of abstract methods and other visit* methods in constructor
// @ts-ignore
return new class extends AbstractParseTreeVisitor<T> {
constructor() {
super();
Object.assign(this, {
defaultResult: () => undefined,
aggregateResult: () => undefined,
...visitor,
});
}
}();
}

export interface Visitor<T>
extends AbstractVisitor<T>,
OmitStrict<JavaParserVisitor<T>, NonOverridableMethods> {}

export interface VoidVisitor
extends OmitStrict<Visitor<void>, 'defaultResult' | 'aggregateResult'> {}

type NonOverridableMethods = keyof ParseTreeVisitor<any>;
type OmitStrict<T, K extends keyof T> = Omit<T, K>;

// Just to create a better name
export interface ConcreteVisitor<T> extends AbstractParseTreeVisitor<T> {}

// from AbstractParseTreeVisitor
interface AbstractVisitor<T> {
/**
* Gets the default value returned by visitor methods. This value is
* returned by the default implementations of
* {@link #visitTerminal visitTerminal}, {@link #visitErrorNode visitErrorNode}.
* The default implementation of {@link #visitChildren visitChildren}
* initializes its aggregate result to this value.
*
* @return The default value returned by visitor methods.
*/
defaultResult: () => T;

/**
* Aggregates the results of visiting multiple children of a node. After
* either all children are visited or {@link #shouldVisitNextChild} returns
* {@code false}, the aggregate value is returned as the result of
* {@link #visitChildren}.
*
* <p>The default implementation returns {@code nextResult}, meaning
* {@link #visitChildren} will return the result of the last child visited
* (or return the initial value if the node has no children).</p>
*
* @param aggregate The previous aggregate value. In the default
* implementation, the aggregate value is initialized to
* {@link #defaultResult}, which is passed as the {@code aggregate} argument
* to this method after the first child node is visited.
* @param nextResult The result of the immediately preceeding call to visit
* a child node.
*
* @return The updated aggregate result.
*/
aggregateResult: (aggregate: T, nextResult: T) => T;

/**
* This method is called after visiting each child in
* {@link #visitChildren}. This method is first called before the first
* child is visited; at that point {@code currentResult} will be the initial
* value (in the default implementation, the initial value is returned by a
* call to {@link #defaultResult}. This method is not called after the last
* child is visited.
*
* <p>The default implementation always returns {@code true}, indicating that
* {@code visitChildren} should only return after all children are visited.
* One reason to override this method is to provide a "short circuit"
* evaluation option for situations where the result of visiting a single
* child has the potential to determine the result of the visit operation as
* a whole.</p>
*
* @param node The {@link RuleNode} whose children are currently being
* visited.
* @param currentResult The current aggregate result of the children visited
* to the current point.
*
* @return {@code true} to continue visiting children. Otherwise return
* {@code false} to stop visiting children and immediately return the
* current aggregate result from {@link #visitChildren}.
*/
shouldVisitNextChild?: (node: RuleNode, currentResult: T) => boolean;
}

export * from './parser/JavaContexts';
Loading