fix: handle nested generated adapter parameters
- commit
- d43cf59a6a7636228f1d591a2a62bbb4555c80da
- author
- brice · · 2026-04-10 12:51 UTC
- parent
- 43acd17
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +77 -9
M cmd/pipewiregen/internal/emitter/emitter.go +24 -9
| @@ -256,23 +256,38 @@ func parseSignature(sig string) (params, results string) { | |||
| 256 | 256 | // extractParamNames parses a typed parameter list like "(argc *int32, argv ***byte)" | |
| 257 | 257 | // and returns a call-site argument list like "(argc, argv)". | |
| 258 | 258 | // It strips all type information, keeping only the parameter names. | |
| 259 | + | // It uses depth-aware comma splitting to correctly handle nested function-type | |
| 260 | + | // parameters like "callback func(int, int)". | |
| 259 | 261 | func extractParamNames(params string) string { | |
| 260 | 262 | inner := strings.Trim(params, "()") | |
| 261 | 263 | if inner == "" { | |
| 262 | 264 | return "()" | |
| 263 | 265 | } | |
| 264 | 266 | ||
| 265 | - | parts := strings.Split(inner, ",") | |
| 266 | - | names := make([]string, 0, len(parts)) | |
| 267 | - | for _, part := range parts { | |
| 268 | - | part = strings.TrimSpace(part) | |
| 269 | - | // Each part is "name type" or "name type1/type2" — the first space separates name from type. | |
| 270 | - | idx := strings.IndexByte(part, ' ') | |
| 267 | + | // Split on commas at depth 0 only (respecting nested parens) | |
| 268 | + | var segments []string | |
| 269 | + | depth := 0 | |
| 270 | + | start := 0 | |
| 271 | + | for i, ch := range inner { | |
| 272 | + | if ch == '(' { | |
| 273 | + | depth++ | |
| 274 | + | } else if ch == ')' { | |
| 275 | + | depth-- | |
| 276 | + | } else if ch == ',' && depth == 0 { | |
| 277 | + | segments = append(segments, strings.TrimSpace(inner[start:i])) | |
| 278 | + | start = i + 1 | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | segments = append(segments, strings.TrimSpace(inner[start:])) | |
| 282 | + | ||
| 283 | + | names := make([]string, 0, len(segments)) | |
| 284 | + | for _, seg := range segments { | |
| 285 | + | // Each segment is "name type" — the first space at depth 0 separates name from type. | |
| 286 | + | idx := strings.IndexByte(seg, ' ') | |
| 271 | 287 | if idx < 0 { | |
| 272 | - | // No space means just a name with no type (shouldn't happen in valid Go, but handle it) | |
| 273 | - | names = append(names, part) | |
| 288 | + | names = append(names, seg) | |
| 274 | 289 | } else { | |
| 275 | - | names = append(names, part[:idx]) | |
| 290 | + | names = append(names, seg[:idx]) | |
| 276 | 291 | } | |
| 277 | 292 | } | |
| 278 | 293 | return "(" + strings.Join(names, ", ") + ")" | |
| @@ -256,23 +256,38 @@ func parseSignature(sig string) (params, results string) { | |||||
| 256 | // extractParamNames parses a typed parameter list like "(argc *int32, argv ***byte)" | 256 | // extractParamNames parses a typed parameter list like "(argc *int32, argv ***byte)" | ||
| 257 | // and returns a call-site argument list like "(argc, argv)". | 257 | // and returns a call-site argument list like "(argc, argv)". | ||
| 258 | // It strips all type information, keeping only the parameter names. | 258 | // It strips all type information, keeping only the parameter names. | ||
| 259 | + | // It uses depth-aware comma splitting to correctly handle nested function-type | |||
| 260 | + | // parameters like "callback func(int, int)". | |||
| 259 | func extractParamNames(params string) string { | 261 | func extractParamNames(params string) string { | ||
| 260 | inner := strings.Trim(params, "()") | 262 | inner := strings.Trim(params, "()") | ||
| 261 | if inner == "" { | 263 | if inner == "" { | ||
| 262 | return "()" | 264 | return "()" | ||
| 263 | } | 265 | } | ||
| 264 | 266 | ||||
| 265 | - | parts := strings.Split(inner, ",") | 267 | + | // Split on commas at depth 0 only (respecting nested parens) |
| 266 | - | names := make([]string, 0, len(parts)) | 268 | + | var segments []string |
| 267 | - | for _, part := range parts { | 269 | + | depth := 0 |
| 268 | - | part = strings.TrimSpace(part) | 270 | + | start := 0 |
| 269 | - | // Each part is "name type" or "name type1/type2" — the first space separates name from type. | 271 | + | for i, ch := range inner { |
| 270 | - | idx := strings.IndexByte(part, ' ') | 272 | + | if ch == '(' { |
| 273 | + | depth++ | |||
| 274 | + | } else if ch == ')' { | |||
| 275 | + | depth-- | |||
| 276 | + | } else if ch == ',' && depth == 0 { | |||
| 277 | + | segments = append(segments, strings.TrimSpace(inner[start:i])) | |||
| 278 | + | start = i + 1 | |||
| 279 | + | } | |||
| 280 | + | } | |||
| 281 | + | segments = append(segments, strings.TrimSpace(inner[start:])) | |||
| 282 | + | ||||
| 283 | + | names := make([]string, 0, len(segments)) | |||
| 284 | + | for _, seg := range segments { | |||
| 285 | + | // Each segment is "name type" — the first space at depth 0 separates name from type. | |||
| 286 | + | idx := strings.IndexByte(seg, ' ') | |||
| 271 | if idx < 0 { | 287 | if idx < 0 { | ||
| 272 | - | // No space means just a name with no type (shouldn't happen in valid Go, but handle it) | 288 | + | names = append(names, seg) |
| 273 | - | names = append(names, part) | |||
| 274 | } else { | 289 | } else { | ||
| 275 | - | names = append(names, part[:idx]) | 290 | + | names = append(names, seg[:idx]) |
| 276 | } | 291 | } | ||
| 277 | } | 292 | } | ||
| 278 | return "(" + strings.Join(names, ", ") + ")" | 293 | return "(" + strings.Join(names, ", ") + ")" | ||
M cmd/pipewiregen/internal/emitter/emitter_test.go +53 -0
| @@ -279,3 +279,56 @@ func assertCleanupBindings(t *testing.T, paths map[string][]byte) { | |||
| 279 | 279 | t.Fatalf("port stream_playback output missing PWStreamDestroy method") | |
| 280 | 280 | } | |
| 281 | 281 | } | |
| 282 | + | ||
| 283 | + | func TestExtractParamNames(t *testing.T) { | |
| 284 | + | tests := []struct { | |
| 285 | + | name string | |
| 286 | + | input string | |
| 287 | + | expect string | |
| 288 | + | }{ | |
| 289 | + | { | |
| 290 | + | name: "empty parameter list", | |
| 291 | + | input: "()", | |
| 292 | + | expect: "()", | |
| 293 | + | }, | |
| 294 | + | { | |
| 295 | + | name: "single parameter", | |
| 296 | + | input: "(stream unsafe.Pointer)", | |
| 297 | + | expect: "(stream)", | |
| 298 | + | }, | |
| 299 | + | { | |
| 300 | + | name: "multiple parameters", | |
| 301 | + | input: "(stream unsafe.Pointer, direction int32, id uint32)", | |
| 302 | + | expect: "(stream, direction, id)", | |
| 303 | + | }, | |
| 304 | + | { | |
| 305 | + | name: "nested function-type parameter with inner commas", | |
| 306 | + | input: "(callback func(int, int), count int32)", | |
| 307 | + | expect: "(callback, count)", | |
| 308 | + | }, | |
| 309 | + | { | |
| 310 | + | name: "nested function-type with multiple inner params", | |
| 311 | + | input: "(handler func(a int32, b int32, c int32), ctx unsafe.Pointer)", | |
| 312 | + | expect: "(handler, ctx)", | |
| 313 | + | }, | |
| 314 | + | { | |
| 315 | + | name: "pointer type with multiple stars", | |
| 316 | + | input: "(argv ***byte)", | |
| 317 | + | expect: "(argv)", | |
| 318 | + | }, | |
| 319 | + | { | |
| 320 | + | name: "no parameters bare parens trimmed to empty", | |
| 321 | + | input: "()", | |
| 322 | + | expect: "()", | |
| 323 | + | }, | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | for _, tt := range tests { | |
| 327 | + | t.Run(tt.name, func(t *testing.T) { | |
| 328 | + | got := extractParamNames(tt.input) | |
| 329 | + | if got != tt.expect { | |
| 330 | + | t.Errorf("extractParamNames(%q) = %q, want %q", tt.input, got, tt.expect) | |
| 331 | + | } | |
| 332 | + | }) | |
| 333 | + | } | |
| 334 | + | } | |
| @@ -279,3 +279,56 @@ func assertCleanupBindings(t *testing.T, paths map[string][]byte) { | |||||
| 279 | t.Fatalf("port stream_playback output missing PWStreamDestroy method") | 279 | t.Fatalf("port stream_playback output missing PWStreamDestroy method") | ||
| 280 | } | 280 | } | ||
| 281 | } | 281 | } | ||
| 282 | + | ||||
| 283 | + | func TestExtractParamNames(t *testing.T) { | |||
| 284 | + | tests := []struct { | |||
| 285 | + | name string | |||
| 286 | + | input string | |||
| 287 | + | expect string | |||
| 288 | + | }{ | |||
| 289 | + | { | |||
| 290 | + | name: "empty parameter list", | |||
| 291 | + | input: "()", | |||
| 292 | + | expect: "()", | |||
| 293 | + | }, | |||
| 294 | + | { | |||
| 295 | + | name: "single parameter", | |||
| 296 | + | input: "(stream unsafe.Pointer)", | |||
| 297 | + | expect: "(stream)", | |||
| 298 | + | }, | |||
| 299 | + | { | |||
| 300 | + | name: "multiple parameters", | |||
| 301 | + | input: "(stream unsafe.Pointer, direction int32, id uint32)", | |||
| 302 | + | expect: "(stream, direction, id)", | |||
| 303 | + | }, | |||
| 304 | + | { | |||
| 305 | + | name: "nested function-type parameter with inner commas", | |||
| 306 | + | input: "(callback func(int, int), count int32)", | |||
| 307 | + | expect: "(callback, count)", | |||
| 308 | + | }, | |||
| 309 | + | { | |||
| 310 | + | name: "nested function-type with multiple inner params", | |||
| 311 | + | input: "(handler func(a int32, b int32, c int32), ctx unsafe.Pointer)", | |||
| 312 | + | expect: "(handler, ctx)", | |||
| 313 | + | }, | |||
| 314 | + | { | |||
| 315 | + | name: "pointer type with multiple stars", | |||
| 316 | + | input: "(argv ***byte)", | |||
| 317 | + | expect: "(argv)", | |||
| 318 | + | }, | |||
| 319 | + | { | |||
| 320 | + | name: "no parameters bare parens trimmed to empty", | |||
| 321 | + | input: "()", | |||
| 322 | + | expect: "()", | |||
| 323 | + | }, | |||
| 324 | + | } | |||
| 325 | + | ||||
| 326 | + | for _, tt := range tests { | |||
| 327 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 328 | + | got := extractParamNames(tt.input) | |||
| 329 | + | if got != tt.expect { | |||
| 330 | + | t.Errorf("extractParamNames(%q) = %q, want %q", tt.input, got, tt.expect) | |||
| 331 | + | } | |||
| 332 | + | }) | |||
| 333 | + | } | |||
| 334 | + | } | |||