gitnexus 1.4.6 → 1.4.7
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/graph/types.d.ts +2 -2
- package/dist/core/ingestion/call-processor.d.ts +7 -1
- package/dist/core/ingestion/call-processor.js +308 -104
- package/dist/core/ingestion/call-routing.d.ts +17 -2
- package/dist/core/ingestion/call-routing.js +21 -0
- package/dist/core/ingestion/parsing-processor.d.ts +2 -1
- package/dist/core/ingestion/parsing-processor.js +32 -6
- package/dist/core/ingestion/pipeline.js +5 -1
- package/dist/core/ingestion/symbol-table.d.ts +13 -3
- package/dist/core/ingestion/symbol-table.js +23 -4
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
- package/dist/core/ingestion/tree-sitter-queries.js +200 -0
- package/dist/core/ingestion/type-env.js +94 -38
- package/dist/core/ingestion/type-extractors/c-cpp.js +27 -2
- package/dist/core/ingestion/type-extractors/csharp.js +40 -0
- package/dist/core/ingestion/type-extractors/go.js +45 -0
- package/dist/core/ingestion/type-extractors/jvm.js +75 -3
- package/dist/core/ingestion/type-extractors/php.js +31 -4
- package/dist/core/ingestion/type-extractors/python.js +89 -17
- package/dist/core/ingestion/type-extractors/ruby.js +17 -2
- package/dist/core/ingestion/type-extractors/rust.js +37 -3
- package/dist/core/ingestion/type-extractors/shared.d.ts +12 -0
- package/dist/core/ingestion/type-extractors/shared.js +110 -3
- package/dist/core/ingestion/type-extractors/types.d.ts +17 -4
- package/dist/core/ingestion/type-extractors/typescript.js +30 -0
- package/dist/core/ingestion/utils.d.ts +25 -0
- package/dist/core/ingestion/utils.js +160 -1
- package/dist/core/ingestion/workers/parse-worker.d.ts +23 -7
- package/dist/core/ingestion/workers/parse-worker.js +68 -26
- package/dist/core/lbug/lbug-adapter.d.ts +2 -0
- package/dist/core/lbug/lbug-adapter.js +2 -0
- package/dist/core/lbug/schema.d.ts +1 -1
- package/dist/core/lbug/schema.js +1 -1
- package/dist/mcp/core/lbug-adapter.d.ts +22 -0
- package/dist/mcp/core/lbug-adapter.js +167 -23
- package/dist/mcp/local/local-backend.js +3 -3
- package/dist/mcp/resources.js +11 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +15 -5
- package/package.json +4 -4
|
@@ -254,7 +254,7 @@ export const CLASS_CONTAINER_TYPES = new Set([
|
|
|
254
254
|
'class_declaration', 'abstract_class_declaration',
|
|
255
255
|
'interface_declaration', 'struct_declaration', 'record_declaration',
|
|
256
256
|
'class_specifier', 'struct_specifier',
|
|
257
|
-
'impl_item', 'trait_item',
|
|
257
|
+
'impl_item', 'trait_item', 'struct_item', 'enum_item',
|
|
258
258
|
'class_definition',
|
|
259
259
|
'trait_declaration',
|
|
260
260
|
'protocol_declaration',
|
|
@@ -275,6 +275,8 @@ export const CONTAINER_TYPE_TO_LABEL = {
|
|
|
275
275
|
class_definition: 'Class',
|
|
276
276
|
impl_item: 'Impl',
|
|
277
277
|
trait_item: 'Trait',
|
|
278
|
+
struct_item: 'Struct',
|
|
279
|
+
enum_item: 'Enum',
|
|
278
280
|
trait_declaration: 'Trait',
|
|
279
281
|
record_declaration: 'Record',
|
|
280
282
|
protocol_declaration: 'Interface',
|
|
@@ -306,6 +308,21 @@ export const findEnclosingClassId = (node, filePath) => {
|
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
310
|
}
|
|
311
|
+
// Go: type_declaration wrapping a struct_type (type User struct { ... })
|
|
312
|
+
// field_declaration → field_declaration_list → struct_type → type_spec → type_declaration
|
|
313
|
+
if (current.type === 'type_declaration') {
|
|
314
|
+
const typeSpec = current.children?.find((c) => c.type === 'type_spec');
|
|
315
|
+
if (typeSpec) {
|
|
316
|
+
const typeBody = typeSpec.childForFieldName?.('type');
|
|
317
|
+
if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
|
|
318
|
+
const nameNode = typeSpec.childForFieldName?.('name');
|
|
319
|
+
if (nameNode) {
|
|
320
|
+
const label = typeBody.type === 'struct_type' ? 'Struct' : 'Interface';
|
|
321
|
+
return generateId(label, `${filePath}:${nameNode.text}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
309
326
|
if (CLASS_CONTAINER_TYPES.has(current.type)) {
|
|
310
327
|
// Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
|
|
311
328
|
if (current.type === 'impl_item') {
|
|
@@ -1129,3 +1146,145 @@ export function extractCallChain(receiverCallNode) {
|
|
|
1129
1146
|
}
|
|
1130
1147
|
return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined;
|
|
1131
1148
|
}
|
|
1149
|
+
/** Node types representing member/field access across languages. */
|
|
1150
|
+
const FIELD_ACCESS_NODE_TYPES = new Set([
|
|
1151
|
+
'member_expression', // TS/JS
|
|
1152
|
+
'member_access_expression', // C#
|
|
1153
|
+
'selector_expression', // Go
|
|
1154
|
+
'field_expression', // Rust/C++
|
|
1155
|
+
'field_access', // Java
|
|
1156
|
+
'attribute', // Python
|
|
1157
|
+
'navigation_expression', // Kotlin/Swift
|
|
1158
|
+
'member_binding_expression', // C# null-conditional (user?.Address)
|
|
1159
|
+
]);
|
|
1160
|
+
/**
|
|
1161
|
+
* Walk a receiver AST node that may interleave field accesses and method calls,
|
|
1162
|
+
* building a unified chain of steps up to MAX_CHAIN_DEPTH.
|
|
1163
|
+
*
|
|
1164
|
+
* For `svc.getUser().address.save()`, called with the receiver of `save`
|
|
1165
|
+
* (`svc.getUser().address`, a field access node):
|
|
1166
|
+
* returns { chain: [{ kind:'call', name:'getUser' }, { kind:'field', name:'address' }],
|
|
1167
|
+
* baseReceiverName: 'svc' }
|
|
1168
|
+
*
|
|
1169
|
+
* For `user.getAddress().city.getName()`, called with receiver of `getName`
|
|
1170
|
+
* (`user.getAddress().city`):
|
|
1171
|
+
* returns { chain: [{ kind:'call', name:'getAddress' }, { kind:'field', name:'city' }],
|
|
1172
|
+
* baseReceiverName: 'user' }
|
|
1173
|
+
*
|
|
1174
|
+
* Pure field chains and pure call chains are special cases (all steps same kind).
|
|
1175
|
+
*/
|
|
1176
|
+
export function extractMixedChain(receiverNode) {
|
|
1177
|
+
const chain = [];
|
|
1178
|
+
let current = receiverNode;
|
|
1179
|
+
while (chain.length < MAX_CHAIN_DEPTH) {
|
|
1180
|
+
if (CALL_EXPRESSION_TYPES.has(current.type)) {
|
|
1181
|
+
// ── Call expression: extract method name + inner receiver ────────────
|
|
1182
|
+
const funcNode = current.childForFieldName?.('function')
|
|
1183
|
+
?? current.childForFieldName?.('name')
|
|
1184
|
+
?? current.childForFieldName?.('method');
|
|
1185
|
+
let methodName;
|
|
1186
|
+
let innerReceiver = null;
|
|
1187
|
+
if (funcNode) {
|
|
1188
|
+
methodName = funcNode.lastNamedChild?.text ?? funcNode.text;
|
|
1189
|
+
}
|
|
1190
|
+
// Kotlin/Swift: call_expression → navigation_expression
|
|
1191
|
+
if (!funcNode && current.type === 'call_expression') {
|
|
1192
|
+
const callee = current.firstNamedChild;
|
|
1193
|
+
if (callee?.type === 'navigation_expression') {
|
|
1194
|
+
const suffix = callee.lastNamedChild;
|
|
1195
|
+
if (suffix?.type === 'navigation_suffix') {
|
|
1196
|
+
methodName = suffix.lastNamedChild?.text;
|
|
1197
|
+
for (let i = 0; i < callee.namedChildCount; i++) {
|
|
1198
|
+
const child = callee.namedChild(i);
|
|
1199
|
+
if (child && child.type !== 'navigation_suffix') {
|
|
1200
|
+
innerReceiver = child;
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (!methodName)
|
|
1208
|
+
break;
|
|
1209
|
+
chain.unshift({ kind: 'call', name: methodName });
|
|
1210
|
+
if (!innerReceiver && funcNode) {
|
|
1211
|
+
innerReceiver = funcNode.childForFieldName?.('object')
|
|
1212
|
+
?? funcNode.childForFieldName?.('value')
|
|
1213
|
+
?? funcNode.childForFieldName?.('operand')
|
|
1214
|
+
?? funcNode.childForFieldName?.('argument') // C/C++ field_expression
|
|
1215
|
+
?? funcNode.childForFieldName?.('expression')
|
|
1216
|
+
?? null;
|
|
1217
|
+
}
|
|
1218
|
+
if (!innerReceiver && current.type === 'method_invocation') {
|
|
1219
|
+
innerReceiver = current.childForFieldName?.('object') ?? null;
|
|
1220
|
+
}
|
|
1221
|
+
if (!innerReceiver && (current.type === 'member_call_expression' || current.type === 'nullsafe_member_call_expression')) {
|
|
1222
|
+
innerReceiver = current.childForFieldName?.('object') ?? null;
|
|
1223
|
+
}
|
|
1224
|
+
if (!innerReceiver && current.type === 'call') {
|
|
1225
|
+
innerReceiver = current.childForFieldName?.('receiver') ?? null;
|
|
1226
|
+
}
|
|
1227
|
+
if (!innerReceiver)
|
|
1228
|
+
break;
|
|
1229
|
+
if (CALL_EXPRESSION_TYPES.has(innerReceiver.type) || FIELD_ACCESS_NODE_TYPES.has(innerReceiver.type)) {
|
|
1230
|
+
current = innerReceiver;
|
|
1231
|
+
}
|
|
1232
|
+
else {
|
|
1233
|
+
return { chain, baseReceiverName: innerReceiver.text || undefined };
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
else if (FIELD_ACCESS_NODE_TYPES.has(current.type)) {
|
|
1237
|
+
// ── Field/member access: extract property name + inner object ─────────
|
|
1238
|
+
let propertyName;
|
|
1239
|
+
let innerObject = null;
|
|
1240
|
+
if (current.type === 'navigation_expression') {
|
|
1241
|
+
for (const child of current.children ?? []) {
|
|
1242
|
+
if (child.type === 'navigation_suffix') {
|
|
1243
|
+
for (const sc of child.children ?? []) {
|
|
1244
|
+
if (sc.isNamed && sc.type !== '.') {
|
|
1245
|
+
propertyName = sc.text;
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
else if (child.isNamed && !innerObject) {
|
|
1251
|
+
innerObject = child;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
else if (current.type === 'attribute') {
|
|
1256
|
+
innerObject = current.childForFieldName?.('object') ?? null;
|
|
1257
|
+
propertyName = current.childForFieldName?.('attribute')?.text;
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
innerObject = current.childForFieldName?.('object')
|
|
1261
|
+
?? current.childForFieldName?.('value')
|
|
1262
|
+
?? current.childForFieldName?.('operand')
|
|
1263
|
+
?? current.childForFieldName?.('argument') // C/C++ field_expression
|
|
1264
|
+
?? current.childForFieldName?.('expression')
|
|
1265
|
+
?? null;
|
|
1266
|
+
propertyName = (current.childForFieldName?.('property')
|
|
1267
|
+
?? current.childForFieldName?.('field')
|
|
1268
|
+
?? current.childForFieldName?.('name'))?.text;
|
|
1269
|
+
}
|
|
1270
|
+
if (!propertyName)
|
|
1271
|
+
break;
|
|
1272
|
+
chain.unshift({ kind: 'field', name: propertyName });
|
|
1273
|
+
if (!innerObject)
|
|
1274
|
+
break;
|
|
1275
|
+
if (CALL_EXPRESSION_TYPES.has(innerObject.type) || FIELD_ACCESS_NODE_TYPES.has(innerObject.type)) {
|
|
1276
|
+
current = innerObject;
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
return { chain, baseReceiverName: innerObject.text || undefined };
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
// Simple identifier — this is the base receiver
|
|
1284
|
+
return chain.length > 0
|
|
1285
|
+
? { chain, baseReceiverName: current.text || undefined }
|
|
1286
|
+
: undefined;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined;
|
|
1290
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
|
2
|
+
import { type MixedChainStep } from '../utils.js';
|
|
2
3
|
import type { ConstructorBinding } from '../type-env.js';
|
|
4
|
+
import type { NodeLabel } from '../../graph/types.js';
|
|
3
5
|
interface ParsedNode {
|
|
4
6
|
id: string;
|
|
5
7
|
label: string;
|
|
@@ -21,7 +23,7 @@ interface ParsedRelationship {
|
|
|
21
23
|
id: string;
|
|
22
24
|
sourceId: string;
|
|
23
25
|
targetId: string;
|
|
24
|
-
type: 'DEFINES' | 'HAS_METHOD';
|
|
26
|
+
type: 'DEFINES' | 'HAS_METHOD' | 'HAS_PROPERTY';
|
|
25
27
|
confidence: number;
|
|
26
28
|
reason: string;
|
|
27
29
|
}
|
|
@@ -29,9 +31,10 @@ interface ParsedSymbol {
|
|
|
29
31
|
filePath: string;
|
|
30
32
|
name: string;
|
|
31
33
|
nodeId: string;
|
|
32
|
-
type:
|
|
34
|
+
type: NodeLabel;
|
|
33
35
|
parameterCount?: number;
|
|
34
36
|
returnType?: string;
|
|
37
|
+
declaredType?: string;
|
|
35
38
|
ownerId?: string;
|
|
36
39
|
}
|
|
37
40
|
export interface ExtractedImport {
|
|
@@ -57,13 +60,25 @@ export interface ExtractedCall {
|
|
|
57
60
|
/** Resolved type name of the receiver (e.g., 'User' for user.save() when user: User) */
|
|
58
61
|
receiverTypeName?: string;
|
|
59
62
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `
|
|
63
|
+
* Unified mixed chain when the receiver is a chain of field accesses and/or method calls.
|
|
64
|
+
* Steps are ordered base-first (innermost to outermost). Examples:
|
|
65
|
+
* `svc.getUser().save()` → chain=[{kind:'call',name:'getUser'}], receiverName='svc'
|
|
66
|
+
* `user.address.save()` → chain=[{kind:'field',name:'address'}], receiverName='user'
|
|
67
|
+
* `svc.getUser().address.save()` → chain=[{kind:'call',name:'getUser'},{kind:'field',name:'address'}]
|
|
64
68
|
* Length is capped at MAX_CHAIN_DEPTH (3).
|
|
65
69
|
*/
|
|
66
|
-
|
|
70
|
+
receiverMixedChain?: MixedChainStep[];
|
|
71
|
+
}
|
|
72
|
+
export interface ExtractedAssignment {
|
|
73
|
+
filePath: string;
|
|
74
|
+
/** generateId of enclosing function, or generateId('File', filePath) for top-level */
|
|
75
|
+
sourceId: string;
|
|
76
|
+
/** Receiver text (e.g., 'user' from user.address = value) */
|
|
77
|
+
receiverText: string;
|
|
78
|
+
/** Property name being written (e.g., 'address') */
|
|
79
|
+
propertyName: string;
|
|
80
|
+
/** Resolved type name of the receiver if available from TypeEnv */
|
|
81
|
+
receiverTypeName?: string;
|
|
67
82
|
}
|
|
68
83
|
export interface ExtractedHeritage {
|
|
69
84
|
filePath: string;
|
|
@@ -93,6 +108,7 @@ export interface ParseWorkerResult {
|
|
|
93
108
|
symbols: ParsedSymbol[];
|
|
94
109
|
imports: ExtractedImport[];
|
|
95
110
|
calls: ExtractedCall[];
|
|
111
|
+
assignments: ExtractedAssignment[];
|
|
96
112
|
heritage: ExtractedHeritage[];
|
|
97
113
|
routes: ExtractedRoute[];
|
|
98
114
|
constructorBindings: FileConstructorBindings[];
|
|
@@ -28,7 +28,7 @@ try {
|
|
|
28
28
|
Kotlin = _require('tree-sitter-kotlin');
|
|
29
29
|
}
|
|
30
30
|
catch { }
|
|
31
|
-
import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode,
|
|
31
|
+
import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, } from '../utils.js';
|
|
32
32
|
import { buildTypeEnv } from '../type-env.js';
|
|
33
33
|
import { isNodeExported } from '../export-detection.js';
|
|
34
34
|
import { detectFrameworkFromAST } from '../framework-detection.js';
|
|
@@ -37,6 +37,7 @@ import { generateId } from '../../../lib/utils.js';
|
|
|
37
37
|
import { extractNamedBindings } from '../named-binding-extraction.js';
|
|
38
38
|
import { appendKotlinWildcard } from '../resolvers/index.js';
|
|
39
39
|
import { callRouters } from '../call-routing.js';
|
|
40
|
+
import { extractPropertyDeclaredType } from '../type-extractors/shared.js';
|
|
40
41
|
// ============================================================================
|
|
41
42
|
// Worker-local parser + language map
|
|
42
43
|
// ============================================================================
|
|
@@ -162,6 +163,7 @@ const processBatch = (files, onProgress) => {
|
|
|
162
163
|
symbols: [],
|
|
163
164
|
imports: [],
|
|
164
165
|
calls: [],
|
|
166
|
+
assignments: [],
|
|
165
167
|
heritage: [],
|
|
166
168
|
routes: [],
|
|
167
169
|
constructorBindings: [],
|
|
@@ -759,6 +761,28 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
759
761
|
});
|
|
760
762
|
continue;
|
|
761
763
|
}
|
|
764
|
+
// Extract assignment sites (field write access)
|
|
765
|
+
if (captureMap['assignment'] && captureMap['assignment.receiver'] && captureMap['assignment.property']) {
|
|
766
|
+
const receiverText = captureMap['assignment.receiver'].text;
|
|
767
|
+
const propertyName = captureMap['assignment.property'].text;
|
|
768
|
+
if (receiverText && propertyName) {
|
|
769
|
+
const srcId = findEnclosingFunctionId(captureMap['assignment'], file.path)
|
|
770
|
+
|| generateId('File', file.path);
|
|
771
|
+
let receiverTypeName;
|
|
772
|
+
if (typeEnv) {
|
|
773
|
+
receiverTypeName = typeEnv.lookup(receiverText, captureMap['assignment']) ?? undefined;
|
|
774
|
+
}
|
|
775
|
+
result.assignments.push({
|
|
776
|
+
filePath: file.path,
|
|
777
|
+
sourceId: srcId,
|
|
778
|
+
receiverText,
|
|
779
|
+
propertyName,
|
|
780
|
+
...(receiverTypeName ? { receiverTypeName } : {}),
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (!captureMap['call'])
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
762
786
|
// Extract call sites
|
|
763
787
|
if (captureMap['call']) {
|
|
764
788
|
const callNameNode = captureMap['call.name'];
|
|
@@ -811,6 +835,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
811
835
|
nodeId,
|
|
812
836
|
type: 'Property',
|
|
813
837
|
...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
|
|
838
|
+
...(item.declaredType ? { declaredType: item.declaredType } : {}),
|
|
814
839
|
});
|
|
815
840
|
const fileId = generateId('File', file.path);
|
|
816
841
|
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
|
@@ -824,10 +849,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
824
849
|
});
|
|
825
850
|
if (propEnclosingClassId) {
|
|
826
851
|
result.relationships.push({
|
|
827
|
-
id: generateId('
|
|
852
|
+
id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
|
|
828
853
|
sourceId: propEnclosingClassId,
|
|
829
854
|
targetId: nodeId,
|
|
830
|
-
type: '
|
|
855
|
+
type: 'HAS_PROPERTY',
|
|
831
856
|
confidence: 1.0,
|
|
832
857
|
reason: '',
|
|
833
858
|
});
|
|
@@ -844,26 +869,19 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
844
869
|
const callForm = inferCallForm(callNode, callNameNode);
|
|
845
870
|
let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
|
|
846
871
|
let receiverTypeName = receiverName ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
847
|
-
let
|
|
848
|
-
// When the receiver is a
|
|
849
|
-
// extractReceiverName returns undefined
|
|
850
|
-
//
|
|
851
|
-
// We capture the base receiver name so processCallsFromExtracted can look it up
|
|
852
|
-
// from constructor bindings. receiverTypeName is intentionally left unset here —
|
|
853
|
-
// the chain resolver in processCallsFromExtracted needs the base type as input and
|
|
854
|
-
// produces the final receiver type as output.
|
|
872
|
+
let receiverMixedChain;
|
|
873
|
+
// When the receiver is a complex expression (call chain, field chain, or mixed),
|
|
874
|
+
// extractReceiverName returns undefined. Walk the receiver node to build a unified
|
|
875
|
+
// mixed chain for deferred resolution in processCallsFromExtracted.
|
|
855
876
|
if (callForm === 'member' && receiverName === undefined && !receiverTypeName) {
|
|
856
877
|
const receiverNode = extractReceiverNode(callNameNode);
|
|
857
|
-
if (receiverNode
|
|
858
|
-
const extracted =
|
|
859
|
-
if (extracted) {
|
|
860
|
-
|
|
861
|
-
// Set receiverName to the base object so Step 1 in processCallsFromExtracted
|
|
862
|
-
// can resolve it via constructor bindings to a base type for the chain.
|
|
878
|
+
if (receiverNode) {
|
|
879
|
+
const extracted = extractMixedChain(receiverNode);
|
|
880
|
+
if (extracted && extracted.chain.length > 0) {
|
|
881
|
+
receiverMixedChain = extracted.chain;
|
|
863
882
|
receiverName = extracted.baseReceiverName;
|
|
864
|
-
//
|
|
865
|
-
// and annotated parameters
|
|
866
|
-
// This sets a base type that chain resolution (Step 2) will use as input.
|
|
883
|
+
// Try the type environment immediately for the base receiver
|
|
884
|
+
// (covers explicitly-typed locals and annotated parameters).
|
|
867
885
|
if (receiverName) {
|
|
868
886
|
receiverTypeName = typeEnv.lookup(receiverName, callNode);
|
|
869
887
|
}
|
|
@@ -878,7 +896,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
878
896
|
...(callForm !== undefined ? { callForm } : {}),
|
|
879
897
|
...(receiverName !== undefined ? { receiverName } : {}),
|
|
880
898
|
...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
|
|
881
|
-
...(
|
|
899
|
+
...(receiverMixedChain !== undefined ? { receiverMixedChain } : {}),
|
|
882
900
|
});
|
|
883
901
|
}
|
|
884
902
|
}
|
|
@@ -926,6 +944,21 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
926
944
|
const nodeLabel = getLabelFromCaptures(captureMap);
|
|
927
945
|
if (!nodeLabel)
|
|
928
946
|
continue;
|
|
947
|
+
// C/C++: @definition.function is broad and also matches inline class methods (inside
|
|
948
|
+
// a class/struct body). Those are already captured by @definition.method, so skip
|
|
949
|
+
// the duplicate Function entry to prevent double-indexing in globalIndex.
|
|
950
|
+
if ((language === SupportedLanguages.CPlusPlus || language === SupportedLanguages.C) &&
|
|
951
|
+
nodeLabel === 'Function') {
|
|
952
|
+
let ancestor = captureMap['definition.function']?.parent;
|
|
953
|
+
while (ancestor) {
|
|
954
|
+
if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') {
|
|
955
|
+
break; // inside a class body — duplicate of @definition.method
|
|
956
|
+
}
|
|
957
|
+
ancestor = ancestor.parent;
|
|
958
|
+
}
|
|
959
|
+
if (ancestor)
|
|
960
|
+
continue; // found a class/struct ancestor → skip
|
|
961
|
+
}
|
|
929
962
|
const nameNode = captureMap['name'];
|
|
930
963
|
// Synthesize name for constructors without explicit @name capture (e.g. Swift init)
|
|
931
964
|
if (!nameNode && nodeLabel !== 'Constructor')
|
|
@@ -948,6 +981,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
948
981
|
: null;
|
|
949
982
|
let parameterCount;
|
|
950
983
|
let returnType;
|
|
984
|
+
let declaredType;
|
|
951
985
|
if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
|
|
952
986
|
const sig = extractMethodSignature(definitionNode);
|
|
953
987
|
parameterCount = sig.parameterCount;
|
|
@@ -963,6 +997,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
963
997
|
}
|
|
964
998
|
}
|
|
965
999
|
}
|
|
1000
|
+
else if (nodeLabel === 'Property' && definitionNode) {
|
|
1001
|
+
// Extract the declared type for property/field nodes.
|
|
1002
|
+
// Walk the definition node for type annotation children.
|
|
1003
|
+
declaredType = extractPropertyDeclaredType(definitionNode);
|
|
1004
|
+
}
|
|
966
1005
|
result.nodes.push({
|
|
967
1006
|
id: nodeId,
|
|
968
1007
|
label: nodeLabel,
|
|
@@ -993,6 +1032,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
993
1032
|
type: nodeLabel,
|
|
994
1033
|
...(parameterCount !== undefined ? { parameterCount } : {}),
|
|
995
1034
|
...(returnType !== undefined ? { returnType } : {}),
|
|
1035
|
+
...(declaredType !== undefined ? { declaredType } : {}),
|
|
996
1036
|
...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
|
|
997
1037
|
});
|
|
998
1038
|
const fileId = generateId('File', file.path);
|
|
@@ -1005,13 +1045,14 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1005
1045
|
confidence: 1.0,
|
|
1006
1046
|
reason: '',
|
|
1007
1047
|
});
|
|
1008
|
-
// ── HAS_METHOD: link
|
|
1048
|
+
// ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
|
|
1009
1049
|
if (enclosingClassId) {
|
|
1050
|
+
const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
|
|
1010
1051
|
result.relationships.push({
|
|
1011
|
-
id: generateId(
|
|
1052
|
+
id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
|
|
1012
1053
|
sourceId: enclosingClassId,
|
|
1013
1054
|
targetId: nodeId,
|
|
1014
|
-
type:
|
|
1055
|
+
type: memberEdgeType,
|
|
1015
1056
|
confidence: 1.0,
|
|
1016
1057
|
reason: '',
|
|
1017
1058
|
});
|
|
@@ -1030,7 +1071,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1030
1071
|
/** Accumulated result across sub-batches */
|
|
1031
1072
|
let accumulated = {
|
|
1032
1073
|
nodes: [], relationships: [], symbols: [],
|
|
1033
|
-
imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0,
|
|
1074
|
+
imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0,
|
|
1034
1075
|
};
|
|
1035
1076
|
let cumulativeProcessed = 0;
|
|
1036
1077
|
const mergeResult = (target, src) => {
|
|
@@ -1039,6 +1080,7 @@ const mergeResult = (target, src) => {
|
|
|
1039
1080
|
target.symbols.push(...src.symbols);
|
|
1040
1081
|
target.imports.push(...src.imports);
|
|
1041
1082
|
target.calls.push(...src.calls);
|
|
1083
|
+
target.assignments.push(...src.assignments);
|
|
1042
1084
|
target.heritage.push(...src.heritage);
|
|
1043
1085
|
target.routes.push(...src.routes);
|
|
1044
1086
|
target.constructorBindings.push(...src.constructorBindings);
|
|
@@ -1064,7 +1106,7 @@ parentPort.on('message', (msg) => {
|
|
|
1064
1106
|
if (msg && msg.type === 'flush') {
|
|
1065
1107
|
parentPort.postMessage({ type: 'result', data: accumulated });
|
|
1066
1108
|
// Reset for potential reuse
|
|
1067
|
-
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0 };
|
|
1109
|
+
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0 };
|
|
1068
1110
|
cumulativeProcessed = 0;
|
|
1069
1111
|
return;
|
|
1070
1112
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import lbug from '@ladybugdb/core';
|
|
2
2
|
import { KnowledgeGraph } from '../graph/types.js';
|
|
3
|
+
/** Expose the current Database for pool adapter reuse in tests. */
|
|
4
|
+
export declare const getDatabase: () => lbug.Database | null;
|
|
3
5
|
export declare const initLbug: (dbPath: string) => Promise<{
|
|
4
6
|
db: lbug.Database;
|
|
5
7
|
conn: lbug.Connection;
|
|
@@ -9,6 +9,8 @@ let db = null;
|
|
|
9
9
|
let conn = null;
|
|
10
10
|
let currentDbPath = null;
|
|
11
11
|
let ftsLoaded = false;
|
|
12
|
+
/** Expose the current Database for pool adapter reuse in tests. */
|
|
13
|
+
export const getDatabase = () => db;
|
|
12
14
|
// Global session lock for operations that touch module-level lbug globals.
|
|
13
15
|
// This guarantees no DB switch can happen while an operation is running.
|
|
14
16
|
let sessionLock = Promise.resolve();
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
export declare const NODE_TABLES: readonly ["File", "Folder", "Function", "Class", "Interface", "Method", "CodeElement", "Community", "Process", "Struct", "Enum", "Macro", "Typedef", "Union", "Namespace", "Trait", "Impl", "TypeAlias", "Const", "Static", "Property", "Record", "Delegate", "Annotation", "Constructor", "Template", "Module"];
|
|
12
12
|
export type NodeTableName = typeof NODE_TABLES[number];
|
|
13
13
|
export declare const REL_TABLE_NAME = "CodeRelation";
|
|
14
|
-
export declare const REL_TYPES: readonly ["CONTAINS", "DEFINES", "IMPORTS", "CALLS", "EXTENDS", "IMPLEMENTS", "HAS_METHOD", "OVERRIDES", "MEMBER_OF", "STEP_IN_PROCESS"];
|
|
14
|
+
export declare const REL_TYPES: readonly ["CONTAINS", "DEFINES", "IMPORTS", "CALLS", "EXTENDS", "IMPLEMENTS", "HAS_METHOD", "HAS_PROPERTY", "ACCESSES", "OVERRIDES", "MEMBER_OF", "STEP_IN_PROCESS"];
|
|
15
15
|
export type RelType = typeof REL_TYPES[number];
|
|
16
16
|
export declare const EMBEDDING_TABLE_NAME = "CodeEmbedding";
|
|
17
17
|
export declare const FILE_SCHEMA = "\nCREATE NODE TABLE File (\n id STRING,\n name STRING,\n filePath STRING,\n content STRING,\n PRIMARY KEY (id)\n)";
|
package/dist/core/lbug/schema.js
CHANGED
|
@@ -22,7 +22,7 @@ export const NODE_TABLES = [
|
|
|
22
22
|
// ============================================================================
|
|
23
23
|
export const REL_TABLE_NAME = 'CodeRelation';
|
|
24
24
|
// Valid relation types
|
|
25
|
-
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'];
|
|
25
|
+
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'];
|
|
26
26
|
// ============================================================================
|
|
27
27
|
// EMBEDDING TABLE
|
|
28
28
|
// ============================================================================
|
|
@@ -12,11 +12,29 @@
|
|
|
12
12
|
* @see https://docs.ladybugdb.com/concurrency — multiple Connections
|
|
13
13
|
* from the same Database is the officially supported concurrency pattern.
|
|
14
14
|
*/
|
|
15
|
+
import lbug from '@ladybugdb/core';
|
|
16
|
+
/** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
|
|
17
|
+
export declare const realStdoutWrite: any;
|
|
15
18
|
/**
|
|
16
19
|
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
|
17
20
|
* Retries on lock errors (e.g., when `gitnexus analyze` is running).
|
|
21
|
+
*
|
|
22
|
+
* Concurrent calls for the same repoId are deduplicated — the second caller
|
|
23
|
+
* awaits the first's in-progress init rather than starting a redundant one.
|
|
18
24
|
*/
|
|
19
25
|
export declare const initLbug: (repoId: string, dbPath: string) => Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Initialize a pool entry from a pre-existing Database object.
|
|
28
|
+
*
|
|
29
|
+
* Used in tests to avoid the writable→close→read-only cycle that crashes
|
|
30
|
+
* on macOS due to N-API destructor segfaults. The pool adapter reuses
|
|
31
|
+
* the core adapter's writable Database instead of opening a new read-only one.
|
|
32
|
+
*
|
|
33
|
+
* The Database is registered in the shared dbCache so closeOne() decrements
|
|
34
|
+
* the refCount correctly. If the Database is already cached (e.g. another
|
|
35
|
+
* repoId already injected it), the existing entry is reused.
|
|
36
|
+
*/
|
|
37
|
+
export declare function initLbugWithDb(repoId: string, existingDb: lbug.Database, dbPath: string): Promise<void>;
|
|
20
38
|
export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
|
|
21
39
|
/**
|
|
22
40
|
* Execute a parameterized query on a specific repo's connection pool.
|
|
@@ -33,3 +51,7 @@ export declare const closeLbug: (repoId?: string) => Promise<void>;
|
|
|
33
51
|
* Check if a specific repo's pool is active
|
|
34
52
|
*/
|
|
35
53
|
export declare const isLbugReady: (repoId: string) => boolean;
|
|
54
|
+
/** Regex to detect write operations in user-supplied Cypher queries */
|
|
55
|
+
export declare const CYPHER_WRITE_RE: RegExp;
|
|
56
|
+
/** Check if a Cypher query contains write operations */
|
|
57
|
+
export declare function isWriteQuery(query: string): boolean;
|