autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
692 lines (691 loc) • 26.1 kB
JavaScript
/**
* @module lang-kotlin
* @description Kotlin AST Walker 插件
*
* 提取: class, interface, object, enum, sealed class, function, property, import, annotation
* 模式: Singleton (object), Factory (companion), DSL, Flow, Sealed
*/
import { ImportRecord } from '../analysis/ImportRecord.js';
function walkKotlin(root, ctx) {
_walkKtNode(root, ctx, null);
}
function _walkKtNode(node, ctx, parentClassName) {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
switch (child.type) {
case 'import_header':
case 'import_directive': {
const id = child.namedChildren.find((c) => c.type === 'identifier' || c.type === 'user_type');
if (id) {
const fullPath = id.text; // e.g. com.example.MyClass or com.example.myFunc
const segments = fullPath.split('.');
const lastName = segments[segments.length - 1];
// Kotlin: import com.example.* (wildcard)
const isWildcard = child.text.includes('.*');
// Kotlin: import com.example.MyClass as Alias
const aliasNode = child.namedChildren.find((c) => c.type === 'import_alias');
const alias = aliasNode?.namedChildren?.find((c) => c.type === 'type_identifier' || c.type === 'simple_identifier')?.text;
if (isWildcard) {
ctx.imports.push(new ImportRecord(fullPath, { symbols: ['*'], kind: 'namespace' }));
}
else {
ctx.imports.push(new ImportRecord(fullPath, {
symbols: [lastName],
alias: alias || lastName,
kind: 'named',
}));
}
}
break;
}
case 'class_declaration': {
const classInfo = _parseKtClass(child);
ctx.classes.push(classInfo);
// Phase 5.3: Extract primary constructor parameter properties (Kotlin DI pattern)
// class UserService(private val repo: UserRepo) → property { name: 'repo', typeAnnotation: 'UserRepo' }
_extractKtConstructorProperties(child, ctx, classInfo.name);
const body = child.namedChildren.find((c) => c.type === 'class_body');
if (body) {
_walkKtClassBody(body, ctx, classInfo.name);
}
break;
}
case 'object_declaration': {
const name = child.namedChildren.find((c) => c.type === 'type_identifier' || c.type === 'simple_identifier')?.text || 'Unknown';
ctx.classes.push({
name,
kind: 'object',
line: child.startPosition.row + 1,
endLine: child.endPosition.row + 1,
});
const body = child.namedChildren.find((c) => c.type === 'class_body');
if (body) {
_walkKtClassBody(body, ctx, name);
}
break;
}
case 'function_declaration': {
const func = _parseKtFunction(child, parentClassName);
ctx.methods.push(func);
break;
}
case 'property_declaration': {
const prop = _parseKtProperty(child, parentClassName);
if (prop) {
ctx.properties.push(prop);
}
break;
}
default: {
if (child.namedChildCount > 0 &&
!['function_body', 'lambda_literal', 'string_literal'].includes(child.type)) {
_walkKtNode(child, ctx, parentClassName);
}
}
}
}
}
function _walkKtClassBody(body, ctx, className) {
for (let i = 0; i < body.namedChildCount; i++) {
const child = body.namedChild(i);
switch (child.type) {
case 'function_declaration': {
ctx.methods.push(_parseKtFunction(child, className));
break;
}
case 'property_declaration': {
const prop = _parseKtProperty(child, className);
if (prop) {
ctx.properties.push(prop);
}
break;
}
case 'companion_object': {
const companionBody = child.namedChildren.find((c) => c.type === 'class_body');
if (companionBody) {
_walkKtClassBody(companionBody, ctx, className);
}
break;
}
case 'class_declaration': {
const inner = _parseKtClass(child);
inner.outerClass = className;
ctx.classes.push(inner);
// Phase 5.3: Extract primary constructor params for inner classes too
_extractKtConstructorProperties(child, ctx, inner.name);
const innerBody = child.namedChildren.find((c) => c.type === 'class_body');
if (innerBody) {
_walkKtClassBody(innerBody, ctx, inner.name);
}
break;
}
case 'object_declaration': {
const name = child.namedChildren.find((c) => c.type === 'type_identifier' || c.type === 'simple_identifier')?.text || 'Unknown';
ctx.classes.push({
name,
kind: 'object',
outerClass: className,
line: child.startPosition.row + 1,
});
const objBody = child.namedChildren.find((c) => c.type === 'class_body');
if (objBody) {
_walkKtClassBody(objBody, ctx, name);
}
break;
}
}
}
}
function _parseKtClass(node) {
const name = node.namedChildren.find((c) => c.type === 'type_identifier' || c.type === 'simple_identifier')?.text || 'Unknown';
// 检查修饰符: data, sealed, abstract, open, enum
const modifiers = [];
for (const child of node.namedChildren) {
if (child.type === 'modifiers' || child.type === 'modifier') {
modifiers.push(child.text);
}
}
let kind = 'class';
if (modifiers.some((m) => /\bdata\b/.test(m))) {
kind = 'data-class';
}
if (modifiers.some((m) => /\bsealed\b/.test(m))) {
kind = 'sealed-class';
}
if (modifiers.some((m) => /\benum\b/.test(m))) {
kind = 'enum';
}
if (modifiers.some((m) => /\babstract\b/.test(m))) {
kind = 'abstract-class';
}
// 继承
const _superclass = null;
const protocols = [];
for (const child of node.namedChildren) {
if (child.type === 'delegation_specifier' || child.type === 'delegation_specifiers') {
// 简化处理
const typeRefs = _collectTypeRefs(child);
protocols.push(...typeRefs);
}
}
let detectedSuper = null;
if (protocols.length > 0) {
detectedSuper = protocols[0];
}
// 注解
const annotations = node.namedChildren
.filter((c) => c.type === 'annotation')
.map((a) => a.text);
return {
name,
kind,
superclass: detectedSuper,
protocols,
annotations,
modifiers,
line: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
};
}
function _parseKtFunction(node, className) {
const name = node.namedChildren.find((c) => c.type === 'simple_identifier')?.text || 'unknown';
const body = node.namedChildren.find((c) => c.type === 'function_body' || c.type === 'block');
const bodyLines = body ? body.endPosition.row - body.startPosition.row + 1 : 0;
const complexity = body ? _estimateComplexity(body) : 1;
const nestingDepth = body ? _maxNesting(body, 0) : 0;
const modifiers = [];
for (const child of node.namedChildren) {
if (child.type === 'modifiers' || child.type === 'modifier') {
modifiers.push(child.text);
}
}
const isSuspend = modifiers.some((m) => /\bsuspend\b/.test(m));
const isOverride = modifiers.some((m) => /\boverride\b/.test(m));
// 检测扩展函数: fun Type.name()
const receiverType = node.namedChildren.find((c) => c.type === 'user_type');
const isExtension = !!receiverType &&
receiverType.startPosition.column <
(node.namedChildren.find((c) => c.type === 'simple_identifier')?.startPosition?.column ||
999);
return {
name,
className,
isClassMethod: false,
isSuspend,
isOverride,
isExtension,
bodyLines,
complexity,
nestingDepth,
line: node.startPosition.row + 1,
kind: 'definition',
};
}
function _parseKtProperty(node, className) {
const name = node.namedChildren.find((c) => c.type === 'simple_identifier' || c.type === 'variable_declaration')?.text || null;
if (!name) {
return null;
}
const isVal = node.text.trimStart().startsWith('val') || node.text.includes(' val ');
const isVar = node.text.trimStart().startsWith('var') || node.text.includes(' var ');
const isLazy = node.text.includes('by lazy');
const isLateinit = node.text.includes('lateinit');
// Phase 5.3: Extract property type for DI resolution
// Kotlin property types can be in variable_declaration or directly on the node:
// val userRepo: UserRepo → variable_declaration { simple_identifier, user_type }
// lateinit var service: UserService → user_type directly on property_declaration
let typeAnnotation = null;
// 1. Try from variable_declaration child
const varDecl = node.namedChildren.find((c) => c.type === 'variable_declaration');
if (varDecl) {
const typeNode = varDecl.namedChildren.find((c) => c.type === 'user_type' || c.type === 'nullable_type');
if (typeNode) {
typeAnnotation = _extractKtTypeName(typeNode);
}
}
// 2. Fallback: try direct children (lateinit var patterns)
if (!typeAnnotation) {
const typeNode = node.namedChildren.find((c) => c.type === 'user_type' || c.type === 'nullable_type');
if (typeNode) {
typeAnnotation = _extractKtTypeName(typeNode);
}
}
// Phase 5.3: Extract annotations on property (e.g. @Inject, @Autowired)
const annotations = [];
for (const child of node.namedChildren) {
if (child.type === 'annotation' || child.type === 'single_annotation') {
annotations.push(child.text);
}
}
// Also check modifiers block
const modifiers = node.namedChildren.find((c) => c.type === 'modifiers');
if (modifiers) {
for (const child of modifiers.namedChildren) {
if (child.type === 'annotation' || child.type === 'single_annotation') {
annotations.push(child.text);
}
}
}
return {
name,
className,
isConstant: isVal,
isMutable: isVar,
isLazy,
isLateinit,
typeAnnotation,
annotations: annotations.length > 0 ? annotations : undefined,
line: node.startPosition.row + 1,
};
}
/**
* Phase 5.3: Extract Kotlin primary constructor parameter properties
*
* Kotlin constructors: class Svc(private val repo: UserRepo, val logger: Logger)
* Each val/var parameter in a primary_constructor becomes a class property.
*
* AST: class_declaration → primary_constructor → class_parameter[]
* Each class_parameter may contain: modifiers, val/var keyword, simple_identifier, user_type
*
* @param classNode class_declaration AST node
* @param ctx walker context
*/
function _extractKtConstructorProperties(classNode, ctx, className) {
const primaryCtor = classNode.namedChildren.find((c) => c.type === 'primary_constructor');
if (!primaryCtor) {
return;
}
for (const param of primaryCtor.namedChildren) {
if (param.type !== 'class_parameter') {
continue;
}
// Only val/var params become properties
const text = param.text;
const isVal = text.includes('val ');
const isVar = text.includes('var ');
if (!isVal && !isVar) {
continue;
}
const name = param.namedChildren.find((c) => c.type === 'simple_identifier')?.text;
if (!name) {
continue;
}
// Extract type annotation
let typeAnnotation = null;
const typeNode = param.namedChildren.find((c) => c.type === 'user_type' || c.type === 'nullable_type');
if (typeNode) {
typeAnnotation = _extractKtTypeName(typeNode);
}
// Extract annotations (e.g. @Inject)
const annotations = [];
for (const child of param.namedChildren) {
if (child.type === 'annotation' ||
child.type === 'single_annotation' ||
child.type === 'modifiers') {
if (child.type === 'modifiers') {
for (const mod of child.namedChildren) {
if (mod.type === 'annotation' || mod.type === 'single_annotation') {
annotations.push(mod.text);
}
}
}
else {
annotations.push(child.text);
}
}
}
ctx.properties.push({
name,
className,
isConstant: isVal,
isMutable: isVar,
isLazy: false,
isLateinit: false,
isConstructorParam: true,
typeAnnotation,
annotations: annotations.length > 0 ? annotations : undefined,
line: param.startPosition.row + 1,
});
}
}
/**
* Phase 5.3: Extract Kotlin type name from user_type or nullable_type node
* Strips generics and nullable marker
*/
function _extractKtTypeName(typeNode) {
if (typeNode.type === 'nullable_type') {
const inner = typeNode.namedChildren.find((c) => c.type === 'user_type');
if (inner) {
const text = inner.text;
const bracketIdx = text.indexOf('<');
return bracketIdx > 0 ? text.slice(0, bracketIdx) : text;
}
return null;
}
// user_type
const text = typeNode.text;
const bracketIdx = text.indexOf('<');
return bracketIdx > 0 ? text.slice(0, bracketIdx) : text;
}
function _collectTypeRefs(node) {
const refs = [];
function walk(n) {
if (n.type === 'user_type' || n.type === 'type_identifier' || n.type === 'simple_identifier') {
refs.push(n.text);
return;
}
for (let i = 0; i < n.namedChildCount; i++) {
walk(n.namedChild(i));
}
}
walk(node);
return refs;
}
// ── Kotlin 模式检测 ──
function detectKtPatterns(root, lang, methods, properties, classes) {
const patterns = [];
// Singleton: object declaration
for (const cls of classes) {
if (cls.kind === 'object') {
patterns.push({ type: 'singleton', className: cls.name, line: cls.line, confidence: 0.95 });
}
}
// Sealed class
for (const cls of classes) {
if (cls.kind === 'sealed-class') {
patterns.push({
type: 'sealed-class',
className: cls.name,
line: cls.line,
confidence: 0.95,
});
}
}
// Data class
for (const cls of classes) {
if (cls.kind === 'data-class') {
patterns.push({ type: 'data-class', className: cls.name, line: cls.line, confidence: 0.95 });
}
}
// Factory: companion object 中的 create/of/from
for (const m of methods) {
if (/^create$|^of$|^from$|^newInstance$/.test(m.name)) {
patterns.push({
type: 'factory',
className: m.className,
methodName: m.name,
line: m.line,
confidence: 0.8,
});
}
}
// Extension function
for (const m of methods) {
if (m.isExtension) {
patterns.push({
type: 'extension-function',
className: m.className,
methodName: m.name,
line: m.line,
confidence: 0.9,
});
}
}
// Android patterns
for (const cls of classes) {
if (cls.annotations?.some((a) => /@Composable/.test(a))) {
patterns.push({ type: 'composable', className: cls.name, line: cls.line, confidence: 0.95 });
}
if (cls.annotations?.some((a) => /@HiltAndroidApp|@AndroidEntryPoint/.test(a))) {
patterns.push({ type: 'hilt-di', className: cls.name, line: cls.line, confidence: 0.95 });
}
if (cls.superclass && /ViewModel$/.test(cls.superclass)) {
patterns.push({ type: 'viewmodel', className: cls.name, line: cls.line, confidence: 0.9 });
}
}
// Composable functions
for (const m of methods) {
if (m.name && /^[A-Z]/.test(m.name) && !m.className) {
// Possible Composable function (PascalCase top-level fun)
// Would need annotation check for higher confidence
}
}
return patterns;
}
// ── 工具函数 ──
function _estimateComplexity(node) {
let complexity = 1;
const BRANCH_TYPES = new Set([
'if_expression',
'for_statement',
'while_statement',
'when_expression',
'when_entry',
'catch_block',
'try_expression',
]);
function walk(n) {
if (BRANCH_TYPES.has(n.type)) {
complexity++;
}
if (n.type === 'conjunction_expression' || n.type === 'disjunction_expression') {
complexity++;
}
for (let i = 0; i < n.namedChildCount; i++) {
walk(n.namedChild(i));
}
}
walk(node);
return complexity;
}
function _maxNesting(node, depth) {
const NESTING_TYPES = new Set([
'if_expression',
'for_statement',
'while_statement',
'when_expression',
'try_expression',
'lambda_literal',
]);
let max = depth;
const nextDepth = NESTING_TYPES.has(node.type) ? depth + 1 : depth;
for (let i = 0; i < node.namedChildCount; i++) {
const childMax = _maxNesting(node.namedChild(i), nextDepth);
if (childMax > max) {
max = childMax;
}
}
return max;
}
// ── Kotlin Call Site 提取 (Phase 5) ──────────────────────────
/** 从 Kotlin AST root 提取所有调用点 */
function extractCallSitesKotlin(root, ctx, _lang) {
const scopes = _collectKtScopes(root);
for (const scope of scopes) {
_extractKtCallSitesFromBody(scope.body, scope.className, scope.methodName, ctx);
}
}
/** 递归收集 Kotlin 中所有函数体作用域 */
function _collectKtScopes(root) {
const scopes = [];
function visit(node, className) {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child.type === 'class_declaration' || child.type === 'object_declaration') {
const name = child.namedChildren.find((c) => c.type === 'type_identifier' || c.type === 'simple_identifier')?.text;
const body = child.namedChildren.find((c) => c.type === 'class_body');
if (body) {
visit(body, name || className);
}
}
else if (child.type === 'companion_object') {
const body = child.namedChildren.find((c) => c.type === 'class_body');
if (body) {
visit(body, className);
}
}
else if (child.type === 'function_declaration') {
const name = child.namedChildren.find((c) => c.type === 'simple_identifier')?.text || 'unknown';
const body = child.namedChildren.find((c) => c.type === 'function_body' || c.type === 'block');
if (body) {
scopes.push({ body, className, methodName: name });
}
}
else if (child.type === 'property_declaration') {
// property with getter/setter or initializer with lambda
const getter = child.namedChildren.find((c) => c.type === 'getter');
const setter = child.namedChildren.find((c) => c.type === 'setter');
const propName = child.namedChildren.find((c) => c.type === 'simple_identifier' || c.type === 'variable_declaration')?.text;
if (getter) {
const body = getter.namedChildren.find((c) => c.type === 'function_body' || c.type === 'block');
if (body) {
scopes.push({ body, className, methodName: `get_${propName || 'prop'}` });
}
}
if (setter) {
const body = setter.namedChildren.find((c) => c.type === 'function_body' || c.type === 'block');
if (body) {
scopes.push({ body, className, methodName: `set_${propName || 'prop'}` });
}
}
}
}
}
visit(root, null);
return scopes;
}
/** 从 Kotlin function body 中递归提取调用点 */
function _extractKtCallSitesFromBody(bodyNode, className, methodName, ctx) {
if (!bodyNode) {
return;
}
const KT_NOISE = new Set([
'println',
'print',
'require',
'check',
'error',
'TODO',
'listOf',
'mapOf',
'setOf',
'mutableListOf',
'mutableMapOf',
'mutableSetOf',
'arrayOf',
'intArrayOf',
'emptyList',
'emptyMap',
'emptySet',
'lazy',
'repeat',
'run',
'let',
'also',
'apply',
'with',
]);
function walk(node) {
if (!node || node.type === 'ERROR' || node.isMissing) {
return;
}
if (node.type === 'call_expression') {
const func = node.namedChildren[0];
if (!func) {
walkChildren(node);
return;
}
let callee, receiver = null, receiverType = null, callType;
if (func.type === 'navigation_expression') {
// obj.method() or Pkg.func()
const parts = func.text.split('.');
if (parts.length >= 2) {
receiver = parts.slice(0, -1).join('.');
callee = parts[parts.length - 1];
if (receiver === 'this' || receiver === 'self') {
receiverType = className;
callType = 'method';
}
else if (receiver === 'super') {
receiverType = className;
callType = 'super';
}
else if (/^[A-Z]/.test(receiver)) {
receiverType = receiver;
callType = 'static';
}
else {
callType = 'method';
receiverType = null;
}
}
else {
callee = func.text;
callType = 'function';
}
}
else if (func.type === 'simple_identifier') {
callee = func.text;
if (KT_NOISE.has(callee)) {
walkChildren(node);
return;
}
// PascalCase → constructor
callType = /^[A-Z]/.test(callee) ? 'constructor' : 'function';
if (callType === 'constructor') {
receiverType = callee;
}
}
else {
callee = func.text?.slice(0, 80) || 'unknown';
callType = 'function';
}
// 计算参数数量
const valueArgs = node.namedChildren.find((c) => c.type === 'call_suffix' || c.type === 'value_arguments');
const argCount = valueArgs ? valueArgs.namedChildCount : 0;
ctx.callSites.push({
callee,
callerMethod: methodName,
callerClass: className,
callType,
receiver,
receiverType,
argCount,
line: node.startPosition.row + 1,
isAwait: false,
});
// walk arguments for nested calls
if (valueArgs) {
walkChildren(valueArgs);
}
// also check trailing lambda
const lambda = node.namedChildren.find((c) => c.type === 'annotated_lambda' || c.type === 'lambda_literal');
if (lambda) {
walkChildren(lambda);
}
return;
}
walkChildren(node);
}
function walkChildren(node) {
for (let i = 0; i < node.namedChildCount; i++) {
walk(node.namedChild(i));
}
}
walk(bodyNode);
}
// ── 插件导出 ──
let _grammar = null;
function getGrammar() {
return _grammar;
}
export function setGrammar(grammar) {
_grammar = grammar;
}
export const plugin = {
getGrammar,
walk: walkKotlin,
detectPatterns: detectKtPatterns,
extractCallSites: extractCallSitesKotlin,
extensions: ['.kt', '.kts'],
};