autosnippet 3.2.6 → 3.2.8
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 +16 -1
- package/bin/cli.js +7 -0
- package/dashboard/dist/assets/index-D5jiDBQG.css +1 -0
- package/dashboard/dist/assets/{index-DfHY_3ln.js → index-e5OKj-Ni.js} +38 -38
- package/dashboard/dist/index.html +2 -2
- package/lib/cli/AiScanService.js +3 -3
- package/lib/core/AstAnalyzer.js +26 -4
- package/lib/core/analysis/CallEdgeResolver.js +402 -0
- package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
- package/lib/core/analysis/CallSiteExtractor.js +629 -0
- package/lib/core/analysis/DataFlowInferrer.js +57 -0
- package/lib/core/analysis/ImportPathResolver.js +189 -0
- package/lib/core/analysis/ImportRecord.js +105 -0
- package/lib/core/analysis/SymbolTableBuilder.js +211 -0
- package/lib/core/ast/ProjectGraph.js +8 -0
- package/lib/core/ast/lang-dart.js +352 -5
- package/lib/core/ast/lang-go.js +212 -10
- package/lib/core/ast/lang-java.js +205 -1
- package/lib/core/ast/lang-kotlin.js +330 -1
- package/lib/core/ast/lang-python.js +31 -2
- package/lib/core/ast/lang-rust.js +284 -3
- package/lib/core/ast/lang-swift.js +180 -1
- package/lib/core/ast/lang-typescript.js +290 -1
- package/lib/external/mcp/McpServer.js +1 -0
- package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +21 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +5 -4
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +70 -4
- package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +95 -1
- package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
- package/lib/external/mcp/handlers/bootstrap-internal.js +17 -6
- package/lib/external/mcp/handlers/consolidated.js +9 -0
- package/lib/external/mcp/handlers/guard.js +3 -3
- package/lib/external/mcp/handlers/structure.js +62 -0
- package/lib/external/mcp/handlers/task.js +182 -10
- package/lib/external/mcp/handlers/wiki-external.js +66 -3
- package/lib/external/mcp/tools.js +36 -1
- package/lib/http/HttpServer.js +4 -0
- package/lib/http/routes/remote.js +1138 -0
- package/lib/http/routes/task.js +1 -0
- package/lib/infrastructure/database/migrations/003_add_remote_commands.js +27 -0
- package/lib/injection/ServiceContainer.js +6 -11
- package/lib/platform/ios/index.js +2 -2
- package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
- package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
- package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
- package/lib/service/chat/ChatAgent.js +1 -1
- package/lib/service/chat/ChatAgentPrompts.js +13 -1
- package/lib/service/chat/ExplorationTracker.js +52 -8
- package/lib/service/chat/HandoffProtocol.js +19 -1
- package/lib/service/chat/WorkingMemory.js +3 -1
- package/lib/service/chat/memory/ActiveContext.js +3 -1
- package/lib/service/chat/memory/SessionStore.js +4 -3
- package/lib/service/chat/tools/ast-graph.js +229 -32
- package/lib/service/chat/tools/index.js +6 -1
- package/lib/service/chat/tools/infrastructure.js +5 -0
- package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
- package/lib/service/knowledge/CodeEntityGraph.js +327 -2
- package/lib/service/knowledge/KnowledgeService.js +5 -1
- package/lib/service/module/ModuleService.js +9 -0
- package/lib/service/wiki/WikiGenerator.js +1 -1
- package/lib/shared/PathGuard.js +1 -1
- package/package.json +12 -1
- package/dashboard/dist/assets/index-BaGY7kJI.css +0 -1
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* 模式: Singleton, Builder, Factory, DI, Stream Pipeline
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { ImportRecord } from '../analysis/ImportRecord.js';
|
|
10
|
+
|
|
9
11
|
function walkJava(root, ctx) {
|
|
10
12
|
_walkJavaNode(root, ctx, null);
|
|
11
13
|
}
|
|
@@ -16,9 +18,29 @@ function _walkJavaNode(node, ctx, parentClassName) {
|
|
|
16
18
|
|
|
17
19
|
switch (child.type) {
|
|
18
20
|
case 'import_declaration': {
|
|
21
|
+
const isStatic = child.namedChildren.some((c) => c.text === 'static');
|
|
19
22
|
const path = child.namedChildren.find((c) => c.type === 'scoped_identifier');
|
|
20
23
|
if (path) {
|
|
21
|
-
|
|
24
|
+
const fullPath = path.text; // e.g. com.example.MyClass or com.example.MyClass.myMethod
|
|
25
|
+
const segments = fullPath.split('.');
|
|
26
|
+
const lastName = segments[segments.length - 1];
|
|
27
|
+
// Java wildcard: import com.example.*
|
|
28
|
+
const isWildcard = child.text.includes('.*');
|
|
29
|
+
if (isWildcard) {
|
|
30
|
+
ctx.imports.push(
|
|
31
|
+
new ImportRecord(fullPath, { symbols: ['*'], kind: 'namespace' })
|
|
32
|
+
);
|
|
33
|
+
} else if (isStatic) {
|
|
34
|
+
// static import: import static com.example.MyClass.method
|
|
35
|
+
ctx.imports.push(
|
|
36
|
+
new ImportRecord(fullPath, { symbols: [lastName], kind: 'named', isTypeOnly: false })
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
// regular: import com.example.MyClass
|
|
40
|
+
ctx.imports.push(
|
|
41
|
+
new ImportRecord(fullPath, { symbols: [lastName], alias: lastName, kind: 'named' })
|
|
42
|
+
);
|
|
43
|
+
}
|
|
22
44
|
}
|
|
23
45
|
break;
|
|
24
46
|
}
|
|
@@ -255,6 +277,22 @@ function _parseJavaField(node, className) {
|
|
|
255
277
|
.filter((c) => c.type === 'marker_annotation' || c.type === 'annotation')
|
|
256
278
|
.map((a) => a.text);
|
|
257
279
|
|
|
280
|
+
// Phase 5.3: Extract field type for DI resolution
|
|
281
|
+
// field_declaration: [modifiers] type_identifier variable_declarator
|
|
282
|
+
let typeAnnotation = null;
|
|
283
|
+
const typeNode = node.namedChildren.find(
|
|
284
|
+
(c) =>
|
|
285
|
+
c.type === 'type_identifier' ||
|
|
286
|
+
c.type === 'generic_type' ||
|
|
287
|
+
c.type === 'scoped_type_identifier'
|
|
288
|
+
);
|
|
289
|
+
if (typeNode) {
|
|
290
|
+
// Strip generics: List<User> → List, Repository<User, Long> → Repository
|
|
291
|
+
const text = typeNode.text;
|
|
292
|
+
const bracketIdx = text.indexOf('<');
|
|
293
|
+
typeAnnotation = bracketIdx > 0 ? text.slice(0, bracketIdx) : text;
|
|
294
|
+
}
|
|
295
|
+
|
|
258
296
|
return {
|
|
259
297
|
name,
|
|
260
298
|
className,
|
|
@@ -262,6 +300,7 @@ function _parseJavaField(node, className) {
|
|
|
262
300
|
isFinal,
|
|
263
301
|
isPrivate,
|
|
264
302
|
annotations,
|
|
303
|
+
typeAnnotation,
|
|
265
304
|
line: node.startPosition.row + 1,
|
|
266
305
|
};
|
|
267
306
|
}
|
|
@@ -413,6 +452,170 @@ function _maxNesting(node, depth) {
|
|
|
413
452
|
return max;
|
|
414
453
|
}
|
|
415
454
|
|
|
455
|
+
// ── Java Call Site 提取 (Phase 5) ─────────────────────────────
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 从 Java AST root 提取所有调用点
|
|
459
|
+
* 遍历 method_declaration / constructor_declaration 中的 block → method_invocation / object_creation_expression
|
|
460
|
+
*/
|
|
461
|
+
function extractCallSitesJava(root, ctx, _lang) {
|
|
462
|
+
const scopes = _collectJavaScopes(root);
|
|
463
|
+
for (const scope of scopes) {
|
|
464
|
+
_extractJavaCallSitesFromBody(scope.body, scope.className, scope.methodName, ctx);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 递归收集 Java 中所有方法体作用域
|
|
470
|
+
*/
|
|
471
|
+
function _collectJavaScopes(root) {
|
|
472
|
+
const scopes = [];
|
|
473
|
+
|
|
474
|
+
function visit(node, className) {
|
|
475
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
476
|
+
const child = node.namedChild(i);
|
|
477
|
+
|
|
478
|
+
if (child.type === 'class_declaration' || child.type === 'enum_declaration' || child.type === 'record_declaration') {
|
|
479
|
+
const name = child.namedChildren.find((c) => c.type === 'identifier')?.text;
|
|
480
|
+
const body = child.namedChildren.find((c) => c.type === 'class_body' || c.type === 'enum_body');
|
|
481
|
+
if (body) {
|
|
482
|
+
visit(body, name || className);
|
|
483
|
+
}
|
|
484
|
+
} else if (child.type === 'interface_declaration') {
|
|
485
|
+
const name = child.namedChildren.find((c) => c.type === 'identifier')?.text;
|
|
486
|
+
const body = child.namedChildren.find((c) => c.type === 'interface_body');
|
|
487
|
+
if (body) {
|
|
488
|
+
visit(body, name || className);
|
|
489
|
+
}
|
|
490
|
+
} else if (child.type === 'method_declaration' || child.type === 'constructor_declaration') {
|
|
491
|
+
const name = child.namedChildren.find((c) => c.type === 'identifier')?.text || '<init>';
|
|
492
|
+
const body = child.namedChildren.find((c) => c.type === 'block');
|
|
493
|
+
if (body) {
|
|
494
|
+
scopes.push({ body, className, methodName: name });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
visit(root, null);
|
|
501
|
+
return scopes;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 从 Java method body 中递归提取调用点
|
|
506
|
+
*/
|
|
507
|
+
function _extractJavaCallSitesFromBody(bodyNode, className, methodName, ctx) {
|
|
508
|
+
if (!bodyNode) return;
|
|
509
|
+
|
|
510
|
+
const JAVA_NOISE = new Set([
|
|
511
|
+
'System', 'Math', 'String', 'Integer', 'Long', 'Double', 'Float',
|
|
512
|
+
'Boolean', 'Character', 'Byte', 'Short', 'Arrays', 'Collections',
|
|
513
|
+
'Objects', 'Optional', 'Stream', 'Collectors', 'List', 'Map', 'Set',
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
function walk(node) {
|
|
517
|
+
if (!node || node.type === 'ERROR' || node.isMissing) return;
|
|
518
|
+
|
|
519
|
+
if (node.type === 'method_invocation') {
|
|
520
|
+
// obj.method(args) or method(args)
|
|
521
|
+
// In Java tree-sitter: namedChildren = [object?, name(identifier), argument_list]
|
|
522
|
+
// The method name is the LAST identifier before argument_list
|
|
523
|
+
const identifiers = node.namedChildren.filter((c) => c.type === 'identifier');
|
|
524
|
+
const args = node.namedChildren.find((c) => c.type === 'argument_list');
|
|
525
|
+
const argCount = args ? args.namedChildCount : 0;
|
|
526
|
+
|
|
527
|
+
let callee, receiver = null, receiverType = null, callType;
|
|
528
|
+
|
|
529
|
+
if (identifiers.length >= 2) {
|
|
530
|
+
// obj.method() — first identifier is receiver, last is method name
|
|
531
|
+
const objectNode = identifiers[0];
|
|
532
|
+
const methodNode = identifiers[identifiers.length - 1];
|
|
533
|
+
receiver = objectNode.text?.slice(0, 80);
|
|
534
|
+
callee = methodNode.text;
|
|
535
|
+
callType = 'method';
|
|
536
|
+
|
|
537
|
+
// Check known noise
|
|
538
|
+
if (receiver && JAVA_NOISE.has(receiver.split('.')[0])) {
|
|
539
|
+
receiverType = receiver;
|
|
540
|
+
callType = 'static';
|
|
541
|
+
}
|
|
542
|
+
} else if (identifiers.length === 1) {
|
|
543
|
+
// Could be: unqualified method(args) or receiver is an expression
|
|
544
|
+
const objectNode = node.namedChildren[0];
|
|
545
|
+
if (objectNode && objectNode.type !== 'identifier' && objectNode.type !== 'argument_list') {
|
|
546
|
+
// Expression receiver: e.g. getService().doWork() or super.method()
|
|
547
|
+
receiver = objectNode.text?.slice(0, 80);
|
|
548
|
+
callee = identifiers[0].text;
|
|
549
|
+
callType = 'method';
|
|
550
|
+
// super.xxx() → CHA 解析到父类
|
|
551
|
+
if (receiver === 'super') {
|
|
552
|
+
callType = 'super';
|
|
553
|
+
receiverType = className;
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
callee = identifiers[0].text;
|
|
557
|
+
callType = 'function'; // unqualified method call within same class
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
callee = node.text?.split('(')[0]?.slice(0, 80) || 'unknown';
|
|
561
|
+
callType = 'function';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
ctx.callSites.push({
|
|
565
|
+
callee,
|
|
566
|
+
callerMethod: methodName,
|
|
567
|
+
callerClass: className,
|
|
568
|
+
callType,
|
|
569
|
+
receiver,
|
|
570
|
+
receiverType,
|
|
571
|
+
argCount,
|
|
572
|
+
line: node.startPosition.row + 1,
|
|
573
|
+
isAwait: false,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// walk arguments for nested calls
|
|
577
|
+
if (args) walkChildren(args);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (node.type === 'object_creation_expression') {
|
|
582
|
+
// new ClassName(args)
|
|
583
|
+
const typeNode = node.namedChildren.find(
|
|
584
|
+
(c) => c.type === 'type_identifier' || c.type === 'generic_type' || c.type === 'scoped_type_identifier'
|
|
585
|
+
);
|
|
586
|
+
const args = node.namedChildren.find((c) => c.type === 'argument_list');
|
|
587
|
+
const argCount = args ? args.namedChildCount : 0;
|
|
588
|
+
|
|
589
|
+
const typeName = typeNode?.text || 'Unknown';
|
|
590
|
+
|
|
591
|
+
ctx.callSites.push({
|
|
592
|
+
callee: typeName,
|
|
593
|
+
callerMethod: methodName,
|
|
594
|
+
callerClass: className,
|
|
595
|
+
callType: 'constructor',
|
|
596
|
+
receiver: null,
|
|
597
|
+
receiverType: typeName,
|
|
598
|
+
argCount,
|
|
599
|
+
line: node.startPosition.row + 1,
|
|
600
|
+
isAwait: false,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
if (args) walkChildren(args);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
walkChildren(node);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function walkChildren(node) {
|
|
611
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
612
|
+
walk(node.namedChild(i));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
walk(bodyNode);
|
|
617
|
+
}
|
|
618
|
+
|
|
416
619
|
// ── 插件导出 ──
|
|
417
620
|
|
|
418
621
|
let _grammar = null;
|
|
@@ -427,5 +630,6 @@ export const plugin = {
|
|
|
427
630
|
getGrammar,
|
|
428
631
|
walk: walkJava,
|
|
429
632
|
detectPatterns: detectJavaPatterns,
|
|
633
|
+
extractCallSites: extractCallSitesJava,
|
|
430
634
|
extensions: ['.java'],
|
|
431
635
|
};
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* 模式: Singleton (object), Factory (companion), DSL, Flow, Sealed
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { ImportRecord } from '../analysis/ImportRecord.js';
|
|
10
|
+
|
|
9
11
|
function walkKotlin(root, ctx) {
|
|
10
12
|
_walkKtNode(root, ctx, null);
|
|
11
13
|
}
|
|
@@ -21,7 +23,28 @@ function _walkKtNode(node, ctx, parentClassName) {
|
|
|
21
23
|
(c) => c.type === 'identifier' || c.type === 'user_type'
|
|
22
24
|
);
|
|
23
25
|
if (id) {
|
|
24
|
-
|
|
26
|
+
const fullPath = id.text; // e.g. com.example.MyClass or com.example.myFunc
|
|
27
|
+
const segments = fullPath.split('.');
|
|
28
|
+
const lastName = segments[segments.length - 1];
|
|
29
|
+
// Kotlin: import com.example.* (wildcard)
|
|
30
|
+
const isWildcard = child.text.includes('.*');
|
|
31
|
+
// Kotlin: import com.example.MyClass as Alias
|
|
32
|
+
const aliasNode = child.namedChildren.find((c) => c.type === 'import_alias');
|
|
33
|
+
const alias = aliasNode?.namedChildren?.find((c) => c.type === 'type_identifier' || c.type === 'simple_identifier')?.text;
|
|
34
|
+
|
|
35
|
+
if (isWildcard) {
|
|
36
|
+
ctx.imports.push(
|
|
37
|
+
new ImportRecord(fullPath, { symbols: ['*'], kind: 'namespace' })
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
ctx.imports.push(
|
|
41
|
+
new ImportRecord(fullPath, {
|
|
42
|
+
symbols: [lastName],
|
|
43
|
+
alias: alias || lastName,
|
|
44
|
+
kind: 'named',
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
25
48
|
}
|
|
26
49
|
break;
|
|
27
50
|
}
|
|
@@ -29,6 +52,11 @@ function _walkKtNode(node, ctx, parentClassName) {
|
|
|
29
52
|
case 'class_declaration': {
|
|
30
53
|
const classInfo = _parseKtClass(child);
|
|
31
54
|
ctx.classes.push(classInfo);
|
|
55
|
+
|
|
56
|
+
// Phase 5.3: Extract primary constructor parameter properties (Kotlin DI pattern)
|
|
57
|
+
// class UserService(private val repo: UserRepo) → property { name: 'repo', typeAnnotation: 'UserRepo' }
|
|
58
|
+
_extractKtConstructorProperties(child, ctx, classInfo.name);
|
|
59
|
+
|
|
32
60
|
const body = child.namedChildren.find((c) => c.type === 'class_body');
|
|
33
61
|
if (body) {
|
|
34
62
|
_walkKtClassBody(body, ctx, classInfo.name);
|
|
@@ -107,6 +135,8 @@ function _walkKtClassBody(body, ctx, className) {
|
|
|
107
135
|
const inner = _parseKtClass(child);
|
|
108
136
|
inner.outerClass = className;
|
|
109
137
|
ctx.classes.push(inner);
|
|
138
|
+
// Phase 5.3: Extract primary constructor params for inner classes too
|
|
139
|
+
_extractKtConstructorProperties(child, ctx, inner.name);
|
|
110
140
|
const innerBody = child.namedChildren.find((c) => c.type === 'class_body');
|
|
111
141
|
if (innerBody) {
|
|
112
142
|
_walkKtClassBody(innerBody, ctx, inner.name);
|
|
@@ -246,6 +276,50 @@ function _parseKtProperty(node, className) {
|
|
|
246
276
|
const isLazy = node.text.includes('by lazy');
|
|
247
277
|
const isLateinit = node.text.includes('lateinit');
|
|
248
278
|
|
|
279
|
+
// Phase 5.3: Extract property type for DI resolution
|
|
280
|
+
// Kotlin property types can be in variable_declaration or directly on the node:
|
|
281
|
+
// val userRepo: UserRepo → variable_declaration { simple_identifier, user_type }
|
|
282
|
+
// lateinit var service: UserService → user_type directly on property_declaration
|
|
283
|
+
let typeAnnotation = null;
|
|
284
|
+
|
|
285
|
+
// 1. Try from variable_declaration child
|
|
286
|
+
const varDecl = node.namedChildren.find((c) => c.type === 'variable_declaration');
|
|
287
|
+
if (varDecl) {
|
|
288
|
+
const typeNode = varDecl.namedChildren.find(
|
|
289
|
+
(c) => c.type === 'user_type' || c.type === 'nullable_type'
|
|
290
|
+
);
|
|
291
|
+
if (typeNode) {
|
|
292
|
+
typeAnnotation = _extractKtTypeName(typeNode);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 2. Fallback: try direct children (lateinit var patterns)
|
|
297
|
+
if (!typeAnnotation) {
|
|
298
|
+
const typeNode = node.namedChildren.find(
|
|
299
|
+
(c) => c.type === 'user_type' || c.type === 'nullable_type'
|
|
300
|
+
);
|
|
301
|
+
if (typeNode) {
|
|
302
|
+
typeAnnotation = _extractKtTypeName(typeNode);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Phase 5.3: Extract annotations on property (e.g. @Inject, @Autowired)
|
|
307
|
+
const annotations = [];
|
|
308
|
+
for (const child of node.namedChildren) {
|
|
309
|
+
if (child.type === 'annotation' || child.type === 'single_annotation') {
|
|
310
|
+
annotations.push(child.text);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Also check modifiers block
|
|
314
|
+
const modifiers = node.namedChildren.find((c) => c.type === 'modifiers');
|
|
315
|
+
if (modifiers) {
|
|
316
|
+
for (const child of modifiers.namedChildren) {
|
|
317
|
+
if (child.type === 'annotation' || child.type === 'single_annotation') {
|
|
318
|
+
annotations.push(child.text);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
249
323
|
return {
|
|
250
324
|
name,
|
|
251
325
|
className,
|
|
@@ -253,10 +327,105 @@ function _parseKtProperty(node, className) {
|
|
|
253
327
|
isMutable: isVar,
|
|
254
328
|
isLazy,
|
|
255
329
|
isLateinit,
|
|
330
|
+
typeAnnotation,
|
|
331
|
+
annotations: annotations.length > 0 ? annotations : undefined,
|
|
256
332
|
line: node.startPosition.row + 1,
|
|
257
333
|
};
|
|
258
334
|
}
|
|
259
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Phase 5.3: Extract Kotlin primary constructor parameter properties
|
|
338
|
+
*
|
|
339
|
+
* Kotlin constructors: class Svc(private val repo: UserRepo, val logger: Logger)
|
|
340
|
+
* Each val/var parameter in a primary_constructor becomes a class property.
|
|
341
|
+
*
|
|
342
|
+
* AST: class_declaration → primary_constructor → class_parameter[]
|
|
343
|
+
* Each class_parameter may contain: modifiers, val/var keyword, simple_identifier, user_type
|
|
344
|
+
*
|
|
345
|
+
* @param {object} classNode — class_declaration AST node
|
|
346
|
+
* @param {object} ctx — walker context
|
|
347
|
+
* @param {string} className
|
|
348
|
+
*/
|
|
349
|
+
function _extractKtConstructorProperties(classNode, ctx, className) {
|
|
350
|
+
const primaryCtor = classNode.namedChildren.find(
|
|
351
|
+
(c) => c.type === 'primary_constructor'
|
|
352
|
+
);
|
|
353
|
+
if (!primaryCtor) return;
|
|
354
|
+
|
|
355
|
+
for (const param of primaryCtor.namedChildren) {
|
|
356
|
+
if (param.type !== 'class_parameter') continue;
|
|
357
|
+
|
|
358
|
+
// Only val/var params become properties
|
|
359
|
+
const text = param.text;
|
|
360
|
+
const isVal = text.includes('val ');
|
|
361
|
+
const isVar = text.includes('var ');
|
|
362
|
+
if (!isVal && !isVar) continue;
|
|
363
|
+
|
|
364
|
+
const name = param.namedChildren.find(
|
|
365
|
+
(c) => c.type === 'simple_identifier'
|
|
366
|
+
)?.text;
|
|
367
|
+
if (!name) continue;
|
|
368
|
+
|
|
369
|
+
// Extract type annotation
|
|
370
|
+
let typeAnnotation = null;
|
|
371
|
+
const typeNode = param.namedChildren.find(
|
|
372
|
+
(c) => c.type === 'user_type' || c.type === 'nullable_type'
|
|
373
|
+
);
|
|
374
|
+
if (typeNode) {
|
|
375
|
+
typeAnnotation = _extractKtTypeName(typeNode);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Extract annotations (e.g. @Inject)
|
|
379
|
+
const annotations = [];
|
|
380
|
+
for (const child of param.namedChildren) {
|
|
381
|
+
if (child.type === 'annotation' || child.type === 'single_annotation' || child.type === 'modifiers') {
|
|
382
|
+
if (child.type === 'modifiers') {
|
|
383
|
+
for (const mod of child.namedChildren) {
|
|
384
|
+
if (mod.type === 'annotation' || mod.type === 'single_annotation') {
|
|
385
|
+
annotations.push(mod.text);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
annotations.push(child.text);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
ctx.properties.push({
|
|
395
|
+
name,
|
|
396
|
+
className,
|
|
397
|
+
isConstant: isVal,
|
|
398
|
+
isMutable: isVar,
|
|
399
|
+
isLazy: false,
|
|
400
|
+
isLateinit: false,
|
|
401
|
+
isConstructorParam: true,
|
|
402
|
+
typeAnnotation,
|
|
403
|
+
annotations: annotations.length > 0 ? annotations : undefined,
|
|
404
|
+
line: param.startPosition.row + 1,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Phase 5.3: Extract Kotlin type name from user_type or nullable_type node
|
|
411
|
+
* Strips generics and nullable marker
|
|
412
|
+
*/
|
|
413
|
+
function _extractKtTypeName(typeNode) {
|
|
414
|
+
if (typeNode.type === 'nullable_type') {
|
|
415
|
+
const inner = typeNode.namedChildren.find((c) => c.type === 'user_type');
|
|
416
|
+
if (inner) {
|
|
417
|
+
const text = inner.text;
|
|
418
|
+
const bracketIdx = text.indexOf('<');
|
|
419
|
+
return bracketIdx > 0 ? text.slice(0, bracketIdx) : text;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
// user_type
|
|
424
|
+
const text = typeNode.text;
|
|
425
|
+
const bracketIdx = text.indexOf('<');
|
|
426
|
+
return bracketIdx > 0 ? text.slice(0, bracketIdx) : text;
|
|
427
|
+
}
|
|
428
|
+
|
|
260
429
|
function _collectTypeRefs(node) {
|
|
261
430
|
const refs = [];
|
|
262
431
|
function walk(n) {
|
|
@@ -401,6 +570,165 @@ function _maxNesting(node, depth) {
|
|
|
401
570
|
return max;
|
|
402
571
|
}
|
|
403
572
|
|
|
573
|
+
// ── Kotlin Call Site 提取 (Phase 5) ──────────────────────────
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 从 Kotlin AST root 提取所有调用点
|
|
577
|
+
*/
|
|
578
|
+
function extractCallSitesKotlin(root, ctx, _lang) {
|
|
579
|
+
const scopes = _collectKtScopes(root);
|
|
580
|
+
for (const scope of scopes) {
|
|
581
|
+
_extractKtCallSitesFromBody(scope.body, scope.className, scope.methodName, ctx);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* 递归收集 Kotlin 中所有函数体作用域
|
|
587
|
+
*/
|
|
588
|
+
function _collectKtScopes(root) {
|
|
589
|
+
const scopes = [];
|
|
590
|
+
|
|
591
|
+
function visit(node, className) {
|
|
592
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
593
|
+
const child = node.namedChild(i);
|
|
594
|
+
|
|
595
|
+
if (child.type === 'class_declaration' || child.type === 'object_declaration') {
|
|
596
|
+
const name = child.namedChildren.find(
|
|
597
|
+
(c) => c.type === 'type_identifier' || c.type === 'simple_identifier'
|
|
598
|
+
)?.text;
|
|
599
|
+
const body = child.namedChildren.find((c) => c.type === 'class_body');
|
|
600
|
+
if (body) {
|
|
601
|
+
visit(body, name || className);
|
|
602
|
+
}
|
|
603
|
+
} else if (child.type === 'companion_object') {
|
|
604
|
+
const body = child.namedChildren.find((c) => c.type === 'class_body');
|
|
605
|
+
if (body) {
|
|
606
|
+
visit(body, className);
|
|
607
|
+
}
|
|
608
|
+
} else if (child.type === 'function_declaration') {
|
|
609
|
+
const name = child.namedChildren.find((c) => c.type === 'simple_identifier')?.text || 'unknown';
|
|
610
|
+
const body = child.namedChildren.find((c) => c.type === 'function_body' || c.type === 'block');
|
|
611
|
+
if (body) {
|
|
612
|
+
scopes.push({ body, className, methodName: name });
|
|
613
|
+
}
|
|
614
|
+
} else if (child.type === 'property_declaration') {
|
|
615
|
+
// property with getter/setter or initializer with lambda
|
|
616
|
+
const getter = child.namedChildren.find((c) => c.type === 'getter');
|
|
617
|
+
const setter = child.namedChildren.find((c) => c.type === 'setter');
|
|
618
|
+
const propName = child.namedChildren.find((c) => c.type === 'simple_identifier' || c.type === 'variable_declaration')?.text;
|
|
619
|
+
if (getter) {
|
|
620
|
+
const body = getter.namedChildren.find((c) => c.type === 'function_body' || c.type === 'block');
|
|
621
|
+
if (body) scopes.push({ body, className, methodName: `get_${propName || 'prop'}` });
|
|
622
|
+
}
|
|
623
|
+
if (setter) {
|
|
624
|
+
const body = setter.namedChildren.find((c) => c.type === 'function_body' || c.type === 'block');
|
|
625
|
+
if (body) scopes.push({ body, className, methodName: `set_${propName || 'prop'}` });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
visit(root, null);
|
|
632
|
+
return scopes;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* 从 Kotlin function body 中递归提取调用点
|
|
637
|
+
*/
|
|
638
|
+
function _extractKtCallSitesFromBody(bodyNode, className, methodName, ctx) {
|
|
639
|
+
if (!bodyNode) return;
|
|
640
|
+
|
|
641
|
+
const KT_NOISE = new Set([
|
|
642
|
+
'println', 'print', 'require', 'check', 'error', 'TODO',
|
|
643
|
+
'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf',
|
|
644
|
+
'arrayOf', 'intArrayOf', 'emptyList', 'emptyMap', 'emptySet',
|
|
645
|
+
'lazy', 'repeat', 'run', 'let', 'also', 'apply', 'with',
|
|
646
|
+
]);
|
|
647
|
+
|
|
648
|
+
function walk(node) {
|
|
649
|
+
if (!node || node.type === 'ERROR' || node.isMissing) return;
|
|
650
|
+
|
|
651
|
+
if (node.type === 'call_expression') {
|
|
652
|
+
const func = node.namedChildren[0];
|
|
653
|
+
if (!func) { walkChildren(node); return; }
|
|
654
|
+
|
|
655
|
+
let callee, receiver = null, receiverType = null, callType;
|
|
656
|
+
|
|
657
|
+
if (func.type === 'navigation_expression') {
|
|
658
|
+
// obj.method() or Pkg.func()
|
|
659
|
+
const parts = func.text.split('.');
|
|
660
|
+
if (parts.length >= 2) {
|
|
661
|
+
receiver = parts.slice(0, -1).join('.');
|
|
662
|
+
callee = parts[parts.length - 1];
|
|
663
|
+
if (receiver === 'this' || receiver === 'self') {
|
|
664
|
+
receiverType = className;
|
|
665
|
+
callType = 'method';
|
|
666
|
+
} else if (receiver === 'super') {
|
|
667
|
+
receiverType = className;
|
|
668
|
+
callType = 'super';
|
|
669
|
+
} else if (/^[A-Z]/.test(receiver)) {
|
|
670
|
+
receiverType = receiver;
|
|
671
|
+
callType = 'static';
|
|
672
|
+
} else {
|
|
673
|
+
callType = 'method';
|
|
674
|
+
receiverType = null;
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
callee = func.text;
|
|
678
|
+
callType = 'function';
|
|
679
|
+
}
|
|
680
|
+
} else if (func.type === 'simple_identifier') {
|
|
681
|
+
callee = func.text;
|
|
682
|
+
if (KT_NOISE.has(callee)) {
|
|
683
|
+
walkChildren(node);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
// PascalCase → constructor
|
|
687
|
+
callType = /^[A-Z]/.test(callee) ? 'constructor' : 'function';
|
|
688
|
+
if (callType === 'constructor') {
|
|
689
|
+
receiverType = callee;
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
callee = func.text?.slice(0, 80) || 'unknown';
|
|
693
|
+
callType = 'function';
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 计算参数数量
|
|
697
|
+
const valueArgs = node.namedChildren.find((c) => c.type === 'call_suffix' || c.type === 'value_arguments');
|
|
698
|
+
const argCount = valueArgs ? valueArgs.namedChildCount : 0;
|
|
699
|
+
|
|
700
|
+
ctx.callSites.push({
|
|
701
|
+
callee,
|
|
702
|
+
callerMethod: methodName,
|
|
703
|
+
callerClass: className,
|
|
704
|
+
callType,
|
|
705
|
+
receiver,
|
|
706
|
+
receiverType,
|
|
707
|
+
argCount,
|
|
708
|
+
line: node.startPosition.row + 1,
|
|
709
|
+
isAwait: false,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// walk arguments for nested calls
|
|
713
|
+
if (valueArgs) walkChildren(valueArgs);
|
|
714
|
+
// also check trailing lambda
|
|
715
|
+
const lambda = node.namedChildren.find((c) => c.type === 'annotated_lambda' || c.type === 'lambda_literal');
|
|
716
|
+
if (lambda) walkChildren(lambda);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
walkChildren(node);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function walkChildren(node) {
|
|
724
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
725
|
+
walk(node.namedChild(i));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
walk(bodyNode);
|
|
730
|
+
}
|
|
731
|
+
|
|
404
732
|
// ── 插件导出 ──
|
|
405
733
|
|
|
406
734
|
let _grammar = null;
|
|
@@ -415,5 +743,6 @@ export const plugin = {
|
|
|
415
743
|
getGrammar,
|
|
416
744
|
walk: walkKotlin,
|
|
417
745
|
detectPatterns: detectKtPatterns,
|
|
746
|
+
extractCallSites: extractCallSitesKotlin,
|
|
418
747
|
extensions: ['.kt', '.kts'],
|
|
419
748
|
};
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 提取: class, function, import, decorator, docstring, module-level assignments
|
|
6
6
|
* 模式: Singleton, Factory, Context Manager, Decorator pattern, Data Class
|
|
7
|
+
*
|
|
8
|
+
* Phase 5: 新增 ImportRecord 结构化导入 + extractCallSites 调用点提取
|
|
7
9
|
*/
|
|
8
10
|
|
|
11
|
+
import { ImportRecord } from '../analysis/ImportRecord.js';
|
|
12
|
+
import { extractCallSitesPython } from '../analysis/CallSiteExtractor.js';
|
|
13
|
+
|
|
9
14
|
function walkPython(root, ctx) {
|
|
10
15
|
_walkPyNode(root, ctx, null);
|
|
11
16
|
}
|
|
@@ -18,7 +23,14 @@ function _walkPyNode(node, ctx, parentClassName) {
|
|
|
18
23
|
case 'import_statement': {
|
|
19
24
|
const modNode = child.namedChildren.find((c) => c.type === 'dotted_name');
|
|
20
25
|
if (modNode) {
|
|
21
|
-
|
|
26
|
+
// import mod / import mod as alias
|
|
27
|
+
const aliasNode = child.namedChildren.find((c) => c.type === 'aliased_import');
|
|
28
|
+
const alias = aliasNode?.namedChildren?.find((c) => c.type === 'identifier')?.text || null;
|
|
29
|
+
ctx.imports.push(new ImportRecord(modNode.text, {
|
|
30
|
+
symbols: ['*'],
|
|
31
|
+
alias: alias || modNode.text.split('.').pop(),
|
|
32
|
+
kind: 'namespace',
|
|
33
|
+
}));
|
|
22
34
|
}
|
|
23
35
|
break;
|
|
24
36
|
}
|
|
@@ -28,7 +40,23 @@ function _walkPyNode(node, ctx, parentClassName) {
|
|
|
28
40
|
(c) => c.type === 'dotted_name' || c.type === 'relative_import'
|
|
29
41
|
);
|
|
30
42
|
if (modNode) {
|
|
31
|
-
|
|
43
|
+
const importPath = modNode.text;
|
|
44
|
+
// from mod import A, B, C
|
|
45
|
+
const importedNames = [];
|
|
46
|
+
for (const c of child.namedChildren) {
|
|
47
|
+
if (c.type === 'dotted_name' && c !== modNode) {
|
|
48
|
+
importedNames.push(c.text);
|
|
49
|
+
} else if (c.type === 'aliased_import') {
|
|
50
|
+
const nameNode = c.namedChildren.find((n) => n.type === 'dotted_name' || n.type === 'identifier');
|
|
51
|
+
if (nameNode) importedNames.push(nameNode.text);
|
|
52
|
+
} else if (c.type === 'wildcard_import') {
|
|
53
|
+
importedNames.push('*');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
ctx.imports.push(new ImportRecord(importPath, {
|
|
57
|
+
symbols: importedNames.length > 0 ? importedNames : [],
|
|
58
|
+
kind: importedNames.includes('*') ? 'namespace' : 'named',
|
|
59
|
+
}));
|
|
32
60
|
}
|
|
33
61
|
break;
|
|
34
62
|
}
|
|
@@ -363,5 +391,6 @@ export const plugin = {
|
|
|
363
391
|
getGrammar,
|
|
364
392
|
walk: walkPython,
|
|
365
393
|
detectPatterns: detectPyPatterns,
|
|
394
|
+
extractCallSites: extractCallSitesPython,
|
|
366
395
|
extensions: ['.py'],
|
|
367
396
|
};
|