gitnexus 1.6.11-rc.2 → 1.6.11-rc.21
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/README.md +43 -1
- package/dist/_shared/impact-risk.d.ts +37 -0
- package/dist/_shared/impact-risk.d.ts.map +1 -0
- package/dist/_shared/impact-risk.js +92 -0
- package/dist/_shared/impact-risk.js.map +1 -0
- package/dist/_shared/index.d.ts +2 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +2 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/analyze-config.d.ts +2 -0
- package/dist/cli/analyze-config.js +16 -0
- package/dist/cli/analyze-options.d.ts +4 -0
- package/dist/cli/analyze.d.ts +17 -0
- package/dist/cli/analyze.js +43 -7
- package/dist/cli/eval-server.js +22 -5
- package/dist/cli/group.js +7 -1
- package/dist/cli/help-i18n.js +2 -0
- package/dist/cli/i18n/en.d.ts +12 -2
- package/dist/cli/i18n/en.js +12 -2
- package/dist/cli/i18n/resources.d.ts +22 -2
- package/dist/cli/i18n/zh-CN.d.ts +10 -0
- package/dist/cli/i18n/zh-CN.js +12 -2
- package/dist/cli/index.js +11 -4
- package/dist/cli/status.js +91 -6
- package/dist/cli/watch-queue.d.ts +41 -0
- package/dist/cli/watch-queue.js +184 -0
- package/dist/cli/watch.d.ts +20 -0
- package/dist/cli/watch.js +372 -0
- package/dist/cli/wiki.js +15 -2
- package/dist/config/ignore-service.d.ts +11 -0
- package/dist/config/ignore-service.js +41 -4
- package/dist/config/repo-control-file.d.ts +3 -0
- package/dist/config/repo-control-file.js +115 -0
- package/dist/core/group/config-parser.js +20 -2
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +33 -2
- package/dist/core/group/extractors/fs-utils.d.ts +2 -0
- package/dist/core/group/extractors/fs-utils.js +86 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
- package/dist/core/group/extractors/graphql-extractor.js +652 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +1 -1
- package/dist/core/group/matching.js +8 -1
- package/dist/core/group/service.js +5 -1
- package/dist/core/group/storage.js +1 -0
- package/dist/core/group/sync.d.ts +3 -1
- package/dist/core/group/sync.js +43 -5
- package/dist/core/group/types.d.ts +19 -5
- package/dist/core/incremental/derived-writeback.d.ts +36 -0
- package/dist/core/incremental/derived-writeback.js +68 -0
- package/dist/core/incremental/subgraph-extract.d.ts +6 -4
- package/dist/core/incremental/subgraph-extract.js +7 -5
- package/dist/core/index-content-drift.d.ts +54 -0
- package/dist/core/index-content-drift.js +127 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
- package/dist/core/ingestion/filesystem-walker.js +20 -3
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
- package/dist/core/ingestion/language-provider.d.ts +43 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/java/captures.js +14 -1
- 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/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -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/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
- 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/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/pipeline-phases/di.js +47 -14
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
- package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
- package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +44 -12
- package/dist/core/ingestion/scope-extractor.js +1 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
- package/dist/core/ingestion/utils/symbol-labels.js +2 -2
- package/dist/core/ingestion/workers/parse-worker.js +28 -2
- package/dist/core/lbug/lbug-adapter.d.ts +59 -0
- package/dist/core/lbug/lbug-adapter.js +154 -1
- package/dist/core/run-analyze.d.ts +17 -0
- package/dist/core/run-analyze.js +231 -23
- package/dist/core/search/fts-indexes.d.ts +19 -1
- package/dist/core/search/fts-indexes.js +28 -1
- package/dist/core/wiki/generator.js +8 -0
- package/dist/core/wiki/grok-client.d.ts +21 -0
- package/dist/core/wiki/grok-client.js +287 -0
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +5 -2
- package/dist/core/wiki/local-cli-client.d.ts +11 -0
- package/dist/core/wiki/local-cli-client.js +22 -9
- package/dist/mcp/local/local-backend.d.ts +24 -7
- package/dist/mcp/local/local-backend.js +190 -81
- package/dist/mcp/local/pdg-impact.d.ts +8 -4
- package/dist/mcp/local/pdg-impact.js +7 -2
- package/dist/mcp/repository-policy.d.ts +5 -1
- package/dist/mcp/repository-policy.js +48 -4
- package/dist/mcp/resources.js +2 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/tools.js +31 -19
- package/dist/server/api.js +23 -64
- package/dist/server/grep-params.d.ts +18 -0
- package/dist/server/grep-params.js +83 -0
- package/dist/server/grep-scan.d.ts +23 -0
- package/dist/server/grep-scan.js +107 -0
- package/dist/server/grep-worker.d.ts +1 -0
- package/dist/server/grep-worker.js +12 -0
- package/dist/server/mcp-http.d.ts +8 -0
- package/dist/server/mcp-http.js +16 -1
- package/dist/storage/file-hash.d.ts +5 -0
- package/dist/storage/file-hash.js +16 -6
- package/dist/storage/fs-atomic.d.ts +24 -0
- package/dist/storage/fs-atomic.js +86 -2
- package/dist/storage/git.d.ts +19 -8
- package/dist/storage/git.js +83 -24
- package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
- package/dist/storage/gitnexus-managed-paths.js +46 -0
- package/dist/storage/parse-cache.d.ts +22 -4
- package/dist/storage/parse-cache.js +106 -29
- package/dist/storage/parsedfile-store.d.ts +34 -60
- package/dist/storage/parsedfile-store.js +177 -171
- package/dist/storage/repo-manager.d.ts +11 -1
- package/dist/storage/repo-manager.js +23 -2
- package/dist/storage/repo-meta.d.ts +12 -0
- package/dist/storage/v8-sidecar.d.ts +48 -0
- package/dist/storage/v8-sidecar.js +347 -0
- package/dist/types/pipeline.d.ts +14 -0
- package/package.json +4 -1
- package/scripts/cross-platform-shard.ts +4 -2
- package/scripts/cross-platform-tests.ts +7 -1
- package/skills/gitnexus-cli.md +11 -3
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
- package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
- package/web/index.html +1 -1
|
@@ -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
|
+
}
|
|
@@ -19,6 +19,7 @@ import { attachJavaSpringConfigBindings } from './spring-config-bindings.js';
|
|
|
19
19
|
import { attachJavaSpringConditionalMetadata } from './spring-conditionals.js';
|
|
20
20
|
import { attachJavaSpringDiMetadata } from './spring-di.js';
|
|
21
21
|
import { attachJavaSpringNonHttpHandlerMetadata } from './spring-non-http-handlers.js';
|
|
22
|
+
import { attachJavaSpringDynamicLookup } from './spring-dynamic-lookup.js';
|
|
22
23
|
import { applyJavaCaptureSideChannel, clearJavaClassAnnotationFacts, } from './capture-side-channel.js';
|
|
23
24
|
import { clearJavaPackageFacts } from './package-facts.js';
|
|
24
25
|
const javaScopeResolver = {
|
|
@@ -65,6 +66,7 @@ const javaScopeResolver = {
|
|
|
65
66
|
attachJavaSpringDiMetadata(graph, parsedFiles, nodeLookup, indexes);
|
|
66
67
|
attachJavaSpringNonHttpHandlerMetadata(graph, parsedFiles, nodeLookup, indexes);
|
|
67
68
|
attachJavaSpringConfigBindings(graph, parsedFiles, nodeLookup, indexes, ctx);
|
|
69
|
+
attachJavaSpringDynamicLookup(graph, parsedFiles, nodeLookup, indexes);
|
|
68
70
|
},
|
|
69
71
|
};
|
|
70
72
|
export { javaScopeResolver };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SpringDynamicLookupFact } from '../../frameworks/spring/dynamic-lookups.js';
|
|
2
|
+
import { type SyntaxNode } from '../../utils/ast-helpers.js';
|
|
3
|
+
/** Capture real Java method invocations; comments and literals are never visited as calls. */
|
|
4
|
+
export declare function captureJavaSpringDynamicLookupFact(node: SyntaxNode, filePath: string): SpringDynamicLookupFact | null;
|
|
5
|
+
/** Standalone extractor for focused tests; production reuses scope-query call nodes. */
|
|
6
|
+
export declare function captureJavaSpringDynamicLookupFacts(rootNode: SyntaxNode, filePath: string): SpringDynamicLookupFact[];
|
|
7
|
+
/** Attach Java lookup facts for later resolution by the shared DI phase. */
|
|
8
|
+
export declare const attachJavaSpringDynamicLookup: (graph: import("../../../graph/types.js").KnowledgeGraph, parsedFiles: readonly import('../../../../_shared/index.js').ParsedFile[], nodeLookup: import("../../scope-resolution/graph-bridge/node-lookup.js").GraphNodeLookup, indexes: import("../../model/scope-resolution-indexes.js").ScopeResolutionIndexes) => void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { makeScopeId } from '../../../../_shared/index.js';
|
|
2
|
+
import { createSpringDynamicLookupMetadataAttacher, springDynamicLookupCardinality, } from '../../frameworks/spring/dynamic-lookups.js';
|
|
3
|
+
import { findAncestorBeforeBoundary, nodeToCapture, } from '../../utils/ast-helpers.js';
|
|
4
|
+
import { getJavaSpringDynamicLookupFacts } from './capture-side-channel.js';
|
|
5
|
+
const CALLABLE_NODE_TYPES = new Set([
|
|
6
|
+
'method_declaration',
|
|
7
|
+
'constructor_declaration',
|
|
8
|
+
'compact_constructor_declaration',
|
|
9
|
+
]);
|
|
10
|
+
const NO_CALLABLE_BOUNDARIES = new Set();
|
|
11
|
+
function classLiteralTypeName(argument) {
|
|
12
|
+
if (argument.type !== 'class_literal' || argument.namedChildCount !== 1)
|
|
13
|
+
return null;
|
|
14
|
+
return argument.namedChild(0)?.text.trim() ?? null;
|
|
15
|
+
}
|
|
16
|
+
/** Capture real Java method invocations; comments and literals are never visited as calls. */
|
|
17
|
+
export function captureJavaSpringDynamicLookupFact(node, filePath) {
|
|
18
|
+
if (node.type !== 'method_invocation')
|
|
19
|
+
return null;
|
|
20
|
+
const receiverName = node.childForFieldName('object')?.text.trim();
|
|
21
|
+
const methodName = node.childForFieldName('name')?.text.trim();
|
|
22
|
+
const argumentsNode = node.childForFieldName('arguments');
|
|
23
|
+
if (receiverName === undefined || methodName === undefined || argumentsNode === null)
|
|
24
|
+
return null;
|
|
25
|
+
if (springDynamicLookupCardinality(receiverName, methodName) === null)
|
|
26
|
+
return null;
|
|
27
|
+
const argumentsWithoutComments = argumentsNode.namedChildren.filter((child) => child.type !== 'line_comment' && child.type !== 'block_comment');
|
|
28
|
+
if (argumentsWithoutComments.length !== 1)
|
|
29
|
+
return null;
|
|
30
|
+
const argument = argumentsWithoutComments[0];
|
|
31
|
+
if (argument === undefined)
|
|
32
|
+
return null;
|
|
33
|
+
const targetTypeName = classLiteralTypeName(argument);
|
|
34
|
+
if (targetTypeName === null)
|
|
35
|
+
return null;
|
|
36
|
+
const owner = findAncestorBeforeBoundary(node, CALLABLE_NODE_TYPES, NO_CALLABLE_BOUNDARIES);
|
|
37
|
+
if (owner === null)
|
|
38
|
+
return null;
|
|
39
|
+
const ownerCapture = nodeToCapture('@spring-dynamic-lookup.owner', owner);
|
|
40
|
+
return {
|
|
41
|
+
ownerScopeId: makeScopeId({
|
|
42
|
+
filePath,
|
|
43
|
+
range: ownerCapture.range,
|
|
44
|
+
kind: 'Function',
|
|
45
|
+
}),
|
|
46
|
+
ownerRange: ownerCapture.range,
|
|
47
|
+
receiverName,
|
|
48
|
+
methodName,
|
|
49
|
+
targetTypeName,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Standalone extractor for focused tests; production reuses scope-query call nodes. */
|
|
53
|
+
export function captureJavaSpringDynamicLookupFacts(rootNode, filePath) {
|
|
54
|
+
return rootNode
|
|
55
|
+
.descendantsOfType('method_invocation')
|
|
56
|
+
.map((node) => captureJavaSpringDynamicLookupFact(node, filePath))
|
|
57
|
+
.filter((fact) => fact !== null);
|
|
58
|
+
}
|
|
59
|
+
/** Attach Java lookup facts for later resolution by the shared DI phase. */
|
|
60
|
+
export const attachJavaSpringDynamicLookup = createSpringDynamicLookupMetadataAttacher({
|
|
61
|
+
getFacts: getJavaSpringDynamicLookupFacts,
|
|
62
|
+
});
|
|
@@ -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 {};
|