gitnexus 1.6.11-rc.18 → 1.6.11-rc.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ingestion/language-provider.d.ts +43 -0
- package/dist/core/ingestion/languages/java/captures.js +2 -0
- package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
- package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
- package/dist/core/ingestion/languages/java.js +2 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
- package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
- package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +2 -0
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/scope-extractor.js +1 -0
- package/dist/core/ingestion/workers/parse-worker.js +24 -0
- package/dist/storage/parse-cache.js +14 -1
- package/package.json +1 -1
|
@@ -297,6 +297,49 @@ interface LanguageProviderConfig {
|
|
|
297
297
|
* Default: undefined (no interface-inheritance route resolution).
|
|
298
298
|
*/
|
|
299
299
|
readonly extractRouteInheritanceTypes?: (tree: Parser.Tree, filePath: string) => SharedSpringType[];
|
|
300
|
+
/**
|
|
301
|
+
* Optional post-capture emission of synthetic structure members (nodes,
|
|
302
|
+
* symbols, ownership edges) that have no AST method node — e.g. Lombok
|
|
303
|
+
* accessors. Called once per file after the capture loop, at the same
|
|
304
|
+
* post-capture site as {@link extractDecoratorRoutes}.
|
|
305
|
+
*
|
|
306
|
+
* `classOwnersByNodeId` maps in-memory tree-sitter node ids of type
|
|
307
|
+
* declarations materialized in THIS file's capture loop to their graph
|
|
308
|
+
* node ids. Keys are never persisted; they exist only for the duration
|
|
309
|
+
* of the worker pass.
|
|
310
|
+
*
|
|
311
|
+
* Default: undefined (no synthetic structure members).
|
|
312
|
+
*/
|
|
313
|
+
readonly synthesizeStructureMembers?: (tree: Parser.Tree, filePath: string, classOwnersByNodeId: ReadonlyMap<number, string>) => {
|
|
314
|
+
nodes: ReadonlyArray<{
|
|
315
|
+
id: string;
|
|
316
|
+
label: string;
|
|
317
|
+
properties: Record<string, unknown>;
|
|
318
|
+
}>;
|
|
319
|
+
symbols: ReadonlyArray<{
|
|
320
|
+
filePath: string;
|
|
321
|
+
name: string;
|
|
322
|
+
nodeId: string;
|
|
323
|
+
type: string;
|
|
324
|
+
ownerId?: string;
|
|
325
|
+
parameterCount?: number;
|
|
326
|
+
requiredParameterCount?: number;
|
|
327
|
+
parameterTypes?: string[];
|
|
328
|
+
returnType?: string;
|
|
329
|
+
visibility?: string;
|
|
330
|
+
isStatic?: boolean;
|
|
331
|
+
isAbstract?: boolean;
|
|
332
|
+
isFinal?: boolean;
|
|
333
|
+
}>;
|
|
334
|
+
relationships: ReadonlyArray<{
|
|
335
|
+
id: string;
|
|
336
|
+
sourceId: string;
|
|
337
|
+
targetId: string;
|
|
338
|
+
type: string;
|
|
339
|
+
confidence: number;
|
|
340
|
+
reason: string;
|
|
341
|
+
}>;
|
|
342
|
+
};
|
|
300
343
|
/**
|
|
301
344
|
* Harvest this file's module-level string constants (#2391 core, #2980 Java
|
|
302
345
|
* parity) into the language-agnostic {@link ModuleConstants} shape, so the
|
|
@@ -18,6 +18,7 @@ import { captureJavaSpringAopFacts } from './spring-aop.js';
|
|
|
18
18
|
import { captureJavaSpringConditionalFacts, } from './spring-conditionals.js';
|
|
19
19
|
import { captureJavaSpringNonHttpHandlerFacts, } from './spring-non-http-handlers.js';
|
|
20
20
|
import { synthesizeJavaRecordComponentAccessorCaptures } from './record-components.js';
|
|
21
|
+
import { synthesizeLombokAccessorCaptures } from './lombok-synthesizer.js';
|
|
21
22
|
/** Declaration anchors that carry function-like arity metadata. */
|
|
22
23
|
const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'];
|
|
23
24
|
/** tree-sitter-java node types that the method extractor accepts. */
|
|
@@ -305,6 +306,7 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
|
|
|
305
306
|
...synthesizeJavaExplicitConstructorReferences(tree.rootNode),
|
|
306
307
|
...synthesizeJavaAnonymousClassDeclarations(tree.rootNode),
|
|
307
308
|
...synthesizeJavaRecordComponentAccessorCaptures(tree.rootNode),
|
|
309
|
+
...synthesizeLombokAccessorCaptures(tree.rootNode),
|
|
308
310
|
...synthesizeCallableFlowCaptures(tree.rootNode, JAVA_CALLABLE_CAPTURE_OPTIONS),
|
|
309
311
|
];
|
|
310
312
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lombok accessor synthesizer for Java.
|
|
3
|
+
*
|
|
4
|
+
* Lombok generates getters/setters at compile time. They are absent from the
|
|
5
|
+
* AST, so calls like `obj.getOrderId()` on a `@Data` class would otherwise
|
|
6
|
+
* leave unresolved CALLS edges. This module walks the tree-sitter Java AST
|
|
7
|
+
* and synthesizes Method graph members for the accessors Lombok would emit
|
|
8
|
+
* under the supported subset.
|
|
9
|
+
*
|
|
10
|
+
* ## Supported subset (v1)
|
|
11
|
+
* - Proven `lombok.Data` / `lombok.Getter` / `lombok.Setter` (FQN or import).
|
|
12
|
+
* - Class- or field-level enable; `AccessLevel.NONE` disables.
|
|
13
|
+
* - Default JavaBeans naming; primitive `boolean isX` → `isX` / `setX`.
|
|
14
|
+
* - Access levels PUBLIC/PROTECTED/PRIVATE/PACKAGE.
|
|
15
|
+
* - `@Accessors(chain=true)` modeled as setter return = declaring type.
|
|
16
|
+
* - `@Accessors(fluent=true)` / `prefix=…`: omit affected accessors (names
|
|
17
|
+
* cannot be proven without full Lombok config).
|
|
18
|
+
* - External `lombok.config`: unsupported (may change semantics invisibly).
|
|
19
|
+
*
|
|
20
|
+
* ## Identity
|
|
21
|
+
* Owner lookup uses in-memory AST node ids only. Method ids are derived from
|
|
22
|
+
* the stable declaring-owner graph key (the Class node id's name segment),
|
|
23
|
+
* never from persisted tree-sitter node ids.
|
|
24
|
+
*/
|
|
25
|
+
import type Parser from 'tree-sitter';
|
|
26
|
+
import type { CaptureMatch } from '../../../../_shared/index.js';
|
|
27
|
+
import { type PlannedJvmAccessor, type SyntheticAccessorResult, type SyntheticVisibility } from '../jvm/accessor-synthesis.js';
|
|
28
|
+
export type LombokVisibility = SyntheticVisibility;
|
|
29
|
+
export type SyntheticSymbol = SyntheticAccessorResult['symbols'][number];
|
|
30
|
+
export type SyntheticNode = SyntheticAccessorResult['nodes'][number];
|
|
31
|
+
export type SyntheticRelationship = SyntheticAccessorResult['relationships'][number];
|
|
32
|
+
export type LombokSynthesisResult = SyntheticAccessorResult;
|
|
33
|
+
export type PlannedLombokAccessor = PlannedJvmAccessor;
|
|
34
|
+
export interface AccessorConfig {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
visibility: LombokVisibility;
|
|
37
|
+
}
|
|
38
|
+
export declare function getterName(fieldName: string, fieldType: string): string;
|
|
39
|
+
export declare function setterName(fieldName: string, fieldType: string): string;
|
|
40
|
+
export declare function synthesizeLombokAccessors(tree: Parser.Tree, filePath: string, classOwnersById: ReadonlyMap<number, string>): LombokSynthesisResult;
|
|
41
|
+
/** Scope captures for Lombok accessors (dual-path parity with record components). */
|
|
42
|
+
export declare function synthesizeLombokAccessorCaptures(rootNode: Parser.SyntaxNode): CaptureMatch[];
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lombok accessor synthesizer for Java.
|
|
3
|
+
*
|
|
4
|
+
* Lombok generates getters/setters at compile time. They are absent from the
|
|
5
|
+
* AST, so calls like `obj.getOrderId()` on a `@Data` class would otherwise
|
|
6
|
+
* leave unresolved CALLS edges. This module walks the tree-sitter Java AST
|
|
7
|
+
* and synthesizes Method graph members for the accessors Lombok would emit
|
|
8
|
+
* under the supported subset.
|
|
9
|
+
*
|
|
10
|
+
* ## Supported subset (v1)
|
|
11
|
+
* - Proven `lombok.Data` / `lombok.Getter` / `lombok.Setter` (FQN or import).
|
|
12
|
+
* - Class- or field-level enable; `AccessLevel.NONE` disables.
|
|
13
|
+
* - Default JavaBeans naming; primitive `boolean isX` → `isX` / `setX`.
|
|
14
|
+
* - Access levels PUBLIC/PROTECTED/PRIVATE/PACKAGE.
|
|
15
|
+
* - `@Accessors(chain=true)` modeled as setter return = declaring type.
|
|
16
|
+
* - `@Accessors(fluent=true)` / `prefix=…`: omit affected accessors (names
|
|
17
|
+
* cannot be proven without full Lombok config).
|
|
18
|
+
* - External `lombok.config`: unsupported (may change semantics invisibly).
|
|
19
|
+
*
|
|
20
|
+
* ## Identity
|
|
21
|
+
* Owner lookup uses in-memory AST node ids only. Method ids are derived from
|
|
22
|
+
* the stable declaring-owner graph key (the Class node id's name segment),
|
|
23
|
+
* never from persisted tree-sitter node ids.
|
|
24
|
+
*/
|
|
25
|
+
import { jvmGetterName, jvmSetterName } from '../jvm/beanspec.js';
|
|
26
|
+
import { createExistingMethodIndex, createJvmAccessorSynthesis, hasExistingMethod, rememberExistingMethodRange, } from '../jvm/accessor-synthesis.js';
|
|
27
|
+
const JAVA_TYPE_DECLS = new Set([
|
|
28
|
+
'class_declaration',
|
|
29
|
+
'enum_declaration',
|
|
30
|
+
'interface_declaration',
|
|
31
|
+
'record_declaration',
|
|
32
|
+
]);
|
|
33
|
+
const LOMBOK_ANNOTATION_PACKAGE = new Map([
|
|
34
|
+
['Data', 'lombok'],
|
|
35
|
+
['Getter', 'lombok'],
|
|
36
|
+
['Setter', 'lombok'],
|
|
37
|
+
['Accessors', 'lombok.experimental'],
|
|
38
|
+
['Tolerate', 'lombok.experimental'],
|
|
39
|
+
]);
|
|
40
|
+
export function getterName(fieldName, fieldType) {
|
|
41
|
+
return jvmGetterName(fieldName, fieldType === 'boolean');
|
|
42
|
+
}
|
|
43
|
+
export function setterName(fieldName, fieldType) {
|
|
44
|
+
return jvmSetterName(fieldName, fieldType === 'boolean');
|
|
45
|
+
}
|
|
46
|
+
// ── Provenance / imports ──────────────────────────────────────────────────
|
|
47
|
+
function annotationSimpleName(nameText) {
|
|
48
|
+
return nameText.split('.').pop() ?? nameText;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compilation-unit imports only — Java `import` is never nested in a type body.
|
|
52
|
+
*/
|
|
53
|
+
function collectLombokImports(root) {
|
|
54
|
+
const bySimple = new Map();
|
|
55
|
+
const starPackages = new Set();
|
|
56
|
+
const shadowedSimpleNames = new Set();
|
|
57
|
+
for (const child of root.children) {
|
|
58
|
+
if (!JAVA_TYPE_DECLS.has(child.type) && child.type !== 'annotation_type_declaration')
|
|
59
|
+
continue;
|
|
60
|
+
const name = child.childForFieldName('name')?.text;
|
|
61
|
+
if (name)
|
|
62
|
+
shadowedSimpleNames.add(name);
|
|
63
|
+
}
|
|
64
|
+
for (const child of root.children) {
|
|
65
|
+
if (child.type !== 'import_declaration')
|
|
66
|
+
continue;
|
|
67
|
+
if (/^import\s+static\b/.test(child.text))
|
|
68
|
+
continue;
|
|
69
|
+
const text = child.text
|
|
70
|
+
.replace(/^import\s+/, '')
|
|
71
|
+
.replace(/;\s*$/, '')
|
|
72
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
73
|
+
.replace(/\s+/g, '')
|
|
74
|
+
.trim();
|
|
75
|
+
if (text === 'lombok.*') {
|
|
76
|
+
starPackages.add('lombok');
|
|
77
|
+
}
|
|
78
|
+
else if (text === 'lombok.experimental.*') {
|
|
79
|
+
starPackages.add('lombok.experimental');
|
|
80
|
+
}
|
|
81
|
+
else if (!text.endsWith('.*')) {
|
|
82
|
+
bySimple.set(annotationSimpleName(text), text);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { bySimple, starPackages, shadowedSimpleNames };
|
|
86
|
+
}
|
|
87
|
+
function isProvenLombokAnnotation(nameText, imports) {
|
|
88
|
+
const simple = annotationSimpleName(nameText);
|
|
89
|
+
const packageName = LOMBOK_ANNOTATION_PACKAGE.get(simple);
|
|
90
|
+
if (packageName === undefined)
|
|
91
|
+
return false;
|
|
92
|
+
if (nameText.includes('.'))
|
|
93
|
+
return nameText === `${packageName}.${simple}`;
|
|
94
|
+
const imported = imports.bySimple.get(simple);
|
|
95
|
+
if (imported !== undefined)
|
|
96
|
+
return imported === `${packageName}.${simple}`;
|
|
97
|
+
if (imports.shadowedSimpleNames.has(simple))
|
|
98
|
+
return false;
|
|
99
|
+
return imports.starPackages.has(packageName);
|
|
100
|
+
}
|
|
101
|
+
// ── AccessLevel / Accessors structural parse ──────────────────────────────
|
|
102
|
+
function parseAccessLevelToken(text) {
|
|
103
|
+
const simple = annotationSimpleName(text.trim());
|
|
104
|
+
switch (simple) {
|
|
105
|
+
case 'PUBLIC':
|
|
106
|
+
return 'public';
|
|
107
|
+
case 'PROTECTED':
|
|
108
|
+
return 'protected';
|
|
109
|
+
case 'PRIVATE':
|
|
110
|
+
return 'private';
|
|
111
|
+
case 'PACKAGE':
|
|
112
|
+
case 'MODULE': // treated as package-private for graph metadata
|
|
113
|
+
return 'package';
|
|
114
|
+
case 'NONE':
|
|
115
|
+
return 'none';
|
|
116
|
+
default:
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function findAccessLevelInAnnotation(ann) {
|
|
121
|
+
// Positional: @Getter(AccessLevel.PROTECTED) or @Getter(lombok.AccessLevel.NONE)
|
|
122
|
+
// Named: @Getter(value = AccessLevel.PRIVATE)
|
|
123
|
+
const stack = [...ann.children];
|
|
124
|
+
while (stack.length > 0) {
|
|
125
|
+
const n = stack.pop();
|
|
126
|
+
if (!n)
|
|
127
|
+
break;
|
|
128
|
+
if (n.type === 'field_access' || n.type === 'identifier') {
|
|
129
|
+
const level = parseAccessLevelToken(n.text);
|
|
130
|
+
if (level !== null)
|
|
131
|
+
return level;
|
|
132
|
+
}
|
|
133
|
+
for (const c of n.children)
|
|
134
|
+
stack.push(c);
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function defaultAccessors() {
|
|
139
|
+
return { fluent: false, hasPrefix: false, chain: false };
|
|
140
|
+
}
|
|
141
|
+
function parseAccessorsAnnotation(ann) {
|
|
142
|
+
const opts = defaultAccessors();
|
|
143
|
+
const stack = [...ann.children];
|
|
144
|
+
while (stack.length > 0) {
|
|
145
|
+
const n = stack.pop();
|
|
146
|
+
if (!n)
|
|
147
|
+
break;
|
|
148
|
+
if (n.type === 'element_value_pair') {
|
|
149
|
+
const key = n.childForFieldName('key')?.text ?? n.children.find((c) => c.type === 'identifier')?.text;
|
|
150
|
+
const valueNode = n.childForFieldName('value') ??
|
|
151
|
+
n.children.find((c) => c.type === 'true' || c.type === 'false' || c.type === 'element_value_array_initializer');
|
|
152
|
+
if (key === 'fluent' && (valueNode?.type === 'true' || valueNode?.type === 'false')) {
|
|
153
|
+
opts.fluent = valueNode.type === 'true';
|
|
154
|
+
}
|
|
155
|
+
if (key === 'chain' && (valueNode?.type === 'true' || valueNode?.type === 'false')) {
|
|
156
|
+
opts.chain = valueNode.type === 'true';
|
|
157
|
+
}
|
|
158
|
+
if (key === 'prefix')
|
|
159
|
+
opts.hasPrefix = true;
|
|
160
|
+
}
|
|
161
|
+
for (const c of n.children)
|
|
162
|
+
stack.push(c);
|
|
163
|
+
}
|
|
164
|
+
const text = ann.text;
|
|
165
|
+
if (/\bprefix\s*=/.test(text))
|
|
166
|
+
opts.hasPrefix = true;
|
|
167
|
+
if (/\bfluent\s*=\s*true\b/.test(text))
|
|
168
|
+
opts.fluent = true;
|
|
169
|
+
if (/\bfluent\s*=\s*false\b/.test(text))
|
|
170
|
+
opts.fluent = false;
|
|
171
|
+
if (/\bchain\s*=\s*true\b/.test(text))
|
|
172
|
+
opts.chain = true;
|
|
173
|
+
if (/\bchain\s*=\s*false\b/.test(text))
|
|
174
|
+
opts.chain = false;
|
|
175
|
+
return opts;
|
|
176
|
+
}
|
|
177
|
+
function parseModifierAnnotations(modifiersNode, imports) {
|
|
178
|
+
const result = {
|
|
179
|
+
getter: null,
|
|
180
|
+
setter: null,
|
|
181
|
+
accessors: defaultAccessors(),
|
|
182
|
+
accessorsPresent: false,
|
|
183
|
+
tolerate: false,
|
|
184
|
+
};
|
|
185
|
+
if (!modifiersNode)
|
|
186
|
+
return result;
|
|
187
|
+
for (const child of modifiersNode.children) {
|
|
188
|
+
if (child.type !== 'marker_annotation' && child.type !== 'annotation')
|
|
189
|
+
continue;
|
|
190
|
+
const nameNode = child.childForFieldName('name');
|
|
191
|
+
const nameText = nameNode?.text ?? '';
|
|
192
|
+
if (!isProvenLombokAnnotation(nameText, imports))
|
|
193
|
+
continue;
|
|
194
|
+
const simple = annotationSimpleName(nameText);
|
|
195
|
+
if (simple === 'Tolerate') {
|
|
196
|
+
result.tolerate = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (simple === 'Accessors') {
|
|
200
|
+
result.accessors = parseAccessorsAnnotation(child);
|
|
201
|
+
result.accessorsPresent = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (simple === 'Data') {
|
|
205
|
+
result.getter ??= { enabled: true, visibility: 'public' };
|
|
206
|
+
result.setter ??= { enabled: true, visibility: 'public' };
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (simple === 'Getter' || simple === 'Setter') {
|
|
210
|
+
const level = child.type === 'annotation' ? findAccessLevelInAnnotation(child) : null;
|
|
211
|
+
const cfg = level === 'none'
|
|
212
|
+
? { enabled: false, visibility: 'public' }
|
|
213
|
+
: { enabled: true, visibility: level ?? 'public' };
|
|
214
|
+
if (simple === 'Getter')
|
|
215
|
+
result.getter = cfg;
|
|
216
|
+
else
|
|
217
|
+
result.setter = cfg;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
function mergeAccessors(classOpts, fieldOpts, fieldAccessorsPresent) {
|
|
223
|
+
return fieldAccessorsPresent ? fieldOpts : classOpts;
|
|
224
|
+
}
|
|
225
|
+
function effectiveAccessor(classCfg, fieldCfg) {
|
|
226
|
+
if (fieldCfg !== null)
|
|
227
|
+
return fieldCfg;
|
|
228
|
+
return classCfg;
|
|
229
|
+
}
|
|
230
|
+
// ── Field / method collection ─────────────────────────────────────────────
|
|
231
|
+
function parseFieldDeclaration(fieldNode, imports) {
|
|
232
|
+
const typeNode = fieldNode.childForFieldName('type');
|
|
233
|
+
const fieldType = typeNode?.text ?? 'Object';
|
|
234
|
+
const modifiers = fieldNode.children.find((c) => c.type === 'modifiers') ?? null;
|
|
235
|
+
let isStatic = false;
|
|
236
|
+
let isFinal = false;
|
|
237
|
+
if (modifiers) {
|
|
238
|
+
for (const mod of modifiers.children) {
|
|
239
|
+
if (mod.text === 'static')
|
|
240
|
+
isStatic = true;
|
|
241
|
+
else if (mod.text === 'final')
|
|
242
|
+
isFinal = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const fieldAnn = parseModifierAnnotations(modifiers, imports);
|
|
246
|
+
const declarators = [];
|
|
247
|
+
const declaratorField = fieldNode.childForFieldName('declarator');
|
|
248
|
+
if (declaratorField)
|
|
249
|
+
declarators.push(declaratorField);
|
|
250
|
+
for (const child of fieldNode.children) {
|
|
251
|
+
if (child.type === 'variable_declarator' && child !== declaratorField) {
|
|
252
|
+
declarators.push(child);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const startLine = fieldNode.startPosition.row + 1;
|
|
256
|
+
const endLine = fieldNode.endPosition.row + 1;
|
|
257
|
+
const out = [];
|
|
258
|
+
for (const declaratorNode of declarators) {
|
|
259
|
+
const nameNode = declaratorNode.childForFieldName('name');
|
|
260
|
+
if (!nameNode)
|
|
261
|
+
continue;
|
|
262
|
+
out.push({
|
|
263
|
+
name: nameNode.text,
|
|
264
|
+
type: fieldType,
|
|
265
|
+
isStatic,
|
|
266
|
+
isFinal,
|
|
267
|
+
startLine,
|
|
268
|
+
endLine,
|
|
269
|
+
declaratorNode,
|
|
270
|
+
fieldGetter: fieldAnn.getter,
|
|
271
|
+
fieldSetter: fieldAnn.setter,
|
|
272
|
+
accessors: fieldAnn.accessors,
|
|
273
|
+
accessorsPresent: fieldAnn.accessorsPresent,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
function methodArityRange(methodNode) {
|
|
279
|
+
const params = methodNode.childForFieldName('parameters');
|
|
280
|
+
if (!params)
|
|
281
|
+
return { min: 0, max: 0 };
|
|
282
|
+
let count = 0;
|
|
283
|
+
for (const child of params.namedChildren) {
|
|
284
|
+
if (child.type === 'spread_parameter')
|
|
285
|
+
return { min: count, max: Number.POSITIVE_INFINITY };
|
|
286
|
+
if (child.type === 'formal_parameter')
|
|
287
|
+
count += 1;
|
|
288
|
+
}
|
|
289
|
+
return { min: count, max: count };
|
|
290
|
+
}
|
|
291
|
+
function collectExistingMethods(classBody, imports) {
|
|
292
|
+
const index = createExistingMethodIndex('case-folded');
|
|
293
|
+
if (!classBody)
|
|
294
|
+
return index;
|
|
295
|
+
const scan = (container) => {
|
|
296
|
+
for (const child of container.children) {
|
|
297
|
+
if (child.type === 'enum_body_declarations') {
|
|
298
|
+
scan(child);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (child.type !== 'method_declaration')
|
|
302
|
+
continue;
|
|
303
|
+
const mods = child.children.find((c) => c.type === 'modifiers') ?? null;
|
|
304
|
+
const ann = parseModifierAnnotations(mods, imports);
|
|
305
|
+
if (ann.tolerate)
|
|
306
|
+
continue;
|
|
307
|
+
const nameNode = child.childForFieldName('name');
|
|
308
|
+
if (!nameNode)
|
|
309
|
+
continue;
|
|
310
|
+
const arity = methodArityRange(child);
|
|
311
|
+
rememberExistingMethodRange(index, nameNode.text, arity.min, arity.max);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
scan(classBody);
|
|
315
|
+
return index;
|
|
316
|
+
}
|
|
317
|
+
const TYPE_BODIES = new Set(['class_body', 'enum_body']);
|
|
318
|
+
function findTypeBody(node) {
|
|
319
|
+
return node.children.find((c) => TYPE_BODIES.has(c.type)) ?? null;
|
|
320
|
+
}
|
|
321
|
+
function findLombokClasses(root, imports) {
|
|
322
|
+
const classes = [];
|
|
323
|
+
function walk(node) {
|
|
324
|
+
if (node.type === 'class_declaration' || node.type === 'enum_declaration') {
|
|
325
|
+
const modifiers = node.children.find((c) => c.type === 'modifiers') ?? null;
|
|
326
|
+
const classAnn = parseModifierAnnotations(modifiers, imports);
|
|
327
|
+
const nameNode = node.childForFieldName('name');
|
|
328
|
+
const className = nameNode?.text ?? '';
|
|
329
|
+
if (className) {
|
|
330
|
+
const body = findTypeBody(node);
|
|
331
|
+
const fields = [];
|
|
332
|
+
if (body) {
|
|
333
|
+
const collectFields = (container) => {
|
|
334
|
+
for (const child of container.children) {
|
|
335
|
+
if (child.type === 'field_declaration') {
|
|
336
|
+
for (const f of parseFieldDeclaration(child, imports)) {
|
|
337
|
+
if (f.isStatic)
|
|
338
|
+
continue;
|
|
339
|
+
fields.push(f);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else if (child.type === 'enum_body_declarations') {
|
|
343
|
+
collectFields(child);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
collectFields(body);
|
|
348
|
+
}
|
|
349
|
+
const anyFieldEnable = fields.some((f) => f.fieldGetter?.enabled === true || f.fieldSetter?.enabled === true);
|
|
350
|
+
const classEnable = classAnn.getter?.enabled === true || classAnn.setter?.enabled === true;
|
|
351
|
+
// Class-level NONE alone is not enable — getter/setter configs may be disabled
|
|
352
|
+
if (classEnable || anyFieldEnable) {
|
|
353
|
+
classes.push({
|
|
354
|
+
node,
|
|
355
|
+
name: className,
|
|
356
|
+
classGetter: classAnn.getter,
|
|
357
|
+
classSetter: classAnn.setter,
|
|
358
|
+
classAccessors: classAnn.accessors,
|
|
359
|
+
fields,
|
|
360
|
+
existingMethods: collectExistingMethods(body, imports),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (const child of node.children)
|
|
366
|
+
walk(child);
|
|
367
|
+
}
|
|
368
|
+
walk(root);
|
|
369
|
+
return classes;
|
|
370
|
+
}
|
|
371
|
+
function planAccessors(cls) {
|
|
372
|
+
const planned = [];
|
|
373
|
+
for (const field of cls.fields) {
|
|
374
|
+
const accessors = mergeAccessors(cls.classAccessors, field.accessors, field.accessorsPresent);
|
|
375
|
+
// fluent/prefix change names — omit rather than invent wrong names
|
|
376
|
+
if (accessors.fluent || accessors.hasPrefix)
|
|
377
|
+
continue;
|
|
378
|
+
const getterCfg = effectiveAccessor(cls.classGetter, field.fieldGetter);
|
|
379
|
+
const setterCfg = effectiveAccessor(cls.classSetter, field.fieldSetter);
|
|
380
|
+
if (getterCfg?.enabled) {
|
|
381
|
+
const gName = getterName(field.name, field.type);
|
|
382
|
+
if (!hasExistingMethod(cls.existingMethods, gName, 0)) {
|
|
383
|
+
planned.push({
|
|
384
|
+
kind: 'getter',
|
|
385
|
+
name: gName,
|
|
386
|
+
returnType: field.type,
|
|
387
|
+
parameterTypes: [],
|
|
388
|
+
visibility: getterCfg.visibility,
|
|
389
|
+
isStatic: false,
|
|
390
|
+
isAbstract: false,
|
|
391
|
+
startLine: field.startLine,
|
|
392
|
+
endLine: field.endLine,
|
|
393
|
+
declaratorNode: field.declaratorNode,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (setterCfg?.enabled && !field.isFinal) {
|
|
398
|
+
const sName = setterName(field.name, field.type);
|
|
399
|
+
if (!hasExistingMethod(cls.existingMethods, sName, 1)) {
|
|
400
|
+
// chain=true → setter returns declaring type; never emit void in that case
|
|
401
|
+
const returnType = accessors.chain ? cls.name : 'void';
|
|
402
|
+
planned.push({
|
|
403
|
+
kind: 'setter',
|
|
404
|
+
name: sName,
|
|
405
|
+
returnType,
|
|
406
|
+
parameterTypes: [field.type],
|
|
407
|
+
visibility: setterCfg.visibility,
|
|
408
|
+
isStatic: false,
|
|
409
|
+
isAbstract: false,
|
|
410
|
+
startLine: field.startLine,
|
|
411
|
+
endLine: field.endLine,
|
|
412
|
+
declaratorNode: field.declaratorNode,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return planned;
|
|
418
|
+
}
|
|
419
|
+
function planLombokAccessorOwners(root) {
|
|
420
|
+
const imports = collectLombokImports(root);
|
|
421
|
+
return findLombokClasses(root, imports).map((cls) => ({
|
|
422
|
+
node: cls.node,
|
|
423
|
+
name: cls.name,
|
|
424
|
+
accessors: planAccessors(cls),
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
const lombokAccessorSynthesis = createJvmAccessorSynthesis({
|
|
428
|
+
language: 'java',
|
|
429
|
+
synthetic: 'lombok',
|
|
430
|
+
planOwners: planLombokAccessorOwners,
|
|
431
|
+
});
|
|
432
|
+
// ── Main API ──────────────────────────────────────────────────────────────
|
|
433
|
+
export function synthesizeLombokAccessors(tree, filePath, classOwnersById) {
|
|
434
|
+
return lombokAccessorSynthesis.synthesize(tree, filePath, classOwnersById);
|
|
435
|
+
}
|
|
436
|
+
/** Scope captures for Lombok accessors (dual-path parity with record components). */
|
|
437
|
+
export function synthesizeLombokAccessorCaptures(rootNode) {
|
|
438
|
+
return lombokAccessorSynthesis.captures(rootNode);
|
|
439
|
+
}
|
|
@@ -28,6 +28,7 @@ import { createJavaCfgVisitor } from '../cfg/visitors/java.js';
|
|
|
28
28
|
import { assertCloneable } from '../workers/clone-safety.js';
|
|
29
29
|
import { collectJavaCaptureSideChannel } from './java/capture-side-channel.js';
|
|
30
30
|
import { javaRecordMethodExtractor, shouldSkipJavaRecordComponentDefinition, } from './java/record-components.js';
|
|
31
|
+
import { synthesizeLombokAccessors } from './java/lombok-synthesizer.js';
|
|
31
32
|
import { emitJavaScopeCaptures, interpretJavaImport, interpretJavaTypeBinding, javaBindingScopeFor, javaImportOwningScope, javaMergeBindings, javaReceiverBinding, javaArityCompatibility, resolveJavaImportTarget, } from './java/index.js';
|
|
32
33
|
/**
|
|
33
34
|
* Java names the platform owns, matched against a BARE IDENTIFIER — a dropped
|
|
@@ -187,6 +188,7 @@ export const javaProvider = defineLanguage({
|
|
|
187
188
|
// ── Route extraction ──
|
|
188
189
|
extractDecoratorRoutes: extractSpringRoutes,
|
|
189
190
|
extractRouteInheritanceTypes: extractSpringTypes,
|
|
191
|
+
synthesizeStructureMembers: synthesizeLombokAccessors,
|
|
190
192
|
// ── #2980: constant harvest + qualified-ref fold for non-literal mapping
|
|
191
193
|
// paths (`@PostMapping(ApiPaths.SAVE_V1)`) — kept behind provider hooks so
|
|
192
194
|
// the shared ingestion layers stay language-agnostic. The heuristic is
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared planning orchestration and emission for synthetic JVM accessors.
|
|
3
|
+
*
|
|
4
|
+
* Language adapters discover accessor plans. This module owns method-collision
|
|
5
|
+
* policy, graph emission, and scope captures without naming any language.
|
|
6
|
+
*/
|
|
7
|
+
import type Parser from 'tree-sitter';
|
|
8
|
+
import type { CaptureMatch } from '../../../../_shared/index.js';
|
|
9
|
+
export type SyntheticVisibility = 'public' | 'protected' | 'private' | 'package';
|
|
10
|
+
export type MethodNameMatching = 'exact' | 'case-folded';
|
|
11
|
+
export interface ExistingMethodIndex {
|
|
12
|
+
readonly matching: MethodNameMatching;
|
|
13
|
+
readonly aritiesByName: Map<string, Set<number>>;
|
|
14
|
+
readonly arityRangesByName: Map<string, Array<{
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
}>>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createExistingMethodIndex(matching: MethodNameMatching): ExistingMethodIndex;
|
|
20
|
+
export declare function rememberExistingMethod(index: ExistingMethodIndex, name: string, arity: number): void;
|
|
21
|
+
export declare function rememberExistingMethodRange(index: ExistingMethodIndex, name: string, min: number, max: number): void;
|
|
22
|
+
export declare function hasExistingMethod(index: ExistingMethodIndex, name: string, arity: number): boolean;
|
|
23
|
+
export interface SyntheticAccessorSymbol {
|
|
24
|
+
filePath: string;
|
|
25
|
+
name: string;
|
|
26
|
+
nodeId: string;
|
|
27
|
+
type: 'Method';
|
|
28
|
+
ownerId: string;
|
|
29
|
+
qualifiedName: string;
|
|
30
|
+
parameterCount: number;
|
|
31
|
+
requiredParameterCount: number;
|
|
32
|
+
parameterTypes: string[];
|
|
33
|
+
returnType: string;
|
|
34
|
+
visibility: SyntheticVisibility;
|
|
35
|
+
isStatic: boolean;
|
|
36
|
+
isAbstract: boolean;
|
|
37
|
+
isFinal: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface SyntheticAccessorNode {
|
|
40
|
+
id: string;
|
|
41
|
+
label: 'Method';
|
|
42
|
+
properties: {
|
|
43
|
+
name: string;
|
|
44
|
+
filePath: string;
|
|
45
|
+
startLine: number;
|
|
46
|
+
endLine: number;
|
|
47
|
+
language: string;
|
|
48
|
+
isExported: boolean;
|
|
49
|
+
synthetic: string;
|
|
50
|
+
visibility: SyntheticVisibility;
|
|
51
|
+
isStatic: boolean;
|
|
52
|
+
returnType: string;
|
|
53
|
+
parameterTypes: string[];
|
|
54
|
+
parameterCount: number;
|
|
55
|
+
qualifiedName: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export interface SyntheticAccessorRelationship {
|
|
59
|
+
id: string;
|
|
60
|
+
sourceId: string;
|
|
61
|
+
targetId: string;
|
|
62
|
+
type: 'HAS_METHOD';
|
|
63
|
+
confidence: number;
|
|
64
|
+
reason: string;
|
|
65
|
+
}
|
|
66
|
+
export interface SyntheticAccessorResult {
|
|
67
|
+
symbols: SyntheticAccessorSymbol[];
|
|
68
|
+
nodes: SyntheticAccessorNode[];
|
|
69
|
+
relationships: SyntheticAccessorRelationship[];
|
|
70
|
+
}
|
|
71
|
+
export interface PlannedJvmAccessor {
|
|
72
|
+
kind: 'getter' | 'setter';
|
|
73
|
+
name: string;
|
|
74
|
+
returnType: string;
|
|
75
|
+
parameterTypes: string[];
|
|
76
|
+
visibility: SyntheticVisibility;
|
|
77
|
+
isStatic: boolean;
|
|
78
|
+
isAbstract: boolean;
|
|
79
|
+
startLine: number;
|
|
80
|
+
endLine: number;
|
|
81
|
+
declaratorNode: Parser.SyntaxNode;
|
|
82
|
+
}
|
|
83
|
+
export interface PlannedJvmAccessorOwner {
|
|
84
|
+
node: Parser.SyntaxNode;
|
|
85
|
+
name: string;
|
|
86
|
+
accessors: readonly PlannedJvmAccessor[];
|
|
87
|
+
}
|
|
88
|
+
interface JvmAccessorSynthesisConfig {
|
|
89
|
+
language: string;
|
|
90
|
+
synthetic: string;
|
|
91
|
+
planOwners(rootNode: Parser.SyntaxNode): readonly PlannedJvmAccessorOwner[];
|
|
92
|
+
}
|
|
93
|
+
export interface JvmAccessorSynthesis {
|
|
94
|
+
synthesize(tree: Parser.Tree, filePath: string, classOwnersById: ReadonlyMap<number, string>): SyntheticAccessorResult;
|
|
95
|
+
captures(rootNode: Parser.SyntaxNode): CaptureMatch[];
|
|
96
|
+
}
|
|
97
|
+
export declare function createJvmAccessorSynthesis(config: JvmAccessorSynthesisConfig): JvmAccessorSynthesis;
|
|
98
|
+
export declare function jvmTypeSimpleName(node: Parser.SyntaxNode): string | undefined;
|
|
99
|
+
export {};
|