feat: add JavaExtractor for tree-sitter Java structural analysis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-04-15 18:42:55 +08:00
Unverified
parent a1fb9585d3
commit 6e500cbfdf
2 changed files with 1017 additions and 0 deletions
@@ -0,0 +1,568 @@
import { describe, it, expect, beforeAll } from "vitest";
import { createRequire } from "node:module";
import { JavaExtractor } from "../java-extractor.js";
const require = createRequire(import.meta.url);
// Load tree-sitter + Java grammar once
let Parser: any;
let Language: any;
let javaLang: any;
beforeAll(async () => {
const mod = await import("web-tree-sitter");
Parser = mod.Parser;
Language = mod.Language;
await Parser.init();
const wasmPath = require.resolve(
"tree-sitter-java/tree-sitter-java.wasm",
);
javaLang = await Language.load(wasmPath);
});
function parse(code: string) {
const parser = new Parser();
parser.setLanguage(javaLang);
const tree = parser.parse(code);
const root = tree.rootNode;
return { tree, parser, root };
}
describe("JavaExtractor", () => {
const extractor = new JavaExtractor();
it("has correct languageIds", () => {
expect(extractor.languageIds).toEqual(["java"]);
});
// ---- Methods/Constructors (mapped to functions) ----
describe("extractStructure - functions (methods & constructors)", () => {
it("extracts methods with params and return types", () => {
const { tree, parser, root } = parse(`public class Foo {
public String getName(int id) {
return "";
}
private void process(String data, int count) {
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(2);
expect(result.functions[0].name).toBe("getName");
expect(result.functions[0].params).toEqual(["id"]);
expect(result.functions[0].returnType).toBe("String");
expect(result.functions[1].name).toBe("process");
expect(result.functions[1].params).toEqual(["data", "count"]);
expect(result.functions[1].returnType).toBe("void");
tree.delete();
parser.delete();
});
it("extracts constructors", () => {
const { tree, parser, root } = parse(`public class Foo {
public Foo(String name, int value) {
this.name = name;
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("Foo");
expect(result.functions[0].params).toEqual(["name", "value"]);
expect(result.functions[0].returnType).toBeUndefined();
tree.delete();
parser.delete();
});
it("extracts methods with no params", () => {
const { tree, parser, root } = parse(`public class Foo {
public void run() {}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("run");
expect(result.functions[0].params).toEqual([]);
expect(result.functions[0].returnType).toBe("void");
tree.delete();
parser.delete();
});
it("extracts methods with generic return types", () => {
const { tree, parser, root } = parse(`public class Foo {
public List<String> getItems() {
return null;
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("getItems");
expect(result.functions[0].returnType).toBe("List<String>");
tree.delete();
parser.delete();
});
it("reports correct line ranges for multi-line methods", () => {
const { tree, parser, root } = parse(`public class Foo {
public int calculate(
int a,
int b
) {
int result = a + b;
return result;
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].lineRange[0]).toBe(2);
expect(result.functions[0].lineRange[1]).toBe(8);
tree.delete();
parser.delete();
});
});
// ---- Classes ----
describe("extractStructure - classes", () => {
it("extracts class with methods and fields", () => {
const { tree, parser, root } = parse(`public class Server {
private String host;
private int port;
public void start() {}
public void stop() {}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Server");
expect(result.classes[0].properties).toEqual(["host", "port"]);
expect(result.classes[0].methods).toEqual(["start", "stop"]);
expect(result.classes[0].lineRange[0]).toBe(1);
tree.delete();
parser.delete();
});
it("extracts empty class", () => {
const { tree, parser, root } = parse(`public class Empty {
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Empty");
expect(result.classes[0].properties).toEqual([]);
expect(result.classes[0].methods).toEqual([]);
tree.delete();
parser.delete();
});
it("includes constructors in methods list", () => {
const { tree, parser, root } = parse(`public class Foo {
public Foo() {}
public void run() {}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes[0].methods).toEqual(["Foo", "run"]);
tree.delete();
parser.delete();
});
});
// ---- Interfaces ----
describe("extractStructure - interfaces", () => {
it("extracts interface with method signatures", () => {
const { tree, parser, root } = parse(`interface Repository {
List<User> findAll();
User findById(int id);
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Repository");
expect(result.classes[0].methods).toEqual(["findAll", "findById"]);
expect(result.classes[0].properties).toEqual([]);
tree.delete();
parser.delete();
});
it("extracts empty interface", () => {
const { tree, parser, root } = parse(`interface Marker {
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Marker");
expect(result.classes[0].methods).toEqual([]);
tree.delete();
parser.delete();
});
});
// ---- Imports ----
describe("extractStructure - imports", () => {
it("extracts regular imports", () => {
const { tree, parser, root } = parse(`import java.util.List;
import java.util.Map;
public class Foo {}
`);
const result = extractor.extractStructure(root);
expect(result.imports).toHaveLength(2);
expect(result.imports[0].source).toBe("java.util.List");
expect(result.imports[0].specifiers).toEqual(["List"]);
expect(result.imports[0].lineNumber).toBe(1);
expect(result.imports[1].source).toBe("java.util.Map");
expect(result.imports[1].specifiers).toEqual(["Map"]);
expect(result.imports[1].lineNumber).toBe(2);
tree.delete();
parser.delete();
});
it("extracts wildcard imports", () => {
const { tree, parser, root } = parse(`import java.util.*;
public class Foo {}
`);
const result = extractor.extractStructure(root);
expect(result.imports).toHaveLength(1);
expect(result.imports[0].source).toBe("java.util");
expect(result.imports[0].specifiers).toEqual(["*"]);
tree.delete();
parser.delete();
});
it("reports correct import line numbers", () => {
const { tree, parser, root } = parse(`import java.util.List;
import java.util.Map;
public class Foo {}
`);
const result = extractor.extractStructure(root);
expect(result.imports[0].lineNumber).toBe(1);
expect(result.imports[1].lineNumber).toBe(3);
tree.delete();
parser.delete();
});
});
// ---- Exports ----
describe("extractStructure - exports", () => {
it("exports public class, methods, and constructor", () => {
const { tree, parser, root } = parse(`public class UserService {
private String name;
public UserService(String name) {
this.name = name;
}
public void start() {}
private void helper() {}
}
`);
const result = extractor.extractStructure(root);
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("UserService"); // class
// The constructor is also named UserService, check it's listed
const userServiceExports = result.exports.filter(
(e) => e.name === "UserService",
);
expect(userServiceExports.length).toBe(2); // class + constructor
expect(exportNames).toContain("start");
expect(exportNames).not.toContain("helper");
expect(exportNames).not.toContain("name"); // private field
tree.delete();
parser.delete();
});
it("does not export non-public classes", () => {
const { tree, parser, root } = parse(`class Internal {
void run() {}
}
`);
const result = extractor.extractStructure(root);
expect(result.exports).toHaveLength(0);
tree.delete();
parser.delete();
});
it("exports public fields", () => {
const { tree, parser, root } = parse(`public class Config {
public String apiKey;
private int retries;
}
`);
const result = extractor.extractStructure(root);
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("Config");
expect(exportNames).toContain("apiKey");
expect(exportNames).not.toContain("retries");
tree.delete();
parser.delete();
});
it("exports public interface", () => {
const { tree, parser, root } = parse(`public interface Repository {
void save();
}
`);
const result = extractor.extractStructure(root);
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("Repository");
tree.delete();
parser.delete();
});
});
// ---- Call Graph ----
describe("extractCallGraph", () => {
it("extracts simple method calls", () => {
const { tree, parser, root } = parse(`public class Foo {
public void process(int data) {
transform(data);
format(data);
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(2);
expect(result[0].caller).toBe("process");
expect(result[0].callee).toBe("transform");
expect(result[1].caller).toBe("process");
expect(result[1].callee).toBe("format");
tree.delete();
parser.delete();
});
it("extracts qualified method calls (e.g. System.out.println)", () => {
const { tree, parser, root } = parse(`public class Foo {
private void log(String message) {
System.out.println(message);
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(1);
expect(result[0].caller).toBe("log");
expect(result[0].callee).toBe("System.out.println");
tree.delete();
parser.delete();
});
it("extracts object creation expressions", () => {
const { tree, parser, root } = parse(`public class Foo {
public void create() {
Bar b = new Bar();
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(1);
expect(result[0].caller).toBe("create");
expect(result[0].callee).toBe("new Bar");
tree.delete();
parser.delete();
});
it("tracks correct caller for constructors", () => {
const { tree, parser, root } = parse(`public class Foo {
public Foo() {
init();
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(1);
expect(result[0].caller).toBe("Foo");
expect(result[0].callee).toBe("init");
tree.delete();
parser.delete();
});
it("reports correct line numbers for calls", () => {
const { tree, parser, root } = parse(`public class Foo {
public void run() {
foo();
bar();
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(2);
expect(result[0].lineNumber).toBe(3);
expect(result[1].lineNumber).toBe(4);
tree.delete();
parser.delete();
});
it("ignores calls outside methods (no caller)", () => {
// Java doesn't really allow top-level calls, but field initializers
// can have method calls. We skip those without a method context.
const { tree, parser, root } = parse(`public class Foo {
private String value = String.valueOf(42);
}
`);
const result = extractor.extractCallGraph(root);
// No enclosing method, so these are skipped
expect(result).toHaveLength(0);
tree.delete();
parser.delete();
});
});
// ---- Comprehensive ----
describe("comprehensive Java file", () => {
it("handles a realistic Java module", () => {
const { tree, parser, root } = parse(`import java.util.List;
import java.util.Map;
public class UserService {
private String name;
private int maxRetries;
public UserService(String name) {
this.name = name;
}
public List<User> getUsers(int limit) {
return fetchFromDb(limit);
}
private void log(String message) {
System.out.println(message);
}
}
interface Repository {
List<User> findAll();
User findById(int id);
}
`);
const result = extractor.extractStructure(root);
// Functions: UserService (constructor), getUsers, log
expect(result.functions).toHaveLength(3);
expect(result.functions.map((f) => f.name).sort()).toEqual(
["UserService", "getUsers", "log"].sort(),
);
// Constructor has params but no return type
const ctor = result.functions.find((f) => f.name === "UserService");
expect(ctor?.params).toEqual(["name"]);
expect(ctor?.returnType).toBeUndefined();
// getUsers has params and generic return type
const getUsers = result.functions.find((f) => f.name === "getUsers");
expect(getUsers?.params).toEqual(["limit"]);
expect(getUsers?.returnType).toBe("List<User>");
// log has params and void return type
const log = result.functions.find((f) => f.name === "log");
expect(log?.params).toEqual(["message"]);
expect(log?.returnType).toBe("void");
// Classes: UserService, Repository
expect(result.classes).toHaveLength(2);
const userService = result.classes.find(
(c) => c.name === "UserService",
);
expect(userService).toBeDefined();
expect(userService!.methods.sort()).toEqual(
["UserService", "getUsers", "log"].sort(),
);
expect(userService!.properties.sort()).toEqual(
["name", "maxRetries"].sort(),
);
const repository = result.classes.find(
(c) => c.name === "Repository",
);
expect(repository).toBeDefined();
expect(repository!.methods).toEqual(["findAll", "findById"]);
expect(repository!.properties).toEqual([]);
// Imports: 2 (java.util.List, java.util.Map)
expect(result.imports).toHaveLength(2);
expect(result.imports[0].source).toBe("java.util.List");
expect(result.imports[0].specifiers).toEqual(["List"]);
expect(result.imports[1].source).toBe("java.util.Map");
expect(result.imports[1].specifiers).toEqual(["Map"]);
// Exports: UserService (class), UserService (constructor), getUsers (public method)
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("UserService");
expect(exportNames).toContain("getUsers");
expect(exportNames).not.toContain("log"); // private
expect(exportNames).not.toContain("name"); // private field
expect(exportNames).not.toContain("maxRetries"); // private field
// Call graph
const calls = extractor.extractCallGraph(root);
const getUsersCalls = calls.filter((e) => e.caller === "getUsers");
expect(getUsersCalls.some((e) => e.callee === "fetchFromDb")).toBe(
true,
);
const logCalls = calls.filter((e) => e.caller === "log");
expect(
logCalls.some((e) => e.callee === "System.out.println"),
).toBe(true);
tree.delete();
parser.delete();
});
});
});
@@ -0,0 +1,449 @@
import type { StructuralAnalysis, CallGraphEntry } from "../../types.js";
import type { LanguageExtractor, TreeSitterNode } from "./types.js";
import { findChild, findChildren } from "./base-extractor.js";
/**
* Extract parameter names from a Java `formal_parameters` node.
*
* Each `formal_parameter` child has a `name` field (identifier) and a `type` field.
*/
function extractParams(paramsNode: TreeSitterNode | null): string[] {
if (!paramsNode) return [];
const params: string[] = [];
const declarations = findChildren(paramsNode, "formal_parameter");
for (const decl of declarations) {
const nameNode = decl.childForFieldName("name");
if (nameNode) {
params.push(nameNode.text);
}
}
// Also handle spread_parameter (varargs): e.g. `String... args`
const spreadParams = findChildren(paramsNode, "spread_parameter");
for (const spread of spreadParams) {
const nameNode = spread.childForFieldName("name");
if (nameNode) {
params.push(nameNode.text);
}
}
return params;
}
/**
* Extract the return type text from a method_declaration node.
*
* In tree-sitter-java, the return type is the `type` named field on method_declaration.
* It can be a type_identifier, generic_type, void_type, integral_type, etc.
*/
function extractReturnType(node: TreeSitterNode): string | undefined {
const typeNode = node.childForFieldName("type");
if (!typeNode) return undefined;
return typeNode.text;
}
/**
* Check if a node has a `modifiers` child containing a specific modifier keyword.
*/
function hasModifier(node: TreeSitterNode, modifier: string): boolean {
const modifiers = findChild(node, "modifiers");
if (!modifiers) return false;
for (let i = 0; i < modifiers.childCount; i++) {
const child = modifiers.child(i);
if (child && child.text === modifier) return true;
}
return false;
}
/**
* Extract the full dotted path from a scoped_identifier node.
*
* Java's scoped_identifier nests recursively:
* `java.util.List` is scoped_identifier(scope: scoped_identifier(scope: identifier "java",
* name: identifier "util"), name: identifier "List")
*
* This returns the full path as a dotted string.
*/
function extractScopedIdentifierPath(node: TreeSitterNode): string {
return node.text;
}
/**
* Get the last component of a dotted import path.
* e.g. "java.util.List" -> "List"
*/
function lastComponent(path: string): string {
const parts = path.split(".");
return parts[parts.length - 1];
}
/**
* Java extractor for tree-sitter structural analysis and call graph extraction.
*
* Handles classes, interfaces, methods, constructors, fields, imports,
* visibility-based exports, and call graphs for Java source code.
*
* Java-specific mapping decisions:
* - Classes and interfaces are mapped to the `classes` array.
* - Constructors are mapped to the `functions` array (named after the class).
* - Methods (including interface method signatures) are listed in the
* containing class/interface's `methods` array and also in the `functions` array.
* - Exports are determined by the `public` modifier on classes, methods,
* constructors, and fields.
* - Fields are extracted as `properties` from `field_declaration` nodes.
*/
export class JavaExtractor implements LanguageExtractor {
readonly languageIds = ["java"];
extractStructure(rootNode: TreeSitterNode): StructuralAnalysis {
const functions: StructuralAnalysis["functions"] = [];
const classes: StructuralAnalysis["classes"] = [];
const imports: StructuralAnalysis["imports"] = [];
const exports: StructuralAnalysis["exports"] = [];
for (let i = 0; i < rootNode.childCount; i++) {
const node = rootNode.child(i);
if (!node) continue;
switch (node.type) {
case "import_declaration":
this.extractImport(node, imports);
break;
case "class_declaration":
this.extractClass(node, functions, classes, exports);
break;
case "interface_declaration":
this.extractInterface(node, functions, classes, exports);
break;
}
}
return { functions, classes, imports, exports };
}
extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] {
const entries: CallGraphEntry[] = [];
const functionStack: string[] = [];
const walkForCalls = (node: TreeSitterNode) => {
let pushedName = false;
// Track entering method/constructor declarations
if (
node.type === "method_declaration" ||
node.type === "constructor_declaration"
) {
const nameNode = node.childForFieldName("name");
if (nameNode) {
functionStack.push(nameNode.text);
pushedName = true;
}
}
// Extract method invocations: e.g. fetchFromDb(limit), System.out.println(msg)
if (node.type === "method_invocation") {
if (functionStack.length > 0) {
const callee = this.extractMethodInvocationName(node);
if (callee) {
entries.push({
caller: functionStack[functionStack.length - 1],
callee,
lineNumber: node.startPosition.row + 1,
});
}
}
}
// Extract object creation: e.g. new Foo()
if (node.type === "object_creation_expression") {
if (functionStack.length > 0) {
const typeNode = node.childForFieldName("type");
if (typeNode) {
entries.push({
caller: functionStack[functionStack.length - 1],
callee: `new ${typeNode.text}`,
lineNumber: node.startPosition.row + 1,
});
}
}
}
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child) walkForCalls(child);
}
if (pushedName) {
functionStack.pop();
}
};
walkForCalls(rootNode);
return entries;
}
// ---- Private helpers ----
/**
* Extract the callee name from a method_invocation node.
*
* Handles:
* - Plain method call: `fetchFromDb(limit)` -> "fetchFromDb"
* - Qualified call: `System.out.println(msg)` -> "System.out.println"
*/
private extractMethodInvocationName(node: TreeSitterNode): string | null {
const nameNode = node.childForFieldName("name");
if (!nameNode) return null;
const objectNode = node.childForFieldName("object");
if (objectNode) {
return `${objectNode.text}.${nameNode.text}`;
}
return nameNode.text;
}
private extractImport(
node: TreeSitterNode,
imports: StructuralAnalysis["imports"],
): void {
// Check for asterisk (wildcard) import: `import java.util.*;`
const hasAsterisk = findChild(node, "asterisk") !== null;
const scopedId = findChild(node, "scoped_identifier");
if (!scopedId) return;
const fullPath = extractScopedIdentifierPath(scopedId);
if (hasAsterisk) {
// Wildcard import: source is the full scope, specifier is "*"
imports.push({
source: fullPath,
specifiers: ["*"],
lineNumber: node.startPosition.row + 1,
});
} else {
// Regular import: source is the full path, specifier is the last component
imports.push({
source: fullPath,
specifiers: [lastComponent(fullPath)],
lineNumber: node.startPosition.row + 1,
});
}
}
private extractClass(
node: TreeSitterNode,
functions: StructuralAnalysis["functions"],
classes: StructuralAnalysis["classes"],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
const methods: string[] = [];
const properties: string[] = [];
const body = node.childForFieldName("body");
if (body) {
this.extractClassBodyMembers(
body,
methods,
properties,
functions,
exports,
);
}
classes.push({
name: nameNode.text,
lineRange: [
node.startPosition.row + 1,
node.endPosition.row + 1,
],
methods,
properties,
});
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
private extractInterface(
node: TreeSitterNode,
functions: StructuralAnalysis["functions"],
classes: StructuralAnalysis["classes"],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
const methods: string[] = [];
const properties: string[] = [];
const body = node.childForFieldName("body");
if (body) {
// Interface body contains method_declaration nodes (signatures without bodies)
const methodNodes = findChildren(body, "method_declaration");
for (const methodNode of methodNodes) {
const methNameNode = methodNode.childForFieldName("name");
if (methNameNode) {
methods.push(methNameNode.text);
}
}
// Interface can also contain constant_declaration (fields)
const fields = findChildren(body, "constant_declaration");
for (const field of fields) {
const declarators = findChildren(field, "variable_declarator");
for (const decl of declarators) {
const declName = decl.childForFieldName("name");
if (declName) {
properties.push(declName.text);
}
}
}
}
classes.push({
name: nameNode.text,
lineRange: [
node.startPosition.row + 1,
node.endPosition.row + 1,
],
methods,
properties,
});
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
/**
* Extract methods, constructors, and fields from a class_body node.
*/
private extractClassBodyMembers(
body: TreeSitterNode,
methods: string[],
properties: string[],
functions: StructuralAnalysis["functions"],
exports: StructuralAnalysis["exports"],
): void {
for (let i = 0; i < body.childCount; i++) {
const child = body.child(i);
if (!child) continue;
switch (child.type) {
case "method_declaration":
this.extractMethod(child, methods, functions, exports);
break;
case "constructor_declaration":
this.extractConstructor(child, methods, functions, exports);
break;
case "field_declaration":
this.extractField(child, properties, exports);
break;
}
}
}
private extractMethod(
node: TreeSitterNode,
methods: string[],
functions: StructuralAnalysis["functions"],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
const paramsNode = node.childForFieldName("parameters");
const params = extractParams(paramsNode ?? null);
const returnType = extractReturnType(node);
methods.push(nameNode.text);
functions.push({
name: nameNode.text,
lineRange: [
node.startPosition.row + 1,
node.endPosition.row + 1,
],
params,
returnType,
});
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
private extractConstructor(
node: TreeSitterNode,
methods: string[],
functions: StructuralAnalysis["functions"],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
const paramsNode = node.childForFieldName("parameters");
const params = extractParams(paramsNode ?? null);
methods.push(nameNode.text);
functions.push({
name: nameNode.text,
lineRange: [
node.startPosition.row + 1,
node.endPosition.row + 1,
],
params,
// Constructors have no return type
});
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
private extractField(
node: TreeSitterNode,
properties: string[],
exports: StructuralAnalysis["exports"],
): void {
const declarators = findChildren(node, "variable_declarator");
for (const decl of declarators) {
const nameNode = decl.childForFieldName("name");
if (nameNode) {
properties.push(nameNode.text);
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
}
}
}