mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat(java): synthesize Lombok @Data/@Getter/@Setter accessor methods
This commit is contained in:
parent
ac68f5254c
commit
e30a4f22d1
3 changed files with 757 additions and 0 deletions
439
gitnexus/src/core/ingestion/languages/java/lombok-synthesizer.ts
Normal file
439
gitnexus/src/core/ingestion/languages/java/lombok-synthesizer.ts
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
/**
|
||||
* Lombok method synthesizer for Java.
|
||||
*
|
||||
* Lombok generates accessor methods (getters/setters) at compile time via
|
||||
* annotation processors. These methods are absent from the AST — every call
|
||||
* to `obj.getOrderId()` on a `@Data` class is an unresolved call edge in the
|
||||
* static graph.
|
||||
*
|
||||
* This module walks the tree-sitter Java AST and, for each class annotated
|
||||
* with `@Getter`, `@Setter`, or `@Data`, synthesizes virtual Method graph
|
||||
* nodes for the accessor methods Lombok would generate. The output mirrors
|
||||
* the shape of real Method symbols/nodes/relationships so the rest of the
|
||||
* ingestion pipeline treats them identically.
|
||||
*
|
||||
* Synthesized methods are:
|
||||
* - **Public** (`visibility: 'public'`): Lombok's default access level.
|
||||
* - **Non-static**: only instance fields get accessors.
|
||||
* - **Skipped when a hand-written method of the same name already exists.**
|
||||
* - **Skipped for fields explicitly suppressed** via `@Getter(AccessLevel.NONE)`.
|
||||
*
|
||||
* Naming follows the JavaBeans convention Lombok uses:
|
||||
* - `String name` → `getName()` / `setName(String)`
|
||||
* - `boolean active` → `isActive()` / `setActive(boolean)`
|
||||
* - `Boolean active` → `getActive()` (boxed → getXxx, per Lombok)
|
||||
*/
|
||||
|
||||
import type Parser from 'tree-sitter';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** A field extracted from the AST for Lombok synthesis. */
|
||||
interface LombokField {
|
||||
name: string;
|
||||
type: string;
|
||||
isStatic: boolean;
|
||||
/** True when @Getter(AccessLevel.NONE) suppresses the getter for this field. */
|
||||
suppressGetter: boolean;
|
||||
}
|
||||
|
||||
/** A class eligible for Lombok accessor synthesis. */
|
||||
interface LombokClass {
|
||||
/** Tree-sitter node of the class_declaration. */
|
||||
node: Parser.SyntaxNode;
|
||||
/** Class simple name. */
|
||||
name: string;
|
||||
/** 'getter' and/or 'setter' depending on which annotations are present. */
|
||||
generateGetters: boolean;
|
||||
generateSetters: boolean;
|
||||
fields: LombokField[];
|
||||
/** Names of methods already declared in this class body (collision guard). */
|
||||
existingMethods: Set<string>;
|
||||
}
|
||||
|
||||
/** Synthetic symbol entry — mirrors the shape pushed to `result.symbols`. */
|
||||
export interface SyntheticSymbol {
|
||||
filePath: string;
|
||||
name: string;
|
||||
nodeId: string;
|
||||
type: 'Method';
|
||||
ownerId: string;
|
||||
parameterCount: number;
|
||||
requiredParameterCount: number;
|
||||
parameterTypes: string[];
|
||||
returnType: string;
|
||||
visibility: string;
|
||||
isStatic: boolean;
|
||||
isAbstract: boolean;
|
||||
isFinal: boolean;
|
||||
isLombok: true;
|
||||
}
|
||||
|
||||
/** Synthetic node entry — mirrors the shape pushed to `result.nodes`. */
|
||||
export interface SyntheticNode {
|
||||
id: string;
|
||||
label: 'Method';
|
||||
properties: Record<string, unknown> & {
|
||||
name: string;
|
||||
filePath: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
language: string;
|
||||
isExported: boolean;
|
||||
synthetic: 'lombok';
|
||||
visibility: string;
|
||||
isStatic: boolean;
|
||||
returnType: string;
|
||||
parameterTypes: string[];
|
||||
parameterCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** Synthetic relationship entry — mirrors the shape pushed to `result.relationships`. */
|
||||
export interface SyntheticRelationship {
|
||||
id: string;
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
type: 'HAS_METHOD';
|
||||
confidence: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface LombokSynthesisResult {
|
||||
symbols: SyntheticSymbol[];
|
||||
nodes: SyntheticNode[];
|
||||
relationships: SyntheticRelationship[];
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Capitalize the first letter of a string. */
|
||||
function capitalize(s: string): string {
|
||||
return s.length > 0 ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
||||
}
|
||||
|
||||
/** Generate the Lombok getter method name for a field. */
|
||||
function getterName(fieldName: string, fieldType: string): string {
|
||||
// Primitive boolean → isXxx(); everything else → getXxx()
|
||||
if (fieldType === 'boolean') {
|
||||
return `is${capitalize(fieldName)}`;
|
||||
}
|
||||
return `get${capitalize(fieldName)}`;
|
||||
}
|
||||
|
||||
/** Generate the Lombok setter method name for a field. */
|
||||
function setterName(fieldName: string): string {
|
||||
return `set${capitalize(fieldName)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract annotation simple names from a tree-sitter `modifiers` node.
|
||||
* Handles both `marker_annotation` (`@Data`) and `annotation` (`@Getter(...)`)
|
||||
* and their fully-qualified forms (`@lombok.Data`).
|
||||
*/
|
||||
function extractAnnotationNames(modifiersNode: Parser.SyntaxNode | null): Set<string> {
|
||||
const names = new Set<string>();
|
||||
if (!modifiersNode) return names;
|
||||
|
||||
for (const child of modifiersNode.children) {
|
||||
if (child.type !== 'marker_annotation' && child.type !== 'annotation') continue;
|
||||
// The name child is a named field 'name' within marker_annotation/annotation
|
||||
const nameNode = child.childForFieldName('name');
|
||||
const text = nameNode?.text ?? '';
|
||||
// Normalize to simple name: `lombok.Data` → `Data`
|
||||
const simpleName = text.split('.').pop() ?? text;
|
||||
if (simpleName) names.add(simpleName);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a field's Lombok accessor is suppressed.
|
||||
*
|
||||
* `@Getter(AccessLevel.NONE)` or `@Setter(AccessLevel.NONE)` on a field
|
||||
* disables that specific accessor. We check for the string `NONE` in the
|
||||
* annotation text as a lightweight heuristic — the annotation argument is
|
||||
* always an enum constant, so `NONE` uniquely identifies suppression.
|
||||
*/
|
||||
function isAccessorSuppressed(
|
||||
fieldNode: Parser.SyntaxNode,
|
||||
accessorType: 'Getter' | 'Setter',
|
||||
): boolean {
|
||||
const modifiers = fieldNode.children.find((c) => c.type === 'modifiers');
|
||||
if (!modifiers) return false;
|
||||
for (const child of modifiers.children) {
|
||||
if (child.type !== 'annotation') continue;
|
||||
const nameNode = child.childForFieldName('name');
|
||||
const simpleName = nameNode?.text.split('.').pop();
|
||||
if (simpleName === accessorType && child.text.includes('NONE')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a field declaration node to extract field name(s) and type.
|
||||
*
|
||||
* `private String name;` → [{ name: 'name', type: 'String', isStatic: false }]
|
||||
* `private final Long id = 0L;` → [{ name: 'id', type: 'Long', isStatic: false }]
|
||||
* `private int x, y;` → [{ name: 'x', ... }, { name: 'y', ... }]
|
||||
*/
|
||||
function parseFieldDeclaration(
|
||||
fieldNode: Parser.SyntaxNode,
|
||||
): { name: string; type: string; isStatic: boolean; suppressGetter: boolean }[] {
|
||||
const results: { name: string; type: string; isStatic: boolean; suppressGetter: boolean }[] = [];
|
||||
|
||||
// Type is in the `type` field
|
||||
const typeNode = fieldNode.childForFieldName('type');
|
||||
const fieldType = typeNode?.text ?? 'Object';
|
||||
|
||||
// Check static — tree-sitter-java uses the keyword itself as the node type
|
||||
// (e.g. `static`, `final`), not a wrapper `modifier` node.
|
||||
const modifiers = fieldNode.children.find((c) => c.type === 'modifiers');
|
||||
let isStatic = false;
|
||||
if (modifiers) {
|
||||
for (const mod of modifiers.children) {
|
||||
if (mod.text === 'static') {
|
||||
isStatic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all variable declarators — handles both single (`int x;`) and
|
||||
// multi-variable (`int x, y;`) declarations. The `declarator` field name
|
||||
// returns only the first one; the rest are unnamed children.
|
||||
const declarators: Parser.SyntaxNode[] = [];
|
||||
const declaratorField = fieldNode.childForFieldName('declarator');
|
||||
if (declaratorField) {
|
||||
declarators.push(declaratorField);
|
||||
}
|
||||
// Also collect unnamed variable_declarator children (multi-variable case)
|
||||
for (const child of fieldNode.children) {
|
||||
if (child.type === 'variable_declarator' && child !== declaratorField) {
|
||||
declarators.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
for (const declaratorNode of declarators) {
|
||||
const nameNode = declaratorNode.childForFieldName('name');
|
||||
if (nameNode) {
|
||||
results.push({ name: nameNode.text, type: fieldType, isStatic, suppressGetter: false });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect method names from a class body (for collision detection).
|
||||
* Walks direct children of the class body for `method_declaration` nodes.
|
||||
*/
|
||||
function collectExistingMethodNames(classBody: Parser.SyntaxNode | null): Set<string> {
|
||||
const names = new Set<string>();
|
||||
if (!classBody) return names;
|
||||
for (const child of classBody.children) {
|
||||
if (child.type === 'method_declaration') {
|
||||
const nameNode = child.childForFieldName('name');
|
||||
if (nameNode) names.add(nameNode.text);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Walk the tree for class_declaration nodes eligible for Lombok synthesis. */
|
||||
function findLombokClasses(root: Parser.SyntaxNode): LombokClass[] {
|
||||
const classes: LombokClass[] = [];
|
||||
|
||||
function walk(node: Parser.SyntaxNode): void {
|
||||
if (node.type === 'class_declaration') {
|
||||
const modifiers = node.children.find((c) => c.type === 'modifiers');
|
||||
const annotations = extractAnnotationNames(modifiers);
|
||||
|
||||
const hasGetter = annotations.has('Getter') || annotations.has('Data');
|
||||
const hasSetter = annotations.has('Setter') || annotations.has('Data');
|
||||
|
||||
if (hasGetter || hasSetter) {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
const className = nameNode?.text ?? '';
|
||||
if (className) {
|
||||
// Find class body
|
||||
const body = node.children.find((c) => c.type === 'class_body');
|
||||
const existingMethods = collectExistingMethodNames(body ?? null);
|
||||
|
||||
// Collect fields
|
||||
const fields: LombokField[] = [];
|
||||
if (body) {
|
||||
for (const child of body.children) {
|
||||
if (child.type !== 'field_declaration') continue;
|
||||
for (const f of parseFieldDeclaration(child)) {
|
||||
// Skip static fields (Lombok doesn't generate instance accessors for static fields)
|
||||
if (f.isStatic) continue;
|
||||
// Mark getter as suppressed if @Getter(AccessLevel.NONE) is on the field
|
||||
if (hasGetter && isAccessorSuppressed(child, 'Getter')) {
|
||||
f.suppressGetter = true;
|
||||
}
|
||||
fields.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
classes.push({
|
||||
node,
|
||||
name: className,
|
||||
generateGetters: hasGetter,
|
||||
generateSetters: hasSetter,
|
||||
fields,
|
||||
existingMethods,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into children for nested classes
|
||||
for (const child of node.children) walk(child);
|
||||
}
|
||||
|
||||
walk(root);
|
||||
return classes;
|
||||
}
|
||||
|
||||
// ── Main API ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Synthesize Lombok accessor methods for a Java file.
|
||||
*
|
||||
* Call this after the normal AST capture loop in parse-worker, for Java files
|
||||
* only. The returned symbols/nodes/relationships should be pushed into the
|
||||
* worker result so they flow through the rest of the pipeline unchanged.
|
||||
*
|
||||
* @param tree The parsed tree-sitter Java AST.
|
||||
* @param filePath Absolute file path.
|
||||
* @param classNodeIds Map from class simple name → graph node ID, for the
|
||||
* classes that already exist in `result.nodes`. Only
|
||||
* classes present in this map get synthesized methods.
|
||||
* @returns Synthesis result, or empty if no Lombok classes found.
|
||||
*/
|
||||
export function synthesizeLombokAccessors(
|
||||
tree: Parser.Tree,
|
||||
filePath: string,
|
||||
classNodeIds: Map<string, string>,
|
||||
): LombokSynthesisResult {
|
||||
const result: LombokSynthesisResult = {
|
||||
symbols: [],
|
||||
nodes: [],
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const lombokClasses = findLombokClasses(tree.rootNode);
|
||||
|
||||
for (const cls of lombokClasses) {
|
||||
const ownerId = classNodeIds.get(cls.name);
|
||||
if (!ownerId) continue; // Class not in the graph — skip
|
||||
|
||||
for (const field of cls.fields) {
|
||||
// Getter (skip if suppressed by @Getter(AccessLevel.NONE))
|
||||
if (cls.generateGetters && !field.suppressGetter) {
|
||||
const gName = getterName(field.name, field.type);
|
||||
if (!cls.existingMethods.has(gName)) {
|
||||
const nodeId = `Method:${filePath}:${cls.name}.${gName}#0`;
|
||||
result.nodes.push({
|
||||
id: nodeId,
|
||||
label: 'Method',
|
||||
properties: {
|
||||
name: gName,
|
||||
filePath,
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
language: 'java',
|
||||
isExported: false,
|
||||
synthetic: 'lombok',
|
||||
visibility: 'public',
|
||||
isStatic: false,
|
||||
returnType: field.type,
|
||||
parameterTypes: [],
|
||||
parameterCount: 0,
|
||||
},
|
||||
});
|
||||
result.symbols.push({
|
||||
filePath,
|
||||
name: gName,
|
||||
nodeId,
|
||||
type: 'Method',
|
||||
ownerId,
|
||||
parameterCount: 0,
|
||||
requiredParameterCount: 0,
|
||||
parameterTypes: [],
|
||||
returnType: field.type,
|
||||
visibility: 'public',
|
||||
isStatic: false,
|
||||
isAbstract: false,
|
||||
isFinal: false,
|
||||
isLombok: true,
|
||||
});
|
||||
result.relationships.push({
|
||||
id: `HAS_METHOD:${ownerId}->${nodeId}`,
|
||||
sourceId: ownerId,
|
||||
targetId: nodeId,
|
||||
type: 'HAS_METHOD',
|
||||
confidence: 1.0,
|
||||
reason: 'lombok-getter',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Setter
|
||||
if (cls.generateSetters) {
|
||||
const sName = setterName(field.name);
|
||||
if (!cls.existingMethods.has(sName)) {
|
||||
const nodeId = `Method:${filePath}:${cls.name}.${sName}#1`;
|
||||
result.nodes.push({
|
||||
id: nodeId,
|
||||
label: 'Method',
|
||||
properties: {
|
||||
name: sName,
|
||||
filePath,
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
language: 'java',
|
||||
isExported: false,
|
||||
synthetic: 'lombok',
|
||||
visibility: 'public',
|
||||
isStatic: false,
|
||||
returnType: 'void',
|
||||
parameterTypes: [field.type],
|
||||
parameterCount: 1,
|
||||
},
|
||||
});
|
||||
result.symbols.push({
|
||||
filePath,
|
||||
name: sName,
|
||||
nodeId,
|
||||
type: 'Method',
|
||||
ownerId,
|
||||
parameterCount: 1,
|
||||
requiredParameterCount: 1,
|
||||
parameterTypes: [field.type],
|
||||
returnType: 'void',
|
||||
visibility: 'public',
|
||||
isStatic: false,
|
||||
isAbstract: false,
|
||||
isFinal: false,
|
||||
isLombok: true,
|
||||
});
|
||||
result.relationships.push({
|
||||
id: `HAS_METHOD:${ownerId}->${nodeId}`,
|
||||
sourceId: ownerId,
|
||||
targetId: nodeId,
|
||||
type: 'HAS_METHOD',
|
||||
confidence: 1.0,
|
||||
reason: 'lombok-setter',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -119,6 +119,7 @@ import type { ConstructorBinding } from '../type-env.js';
|
|||
import { detectFrameworkFromAST } from '../framework-detection.js';
|
||||
import { generateId } from '../../../lib/utils.js';
|
||||
import { defaultExportNameCollides } from '../languages/typescript/cjs-export-assignment.js';
|
||||
import { synthesizeLombokAccessors } from '../languages/java/lombok-synthesizer.js';
|
||||
import {
|
||||
extractVueScript,
|
||||
extractTemplateComponents,
|
||||
|
|
@ -3081,6 +3082,25 @@ const processFileGroup = (
|
|||
if (springTypes.length > 0) (result.springTypes ??= []).push(...springTypes);
|
||||
}
|
||||
|
||||
// Lombok accessor synthesis: generate virtual Method nodes for getter/setter
|
||||
// methods that Lombok generates at compile time (@Data/@Getter/@Setter).
|
||||
// Java only — the synthesizer walks the AST for annotated classes.
|
||||
if (language === SupportedLanguages.Java) {
|
||||
// Build class-name → node-ID map from symbols created in this file
|
||||
const classNodeIds = new Map<string, string>();
|
||||
for (const sym of result.symbols) {
|
||||
if (sym.type === 'Class' && sym.name) {
|
||||
classNodeIds.set(sym.name, sym.nodeId);
|
||||
}
|
||||
}
|
||||
const lombok = synthesizeLombokAccessors(tree, file.path, classNodeIds);
|
||||
if (lombok.symbols.length > 0) {
|
||||
for (const node of lombok.nodes) result.nodes.push(node as never);
|
||||
for (const sym of lombok.symbols) result.symbols.push(sym as never);
|
||||
for (const rel of lombok.relationships) result.relationships.push(rel as never);
|
||||
}
|
||||
}
|
||||
|
||||
// Vue: emit CALLS edges for components used in <template>
|
||||
if (language === SupportedLanguages.Vue) {
|
||||
const templateComponents = extractTemplateComponents(file.content);
|
||||
|
|
|
|||
298
gitnexus/test/unit/lombok-synthesizer.test.ts
Normal file
298
gitnexus/test/unit/lombok-synthesizer.test.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
/**
|
||||
* Unit test: Lombok accessor method synthesis.
|
||||
*
|
||||
* Tests the lombok-synthesizer module directly (no worker pool needed).
|
||||
* Verifies that @Data/@Getter/@Setter annotated classes produce the correct
|
||||
* synthetic getter/setter methods, with proper naming conventions, collision
|
||||
* guards, and AccessLevel.NONE suppression.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import Parser from 'tree-sitter';
|
||||
import Java from 'tree-sitter-java';
|
||||
import { synthesizeLombokAccessors } from '../../src/core/ingestion/languages/java/lombok-synthesizer.js';
|
||||
|
||||
function parse(code: string): Parser.Tree {
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(Java);
|
||||
return parser.parse(code);
|
||||
}
|
||||
|
||||
const FILE_PATH = '/test/Order.java';
|
||||
|
||||
describe('synthesizeLombokAccessors', () => {
|
||||
describe('@Data annotation', () => {
|
||||
it('generates both getter and setter for each field', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Order {
|
||||
private String orderId;
|
||||
private Long amount;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Order', 'Class:/test/Order.java:Order']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
// 2 fields × 2 (getter + setter) = 4 synthetic methods
|
||||
expect(result.symbols).toHaveLength(4);
|
||||
|
||||
const names = result.symbols.map((s) => s.name).sort();
|
||||
expect(names).toEqual(['getAmount', 'getOrderId', 'setAmount', 'setOrderId']);
|
||||
});
|
||||
|
||||
it('sets correct return types and parameter types', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Order {
|
||||
private String orderId;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Order', 'Class:/test/Order.java:Order']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
const getter = result.symbols.find((s) => s.name === 'getOrderId')!;
|
||||
expect(getter.returnType).toBe('String');
|
||||
expect(getter.parameterTypes).toEqual([]);
|
||||
expect(getter.parameterCount).toBe(0);
|
||||
|
||||
const setter = result.symbols.find((s) => s.name === 'setOrderId')!;
|
||||
expect(setter.returnType).toBe('void');
|
||||
expect(setter.parameterTypes).toEqual(['String']);
|
||||
expect(setter.parameterCount).toBe(1);
|
||||
});
|
||||
|
||||
it('creates HAS_METHOD relationships linking to the class', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Order {
|
||||
private String orderId;
|
||||
}
|
||||
`);
|
||||
const classId = 'Class:/test/Order.java:Order';
|
||||
const classNodeIds = new Map([['Order', classId]]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
for (const rel of result.relationships) {
|
||||
expect(rel.type).toBe('HAS_METHOD');
|
||||
expect(rel.sourceId).toBe(classId);
|
||||
expect(rel.confidence).toBe(1.0);
|
||||
}
|
||||
expect(result.relationships.every((r) => r.reason.startsWith('lombok-'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('boolean field naming convention', () => {
|
||||
it('uses isXxx() for primitive boolean fields', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Config {
|
||||
private boolean active;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Config', 'Class:/test/Config.java:Config']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
const names = result.symbols.map((s) => s.name).sort();
|
||||
expect(names).toContain('isActive');
|
||||
expect(names).not.toContain('getActive');
|
||||
// Setter is always setXxx regardless of type
|
||||
expect(names).toContain('setActive');
|
||||
});
|
||||
|
||||
it('uses getXxx() for Boolean (boxed) fields', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Config {
|
||||
private Boolean enabled;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Config', 'Class:/test/Config.java:Config']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
const names = result.symbols.map((s) => s.name).sort();
|
||||
expect(names).toContain('getEnabled');
|
||||
expect(names).not.toContain('isEnabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('individual @Getter and @Setter', () => {
|
||||
it('only generates getters with @Getter', () => {
|
||||
const tree = parse(`
|
||||
@Getter
|
||||
public class ReadOnly {
|
||||
private String name;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['ReadOnly', 'Class:/test/ReadOnly.java:ReadOnly']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
expect(result.symbols).toHaveLength(1);
|
||||
expect(result.symbols[0].name).toBe('getName');
|
||||
});
|
||||
|
||||
it('only generates setters with @Setter', () => {
|
||||
const tree = parse(`
|
||||
@Setter
|
||||
public class WriteOnly {
|
||||
private String name;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['WriteOnly', 'Class:/test/WriteOnly.java:WriteOnly']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
expect(result.symbols).toHaveLength(1);
|
||||
expect(result.symbols[0].name).toBe('setName');
|
||||
});
|
||||
});
|
||||
|
||||
describe('collision guard', () => {
|
||||
it('skips getter when a hand-written method of the same name exists', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Order {
|
||||
private String orderId;
|
||||
|
||||
public String getOrderId() {
|
||||
return customLogic();
|
||||
}
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Order', 'Class:/test/Order.java:Order']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
// Getter is skipped (hand-written exists), setter is still generated
|
||||
expect(result.symbols).toHaveLength(1);
|
||||
expect(result.symbols[0].name).toBe('setOrderId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('static field exclusion', () => {
|
||||
it('does not generate accessors for static fields', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Constants {
|
||||
private static String GLOBAL = "default";
|
||||
private String instance;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Constants', 'Class:/test/Constants.java:Constants']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
// Only the instance field gets accessors
|
||||
const names = result.symbols.map((s) => s.name).sort();
|
||||
expect(names).toEqual(['getInstance', 'setInstance']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AccessLevel.NONE suppression', () => {
|
||||
it('skips getter when @Getter(AccessLevel.NONE) is on a field', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Order {
|
||||
@Getter(AccessLevel.NONE)
|
||||
private String secret;
|
||||
private String name;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Order', 'Class:/test/Order.java:Order']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
const names = result.symbols.map((s) => s.name).sort();
|
||||
// secret getter is suppressed, but setter is still generated (@Data includes @Setter)
|
||||
expect(names).toEqual(['getName', 'setName', 'setSecret']);
|
||||
expect(names).not.toContain('getSecret');
|
||||
});
|
||||
});
|
||||
|
||||
describe('no Lombok annotations', () => {
|
||||
it('returns empty result for classes without Lombok annotations', () => {
|
||||
const tree = parse(`
|
||||
public class PlainClass {
|
||||
private String name;
|
||||
|
||||
public String getName() { return name; }
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['PlainClass', 'Class:/test/PlainClass.java:PlainClass']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
expect(result.symbols).toHaveLength(0);
|
||||
expect(result.nodes).toHaveLength(0);
|
||||
expect(result.relationships).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('class not in graph', () => {
|
||||
it('skips classes not present in the classNodeIds map', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Orphan {
|
||||
private String name;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map<string, string>(); // empty
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
expect(result.symbols).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested classes', () => {
|
||||
it('handles nested @Data classes', () => {
|
||||
const tree = parse(`
|
||||
public class Outer {
|
||||
@Data
|
||||
public static class Inner {
|
||||
private String value;
|
||||
}
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([
|
||||
['Inner', 'Class:/test/Outer.java:Inner'],
|
||||
['Outer', 'Class:/test/Outer.java:Outer'],
|
||||
]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
// Only Inner has @Data
|
||||
expect(result.symbols).toHaveLength(2);
|
||||
const names = result.symbols.map((s) => s.name).sort();
|
||||
expect(names).toEqual(['getValue', 'setValue']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-variable declarations', () => {
|
||||
it('handles `int x, y;` style declarations', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Point {
|
||||
private int x, y;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Point', 'Class:/test/Point.java:Point']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
// 2 fields × 2 accessors = 4 methods
|
||||
expect(result.symbols).toHaveLength(4);
|
||||
const names = result.symbols.map((s) => s.name).sort();
|
||||
expect(names).toEqual(['getX', 'getY', 'setX', 'setY']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('node properties', () => {
|
||||
it('marks synthetic methods with synthetic: lombok', () => {
|
||||
const tree = parse(`
|
||||
@Data
|
||||
public class Order {
|
||||
private String orderId;
|
||||
}
|
||||
`);
|
||||
const classNodeIds = new Map([['Order', 'Class:/test/Order.java:Order']]);
|
||||
const result = synthesizeLombokAccessors(tree, FILE_PATH, classNodeIds);
|
||||
|
||||
for (const node of result.nodes) {
|
||||
expect(node.properties.synthetic).toBe('lombok');
|
||||
expect(node.properties.visibility).toBe('public');
|
||||
expect(node.properties.isStatic).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue