Skip to content

Commit d912c53

Browse files
committed
fix(ast): improve Swift parser robustness and type handling
Fix operator range, add bounds checks, and refine type parsing. Handle parenthesized optionals, improve array/dictionary detection, and fix protocol composition parsing. Use parse-tree tokens for modifiers and extract correct function names in member calls.
1 parent e4aad6e commit d912c53

File tree

4 files changed

+53
-37
lines changed

4 files changed

+53
-37
lines changed

chapi-ast-swift/src/main/antlr/Swift5Parser.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1468,7 +1468,7 @@ element_name
14681468

14691469
// Function Type
14701470
function_type
1471-
: attributes? function_type_argument_clause THROWS? arrow_operator type
1471+
: attributes? function_type_argument_clause (THROWS (LPAREN type RPAREN)?)? arrow_operator type
14721472
;
14731473

14741474
function_type_argument_clause

chapi-ast-swift/src/main/java/chapi/ast/antlr/SwiftSupport.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,7 @@ To use it in the ternary conditional (? :) operator, it must have
110110
operatorHead.set(0x3001, 0x3003 + 1);
111111

112112
// operator-head → U+3008–U+3030
113-
operatorHead.set(0x3008, 0x3020 + 1);
114-
115-
operatorHead.set(0x3030);
113+
operatorHead.set(0x3008, 0x3030 + 1);
116114

