detached d43cf59

fix: handle nested generated adapter parameters

commit
d43cf59a6a7636228f1d591a2a62bbb4555c80da
author
brice · · 2026-04-10 12:51 UTC
parent
43acd17
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
256256 // extractParamNames parses a typed parameter list like "(argc *int32, argv ***byte)"
257257 // and returns a call-site argument list like "(argc, argv)".
258258 // 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)".
259261 func extractParamNames(params string) string {
260262 inner := strings.Trim(params, "()")
261263 if inner == "" {
262264 return "()"
263265 }
264266
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, ' ')
271287 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)
274289 } else {
275- names = append(names, part[:idx])
290+ names = append(names, seg[:idx])
276291 }
277292 }
278293 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) {
279279 t.Fatalf("port stream_playback output missing PWStreamDestroy method")
280280 }
281281 }
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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close