diff --git a/chapi-ast-java/src/main/antlr/JavaLexer.g4 b/chapi-ast-java/src/main/antlr/JavaLexer.g4 index 48667994..4e8ad2f5 100644 --- a/chapi-ast-java/src/main/antlr/JavaLexer.g4 +++ b/chapi-ast-java/src/main/antlr/JavaLexer.g4 @@ -141,6 +141,9 @@ SEMI : ';'; COMMA : ','; DOT : '.'; +// Java 22+ (preview, continued into later releases): unnamed variables / patterns +UNDERSCORE : '_'; + // Operators ASSIGN : '='; diff --git a/chapi-ast-java/src/main/antlr/JavaParser.g4 b/chapi-ast-java/src/main/antlr/JavaParser.g4 index 7801246f..75c7074b 100644 --- a/chapi-ast-java/src/main/antlr/JavaParser.g4 +++ b/chapi-ast-java/src/main/antlr/JavaParser.g4 @@ -255,7 +255,13 @@ variableDeclarator ; variableDeclaratorId - : identifier ('[' ']')* + : (identifier | UNDERSCORE) ('[' ']')* + ; + +// Java 22+ (preview, continued into later releases): unnamed variables / patterns +variableName + : identifier + | UNDERSCORE ; variableInitializer @@ -310,7 +316,7 @@ lambdaLVTIList ; lambdaLVTIParameter - : variableModifier* VAR identifier + : variableModifier* VAR (identifier | UNDERSCORE) ; qualifiedName @@ -474,7 +480,7 @@ blockStatement ; localVariableDeclaration - : variableModifier* (VAR identifier '=' expression | typeType variableDeclarators) + : variableModifier* (VAR (identifier | UNDERSCORE) '=' expression | typeType variableDeclarators) ; identifier @@ -539,7 +545,7 @@ statement ; catchClause - : CATCH '(' variableModifier* catchType identifier ')' block + : CATCH '(' variableModifier* catchType (identifier | UNDERSCORE) ')' block ; catchType @@ -559,7 +565,7 @@ resources ; resource - : variableModifier* (classOrInterfaceType variableDeclaratorId | VAR identifier) '=' expression + : variableModifier* (classOrInterfaceType variableDeclaratorId | VAR (identifier | UNDERSCORE)) '=' expression | qualifiedName ; @@ -574,7 +580,7 @@ switchLabel : CASE ( constantExpression = expression | enumConstantName = IDENTIFIER - | typeType varName = identifier + | typeType varName = variableName ) | DEFAULT ; @@ -697,8 +703,9 @@ lambdaExpression lambdaParameters : identifier + | UNDERSCORE | '(' formalParameterList? ')' - | '(' identifier (',' identifier)* ')' + | '(' (identifier | UNDERSCORE) (',' (identifier | UNDERSCORE))* ')' | '(' lambdaLVTIList? ')' ; diff --git a/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaAnalyser.kt b/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaAnalyser.kt index 2384c68d..f1c3846c 100644 --- a/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaAnalyser.kt +++ b/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaAnalyser.kt @@ -11,6 +11,11 @@ import org.antlr.v4.runtime.CommonTokenStream import org.antlr.v4.runtime.tree.ParseTreeWalker open class JavaAnalyser : TwoStepAnalyser() { + companion object { + // Reuse ParseTreeWalker instance to reduce object creation + private val walker = ParseTreeWalker() + } + override fun analysis(code: String, filePath: String, parseMode: ParseMode): CodeContainer { return when (parseMode) { ParseMode.Basic -> identBasicInfo(code, filePath) @@ -27,7 +32,7 @@ open class JavaAnalyser : TwoStepAnalyser() { val context = this.parse(str).compilationUnit() val listener = JavaFullIdentListener(fileName, classes) - ParseTreeWalker().walk(listener, context) + walker.walk(listener, context) return listener.getNodeInfo() } @@ -35,7 +40,7 @@ open class JavaAnalyser : TwoStepAnalyser() { val context = this.parse(str).compilationUnit() val listener = JavaBasicIdentListener(fileName) - ParseTreeWalker().walk(listener, context) + walker.walk(listener, context) return listener.getNodeInfo() } @@ -43,8 +48,13 @@ open class JavaAnalyser : TwoStepAnalyser() { open fun parse(str: String): JavaParser { val fromString = CharStreams.fromString(str) val lexer = JavaLexer(fromString) + // Remove default error listeners to reduce I/O overhead + lexer.removeErrorListeners() + val tokenStream = CommonTokenStream(lexer) val parser = JavaParser(tokenStream) + // Remove default error listeners + parser.removeErrorListeners() return parser } } diff --git a/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaBasicIdentListener.kt b/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaBasicIdentListener.kt index a9af86a9..cd91ad75 100644 --- a/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaBasicIdentListener.kt +++ b/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaBasicIdentListener.kt @@ -12,8 +12,11 @@ open class JavaBasicIdentListener(fileName: String) : JavaAstListener() { Language = "java", Kind = ContainerKind.SOURCE_FILE ) - private var classNodes: List = listOf() - private var imports: List = listOf() + private var classNodes: MutableList = mutableListOf() + private var imports: MutableList = mutableListOf() + // Index for fast import lookup by class name + // Using MutableList to handle import collisions + private var importsByClassName: MutableMap> = mutableMapOf() private var currentNode = CodeDataStruct() private var currentFunction = CodeFunction(IsConstructor = false) @@ -58,8 +61,14 @@ open class JavaBasicIdentListener(fileName: String) : JavaAstListener() { codeImport.PathSegments = fullSource.split(".") - imports += codeImport + imports.add(codeImport) codeContainer.Imports += codeImport + + // Build import index for non-wildcard imports only + if (!isWildcard) { + val className = fullSource.substringAfterLast('.') + importsByClassName.getOrPut(className) { mutableListOf() }.add(codeImport) + } } override fun enterPackageDeclaration(ctx: JavaParser.PackageDeclarationContext?) { @@ -83,9 +92,14 @@ open class JavaBasicIdentListener(fileName: String) : JavaAstListener() { } override fun exitClassBodyDeclaration(ctx: JavaParser.ClassBodyDeclarationContext?) { + // Class members are handled in their own enter/exit callbacks. + // Finalizing the class here would duplicate nodes (one per member) and miss empty classes. + } + + override fun exitClassDeclaration(ctx: JavaParser.ClassDeclarationContext?) { hasEnterClass = false if (currentNode.NodeName != "") { - classNodes += currentNode + classNodes.add(currentNode) } currentNode = CodeDataStruct() } @@ -93,17 +107,24 @@ open class JavaBasicIdentListener(fileName: String) : JavaAstListener() { open fun buildImplements(ctx: JavaParser.ClassDeclarationContext): List { return ctx.typeList() .map { it.text } - .filter { imports.containsType(it) } + .filter { containsType(it) } } - private fun List.containsType(it: String?): Boolean { - return imports.filter { imp -> imp.Source.endsWith(".$it") }.toTypedArray().isNotEmpty() + private fun containsType(typeName: String?): Boolean { + if (typeName == null) return false + // Fast lookup using index + val importsByName = importsByClassName[typeName] + if (importsByName != null && importsByName.isNotEmpty()) { + return true + } + // Fallback to linear search for wildcard imports + return imports.any { imp -> imp.Source.endsWith(".$typeName") } } open fun buildImplements(ctx: JavaParser.EnumDeclarationContext): List { val typeText = ctx.typeList().text return when { - imports.containsType(typeText) -> listOf(typeText) + containsType(typeText) -> listOf(typeText) else -> listOf() } } @@ -120,7 +141,7 @@ open class JavaBasicIdentListener(fileName: String) : JavaAstListener() { override fun exitInterfaceDeclaration(ctx: JavaParser.InterfaceDeclarationContext?) { hasEnterClass = false if (currentNode.NodeName != "") { - classNodes += currentNode + classNodes.add(currentNode) } } @@ -236,14 +257,13 @@ open class JavaBasicIdentListener(fileName: String) : JavaAstListener() { override fun exitEnumBodyDeclarations(ctx: JavaParser.EnumBodyDeclarationsContext?) { hasEnterClass = false if (currentNode.NodeName != "") { - classNodes += currentNode + classNodes.add(currentNode) } currentNode = CodeDataStruct() } fun getNodeInfo(): CodeContainer { - codeContainer.DataStructures = classNodes + codeContainer.DataStructures = classNodes.toList() return codeContainer } -} - +} \ No newline at end of file diff --git a/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaFullIdentListener.kt b/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaFullIdentListener.kt index 243e4914..3431ad07 100644 --- a/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaFullIdentListener.kt +++ b/chapi-ast-java/src/main/kotlin/chapi/ast/javaast/JavaFullIdentListener.kt @@ -11,12 +11,12 @@ data class JavaTargetType(var targetType: String = "", var callType: CallType = open class JavaFullIdentListener(fileName: String, val classes: List) : JavaAstListener() { private var isEnterFunction: Boolean = false - private var currentAnnotations: List = listOf() + private var currentAnnotations: MutableList = mutableListOf() private var currentCreatorNode: CodeDataStruct = CodeDataStruct() private var isOverrideMethod: Boolean = false - private var fields = listOf() - private var methodCalls = listOf() + private var fields: MutableList = mutableListOf() + private var methodCalls: MutableList = mutableListOf() private var methodMap = mutableMapOf() private var creatorMethodMap = mutableMapOf() private var localVars = mutableMapOf() @@ -27,14 +27,18 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : private var currentClzExtend: String = "" private var hasEnterClass: Boolean = false - private var classNodes: List = listOf() + private var classNodes: MutableList = mutableListOf() private var innerNode: CodeDataStruct = CodeDataStruct() private var classNodeStack = Stack() - private var methodQueue: List = listOf() + private var methodQueue: MutableList = mutableListOf() - private var imports: List = listOf() + private var imports: MutableList = mutableListOf() + // Index for fast import lookup by class name (O(1) instead of O(n)) + // Using MutableList to handle import collisions (same class name from different packages) + private var importsByClassName: MutableMap> = mutableMapOf() + private var importsByFullSource: MutableMap = mutableMapOf() private var lastNode = CodeDataStruct() private var currentNode = CodeDataStruct() @@ -94,8 +98,25 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : codeImport.PathSegments = fullSource.split(".") - imports += codeImport + imports.add(codeImport) codeContainer.Imports += codeImport + + // Build import indexes for fast lookup + importsByFullSource[fullSource] = codeImport + + if (isStatic) { + // For static imports, index by the declaring class name (from Source), + // not by the static member name + val sourceClassName = codeImport.Source.substringAfterLast('.') + importsByClassName.getOrPut(sourceClassName) { mutableListOf() }.add(codeImport) + // Also index by source for static imports (fully-qualified class name) + importsByFullSource[codeImport.Source] = codeImport + } else if (!isWildcard) { + // For non-static, non-wildcard imports, index by the imported class name + // Skip wildcard imports as they cannot be efficiently indexed + val className = fullSource.substringAfterLast('.') + importsByClassName.getOrPut(className) { mutableListOf() }.add(codeImport) + } } override fun enterClassDeclaration(ctx: JavaParser.ClassDeclarationContext?) { @@ -173,10 +194,10 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : } private fun exitBody() { - currentNode.Fields = fields + currentNode.Fields = fields.toList() currentNode.setMethodsFromMap(methodMap) - classNodes += currentNode + classNodes.add(currentNode) initClass() } @@ -186,8 +207,8 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : currentFunction = CodeFunction(IsConstructor = false) methodMap = mutableMapOf() - methodCalls = listOf() - fields = listOf() + methodCalls = mutableListOf() + fields = mutableListOf() isOverrideMethod = false } @@ -248,8 +269,9 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : } private fun buildMethodParameters(params: JavaParser.FormalParametersContext?): List { - var methodParams = listOf() - if (params == null) return methodParams + if (params == null) return emptyList() + + val methodParams = mutableListOf() // New grammar structure: formalParameters can contain formalParameter directly // and/or formalParameterList @@ -269,7 +291,7 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : TypeType = paramType, TypeRef = JavaTypeRefBuilder.build(formalParam.typeType()) ) - methodParams += parameter + methodParams.add(parameter) } } @@ -287,7 +309,7 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : TypeType = paramType, TypeRef = JavaTypeRefBuilder.build(param.typeType()) ) - methodParams += parameter + methodParams.add(parameter) } } } @@ -297,7 +319,7 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : override fun exitMethodDeclaration(ctx: JavaParser.MethodDeclarationContext?) { this.isEnterFunction = false - this.currentAnnotations = listOf() + this.currentAnnotations = mutableListOf() if (localVars.isNotEmpty()) { addLocalVarsToFunction() } @@ -352,7 +374,7 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : } private fun sendResultToMethodCallMap(codeCall: CodeCall) { - methodCalls += codeCall + methodCalls.add(codeCall) val currentMethodName = getMethodMapName(currentFunction) val method = methodMap[currentMethodName] @@ -486,7 +508,7 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : creatorMethodMap[getMethodMapName(codeFunction)] = codeFunction } else { currentFunction = codeFunction - methodQueue += currentFunction + methodQueue.add(currentFunction) methodMap[getMethodMapName(codeFunction)] = codeFunction } } @@ -497,7 +519,7 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : name = methodQueue[methodQueue.size - 1].Name } - return codeContainer.PackageName + "." + currentClz + "." + name + ":" + method.Position.StartLine.toString() + return "${codeContainer.PackageName}.${currentClz}.${name}:${method.Position.StartLine}" } private fun buildExtend(extendName: String): String { @@ -514,26 +536,24 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : val callType: CallType = CallType.FUNCTION if (currentClz == targetType) { return JavaTargetType( - targetType = codeContainer.PackageName + "." + targetType, + targetType = "${codeContainer.PackageName}.${targetType}", callType = CallType.SELF ) } - // second, parse from import + // second, parse from import using index (O(1) lookup) val pureTargetType = buildPureTargetType(targetType) - if (pureTargetType != "") { - for (imp in imports) { - if (imp.Source.endsWith(pureTargetType)) { - return JavaTargetType( - targetType = imp.Source, - callType = CallType.CHAIN - ) - } else if (imp.UsageName.isNotEmpty() && imp.UsageName.contains(pureTargetType)) { - return JavaTargetType( - targetType = imp.Source, - callType = CallType.STATIC - ) - } + if (pureTargetType.isNotEmpty()) { + // Fast lookup using index - check if there are any imports for this class name + val importsByName = importsByClassName[pureTargetType] + if (importsByName != null && importsByName.isNotEmpty()) { + // Use the first matching import (consistent with original behavior) + val importByClassName = importsByName[0] + val isStatic = importByClassName.UsageName.isNotEmpty() && importByClassName.UsageName.contains(pureTargetType) + return JavaTargetType( + targetType = importByClassName.Source, + callType = if (isStatic) CallType.STATIC else CallType.CHAIN + ) } } @@ -549,13 +569,12 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : // others, may be from parent if (pureTargetType == "super" || pureTargetType == "this") { - for (imp in imports) { - if (imp.Source.endsWith(currentClzExtend)) { - return JavaTargetType( - targetType = imp.Source, - callType = CallType.SUPER - ) - } + val importsByExtend = importsByClassName[currentClzExtend] + if (importsByExtend != null && importsByExtend.isNotEmpty()) { + return JavaTargetType( + targetType = importsByExtend[0].Source, + callType = CallType.SUPER + ) } } @@ -619,15 +638,15 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : typeValue, typeKey, Modifiers = listOf(), - Annotations = this.currentAnnotations, + Annotations = this.currentAnnotations.toList(), TypeRef = typeRef ) - fields += field + fields.add(field) buildFieldCall(typeTypeText, ctx) } - this.currentAnnotations = listOf() + this.currentAnnotations = mutableListOf() } private fun buildFieldCall(typeType: String, ctx: JavaParser.FieldDeclarationContext) { @@ -699,7 +718,7 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : currentNode.Annotations += annotation } } else { - currentAnnotations += this.buildAnnotation(ctx) + currentAnnotations.add(this.buildAnnotation(ctx)) } } @@ -888,13 +907,11 @@ open class JavaFullIdentListener(fileName: String, val classes: List) : } open fun buildEnumImplements(ctx: JavaParser.EnumDeclarationContext): List { - var implements = listOf() val type = ctx.typeList() var target = this.warpTargetFullType(type.text).targetType if (target == "") { target = type.text } - implements += target - return implements + return listOf(target) } } diff --git a/chapi-ast-java/src/test/kotlin/chapi/ast/javaast/JavaIdentCallAppTest.kt b/chapi-ast-java/src/test/kotlin/chapi/ast/javaast/JavaIdentCallAppTest.kt index 9e93765d..2274541e 100644 --- a/chapi-ast-java/src/test/kotlin/chapi/ast/javaast/JavaIdentCallAppTest.kt +++ b/chapi-ast-java/src/test/kotlin/chapi/ast/javaast/JavaIdentCallAppTest.kt @@ -22,6 +22,25 @@ internal class JavaIdentCallAppTest { JavaAnalyser().identFullInfo(code, "AllInOne8.java") } + @Test + fun shouldNotCrashForJava24UnnamedVariablesAndPatterns() { + val code = """ +public class Java24Unnamed { + public void test(Object o) { + // unnamed pattern variable (Java 22+) + switch (o) { + case String _ -> { } + default -> { } + } + + // unnamed lambda parameter (Java 22+) + Runnable r = (_) -> { }; + } +} + """ + JavaAnalyser().identFullInfo(code, "Java24Unnamed.java") + } + private val helloworld = """ public class HelloWorld { public static void main(String []args) {