117115
// operator-character → operator-head­
118116
operatorCharacter = (BitSet) operatorHead.clone();
@@ -247,6 +245,7 @@ public static boolean isBinaryOp(TokenStream tokens) {
247245
if (stop == -1) return false;
248246

249247
int start = tokens.index();
248+
if (start == 0) return false; // Guard against index out of bounds
250249
Token currentToken = tokens.get(start);
251250
Token prevToken = tokens.get(start - 1); // includes hidden-channel tokens
252251
Token nextToken = tokens.get(stop + 1);
@@ -281,6 +280,7 @@ public static boolean isPrefixOp(TokenStream tokens) {
281280
if (stop == -1) return false;
282281

283282
int start = tokens.index();
283+
if (start == 0) return true; // At beginning, treat as prefix
284284
Token prevToken = tokens.get(start - 1); // includes hidden-channel tokens
285285
Token nextToken = tokens.get(stop + 1);
286286
boolean prevIsWS = isLeftOperatorWS(prevToken);
@@ -306,6 +306,7 @@ public static boolean isPostfixOp(TokenStream tokens) {
306306
if (stop == -1) return false;
307307

308308
int start = tokens.index();
309+
if (start == 0) return false; // At beginning, cannot be postfix
309310
Token prevToken = tokens.get(start - 1); // includes hidden-channel tokens
310311
Token nextToken = tokens.get(stop + 1);
311312
boolean prevIsWS = isLeftOperatorWS(prevToken);

chapi-ast-swift/src/main/kotlin/chapi/ast/swiftast/SwiftFullIdentListener.kt

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -161,18 +161,15 @@ class SwiftFullIdentListener(private val filePath: String) : Swift5ParserBaseLis
161161
private fun extractClassModifiers(ctx: Swift5Parser.Class_declarationContext): List<String> {
162162
val modifiers = mutableListOf<String>()
163163

164-
// Parse from context text to extract modifiers
165-
// The grammar is: attributes? (access_level_modifier? FINAL? | FINAL access_level_modifier?) CLASS
166-
val text = ctx.text
167-
val classKeywordIndex = text.indexOf("class")
168-
if (classKeywordIndex > 0) {
169-
val prefix = text.substring(0, classKeywordIndex).lowercase()
170-
if (prefix.contains("public")) modifiers.add("public")
171-
if (prefix.contains("private")) modifiers.add("private")
172-
if (prefix.contains("internal")) modifiers.add("internal")
173-
if (prefix.contains("fileprivate")) modifiers.add("fileprivate")
174-
if (prefix.contains("open")) modifiers.add("open")
175-
if (prefix.contains("final")) modifiers.add("final")
164+
// Use parse-tree tokens instead of substring matching to avoid issues
165+
// like "fileprivate" matching both "fileprivate" and "private"
166+
ctx.access_level_modifier()
167+
?.let { buildAccessLevelModifier(it) }
168+
?.takeIf { it.isNotEmpty() }
169+
?.let { modifiers.add(it) }
170+
171+
if (ctx.FINAL() != null) {
172+
modifiers.add("final")
176173
}
177174

178175
return modifiers
@@ -638,7 +635,13 @@ class SwiftFullIdentListener(private val filePath: String) : Swift5ParserBaseLis
638635
val postfixExpr = ctx.parent as? Swift5Parser.Postfix_expressionContext ?: return
639636
val primaryExpr = postfixExpr.primary_expression()
640637

641-
val functionName = primaryExpr?.unqualified_name()?.identifier()?.text ?: primaryExpr?.text ?: return
638+
// For member calls like foo.bar(), extract "bar" from explicit_member_suffix
639+
// instead of "foo" from primary_expression
640+
val lastMember = postfixExpr.explicit_member_suffix().lastOrNull()
641+
val functionName = lastMember?.identifier()?.text
642+
?: primaryExpr?.unqualified_name()?.identifier()?.text
643+
?: primaryExpr?.text
644+
?: return
642645

643646
val call = CodeCall(
644647
FunctionName = functionName,

chapi-ast-swift/src/main/kotlin/chapi/ast/swiftast/SwiftTypeRefBuilder.kt

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,33 @@ object SwiftTypeRefBuilder {
3131
val trimmed = typeText.trim()
3232

3333
// Handle optional types (suffix ? or !)
34-
if (trimmed.endsWith("?") && !trimmed.startsWith("(")) {
34+
// This correctly handles parenthesized optionals like (Int, String)? and ((Int)->Void)?
35+
if (trimmed.endsWith("?")) {
3536
val baseType = parseType(trimmed.dropLast(1))
3637
return CodeTypeRef.nullable(baseType)
3738
}
3839

39-
if (trimmed.endsWith("!") && !trimmed.startsWith("(")) {
40+
if (trimmed.endsWith("!")) {
4041
val baseType = parseType(trimmed.dropLast(1))
4142
return baseType.copy(
4243
raw = trimmed,
4344
nullable = true // Implicitly unwrapped optional is also nullable
4445
)
4546
}
4647

47-
// Handle array literal syntax [Type]
48-
if (trimmed.startsWith("[") && trimmed.endsWith("]") && !trimmed.contains(":")) {
49-
val innerType = trimmed.drop(1).dropLast(1).trim()
50-
val elementType = parseType(innerType)
51-
return CodeTypeRef.array(elementType)
52-
}
53-
54-
// Handle dictionary literal syntax [Key: Value]
55-
if (trimmed.startsWith("[") && trimmed.endsWith("]") && trimmed.contains(":")) {
48+
// Handle array literal syntax [Type] or dictionary literal syntax [Key: Value]
49+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
5650
val inner = trimmed.drop(1).dropLast(1).trim()
5751
val colonIndex = findTopLevelColon(inner)
58-
if (colonIndex > 0) {
52+
if (colonIndex >= 0) {
53+
// Dictionary type [Key: Value]
5954
val keyType = parseType(inner.substring(0, colonIndex).trim())
6055
val valueType = parseType(inner.substring(colonIndex + 1).trim())
6156
return CodeTypeRef.map(keyType, valueType)
57+
} else {
58+
// Array type [Type]
59+
val elementType = parseType(inner)
60+
return CodeTypeRef.array(elementType)
6261
}
6362
}
6463

@@ -97,13 +96,11 @@ object SwiftTypeRefBuilder {
9796
return CodeTypeRef.function(paramTypes, returnType)
9897
}
9998

100-
// Handle protocol composition Type1 & Type2
101-
if (trimmed.contains("&")) {
102-
val parts = trimmed.split("&").map { it.trim() }
103-
if (parts.size > 1) {
104-
val types = parts.map { parseType(it) }
105-
return CodeTypeRef.intersection(*types.toTypedArray())
106-
}
99+
// Handle protocol composition Type1 & Type2 (top-level only)
100+
val compositionParts = splitByTopLevelAmpersand(trimmed)
101+
if (compositionParts.size > 1) {
102+
val types = compositionParts.map { parseType(it) }
103+
return CodeTypeRef.intersection(*types.toTypedArray())
107104
}
108105

109106
// Handle opaque types: some Protocol
@@ -184,6 +181,21 @@ object SwiftTypeRefBuilder {
184181
* Split by comma at the top level (not inside brackets or generics)
185182
*/
186183
private fun splitByTopLevelComma(text: String): List<String> {
184+
return splitByTopLevelDelimiter(text, ',')
185+
}
186+
187+
/**
188+
* Split by ampersand at the top level (not inside brackets or generics)
189+
* Used for protocol composition types like "Foo<Bar & Baz> & Qux"
190+
*/
191+
private fun splitByTopLevelAmpersand(text: String): List<String> {
192+
return splitByTopLevelDelimiter(text, '&')
193+
}
194+
195+
/**
196+
* Split by a delimiter at the top level (not inside brackets or generics)
197+
*/
198+
private fun splitByTopLevelDelimiter(text: String, delimiter: Char): List<String> {
187199
val result = mutableListOf<String>()
188200
var depth = 0
189201
var start = 0
@@ -192,7 +204,7 @@ object SwiftTypeRefBuilder {
192204
when (text[i]) {
193205
'[', '(', '<' -> depth++
194206
']', ')', '>' -> depth--
195-
',' -> {
207+
delimiter -> {
196208
if (depth == 0) {
197209
result.add(text.substring(start, i).trim())
198210
start = i + 1

0 commit comments

Comments
 (0)