Skip to content

Commit 870e9bb

Browse files
authored
Merge pull request #11 from devanshj/buncha-improvements
Fixes: grammar for super invocation, Features: `walk`, `createVisitor`
2 parents b7be319 + 1211240 commit 870e9bb

File tree

13 files changed

+258
-16182
lines changed

13 files changed

+258
-16182
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
node_modules
22
dist
3+
src/parser/JavaParser.ts
4+
src/parser/JavaParserListener.ts
5+
src/parser/JavaParserVisitor.ts
6+
src/parser/JavaLexer.ts
7+
src/parser/JavaContexts.ts

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# java-ast
22

3-
Java Parser for JavaScript/TypeScript, based on [antlr4ts](https://www.npmjs.com/package/antlr4ts)
3+
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)
44

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

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

14-
const ast = parse(`package test;\n\nclass TestClass {}\n`);
15-
// do something with ast, e.g. console.log(ast.toStringTree());
14+
const countMethods = (source: string) => {
15+
let ast = parse(source);
16+
17+
return createVisitor({
18+
visitMethodDeclaration: () => 1,
19+
defaultResult: () => 0,
20+
aggregateResult: (a, b) => a + b,
21+
}).visit(ast);
22+
};
23+
24+
console.log(
25+
countMethods(`
26+
class A {
27+
int a;
28+
void b() {}
29+
void c() {}
30+
}
31+
class B {
32+
void z() {}
33+
}
34+
`),
35+
); // logs 3
1636
```

generate-contexts.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// @ts-check
2+
3+
const fs = require('fs').promises;
4+
const path = require('path');
5+
const { EOL } = require('os');
6+
7+
const listenerFilePath = path.join(__dirname, '/src/parser/JavaParserListener.ts');
8+
const contextsFilePath = path.join(__dirname, '/src/parser/JavaContexts.ts');
9+
10+
const main = () =>
11+
fs
12+
.stat(listenerFilePath)
13+
.catch(
14+
() => 'src/parser/JavaParserListener.ts not found use generate:parser script to generate it',
15+
)
16+
.then(() => fs.readFile(listenerFilePath, 'utf-8'))
17+
.then((listenerSource) =>
18+
listenerSource
19+
.split(EOL)
20+
.map((l) => {
21+
let matches = l.match(/import\s*\{\s*(.*Context)\s*\}.*/);
22+
if (matches === null) return null;
23+
return matches[1];
24+
})
25+
.filter((c) => c !== null),
26+
)
27+
.then((contexts) => contexts.reduce((list, context) => list + ` ${context},${EOL}`, ''))
28+
.then((exportList) => `export {${EOL}${exportList}} from './JavaParser';`)
29+
.then((contextsSource) => fs.writeFile(contextsFilePath, contextsSource));
30+
31+
main().catch((error) => {
32+
if (typeof error !== 'string') throw error;
33+
console.log('Failure: ' + error);
34+
process.exit(1);
35+
});

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
"format": "prettier --write src/**.ts **/*.json",
2626
"prepublish": "yarn build",
2727
"generate:parser": "antlr4ts -visitor -o src/parser src/parser/JavaLexer.g4 src/parser/JavaParser.g4",
28+
"generate:contexts": "node generate-contexts.js",
2829
"precommit": "lint-staged",
2930
"postcommit": "git update-index --again",
3031
"test": "jest"
3132
},
3233
"devDependencies": {
3334
"@types/jest": "^23.1.5",
35+
"@types/node": "^14.0.22",
3436
"antlr4ts-cli": "^0.4.0-alpha.4",
3537
"husky": "^0.14.3",
3638
"jest": "^23.3.0",
@@ -39,7 +41,7 @@
3941
"rimraf": "^2.6.2",
4042
"ts-jest": "^23.0.0",
4143
"tslint": "^5.10.0",
42-
"typescript": "~2.8.2"
44+
"typescript": "^3.9.6"
4345
},
4446
"dependencies": {
4547
"antlr4ts": "^0.4.1-alpha.0"

src/index.spec.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,65 @@
1-
import { parse } from './index';
1+
import { createVisitor, parse, walk } from './index';
22

3-
describe('Java AST parser', () => {
3+
describe('parser', () => {
44
it('should parse the given Java code and return the AST', () => {
55
const tree = parse(`
66
class TestClass {
77
}
88
`);
99
expect(tree.children[0].getChild(0).getChild(1).text).toEqual('TestClass');
1010
});
11+
12+
it('should handle super invocation with arguments', () => {
13+
const tree = parse(`
14+
class B extends A {
15+
public B() {
16+
super(1);
17+
}
18+
}
19+
`);
20+
21+
const expressions = [];
22+
walk({ enterExpression: (c) => expressions.push(c.text) }, tree);
23+
24+
expect(expressions).toContain('super(1)');
25+
});
26+
27+
it('should allow super alone as expression', () => {
28+
const tree = parse(`
29+
class B extends A {
30+
public B() {
31+
super;
32+
}
33+
}
34+
`);
35+
36+
const expressions = [];
37+
walk({ enterExpression: (c) => expressions.push(c.text) }, tree);
38+
39+
expect(expressions).toContain('super');
40+
});
41+
});
42+
43+
describe('usage example', () => {
44+
it('works', () => {
45+
const countMethods = (source: string) =>
46+
createVisitor({
47+
visitMethodDeclaration: () => 1,
48+
defaultResult: () => 0,
49+
aggregateResult: (a, b) => a + b,
50+
}).visit(parse(source));
51+
52+
expect(
53+
countMethods(`
54+
class A {
55+
int a;
56+
void b() {}
57+
void c() {}
58+
}
59+
class B {
60+
void z() {}
61+
}
62+
`),
63+
).toEqual(3);
64+
});
1165
});

src/index.ts

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,137 @@
1+
// parse
12
import { ANTLRInputStream, CommonTokenStream } from 'antlr4ts';
23
import { JavaLexer } from './parser/JavaLexer';
34
import { CompilationUnitContext, JavaParser } from './parser/JavaParser';
4-
export { CompilationUnitContext };
5+
6+
// walk
7+
import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker';
8+
import { JavaParserListener } from './parser/JavaParserListener';
9+
import { JavaParserVisitor } from './parser/JavaParserVisitor';
10+
11+
// createVisitor
12+
import { AbstractParseTreeVisitor } from 'antlr4ts/tree/AbstractParseTreeVisitor';
13+
import { ParseTreeVisitor } from 'antlr4ts/tree/ParseTreeVisitor';
14+
import { RuleNode } from 'antlr4ts/tree/RuleNode';
515

616
/**
717
* Parses the given source code and returns the AST
818
* @param source Java source code to parse
919
*/
10-
export function parse(source: string): CompilationUnitContext {
20+
export function parse(source: string): ParseTree {
1121
const chars = new ANTLRInputStream(source);
1222
const lexer = new JavaLexer(chars);
1323
const tokens = new CommonTokenStream(lexer);
1424
const parser = new JavaParser(tokens);
1525
return parser.compilationUnit();
1626
}
27+
28+
// Just to create a more user-friendly name as all arguments that are name 'tree' take this
29+
// (type alias doesn't create a new name)
30+
// tslint:disable-next-line:no-empty-interface
31+
export interface ParseTree extends CompilationUnitContext {}
32+
33+
/**
34+
* Walks a parse tree
35+
* @see https://github.com/antlr/antlr4/blob/master/doc/listeners.md
36+
*/
37+
export function walk(listener: JavaParserListener, tree: ParseTree) {
38+
ParseTreeWalker.DEFAULT.walk(listener, tree);
39+
}
40+
export { JavaParserListener } from './parser/JavaParserListener';
41+
42+
/**
43+
* Create a parse tree visitor
44+
*/
45+
export function createVisitor<T>(visitor: Visitor<T>): ConcreteVisitor<T>;
46+
export function createVisitor(visitor: VoidVisitor): ConcreteVisitor<void>;
47+
export function createVisitor<T>(visitor: Visitor<T>): ConcreteVisitor<T> {
48+
// we don't want users to write classes because it's not JavaScript-y
49+
// so we'll set implementation of abstract methods and other visit* methods in constructor
50+
// @ts-ignore
51+
return new class extends AbstractParseTreeVisitor<T> {
52+
constructor() {
53+
super();
54+
Object.assign(this, {
55+
defaultResult: () => undefined,
56+
aggregateResult: () => undefined,
57+
...visitor,
58+
});
59+
}
60+
}();
61+
}
62+
63+
export interface Visitor<T>
64+
extends AbstractVisitor<T>,
65+
OmitStrict<JavaParserVisitor<T>, NonOverridableMethods> {}
66+
67+
export interface VoidVisitor
68+
extends OmitStrict<Visitor<void>, 'defaultResult' | 'aggregateResult'> {}
69+
70+
type NonOverridableMethods = keyof ParseTreeVisitor<any>;
71+
type OmitStrict<T, K extends keyof T> = Omit<T, K>;
72+
73+
// Just to create a better name
74+
export interface ConcreteVisitor<T> extends AbstractParseTreeVisitor<T> {}
75+
76+
// from AbstractParseTreeVisitor
77+
interface AbstractVisitor<T> {
78+
/**
79+
* Gets the default value returned by visitor methods. This value is
80+
* returned by the default implementations of
81+
* {@link #visitTerminal visitTerminal}, {@link #visitErrorNode visitErrorNode}.
82+
* The default implementation of {@link #visitChildren visitChildren}
83+
* initializes its aggregate result to this value.
84+
*
85+
* @return The default value returned by visitor methods.
86+
*/
87+
defaultResult: () => T;
88+
89+
/**
90+
* Aggregates the results of visiting multiple children of a node. After
91+
* either all children are visited or {@link #shouldVisitNextChild} returns
92+
* {@code false}, the aggregate value is returned as the result of
93+
* {@link #visitChildren}.
94+
*
95+
* <p>The default implementation returns {@code nextResult}, meaning
96+
* {@link #visitChildren} will return the result of the last child visited
97+
* (or return the initial value if the node has no children).</p>
98+
*
99+
* @param aggregate The previous aggregate value. In the default
100+
* implementation, the aggregate value is initialized to
101+
* {@link #defaultResult}, which is passed as the {@code aggregate} argument
102+
* to this method after the first child node is visited.
103+
* @param nextResult The result of the immediately preceeding call to visit
104+
* a child node.
105+
*
106+
* @return The updated aggregate result.
107+
*/
108+
aggregateResult: (aggregate: T, nextResult: T) => T;
109+
110+
/**
111+
* This method is called after visiting each child in
112+
* {@link #visitChildren}. This method is first called before the first
113+
* child is visited; at that point {@code currentResult} will be the initial
114+
* value (in the default implementation, the initial value is returned by a
115+
* call to {@link #defaultResult}. This method is not called after the last
116+
* child is visited.
117+
*
118+
* <p>The default implementation always returns {@code true}, indicating that
119+
* {@code visitChildren} should only return after all children are visited.
120+
* One reason to override this method is to provide a "short circuit"
121+
* evaluation option for situations where the result of visiting a single
122+
* child has the potential to determine the result of the visit operation as
123+
* a whole.</p>
124+
*
125+
* @param node The {@link RuleNode} whose children are currently being
126+
* visited.
127+
* @param currentResult The current aggregate result of the children visited
128+
* to the current point.
129+
*
130+
* @return {@code true} to continue visiting children. Otherwise return
131+
* {@code false} to stop visiting children and immediately return the
132+
* current aggregate result from {@link #visitChildren}.
133+
*/
134+
shouldVisitNextChild?: (node: RuleNode, currentResult: T) => boolean;
135+
}
136+
137+
export * from './parser/JavaContexts';

0 commit comments

Comments
 (0)